@naswerks/engineering-loop 0.7.1 → 0.9.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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +199 -0
  4. package/README.md +12 -7
  5. package/bin/verify-staged.mjs +136 -23
  6. package/bin/witness-acts.mjs +64 -0
  7. package/bin/witness-bodycheck.mjs +78 -0
  8. package/bin/witness-edits.mjs +60 -0
  9. package/bin/witness-extract-cmd.mjs +57 -0
  10. package/bin/witness-extract-seed.mjs +65 -0
  11. package/bin/witness-file-ledger.mjs +57 -0
  12. package/bin/witness-gaps.mjs +57 -0
  13. package/bin/witness-latency.mjs +55 -0
  14. package/bin/witness-lib.mjs +124 -0
  15. package/bin/witness-plane-window.mjs +57 -0
  16. package/bin/witness-procs.mjs +71 -0
  17. package/bin/witness-spawn-task.mjs +61 -0
  18. package/bin/witness-tx.mjs +60 -0
  19. package/bin/witness-watch.mjs +405 -0
  20. package/bin/witness-writes.mjs +64 -0
  21. package/package.json +1 -1
  22. package/references/docs-workflow.md +44 -24
  23. package/references/engineering-loop.md +42 -24
  24. package/skills/docs-audit-full/SKILL.md +2 -1
  25. package/skills/docs-process/SKILL.md +13 -7
  26. package/skills/docs-write/SKILL.md +1 -1
  27. package/skills/init/scripts/doctor.mjs +43 -5
  28. package/skills/init/scripts/owned.mjs +10 -0
  29. package/skills/spec-build/SKILL.md +12 -4
  30. package/skills/spec-coordinator/SKILL.md +149 -80
  31. package/skills/spec-ignite/SKILL.md +25 -21
  32. package/skills/spec-pipeline/SKILL.md +151 -82
  33. package/skills/spec-retro/SKILL.md +14 -9
  34. package/skills/spec-review/SKILL.md +32 -14
  35. package/skills/spec-seat/SKILL.md +107 -48
  36. package/skills/spec-witness/SKILL.md +267 -212
  37. package/templates/docs/readme-research.md +2 -2
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // When a session first wrote into the source folders, set against the plane verbs it called: the
3
+ // plan-before-build tripwire is a seat's first source write against its landed `accept_plan`. Caveats a
4
+ // ledger names with the result: a write made through a shell copy is not seen here (only the edit tools
5
+ // are), and a build's side effect can dirty a source folder with no seat edit at all. Read-only.
6
+ //
7
+ // node witness-writes.mjs <file.jsonl> <worktree-folder-name> [--source-dirs src,tests] [--plane <server>] [--config <path>]
8
+ // node witness-writes.mjs --self-test
9
+
10
+ import { rmSync } from 'node:fs';
11
+ import { contentOf, entries, flag, isMain, loadConfig, planePrefix, positionals, report, scratch, toolUse, writeTranscript } from './witness-lib.mjs';
12
+
13
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+
15
+ export async function writes(file, key, sourceDirs, prefix) {
16
+ const realTree = new RegExp(`${escape(key)}[\\\\/]+(${sourceDirs.map(escape).join('|')})[\\\\/]`);
17
+ let first = null;
18
+ let last = null;
19
+ let count = 0;
20
+ const plane = [];
21
+ for await (const { line, entry } of entries(file)) {
22
+ for (const c of contentOf(entry)) {
23
+ if (c.type !== 'tool_use') continue;
24
+ if (typeof c.name === 'string' && c.name.startsWith(prefix)) plane.push(`${line} ${entry.timestamp} ${c.name.slice(prefix.length)} ${JSON.stringify(c.input).slice(0, 120)}`);
25
+ const path = String(c.input?.file_path ?? c.input?.notebook_path ?? '');
26
+ if (!/^(Write|Edit|MultiEdit|NotebookEdit)$/.test(c.name) || !realTree.test(path)) continue;
27
+ count++;
28
+ first ??= `${line} ${entry.timestamp} ${c.name} ${path}`;
29
+ last = `${line} ${entry.timestamp}`;
30
+ }
31
+ }
32
+ return [`first source write: ${first}`, `source writes: ${count}, last: ${last}`, 'plane calls:', ...plane];
33
+ }
34
+
35
+ async function selfTest() {
36
+ const dir = scratch('witness-writes');
37
+ try {
38
+ const file = writeTranscript(dir, 'seat.jsonl', [
39
+ toolUse(1, 'a', 'Write', { file_path: 'C:/work/pipeline-7/docs/notes.md' }),
40
+ toolUse(2, 'b', 'mcp__plane__post_plan', { path: 'plan.md' }),
41
+ toolUse(3, 'c', 'Bash', { command: 'cp draft.ts C:/work/pipeline-7/src/a.ts' }),
42
+ toolUse(4, 'd', 'Edit', { file_path: 'C:\\work\\pipeline-7\\src\\a.ts' }),
43
+ toolUse(5, 'e', 'Write', { file_path: 'C:/work/pipeline-7/tests/a.test.ts' }),
44
+ ]);
45
+ const lines = await writes(file, 'pipeline-7', ['src', 'tests'], 'mcp__plane__');
46
+ return report('witness-writes', [
47
+ { name: 'the first source write is the edit into src', ok: lines[0].startsWith('first source write: 4 ') && lines[0].includes('Edit'), detail: lines[0] },
48
+ { name: 'both source writes are counted, and the plane call is listed', ok: lines[1].startsWith('source writes: 2') && lines.some((l) => l.includes('post_plan')), detail: lines.join(' | ') },
49
+ { name: 'CONTROL: a docs write and a shell copy are not source writes', ok: !lines[0].includes('notes.md') && !lines[0].includes('cp '), detail: lines[0] },
50
+ ]);
51
+ } finally {
52
+ rmSync(dir, { recursive: true, force: true });
53
+ }
54
+ }
55
+
56
+ if (isMain(import.meta.url)) {
57
+ const argv = process.argv.slice(2);
58
+ if (argv.includes('--self-test')) process.exit(await selfTest());
59
+ const config = loadConfig(argv);
60
+ const [file, key] = positionals(argv, ['--plane', '--config', '--source-dirs']);
61
+ if (!file || !key) { console.log('usage: witness-writes.mjs <file.jsonl> <worktree-folder-name> [--source-dirs src,tests] [--plane <server>]'); process.exit(2); }
62
+ const sourceDirs = (flag(argv, '--source-dirs') ?? config.sourceDirs.join(',')).split(',').filter(Boolean);
63
+ for (const line of await writes(file, key, sourceDirs, planePrefix(argv, config))) console.log(line);
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naswerks/engineering-loop",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "The engineering loop as a Claude Code plugin: a knowledge loop (docs-*), an execution loop (spec-*), and init for a fresh repository. The package root is the plugin root.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- <!-- naswerks-loop: version=0.7.1 -->
1
+ <!-- naswerks-loop: version=0.9.0 -->
2
2
  # Docs Workflow
3
3
 
4
4
  > How knowledge flows through `docs/`: the loop, the folder rules, the templates, and the `docs-*` skills.
@@ -41,7 +41,7 @@ narrative. That's also why it's a separate session, not the tail of the implemen
41
41
  | Folder | What it holds | Lifecycle |
42
42
  |--------|---------------|-----------|
43
43
  | `_meta/` | This system — `doc-index` (the menu) + `docs-workflow` (this file) + `engineering-loop` (the seats and the ceremony) | Maintained as the system evolves; the sections marked **(repository-owned)** are rewritten by each repository to its own conventions |
44
- | `research/` | The running tab of ideas to do, not yet built. The folder is the queue. | Created by `docs-backlog`; stays through the build, then **archived with its working doc(s)** by `docs-process` |
44
+ | `research/` | The running tab of ideas to do, not yet built. The folder is the queue. | Created by `docs-backlog`; stays through the build, then **archived with its working doc(s)** by `docs-process` — or, when the platform moves past it unbuilt, **retired** to `archive/retired/` |
45
45
  | `working/` | Active + finished-but-unprocessed session docs (one effort may chain several). The folder is the queue. | **Leaves via `docs-process`** — research + the whole chain archived together once the living docs are patched |
46
46
  | `archive/` | Completed/abandoned session docs, grouped into **topic** folders (or the home "The archive convention" below names for them) | Write-once. Frozen history — never edit archived docs |
47
47
  | `guides/` | How to ADD a new thing — step-by-step walkthroughs | Living — patched by `docs-process` |
@@ -83,7 +83,7 @@ that replaces the section rewrites alongside it):
83
83
 
