Use when writing, drafting, reviewing, or editing unslop spec files (*.spec.md). Activates for spec creation, takeover spec drafting, and spec editing guidance.
From unslopnpx claudepluginhub lewdwig-v/unslop --plugin unslopThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides slash command development for Claude Code: structure, YAML frontmatter, dynamic arguments, bash execution, user interactions, organization, and best practices.
The two spec layers have different stances. Abstract Specs describe intent — what a file must do, what constraints it must satisfy, what behavior it must exhibit. Concrete Specs describe strategy — what algorithm, pattern, or type structure delivers those guarantees, in language-agnostic terms. Neither layer contains target-language code; code is disposable, specs are the source of truth.
Unslop uses a compiler-inspired two-layer spec architecture:
| Layer | File | Describes | Analogy |
|---|---|---|---|
| Abstract Spec | *.spec.md | Observable behavior, constraints, contracts | High-Level IR (the "What" and "Why") |
| Concrete Spec | *.impl.md | Algorithm, patterns, type structure | Mid-Level IR (the "How") |
This skill primarily governs Abstract Specs. The Pseudocode Discipline section below also applies to Concrete Specs. For broader Concrete Spec writing guidance (Strategy, Lowering Notes, Type Sketch), see the unslop/concrete-spec skill.
The boundary between the two layers follows a simple rule: if it's observable from outside the module, it belongs in the Abstract Spec. If it's an internal strategy choice, it belongs in the Concrete Spec (or nowhere — most strategy choices are ephemeral).
Examples of the boundary:
| Abstract Spec (*.spec.md) | Concrete Spec (*.impl.md) |
|---|---|
| "Retries with exponential backoff, max 5 attempts" | "Full Jitter algorithm: sleep = cap * random()" |
| "Results are sorted by relevance score" | "Uses a min-heap for top-K selection, O(n log k)" |
| "Deduplicates within a 5-minute window" | "Sliding window with a hash set, pruned on insert" |
| "Responses cached for 5 minutes" | "LRU cache with TTL, max 1000 entries" |
The Abstract Spec says what guarantee the caller gets. The Concrete Spec says what algorithm delivers that guarantee. The generated code says how that algorithm is expressed in the target language.
Abstract specs are written in terms of observable behavior, contracts, and constraints — not data structures, algorithms, or control flow. (Concrete specs intentionally use algorithm and pattern vocabulary; the restrictions below apply to *.spec.md files.)
| Good (intent) | Bad (implementation) |
|---|---|
| Messages are stored in SQLite with a monotonic sequence ID | Use INSERT OR REPLACE with a rowid alias column |
| Retries use exponential backoff with jitter, max 5 attempts | sleep(2**attempt + random.uniform(0,1)) |
| Validation rejects inputs over 1MB | if len(data) > 1_048_576: raise ValueError |
| HTTP responses are cached for 5 minutes | Use a dict with time.time() keys and prune entries older than 300 |
The right column belongs in code, not specs. If you find yourself writing it in a spec, delete it and replace it with the observable guarantee it was trying to achieve.
Nail down the details that constrain the implementation space and that callers or operators will depend on:
Leave these to the implementation:
If your spec reads like commented-out code, it's over-specified. If it reads like a product brief with no constraints, it's under-specified.
Over-specified specs break regeneration — every implementation detail becomes load-bearing, so the model can't choose a better approach. Under-specified specs break validation — there's nothing to check the generated code against.
Aim for the middle: the spec should be testable without being prescriptive.
When a spec intentionally leaves a decision open, mark it explicitly. This prevents the ambiguity linter from blocking generation on deliberate flexibility.
Two mechanisms:
Inline marker — add [open] on the same line as the flexible statement:
Caching strategy uses an appropriate eviction policy [open]
Dedicated section — list broader open questions with rationale:
## Open Questions
- Whether to use LRU or LFU eviction — will benchmark after first deployment
- Error retry backoff curve — depends on upstream SLA negotiations
Use Open Questions for decisions that:
Do NOT use Open Questions to dodge spec writing. If a constraint is knowable now, specify it. The ambiguity linter will flag abusive use of [open] on constraints that clearly need pinning down.
These are not required, but they cover the ground most specs need:
Use all of them, some of them, or none — structure the spec to match the complexity of the file it describes. A 20-line utility doesn't need five headings.
Specs are named <file>.spec.md and placed alongside the managed file:
src/retry.py -> src/retry.py.spec.mdsrc/api/handler.ts -> src/api/handler.ts.spec.mdDirectory modules (e.g., Rust mod.rs, Python __init__.py): Use the directory name for the spec, not the file name. Add a managed-file field to the frontmatter so the resolver knows which file the spec manages:
src/dispatch/mod.rs -> src/dispatch/dispatch.spec.md with managed-file: src/dispatch/mod.rssrc/auth/__init__.py -> src/auth/auth.spec.md with managed-file: src/auth/__init__.py---
managed-file: src/dispatch/mod.rs
depends-on:
- src/dispatch/omnibar.spec.md
intent: >
Pure state machine core. dispatch(&mut AppState, Action) -> Vec<Effect>
processes every user action and returns effects for I/O.
intent-approved: 2026-03-26T14:32:00Z
intent-hash: a1b2c3d4e5f6
---
# dispatch spec
The managed-file field overrides the default filename-stripping heuristic. When absent, the resolver falls back to stripping .spec.md from the spec filename (the legacy behavior). The same convention applies to concrete specs.
The intent field records the human-approved summary of what the spec governs -- the compressed, reviewable statement of purpose that the user confirmed during the intent lock.
| Field | Description |
|---|---|
intent | The approved intent statement. Single-line or YAML folded scalar (>). This is the reviewable surface -- "what's this module for?" in 2-3 sentences. |
intent-approved | ISO 8601 timestamp of when the user approved the intent. |
intent-hash | 12-char hex hash of the intent text. Computed by the tooling. If someone edits the intent without re-approving, the hash mismatch is a hard error. |
Lifecycle:
/unslop:takeover Step 1b (Intent Lock) after user approval/unslop:sync and /unslop:generate -- if the spec change alters the module's stated intent, the Architect flags it for re-lock--- fences during regeneration. Frontmatter is a protected region.Tamper detection: The tooling computes intent-hash from the intent text. If the hash doesn't match (someone edited the intent without re-running the intent lock), the pipeline stops with a hard error before any semantic analysis runs.
The non-goals field records explicit exclusions -- things the spec deliberately does NOT cover. Non-goals are generated during /unslop:elicit and ratified by the user.
---
non-goals:
- Circuit breaker or load shedding (handled by upstream proxy)
- Request deduplication (caller's responsibility)
- Retry of non-idempotent methods (POST, PATCH)
---
Each entry is a plain text statement. Non-goals are:
/unslop:generate surfaces tension if generated code implements a non-goal. /unslop:weed flags code-drifted findings where code implements something in non-goals.Non-goals complement the spec's positive constraints. Constraints say "the code MUST do X." Non-goals say "the code MUST NOT do Y, and here's why."
When an upstream spec changes via /unslop:change, downstream specs that depend on it are flagged with needs-review to prevent silent intent corruption.
| Field | Description |
|---|---|
needs-review | Intent-hash of the upstream spec at the time of flagging. Identifies which change triggered the review obligation. |
review-acknowledged | Intent-hash of the upstream change that was consciously dismissed. Proves the user reviewed and decided the change doesn't affect this spec. |
Lifecycle:
needs-review is written by /unslop:change after a spec mutation, for each downstream dependent the user did not immediately review.needs-review causes a soft-block in /unslop:generate and /unslop:sync -- the user must acknowledge or address the flag before code generation proceeds.review-acknowledged is written when the user chooses to acknowledge and proceed past the soft-block./unslop:elicit amendment pass (the new intent-hash proves the spec was reviewed).needs-review overwrites both fields.Why the hash? A naked boolean flag would tell you "something upstream changed" but not what. The hash lets you diff against the specific upstream change, and it lets the system distinguish "flagged and ignored" from "flagged and consciously dismissed."
The uncertain field records items flagged by /unslop:distill as potentially accidental behaviour rather than deliberate design. Each entry gives /unslop:elicit a structured question to ask the user.
---
uncertain:
- title: "Unbounded retry loop"
observation: "Code retries indefinitely with no cap. No test covers this path."
question: "Is the missing cap intentional or an oversight?"
---
Each entry has three required fields: title, observation, question.
/unslop:distill during spec inference./unslop:elicit in distillation review mode.The discovered field records correctness requirements found by the Archaeologist during Generate Stage 0 (spec projection) that the abstract spec didn't anticipate.
---
discovered:
- title: "Implicit ordering constraint"
observation: "Retry depends on token refresh before each attempt."
question: "Should the spec require token refresh before retry?"
---
Each entry has three required fields: title, observation, question. Same structure as uncertain:, but different provenance.
uncertain: | discovered: | |
|---|---|---|
| Written by | Archaeologist in distill mode | Archaeologist in generate mode |
| Question | "Was this accidental?" | "Does your intent require this?" |
| Cleared by | Elicit distillation review | Generate discovery gate (Stage 0b) |
| Persistence | May remain as warnings | Must be resolved before generate proceeds |
uncertain:, discovered constraints are transient -- they must be resolved before generation proceeds because they may affect correctness of the generated code.The distilled-from field records which source file(s) a spec was inferred from and their content hash at distillation time.
---
distilled-from:
- path: src/retry.py
hash: a3f8c2e9b7d1
---
Each entry has two required fields: path and hash.
/unslop:distill.distilled-from: and intent-approved: <timestamp> means "machine-inferred, then human-ratified." Clearing the provenance would destroy the audit trail.distilled-from hash, the spec may be out of date relative to the code it was inferred from.pending classification: A spec with distilled-from: and missing managed files is classified as structural (not pending), because the provenance indicates the spec was inferred from existing code that has since disappeared.The absorbed-from field records which file specs were merged to produce this spec via /unslop:absorb.
---
absorbed-from:
- path: src/retry.py.spec.md
hash: a3f8c2e9b7d1
- path: src/backoff.py.spec.md
hash: 7e2b4f1c8a93
---
Each entry has two required fields: path and hash.
/unslop:absorb.provenance-history: when the merged spec receives its first intent-approved timestamp after the absorb.pending classification: A spec with absorbed-from: and missing managed files is classified as structural (not pending), because the provenance indicates an in-progress granularity change.The exuded-from field records which unit spec was partitioned to produce this spec via /unslop:exude.
---
exuded-from:
- path: src/network.unit.spec.md
hash: b4c7d2e1f8a3
---
Each entry has two required fields: path and hash. Symmetric with absorbed-from: -- a spec may be exuded from multiple unit specs across composition cycles.
/unslop:exude.provenance-history: on first intent-approved cycle after the exude.pending classification: Same as absorbed-from:.The provenance-history field is an append-only audit log recording structural lineage through absorb/exude cycles.
---
provenance-history:
- type: absorbed-from
path: src/retry.py.spec.md
hash: a3f8c2e9b7d1
timestamp: 2026-03-15T14:30:00Z
- type: exuded-from
path: src/network.unit.spec.md
hash: b4c7d2e1f8a3
timestamp: 2026-03-20T09:15:00Z
---
Each entry has four required fields: type, path, hash, timestamp.
provenance-history: before analysis. It is consumed only by display (status) and audit tooling.absorbed-from: and exuded-from: entries are moved here when the spec is ratified.A spec is in the pending freshness state when it describes intent with no current implementation -- not because something went wrong, but because generate hasn't run yet.
pending (neutral, not a warning)distilled-from:, absorbed-from:, exuded-from:)provenance-history: does NOT count as active provenance.intent-approved, needs-review, uncertain: -- pending is about implementation existence, not spec quality.stale or drifted. Never triggers weed.The rejected field records design decisions that were explicitly considered and dismissed, with the reasoning that led to the rejection.
---
rejected:
- title: "Database-backed storage"
rationale: "Zero runtime dependencies required. SQLite adds a binary dependency and complicates deployment to Lambda."
- title: "Global retry counter"
rationale: "Per-request isolation is a hard requirement. A shared counter creates contention under concurrent load."
---
Each entry has two required fields: title and rationale.
/unslop:elicit when the user explicitly rejects a proposed approach with a reason. The Architect prompts once for a rationale; if the user declines, no entry is recorded ("no rationale, no record")./unslop:elicit in amendment mode -- the Architect reads rejected: before proposing changes to avoid re-proposing rejected approaches./unslop:generate Stage 0 -- the Archaeologist reads rejected: and, if its preferred strategy aligns with a rejected entry, surfaces a discovered: item for user decision rather than silently proceeding.Distinction from non-goals:: Non-goals are intent assertions ("we are not doing X"). Rejected alternatives are reasoning records ("we considered X and decided against it because Y"). The model needs both -- non-goals to know what's out of scope, rejected alternatives to know why so it doesn't argue back.
The spec changelog records intent mutations in two linked layers: a structured frontmatter envelope and a narrative body section.
spec-changelog: Frontmatter---
spec-changelog:
- hash: abc123def456
timestamp: 2026-03-27T14:30:00Z
operation: elicit-amend
prior-hash: 9f8e7d6c5b4a
- hash: 7a8b9c0d1e2f
timestamp: 2026-03-27T10:15:00Z
operation: absorb
prior-hash: null
---
Each entry has four required fields: hash (intent-hash after change), timestamp (ISO 8601), operation (what produced the delta), prior-hash (intent-hash before change, null for first entry).
Operation vocabulary: elicit-create, elicit-amend, elicit-distill-review, distill, absorb, exude, change-tactical, change-pending.
spec-changelog: before analysis. Consumed only by display (status) and audit tooling.## Changelog SectionAlways the last section in the spec body. Reverse chronological (most recent first). Each entry keyed by first 6 characters of intent-hash:
## Changelog
### abc123 -- 2026-03-27
Narrowed retry scope after discovering the connection pool handles its own
backoff. Considered making retry configurable per-caller but rejected it --
YAGNI.
### 7a8b9c -- 2026-03-27
Initial spec created via absorb from retry.py.spec.md and backoff.py.spec.md.
Written by the agent that produced the change, at the moment of mutation while reasoning is still in context.
The constitutional-overrides field records explicit, audited overrides of project principles.
---
constitutional-overrides:
- principle: "All error handling must use typed Result types"
rationale: "Legacy API requires exception-based error handling for backward compatibility"
timestamp: 2026-03-27T14:30:00Z
---
Each entry has three required fields: principle, rationale, timestamp.
/unslop:elicit when the user passes --force-constitutional to override a constitutional violation during ratification. Rationale is mandatory -- empty rationale is rejected./unslop:weed for stale override detection (principle removed or changed since override was recorded).Domain skills are project-local or user-local patterns that augment the generation pipeline. They live in .unslop/skills/<name>/SKILL.md (project-local) or ~/.config/unslop/skills/<name>/SKILL.md (user-local) and use YAML frontmatter to declare their scope and enforcement level.
| Field | Required | Values | Description |
|---|---|---|---|
name | Yes | String | Skill identifier. Must match the directory name (.unslop/skills/<name>/SKILL.md). |
description | Yes | String | One-line description of the pattern or convention. |
enforcement | No (default: advisory) | advisory, constitutional | Advisory skills describe preferred patterns. Constitutional skills describe invariants checked by the Saboteur alongside principles.md. |
applies-to | No (default: all files) | List of glob patterns | Limits the skill to files matching the globs. Empty list or absent field means the skill applies to all files. |
crystallized-from | No | List of {spec, pattern} objects | Provenance -- which specs exhibited the pattern and what expression was matched. Written by /unslop:crystallize. |
---
name: typed-error-handling
description: All error handling uses typed Result patterns
enforcement: advisory
applies-to:
- "src/**/*.py"
crystallized-from:
- spec: src/retry.py.spec.md
pattern: "Result<Response, ConnectionError>"
- spec: src/pool.py.spec.md
pattern: "Result<Connection, PoolError>"
---
discovered: item is surfaced. The Saboteur does not check advisory skills.principles.md). Violations produce the same finding structure and soft-block ratification.enforcement: constitutional are silently downgraded to advisory. Constitutional enforcement requires project-local skills (version-controlled, code-reviewed).When a managed file imports from or relies on another managed file, declare the dependency in YAML frontmatter:
---
depends-on:
- src/auth/tokens.py.spec.md
- src/auth/errors.py.spec.md
---
# handler.py spec
...
Declare depends-on when:
Do NOT declare dependencies on:
Paths are relative to the project root. Only list direct dependencies — prunejuice resolves transitive dependencies automatically.
When a concrete spec (*.impl.md) needs to track symbol-level blockers -- constraints the abstract spec wants to express but the implementation can't fulfill yet -- use blocked-by in the concrete spec frontmatter:
---
source-spec: src/roots.rs.spec.md
target-language: Rust
ephemeral: false
blocked-by:
- symbol: "binding::vm_impl::RustVM::VMScanning"
reason: "unconditionally aliases RustScanning -- needs cfg-gate"
resolution: "cfg-gate VMScanning alias in binding/vm_impl.rs takeover"
affects: "Scanning<RustVM> impl"
---
All four fields (symbol, reason, resolution, affects) are required. blocked-by is only meaningful on permanent concrete specs (ephemeral: false).
Unlike depends-on (file-level, passive), blocked-by is symbol-level and names a specific resolution action. It's a directed action item that can be removed once the upstream change happens.
For tightly coupled files that form a logical unit (a Python module, a Rust crate), you can write a single spec that describes the entire unit.
Unit specs are named <directory-name>.unit.spec.md and placed inside the directory (e.g., src/auth/auth.unit.spec.md).
A unit spec MUST include a ## Files section listing each output file and its responsibility:
# auth module spec
## Files
- `__init__.py` — public API re-exports
- `tokens.py` — JWT token creation and verification
- `middleware.py` — request authentication middleware
- `errors.py` — authentication error types
## Behavior
...
Use unit specs when:
Use per-file specs when:
Pseudocode appears in Concrete Specs (*.impl.md) inside ```pseudocode fenced blocks. It is the Middle-End IR — a human-readable blueprint for logic that bridges high-level intent and machine execution. These constraints ensure pseudocode remains a high-fidelity lowering target that the Builder can reliably "compile."
These rules ensure unambiguous parsing by both humans and the Builder:
One statement per line. Each line represents a single logical action. Multi-statement lines obscure control flow.
Capitalized keywords for flow control. Use a consistent set:
IF, ELSE IF, ELSE, WHILE, FOR, REPEAT UNTILSET, RETURN, RAISE, CALL, EMITTRY, CATCHFUNCTION ... END FUNCTION, BEGIN ... END for non-function blocksIndentation-based hierarchy. Mandatory indentation (2 or 4 spaces, consistent within a block) to show scope and nesting. No braces, no end keywords for control flow (scope is implicit from indentation, like Python but without the colon).
Operator discipline is context-sensitive. The linter enforces different rules depending on the statement type:
SET, FOR, INCREMENT, DECREMENT): MUST use ← (or := as fallback). Never bare =. These statements initialize or mutate state — FOR i ← 0 TO 9, not FOR i = 0 TO 9.IF, ELSE IF, WHILE, UNTIL, WHEN, ASSERT): bare = is allowed as equality comparison. UNTIL status = DONE is correct — it's a boolean test, not an assignment.= is flagged as a violation (use SET ... ← to make intent explicit).Descriptive names, not abbreviations. delay, attempts, upper_bound — not d, a, ub. Named constants for magic numbers: MAX_RETRY_ATTEMPTS not 5.
Pseudocode must be detailed enough to be unambiguous but abstract enough to stay portable:
Elide:
## Type Sketch)Include:
delay ← random_uniform(0, upper_bound))// O(n log n), // amortized O(1)The pseudocode must remain implementation-independent:
auth_lib.verify(token), write VERIFY token_signature AGAINST public_key. Instead of random.uniform(0, n), write random_uniform(0, n) — a mathematical operation, not a library invocation.def, func, fn, let, var, const, := (Go-style), -> (Rust/Haskell), lambda, =>. Use the capitalized keywords above.delay ← MIN(base × 2^attempt, cap) is clearer than "set the delay to the smaller of the exponential value and the cap."APPEND item TO collection, REMOVE item FROM collection, LOOKUP key IN map — not language-specific method syntax.FUNCTION retry(operation, config)
SET last_error ← null
FOR attempt ← 0 TO config.max_retries - 1
TRY
SET result ← CALL operation()
RETURN result
CATCH error
SET last_error ← error
IF attempt < config.max_retries - 1
SET upper_bound ← MIN(config.base_delay × 2^attempt, config.max_delay)
SET delay ← random_uniform(0, upper_bound) // Full Jitter
WAIT delay
RAISE MaxRetriesExceeded(config.max_retries, last_error)
END FUNCTION
| Violation | Example | Fix |
|---|---|---|
| Language-specific keyword | def retry(...) | FUNCTION retry(...) |
| Library call | time.sleep(delay) | WAIT delay |
| Bare assignment | delay = x | SET delay ← x |
FOR with bare = | FOR i = 0 TO 9 | FOR i ← 0 TO 9 |
| Abbreviated names | d, cfg, e | delay, config, error |
| Missing edge case | No error branch | Add CATCH / IF error |
| Magic number | if attempts > 5 | IF attempts > MAX_RETRY_ATTEMPTS |
| Multi-statement line | x = 1; y = 2 | Two separate lines |
Use this when creating a spec for a file that has no existing spec. Fill in what is known; leave sections as stubs rather than omitting them.
# [filename] spec
## Purpose
[What this file does and why it exists]
## Behavior
[What it should do — the observable contract]
## Constraints
[Bounds, limits, invariants, error conditions]
## Dependencies
[External services, libraries, or other managed files it relies on]
## Error Handling
[How errors are surfaced, what fails visibly vs silently, recovery behavior]
## Open Questions
[Decisions intentionally deferred — remove this section if none]