@sabaiway/agent-workflow-kit 11.1.0 → 12.0.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 CHANGED
@@ -4,6 +4,89 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 12.0.0 — the reader mirror refuses an open-list clause, the outgoing reader becomes a prior, the consult line names the second case and the lens walks around every check (AD-133)
8
+
9
+ **MAJOR by the AD-117 class.** `references/scripts/spec-schema.mjs` and its test are the memory 8.0.0 bodies
10
+ byte-identically — `spec-check` relays the new `open-list` rule unchanged, so a consumer store valid under the old canon
11
+ can refuse under this kit; the remedy is the closing clause, never a fourth case. `tools/script-priors.mjs` and
12
+ `FROZEN_PRIORS` gain the outgoing reader pair as `5.0.0`..`7.2.1` with its fixture bodies under
13
+ `test/fixtures/script-priors/5.0.0/`, so a deployed 5.0.0–7.2.1 reader classifies `prior`, never `custom`.
14
+
15
+ **The consult clause and the lens bullet.** `tools/procedures.mjs` `CONSULT_LINE` — two source literals joined into
16
+ ONE rendered line, each under the 1000-byte cap — renders inside the agy `--focus` payload "If this finding is the
17
+ second case against the same check, reply with the replacement invariant, not an added case."; the codex consult-brief
18
+ and the lens re-dispatch forms say "with the same payload"; `tools/procedures.test.mjs` pins the payload order for both
19
+ activities. `references/agents/review-lens.md` gains the "How to review" bullet **When the artifact is a plan, walk
20
+ around every check** (the S12 arm in `cheap-agents-derived.test.mjs`; the placed copy takes the same line on refresh).
21
+ `tools/spec-coverage.test/cli.test.mjs` characterizes the S6 exit partition (`--help` / `-h` as the sole argument, an
22
+ unreadable store, a stray `--reason` beside `--check`, the debt-write failure as an exception).
23
+
24
+ ## 11.2.0 — a field report is triaged through a rendered procedure over a checked record (AD-132)
25
+
26
+ Three consumer field reports were triaged ad hoc — verify each claim on HEAD, record the verdict,
27
+ queue with a hand ratchet bump — with no record shape, no anchor check and nothing rendering the
28
+ steps. The answer is not a skill: a fourth activity, a record grammar a checker can refuse, and a
29
+ procedure the advisor renders. Contract: `docs/ai/specs/kit/feedback-triage.md` (live, S1–S15 bound).
30
+
31
+ **The fourth activity.** `ACTIVITIES` in `tools/carriers.mjs` gains `feedback-triage` with ONE slot,
32
+ `review`, and `policy: false` — a chore inside a session like `routine`; no `carrier` slot, no
33
+ autonomy block. `procedures feedback-triage` renders the engine's canon section verbatim (Record ·
34
+ Verify · Check · review {recipe} · Rows · Fold; Definition of Done: a checked record, its rows in the
35
+ queue, the ratchet moved by exactly the rows rendered). Every activity list names it (four mode docs name
36
+ it and `recommendations.md` carries the count, both READMEs, the engine SKILL and README, the methodology slot — the outgoing three-activity
37
+ fragment joins `KNOWN_PRIOR_METHODOLOGY_SLOT`); `set-recipe`, `recipes`, `status` and the registry
38
+ survey take the slot; the golden active-recipes line carries `feedback-triage.review = reviewed`
39
+ (computed default); the tarball pin counts 287.
40
+
41
+ **The record and its checker.** `docs/plans/FEEDBACK-<date>-<slug>.md` — scratch by the in-flight-plan
42
+ prefix — is one grammar, version 1: `# Feedback:` title, `Source:`, `Head: <object id>`, `## Claims`
43
+ and ONE pipe table (`#` · claim · backticked `path:line[-line]` anchors · `confirmed` / `corrected` /
44
+ `refuted` / `works-as-designed` · `queue <ROW-ID>` / `already-queued <ROW-ID>` / `declined: <reason>` /
45
+ `folded: <reason>`), optional `## Notes`. `tools/feedback-record.mjs` (the pure half: parse, judge,
46
+ render rows, render excerpts, the ratchet line — imports nothing) and `tools/feedback-record-cli.mjs`
47
+ (`--check <record>`, `--check <record> --excerpts <out>`, `--rows <record>`; `main(argv, deps)` never
48
+ exits). `--check` reads the record and every anchor no-follow and capped, realpath-contained under
49
+ the cwd, decodes fatally, compares `Head:` to `git rev-parse HEAD` under the git-location leaf with
50
+ `GIT_OPTIONAL_LOCKS=0`, then proves tracking POSITIVELY: each anchor's LEXICAL git-top-relative path
51
+ rides ONE `git status --porcelain -z` and ONE `git ls-files -v -z` as the pathspec `:(literal)<rel>`
52
+ — a rel the index does not list is `anchor-dirty` "not tracked at the stamped HEAD" (`.git/config`,
53
+ an untracked file, a path through a symlinked parent), a tag other than `H` is `anchor-dirty` behind
54
+ a concealing index bit, `H` with a status entry "differs", `H` and clean is proof. Every defect has
55
+ one name from a closed set (`title` · `source` · `head` · `table` · `row-cells` · `claim-id` ·
56
+ `anchor-grammar` · `anchor-path` · `verdict` · `disposition` · `anchor-absent` · `anchor-unreadable`
57
+ · `anchor-line` · `anchor-dirty` · `git-location` · `head-mismatch` · `excerpts-destination` ·
58
+ `excerpts-budget` · `excerpts-write` · `EEXIST`): a structural defect (title, source, head, table)
59
+ ends the read with its name, a row defect is listed per row and an anchor finding per anchor —
60
+ never the first only. Exit 0 accept · 1 findings · 2 usage. `--excerpts` writes the verbatim anchor lines — the `--facts` payload of
61
+ the record's agy review — under `grounding.mjs`'s scratch-destination guard with an exclusive create,
62
+ budgeted at `DEFAULT_MAX_PROMPT_BYTES` minus the record minus `EXCERPTS_FRAMING_RESERVE_BYTES` (4096),
63
+ each text split once, the walk stopped at the budget, tail-trimmed through `trimToBudget`; a text
64
+ that would not begin with an excerpt line is `excerpts-budget`, nothing written. `--rows` prints one
65
+ skeleton queue row per distinct `queue <ROW-ID>` and the `--max-rows` ratchet line read from the git
66
+ top's `docs/ai/gates.json`. Suites: `feedback-record.test.mjs`, `feedback-record-cli.test.mjs`,
67
+ `feedback-record-cli-anchors.test.mjs` (the S9 state table) over the shared
68
+ `feedback-record-cli-harness.test.mjs`.
69
+
70
+ **The advisor renders the record as the review artifact.** For `feedback-triage` the procedures
71
+ advisor suppresses the driving contracts and the grounding pre-step and renders, after the slot's
72
+ reason, the check line with `--excerpts`, then one line per review member — `codex-review plan
73
+ <record>`, `agy-review plan <record> --facts @<facts>`, a ready lens's brief over the record plus the
74
+ excerpts, a skipped line for a not-ready member — and `review-rounds-cli --artifact <record>
75
+ --activity feedback-triage` followed by the persisted-obligation fact whenever the slot carries a
76
+ bridge obligation (a lens-only roster gets the no-receipt fact); `<record>` and `<facts>` stay
77
+ literal. The JSON key is `feedbackTriage`; the lens survey seam is `ctx.surveyLens ?? ((spec) => …)`.
78
+
79
+ **The seed note.** The canonical `_README` names four activities and states the defaults honestly —
80
+ the seed keeps its two-activity shape, so `feedback-triage.review` is silent and "reviewed as soon as
81
+ a review backend is ready (solo until then)"; the outgoing four-activity note joins
82
+ `KNOWN_PRIOR_README` and both seed twins carry the canonical byte for byte.
83
+
84
+ **Recorded tech debt (queue).** The note's "every slot seeded below" clause is a claim about the file;
85
+ the plain-`solo` render of the activity; the status flags decide nothing after the positive proof;
86
+ the registry suite walks one direction only; the import-boundary pin's grammar; S1's wording; a
87
+ submodule anchor reads "not tracked"; `status` / Recommendations drop `r.roster`; the `--kit-tools`
88
+ tier cannot take the checker.
89
+
7
90
  ## 11.1.0 — the bootstrap offers enforcement on every project, and the advisor closes the loop (AD-131)