84
84
  | Section | What the skills read from it | Also echoed in |
85
85
  |---------|------------------------------|----------------|
86
- | **The archive convention** (this file) | the archive home for each kind of effort (hand-cranked, spec pipeline, follow-on round) and for audit reports · the shape inside it · what a working doc's `## Archive` line says, and whether an umbrella carries an `Archive:` line · the links a move repairs · what is frozen once filed | The Loop (the diagram and its prose) · Folders (`research/`, `working/`, `archive/`) · "Living docs vs session docs" · the skills table (`docs-process`) · Conventions (incident receipts, archived docs) · `engineering-loop.md` "The loop at a glance" |
86
+ | **The archive convention** (this file) | the archive home for each kind of effort (hand-cranked, spec pipeline, follow-on round) and for audit reports · the shape inside it · what a working doc's `## Archive` line says, and whether a pipeline's ignition brief carries an `Archive:` line · the links a move repairs · what is frozen once filed | The Loop (the diagram and its prose) · Folders (`research/`, `working/`, `archive/`) · "Living docs vs session docs" · the skills table (`docs-process`) · Conventions (incident receipts, archived docs) · `engineering-loop.md` "The loop at a glance" |
87
87
  | **Doc metadata** (this file) | the header — its form and its place · the sweep: one command, one line per doc · the word for each loop state (below) · where the verified date lives, and the rule that moves it · whether `## Lineage` stays · the gates a doc passes, and the folders they exempt | the skills table (`docs-status`) · Conventions (the machine layer; dates in the header) · `engineering-loop.md`'s own header line and its `## Lineage` |
88
88
  | **Templates** (this file) | each template's header lines, at the state its writer names | — |
89
89
  | **This repository's rails** (`engineering-loop.md`) | the policy document, and how a seat reaches it · the gates a commit passes · the suite before a PR · the base branch, the PR target, the branch names and how a PR is opened · the staging rule · the repository's own skills a seat meets | the POLICY row of "Where a rule lives" · the Hard rules, which the rails add to and never subtract from |
@@ -142,7 +142,7 @@ Keep what the skills need to find, change the words to yours:
142
142
  re-sorted, every file keeps its name and its place, `sessions/` rides along untouched, and the effort's
143
143
  working docs join the package under `implementation/`, with the tracker still the "read me first" at its
144
144
  root. When where a pipeline files depends on something only its authoring session knows, say so —
145
- `spec-pipeline` then records the resolved path in the umbrella's blockquote as an `Archive:` line.
145
+ `spec-pipeline` then records the resolved path in the brief's `## Where it came from` as an `Archive:` line.
146
146
  - **Your rails.** Fill each item of `engineering-loop.md` § This repository's rails from what the
147
147
  repository already wrote down — a policy document, hooks, CI, a pull-request template.
148
148
  - **Your menu.** A repository that already indexes its docs declares its menu curated and names that index:
@@ -196,17 +196,20 @@ one** (via `spec-pipeline`). The trigger for the layout is simply *"does this ef
196
196
  ```
197
197
  archive/{topic}/
198
198
  project-status.md <- the tracker/index — root, "read me first" (spans both halves)
199
- live-checks.md <- the human's verification rows, accumulated as the pipeline ran
200
- specs/ <- the spec pipeline: 00-ignition-brief · 01 umbrella · 02..NN slices
199
+ live-checks.md <- the rows the owner dictated, if any
200
+ specs/ <- the spec pipeline: 00-ignition-brief · 01..NN slices
201
201
  implementation/ <- the per-session working docs that built it
202
202
  sessions/ <- THE PIPELINE'S CONVERSATION, archived intact:
203
203
  {spec-slug}/plan.md + {spec-slug}/tldr.md per build session
204
204
  (TWO files — questions ride the PLAN TLDR, and there are no
205
205
  status snapshots, because the tracker is committed live and
206
206
  its git history carries every revision)
207
+ docs-process/plan.md + tldr.md the docs-process row's board
208
+ {step-slug}/receipt.md a human step's receipt
207
209
  code-review.md the review seat's outside look, if
208
210
  the pipeline had a review row
209
211
  coordinator-retrospective.md the coordinator's inside look
212
+ witness-ledger-{date}.md a witness ledger, filed on the human's word
210
213
  These are TESTIMONY: an archive pass repairs links elsewhere and
211
214
  leaves sessions/** byte-untouched. Older archived sessions may
212
215
  still hold questions.md / status-after-*.md; that is historical
@@ -218,7 +221,7 @@ already-archived effort gets its **own sibling topic folder**, never filed insid
218
221
  Rules: (a) name the new topic `{family-prefix}-{subtopic}` so the family groups
219
222
  alphabetically; (b) each folder keeps the full split shape (own `project-status.md` at root); (c) the
220
223
  **family index is the living feature doc's `## Lineage`** — it accumulates Idea/Spec/Built rows from
