@sabaiway/agent-workflow-kit 10.6.0 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +7 -6
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/launchers/windsurf-workflow.md +2 -2
- package/package.json +1 -1
- package/references/contracts.md +1 -1
- package/references/modes/bootstrap.md +5 -5
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +2 -0
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/references/scripts/archiver-structure.test.mjs +48 -3
- package/references/scripts/install-git-hooks.test.mjs +31 -1
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/agent_rules.md +1 -1
- package/references/templates/changelog.md +1 -1
- package/references/templates/decisions.md +2 -2
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +20 -9
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +2 -2
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +18 -4
- package/tools/recommendations.mjs +53 -3
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +43 -34
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- package/tools/worktrees.mjs +2 -2
|
@@ -19,16 +19,28 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/flow-writer.mjs <arm> …`** — the arm s
|
|
|
19
19
|
| `degrade-justification --backend <b> [--down-mark <d>] [--degrade-digest <d>]` | binds a core degrade to a then-active down-mark (#25) — both digests resolve automatically; an explicit digest only VERIFIES the resolved authority (a foreign digest never mints), and the store's locked preflight re-refuses a mark closed in the race window |
|
|
20
20
|
| `maintainer-override <planId> --backend <b> --checkpoint-approved` | the checkpoint-approved ship-over-veto record — it **prints the FULL bound set** it is about to record and **requires the explicit flag** (#38); without the flag the bound set still prints and nothing is written |
|
|
21
21
|
| `consult-attestation <planId> --backend <b> --nonce <n> --proposed-fix-digest <d>` | binds a consult to a real dispatch's findings (#11/#33): `findingDigest` is **computed from the `{backend, nonce}`-named finding manifest** beside the receipts file (never hand-supplied); `proposedFixDigest` is the explicit consult-time input; refuses without an open step, or without a readable identity-matching manifest |
|
|
22
|
-
| `round-open <planId> --backend <b> [--backend <b> …] [--step <stepId>] [--new-cycle] [--justification <t>]` | the round's **pre-dispatch half** (#41): mints
|
|
23
|
-
| `round-land <planId> [--dispose
|
|
22
|
+
| `round-open <planId> --backend <b> [--backend <b> …] [--step <stepId>] [--new-cycle] [--justification <t>]` | the round's **pre-dispatch half** (#41): mints before any backend runs and applies the cap, walk and completeness gates detailed below |
|
|
23
|
+
| `round-land <planId> [--dispose <action> …]` | the round's **post-arrival half** (#42/#13/#33): binds exact arrivals and records one of the five dispositions detailed below |
|
|
24
24
|
| `freeze <planId>` / `converged <planId>` | the step terminals, gated on **completeness + the sanctioned-move rule** (no premature terminal): every dispatch landed or **justified-degraded** at the dispatched tree (a core degrade + a mint-time-valid `degrade-justification` at the round's `{base, fingerprint}` — a bare degrade is not base-bound); every landed non-ship receipt rides a round with a non-empty disposition ledger (a form floor — semantic per-finding completeness is an honest limit); the tree sits at the last round's fingerprint or reached it through an **anchored** declared bookkeeping-delta chain + refresh (a pre-round chain never re-certifies a later move). Both walks **re-run on the locked store snapshot** at append time |
|
|
25
25
|
| `unfreeze <planId> [--justification <t>]` | reopens the frozen step (in-step) or the just-converged terminal (boundary); **cap 1 per cycle** — the design's post-freeze checkpoint |
|
|
26
|
-
| `internal-attestation <planId> --lens <l> … [--degraded <b> …] --model <m> [--effort <e>] [--tier <t>] --authority <a
|
|
26
|
+
| `internal-attestation <planId> --lens <l> … [--degraded <b> …] --model <m> [--effort <e>] [--tier <t>] --authority <a> [--walk <file>]` | the #28 internal-review attestation at the open step's round, gated on #68 adoption coverage. `--walk` is a capped, no-follow closed JSON file relative to the absolute git common dir; for every judged plan row every class in TAGGED ∪ PRESENT needs non-empty `checked`, while extra entries are ignored and PRESENT minus TAGGED is recorded as `walk.uncovered`. |
|
|
27
27
|
| `write-plan-id <plan-file> --plan-id <id>` | adds the frontmatter `planId` line — bounded to an EXISTING regular file under `docs/plans/` (never a symlink), contained-atomic, same-id idempotent, **different-id refuses** (#58) |
|
|
28
28
|
|
|
29
|
+
`round-open` mints the round record BEFORE any backend runs — per `--backend` a fresh nonce + receipts watermark; stdout prints one `dispatch backend=<b> nonce=<n> watermark=<w>` line per dispatch (pass the nonce as `--nonce <n>` on the wrapper invocation — the plain-argument lane onto the `AW_REVIEW_NONCE` seam; hand the pair to `receipt-deadline`).
|
|
30
|
+
|
|
31
|
+
Every round after the first requires an authoritative walk-carrying attestation at the preceding round's `{planId, cycle, stepId, round}` and the CURRENT tree's `{base, fingerprint}` when the adopted plan has a judgeable ledger row; `--justification` may lift that absence as an echoed input, and the ABSENCE of the attestation is its durable trail. At `cap reached` or `crossover`, every blocking item from the latest head's landed receipts must be covered by equal item digest: uncovered items print verbatim, enumeration failure is an advisory plus the existing disposition floor, and a lost manifest is covered only by `custody-lost`; this cap refusal is never lifted by `--justification`.
|
|
32
|
+
|
|
33
|
+
Both lock-free and locked `round-open` checks run cap → walk → completeness floor. Boundary invocations need `--step`; `--new-cycle` opens the next cycle; a fingerprint move opens a new round.
|
|
34
|
+
|
|
35
|
+
For `round-land`, exactly one fresh code-artifact non-probe line of the dispatched backend **carrying the dispatch's exact nonce** (the wrapper stamps `AW_REVIEW_NONCE` into the receipt — a delayed or cross-chain answer can never cross-bind) past the watermark is the candidate, and only the canonical **ATTESTING** class binds. It revises the ONE round record in place with five closed actions.
|
|
36
|
+
|
|
37
|
+
`folded --finding <quote> --proof-kind … --proof-digest <d>` and `rejected --finding <quote> --reason <r>` retain their bindings; `queued --finding <quote> --claim <invariant> --proof-kind consult-attestation|red-proof --proof-digest <d>` requires `decideFoldScope` ACCEPT, derives `debtId` from the resolved row and computes `debtDigest` from its block, then binds the proof to this head, item and claim.
|
|
38
|
+
|
|
39
|
+
`escalated --finding <quote> --override-digest <d>` binds a veto override and only that receipt's findings; `custody-lost --receipt <receiptDigest>` carries no quote, is recordable whenever the carrier loss is real, covers that receipt for the cap, and excludes it from later manifest re-reads. Arrivals still bind only canonical attesting receipts and identity-matching manifest bytes.
|
|
40
|
+
|
|
29
41
|
Operand shapes: a positional operand may follow a literal `--` and a value flag accepts `--flag=<value>` — the lanes a leading-dash id or value rides; the checker's printed recoveries compose exactly these shapes. Every arm computes its **tree context** (owner, base, fingerprint; cycle/round from the chain walk) — you never hand-supply tree identity. Chain arms **refuse a foreign worktree's chain** (#57): move a chain only from the worktree that adopted it.
|
|
30
42
|
|
|
31
|
-
**Round-machinery caps (enforced at the arms; the store owns transition legality):** HARD_MAX **3 rounds per {cycle, step}** and **1 post-freeze unfreeze per cycle**.
|
|
43
|
+
**Round-machinery caps (enforced at the arms; the store owns transition legality):** HARD_MAX **3 rounds per {cycle, step}** and **1 post-freeze unfreeze per cycle**. The walk obligation holds when the adopted plan's ledger parses with at least one valid row; a plan with no judgeable row owes no walk. Both lock-free and locked round-open checks run cap → walk → completeness floor. The justification predicate is `overCap || walkRefusalFired`: a non-empty justification lifts only a design-cap or missing-walk refusal and is echoed; an in-cap invocation with its walk satisfied refuses it as usage. The blocking-item cap is independent and never lifted. A `custody-lost` entry is recordable whenever the carrier loss is real. One-way door: records carrying `walk`, the queued proof trio, `escalated`, or `custody-lost` require kit >= 11 to read; pre-rung records remain valid.
|
|
32
44
|
|
|
33
45
|
**Exit codes:** `0` success (incl. the `write-plan-id` idempotent no-op); `2` usage; `1` refusal (a store STOP verbatim, a derivation failure, or the missing checkpoint flag).
|
|
34
46
|
|
|
@@ -36,6 +36,8 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/procedures.mjs <activity> [--override <slo
|
|
|
36
36
|
|
|
37
37
|
**Spec-store block (the feature-spec layer, `plan-execution` only).** After a `plan-execution` render the advisor also prints `Spec store (the feature-spec layer) — …`: the two POPULATED `spec-check` commands and the session register they read. **Session lane** — `--ops-file docs/plans/spec-ops.list` (or repeated `--op <add|modify|remove>=<path>` / `rename=<old>:<new>`) judges the closure of the declared change: the targets plus each one's listing parent, on their post-state, their reader verdict, their per-kind line cap, their scenario bindings and the containment of every path they name. **Store lane** — `--all` judges the whole store instead and adds the four cross-document invariants (an unlisted child, DISTINCT from an orphan · acyclicity · store-wide slug uniqueness · module overlap); an absent store root refuses the run rather than reporting an empty clean store. The two lanes are exclusive. **Register lifecycle:** `docs/plans/spec-ops.list` is *session scratch* — the session writes it, the plan's Phase Cleanup deletes it — and it is **never defaulted** by the tool, because an unnamed register would attest a post-state nobody declared (the `fold-scope --queue` precedent). `--json` carries the same lines under **`specCheck`** (an empty array outside `plan-execution`). Advisory like the finding-scope block: nothing records that it ran. Exit `0` clean · `1` findings · `2` usage.
|
|
38
38
|
|
|
39
|
+
**Robustness-brief block (`plan-execution` only).** The advisor renders `node ${CLAUDE_SKILL_DIR}/tools/robustness-brief.mjs --plan <plan>` populated when exactly one plan is in flight and names the zero/several-plan placeholder reason otherwise. `--json` carries the identical lines under **`robustnessBrief`**, an empty array outside `plan-execution`; human and structured renders share one block.
|
|
40
|
+
|
|
39
41
|
**Cap-soft-skip degradation (the feature's only AUTO route).** The activity procedures are auto-discoverable only through the one-line **`workflow:methodology`** pointer (this kit + the engine carry `disable-model-invocation:true`, so NL like "write a plan" does **not** auto-load this skill). On a deployment whose methodology pointer was cap-soft-skipped — or whose pre-existing customized pointer lacks the procedures clause — the procedures are still reachable by **explicitly** invoking `/agent-workflow-kit procedures`; surface that plainly rather than treating it as a gap.
|
|
40
42
|
|
|
41
43
|
**Invariants:** read-only · never writes · never commits · never runs a subscription CLI · the deterministic resolution is the kit's, the recipe execution is the orchestrator's.
|
|
@@ -33,6 +33,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
33
33
|
|
|
34
34
|
- `source-size` — the practice's arms differ by what the declared gate actually IS. **`adopted-elsewhere`** — the gate runs a DIFFERENT real copy of this checker (a vendored deployment): the practice is running, the advisor simply cannot resolve it as its own sibling, and the realpath anchor deliberately does not widen (it is what stops a lookalike from certifying). So the convergence is the ack writer's preview (`--lane source-size-copy` → `sourceSizeCopyAck`), never `--adopt`, which would mint the record and then be refused by the fill on the id already declared. The fingerprint binds the declared claims AS AUTHORED, sorted — not their resolved paths, which are machine-specific and would churn a committed ack between machines — so a second external copy re-fires it. **`id-squatter`** — a gate carries the `source-size` id and is not this checker under any reading: the id claims the practice while nothing measures module size, so it is **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
|
|
35
35
|
- `gate-hook` — the base arm is the ordinary opt-in wiring offer. The **`marker-stale`** arm is not: the placed hook validates your declaration through its OWN baked copy and goes dark on any key it does not know, so a declaration carrying the `lcovProducer` key under a hook that predates it silently turns auto-approval OFF, and every gate prompts again with no error anywhere. The condition is the key's PRESENCE, not its value — an older hook rejects a key it does not know whatever that key says, so `"lcovProducer": false` darkens it exactly as `true` does. The arm is deliberately marker-scoped — a stale hook is otherwise harmless — and its recovery is the writer's own: `gate-hook --apply` places only an ABSENT target, so converging means deleting the placed hook and re-placing it. That makes it a destructive **HAND-APPLY** (`rm` + `--apply`, absolute path so it can only delete this project's hook), never something the consent flow runs for you. When the read-lane is also enabled, this arm and `read-lane.stale` would report the same file with the same recovery, so exactly one renders — **this one**, because its cause is the true one: a hook that postdates the read-lane and merely predates the marker key reads `lanes.json` perfectly well, and the read-lane arm's wording would be false over it. Risk profile: deleting one placed hook file, then re-placing it from the bundle; the declaration is never touched.
|
|
36
|
+
- `enforcement` — copies the bundled `references/scripts/*.mjs` runnable scripts and their tests, then installs the pre-commit hook. It is HAND-APPLY: the agent never runs it; the offer renders only when `scripts/` is absent or a plain directory contains no bundled basename, so the copy never clobbers an existing file and never follows a link. The hook invokes `node` directly, so a committer without it on PATH gets a loud failure. The installer refuses to overwrite a pre-commit hook it did not write — on that refusal stop and merge the hook by hand (bootstrap step 9); the copied scripts stay in place. Risk profile: local tooling and a Git hook are added only when the maintainer applies the shown command.
|
|
36
37
|
- `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project, which is a different kind of consent from the other velocity items: a registration is a command your MCP client will RUN, so the apply here is the mode's own **flagless preview**, which prints the exact entry and writes nothing — the `--apply` that follows is a SEPARATE step you run after reading that entry, and the consent flow never carries it. What the registration buys: path questions and literal searches become typed tool calls whose arguments are named JSON fields instead of a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — they are perfectly legal bytes to search for, there is simply no shell to read them as operators. Posture: the server is a **read-only child of your client** (path/type/size/line facts and literal search over this project root; no write and no exec API), and like the client itself it runs **outside the Bash sandbox** — the sandbox is not what bounds it, the server's own root containment is. The two allow rules it adds make those two tools promptless and nothing else. Two arms are **HAND-APPLY** and never run for you: **`.differing`** — an `agent-workflow` entry already stands in `.mcp.json` and **structurally differs** from what this kit copy would write (another kit copy, a hand-edited path, an added `env`; the comparison ignores key order, so re-serialized identical bytes are the same registration), and silently changing what an MCP server launches is exactly what consent must not slide past, so the remedy is your edit; and the **masked** arm, where an OS sandbox hides `.mcp.json` behind a device node — the kit cannot write there, so it hands you the text to paste from outside the sandbox. When the file is masked but the settings half is already complete, the item **does not render at all**: what is unobservable becomes a stated SKIP, so optimality is withheld rather than a registration you already made being offered again. Stated limit: this item does **not** detect a `disabledMcpjsonServers` veto, so a converged `mcp-channel` means *what the mode writes is in place*, not *the client will load it* — see `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` for why that check was subtracted rather than half-built. Risk profile: a new read-only channel your client will launch; no write or exec exposure, and no existing declaration is touched.
|
|
37
38
|
- ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
|
|
38
39
|
- `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it writes the reader/checker pairs and the store root, create-only, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries a NAMED **HAND-APPLY alternative** — the DECLINE preview (`ack-write --lane spec-adoption`, a dry-run that prints the exact `--apply`), which records `specAdoptionAck` in the family-owned `docs/ai/acks.json`. **The two are exclusive, and the order is part of the contract:** ask which the maintainer wants BEFORE the confirmation; on "seed" run the apply; on "decline" run NOTHING — hand over the preview line and let them run it and its printed `--apply`; never run the seed after a decline was chosen, and never run the decline preview for them. **`adopting`** (a store with no live contract): an optional item naming the draft count; its apply is that same decline preview (a dry-run; after the SAME confirmation you run the `--apply` it prints), and the honest remedy is authoring a contract from the skill's `SPEC_TEMPLATE.md`. A recorded decline silences BOTH arms and shows on `status` as ` — declined`; one live contract makes the decline moot. An unreadable store is a stated skip, never `flow optimal`. Risk profile: the seed writes only create-only files under `scripts/` and `docs/ai/specs/`; the decline writes one key into the ack store; nothing here authors a spec or blocks a plan.
|
|
@@ -7,7 +7,7 @@ Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shippe
|
|
|
7
7
|
Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
|
|
8
8
|
|
|
9
9
|
1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, the `held session: <id> — <n> fold(s) rode it` / `none` / substitution line when configured delegated execution arms it, and per-backend receipt state (current / stale / ungrounded / probe / rejected / missing) with verdict + grounding + timestamp.
|
|
10
|
-
2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute = delegated` with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. The accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a non-
|
|
10
|
+
2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute = delegated` with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. The accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a cwd where git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree location that passes — `error`, `redirected`, `no-work-tree` and `env-only` refuse by their state name once a receipt could be required), or every recipe-named backend SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a not-a-repository cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY review-backend escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (ANY edit after a review moves the fingerprint), ungrounded, vetoed, or unrecognized. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
|
|
11
11
|
3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** The candidate line for your own `docs/ai/gates.json`: `{ "id": "review-state", "title": "Review receipts current for the uncommitted tree", "cmd": "node <path-to-this-skill>/tools/review-state.mjs --check" }` — with the path your project actually reaches the kit by, QUOTED so a path with spaces survives, executable from the project root. The consent-gated gates-init preview — `node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>` (dry-run: it prints the derived entries and writes NOTHING; `--apply --only review-state` writes exactly this one on your explicit yes) — offers exactly this entry, path resolved and quoted, ONLY when your `docs/ai/orchestration.json` declares `reviewed`/`council` on `plan-execution.review` (the slot this checker enforces). The full consent-fill contract is in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate — and `run-gates --final` requires this check among its canonical core gates.
|
|
12
12
|
4. **`--await [--timeout <s>]`** (BUGFREE-3 / AD-049) → BLOCK until every recipe-named backend is SATISFIED for the current tree (i.e. until `--check` would PASS), or the bounded timeout elapses (a loud exit 1; default 900s). Run it after dispatching the review bridges to WAIT for their receipts to land instead of hand-polling a pid: the durable completion signal is the **receipt**, never a process event (a harness "completed" notification fires early; a bridge's output late-flushes). It inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. A held-session refusal and an **AUTHORITATIVE veto terminate the wait loudly BEFORE the deadline**: neither can be moved by waiting; only the held-session retry/degrade lane or a fresh review can move its corresponding refusal. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly. For waiting on **one dispatch's arrival** (never obligation satisfaction) use the receipt-arrival deadline runner, `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md`.
|
|
13
13
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
### Mode: robustness-brief
|
|
2
|
+
|
|
3
|
+
<!-- opt-in-capability: none — a read-only generator over a plan file and the kit's own list -->
|
|
4
|
+
|
|
5
|
+
Read-only **executor-brief generator** — it turns a ledger row's `robust:<class>[,<class>]` tag into
|
|
6
|
+
the block of concrete robustness literals the executor must prove BEFORE the first line of code.
|
|
7
|
+
Measured 2026-08-30: a diff correct to its contract took 13 review rounds, each finding one more
|
|
8
|
+
standard invariant (a git location variable, a sequencer ref, `%B`'s newline, `textconv`, fsync, a
|
|
9
|
+
no-process spawn error) that nobody had written into the brief. The list is data
|
|
10
|
+
(`${CLAUDE_SKILL_DIR}/references/robustness-literals.json`, versioned, language-neutral); this mode
|
|
11
|
+
renders it, so a retro extends the list instead of a reviewer re-finding a member.
|
|
12
|
+
|
|
13
|
+
**What it reads.** The plan file named by `--plan`, opened as a regular file no-follow through the
|
|
14
|
+
kit's capped descriptor read (1 MiB — a plan is capped at 100 lines) with its realpath required
|
|
15
|
+
under the realpath of the cwd (the project root the advisor renders the command from; no git is
|
|
16
|
+
asked): an absolute path outside, a `..` traversal out of it, a symlink leading out, a non-regular file and
|
|
17
|
+
an over-cap plan each refuse **exit 2** by name — the generator PRINTS row text, so unlike
|
|
18
|
+
plan-shape-cli's unbounded no-follow read it is bounded. The plan is judged on every rule of plan-shape's structural half (headings, section caps,
|
|
19
|
+
row grammar, ids, bytes, lexical containment, sweep form, duplicate paths, the total line, the
|
|
20
|
+
acceptance and governing-spec presence rules) over EMPTY facts (`{}` — never undefined); the
|
|
21
|
+
facts-bound half — realpath containment and sweep expansion — stays `plan-shape --check`'s. A plan
|
|
22
|
+
that fails the structural half is refused, never half-rendered. And the kit's own list through its
|
|
23
|
+
reader leaf (a list that fails the closed schema is refused by name; the tag grammar is that leaf's
|
|
24
|
+
`parseRobustTag`, ONE home). Nothing else: no git, no project config, no network.
|
|
25
|
+
|
|
26
|
+
Run `node ${CLAUDE_SKILL_DIR}/tools/robustness-brief.mjs --plan <plan> [--row <id>] [--coverage]`:
|
|
27
|
+
|
|
28
|
+
1. Plain run → the Markdown block, ready to paste into the dispatch brief: a header naming the list
|
|
29
|
+
version and the plan path; then, for every tagged row, its id and path, and per tagged class the
|
|
30
|
+
class's `prove` sentence followed by its members as `literal — note (source)`. Every
|
|
31
|
+
plan-derived path — the header's and each row's — is rendered through the kit's string display
|
|
32
|
+
escape (`escapeForDisplay`), so no output line carries a raw control byte, U+2028 or U+2029; a
|
|
33
|
+
U+FFFD passes as the printable replacement character (stated, not escaped). The brief is built
|
|
34
|
+
whole before a byte is printed — never a partial brief. **Exit 0.** A plan with no `robust:`
|
|
35
|
+
token prints ONE stated line and still exits 0 — an untagged ledger is a fact about the plan, not
|
|
36
|
+
a failure of the tool. A row whose tag the grammar refuses (`no-class`, `empty-class`,
|
|
37
|
+
`duplicate-class`, `invalid-class`, `multiple-tags`) or names an unknown class id is **exit 1**
|
|
38
|
+
naming the row — never a zero-tags success.
|
|
39
|
+
2. **`--row <id>`** → the block for that one row (the slice brief of a subagent or a delegated
|
|
40
|
+
dispatch). **Exit 1** when the row is absent from the ledger or carries no tag, so a brief can
|
|
41
|
+
never be assembled from a row that names nothing to prove.
|
|
42
|
+
3. **Exit 1** also when the plan fails the structural half (the refusal lines are printed first);
|
|
43
|
+
**exit 2** usage — every operand is validated at parse: an unknown argument, an uncontained or
|
|
44
|
+
over-cap `--plan` (above), a malformed-UTF-8 plan (`malformed-utf8` — a genuine U+FFFD passes)
|
|
45
|
+
and a list the reader refuses by name (the tool's own installation, the plan-shape-facts precedent) are usage.
|
|
46
|
+
4. Where it belongs (procedures.md `plan-execution` step 2): the dispatch brief — delegated or
|
|
47
|
+
subagent — carries the generated block for every tagged row; the procedures advisor renders the
|
|
48
|
+
populated command in the plan-execution render. Tag a row by writing `robust:<class>` in its
|
|
49
|
+
responsibility sentence; `plan-shape --check` refuses a class id the list does not carry
|
|
50
|
+
(`robust-class`) and leaves an untagged row unjudged.
|
|
51
|
+
5. **`--coverage`** judges every valid `create`/`modify` row except `.md`/`.txt` documents and the shipped list itself. A class is PRESENT when the row file's bytes carry a bounded member token of kind env/flag/ref/syscall/errno/argv; `state` is never searched, `uncovered = present − tagged`, delete and absent rows are listed, and sweep rows union every path expanded by `plan-shape-facts`' one glob compiler. Reads use the same 1 MiB capped no-follow primitive. The table exits `0` when covered, `1` with each `row:class:literal` when uncovered, and `2` on a named row-read refusal. Bootstrap: for rows up to the row that lands this mode, apply the rule by hand; thereafter run coverage before every dispatch and before `flow-writer adoption`.
|
|
52
|
+
|
|
53
|
+
**Honest bounds (stated, accepted):** the tag is the author's claim — the tool cannot know that an
|
|
54
|
+
untagged row touches git, the reviewer still can; the list names classes and carries only MEASURED
|
|
55
|
+
members, so a fact of someone else's runtime that nobody has measured yet is not in it until a retro
|
|
56
|
+
adds it with its `source`; `source` is provenance of the measurement, not a path an installed kit
|
|
57
|
+
opens; a class with only `state` members is blind and can never be PRESENT. The block informs the
|
|
58
|
+
brief and never judges a diff — the hostile harness and the diff review stay the last line.
|
|
59
|
+
|
|
60
|
+
**Invariants:** read-only · never writes · never commits · spawns nothing · the plain run reads the plan file and the kit list; `--coverage` additionally reads every judged row file through the capped no-follow read and the repository's regular-file listing for sweep expansion — nothing else, still no git, no spawn and no write · exit 0 printed / 1 refused / 2 usage ·
|
|
61
|
+
`main(argv, deps)` returns, never exits.
|
|
@@ -36,6 +36,8 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
|
|
|
36
36
|
- `node ${CLAUDE_SKILL_DIR}/tools/repo-search.mjs --pattern <literal> --path <p>` (wildcard — the LITERAL search lane; for a pattern carrying a shell-significant byte use `--pattern-file <p>` instead, and for a TARGET PATH carrying one use `--paths-file <p>` — either way the bytes never enter the command string. one of the TWO tier tools that carry hook residual coverage — see the honesty note below)
|
|
37
37
|
- `node ${CLAUDE_SKILL_DIR}/tools/path-inventory.mjs --path <p>` (wildcard — the read-only inventory lane: exists / type / size / `wc -l` lines / one-level listing / small-file contents for N paths in ONE call, so small path questions never need a composed shell. `--paths-file <p>` is its out-of-band target lane; hook residual coverage as with repo-search)
|
|
38
38
|
- `node ${CLAUDE_SKILL_DIR}/tools/review-rounds-cli.mjs --artifact <plan>` (wildcard — the read-only round table over the review receipts, the verdict half of the per-round emission the `procedures` advisor names every round; the path argument is resolved and compared, never opened as a pattern, so it rides the settings-level posture like the rest)
|
|
39
|
+
- `node ${CLAUDE_SKILL_DIR}/tools/control-bytes.mjs --check` (wildcard — the read-only control-byte gate over the work tree: read-only git queries and descriptor-bound reads, no write API; the agent's own hand run before the declared gate runs it)
|
|
40
|
+
- `node ${CLAUDE_SKILL_DIR}/tools/robustness-brief.mjs --plan <plan>` (wildcard — the read-only executor-brief generator over a plan and the kit's shipped literal list; no write API and no process spawn)
|
|
39
41
|
- `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs --cwd ${PROJECT_ROOT}` — **EXACT byte-string only**, and honestly **project-exec, not read-only**: it runs YOUR declared `docs/ai/gates.json` commands — the same trust boundary the opt-in hook grants byte-exact per-cmd. A wildcard would be BROADER than that boundary (`--cwd <dir>` executes another project's declared gates), so the bare cwd-defaulting form, any other `--cwd`, `--only`, and **`--final`** forms all still prompt (`--final` WRITES the final-run receipt into the core-evidence store — a recording run is never auto-approved).
|
|
40
42
|
- `node ${CLAUDE_SKILL_DIR}/tools/source-size-check.mjs --check` — **EXACT byte-string only**, and only this READ-ONLY mode: the tool's `--write-baseline` and `--adopt` modes WRITE (the recorded ratchet, and a gate declaration) and are deliberately uncovered — they still prompt, always. Every other form, `--cwd` included, keeps prompting. **What it covers, precisely:** the agent's own direct invocation. It is **not** the byte-string a DECLARED gate carries — the fill emits that path double-quoted (a kit path with a space must survive), and a seedable allow rule may carry no quotes at all, so the two spellings cannot be one string; a declared gate is auto-approved by the opt-in hook instead, byte-exact against `docs/ai/gates.json`.
|
|
41
43
|
- Writer previews, **exact arg-free dry-run byte-strings only** (the SEEDED tier byte-string is the arg-free preview of each): `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs` — every `--apply`/`--write`/`--yes` still prompts, always. (`gate-hook` also has a **`--read-lane`** flagged preview — the opt-in read-only compound lane, `${CLAUDE_SKILL_DIR}/references/modes/hook.md`; that flagged form is NOT the seeded arg-free byte-string, so it may **prompt once** — it IS a consent flow, stated, no silent cap.)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"version": 1,
|
|
4
|
+
"classes": [
|
|
5
|
+
{
|
|
6
|
+
"id": "git-location",
|
|
7
|
+
"prove": "Prove that every git query reaches the intended repository: every GIT_-named variable is stripped before cwd discovery, the git-dir, common-dir and work-tree identities agree between the ambient and the stripped discovery, and the hermetic environment is the fixtures' own, never a consumer's.",
|
|
8
|
+
"members": [
|
|
9
|
+
{ "literal": "GIT_", "kind": "env", "note": "Strip every ASCII-case-insensitive git location variable before cwd discovery.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
10
|
+
{ "literal": "GIT_DIR", "kind": "env", "note": "An ambient git directory can redirect discovery to another repository.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
11
|
+
{ "literal": "GIT_WORK_TREE", "kind": "env", "note": "An ambient work-tree override can pair one git directory with another tree.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
12
|
+
{ "literal": "GIT_COMMON_DIR", "kind": "env", "note": "An ambient common directory can redirect shared repository state.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
13
|
+
{ "literal": "GIT_INDEX_FILE", "kind": "env", "note": "A pre-commit hook can name a temporary or copied index.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
14
|
+
{ "literal": "GIT_PREFIX", "kind": "env", "note": "A hook inherits the prefix git measured for its invocation.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
15
|
+
{ "literal": "GIT_CONFIG_PARAMETERS", "kind": "env", "note": "Command-line git configuration reaches hooks through this variable.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
16
|
+
{ "literal": "GIT_EXEC_PATH", "kind": "env", "note": "A hook-shaped environment carries git's executable path.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
17
|
+
{ "literal": "HOME", "kind": "env", "note": "Pin the home directory for hermetic git configuration.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
18
|
+
{ "literal": "XDG_CONFIG_HOME", "kind": "env", "note": "Pin the XDG configuration home beside HOME.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
19
|
+
{ "literal": "GIT_CONFIG_GLOBAL", "kind": "env", "note": "Disable the host's global git configuration file.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
20
|
+
{ "literal": "GIT_CONFIG_SYSTEM", "kind": "env", "note": "Disable the host's system git configuration file.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
21
|
+
{ "literal": "GIT_CONFIG_NOSYSTEM", "kind": "env", "note": "Tell git not to consult system configuration.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
22
|
+
{ "literal": "GIT_ATTR_NOSYSTEM", "kind": "env", "note": "Tell git not to consult system attributes.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
23
|
+
{ "literal": "LC_ALL", "kind": "env", "note": "Pin git's diagnostic language before matching its answers.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
24
|
+
{ "literal": "PATH", "kind": "env", "note": "A supplied PATH is honored and an omitted PATH borrows the process value.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
25
|
+
{ "literal": "--absolute-git-dir", "kind": "argv", "note": "Compare the ambient and stripped git-directory identities.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
26
|
+
{ "literal": "--git-common-dir", "kind": "argv", "note": "Compare the ambient and stripped common-directory identities.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
27
|
+
{ "literal": "--show-toplevel", "kind": "argv", "note": "Compare the ambient and stripped work-tree identities.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
28
|
+
{ "literal": "work-tree", "kind": "state", "note": "All three ambient and stripped identities agree.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
29
|
+
{ "literal": "not-a-repository", "kind": "state", "note": "Both discoveries report that cwd reaches no repository.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
30
|
+
{ "literal": "error", "kind": "state", "note": "A probe, process result, path identity or answer failed closed.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
31
|
+
{ "literal": "redirected", "kind": "state", "note": "Ambient and stripped discovery reach different repository identities.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
32
|
+
{ "literal": "no-work-tree", "kind": "state", "note": "The git directory agrees but names no work tree.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
33
|
+
{ "literal": "env-only", "kind": "state", "note": "Only the ambient git environment reaches a repository.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
34
|
+
{ "literal": "GIT_MAX_BUFFER", "kind": "state", "note": "Every declared non-test git spawn buffer under agent-workflow-kit/tools/ uses the 256 MiB GIT_MAX_BUFFER; git-env.mjs declares it and every other scanned use imports it.", "source": "agent-workflow-kit/tools/git-env.mjs" }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "git-operation-state",
|
|
39
|
+
"prove": "Prove that an interrupted or conflicted git operation cannot be mistaken for an ordinary clean work tree.",
|
|
40
|
+
"members": [
|
|
41
|
+
{ "literal": "rebase-apply", "kind": "state", "note": "A stopped git am leaves the apply-backend directory even when ordinary diffs are clean.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
42
|
+
{ "literal": "rebase-merge", "kind": "state", "note": "A stopped rebase on the merge backend leaves this directory and REBASE_HEAD while status names the conflicted path.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
43
|
+
{ "literal": "MERGE_HEAD", "kind": "ref", "note": "A merge in progress leaves this sequencer reference.", "source": "scripts/release/release-run.mjs" },
|
|
44
|
+
{ "literal": "CHERRY_PICK_HEAD", "kind": "ref", "note": "A cherry-pick in progress leaves this sequencer reference.", "source": "scripts/release/release-run.mjs" },
|
|
45
|
+
{ "literal": "REVERT_HEAD", "kind": "ref", "note": "A revert in progress leaves this sequencer reference.", "source": "scripts/release/release-run.mjs" },
|
|
46
|
+
{ "literal": "REBASE_HEAD", "kind": "ref", "note": "A rebase in progress leaves this sequencer reference.", "source": "scripts/release/release-run.mjs" },
|
|
47
|
+
{ "literal": "index.lock", "kind": "state", "note": "A held index lock blocks writes while read-only index queries still answer.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
48
|
+
{ "literal": "unmerged stage 1", "kind": "state", "note": "The conflicted path carries the common-ancestor index stage.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
49
|
+
{ "literal": "unmerged stage 2", "kind": "state", "note": "The conflicted path carries the current-side index stage.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
50
|
+
{ "literal": "unmerged stage 3", "kind": "state", "note": "The conflicted path carries the other-side index stage.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
51
|
+
{ "literal": "unmerged path twice", "kind": "state", "note": "Git 2.43 lists one conflicted path once per unmerged side; git am and bisect leave none of the four sequencer refs above.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "git-read-flags",
|
|
56
|
+
"prove": "Prove that host git configuration, attributes and output framing cannot hide bytes or paths from a read.",
|
|
57
|
+
"members": [
|
|
58
|
+
{ "literal": "status.showUntrackedFiles", "kind": "flag", "note": "Porcelain status can hide untracked files when this config is no.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
59
|
+
{ "literal": "core.autocrlf", "kind": "flag", "note": "Work-tree CRLF and blob LF can compare clean under this config.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
60
|
+
{ "literal": "core.symlinks", "kind": "flag", "note": "A host can materialize a tracked symlink as a placeholder regular file.", "source": "agent-workflow-kit/tools/control-bytes.mjs" },
|
|
61
|
+
{ "literal": "--no-textconv", "kind": "flag", "note": "Disable textconv so configured filters cannot erase staged content from a diff.", "source": "scripts/release/release-run.mjs" },
|
|
62
|
+
{ "literal": "--ignore-submodules=none", "kind": "flag", "note": "Override a host config that hides every submodule change.", "source": "scripts/release/release-run.mjs" },
|
|
63
|
+
{ "literal": "-z", "kind": "flag", "note": "Use NUL framing for path-bearing git output.", "source": "agent-workflow-kit/tools/control-bytes.mjs" },
|
|
64
|
+
{ "literal": "--exclude-standard", "kind": "flag", "note": "Apply repository ignore rules only where the query explicitly intends them.", "source": "agent-workflow-kit/tools/control-bytes.mjs" },
|
|
65
|
+
{ "literal": "%B", "kind": "flag", "note": "Raw subject and body output carries its terminating newline and needs explicit framing.", "source": "scripts/check-ascii-letters.mjs" },
|
|
66
|
+
{ "literal": "LC_ALL=C", "kind": "env", "note": "Pin git's own diagnostic wording before matching it.", "source": "agent-workflow-kit/tools/git-env.mjs" }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "spawn-outcome",
|
|
71
|
+
"prove": "Prove that every process-launch and process-result state is handled explicitly and never collapses into an empty success.",
|
|
72
|
+
"members": [
|
|
73
|
+
{ "literal": "ENOENT", "kind": "errno", "note": "The executable may be absent from PATH before a child starts.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
74
|
+
{ "literal": "EACCES", "kind": "errno", "note": "The executable or a required path may be present but inaccessible.", "source": "agent-workflow-kit/tools/fs-safe.mjs" },
|
|
75
|
+
{ "literal": "EPERM", "kind": "errno", "note": "A sandbox or host policy may refuse the attempted process or filesystem operation.", "source": "agent-workflow-kit/tools/fs-safe.mjs" },
|
|
76
|
+
{ "literal": "signal", "kind": "state", "note": "A child killed by a signal has no ordinary exit status.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
77
|
+
{ "literal": "synchronous throw", "kind": "state", "note": "An injected or native runner can throw before returning a process result.", "source": "agent-workflow-kit/tools/control-bytes.mjs" },
|
|
78
|
+
{ "literal": "exit 128", "kind": "state", "note": "Git's own not-a-repository or no-work-tree wording classifies only the matching exit 128 answer.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
79
|
+
{ "literal": "empty answer", "kind": "state", "note": "A zero exit with no identity text is an error, never a repository path.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
80
|
+
{ "literal": "maxBuffer", "kind": "state", "note": "A declared output ceiling is shared rather than silently truncated or copied.", "source": "agent-workflow-kit/tools/git-env.mjs" }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "durable-write",
|
|
85
|
+
"prove": "Prove that a durable write binds the intended regular file, publishes atomically and refuses an existing exclusive target.",
|
|
86
|
+
"members": [
|
|
87
|
+
{ "literal": "O_NOFOLLOW", "kind": "syscall", "note": "Refuse a symlink leaf at descriptor open.", "source": "agent-workflow-kit/tools/fs-read-nofollow.mjs" },
|
|
88
|
+
{ "literal": "O_NONBLOCK", "kind": "syscall", "note": "Prevent a hostile FIFO from blocking a descriptor open forever.", "source": "agent-workflow-kit/tools/fs-read-nofollow.mjs" },
|
|
89
|
+
{ "literal": "fsync", "kind": "syscall", "note": "Sync the temporary file before rename and the directory after publication where supported.", "source": "scripts/release/release-run.mjs" },
|
|
90
|
+
{ "literal": "rename", "kind": "syscall", "note": "Publish the fully written temporary file atomically after fsync.", "source": "scripts/release/release-run.mjs" },
|
|
91
|
+
{ "literal": "wx", "kind": "flag", "note": "Open a temporary file or lock with exclusive creation.", "source": "scripts/release/release-run.mjs" },
|
|
92
|
+
{ "literal": "EEXIST", "kind": "errno", "note": "An existing exclusive lock is a named busy state, never permission to clobber.", "source": "scripts/release/release-run.mjs" }
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "bytes-not-strings",
|
|
97
|
+
"prove": "Prove that byte-domain identities remain bytes until a deliberate lossless decoding boundary.",
|
|
98
|
+
"members": [
|
|
99
|
+
{ "literal": "undecoded", "kind": "state", "note": "Path records remain Buffer slices through NUL framing.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
100
|
+
{ "literal": "NUL", "kind": "state", "note": "Git path records are split on the byte delimiter rather than whitespace or lines.", "source": "agent-workflow-kit/tools/git-env.mjs" },
|
|
101
|
+
{ "literal": "partial UTF-8 sequence", "kind": "state", "note": "A file can be observed while its last multibyte sequence is incomplete.", "source": "agent-workflow-kit/tools/hostile-git-harness.test.mjs" },
|
|
102
|
+
{ "literal": "raw control byte", "kind": "state", "note": "A report line must never carry a raw line-breaking control character.", "source": "agent-workflow-kit/tools/repo-lex.mjs" },
|
|
103
|
+
{ "literal": "U+FFFD", "kind": "state", "note": "Lossy decoding can fold distinct invalid byte sequences to the replacement character.", "source": "agent-workflow-kit/tools/repo-lex.mjs" }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
8
|
// Structural pin (green by construction, a tripwire not a proof): all three archivers read
|
|
8
9
|
// documents through the ONE shared tokenizer. A fourth archiver — or a future edit that reaches
|
|
@@ -12,6 +13,50 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
13
|
|
|
13
14
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const ARCHIVERS = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-decisions.mjs'];
|
|
16
|
+
const ARCHIVER_CLOSURE = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-caps.mjs', 'markdown-blocks.mjs'];
|
|
17
|
+
const FIXED_DATE = '2026-09-02';
|
|
18
|
+
const MALFORMED_PACKAGE_JSON = '{ not json';
|
|
19
|
+
const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
|
|
20
|
+
const NAMED_FIXTURE = 'named-fixture';
|
|
21
|
+
|
|
22
|
+
const buildArchiveHeadings = async (packageJson) => {
|
|
23
|
+
const root = mkdtempSync(join(tmpdir(), 'archiver-name-'));
|
|
24
|
+
const scripts = join(root, 'scripts');
|
|
25
|
+
mkdirSync(scripts);
|
|
26
|
+
for (const name of ARCHIVER_CLOSURE) cpSync(join(DIR, name), join(scripts, name));
|
|
27
|
+
if (packageJson !== null) writeFileSync(join(root, 'package.json'), packageJson);
|
|
28
|
+
const changelog = await import(pathToFileURL(join(scripts, 'archive-changelog.mjs')).href);
|
|
29
|
+
const issues = await import(pathToFileURL(join(scripts, 'archive-issues.mjs')).href);
|
|
30
|
+
const result = {
|
|
31
|
+
fallback: basename(root),
|
|
32
|
+
changelog: changelog.buildRecent([], FIXED_DATE),
|
|
33
|
+
issues: issues.buildResolvedFile(null, [], FIXED_DATE),
|
|
34
|
+
};
|
|
35
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assertArchiveHeadings = (result, projectName) => {
|
|
40
|
+
assert.ok(result.changelog.includes(`# Changelog WARM Archive \u2014 ${projectName}`));
|
|
41
|
+
assert.ok(result.issues.includes(`# Resolved Issues \u2014 ${projectName}`));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('the archivers head their archives with the directory basename when no package.json, or a malformed one, names the project', () => {
|
|
45
|
+
it('no package.json names the directory basename', async () => {
|
|
46
|
+
const result = await buildArchiveHeadings(null);
|
|
47
|
+
assertArchiveHeadings(result, result.fallback);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('a malformed package.json names the directory basename', async () => {
|
|
51
|
+
const result = await buildArchiveHeadings(MALFORMED_PACKAGE_JSON);
|
|
52
|
+
assertArchiveHeadings(result, result.fallback);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('a named package.json wins', async () => {
|
|
56
|
+
const result = await buildArchiveHeadings(NAMED_PACKAGE_JSON);
|
|
57
|
+
assertArchiveHeadings(result, NAMED_FIXTURE);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
15
60
|
|
|
16
61
|
describe('every archiver reads through the tokenizer — no raw scan survives', () => {
|
|
17
62
|
for (const name of ARCHIVERS) {
|
|
@@ -8,12 +8,15 @@ import { describe, it } from 'node:test';
|
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
9
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, cpSync, existsSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join, dirname, resolve } from 'node:path';
|
|
11
|
+
import { basename, join, dirname, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
|
|
15
15
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const INSTALLER_SRC = join(HERE, 'install-git-hooks.mjs');
|
|
17
|
+
const MALFORMED_PACKAGE_JSON = '{ not json';
|
|
18
|
+
const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
|
|
19
|
+
const NAMED_FIXTURE = 'named-fixture';
|
|
17
20
|
|
|
18
21
|
// The installer anchors ROOT at <its dir>/.. — deploy it into a fixture's scripts/ like a consumer.
|
|
19
22
|
const mkProject = () => {
|
|
@@ -39,6 +42,33 @@ const hooksPathOf = (root) => {
|
|
|
39
42
|
return resolve(root, r.stdout.trim());
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
const markerFor = (name) => `# ${name}:install-git-hooks.mjs`;
|
|
46
|
+
const readHook = (root) => readFileSync(join(hooksPathOf(root), 'pre-commit'), 'utf8');
|
|
47
|
+
|
|
48
|
+
describe('install-git-hooks \u2014 beside no package.json, or a malformed one, the marker names the directory basename', () => {
|
|
49
|
+
it('no package.json, then a MALFORMED one \u2014 the marker names the directory basename', () => {
|
|
50
|
+
const { root } = mkProject();
|
|
51
|
+
const expected = markerFor(basename(root));
|
|
52
|
+
const absent = runInstaller(root);
|
|
53
|
+
assert.equal(absent.status, 0, absent.stderr);
|
|
54
|
+
assert.ok(readHook(root).split('\n').includes(expected));
|
|
55
|
+
writeFileSync(join(root, 'package.json'), MALFORMED_PACKAGE_JSON);
|
|
56
|
+
const malformed = runInstaller(root);
|
|
57
|
+
assert.equal(malformed.status, 0, malformed.stderr);
|
|
58
|
+
assert.ok(readHook(root).split('\n').includes(expected));
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('a named package.json wins \u2014 the marker carries its name', () => {
|
|
63
|
+
const { root } = mkProject();
|
|
64
|
+
writeFileSync(join(root, 'package.json'), NAMED_PACKAGE_JSON);
|
|
65
|
+
const result = runInstaller(root);
|
|
66
|
+
assert.equal(result.status, 0, result.stderr);
|
|
67
|
+
assert.ok(readHook(root).split('\n').includes(markerFor(NAMED_FIXTURE)));
|
|
68
|
+
rmSync(root, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
42
72
|
describe('install-git-hooks — the hooks path comes from git plumbing (C7)', () => {
|
|
43
73
|
it('a normal repo installs at the git-path hooks location', () => {
|
|
44
74
|
const { root } = mkProject();
|
|
@@ -9,7 +9,7 @@ The non-obvious traps — scan these before bootstrapping or upgrading. Each is
|
|
|
9
9
|
- **`CLAUDE.md` is a symlink, not a copy.** `ln -s AGENTS.md CLAUDE.md` — single source, no duplication. A copy drifts; a symlink can't.
|
|
10
10
|
- **Never overwrite an existing entry point or hook.** If `AGENTS.md` / `CLAUDE.md` already exist, or the installer reports a pre-existing non-marker git hook, **stop and ask** the user to merge vs replace — don't clobber.
|
|
11
11
|
- **Unrecognized invocations are read-only.** Only a **known** subcommand reaches its mode; the **bare** invocation bootstraps (and an existing `docs/ai/` makes it ask upgrade-vs-bootstrap first, never overwrite); **any other / ambiguous** token routes to `help` (read-only). A garbage invocation never writes. The mapping is pinned by `tools/commands.mjs` `routeInvocation` (unit-tested) — don't hand-route around it.
|
|
12
|
-
- **
|
|
12
|
+
- **Enforcement is offered on every project.** The dependency-free scripts are available because the agent host already runs the navigator; for the offer, the only skip condition is that the agent host cannot run them. The hook calls `node scripts/<x>.mjs`, so a committer without `node` on PATH gets the hook's own loud failure. **A skip names a PROVEN fact, never a proxy:** upgrade ensures judge Node by EVIDENCE — a root `package.json` OR any kit-seeded `scripts/*.mjs` already deployed — and the `skipped-no-node-evidence` line lists every probe that answered absent; a reason the tool could disprove is a tool defect. **A skip line that contradicts the observed tree is raised as a finding in the report, never pasted as neutral** — report the contradiction with the proven fact and do not build on the skip.
|
|
13
13
|
- **Conversational language never translates artifacts.** It governs *dialogue only*. Code, identifiers, paths, commands, log output, abbreviations, and every deployed `docs/ai/` / `AGENTS.md` file stay in their source language. See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract).
|
|
14
14
|
- **Never auto-commit.** Report quality-gate results and wait for explicit approval — in both modes.
|
|
15
15
|
- **Never leak kit internals to the user — and a tool-COMPOSED user-facing line holds the same bar at the source.** No ADR ids, tool / function / operation names (`reconcile`, `inject`, `ensureSlot`), marker / slot / fragment / anchor terminology, or verbatim tool stderr **inside the human sentence** of anything the user reads. Translate every tool outcome into plain language a third-party user — who has never read this `SKILL.md` — can understand and act on (e.g. the cap-refusal report in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3). The composed lines themselves are **user-grade** language: machine tokens and tool self-labels belong to the **machine-line channel** — the `[run-gates] status=…` grammar (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`), a line's leading self-label/prefix, or a runnable command/path the user can act on — never mid-sentence; alarm words (`PARTIALLY`, `incomplete`, `failed`, `broken`, `persists`) render only in outcomes gated on a **detected abnormal condition**. ONE designed exception, stated not implied: the configuration ensures' LEADING outcome token — one closed-vocabulary token, a failure's closed cause word opening its detail line — is that contract's own machine slot, not a leak. The **verbatim**-paste contract stays: the agent pastes tool-composed outcome lines as written and never re-composes their facts — the lines are user-grade at the source, so pasting them verbatim IS the plain language.
|
|
@@ -37,7 +37,7 @@ Read in order, then confirm before starting:
|
|
|
37
37
|
Before claiming "done":
|
|
38
38
|
1. Run all quality gates (lint + type-check + tests) — all green.
|
|
39
39
|
2. Update docs: `current_state.md` (feature ready), `changelog.md` (entry), `handover.md` (**REPLACE** the last-session block — session delta, never append; older deltas live in `changelog.md` → `history/`), `pages/<page>.md` (matches implementation), the governing spec (its revision lands with the code: `draft` -> `live` on the landing row). Only bump "Last Updated" when content actually changed.
|
|
40
|
-
3. Run the docs cap-validator + index-freshness gate (pre-commit also enforces). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
|
|
40
|
+
3. Run the docs cap-validator + index-freshness gate (the pre-commit hook also enforces it where installed). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
|
|
41
41
|
4. If the work executed a plan file — run that plan's final **Phase: Cleanup** (see the planning skill / §5). Without it the plan is not done.
|
|
42
42
|
5. **Ask before committing** (§4): report lint / type-check / test counts + docs status, then wait for explicit approval. DO NOT auto-commit.
|
|
43
43
|
|
|
@@ -20,7 +20,7 @@ maxLines: 700
|
|
|
20
20
|
**Changes:**
|
|
21
21
|
- Created `AGENTS.md` (entry point) + `CLAUDE.md` symlink.
|
|
22
22
|
- Created `docs/ai/` with the spec files + `pages/`.
|
|
23
|
-
-
|
|
23
|
+
- Offered the docs cap-validator / index-freshness / archive scripts + pre-commit hook (installed on a yes).
|
|
24
24
|
- Recorded the real stack, scripts, and architecture into the relevant files.
|
|
25
25
|
|
|
26
26
|
**Quality gates:** n/a (no code changes).
|
|
@@ -13,7 +13,7 @@ maxLines: 500
|
|
|
13
13
|
> consequences, newest at the bottom. Link related ADRs with `[[AD-XXX]]`; retrieval is by the
|
|
14
14
|
> `AD-NNN` id (filename), grep over the flat store, or the `[[AD-NNN]]` supersession chain.
|
|
15
15
|
|
|
16
|
-
> **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically; no cap is ever raised and there is no monolithic ledger.
|
|
16
|
+
> **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically where the enforcement scripts are deployed (installed on a yes); no cap is ever raised and there is no monolithic ledger.
|
|
17
17
|
|
|
18
18
|
## AD-001 — Adopt AI-agent memory system (`docs/ai/`)
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ maxLines: 500
|
|
|
22
22
|
|
|
23
23
|
**Context.** Multi-session AI work loses context between runs. Without a structured handover, each new session re-reads code, re-discovers decisions, and repeats past mistakes.
|
|
24
24
|
|
|
25
|
-
**Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via
|
|
25
|
+
**Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via the deployed scripts + pre-commit hook (offered on every project, installed on a yes). Deployed via the `agent-workflow-kit` skill.
|
|
26
26
|
|
|
27
27
|
**Rationale.** Single entry + structured spec files = constant boot-up cost regardless of project size. ADRs prevent litigating the same decision twice. `pages/<page>.md` keeps behaviour canonical (docs > assumptions). Caps + one-file-per-ADR archival keep every record scannable as the decision history grows without bound.
|
|
28
28
|
|
|
@@ -14,7 +14,7 @@ import { readFileSync, lstatSync } from 'node:fs';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
18
18
|
|
|
19
19
|
// ── the CLOSED changed-path classification rule (AD-046 Decision 5; no heuristics) ───────────────
|
|
20
20
|
|
package/tools/commands.mjs
CHANGED
|
@@ -224,6 +224,20 @@ const CATALOG = [
|
|
|
224
224
|
kind: READ_ONLY,
|
|
225
225
|
oneLine: 'Check that every configured review backend has receipted the current uncommitted tree with a fresh grounded review; --check turns it into a gate exit code.',
|
|
226
226
|
},
|
|
227
|
+
{
|
|
228
|
+
key: 'control-bytes',
|
|
229
|
+
invocation: invocationOf('control-bytes'),
|
|
230
|
+
group: 'Orchestrate',
|
|
231
|
+
kind: READ_ONLY,
|
|
232
|
+
oneLine: 'Refuse any raw control byte (C0 or DEL; TAB, LF, CR admitted) in the work tree — a path name, a regular file\'s content unless a git attribute says binary, a symlink target; --check is the gate spelling of the same report and exit code.',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: 'robustness-brief',
|
|
236
|
+
invocation: invocationOf('robustness-brief'),
|
|
237
|
+
group: 'Orchestrate',
|
|
238
|
+
kind: READ_ONLY,
|
|
239
|
+
oneLine: 'Generate the concrete robustness literals for every tagged row in a plan, or for one selected row.',
|
|
240
|
+
},
|
|
227
241
|
{
|
|
228
242
|
key: 'receipt-deadline',
|
|
229
243
|
invocation: invocationOf('receipt-deadline'),
|