aiwf-check
Use when the user wants to validate the planning tree or asks why `aiwf check` reported a finding. Explains each finding code and the typical fix.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when the user wants to validate the planning tree or asks why `aiwf check` reported a finding. Explains each finding code and the typical fix.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Closes an aiwf epic — verifies all milestones done, scaffolds a wrap artefact, harvests ADR candidates, runs scoped doc-lint, merges the epic branch into mainline with a trailered merge commit, promotes the epic to done. Use when the user says "wrap E-NN" or "close the auth epic" and every milestone in the epic is wrapped. Commit and push require explicit human approval.
Closes an aiwf milestone — verifies all ACs met, runs scoped doc-lint, finalizes the milestone spec's wrap-side sections, promotes status to done, prepares the wrap commit. Use when the user says "wrap M-NNNN" or "finish the cache milestone" and the readiness check per `aiwfx-start-milestone` has passed. Commit and push require explicit human approval.
The aiwf per-repo code-health ritual — the whole-codebase companion to wf-review-code's per-diff gate. A stack-agnostic field guide of code-health principles: module boundaries, contracts, data discipline, tests that pin behavior, errors/logs/audit, reasoning aids, operational properties. Use when designing a new module, planning a refactor, reviewing a non-trivial diff, writing a spec that introduces new boundaries, or scoring an inherited codebase Strong/Weak/Missing with file:line evidence. These are advisory forces, not rules — consult them, don't enforce them; the project's own conventions win.
Sets up and begins an aiwf milestone — preflight checks, branch setup, status promotion to in_progress, then iterative TDD via wf-tdd-cycle. Use when the user says "start milestone M-NNNN" or "implement M-NNNN" and a draft milestone spec exists. Commits and pushes require explicit human approval.
Use when terminal-status entities have accumulated in the active tree and the operator wants to sweep them into per-kind `archive/` subdirs, or when `aiwf check` reports `archive-sweep-pending`. Explains dry-run vs `--apply`, the no-reverse rule, the `archive.sweep_threshold` knob, merge edge cases, and the per-kind storage layout.
Use when the user wants to edit (rewrite or replace) the markdown body of an existing entity — goal/scope/context prose, AC body sections inside a milestone, ADR rationale, gap problem statement, etc. Runs `aiwf edit-body` so the change rides through a verb route with proper trailers, instead of a plain `git commit` that triggers a `provenance-untrailered-entity-commit` warning.
| name | aiwf-check |
| description | Use when the user wants to validate the planning tree or asks why `aiwf check` reported a finding. Explains each finding code and the typical fix. |
The aiwf check verb is a pure function from the working tree to a list of findings. It runs as a pre-push git hook; that hook is the chokepoint that turns the framework's guarantees into mechanical enforcement.
aiwf check # human-readable text
aiwf check --format=json # JSON envelope for tooling
aiwf check --format=json --pretty
aiwf check --since <ref> # explicit base for the provenance untrailered-entity audit
aiwf check --shape-only # tree-discipline rule only; used by the pre-commit hook
| Hook | What it runs | What it catches |
|---|---|---|
pre-commit | aiwf check --shape-only | Stray files under work/ (unexpected-tree-file). Fast LLM-loop signal — the bad commit never lands. Agent-agnostic (any client running git commit triggers it). Blocks only when aiwf.yaml: tree.strict: true; otherwise warns and proceeds. |
pre-push | Full aiwf check | Everything else: frontmatter shape, refs resolve, FSM, provenance, contract config. Audit chokepoint where push-blocking is appropriate; tolerant of WIP between commits. |
--shape-only skips the trunk read, provenance walk, and contract validation, so the pre-commit hook stays fast and never blocks on transient WIP findings. Use it directly only when you want the fast subset; for normal validation, plain aiwf check is the right invocation.
--since <ref> — provenance audit scopeThe untrailered-entity audit (provenance-untrailered-entity-commit) walks a single revision range. The default is @{u}..HEAD, so commits already pushed to the upstream are someone else's responsibility to repair.
When the branch has no upstream (a fresh feature branch, or a branch whose remote was deleted), the default range is undefined and the audit is skipped with one provenance-untrailered-scope-undefined warning, rather than scanning all of HEAD and flooding the operator with commits already merged in from trunk. To opt back in, either configure an upstream (git push -u origin <branch>) or pass --since <ref>:
aiwf check --since main # walk main..HEAD on the local branch
aiwf check --since HEAD~50 # walk the last 50 commits
| Code | Meaning | Typical fix |
|---|---|---|
ids-unique | Two entities share an id. Almost always from a parallel-branch merge. | aiwf reallocate <path> on the loser. |
ids-unique/trunk-collision | An id allocated on this branch is also allocated on the configured trunk ref (default refs/remotes/origin/main) at a different path — i.e. two different entities now share it across branches. The cross-tree variant of ids-unique. | aiwf reallocate <path> on whichever side hasn't reached trunk yet. The pre-push hook surfaces this before the colliding push lands. |
frontmatter-shape | Required field missing or malformed. | Add the field; check the kind's id format. |
status-valid | Status is not in the kind's allowed set. | Pick a status from the kind's set (see aiwf-promote). |
priority-valid | The priority field's value is outside the closed set (urgent, high, medium, low). | Correct the value by hand and re-run aiwf check. |
refs-resolve/unresolved | A reference points at an id that does not exist. | Either the target was never created, or the id is mistyped. |
refs-resolve/unresolved-milestone | The composite-id reference's milestone half (M-NNN/AC-N) names a milestone that does not exist. | Fix the milestone id or create the milestone. |
refs-resolve/unresolved-ac | The composite-id reference's AC half (M-NNN/AC-N) names an AC that does not exist on the milestone. | Fix the AC number or add the missing AC. |
refs-resolve/wrong-kind | A reference points at an entity of the wrong kind. | A milestone's parent must be an epic; an ADR's supersedes must be ADRs; etc. |
body-prose-id/malformed-shape | An entity's body prose contains an id-shaped token whose suffix isn't a valid id (letter suffix M-a, uppercase placeholder M-NNNN, or narrow-numeric M-1). | Replace with the canonical allocated id (M-0001), or wrap in backticks if the prose is discussing id syntax. Conversational sequential labels like M-1/M-2 belong in chat, not committed prose. |
body-prose-id/unresolved | An entity's body prose references a well-formed id (M-9999) that resolves to no entity. | Fix the spelling, or wrap in backticks if the prose is discussing a hypothetical id rather than a real reference. |
body-prose-id/unresolved-milestone | A composite id in body prose (M-NNNN/AC-N) names a milestone that does not exist. | Fix the milestone id or remove the reference. |
body-prose-id/unresolved-ac | A composite id in body prose names an AC that does not exist on the parent milestone. | Fix the AC number or add the AC. |
no-cycles | A cycle in the milestone depends_on DAG or the ADR supersedes chain. | Remove a back-edge. |
no-cycles/depends_on | The cycle is in milestone depends_on edges. | Break a back-edge in the milestone DAG. |
depends-on-cancelled | A non-terminal milestone's depends_on names a milestone that has since reached the negative-terminal status cancelled — the dependency can never be satisfied. | Retarget the dependency via aiwf milestone depends-on <milestone-id> --on <remaining-ids> (or --clear to empty it), or cancel the dependent milestone too. |
no-cycles/supersedes | The cycle is in ADR supersedes edges. | Break the chain — an ADR cannot transitively supersede itself. |
case-paths | Two entity paths differ only in case. Linux commits both; macOS / Windows case-insensitive filesystems collapse them to one entity. | git mv one of the directories so the names differ in more than case. |
load-error | A file under work/ failed to parse — malformed YAML frontmatter, unreadable file, or a structural issue the loader couldn't recover from. | Open the named file and fix the parse issue; subsequent checks run once load succeeds. |
contract-config | A contract binding in aiwf.yaml references an id with no entity, a missing schema/fixtures path, or a contract entity has no binding. | Run aiwf contract bind / aiwf add contract, fix the path, or aiwf contract unbind. |
contract-config/missing-entity | The binding's id: points at a contract entity that doesn't exist. | Either create the contract entity or remove the stale binding. |
contract-config/missing-schema | The binding's schema: path doesn't exist on disk. | Fix the path or create the schema file. |
contract-config/missing-fixtures | The binding's fixtures: directory doesn't exist on disk. | Fix the path or create the fixtures tree. |
contract-config/no-binding | A contract entity exists but no binding in aiwf.yaml references its id. | aiwf contract bind <id> --validator <name> --schema <path> --fixtures <path>. |
fixture-rejected | A valid/ fixture failed the schema. | Make the schema accept it, or move it to invalid/. |
fixture-accepted | An invalid/ fixture passed the schema. | Tighten the schema, or move to valid/. |
evolution-regression | A historical valid/ fixture fails the HEAD schema. | Revert the schema change, migrate the fixture, or rebind. |
validator-error | Every valid fixture for a contract was rejected — the schema or validator invocation is likely broken. | Inspect the captured stderr and fix the schema or validator command. |
environment | Validator binary not on PATH. | Install it (see the recipe's install instructions) or fix command: in aiwf.yaml. |
acs-shape/id | An AC's id doesn't match AC-N or doesn't follow the per-milestone 1..max ordering. | Fix the id in the milestone's acs[] list. |
acs-shape/title | An AC's title is missing or whitespace-only. | Fill in the title. |
acs-shape/status | An AC's status is not in {open, met, deferred, cancelled}. | Use one of the four statuses (deferred is a live terminal AC state). |
acs-shape/tdd-phase | An AC's tdd_phase is set on a milestone that is not tdd: required, OR it's set to a value not in {red, green, refactor, done}. | Either set the milestone to tdd: required, remove the field from the AC, or fix the phase value. |
acs-shape/tdd-policy | An AC at tdd_phase: done is in a milestone that is not tdd: required. | Either flip the milestone to tdd: required or remove the tdd_phase field. |
acs-body-coherence/missing-heading | The frontmatter acs[] lists an AC, but the body has no ### AC-N — <title> heading for it. | Run aiwf add ac (which scaffolds the heading), or hand-edit the body to add it. |
acs-body-coherence/orphan-heading | The body has an ### AC-N — ... heading but the frontmatter acs[] list does not include AC-N. | Either remove the heading or add the missing AC to acs[]. |
acs-body-coherence/duplicate-heading | The ## Acceptance criteria section repeats a ### AC-N heading for the same id. A duplicate of an id that is also in frontmatter is neither missing nor orphan, so it would otherwise pass clean. Scoped to the AC section, so the ## Work log convention (which repeats ### AC-N — <outcome> headings) is not flagged. | Delete the extra heading; keep exactly one ### AC-N per AC in the section. aiwf add ac now rewrites a placeholder heading in place rather than appending a second one. |
acs-empty-body | A non-archived milestone is in_progress or done and a non-cancelled AC's body under its ### AC-N heading carries no non-heading prose — a title-only stub is not a real contract for that criterion. Unlike the pre-existing entity-body-empty warning (which the terminal-status lifecycle gate silences at done), this rule stays live through done since that is exactly one of its two in-scope statuses. An AC with no ### AC-N heading at all is acs-body-coherence/missing-heading's concern instead. | Write real prose under the AC's ### AC-N heading via aiwf edit-body <milestone-id>. |
archived-entity-not-terminal | A file lives under a per-kind archive/ subdirectory but its frontmatter status is not terminal — i.e., a contributor hand-edited the status off-terminal after the entity was swept (per the archive convention §"Reversal"). The remediation is to revert the hand-edit, not to relocate the file — the kernel does not provide a reverse-archive verb; the canonical pattern when a closed entity needs revisiting is to file a new entity that references the archived one. | Restore the status to a terminal value, or file a new entity that resolves/supersedes the archived one. |
epic-terminal-non-terminal-children | An epic's frontmatter status is terminal (done/cancelled) while it still owns one or more non-terminal child milestones. aiwf promote/aiwf cancel already refuse to move an epic to a terminal status while a child milestone is non-terminal, so a genuine bypass of those guards (a hand-edit, a pre-guard binary) is one way to reach this. The other: aiwf add milestone / aiwf import creating a fresh milestone under an epic that was already terminal — that path has no dedicated guard yet, so this finding stands in for one. Fires regardless of whether the epic's file has been swept into archive/. | Bring each listed child milestone to a terminal status via aiwf promote <milestone-id> done or aiwf cancel <milestone-id> — the epic itself needs no action, since it is already terminal. |
acs-tdd-audit | On a tdd: required milestone, an AC is status: met but its tdd_phase is not done — met without a completed red→green→done cycle. Severity is error under tdd: required and warning under tdd: advisory; the audit does not run under tdd: none. | Drive the AC through its phases (aiwf promote M-NNNN/AC-N --phase …) to done before met, or set the milestone's tdd: policy to match the discipline that actually applies. |
milestone-done-incomplete-acs | A milestone is status: done but one or more of its ACs are still open (not met / deferred / cancelled). | Resolve every AC to a terminal state before wrap; or aiwf promote M-NNNN done --force --reason "…" to override (the standing check keeps surfacing it). |
milestone-done-zero-acs | A non-archived milestone is status: done with an empty acs: list — advisory, not a refusal; a permanently AC-less milestone is a legitimate end state. | Add an AC via aiwf add ac M-NNNN --title "…" if this was unintentional; otherwise no action needed. |
milestone-draft-incomplete-acs | A non-archived draft milestone carries an incomplete AC contract: subcode zero-acs when acs: is empty, or subcode empty-body when it has ACs but one carries no non-heading prose under its ### AC-N heading (the draft-rung, warning-severity mirror of acs-empty-body, which fires error one FSM stage later at in_progress/done). A warning, not a refusal: draft is a legitimate mid-planning state, so this surfaces the missing-contract gap without blocking, keeping a milestone from landing on main with no visible ACs. An AC with no ### AC-N heading at all is acs-body-coherence/missing-heading's concern instead. | Add the ACs at plan time via aiwf add ac M-NNNN --title "…" (zero-acs) or fill the AC body via aiwf edit-body M-NNNN (empty-body); no action needed if the milestone is intentionally still being scoped. |
milestone-cancelled-incomplete-acs | A milestone is status: cancelled but one or more of its ACs are still open. aiwf promote/aiwf cancel both refuse this transition through normal use, with no --force override — so this state means the verb layer was bypassed entirely (a hand-edit, a pre-fix binary). | Resolve every open AC to a terminal state (met / deferred / cancelled). |
id-path-consistent | An entity's frontmatter id disagrees with the id encoded in its on-disk filename/slug. | Renumber via aiwf reallocate <path> (rewrites both sides + references), aiwf rename if only the slug drifted, or hand-correct whichever side is wrong. |
body-prose-id | An entity's body prose contains an id-shaped token whose suffix isn't a valid allocated id (letter suffix, uppercase NNNN placeholder, or narrow-numeric). Surfaces per subcode (e.g. body-prose-id/malformed-shape). | Replace the token with the canonical allocated id, or wrap it in backticks if the prose is discussing id syntax rather than referencing a real entity. |
skill-body-id | A shipped consumer surface cites a real entity id — every *.md under internal/skills/embedded{,-rituals,-guidance}/** (skill bodies AND description: frontmatter, entity templates, role-agent cards, the guidance fragment) plus the statusline's # comments. The mirror image of body-prose-id (there a real id is required; here it is the defect). Inert in a consumer repo, where the source tree is absent. | Replace the real id with a canonical <prefix>-NNNN placeholder, or move the reference into a design/ADR doc-link (the one carve-out). |
git-config-core-worktree-misset | The repo's core.worktree git config points somewhere unexpected, which can misdirect kernel git operations. | Run git config --local --unset core.worktree from the repo root (keep an override only if your workflow — e.g. a bare repo — specifically requires it). |
| Code | Meaning |
|---|---|
titles-nonempty | Title is missing or whitespace-only. |
roadmap-case-collision | More than one case-variant of the generated roadmap artifact exists at the repo root (e.g. both ROADMAP.md and roadmap.md). Only physically possible on a case-sensitive filesystem; aiwf render roadmap --write reconciles to a single existing variant but cannot pick between two, so it leaves this advisory for you to resolve. Fix: git rm one variant so a single canonical ROADMAP.md (or the lowercase convention the repo already uses) remains. |
adr-supersession-mutual | ADR A says it's superseded by B, but B does not list A in its supersedes. |
gap-addressed-has-resolver | Gap is addressed but addressed_by is empty. |
epic-active-no-drafted-milestones | An epic at status active has zero milestones at status draft. The kernel-side preflight signal for aiwfx-start-epic: an active epic without queued draft work is a forward-motion gap. Strict-literal reading — the rule asks "what's queued next?", not "is anything in flight?", so it stays firing through the epic's lifecycle until either a new milestone is drafted or the epic is wrapped. Fix: aiwf add milestone --epic E-NN --tdd <policy> --title "..." to queue the next milestone, or aiwf promote E-NN done if all planned work is in flight or done. |
unexpected-tree-file | A file under work/ is not a recognized entity file — tree-shape changes go through aiwf <verb>, not direct writes. Promoted to error when aiwf.yaml: tree.strict: true. Configure exemptions via aiwf.yaml: tree.allow_paths (list of filepath.Match globs). Files inside a contract's directory (work/contracts/C-NNN-*/) are auto-exempt. See docs/design/tree-discipline.md. |
provenance-untrailered-entity-commit | A commit in the audit range (@{u}..HEAD by default; see --since) touched an entity file with no aiwf-verb: trailer (manual git commit). One finding per (commit, entity) — a commit touching three entities emits three findings, each tagged with its entity id. Repair with aiwf <verb> <id> --audit-only --reason "..." per entity; the matching finding clears on the next push. Audit-only on M-NNN/AC-N rolls up to M-NNN for matching. |
provenance-untrailered-entity-commit/squash-merge | Same finding, specialized when the offending commit's subject ends with (#NNN) — i.e., GitHub's default squash-merge pattern. Squash-merging through the GitHub UI silently drops the squashed commits' aiwf-verb trailers, even when the source commits were well-formed. Either change the repo's merge strategy to rebase-merge or --no-ff merge for branches that touch entity files, or run aiwf <verb> <id> --audit-only --reason "..." per entity touched to backfill the audit trail. |
provenance-untrailered-scope-undefined | The audit range is undefined: the branch has no upstream and --since <ref> was not passed. The audit is skipped. Configure an upstream (git push -u origin <branch>) or pass --since <ref> to opt back in. |
trailer-verb-unknown | A commit's aiwf-verb: trailer carries a value that is not in the closed set of registered verbs and subverbs — every command path from aiwf's Cobra tree, joined by hyphens (e.g. add, add-ac, milestone-depends-on, render-roadmap). Typical sources: an LLM-fabricated value on a hand-rolled Conventional-Commits commit (e.g. aiwf-verb: implement on a feat(...) commit), or a plugin-side ritual verb that lives outside aiwf's CLI. Split severity: commits whose ancestry includes the commit-msg-hook-install SHA emit at error with a remediation hint — the hook would have refused them at composition time, so landing them required --no-verify or git plumbing. Pre-hook history and any clone where the hook-install SHA is unreachable (shallow clone, fork divergence) stay at warning so addressed_by_commit refs and historical fabrications aren't retroactively broken. Fix: if the trailer was fabricated, amend the commit and drop the line — plain feat(...) / fix(...) code commits don't need an aiwf-verb: trailer. If a plugin emits the value, change the plugin to use a verb name aiwf registers (or to omit the trailer). Sovereign-human override: aiwf acknowledge illegal <sha> --reason "..." silences the specific commit's finding without rewriting history. |
id-rename-untrailered | A commit between merge-base(HEAD, trunk) and HEAD renames an id-bearing entity file (work/<kind>/<id>-<slug>.md or the equivalent per entity.PathKind) AND lacks an aiwf-verb: trailer in the rename-class closed set (retitle / rename / reallocate / archive / move). The chokepoint catches the CLAUDE.md §"Id-collision resolution at merge time" operator-discipline failure mode: resolving a trunk-collision via inline git mv instead of aiwf reallocate <new-id-or-path>. The immediate trunk-collision finding clears (gitops' rename detection paired the move via its trailer-driven path or cumulative-similarity fallback), but the kernel trailer history misses the renumber event — aiwf history <id> doesn't bridge to the new id, cross-references in body prose aren't rewritten, and any future check rule keyed on aiwf-verb: reallocate doesn't see the rename. Warning severity at first land; future tightening to error is deferred once usage demonstrates the discipline. Canonical resolution: aiwf reallocate <new-id-or-path> records the renumber with the proper trailer set and rewrites cross-references. Sovereign-human override: aiwf acknowledge illegal <sha> --reason "..." silences the specific commit's finding without rewriting history (for renames that were deliberate). |
acs-tdd-tests-missing | An AC at tdd_phase: done under a tdd: required milestone has no aiwf-tests: trailer on any commit in its history. Gated by aiwf.yaml.tdd.require_test_metrics: true; default off. Fix: re-run the cycle through aiwf promote --phase ... --tests "pass=N fail=N skip=N", or set the YAML field to false to silence. |
terminal-entity-not-archived | An entity has a terminal status (e.g. done, addressed, wontfix) but its file is still in an active dir — the normal transient state under the archive convention's decoupled model. One warning per pending-sweep entity. Advisory by default; the archive.sweep_threshold knob flips this to blocking past N. The aggregate archive-sweep-pending finding summarizes the count. Fix: run aiwf archive --dry-run to preview the sweep, then aiwf archive --apply to move terminals into their per-kind archive/ subdir in one commit. |
archive-sweep-pending | Aggregate finding reporting the count of terminal-entity-not-archived instances. Per-tree (no path/entity id). Hidden when zero. Advisory by default; the archive.sweep_threshold knob flips this to blocking past N. Fix: same as terminal-entity-not-archived — run aiwf archive --dry-run to preview the sweep, then aiwf archive --apply to clear the backlog. |
entity-id-narrow-width | The active tree mixes narrow and canonical (4-digit) entity-id widths — i.e., the tree is mid-migration to the canonical-width policy. Per the canonical-width policy §"Drift control" the rule is silent on uniform trees (either all-narrow or all-canonical) and fires only when both widths coexist outside <kind>/archive/. One warning per narrow active entity. Archive entries never participate in the active-tree state assessment. Fix: run aiwf rewidth --apply to canonicalize the active tree in a single commit, or hand-correct the rogue narrow file if it landed by allocator regression / hand-edit. Pre-migration consumers (uniform-narrow trees) stay silent indefinitely — the kernel does not nag. |
refs-resolve/cross-branch-pending | A structured reference (e.g. depends_on, parent) points at an id absent from the local working tree but present on another local branch (refs/heads/*) or remote-tracking ref (refs/remotes/*) — real, just not merged into this branch yet. Recomputed live on every run (nothing cached): if the source branch is later deleted or abandoned, this re-escalates to refs-resolve/unresolved on the next run. |
body-prose-id/cross-branch-pending | The body-prose mirror of refs-resolve/cross-branch-pending: an id-shaped token in prose resolves against another local branch or remote-tracking ref rather than the local working tree. Same live-recompute re-escalation to body-prose-id/unresolved if the source branch disappears. |
refs-resolve/cross-branch-collision | A structured reference resolves against the cross-branch view, but the id carries diverging content across two or more refs. Non-blocking — divergence is ambiguous between an in-flight edit on an unmerged branch (common, especially across worktrees of this repo, which share local branch refs) and a genuine duplicate-mint collision; the latter is still caught, just later, by the blocking ids-unique/trunk-collision check once both copies land in a shared tree. |
body-prose-id/cross-branch-collision | The body-prose mirror of refs-resolve/cross-branch-collision. |
entity-body-empty | An entity's load-bearing body section is empty — no non-heading non-whitespace content between the section heading and the next heading or EOF. HTML comments do not satisfy the rule (<!-- TODO --> is operator intent to defer, not the prose the design specifies). Per-kind subcodes name which kind fired: entity-body-empty/epic, entity-body-empty/milestone, entity-body-empty/ac, entity-body-empty/gap, entity-body-empty/adr, entity-body-empty/decision, entity-body-empty/contract. Asymmetric semantics: top-level ## Section bodies treat sub-headings as content (a milestone's ## Acceptance criteria is non-empty when it contains ### AC-N headings, even with no parent-level prose); ### AC-N bodies require true non-heading prose, since they are the leaf-prose container. Grandfather rule: this finding is independent of acs-tdd-audit — empty-body warnings on historical met + tdd_phase: done ACs do not retroactively re-engage the TDD audit. Fix: write prose for the named section. For ACs, aiwf add ac --body-file <path> scaffolds the body at create time; for existing ACs and all other kinds today, edit the file and run aiwf edit-body <id>. The check is permissive about what the prose is — paragraphs, bullet lists, code blocks, single sentences all clear the rule (kernel principle: prose is not parsed). Severity escalates to error under aiwf.yaml: tdd.strict: true. |
milestone-tdd-undeclared | A milestone's frontmatter lacks the tdd: policy field. Absent tdd: is silently treated as tdd: none, so the AC TDD audit never engages and the policy decision was never recorded. The defense-in-depth backstop for the hard --tdd requirement at aiwf add milestone: the creation verb is the chokepoint, but it can't see a field stripped by a later hand-edit nor a milestone brought in by a path that bypasses the verb (aiwf import, a raw write). Archive-scoped per the archive convention §"Check shape rules" — archived milestones never fire, so historical (grandfathered) milestones stay silent. Grandfather rule: independent of acs-tdd-audit — a tdd-absent milestone with already-met ACs surfaces this warning but is not retroactively re-audited for TDD phases. Fix: create with aiwf add milestone --tdd <required|advisory|none>, or set tdd: in the frontmatter for an existing milestone. Severity escalates to error under aiwf.yaml: tdd.strict: true. |
priority-not-applicable | An entity's priority frontmatter value is present, but the entity's kind does not carry its own priority — only gap and decision do. The mechanical backstop for that scope: the field lives on the shared entity struct, so nothing at decode time stops it from being set on the wrong kind. Presence, not blanking — unlike area on a milestone, an out-of-scope kind's stored value is left intact rather than silently dropped at load, so this check can see and report it. Absence is never evaluated — absent / empty priority never fires. Warning severity, no strictness knob, mirroring area-unknown's posture. Fix: remove the priority: field by hand and re-run aiwf check. |
area-unknown | An entity's area frontmatter value is present and non-empty but is not a member of the aiwf.yaml: areas.members set — the present-⇒-declared chokepoint for the optional workstream grouping tag, i.e. typo protection. The authoritative surface: a creation-time --area flag alone can't catch a later hand-edit or an aiwf import that introduces an undeclared area, mirroring the defense-in-depth pattern milestone-tdd-undeclared follows. Inert when no areas block is declared — the field parses but nothing validates; absence is never evaluated — absent / empty / explicit-null area all deserialize to "" and never fire. Reads the stored area, so only the root kinds (epic, ADR, gap, decision, contract) that carry their own area fire; a milestone derives its area from its parent epic and never double-reports under a bad-area epic. Archive-scoped per the archive convention §"Check shape rules" — archived entities never fire. Warning severity, no strictness knob (escalation deferred until real friction shows). Fix: correct the typo to match a declared member, add the value to aiwf.yaml: areas.members if it's a legitimate new workstream, or remove the area field. |
area-required | An entity of a self-tagging root kind (epic, ADR, gap, decision, contract) has no area frontmatter at all, while the consumer has opted into strictness via aiwf.yaml: areas.required: true — the present-at-all chokepoint for the 1:1 monorepo where every entity belongs to exactly one project. Orthogonal to area-unknown: that finding polices present-⇒-declared (typo protection); this one polices present-at-all (untagged is unassigned). Inert by default — with required absent or false the rule emits nothing (it is a gate, not a warning→error bump), so a tree with no areas.required validates byte-for-byte as before. validate() rejects required: true with zero members, so the knob can only bite a config that also declares a member set. Milestone-exempt — a milestone derives its area from its parent epic (blanked at load), so an untagged epic fires exactly once rather than once per untagged milestone underneath it. Archive-scoped per the archive convention §"Check shape rules" — archived entities never fire. Error severity (blocks pre-push). The same condition is also refused fail-fast at aiwf add (no untagged create of a root kind under required: true). Fix: tag the entity with aiwf set-area <id> <member>, or remove areas.required from aiwf.yaml if untagged entities are acceptable. |
area-dead-glob | A declared area's paths: glob (aiwf.yaml: areas.members[].paths) matches no real file or directory under the repo root — dead config from a renamed, deleted, or typo'd project path leaving that area's path oracle empty. This is the path-claim axis, orthogonal to the entity-tag axis where area-unknown / area-required live: it reads the filesystem against the declared globs and never reads any entity's area tag. Per-glob — each declared glob must locate at least one path, and each dead glob fires its own finding naming the member and the glob. Match semantics route through internal/areamatch (the doublestar-backed SSOT), so ** is evaluated the same way every area-path consumer evaluates it. Reads the filesystem read-only and never fails on IO — a missing or unreadable root yields no findings rather than firing dead-glob for every area (the roadmap-case-collision precedent); malformed globs are owned by config-load validation (Tier 1), not re-reported here. Inert without paths — a label-only / legacy string-form areas block (no member declares paths:) fires nothing on the path axis. Warning severity by default, escalated to error under aiwf.yaml: areas.required: true (the same ApplyAreaRequiredStrict post-pass that escalates area-unknown), so a monorepo that opted into strictness cannot push an area pointing at nothing. Fix: correct the glob to the path's real location, recreate the moved/renamed directory, or remove the dead glob from that member's paths:. |
area-overlap | Two declared areas' paths: globs (aiwf.yaml: areas.members[].paths) both claim the same directory — ambiguous attribution. The companion of area-dead-glob on the path-claim axis: dead-glob is the no-empty-column law (every area locates something), overlap is the row-disjointness law (no directory claimed twice). The ambiguity it catches would make the entity-touching checks (mistag, auto-derive) non-deterministic, so the path oracle must be a partition. One finding per overlapping area-pair, naming both areas and a representative shared path (the lexically-smallest shared path, for determinism). Match semantics route through internal/areamatch; reads the filesystem read-only via MatchFS (overlap needs the full matched sets to intersect) and never fails on IO (the roadmap-case-collision precedent). Inert with fewer than two paths:-carrying areas. Warning severity by default, escalated to error under aiwf.yaml: areas.required: true (the same ApplyAreaRequiredStrict post-pass that escalates area-unknown and area-dead-glob). Fix: narrow one area's glob so each directory belongs to at most one area. |
area-mistag | An entity's area tag and its linked commits disagree: the entity's commits (gathered via the aiwf-entity: trailer) touched paths falling entirely in another declared area's paths: territory, none in the entity's own. This is the entity-touching consumer of the path-claim axis — where area-dead-glob / area-overlap police the config↔filesystem partition, mistag polices config↔history consistency. It is the check that actually catches "filed against the wrong area, flew under the radar" — the failure label-only areas are blind to. The entity's effective area comes from Tree.ResolvedArea (a milestone is judged against its parent epic's area); match semantics route through the internal/areamatch SSOT. It only sees code commits that carry the aiwf-entity: trailer — trailer hygiene gates its reach, so an untagged code commit is a deliberate false-negative (the right failure mode for an advisory warning that never blocks). Only area-claimed paths participate — planning files, docs, and unclaimed code match no glob and never trigger it, so an entity's own planning commits never false-fire. Inert when no area declares paths:, when the entity's own area declares none, when the entity has no linked commits, when the entity carries the reserved global sentinel (inherently cross-cutting), or when archived (per the archive convention §"Check shape rules"). Warning severity, and it never escalates — deliberately absent from the ApplyAreaRequiredStrict post-pass that escalates area-unknown / area-dead-glob / area-overlap, because legitimate cross-cutting work exists and the sanctioned escape valve is acknowledgement, not a strictness bump. Fix: retag the entity with aiwf set-area, or — if the work is genuinely cross-cutting — acknowledge it via aiwf acknowledge mistag. |
area-unslotted | An immediate child directory of an operator-declared coverage root (aiwf.yaml: areas.coverage_roots) is claimed by no declared area's paths: glob — an unslotted project. This is the covering law of the path-claim axis: area-dead-glob is the no-empty-column law (every area locates something), area-overlap is the row-disjointness law (no directory claimed twice), and area-unslotted is the covering law (every in-scope directory is claimed by some area). The universe is scoped and opt-in — only the immediate children of the declared coverage root(s) must tile; directories outside any declared root are unscoped and never flagged, so a single-project / semantic-section repo (which declares no coverage root) is never flagged wholesale. Roots are literal directory paths (not globs), may be nested, and several may be declared for a mixed-depth layout (the immediate-children contract is per root). Match semantics route through the doublestar-backed internal/areamatch SSOT — a whole-project glob (projects/app-a/**) claims its bare project directory (projects/app-a), so no second matcher. Reads the filesystem read-only and never fails on IO — single-level os.ReadDir per declared root; a missing or unreadable root yields no findings (the roadmap-case-collision precedent); enumerating only declared roots (never a blanket walk) sidesteps .git / node_modules / build-output noise. Inert when no coverage root is declared (the activation signal) and when no area declares paths: (the path axis is dormant). Warning severity by default, escalated to error under aiwf.yaml: areas.required: true (the same ApplyAreaRequiredStrict post-pass that escalates area-unknown / area-dead-glob / area-overlap). Fix: slot the directory into an area (add it to a member's paths:), narrow the coverage root, or remove the root if that subtree is not a project-tiling scope. |
area-coverage-root-missing | A declared coverage root (aiwf.yaml: areas.coverage_roots) resolves to no directory — it does not exist, or it names a file. Dead config, the coverage analogue of area-dead-glob: a silently-skipped dead root would give false confidence that coverage is active for that scope. An os.Stat guard (mirroring area-dead-glob) distinguishes "resolves to no directory" (warn) from a transient/permission IO error (skip, the roadmap-case-collision precedent). One finding per dead root. Warning by default, escalated to error under aiwf.yaml: areas.required: true (the same ApplyAreaRequiredStrict post-pass). Fix: correct the path to the real coverage-scope directory, or remove the dead entry. |
area-coverage-no-paths | aiwf.yaml: areas.coverage_roots is declared but no area declares paths:, so the path oracle is dormant and area-unslotted has nothing to match against. Surfaced rather than silently no-op'd — the operator took an affirmative action (declaring coverage roots) whose prerequisite (paths:) is missing, distinct from the legitimately-inert no-coverage-root case. One finding per tree (not per root). Warning by default, escalated to error under aiwf.yaml: areas.required: true. Fix: add paths: to a member (areas.members[].paths), or remove the coverage roots if path-based coverage isn't wanted yet. |
fsm-history-consistent/illegal-transition | A status-change commit moves an entity's status: between two values that are not connected by a legal edge in the kind's FSM (e.g., epic proposed → done skipping active), AND the commit has no aiwf-force: trailer to record a sovereign override. The kernel chokepoint that makes the per-entity status FSM a tree-invariant rather than just a verb-precondition. Error severity (blocks pre-push). The walk is per-entity via git log --follow, so renames preserve history; the prior status is read from git show <parent-sha>:<path>. Fix: re-route the change through aiwf promote <id> <to> (which only accepts FSM-legal moves) or aiwf cancel <id>; when the exceptional flip is genuinely warranted, re-run the verb with --force --reason "..." so the override rides in the trailers. |
fsm-history-consistent/forced-untrailered | A status-change commit matches a sovereign-act shape — a transition the kernel deliberately requires explicit override for, such as epic proposed → active (the kernel's ratification semantics for that transition) — but the commit carries no aiwf-force: trailer. The transition itself is recognized by the FSM; the missing trailer is what fires the finding. Error severity. Fix: re-run the verb with --force --reason "..." so the sovereign nature of the act is recorded, or undo the change via the corresponding inverse verb. The aiwf-actor on a force-trailered commit must be human/... (provenance rule), which is what makes the sovereign trail auditable. |
fsm-history-consistent/manual-edit | A status-change commit is a legal FSM step (the transition exists in the kind's FSM) but the commit message has no aiwf-verb: trailer at all — a hand-edit + git commit that bypassed the kernel verb path. Overlaps with provenance-untrailered-entity-commit (which fires per-entity on any untrailered touch) but with FSM-specific framing and error severity (vs the provenance code's warning). Audit-only suppression: a later commit carrying aiwf-audit-only: with an aiwf-entity: matching this entity clears the finding for the (commit, entity) pair, mirroring how provenance-untrailered-entity-commit works. Fix: re-route through aiwf promote / aiwf cancel, or — when the change is already merged and rewriting is wrong — run aiwf <verb> <id> --audit-only --reason "..." per affected entity so the audit-only commit records the trail. |
fsm-history-consistent/history-walk-error | The walker hit a real failure reading the named entity's commit history during the batched walk — a subprocess crash, a blob-read protocol error, or a context cancelled mid-walk. This subcode and the partial-preservation contract it pins: the rule routes through gitops.BulkRevwalk (one git log subprocess for the whole repo) and gitops.BlobReader (one long-lived git cat-file --batch for status reads); per-blob read failures emit one history-walk-error per affected (entity, commit) pair while other entities' findings still surface alongside (CLAUDE.md §Engineering principles — "Errors are findings, not parse failures"). Error severity. Fix: re-run aiwf check to confirm whether the failure is transient (concurrent test load on macOS, kernel resource pressure); if it repeats, inspect git fsck and .git/objects/ permissions in the consumer repo. |
acs-title-prose | An AC title is prose-shaped rather than a short label — the whole title renders as one big heading. Fix: shorten the title to a single short label and move the detail into the AC body under ### AC-N (e.g. via aiwf add ac --body-file). |
promote-on-wrong-branch | An activating-promote (aiwf promote E-NNNN active / aiwf promote M-NNNN in_progress) landed on a branch other than the entity's expected parent branch, contrary to the branch model (sovereign activating acts land on the parent branch before the ritual branch is cut). Branch-choreography class; warning severity, surfaced advisory-first. Fix: land activations on the parent branch; if the placement was deliberate, aiwf acknowledge illegal <sha> --reason "…" as a human, or add an aiwf-force: <reason> trailer to the promote commit. |
isolation-escape | An AI-actor commit landed on a branch that does not match its active scope's recorded aiwf-branch: (the branch-choreography model — an AI-actor commit must ride the branch its active scope names). The rule fails shut on correctness — it stays silent when branch resolution was lost — so this fires only on a confidently-wrong placement. Fix: if deliberate sovereign work, aiwf acknowledge illegal <sha> --reason "…" as a human; or re-author via git cherry-pick -x <sha>; or amend with an aiwf-force: + aiwf-actor: human/<id> trailer. |
isolation-escape-shallow-clone | The repository is a shallow clone, so the per-commit branch map is left empty and isolation-escape cannot run — a total-coverage gap made mechanically visible. Fix: unshallow with git fetch --unshallow, or in CI use actions/checkout with fetch-depth: 0. |
isolation-escape-oracle-failure | Advisory: the branch-choreography oracle could not resolve one or more refs, so isolation-escape could not be checked for the affected commits (it fails shut on correctness, open on coverage — operator visibility, not a blocker). Fix: inspect the named ref/failure mode; it clears once the ref resolves. |
isolation-escape-orphaned-ai-commit | An AI-actor commit was orphaned by a non-fast-forward update (force-push) on a ritual branch, so the kernel cannot tell from the orphan alone whether it was on the correct branch. Fix: review the commit; if it was deliberate sovereign-human work, record it via aiwf acknowledge illegal <sha> --reason "…" as a human. |
Areas
pathsschema. The two path-axis findings above —area-dead-globandarea-overlap— are the first consumers ofaiwf.yaml: areas.members[].paths: an optional, per-member list of**-capable globs locating each declared area's source tree. A member written in the legacy bare-string form (members: [app-a]) carries nopaths, so the path-axis checks stay inert for it. Glob syntax is validated at config load (a malformed glob is a hard load error naming the bad glob); match semantics are the doublestar-backedinternal/areamatchSSOT, shared with the entity-touching area checks —area-mistagnow consumes the same globs; auto-derive is still to come.
Areas
coverage_rootsschema.aiwf.yaml: areas.coverage_rootsis an optional list of repo-relative directory paths that opts a multi-project monorepo into the scoped-coverage law: within each declared root, every immediate child directory is a project expected to be claimed by some area'spaths:glob, and an unclaimed child firesarea-unslotted. Its presence is the activation signal — absent, the coverage check is inert, so a single-project / semantic-section repo is never flagged wholesale; it is also inert when no member declarespaths:. Each root is a literal directory path (not a glob); roots may be nested, and several may be declared for a mixed-depth layout, since the immediate-children contract is per declared root. Validated at config load (each entry non-empty, whitespace-clean, and a valid repo-relative path — no leading slash, no..segments). Enumeration is single-level (oneos.ReadDirper declared root), read-only, and never fails on a transient/permission IO error (theroadmap-case-collisionprecedent); hidden (dot-prefixed) immediate children (.git/.github/.claude/ …) are skipped — hidden directories are tooling/VCS artifacts, never projects (the Unix dotfile convention). This covers only hidden dirs: a"."root still enumerates non-hidden top-level dirs (docs/,node_modules/), so point coverage at a dedicated project-parent root (projects/,apps/) rather than"."unless every non-hidden top-level directory is genuinely a project. The "is this directory claimed?" test routes through the same doublestar-backedinternal/areamatchSSOT the path-axis checks use — no second matcher. Two opted-in-but-undeliverable misconfigurations are surfaced, not silently skipped: a declared root that resolves to no directory firesarea-coverage-root-missing(dead config, the coverage analogue ofarea-dead-glob), and coverage_roots declared with no areapaths:firesarea-coverage-no-paths(the path oracle is dormant). All three coverage findings are warning severity by default, escalated to error underaiwf.yaml: areas.required: true(the sameApplyAreaRequiredStrictpost-pass that escalatesarea-unknown/area-dead-glob/area-overlap).
These fire on commit history, not tree state. Each names the offending commit's short SHA in its message.
| Code | Meaning | Typical fix |
|---|---|---|
provenance-trailer-incoherent | A required-together pair is partial, or a mutually-exclusive pair are both present (e.g., aiwf-on-behalf-of: without aiwf-authorized-by:, aiwf-actor: ai/... without aiwf-principal:, aiwf-actor: human/... with aiwf-principal:). | Re-create the commit using the correct verb invocation; --principal human/<id> is required when the actor is non-human. |
provenance-force-non-human | aiwf-force: present on a commit whose aiwf-actor: is not human/.... | --force is sovereign — only humans wield it. Have a human invoke the verb directly. |
provenance-actor-malformed | aiwf-actor: does not match <role>/<id>. | git config user.email is malformed; fix it (see aiwf doctor). |
provenance-principal-non-human | aiwf-principal: role is not human/. | Principal must be human/; agents and bots cannot be principals. |
provenance-on-behalf-of-non-human | aiwf-on-behalf-of: role is not human/. | Same as principal — rebuild from the originating authorize commit. |
provenance-authorized-by-malformed | aiwf-authorized-by: is not 7–40 hex. | Copy the correct SHA from aiwf history <scope-entity>. |
provenance-authorization-missing | The authorize SHA does not name an aiwf-verb: authorize / aiwf-scope: opened commit. | Typo or stale SHA after force-push; use the full SHA. |
provenance-authorization-out-of-scope | The verb's target entity has no reference path to the scope-entity. | Either authorize the right entity or work on something the existing scope already reaches. |
provenance-authorization-ended | The scope was already ended (terminal-promote / revoke). | Open a fresh scope with aiwf authorize <id> --to <agent>. |
provenance-no-active-scope | An ai/... actor produced a commit with no aiwf-on-behalf-of:. | Open an authorization scope, or run the verb as the human directly. |
provenance-audit-only-non-human | aiwf-audit-only: present on a non-human actor's commit. | Only humans may backfill audit trails. |
--no-verify to "fix it later" — broken state on main is the thing this hook exists to prevent.aiwf cancel <id> is the right way to retire an entity.provenance-untrailered-entity-commit warning — aiwf <verb> <id> --audit-only --reason "..." is the first-class per-entity repair path and keeps history append-only. One audit-only commit clears one entity's finding; commits that touched multiple entities need one audit-only per entity (or a single audit-only on a parent that all touched ids reach via composite-id rollup).