221
- every round; (d) the new pipeline's umbrella/tracker back-link to the prior round's archive (allowed —
224
+ every round; (d) the new pipeline's brief/tracker back-link to the prior round's archive (allowed —
222
225
  cross-effort links into archives are historical record). Never reopen a frozen topic folder to add
223
226
  a round.
224
227
 
@@ -227,12 +230,29 @@ a round.
227
230
  working docs' `Source:` to the spec's new path (`../specs/NN-*.md`), the spec / `project-status` links, the
228
231
  index's links into `specs/`, and the feature doc's `## Lineage` rows (`Spec` to `specs/…`,
229
232
  `Built` to `implementation/…`). **Pre-existing cross-effort links** in archived docs (to *other* topic
230
- folders / research chains) are **left as historical record** — don't chase those.
231
-
232
- **`archive/audit/` is the one exception to topic-folder grouping.** Audit reports mirror the *living-doc
233
- groups* instead — `archive/audit/{guides,patterns,infrastructure,features}/{doc}-audit.md` — so each
234
- report sits beside the docs it audits. The cross-cutting `health-report-{date}.md` (from
235
- `docs-audit-full`) stays at the `audit/` root.
233
+ folders / research chains) are **left as historical record** — don't chase those. The one exception is
234
+ provenance: when a doc moves, every link to it from another archived doc's header blockquote (`Source:`,
235
+ `Builds on`) or `## Lineage` footer, or from a line marked `Idea` / `Spec` / `Built` / `Source:`, is repointed,
236
+ `sessions/**` excepted, because that is how a reader replays an effort.
237
+
238
+ **Two homes are the exceptions to topic-folder grouping.**
239
+
240
+ - **`archive/audit/`** — audit reports mirror the *living-doc groups* instead —
241
+ `archive/audit/{guides,patterns,infrastructure,features}/{doc}-audit.md` — so each report sits beside the
242
+ docs it audits. The cross-cutting `health-report-{date}.md` (from `docs-audit-full`) stays at the `audit/`
243
+ root.
244
+ - **`archive/retired/`** — research the platform moves past without building it (superseded by a different
245
+ design, its product retired, or the idea dropped), laid out exactly as it stood in `research/`: a folder
246
+ stays a folder, a loose doc stays loose, names and headers unchanged, and no link inside it is repaired.
247
+ Before it moves, its still-good ideas go one line each into `research/misc/idea-scratchpad.md`, its
248
+ code-verified open defects become `## Open Issues` rows in the living doc that owns the code, and a row in
249
+ `archive/retired/README.md` says why it was retired and where its live parts went. Links into it from
250
+ living docs and kept research are repaired. Research that shipped is filed with the effort that delivered
251
+ it; when that effort is already filed, the doc goes into the effort's home (a split effort's `research/`),
252
+ which completes the effort's own filing and is not a new round. Research that fed several efforts rather
253
+ than one goes on its theme's shelf (`archive/infrastructure/`, say). A running tab, or a research doc that
254
+ spanned several efforts, whose shipped parts are already filed with their own efforts retires with its unbuilt remainder; those efforts'
255
+ provenance links then point at it on the shelf.
236
256
 
237
257
  ---
238
258
 
@@ -301,7 +321,7 @@ links become the `Related` row:
301
321
  - **Real lineage only.** Wire a row only where the idea/implementation origin is *genuine* — never on