8
91
 
9
92
  Bootstrap steps 8–9 (the enforcement scripts + the pre-commit hook) used to decide by the step-1
package/README.md CHANGED
@@ -232,8 +232,8 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
232
232
  | `/agent-workflow-kit gates` | any time | **project gate runner** — runs the verification commands **your project itself declares** in `docs/ai/gates.json` (seeded at deploy; hand-editable `{ id, title, cmd }` entries plus the optional boolean `lcovProducer`, each `cmd` one bash line) as one batch: a per-gate **PASS/FAIL table** + one machine-readable summary line, exit 0 iff all green; a failing gate's own output is shown verbatim; `--only <id>` re-runs one. Honest distinct outcomes for a missing / empty / malformed declaration — never a silent green. The runner writes nothing **by default** and never commits — opt-in `--final` runs the FULL declared matrix and mints ONE final-run receipt into the git-dir core-evidence store (status green/red, pre/post tree fingerprints, the declaration, the consumed lcov's sha; the receipt the commit-guard binds); it executes only your own declared commands (a batching convenience, not a sandbox). The velocity tier auto-approves only the exact no-`--final` form — the recording run stays explicit. |
233
233
  | `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
234
234
  | `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
235
- | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (three activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
236
- | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
235
+ | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (four activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
236
+ | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine` / `feedback-triage`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same) — except `feedback-triage`, whose record-bound block carries the check with `--excerpts`, the resolved member lines (ready bridge commands, the ready lens brief, or explicit skipped members), and either the record round-table command for a bridge obligation or the no-receipt fact otherwise: the record is the artifact. `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
237
237
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
238
238
  | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). `--await [--timeout <s>]` (AD-049) BLOCKS until every recipe-named backend has receipted the current tree — the durable completion signal is the receipt, never a process event — so you wait for the bridges instead of hand-polling. Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
239
239
  | `/agent-workflow-kit sandbox-masks` | any time | **cosmetic exclude lane for sandbox device masks** — an OS sandbox (Claude Code) injects character-device masks into the work tree as untracked `git status` noise; the review domain already ignores them **by construction** (never-committable untracked classes — char/block devices, FIFOs, sockets — are excluded from the fingerprint, the assembled review payload, and the clean checks). This mode hides them from `git status` too: flagless = read-only probe (derives the CURRENT mask set from the unfiltered walk + lstat — never a frozen list — and revalidates fenced entries, loudly flagging one that became a real path); `--apply` = consent-gated FULL-BLOCK replace of its own fenced block in `git rev-parse --git-path info/exclude` (stale masks drop by construction; `--clear` always means REMOVE the block — it takes precedence over the derivation). Writes ONLY its fence — never `.gitignore`, never global config; symlinked/non-regular exclude paths and malformed fences fail closed. Watch note: a real file at an excluded path is silently skipped by bulk staging (`git add -A`/`git add .`) — delete the stale line first; the probe flags exactly this case. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '11.1.0'
6
+ version: '12.0.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "11.1.0",
6
+ "version": "12.0.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "11.1.0",
3
+ "version": "12.0.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -29,6 +29,7 @@ How to review:
29
29
  restatement of design intent back to the orchestrator.
30
30
  - **Respect what is already decided.** The prompt will name findings already folded and decisions
31
31
  already locked. Re-raising them is churn; checking whether the FIX is correct is real work.
32
+ - **When the artifact is a plan, walk around every check.** For every check bullet of each governing spec, name the invariant it proves and one state that walks around it; a named state is a finding with the replacement invariant, never an added case.
32
33
  - **Accuracy over volume.** A wrong finding costs the orchestrator more than a missed one — it
33
34
  spends a verification cycle and erodes trust in the whole list. If an angle turns up nothing, say
34
35
  so in one line and move on. Never pad.
@@ -4,11 +4,12 @@
4
4
 
5
5
  Read-only **activity-procedures advisor**. Answers *"what are the steps of this named activity, and which recipe applies at each slot here?"* It composes the orchestration recipes (`${CLAUDE_SKILL_DIR}/references/modes/recipes.md`) into **named activities** with **typed recipe slots**. It **never writes, never commits, never runs a subscription CLI** — the deterministic resolution lives in the kit; the orchestrator runs the resolved recipe via the bridge skills or the executor vehicle and **owns any commit when the activity has a commit boundary** (every other carrier never commits). Not every activity commits: `plan-authoring` ends at approval and produces no commit (plans are ephemeral, never committed); `plan-execution` commits per ledger row.
6
6
 
7
- The three activities (canon in the **installed engine**, `references/procedures.md`; the registry is `tools/carriers.mjs`):
7
+ The four activities (canon in the **installed engine**, `references/procedures.md`; the registry is `tools/carriers.mjs`):
8
8
 
9
9
  - **`plan-authoring`** (slots: `author`, `fold`, `review`) — research → draft (the resolved `author` carrier drafts: Solo writes it, Subagent drafts from the orchestrator's brief) → self-review with the readers sweep first → **review {recipe}** → consult, then fold/loop through the resolved `fold` carrier → present for approval; enforce the mandatory Cleanup.
10
10
  - **`plan-execution`** (slots: `execute`, `review`) — per ledger row: resolve the recipe → if Delegated, dispatch execution first; if Subagent, dispatch file-disjoint slices to the executor vehicle and verify each returned slice → implement → self-review → **review {recipe}** → gates → commit boundary; after the last row, the project-declared stages and Phase: Cleanup run as rows of their own.
11
11
  - **`routine`** (slots: `carrier`, `parallel`) — name the chore and its file-disjoint slices → resolve → carry it (Solo, or Subagent: a read-only slice rides a placed read-only vehicle, a write-capable one rides the executor; `parallel` on → concurrently) → verify every returned slice; the commit boundary is unchanged.
12
+ - **`feedback-triage`** (slot: `review`) — record → verify every claim by code on the stamped HEAD → `feedback-record-cli --check` → **review {recipe}** over the record, the excerpts as agy's `--facts` payload → `--rows` renders the queue rows and the ratchet line → fold only a false statement in shipped copy.
12
13
 
13
14
  Value sets per slot type: `review` accepts `solo | reviewed | council`; `execute` accepts `solo | delegated | subagent`; a carrier slot (`author`, `fold`, `carrier`) accepts `solo | subagent`; `parallel` is a **switch**, `on | off`, resolved outside the recipe lattice. Defaults when the config is silent: `review` → Reviewed if a review-capable backend is ready, else Solo; `author`, `fold`, `execute`, `carrier` → Solo (a placed executor never flips a default); `parallel` → on. The orchestrator owns every commit; every other carrier — a bridge or a subagent — never commits.
14
15
 
@@ -38,6 +39,8 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/procedures.mjs <activity> [--override <slo
38
39
 
39
40
  **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
41
 
42
+ **Record-bound review block (`feedback-triage` only).** In place of the driving contracts and the grounding pre-step the advisor renders the check-with-excerpts line, then one line per review member — a ready bridge's plan-mode command, a ready lens's brief (the record plus the excerpts), a skipped line for a not-ready member — and the round-table line while the slot carries a bridge obligation (a roster naming no bridge: the no-receipt fact in its place; a fully degraded reviewed or council slot keeps the line). The round-table line is followed by the standing fact that the table judges the obligation `review-rounds-cli` resolves from `docs/ai/orchestration.json`, never this run's `--override`. `--json` carries the identical lines under **`feedbackTriage`** (an empty array for the other activities).
43
+
41
44
  **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.
42
45
 
43
46
  **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.
@@ -16,7 +16,7 @@ The five recipes (defined over each bridge's `provides` roles — `codex`: execu
16
16
  2. **Offer the choice** via **`AskUserQuestion` where your agent supports it** (`AskUserQuestion` in Claude Code) — one option per recipe, the `recommendRecipe` choice listed **first** — otherwise in prose. Then print `planRecipe(chosen, detection)` (the per-stage dispatch + degradation reasons + quota/health notes) so the user sees exactly what running it entails.
17
17
  3. **Availability = `readiness === ready`, full stop.** Every other readiness supplies the human reason (needs-skill → "not installed — `/agent-workflow-kit setup`"; needs-cli → "install the CLI"; needs-credentials → "log in"; degraded → "wrapper not on PATH — `/agent-workflow-kit setup`"). This is set-up state only — **never** a claim that a backend's service is responsive (the detector cannot observe a runtime stall; `agy`'s Issue-001 is a *standing advisory*, not a readiness signal).
18
18
 
19
- **The activities and their slots.** One registry (`tools/carriers.mjs`, re-exported by `recipes.mjs`) names **three** activities and their typed slots: `plan-authoring` → `author`, `fold`, `review`; `plan-execution` → `execute`, `review`; `routine` → `carrier`, `parallel`. Each slot's accepted values follow its TYPE — `review` accepts `solo|reviewed|council`; `execute` accepts `solo|delegated|subagent`; `carrier` accepts `solo|subagent`. **`parallel` is a flag (`on|off`), not a recipe:** it resolves OUTSIDE the recipe lattice, never degrades, and never reaches the planner. A silent slot takes its **computed default** — `solo` for `author`, `fold`, `execute` and `carrier` (placing the executor vehicle never flips a default; the subagent carrier is opt-in exactly as `delegated` is), `reviewed`/`solo` by readiness for `review`, and `on` for `parallel`.
19
+ **The activities and their slots.** One registry (`tools/carriers.mjs`, re-exported by `recipes.mjs`) names **four** activities and their typed slots: `plan-authoring` → `author`, `fold`, `review`; `plan-execution` → `execute`, `review`; `routine` → `carrier`, `parallel`; `feedback-triage` → `review`. Each slot's accepted values follow its TYPE — `review` accepts `solo|reviewed|council`; `execute` accepts `solo|delegated|subagent`; `carrier` accepts `solo|subagent`. **`parallel` is a flag (`on|off`), not a recipe:** it resolves OUTSIDE the recipe lattice, never degrades, and never reaches the planner. A silent slot takes its **computed default** — `solo` for `author`, `fold`, `execute` and `carrier` (placing the executor vehicle never flips a default; the subagent carrier is opt-in exactly as `delegated` is), `reviewed`/`solo` by readiness for `review`, and `on` for `parallel`.
20
20
 
21
21
  **The configured-recipe line (`--active-line`, read-only).** `node ${CLAUDE_SKILL_DIR}/tools/recipes.mjs --active-line` prints exactly **one** machine-composed line: the **CONFIGURED** recipe of every activity/slot, resolved from the target project's `docs/ai/orchestration.json` (read from the current directory) + live readiness — each slot with its source (configured vs computed default), its degradation stated, and its dispatched wrapper set — explicitly contrasted with the readiness **recommendation** (which is informational; the configured recipes are what runs). Paste it verbatim: it fills the session-start discovery step (the deployed `agent_rules.md` §1.1) and the handover "Active recipes:" slot; `set-recipe` echoes the same line after every successful write. A malformed config fails loud (exit 1), never a silent fallback.
22
22
 
@@ -20,7 +20,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
20
20
  3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
21
21
  4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
22
22
 
23
- **The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the three activities. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it never overwrites a diverged file and STOPs on a symlink, so on `unusable` the item's apply line carries a `HAND-APPLY:` precondition (fix or remove the file, then run the writer) and an `unusable` vehicle converges only after the maintainer fixes or deletes the file the reason names (the delete-to-reseed the agents mode documents) and applies then. Risk profile: create-only writes under `.claude/agents/` — never `settings.json`, never a commit.
23
+ **The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the four activities. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it never overwrites a diverged file and STOPs on a symlink, so on `unusable` the item's apply line carries a `HAND-APPLY:` precondition (fix or remove the file, then run the writer) and an `unusable` vehicle converges only after the maintainer fixes or deletes the file the reason names (the delete-to-reseed the agents mode documents) and applies then. Risk profile: create-only writes under `.claude/agents/` — never `settings.json`, never a commit.
24
24
 
25
25
  **Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
26
26
 
@@ -21,13 +21,14 @@ mapping their wording onto them is your job, not a shipped phrase list.
21
21
 
22
22
  Run **`node ${CLAUDE_SKILL_DIR}/tools/set-recipe.mjs [--set <activity>.<slot>=<value>]… [--unset <activity>.<slot>]… [--write] [--json]`**:
23
23
 
24
- 1. **Grammar — always fully-qualified `<activity>.<slot>`** (the kit never guesses the activity; a bare `review=council` is rejected). The writer takes **every slot of the three activities**, and `--help` prints the registry (`tools/carriers.mjs`) as two blocks:
24
+ 1. **Grammar — always fully-qualified `<activity>.<slot>`** (the kit never guesses the activity; a bare `review=council` is rejected). The writer takes **every slot of the four activities**, and `--help` prints the registry (`tools/carriers.mjs`) as two blocks:
25
25
 
26
26
  ```
27
27
  Activities and their slots:
28
28
  plan-authoring → author, fold, review
29
29
  plan-execution → execute, review
30
30
  routine → carrier, parallel
31
+ feedback-triage → review
31
32
 
32
33
  Accepted values per slot type:
33
34
  review slots accept solo | reviewed | council
@@ -13,7 +13,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]
13
13
  > **Status reads `refresh`; the shared version block + the bootstrap/upgrade footers stay `notes`-based (unchanged this release).** `${CLAUDE_SKILL_DIR}/references/modes/status.md` has its OWN status-only render (above), keyed on `installed[].refresh.behind` / `refresh.recommend`. The shared **version block** (under *The version block + welcome mat* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and the bootstrap (step 11) + every upgrade (steps 4 / 8) report footer still consume `installed[].notes` verbatim — that wiring is deliberately **untouched** here (their migration onto `refresh` is deferred). Do not rewrite those footers onto `refresh`.
14
14
  2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`** or **`old-unrotated`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits) — the note is the SAME for both, they differ only in how the older layout was detected (`old` = a retired archive file is still on disk; `old-unrotated` = the project's deployed rotation script predates the store); `migrated` / `none` need no note. Then **one `specs` line, always** (from `project.specs` — the feature-spec layer's adoption state, judged from the store alone): `not adopted` (no `docs/ai/specs/` — the layer was never seeded), `adopting (N draft)` (a store with no live contract yet), `adopted (N live, M draft)`, or `could not be read — <reason>` (the store or a document in it could not be observed; never counted as any of the other three). A recorded decline (`project.specs.declined`) appends ` — declined` to the first two. The internal tokens are `not-adopted` / `adopting` / `adopted` / `unreadable`; render the plain phrases above, never the tokens. An envelope without the field says the installed kit predates it — say so rather than inventing a state.
15
15
  3. **Settings (`--dir`, one line each)** (from `project.settings`):
16
- - **recipes** — the effective recipe per slot, **one line per slot** over the three activities (`plan-authoring.author` / `.fold` / `.review`, `plan-execution.execute` / `.review`, `routine.carrier` / `.parallel`); each line also carries **where the value came from** (`source` — the project's config, an explicit override, or a computed default) and, when the resolved recipe is not the one that was asked for, **what it degraded from** (`degradedFrom`). Say both in plain words (never the field names): an effective recipe alone cannot tell a deliberate choice from a default, nor a degrade from a config that really names the resolved recipe — and a slot showing `solo` after a degrade from `subagent` is exactly the case the **subagents** line below explains. Detail → `/agent-workflow-kit procedures` / `recipes`; a `recipes.detectError` → say the backends couldn't be checked, so bridge-backed recipes floored at solo; the executor vehicle is unaffected.
16
+ - **recipes** — the effective recipe per slot, **one line per slot** over the four activities (`plan-authoring.author` / `.fold` / `.review`, `plan-execution.execute` / `.review`, `routine.carrier` / `.parallel`, `feedback-triage.review`); each line also carries **where the value came from** (`source` — the project's config, an explicit override, or a computed default) and, when the resolved recipe is not the one that was asked for, **what it degraded from** (`degradedFrom`). Say both in plain words (never the field names): an effective recipe alone cannot tell a deliberate choice from a default, nor a degrade from a config that really names the resolved recipe — and a slot showing `solo` after a degrade from `subagent` is exactly the case the **subagents** line below explains. Detail → `/agent-workflow-kit procedures` / `recipes`; a `recipes.detectError` → say the backends couldn't be checked, so bridge-backed recipes floored at solo; the executor vehicle is unaffected.
17
17
  - **attribution** — `includeCoAuthoredBy` effective; call out a **local override** only when `local` is non-null **and** differs from `project` (a `null` `local` means the key is absent there, so the project value stands — that is not an override).
18
18
  - **velocity** — the effective `permissions.defaultMode` + whether an allowlist is seeded (detail → `/agent-workflow-kit velocity`).
19
19
  - **subagents** — how many of the kit's subagent vehicles are placed (`agents.placed` of `agents.bundled`; zero placed → the optional `/agent-workflow-kit agents` opt-in), then the **split the line always states separately**: the **read-only vehicles** (`agents.readOnly` — four; none of them grants a shell, three ride a cheap model for mechanical work and one is a read-only review lens) and the **ONE full-tool `executor`**, whose own state is `agents.executor`: *placed* / *customized* (both dispatchable) / *unusable* (`agents.executorReason` carries the reason — say it) / *missing*. That state is what decides whether a slot configured `subagent` can ride it: on **unusable** or **missing** every such slot runs solo instead, so surface it plainly beside the recipes lines and point at `/agent-workflow-kit agents` to place it. It is a claim about the FILE, never about the host. An envelope with no executor state says the installed kit predates the field — say so rather than inventing a state.
@@ -1,13 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // spec-schema.mjs — the ONE reader that DEFINES a well-formed spec document under docs/ai/specs/.
3
- //
4
- // Pure text in, verdict out: readSpecDocument(text, rel) never touches the filesystem and imports
5
- // nothing, so it seeds layout-free into any deployment. The navigator's collapse (check-docs-size.mjs)
6
- // and the future spec-check read through THIS module — "malformed" has one definition, never two.
7
- // `rel` is the path INSIDE docs/ai/specs/ (the store root navigator is `index.md`).
8
- //
9
- // SPEC_SCHEMA carries the frozen values; the engine canon (references/specs.md) is pinned against it.
10
- // A refusal names exactly one rule id per defect, so a fixture corpus can be read rule by rule.
2
+ // Canon: the methodology engine's references/specs.md (frozen schema, refusals table).
11
3
 
12
4
  export const SPEC_SCHEMA = Object.freeze({
13
5
  storePrefix: 'docs/ai/specs/',
@@ -34,17 +26,25 @@ export const SPEC_SCHEMA = Object.freeze({
34
26
  rootOwnedKeys: Object.freeze(['status', 'revision']),
35
27
  rootOwnedSections: Object.freeze(['## Scenarios', '## Out of scope', '## Module', '## Parts']),
36
28
  scenarioGrammar: '- S<N> <name> :: <repo-relative test path> :: spec:<slug>/S<N> | - S<N> <name> :: unbound',
29
+ openList: Object.freeze({
30
+ checkVerbs: Object.freeze(['refuse', 'refusal', 'REFUSE', 'REFUSAL']),
31
+ enumerationForms: Object.freeze([Object.freeze([',', ' or ']), Object.freeze([' · '])]),
32
+ closureTokens: Object.freeze([
33
+ 'only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else',
34
+ 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed',
35
+ ]),
36
+ emphaticToken: 'ONLY',
37
+ }),
37
38
  rules: Object.freeze([
38
39
  'frontmatter', 'frontmatter-key', 'substrate-key', 'type', 'kind', 'maxlines', 'status', 'revision',
39
40
  'root-owns', 'slug', 'kind-path', 'root-uplink', 'title', 'section-missing', 'section-order',
40
- 'section-forbidden', 'fence', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
41
+ 'section-forbidden', 'fence', 'open-list', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
41
42
  'scenario-number', 'scenario-marker', 'scenario-path', 'scenarios-empty', 'out-of-scope', 'module-line', 'module-empty',
42
43
  'module-traversal', 'module-absolute', 'module-backslash', 'module-glob', 'module-mix', 'parts',
43
44
  ]),
44
45
  });
45
46
 
46
- // The descriptor check-docs-size.mjs joins to its ADR group: rows under `prefix` whose reader verdict
47
- // is clean collapse into ONE navigator row linking `navPath`; a row with reader errors stays visible.
47
+ // Consumer contract: scripts/check-docs-size.mjs.
48
48
  export const SPECS_COLLAPSE = Object.freeze({
49
49
  prefix: SPEC_SCHEMA.storePrefix,
50
50
  navPath: `${SPEC_SCHEMA.storePrefix}${SPEC_SCHEMA.navigatorFile}`,
@@ -72,8 +72,6 @@ const FORBIDDEN_SECTIONS = Object.freeze({
72
72
 
73
73
  const KNOWN_KEYS = Object.freeze([...SPEC_SCHEMA.substrateKeys, 'kind', ...SPEC_SCHEMA.rootOwnedKeys]);
74
74
 
75
- // The frontmatter is a closed key set: an unknown key, a repeated key or a line that is not
76
- // `key: value` is a defect (`frontmatter-key`), never silently dropped or last-one-wins.
77
75
  const parseFrontmatter = (text) => {
78
76
  const match = text.match(FRONTMATTER_RE);
79
77
  if (!match) return null;
@@ -91,10 +89,6 @@ const parseFrontmatter = (text) => {
91
89
 
92
90
  const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
93
91
 
94
- // The body as { title, sections: [{ heading, lines }], preamble, fenced } — the title is the FIRST
95
- // `# ` line, every `## ` line opens a section, the lines before the first section are the preamble.
96
- // The reader parses NO markdown code: a fence line is recorded (the `fence` refusal) rather than
97
- // modelled, so a spec carries no code sample and no line is ever ambiguous between code and structure.
98
92
  const parseBody = (body) => {
99
93
  const sections = [];
100
94
  const preamble = [];
@@ -116,8 +110,6 @@ const parseBody = (body) => {
116
110
  return { title: state.title, sections, preamble, fenced, sectionBeforeTitle: state.sectionBeforeTitle };
117
111
  };
118
112
 
119
- // Lexical classification of a repo-relative path field — the same vocabulary for module roots and
120
- // scenario bindings. Realpath/symlink containment needs the filesystem and is the checker's duty.
121
113
  export const classifyPath = (path) => {
122
114
  if (path.trim() === '') return 'empty';
123
115
  if (path.includes('\\')) return 'backslash';
@@ -139,8 +131,47 @@ const MODULE_PATH_RULES = Object.freeze({
139
131
  const isBullet = (line) => line === '-' || line.startsWith('- ');
140
132
  const bulletsOf = (lines) => lines.filter(isBullet).map((line) => line.slice(2));
141
133
  const contentOf = (lines) => lines.filter((line) => line.trim() !== '');
134
+ const isAtxHeading = (line) => /^ {0,3}#{1,6}(?:[ \t]|$)/.test(line);
135
+ const topLevelBulletsOf = (lines) => lines.reduce((state, raw) => {
136
+ const line = raw.replace(/\s+$/, '');
137
+ if (isBullet(line)) {
138
+ state.current = [line === '-' ? '' : line.slice(2)];
139
+ state.bullets.push(state.current);
140
+ } else if (line.trim() === '' || isAtxHeading(line)) state.current = null;
141
+ else if (state.current !== null) state.current.push(line.trim());
142
+ return state;
143
+ }, { bullets: [], current: null }).bullets.map((parts) => parts.join(' ').replace(/\s+/g, ' ').trim());
144
+
145
+ const CLAUSE_BREAK = String.fromCharCode(0);
146
+ const MASKED_DOT = String.fromCharCode(1);
147
+ const MASKED_SEMICOLON = String.fromCharCode(2);
148
+ const maskClauseSpans = (text) => [...text].reduce((state, character) => {
149
+ if (character === '`') state.inSpan = !state.inSpan;
150
+ state.output.push(state.inSpan && character === '.' ? MASKED_DOT : state.inSpan && character === ';' ? MASKED_SEMICOLON : character);
151
+ return state;
152
+ }, { inSpan: false, output: [] }).output.join('');
153
+ const clausesOf = (bullet) => maskClauseSpans(bullet).replace(/([.;][*`\)\]]*) /g, `$1${CLAUSE_BREAK}`).split(CLAUSE_BREAK)
154
+ .map((clause) => clause.split(MASKED_DOT).join('.').split(MASKED_SEMICOLON).join(';').trim()).filter(Boolean);
155
+ const containsInOrder = (text, tokens) => tokens.reduce((position, token) => {
156
+ const found = position === -1 ? -1 : text.indexOf(token, position);
157
+ return found === -1 ? -1 : found + token.length;
158
+ }, 0) !== -1;
159
+ const hasBoundedToken = (clause, token, flags) => new RegExp(`(^|[^A-Za-z0-9_-])${token}([^A-Za-z0-9_-]|$)`, flags).test(clause);
160
+ const checkOpenLists = (parsed, kind, errors) => {
161
+ const groups = kind === 'spec' ? parsed.sections.filter((section) => section.heading === '## Contract').map((section) => section.lines)
162
+ : kind === 'part' ? [parsed.preamble, ...parsed.sections.map((section) => section.lines)] : [];
163
+ for (const clause of groups.flatMap(topLevelBulletsOf).flatMap(clausesOf)) {
164
+ const hasVerb = SPEC_SCHEMA.openList.checkVerbs.some((verb) => clause.includes(verb));
165
+ const hasEnumeration = SPEC_SCHEMA.openList.enumerationForms.some((form) => containsInOrder(clause, form));
166
+ const hasClosure = SPEC_SCHEMA.openList.closureTokens.some((token) => hasBoundedToken(clause, token, 'i'))
167
+ || hasBoundedToken(clause, SPEC_SCHEMA.openList.emphaticToken, '');
168
+ if (hasVerb && hasEnumeration && !hasClosure) {
169
+ const prefix = clause.split(/\s+/).slice(0, 6).join(' ');
170
+ errors.push({ rule: 'open-list', message: `the clause "${prefix}…" is an open list — add a closing form to it such as "only if …; everything else …"` });
171
+ }
172
+ }
173
+ };
142
174
 
143
- // The slug a document owns: the file stem for a flat file, the folder name for an index.md.
144
175
  const describeRel = (rel) => {
145
176
  const segments = rel.split('/');
146
177
  const file = segments[segments.length - 1];
@@ -314,11 +345,7 @@ const checkParts = (parsed, at, errors) => {
314
345
  if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
315
346
  };
316
347
 
317
- // The structure verdict (additive, slice 2a): the DETERMINISTIC extraction of what parsed, per
318
- // section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
319
- // grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
320
- // module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
321
- // `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
348
+ // Structure verdict: the same canon, "The frozen schema".
322
349
  const scenarioEntry = (line) => {
323
350
  const scenario = parseScenario(line);
324
351
  if (scenario === null) return null;
@@ -345,10 +372,6 @@ const extractStructure = (parsed) => ({
345
372
  module: extractModule(parsed),
346
373
  });
347
374
 
348
- // The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
349
- // message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
350
- // missing or defective frontmatter and an unknown kind each end the read (structure stays null),
351
- // because no shape can be judged without them.
352
375
  export const readSpecDocument = (text, rel) => {
353
376
  const errors = [];
354
377
  const warnings = [];
@@ -372,6 +395,7 @@ export const readSpecDocument = (text, rel) => {
372
395
  const at = checkPath(rel, kind, errors);
373
396
  const parsed = parseBody(body);
374
397
  if (parsed.fenced.length > 0) errors.push({ rule: 'fence', message: `"${parsed.fenced[0]}" — a spec document carries no code fence` });
398
+ checkOpenLists(parsed, kind, errors);
375
399
  checkSections(parsed, kind, errors);
376
400
  if (kind === 'index') {
377
401
  checkChildren(parsed, errors);
@@ -32,6 +32,8 @@ const refuses = (text, rel, rule) => {
32
32
  const verdict = readSpecDocument(text, rel);
33
33
  expect(rulesOf(verdict)).toEqual([rule]);
34
34
  };
35
+ const withContract = (contract) => specDoc().replace('Accepts a credential pair.', contract);
36
+ const openListErrors = (contract, rel = 'login.md') => readSpecDocument(withContract(contract), rel).errors;
35
37
 
36
38
  describe('readSpecDocument — accept', () => {
37
39
  it('a flat draft spec reads clean with kind/status/revision', () => {
@@ -269,6 +271,69 @@ describe('readSpecDocument — the structure verdict (additive, slice 2a)', () =
269
271
  });
270
272
  });
271
273
 
274
+ describe('readSpecDocument — open-list clauses', () => {
275
+ const refusal = 'the clause "an anchor path reported modified, untracked…" is an open list — add a closing form to it such as "only if …; everything else …"';
276
+ const open = '- an anchor path reported modified, untracked or ignored is the named refusal `anchor-dirty`';
277
+ const closed = '- an anchor path is accepted only if `ls-files -v` lists it with tag `H` and `status` is silent — everything else, a path reported modified, untracked or ignored, is the named refusal `anchor-dirty`';
278
+
279
+ it('refuses the measured clause with the exact message; verb alone, form alone and an in-clause closure pass', () => {
280
+ expect(openListErrors(open)).toEqual([{ rule: 'open-list', message: refusal }]);
281
+ for (const text of [closed, '- refuse one case', '- alpha, beta or gamma']) expect(openListErrors(text)).toEqual([]);
282
+ });
283
+
284
+ it('joins continuation lines, collapses whitespace and keeps an indented bullet in its top-level bullet', () => {
285
+ for (const text of ['- refuse alpha,\n beta or ignored', '- refuse alpha,\n - beta or ignored']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
286
+ });
287
+
288
+ it('ends a bullet at a blank, the next top-level bullet or every ATX heading; Contract prose is ignored', () => {
289
+ for (const tail of ['\n\nEverything else closes', '\n- Everything else closes', ...Array.from({ length: 6 }, (_, i) => `\n${'#'.repeat(i + 1)} Everything else closes`)]) {
290
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
291
+ }
292
+ expect(openListErrors('refuse alpha, beta or gamma')).toEqual([]);
293
+ });
294
+
295
+ it('ends a bullet at indented and tab-separated ATX headings', () => {
296
+ for (const tail of ['\n ## Everything else closes', '\n#\tEverything else closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
297
+ });
298
+
299
+ it('starts a new empty top-level bullet before following prose', () => {
300
+ for (const tail of ['\n- \nEverything else closes', '\n-\nonly if closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
301
+ });
302
+
303
+ it('judges an open list in every Contract section', () => {
304
+ const duplicate = specDoc().replace('## Scenarios', '## Contract\n\n- refuse alpha, beta or gamma\n\n## Scenarios');
305
+ expect(rulesOf(readSpecDocument(duplicate, 'login.md')).includes('open-list')).toBe(true);
306
+ });
307
+
308
+ it('splits clauses at punctuation plus markdown tails, but not inside backtick spans', () => {
309
+ for (const tail of ['.** ', '.` ', '.) ', '.] ', '; ']) expect(rulesOf(readSpecDocument(withContract(`- refuse alpha, beta or gamma${tail}Everything else closes`), 'login.md'))).toEqual(['open-list']);
310
+ for (const text of ['- refuse `alpha. beta`, gamma or delta', '- refuse `alpha; beta`, gamma or delta', '- refuse `alpha. beta, gamma or delta']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
311
+ expect(openListErrors('- refuse alpha``. beta, gamma or delta')).toEqual([]);
312
+ });
313
+
314
+ it('keeps span content in judgement, and a closure in a neighbouring clause cannot lift a refusal', () => {
315
+ expect(rulesOf(readSpecDocument(withContract('- `refuse` alpha, beta or gamma'), 'login.md'))).toEqual(['open-list']);
316
+ expect(openListErrors('- refuse alpha, beta or gamma with `everything else`')).toEqual([]);
317
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma. Everything else closes'), 'login.md'))).toEqual(['open-list']);
318
+ });
319
+
320
+ it('matches verbs, forms and closure tokens with the frozen case and boundary rules', () => {
321
+ for (const verb of ['refuse', 'refuses', 'refused', 'refusal', 'refusals', 'REFUSE', 'REFUSAL']) expect(rulesOf(readSpecDocument(withContract(`- ${verb} alpha, beta or gamma`), 'login.md'))).toEqual(['open-list']);
322
+ for (const text of ['- Refuse alpha, beta or gamma', '- refuse alpha, beta and gamma']) expect(openListErrors(text)).toEqual([]);
323
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha · beta'), 'login.md'))).toEqual(['open-list']);
324
+ for (const token of SPEC_SCHEMA.openList.closureTokens) expect(openListErrors(`- ${token.toUpperCase()}: refuse alpha, beta or gamma`)).toEqual([]);
325
+ for (const text of ['- refuse alpha, beta or gamma — none of these', '- refuse alpha, beta or gamma — closed-key', '- refuse alpha, beta or gamma — READ-ONLY', '- refuse alpha, beta or gamma — if only this', '- refuse alpha, beta or gamma — not only this']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
326
+ expect(openListErrors('- refuse alpha, beta or gamma — ONLY this')).toEqual([]);
327
+ });
328
+
329
+ it('judges every part bullet, never an index, and freezes every schema list and inner pair', () => {
330
+ expect(rulesOf(readSpecDocument(partDoc({ extra: `\n${open}\n` }), 'auth/login/sessions.md'))).toEqual(['open-list']);
331
+ expect(readSpecDocument(indexDoc({ children: [], preamble: `\n${open}\n` }), 'auth/index.md').errors).toEqual([]);
332
+ expect(SPEC_SCHEMA.openList).toEqual({ checkVerbs: ['refuse', 'refusal', 'REFUSE', 'REFUSAL'], enumerationForms: [[',', ' or '], [' · ']], closureTokens: ['only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else', 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed'], emphaticToken: 'ONLY' });
333
+ expect([SPEC_SCHEMA.openList, SPEC_SCHEMA.openList.checkVerbs, SPEC_SCHEMA.openList.enumerationForms, ...SPEC_SCHEMA.openList.enumerationForms, SPEC_SCHEMA.openList.closureTokens].every(Object.isFrozen)).toBe(true);
334
+ });
335
+ });
336
+
272
337
  describe('classifyPath + the frozen constants', () => {
273
338
  it('classifies the lexical path forms', () => {
274
339
  expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
@@ -1,5 +1,5 @@
1
1
  {
2
- "_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Three activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review) and 'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize), or an explicit roster array such as [\"codex-review\", \"agy-review\", \"review-lens\"] in hand-edit form; an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is 'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, fold, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
2
+ "_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Four activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review), 'routine' (slots carrier, parallel) and 'feedback-triage' (slot review). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize), or an explicit roster array such as [\"codex-review\", \"agy-review\", \"review-lens\"] in hand-edit form; an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. Every slot seeded below is 'solo' — no execution backend required. A slot the seed leaves silent takes the computed default stated further down: 'feedback-triage.review' is reviewed as soon as a review backend is ready (solo until then); 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' stay 'solo', and 'routine.parallel' stays 'on'. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, fold, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
3
3
  "plan-authoring": {
4
4
  "review": "solo"
5
5
  },
@@ -15,6 +15,7 @@ export const ACTIVITIES = {
15
15
  'plan-authoring': { slots: { author: 'carrier', fold: 'carrier', review: 'review' }, policy: true },
16
16
  'plan-execution': { slots: { execute: 'execute', review: 'review' }, policy: true },
17
17
  routine: { slots: { carrier: 'carrier', parallel: 'switch' }, policy: false },
18
+ 'feedback-triage': { slots: { review: 'review' }, policy: false },
18
19
  };
19
20
 
20
21
  export const POLICY_ACTIVITIES = Object.fromEntries(