302
322
  coincidental topic overlap (junk links would pollute the graph's communities).
303
323
  - Most `patterns/`/`guides/` docs carry only `Related` (maybe `Built`); `features/` carry the full chain.
304
- - **`Spec` mirrors `Built`; the vision goes to `Idea`.** For a spec-driven effort: put the umbrella +
324
+ - **`Spec` mirrors `Built`; the vision goes to `Idea`.** For a spec-driven effort: put the ignition brief +
305
325
  any `00-operating-frame` (the *why/what*) in `Idea`, and list in `Spec` **every per-slice spec that a
306
326
  listed `Built` session implemented** — the two rows stay symmetric. A session in `Built` whose spec is
307
327
  missing from `Spec` (or vice-versa) is the smell. Don't silently drop chain members because they feel
@@ -430,17 +450,17 @@ once the code is fixed. Keep entries one line, dated, and pointing at a `file:li
430
450
  ### Spec doc (a numbered pipeline — `spec-pipeline`)
431
451
 
432
452
  When a research idea graduates into a real plan, `spec-pipeline` explodes it into a numbered pipeline in
433
- `docs/research/{topic}/`: `00-ignition-brief.md` (the coordinator seat's standing orders for the run —
434
- written whole at authoring, verified at ignition) · `01-{topic}.md` (the umbrella) · `02..NN-{slice}.md` (**one
435
- self-contained spec per planned session**) · `project-status.md` (the tracker, which carries its own
436
- section notes) · `live-checks.md` (the human's verification rows).
437
- Each per-session spec uses this template (carries `Builds on`/`Source:` so the chain stays navigable):
453
+ `docs/research/{topic}/`: `00-ignition-brief.md` (the coordinator seat's spec and the charter every seat
454
+ reads — written whole at authoring, verified at ignition) · `01..NN-{slice}.md` (**one self-contained spec
455
+ per build seat** — every numbered file is one seat's spec) · `project-status.md` (the tracker, which carries
456
+ its own section notes) · `live-checks.md` (created empty; the rows the owner dictates).
457
+ Each per-seat spec uses this template (carries `Builds on`/`Source:` so the chain stays navigable):
438
458
 
439
459
  ```markdown
440
460
  # Spec NN — {Slice title}
441
461
 
442
462
  > {what this slice builds} — {why / the gap it closes}. {what it builds on / supersedes / defers}.
443
- > Builds on [{prior}]({prior}.md). Source: [{umbrella / research}](...). Written {date}.
463
+ > Builds on [{prior}]({prior}.md). Source: [{brief / research}](...). Written {date}.
444
464
  <!-- meta: type=spec; status=draft; verified={date}; lineage=N -->
445
465
 
446
466
  ## The Problem / The Target
@@ -452,7 +472,7 @@ Each per-session spec uses this template (carries `Builds on`/`Source:` so the c
452
472
  ```
453
473
 
454
474
  Specs stay physically in `research/{topic}/` through the build; when the effort ships, `docs-process` archives
455
- the umbrella + specs + working docs together into `archive/{topic}/` — a spec-driven effort splits into
475
+ the brief + specs + working docs together into `archive/{topic}/` — a spec-driven effort splits into
456
476
  `specs/` + `implementation/` with `project-status.md` at the root (see "Two topic-folder shapes" above). The
457
477
  spec set *is* the design history.
458
478
 
@@ -471,11 +491,11 @@ come from this copy, so a repository changes them once, here.
471
491
  | Skill | When | What it does |
472
492
  |-------|------|-------------|
473
493
  | `docs-backlog` | Have an idea | Capture it as a research doc in `research/` |
474
- | `spec-pipeline` | A research idea is graduating into a real plan | Explode the plan into a numbered spec pipeline (umbrella + one self-contained spec per session + `project-status`) in `research/{topic}/`; the plan becomes ephemeral |
494
+ | `spec-pipeline` | A research idea is graduating into a real plan | Explode the plan into a numbered spec pipeline (the ignition brief + one self-contained spec per build seat + `project-status`) in `research/{topic}/`; the plan becomes ephemeral |
475
495
  | `spec-coordinator` | Right after `spec-pipeline` (same session, context-loaded) or to resume an effort fresh | Assume the coordinator seat: cut seat prompts (one per spec), review/verify TLDRs, give commit calls, own `project-status.md` — the same loop hand-cranked or hosted |
476
- | `spec-seat` | **Every** seat a coordinator arms — invoked FIRST, before the job skill | The CONTRACT, not a seat: the shared half of every seat's procedure — beliefs, plain-text PLAN TLDR, numbered questions w/ recommendations, six-section FINAL TLDR, commit only on the relayed call |
496
+ | `spec-seat` | **Every** seat a coordinator arms — invoked FIRST, before the job skill | The CONTRACT, not a seat: the shared half of every seat's procedure — beliefs, plain-text PLAN TLDR, numbered questions w/ recommendations, the hand-cranked relay, six-section FINAL TLDR, commits once the plan is approved |
477
497
  | `spec-build` | Igniting a build or fixit seat — manually or platform-spawned, after `spec-seat` | The BUILD JOB only: one spec, the role's reading list, the pin taxonomy, the suite protocol; `fixit` variant for no-spec defect sessions. Its ceremony comes from `spec-seat` |
478
- | `spec-witness` | A pipeline looks wedged, actors' claims conflict, or you want a substrate-verified state report | The read-only fourth seat: the control plane's read lanes, transcripts, git; zero writes, zero rulings — the seats act, the human judges, the witness testifies |
498
+ | `spec-witness` | A pipeline looks wedged, actors' claims conflict, or you want a substrate-verified state report | The outside watcher — a session outside the plane, never a declared seat, no credential: git, transcripts, board files, the process table; zero writes into the run, zero rulings — the seats act, the human judges, the witness testifies |
479
499
  | `spec-ignite` | Right after `spec-pipeline` when the pipeline runs HOSTED (vs `spec-coordinator` for hand-cranked) | The ignition copilot: verify the committed brief, derive the stage card (the human clicks Create), "kick it", a hosted coordinator runs the pipeline; two verbal gates, everything announced, every UI gap a named finding |
480
500
  | `spec-review` | A pipeline's builds are done and it has a review row | The outside look, before the retro and before the PR: code + patterns + composition across session seams + the loop's own comms, in two independent passes. Files `sessions/code-review.md` and classifies which findings earn a fixit. **Its value is having none of the pipeline's context** |
481
501
  | `spec-retro` | The coordinator's LAST act, after any review and any fixit | The inside look: a substrate-verified grade, the ceremony audit (did the acts the seat believes it performed actually write rows?), the ledger reconciliation, the true economics, the blindnesses, and the ranked fixit backlog |
@@ -1,4 +1,4 @@
1
- <!-- naswerks-loop: version=0.7.1 -->
1
+ <!-- naswerks-loop: version=0.9.0 -->
2
2
  # The Engineering Loop — flow, seats, skills, and the ceremony
3
3
 
4
4
  > The quick intro to how work gets built with this loop: the two loops (`docs-*` knowledge, `spec-*`
@@ -34,9 +34,11 @@ out-of-pipeline buys a clean PR diff and no contingency. Nothing else about the
34
34
  ```
35
35
  Idea -> docs-backlog -> research doc in docs/research/
36
36
  Research matures -> a plan (a plan-approval mode, if the harness has one)
37
- Plan graduates -> spec-pipeline -> docs/research/{topic}/: umbrella + one spec
38
- per session + project-status.md (the plan is
39
- now throwaway — the specs carry every crumb)
37
+ Plan graduates -> spec-pipeline -> docs/research/{topic}/: the ignition brief (the
38
+ coordinator's spec and the charter every seat
39
+ reads) + one spec per build seat +
40
+ project-status.md (the plan is now throwaway —
41
+ the specs carry every crumb)
40
42
  Same session — CHOOSE THE SEAT:
41
43
  hand-cranked -> spec-coordinator -> this session becomes the COORDINATOR and cuts
42
44
  Prompt #1 (you relay every message)
@@ -45,8 +47,9 @@ Same session — CHOOSE THE SEAT:
45
47
  click Create, "kick it", a hosted coordinator
46
48
  runs the whole pipeline; you own parks +
47
49
  completes + the kill-switch
48
- Per spec (new session) -> BUILD SEAT -> recon, PLAN TLDR, rulings, build,
49
- FINAL TLDR, docs-write, commit (on call)
50
+ Per spec (new session) -> BUILD SEAT -> recon, PLAN TLDR, rulings, build, docs-write,
51
+ commit + branch push (by hand: on the plan's
52
+ approval; hosted: on the call), FINAL TLDR
50
53
  Builds done, if chosen -> spec-review -> the OUTSIDE look, before the retro and before the
51
54
  (a pipeline row) PR: code + patterns + composition across seams +
52
55
  the loop's own comms; sessions/code-review.md.
@@ -59,21 +62,27 @@ Close (coordinator) -> spec-retro -> the INSIDE look: the ceremon
59
62
  Effort done -> docs-process -> living docs patched (consuming the working docs
60
63
  (fresh session, or a AND code-review.md); specs + working docs
61
64
  pipeline row if chosen) archived together to docs/archive/{topic}/
62
- Then, and ONLY these -> the human MERGES the PR, and runs the live-checks rows <- THE TWO HUMAN GATES
65
+ Then, and ONLY these -> the human MERGES the PR, and takes the quick pass <- THE TWO HUMAN GATES
63
66
  Anytime / periodic -> docs-status, docs-audit-feature, docs-audit-full <- NEVER a pipeline row
64
- Alongside, optional -> spec-witness — a session OUTSIDE the worktree, read-only, zero writes: a
67
+ Alongside, optional -> spec-witness — a session OUTSIDE the plane, read-only, no credential: a
65
68
  window into a running pipeline so the human can take its pulse without the UI
66
69
  ```
67
70
 
68
71
  **Two things are the human's, permanently and by design:** the **merge** (a person in the merge path is
69
- the point, not a gap) and the **live-checks** rows (a machine cannot hand-test a UI). Everything between
70
- `idea` and `archived` a pipeline can do for itself.
72
+ the point, not a gap) and the **live checks** (a machine cannot hand-test a UI). Live checks are the
73
+ owner's — never a seat's job and never a doc's row: the coordinator offers a short, ordered quick pass in
74
+ the chat, and `live-checks.md` holds only the rows the owner dictates. Everything between `idea` and
75
+ `archived` a pipeline can do for itself.
76
+
77
+ **Pipelines never intertwine.** A pipeline's GATE is that its predecessor merged — never a seat of another
78
+ pipeline — and no seat waits on another pipeline. Independent pipelines may run at the same time, each on
79
+ its own branch, merged one after the other.
71
80
 
72
81
  **`docs-audit-*` is never a pipeline row.** It runs off-hours and scheduled, over the whole doc tree — not
73
82
  inside an effort, and not at an effort's close. A stale living doc is not something a pipeline must stop for.
74
83
 
75
84
  **The pipeline's SHAPE is decided at authoring, not by a runtime flag.** `spec-pipeline` asks whether the
76
- effort wants a review row and a docs-process row, and writes them into the row table — so the coordinator
85
+ effort wants a review row and a docs-process row, and writes them into the brief's sequence — so the coordinator
77
86
  learns the shape by reading its own sequence, and nothing needs a toggle. A pipeline can be plain,
78
87
  `+review`, `+review +process`, or `+process`.
79
88
 
@@ -120,10 +129,10 @@ What a commit and a PR pass here, beyond the loop. These add to the Hard rules b
120
129
 
121
130
  | Seat | Who | Holds | Never does |
122
131
  |---|---|---|---|
123
- | **Human** | you | final approvals (plan-approve click, hard questions, push/publish, hand-testing), the relay in manual mode | writes feature code mid-pipeline |
132
+ | **Human** | you | final approvals (plan-approve click, hard questions, publish, the merge, hand-testing), the relay in manual mode | writes feature code mid-pipeline |
124
133
  | **Coordinator** | one long-lived session per effort | the north star + full pipeline context; cuts each seat prompt from the previous session's *actual* results; judges PLAN/FINAL TLDRs; rules with rationale; verifies load-bearing claims itself; owns `project-status.md` | writes feature code; trusts self-reported "N green" |
125
- | **Seat** | one fresh session per spec | exactly ONE spec; recons the real code first; builds; reports honestly | touches the tracker or memory; commits without the relayed call; runs `docs-write` unprompted |
126
- | **Witness** *(optional, `spec-witness`)* | a read-only session with substrate access | testifies from rows/transcripts/git when claims conflict or a pipeline looks wedged; answers "whose turn is it" | writes anything; injects anything; rules on anything |
134
+ | **Seat** | one fresh session per spec | exactly ONE spec; recons the real code first; builds; reports honestly | touches the tracker, `live-checks.md` or memory; commits before its plan is approved; pushes to the base branch; runs `docs-write` unprompted |
135
+ | **Witness** *(optional, `spec-witness`)* | a session outside the plane, usually on the runner's machine — never a declared seat, no credential | testifies from git, transcripts, board files and the process table when claims conflict or a pipeline looks wedged; answers "whose turn is it"; drafts messages for the human | writes into the run; injects anything; rules on anything |
127
136
 
128
137
  The seat, not the chat, is the product: `project-status.md` must stay current enough that a
129
138
  fresh session can resume as coordinator losslessly (`spec-coordinator {topic}`).
@@ -144,7 +153,7 @@ fresh session can resume as coordinator losslessly (`spec-coordinator {topic}`).
144
153
  | `spec-ignite` | spec | when an authored pipeline runs HOSTED (the `spec-coordinator` alternative) | the ignition copilot, and it can run COLD: gates, re-verify the brief's baselines + platform state against live HEAD, derive the stage card (human clicks Create), "kick it" with the brief's own kick text as the task; every UI gap a named finding |
145
154
  | `spec-review` | spec | a pipeline's builds are done, if it has a review row | the OUTSIDE look, before the retro and before the PR: correctness/security/**composition across session seams** · the patterns, with every departure identified · the loop's own comms. Two independent passes, consolidated. Classifies which findings earn a fixit; **its value is having none of the pipeline's context** |
146
155
  | `spec-retro` | spec | the coordinator's LAST act, after any review and any fixit | the INSIDE look: a substrate-verified grade, the ceremony audit (did the acts write rows?), the ledger reconciliation, the true economics, the blindnesses, the ranked fixit backlog — *a retrospective that cannot indict its own author is marketing* |
147
- | `spec-witness` | spec | a pipeline looks wedged, or actors' claims conflict | the read-only fourth seat: substrate access, zero writes, zero rulings |
156
+ | `spec-witness` | spec | a pipeline looks wedged, or actors' claims conflict | the outside watcher: read-only, no credential, never a declared seat; its tools ship in `bin/` |
148
157
  | `init` | — | a repository that has none of this yet | create the `docs/` shape, the method docs, first drafts of every doc the skills read; `init doctor` says what is missing |
149
158
 
150
159
  ### Where a rule lives — policy, procedure, task
@@ -163,7 +172,7 @@ One copy per rule: where a prompt and a skill would say the same thing, the prom
163
172
  holds it.
164
173
 
165
174
  **Why the split exists.** A single base contract every seat kind shared, with the build job mixed in,
166
- sends the REVIEW row into the umbrella its own charter excludes and offers the DOCS-PROCESS row a
175
+ sends the REVIEW row into the coordinator's settled framing its own charter excludes and offers the DOCS-PROCESS row a
167
176
  `docs-write` from a build skill's close-out — the row that has just *emptied* `docs/working/`. One base
168
177
  class, three wrong instructions, and none of it visible until the loop runs end to end.
169
178
 
@@ -182,19 +191,26 @@ diff.
182
191
  `blocking-precondition` / `read-full-plan`), one CAPS ruling per question, decision first
183
192
  then the WHY. The coordinator diffs the seat's beliefs against the ledger before ruling.
184
193
  4. **Plan-approve = the freedom moment** (manual: approve + bypass-permissions; hosted: the gate
185
- flips). The seat builds uninterrupted; mid-run questions repeat the relay loop.
194
+ flips). The seat builds uninterrupted; mid-run questions repeat the relay loop. **By hand, the seat
195
+ then runs to done without a stop** — build, suites, `docs-write`, commits with `verify-staged`, its
196
+ branch pushes (never the base branch) — its notes from the coordinator transcribed verbatim into its
197
+ `tldr.md` first. Its only stops: a destructive act outside the effort's branches, a credential
198
+ exposure, a host restart, a `verify-staged` failure.
186
199
  5. **Seat ends with the six-section FINAL TLDR** — shipped+verification, verbatim runner
187
- summaries (with invocation commands), honest gaps, deferred findings, live checklist
188
- entries, staging list + commit HELD + session cost.
200
+ summaries (with invocation commands), honest gaps, deferred findings, what the owner could try
201
+ (in the chat, never a file), staging list + its receipt (hosted: commit HELD) + session cost.
189
202
  6. **Coordinator verifies one load-bearing claim itself** (receipts compared first; rerun only in
190
- doubt), then the **close-out call**: optional fix/rerun, `docs-write` with an itemized
191
- carry, COMMIT CALL with the seat's own staging list, push (human approves the push).
203
+ doubt). By hand it reviews the FINAL against the tree and writes `## COORDINATOR ACCEPT (final —
204
+ {slice})` into the seat's `tldr.md`; hosted it sends the **close-out call**: optional fix/rerun,
205
+ `docs-write` with an itemized carry, COMMIT CALL with the seat's own staging list, then the push.
192
206
  7. **Coordinator flips the tracker row** to SHIPPED + hash and transcribes deferred findings — the
193
207
  seat never writes the tracker. **The coordinator also commits the closing seat's post-push
194
208
  close-out append, byte-untouched, as part of closing the row** — the append lands after the
195
209
  seat's own commit, so left alone it hands the NEXT seat a dirty tree. Goto 1.
196
210
 
197
- **Hard rules:** no commit/push without the relayed call; explicit staging, never `git add -A`, verified
211
+ **Hard rules:** no commit or push before the plan is approved, and never a push to the base branch —
212
+ approved work includes its commit, its push and the coordinator's PR, and nobody asks for them again;
213
+ explicit staging, never `git add -A`, verified
198
214
  with `verify-staged`; codegen only through the command `patterns/codegen.md` names (when the repo has
199
215
  one); component files as
200
216
  `patterns/frontend-patterns.md` states; additive-only, never amend pushed commits; parallel sessions only
@@ -212,7 +228,7 @@ COORDINATOR -> COORDINATOR RESPONSE — PLAN {ACCEPTED / ACCEPTED-WITH-RIDERS /
212
228
  (DECISION — why) · riders · "Go build."
213
229
  SEAT -> (builds free; mid-run question: numbered text, STOP; COORDINATOR ruling; resume)
214
230
  SEAT -> FINAL TLDR — (a) SHIPPED+VERIFICATION (b) VERBATIM RUNNER SUMMARIES (c) HONEST GAPS
215
- (d) DEFERRED FINDINGS (e) LIVE CHECKLIST (f) STAGING + commit HELD + cost
231
+ (d) DEFERRED FINDINGS (e) WHAT THE OWNER COULD TRY (f) STAGING + receipt + cost
216
232
  COORDINATOR -> FINAL TLDR ACCEPTED (+ what the coordinator verified itself) · 1. docs-write, carry: …
217
233
  · 2. COMMIT CALL — message: … · then push, report hash
218
234
  SEAT -> books the working doc, commits, pushes, reports hash + post-push status
@@ -239,9 +255,11 @@ with the effort:
239
255
  docs/research/{topic}/sessions/ <- per-session work in {spec-slug}/, effort-wide at the root
240
256
  {spec-slug}/plan.md each seat the full plan, posted once, corrected on pushback (PULL-only)
241
257
  {spec-slug}/tldr.md each seat the two-voice chat — seat turns AND coordinator rulings interleaved
258
+ docs-process/plan.md + tldr.md the P row its board, here from the start — never a gitignored folder
259
+ {step-slug}/receipt.md a human a `step` row's receipt, in the step's own commit
242
260
  code-review.md the review the outside look, if the pipeline had a review row
243
261
  coordinator-retrospective.md the coord. the close-out audit (the inside look)
244
- witness-ledger-*.md a human the witness is zero-writes, so its ledger arrives by hand
262
+ witness-ledger-*.md a human the witness writes nothing into the run; its ledger is filed on the human's word
245
263
  ```
246
264
 
247
265
  Two files per build session; questions ride the TLDR, and there are no status snapshots because the
@@ -27,7 +27,8 @@ what to audit next.
27
27
  `docs/patterns/` docs; note drift.
28
28
 
29
29
  4. **Research scan** — scan `docs/research/`; flag ideas that look already-implemented (so they can be
30
- graduated/removed) and ideas worth surfacing.
30
+ filed with their effort, or retired as § The archive convention (repository-owned) says) and ideas worth
31
+ surfacing.
31
32
 
32
33
  5. **Working/archive sanity** — note any docs lingering in `docs/working/` that look done (should be
33
34
  filed via `docs-process`), and confirm the filed efforts are intact where § The archive convention
@@ -18,9 +18,13 @@ Most of the time a human runs this cold, after a merge, and this section does no
18
18
  you have not. It carries the blackboard, the question lane, the PLAN TLDR, the verdict vocabulary, the
19
19
  six-section FINAL TLDR, and the close-out order. Two things are yours specifically:
20
20
 
21
- - **You are handed a MANIFEST of this effort's working docs BY NAME. `docs/working/` is a SHARED QUEUE
22
- — never infer membership from the folder.** A seat that archives a neighbouring effort's docs cannot
23
- undo it. If your prompt did not name them, ask before you move anything.
21
+ - **You are handed a MANIFEST of this effort's working docs BY NAME — every input arrives as a LIST,
22
+ never a folder. `docs/working/` is a SHARED QUEUE — never infer membership from the folder.** A seat that
23
+ archives a neighbouring effort's docs cannot undo it. If your prompt did not name them, ask before you move
24
+ anything.
25
+ - **Your board is `docs/research/{topic}/sessions/docs-process/`** (`plan.md` + `tldr.md`) from the start —
26
+ never a gitignored folder you copy from later. The folder moves with the effort when you file it, so the
27
+ board you commit is the board you wrote, close-out receipt included.
24
28
  - **Do NOT run `/docs-write`.** You are that skill's CONSUMER, never its caller — a working doc from you
25
29
  would be an orphan in the queue you just emptied, for an effort you just archived. Your artifacts are
26
30
  the patched living docs and the archive; go straight to the commit call.
@@ -77,7 +81,7 @@ branch, which is the truth for that PR, and you say so at close.
77
81
  spec pipeline (the `Spec` row), and the working-doc sequence / `## Archive` target (the `Built` row);
78
82
  derive `Related` from existing peer links. Compute any field the header derives from those links the
79
83
  way the section says. **Keep `Spec` and `Built` symmetric** — list **every per-slice spec that a
80
- `Built` session implemented** (the umbrella + `00-operating-frame` go in `Idea`, not `Spec`); a `Built`
84
+ `Built` session implemented** (the ignition brief + any `00-operating-frame` go in `Idea`, not `Spec`); a `Built`
81
85
  session whose spec is missing from `Spec` is the smell (the `## Lineage` rules § Doc metadata
82
86
  (repository-owned) gives).
83
87
  - *Patching an existing doc*: update the status if it changed and recompute any field the header
@@ -92,6 +96,8 @@ branch, which is the truth for that PR, and you say so at close.
92
96
  section (create the section if absent). Only what you actually saw in the code this pass — never
93
97
  speculation. A sizable design idea goes to `docs-backlog` (research/) instead. `docs-status`
94
98
  surfaces these so they aren't lost.
99
+ - **Never add a live check to a living doc.** Live checks are the owner's: a working doc's "the owner
100
+ could try" lines are not a checklist item, and no living doc grows a row of checks to run.
95
101
 
96
102
  3. **A new living doc?** If the work added a feature, ensure it has a doc in `docs/features/` (one doc per
97
103
  feature). Then register every living doc this run created where the menu's shape line says
@@ -110,11 +116,11 @@ branch, which is the truth for that PR, and you say so at close.
110
116
  patched and verified, file the whole effort where `docs/_meta/docs-workflow.md` § The archive convention
111
117
  (repository-owned) says, in the shape it gives for this kind of effort (a small effort, a spec pipeline,
112
118
  a follow-on round). That section holds the destination, the shape and the links a move repairs; this
113
- step holds the procedure, which is the same everywhere. A pipeline whose umbrella carries an `Archive:`
114
- line files there.
119
+ step holds the procedure, which is the same everywhere. A pipeline whose ignition brief carries an
120
+ `Archive:` line files there.
115
121
 
116
122
  **FIRST, RECONCILE THE DESTINATIONS — `rg -A1 '^## Archive' <every working doc in the effort>`, plus
117
- the umbrella's `Archive:` line when there is a pipeline.**
123
+ the brief's `Archive:` line when there is a pipeline.**
118
124
  This step says *"one archive home"* and *"use the `## Archive` line"* — a singular read over a PLURAL
119
125
  source, so the set is checked before any move. **If the docs disagree, STOP and ask.** Do not pick the
120
126
  majority, do not pick the first, and do not pick the one you happen to be reading. The failure is not
@@ -31,7 +31,7 @@ keeps the doc editable until its living-doc obligations are discharged.
31
31
  - **Watch-Outs** — edge cases, gotchas
32
32
  - **Living Docs to Update** — the bridge to `docs-process`. For each stale living doc, name it and say what changed and in what form. Be specific ("references `OldEntity`, should be `NewEntity`"), not vague.
33
33
 
34
- 3. **Record the archive target — don't move the file.** Add an `## Archive` line to the doc, in the form the session template gives it (`docs/_meta/docs-workflow.md` § Templates (repository-owned)), naming where § The archive convention (repository-owned) files this effort — reuse what that section says to reuse, and name it the way that section names things. When the effort has a spec pipeline whose umbrella carries an `Archive:` line, that path is the target. `docs-process` files it there later. The doc itself **stays in `docs/working/`**.
34
+ 3. **Record the archive target — don't move the file.** Add an `## Archive` line to the doc, in the form the session template gives it (`docs/_meta/docs-workflow.md` § Templates (repository-owned)), naming where § The archive convention (repository-owned) files this effort — reuse what that section says to reuse, and name it the way that section names things. When the effort has a spec pipeline whose ignition brief carries an `Archive:` line, that path is the target. `docs-process` files it there later. The doc itself **stays in `docs/working/`**.
35
35
 
36
36
  4. **Stop.** The finished doc sits in `docs/working/` as the queue. Do NOT patch living docs, do NOT grep for stale references, do NOT file it — all of that is `docs-process`.
37
37
 
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
5
  import { join, resolve, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { execFileSync } from 'node:child_process';
8
- import { OWNED, KINDS, KIND_SKILLS, NOT_SKILLS, skillTokens, compareVersions, compareMinor, frontmatterListCount, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
8
+ import { OWNED, KINDS, STEP, ROLES, SPEC_FILE, KIND_SKILLS, NOT_SKILLS, skillTokens, compareVersions, compareMinor, frontmatterListCount, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
9
9
 
10
10
  const repo = resolve(process.argv[2] || process.cwd());
11
11
  const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
@@ -197,6 +197,11 @@ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
197
197
  // The kick is sent verbatim as the coordinator's task, so a skill it names that this pack does not carry
198
198
  // (a renamed one, most often) tells the coordinator to invoke nothing.
199
199
  const packSkills = new Set(existsSync(join(packRoot, 'skills')) ? readdirSync(join(packRoot, 'skills')) : []);
200
+ // A repository's name as a sequence row may write it: one path segment, no separators, no spaces, of at most
201
+ // MAX_REPOSITORY_NAME characters; a row names at most MAX_REPOSITORIES, none twice.
202
+ const REPOSITORY_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
203
+ const MAX_REPOSITORY_NAME = 200;
204
+ const MAX_REPOSITORIES = 8;
200
205
  const researchDir = join(repo, 'docs', 'research');
201
206
  if (existsSync(researchDir)) {
202
207
  for (const topic of readdirSync(researchDir)) {
@@ -209,21 +214,54 @@ if (existsSync(researchDir)) {
209
214
  else if (!kick.split('\n').some((l) => l.trimStart().startsWith('>'))) problems.push('the `## The kick` section carries no blockquote; its whole text stands in for the task');
210
215
  const unknown = kick === null || packSkills.size === 0 ? [] : [...new Set(skillTokens(kick))].filter((t) => !packSkills.has(t) && !NOT_SKILLS.includes(t));
211
216
  const seq = sectionOf(text, '## The sequence');
217
+ const named = new Set();
212
218
  if (seq === null) problems.push('the brief has no `## The sequence` section, so no seats could be read');
213
219
  else {
214
220
  const tableRows = seq.split('\n').filter((l) => l.trim().startsWith('|'));
215
221
  if (tableRows.length === 0) problems.push('the `## The sequence` section holds no table, so no seats could be read');
222
+ const cellsOf = (line) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim().replace(/^`|`$/g, '').trim());
223
+ // The optional Role column, found by its header; a row's cell there is a role or empty.
224
+ const roleAt = tableRows.length ? cellsOf(tableRows[0]).findIndex((c) => c.toLowerCase() === 'role') : -1;
225
+ // The optional Repository column, found by its header; a row's cell there names the repositories the
226
+ // seat also writes in, or is empty. The table is read by kind value, so a kind word or `step` in that
227
+ // cell would be read as the row's kind: it fails by name, before either is looked for.
228
+ const repositoryAt = tableRows.length ? cellsOf(tableRows[0]).findIndex((c) => c.toLowerCase() === 'repository') : -1;
229
+ // The seat rows' kinds, in order: where a listed fixit sits is read from them.
230
+ const seatKinds = [];
216
231
  for (const line of tableRows.slice(1)) {
217
- const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim().replace(/^`|`$/g, '').trim());
232
+ const cells = cellsOf(line);
218
233
  if (cells.every((c) => /^[-: ]*$/.test(c))) continue;
219
- if (!cells.some((c) => KINDS.includes(c))) { problems.push(`a sequence row names no seat kind and composes nothing: \`${line.trim()}\``); continue; }
220
- const spec = cells.map((c) => /\b(\d{2}-[A-Za-z0-9._-]*?\.md)\b/.exec(c)).find(Boolean);
234
+ const spec = cells.map((c) => SPEC_FILE.exec(c)).find(Boolean);
235
+ if (spec) named.add(spec[1]);
236
+ const repositories = repositoryAt >= 0 ? (cells[repositoryAt] ?? '') : '';
237
+ const names = !repositories || /^[-—]$/.test(repositories) ? [] : repositories.split(/\s*[·,]\s*/).map((n) => n.replace(/`/g, '').trim()).filter((n) => n !== '');
238
+ const notNames = names.filter((n) => KINDS.includes(n) || n === STEP || !REPOSITORY_NAME.test(n) || n.length > MAX_REPOSITORY_NAME);
239
+ if (notNames.length) {
240
+ for (const n of notNames) problems.push(`a sequence row's repository \`${n.length > 60 ? `${n.slice(0, 60)}…` : n}\` is not a repository's name (one path segment of at most ${MAX_REPOSITORY_NAME} characters; a kind word or \`${STEP}\` there is read as the row's kind): \`${line.trim().slice(0, 200)}\``);
241
+ continue;
242
+ }
243
+ if (names.length > MAX_REPOSITORIES) problems.push(`a sequence row names ${names.length} repositories and a seat writes at most ${MAX_REPOSITORIES}: \`${line.trim()}\``);
244
+ for (const n of new Set(names.filter((name, i) => names.indexOf(name) !== i))) problems.push(`a sequence row names the repository \`${n}\` twice: \`${line.trim()}\``);
245
+ if (cells.includes(STEP)) continue;
246
+ if (!cells.some((c) => KINDS.includes(c))) { problems.push(`a sequence row names no seat kind and composes nothing (a row that is not a seat says \`${STEP}\`): \`${line.trim()}\``); continue; }
247
+ seatKinds.push(cells.find((c) => KINDS.includes(c)));
248
+ const role = roleAt >= 0 ? (cells[roleAt] ?? '') : '';
249
+ if (role && !/^[-—]$/.test(role) && !ROLES.includes(role)) problems.push(`a sequence row's role \`${role}\` is not one of ${ROLES.map((r) => `\`${r}\``).join(' · ')}: \`${line.trim()}\``);
221
250
  if (spec && !existsSync(join(researchDir, topic, spec[1]))) problems.push(`the sequence names \`${spec[1]}\` and it is missing from disk`);
222
251
  }
252
+ // A listed fixit is the one its review implies only when it is the next seat row after that review. Listed
253
+ // anywhere else, a launcher reads a second fixit beside the implied one, and that one names no repository.
254
+ const reviewAt = seatKinds.indexOf('review');
255
+ const fixitAt = seatKinds.indexOf('fixit');
256
+ if (reviewAt >= 0 && fixitAt >= 0 && fixitAt !== reviewAt + 1) problems.push('the sequence lists a `fixit` row that is not the next seat row after its `review` row, so a launcher reads two fixits and the extra one names no repository: move the row directly after the review');
223
257
  }
258
+ // An old-shape folder carries an umbrella: a `01-` file no sequence row names. It still runs on an older
259
+ // pack; carry it over before it runs on this one.
260
+ const umbrellas = seq === null ? [] : readdirSync(join(researchDir, topic)).filter((f) => /^01-.+\.md$/.test(f) && !named.has(f));
224
261
  for (const pr of problems) row('FAIL', `docs/research/${topic}/00-ignition-brief.md`, pr);
225
262
  for (const t of unknown) row('DEGRADE', `docs/research/${topic}/00-ignition-brief.md`, `the kick names \`${t}\`, a skill this pack does not carry; the coordinator's skill is \`${KIND_SKILLS.coordinator}\``);
226
- if (!problems.length && !unknown.length) row('ok', `docs/research/${topic}/00-ignition-brief.md`, 'the kick and the sequence read clean');
263
+ for (const u of umbrellas) row('DEGRADE', `docs/research/${topic}/00-ignition-brief.md`, `old shape — fold \`${u}\` into the brief (CHANGELOG 0.8.0 § Upgrading)`);
264
+ if (!problems.length && !unknown.length && !umbrellas.length) row('ok', `docs/research/${topic}/00-ignition-brief.md`, 'the kick and the sequence read clean');
227
265
  }
228
266
  }
229
267
 
@@ -24,6 +24,16 @@ export const STAMP = /^(<!-- naswerks-loop: version=)([0-9]+\.[0-9]+\.[0-9]+[^ ]
24
24
  // The seat kinds a pipeline row may name, checked the way the control plane checks a brief's sequence.
25
25
  export const KINDS = ['coordinator', 'build', 'fixit', 'review', 'retro', 'witness', 'docs-process'];
26
26
 
27
+ // The one word for a sequence row that is not a seat (a publish, a merge, a human sitting). No misspelled
28
+ // seat kind can equal it, so a row whose kind is misspelled still fails by name instead of reading as a step.
29
+ export const STEP = 'step';
30
+
31
+ // The roles a sequence row's optional `Role` cell may carry: which reading list a build seat takes.
32
+ export const ROLES = ['backend', 'frontend', 'mixed'];
33
+
34
+ // A numbered file a sequence row names: a seat's spec, or the brief itself on the coordinator's row.
35
+ export const SPEC_FILE = /\b(\d{2}-[A-Za-z0-9._-]*?\.md)\b/;
36
+
27
37
  // The skill each kind runs as its job. A spec seat's skill is named for its kind, so a skill's name says
28
38
  // which seat runs it: `fixit` is the build job's no-spec variant, and `docs-process` is the knowledge
29
39
  // loop's own skill, seated by a pipeline as its last row.