@naswerks/engineering-loop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/CHANGELOG.md +32 -0
  4. package/LICENSE +21 -0
  5. package/README.md +78 -0
  6. package/bin/verify-staged.mjs +247 -0
  7. package/package.json +35 -0
  8. package/references/docs-workflow.md +417 -0
  9. package/references/engineering-loop.md +298 -0
  10. package/skills/docs-audit-feature/SKILL.md +88 -0
  11. package/skills/docs-audit-full/SKILL.md +54 -0
  12. package/skills/docs-backlog/SKILL.md +41 -0
  13. package/skills/docs-process/SKILL.md +146 -0
  14. package/skills/docs-status/SKILL.md +47 -0
  15. package/skills/docs-write/SKILL.md +43 -0
  16. package/skills/init/SKILL.md +163 -0
  17. package/skills/init/scripts/doctor.mjs +176 -0
  18. package/skills/spec-child/SKILL.md +149 -0
  19. package/skills/spec-ignite/SKILL.md +277 -0
  20. package/skills/spec-parent/SKILL.md +634 -0
  21. package/skills/spec-pipeline/SKILL.md +453 -0
  22. package/skills/spec-retro/SKILL.md +398 -0
  23. package/skills/spec-review/SKILL.md +266 -0
  24. package/skills/spec-seat/SKILL.md +512 -0
  25. package/skills/spec-witness/SKILL.md +222 -0
  26. package/templates/claude-md-stanza.md +13 -0
  27. package/templates/docs/_meta/doc-index.md +90 -0
  28. package/templates/docs/infrastructure/background-work.md +53 -0
  29. package/templates/docs/infrastructure/realtime-events.md +54 -0
  30. package/templates/docs/patterns/backend-patterns.md +96 -0
  31. package/templates/docs/patterns/codegen.md +69 -0
  32. package/templates/docs/patterns/frontend-patterns.md +92 -0
  33. package/templates/docs/patterns/state-management.md +73 -0
  34. package/templates/docs/patterns/testing.md +103 -0
  35. package/templates/docs/patterns/ui-style-guide.md +76 -0
  36. package/templates/docs/patterns/vertical-slice-anatomy.md +61 -0
  37. package/templates/docs/readme-archive.md +9 -0
  38. package/templates/docs/readme-features.md +7 -0
  39. package/templates/docs/readme-guides.md +7 -0
  40. package/templates/docs/readme-infrastructure.md +7 -0
  41. package/templates/docs/readme-patterns.md +9 -0
  42. package/templates/docs/readme-research.md +9 -0
  43. package/templates/docs/readme-working.md +9 -0
  44. package/templates/settings-pin.json +10 -0
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: docs-status
3
+ description: Read-only orientation at session start — what's recently archived, what's in progress in working/, and what ideas are waiting in research/. Use at session start or when the user asks for docs status, or runs docs-status.
4
+ ---
5
+
6
+ # docs-status
7
+
8
+ A quick, read-only pulse of the docs pipeline. No files created. Report by name with context, not
9
+ just counts.
10
+
11
+ ## Steps
12
+
13
+ 1. **`working/` — in flight.** List each doc in `docs/working/` with a one-line "what it's about /
14
+ how far along." A folder's `README.md` is never a queue entry — skip it. These are active efforts; a non-empty queue means living docs may be going stale.
15
+
16
+ 2. **`research/` — waiting ideas.** List each doc in `docs/research/` (and topic subfolders) by name
17
+ with a one-line hook. This is the backlog of things to pick up next.
18
+
19
+ 3. **`archive/` — recently done.** List the most recently modified topic folders / session docs in
20
+ `docs/archive/` (a handful, newest first) so the user can see what just shipped.
21
+
22
+ 4. **Meta sweep — the condensed status table.** Run `rg -o '<!-- meta:.*-->' docs/` and parse each
23
+ `type · status · verified · lineage` into a compact one-line-per-doc table (group by folder). This is the
24
+ headline pulse — richer than the old date-only scan. Read-only; do not write anything. Flag, in order of
25
+ concern:
26
+ - **missing-meta** — a living doc (`guides/ patterns/ infrastructure/ features/`) with no `<!-- meta:`
27
+ comment. Read its blockquote `Last verified` instead (back-compat: read either, **prefer the
28
+ comment**) and list it as a gap to fill.
29
+ - **stale** — `verified` older than ~14 days, a candidate for `docs-audit-feature` (the existing nudge;
30
+ note the harder 90-day line the future `docs_freshness` tool will use).
31
+ - **dead lineage** — a `## Lineage` link whose target file no longer exists (a broken breadcrumb).
32
+ - **low-lineage feature** — `lineage=0` on a `type=feature` doc = ungrounded; worth wiring its lineage.
33
+
34
+ 5. **Open issues.** Grep living docs for an `## Open Issues` section; list which docs/features carry
35
+ unresolved, code-verified problems (and how many) so known risks surface at orientation instead of
36
+ rotting silently. These are candidates to fix, or to graduate into a `working/` effort.
37
+
38
+ 6. **Recommend a next action, escalating by scale:**
39
+ - Working queue non-empty: "process the working doc X" first.
40
+ - Clear: "pick up research idea Y" (with a one-line why).
41
+ - A few stale docs: `docs-audit-feature` on those specific docs.
42
+ - **Many stale docs, or a long absence (~30+ days of no doc activity)**: recommend `docs-audit-full`
43
+ to get a prioritized re-verify plan instead of one-off audits.
44
+
45
+ ## Notes
46
+ - Output is a conversational summary only. There is no backlog doc and no release doc to read —
47
+ `working/`, `research/`, and `archive/` ARE the status.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: docs-write
3
+ description: Finalize the working doc at the end of an implementation session and leave it in docs/working/ as the queue for docs-process to patch + file. Use at the end of a coding session, when the user says to write up the work, document the session, book-end it, or run docs-write.
4
+ ---
5
+
6
+ # docs-write
7
+
8
+ Capture an implementation session into a finished session doc that **stays in `docs/working/`** as the
9
+ queue for `docs-process`. This is the **first** half of the bookend — it does NOT patch living docs and
10
+ does NOT archive. Both happen in `docs-process`, once the living docs are patched and verified against
11
+ code. Leaving the doc in `working/` is what makes `working/` a real queue (`docs-status` reads it) and
12
+ keeps the doc editable until its living-doc obligations are discharged.
13
+
14
+ ## Steps
15
+
16
+ 1. **Review what happened** — prefer conversation memory (you just did the work). Use `git diff --stat` only as a fallback to confirm file paths.
17
+
18
+ 2. **Write/finish the working doc.** If a doc for this effort already exists in `docs/working/`, finish it; otherwise create `docs/working/{topic}-{description}.md`. Use the session-doc template in `docs/_meta/docs-workflow.md`. Required sections:
19
+ - Title + one-line summary; completion date; branch
20
+ - **Meta-comment** — directly below the blockquote, emit one line: `<!-- meta: type=working; status={wip|shipped}; verified={today}; lineage=N -->` (see "Doc metadata" in `docs-workflow.md`). `lineage` = the count of provenance links the doc carries (`Source:` + `Builds on` + any `## Lineage` rows).
21
+ - **Session-comment** — next line: `<!-- session: claude-session-id={the harness's session id} -->` (Claude Code: `$CLAUDE_CODE_SESSION_ID` / `$env:CLAUDE_CODE_SESSION_ID`). Applies to EVERY session that writes a working doc — solo sessions with no pipeline, no coordinator, and no research doc included; the id alone is the point. Append `; name={...}` only when the session actually has a human-given window name (pipeline seats do; most solo sessions won't — omit it, never invent one). The harness's transcript store is ephemeral (a retention window, and the format moves between releases); this line is the durable link from the work to its conversation, so audit-grade sessions can be found later.
22
+ - **Builds on** — if this continues prior work, link the prior doc: `Builds on [prior](prior.md)`
23
+ - **Source** — if idea-driven, link the `research/` doc
24
+ - **The Problem** / **The Target**
25
+ - **Research & Decisions** — options, verdicts, rationale (table). Write this for **future us**: capture **what we actually did vs. what we set out to do** (where the plan changed and why), **what we tried that didn't work** (dead-ends, gotchas, abandoned approaches — so nobody re-walks them), and **every decision we made** with its reasoning. The point of a session doc is the narrative a later session can't reconstruct from the diff: the diff shows the final state, not the choices or the things that failed on the way there.
26
+ - **Implementation** — what changed, before/after where useful
27
+ - **Change shape** — orientation, NOT a file-by-file inventory (the commit is the inventory). Name the key new/moved pieces and the structural facts a later session needs; defer the full list to the commit. A file-by-file dump makes the doc read like a review.
28
+ - **Verification** — build/test results; what's tested, what isn't
29
+ - **Watch-Outs** — edge cases, gotchas
30
+ - **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.
31
+
32
+ 3. **Record the archive target — don't move the file.** Add an `## Archive` line to the doc naming the intended **topic folder** under `docs/archive/{topic}/` (reuse an existing topic folder; descriptive name, **no numeric prefixes** — the `Builds on` link is the ordering). `docs-process` files it there later. The doc itself **stays in `docs/working/`**.
33
+
34
+ 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 move it to `archive/` — all of that is `docs-process`.
35
+
36
+ ## Notes
37
+ - **Provenance carries into the living docs.** The `Source:` (research) and `Builds on` (prior working) links
38
+ in the blockquote already *are* lineage edges — at `docs-process` time they populate the living doc's
39
+ `## Lineage` (`Idea` / `Built` rows). Optionally also render a `## Lineage` block in the working doc itself
40
+ for symmetry, but the blockquote links are the load-bearing record.
41
+ - **Write for future us, not for review.** Record **what we did vs. what we thought we were going to do**, **what we tried that didn't work**, and **the decisions we made (and why)**. Those three are the load-bearing content — the commit already captures the final file state; the doc captures the reasoning, the reversals, and the dead-ends so the next session doesn't repeat them.
42
+ - The doc becomes frozen history only when `docs-process` files it into `docs/archive/` (after the living docs are patched). Until then it's editable in `working/`.
43
+ - If the work was pure research/exploration with no code change, use `docs-backlog` instead.
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: init
3
+ description: Make a repository able to run the loop — create the docs/ shape, the two method docs, a scanned first draft of every doc the loop's skills read, the doc-index menu with the repo's own slots, the Start-here stanza and the plugin pin; then say what is still missing. Run init in a repository that has none of this; run init doctor to check one that does. A second init writes nothing and prints the doctor report.
4
+ ---
5
+
6
+ # init
7
+
8
+ Every seat the loop arms reads `docs/_meta/*` and a floor of `patterns/*` docs before it writes a line;
9
+ the coordinator keeps a tracker in `docs/research/{topic}/`; `docs-write` files into `docs/working/`; the
10
+ control plane admits a spec folder only when the repository carries `docs/_meta/docs-workflow.md`.
11
+ `init` creates all of that in a repository that has none of it — as REAL first drafts scanned off the
12
+ repo, never as empty headings — and `init doctor` says, per path, whether the loop can run.
13
+
14
+ **Two invocations:** `init` (create what is missing, then the report) · `init doctor` (the report only).
15
+ A second `init` writes nothing and IS the doctor report.
16
+
17
+ **The law of the draft:** a section is filled only from files the scan opened, and every drafted doc
18
+ names them in its provenance line. Stack, trees, commands, config keys and Key Files are copied verbatim.
19
+ **Rules and gotchas are never invented** — a rule is written only when the repository already wrote it
20
+ down somewhere (a CONTRIBUTING, an analyzer config, a lint rule, a base-class docstring), and the bullet
21
+ cites that file. A section with nothing to fill carries the literal absent-line and nothing else:
22
+
23
+ ```
24
+ No {X} detected — fill this in: {what the section must answer}.
25
+ ```
26
+
27
+ The loop itself supplies exactly three seed rules that need no source: never hand-write a generated
28
+ folder (when one exists); suites run serially with the output captured to a UTF-8 file and the summary
29
+ block read, never the exit code alone; a pin must be able to fail.
30
+
31
+ ## What init creates — the closed set
32
+
33
+ ```
34
+ docs/
35
+ _meta/docs-workflow.md a byte copy of the pack's references/docs-workflow.md (stamped)
36
+ _meta/engineering-loop.md a byte copy of references/engineering-loop.md (stamped)
37
+ _meta/doc-index.md the menu, written LAST — every doc that exists + three repo slots:
38
+ ## Role reading lists · ## Apps and their build commands · ## The evidence tools
39
+ research/ working/ archive/ guides/ patterns/ infrastructure/ features/ each with a README.md
40
+ patterns/vertical-slice-anatomy.md patterns/backend-patterns.md patterns/frontend-patterns.md
41
+ patterns/testing.md patterns/codegen.md the floor — always drafted
42
+ patterns/state-management.md patterns/ui-style-guide.md
43
+ infrastructure/realtime-events.md infrastructure/background-work.md only when the scan finds the concern
44
+ CLAUDE.md the Start-here stanza appended (created with only the stanza if absent)
45
+ AGENTS.md the same stanza, only when the file already exists
46
+ .claude/settings.json the plugin pin merged in (never clobbers existing keys)
47
+ ```
48
+
49
+ Eight folders, and the set is closed. Anything else already under `docs/` is left alone and reported as
50
+ "outside the loop's shape — not touched" — never an error, never a queue entry.
51
+
52
+ **Every template lives at the pack root under `templates/`** (`templates/docs/patterns/*.md`,
53
+ `templates/docs/infrastructure/*.md`, `templates/docs/_meta/doc-index.md`, `templates/docs/readme-*.md`,
54
+ `templates/claude-md-stanza.md`, `templates/settings-pin.json`). Under Claude Code the pack root is
55
+ `${CLAUDE_PLUGIN_ROOT}` and this skill's own folder is `${CLAUDE_SKILL_DIR}`; in another harness, the
56
+ pack root is the folder that holds `.claude-plugin/plugin.json` — two levels above this SKILL.md.
57
+
58
+ ## The scan — read everything first, write nothing yet
59
+
60
+ Run all three passes with your file tools before the first write. Keep a table in your turn — `file ·
61
+ what it says` — and cite from it. A draft written mid-scan is a draft written from memory.
62
+
63
+ 1. **Refuse outside a git repository**: `git rev-parse --show-toplevel` (the refusal is
64
+ `init-needs-a-git-repository`). Everything below is relative to that root.
65
+ 2. **The stacks.** `*.sln`, `*.slnx`, `*.csproj`, `Directory.Build.props`, `Directory.Packages.props` ·
66
+ `package.json` (+ lockfile, `workspaces`), `pnpm-workspace.yaml` · `pyproject.toml`, `setup.cfg`,
67
+ `requirements*.txt` · `go.mod` · `Cargo.toml` · `Gemfile` · `pom.xml`, `build.gradle*`. Frameworks and
68
+ versions by dependency name.
69
+ 3. **The commands and the shape.** Test runners and scripts (`package.json` `scripts`, `Makefile`,
70
+ `justfile`, `scripts/*`) · CI workflow files (`.github/workflows/*.yml`, `azure-pipelines.yml`,
71
+ `.gitlab-ci.yml`) — the jobs are the lanes · app roots (`angular.json` projects, `apps/*`,
72
+ `packages/*`, `web/*` and `services/*` under `src/`, `cmd/*`, an executable `*.csproj`) · `generated/` folders
73
+ and codegen configs (`codegen.ts`, `codegen.yml`, `openapi*`, `*.proto`, `tgconfig.json`, `prisma/`) ·
74
+ the folder layout two deep · `git log --oneline -20` (the message convention) · a realtime library
75
+ (signalr, socket.io, ws, sse, pusher, ably) · a state library (ngxs, ngrx, redux, zustand, pinia,
76
+ mobx) · a UI library or tokens file (tailwind, daisyui, mui, chakra, bootstrap, shadcn) · a queue,
77
+ worker or scheduler (a service bus, RabbitMQ, SQS, BullMQ, Hangfire, Quartz, Celery, sidekiq).
78
+ 4. **The rules the repository already wrote down.** `CONTRIBUTING.md`, `CLAUDE.md`, `AGENTS.md`, a
79
+ README "Conventions" section · `.editorconfig`, eslint / biome / prettier / stylelint configs ·
80
+ analyzer configs (`.stylecop`, `.globalconfig`, `Directory.Build.props` rules such as
81
+ `TreatWarningsAsErrors`, `Nullable`, banned-API lists) · `tsconfig*.json` strictness ·
82
+ `BannedSymbols.txt`, `FLAKY-TESTS.md` · CODEOWNERS · `.husky/`, `.githooks/` · PR templates · the
83
+ existing `docs/` tree · `.claude/settings.json`.
84
+
85
+ ## The write order — create-if-missing at every step
86
+
87
+ An existing file always wins and is reported `repo-owned`, whether it is a draft or not, loop-shaped or
88
+ not. `init` never overwrites, never reorders, never deletes.
89
+
90
+ 1. The seven folders, each with its `README.md` from `templates/docs/readme-{folder}.md` — the README is
91
+ the only file init writes into a folder that already exists.
92
+ 2. `docs/_meta/docs-workflow.md` and `docs/_meta/engineering-loop.md` — byte copies of the pack's
93
+ `references/*` (their first line is the version stamp; copy it with the file).
94
+ 3. The five floor drafts from `templates/docs/patterns/*.md`, filled as below.
95
+ 4. The conditional drafts — `state-management`, `ui-style-guide`, `realtime-events`, `background-work` —
96
+ only where pass 3 found the concern.
97
+ 5. `docs/_meta/doc-index.md` from its template, LAST, listing every doc that now exists under the four
98
+ living folders (drafted or repo-owned), the three repo slots filled from the scan.
99
+ 6. The Start-here stanza (`templates/claude-md-stanza.md`) appended to `CLAUDE.md` (created if absent)
100
+ and to `AGENTS.md` (only if present); skipped when the opening marker
101
+ `<!-- naswerks-loop:start-here -->` is already there.
102
+ 7. `.claude/settings.json` — merge the two keys from `templates/settings-pin.json`; create the file if
103
+ absent; never remove or reorder existing keys.
104
+
105
+ Then the report: one table, one row per path — `created` · `repo-owned` · `skipped: marker present` —
106
+ followed by the doctor verdict line.
107
+
108
+ ## Filling a template
109
+
110
+ Every template section carries an `<!-- init: scan … fill … absent … -->` instruction with three parts:
111
+ **scan** (what to read), **fill** (what a real fill looks like — a fenced command, a tree, a table row),
112
+ **absent** (the exact line to write when nothing was found). Replace the comment with the fill or the
113
+ absent-line; **never leave an `<!-- init: -->` comment in the output** (`doctor` reads one as DEGRADE).
114
+ Replace `{today}` with today's date and `{files}` in the provenance line with the comma-separated files
115
+ the scan opened for that doc.
116
+
117
+ The shape every drafted doc keeps — because it is the shape the seats know how to read:
118
+
119
+ 1. the H1, the one-line blockquote ending `Last verified {today}.`, the `<!-- meta: … status=draft … -->`
120
+ line, the `<!-- naswerks-loop: draft; scanned=… -->` line;
121
+ 2. Overview, then Stack, then the shape (a tree copied from a REAL folder), then one section per
122
+ concept in the micro-grammar *prose rule, a REAL example, `### Rules`, `### Gotcha:`*;
123
+ 3. decision tables that decide, checklists whose items are real paths, a `Legacy (never use)` column
124
+ where the repo has one;
125
+ 4. exact commands in fenced blocks; identifiers in backticks;
126
+ 5. `## Key Files`, `## Lineage`.
127
+
128
+ A section the scan can only half-fill says which half. A rule with no cited source is not written.
129
+
130
+ ## doctor
131
+
132
+ `node ${CLAUDE_SKILL_DIR}/scripts/doctor.mjs [repo-root]` — the `scripts/` folder beside this SKILL.md;
133
+ Node 22 or later, no dependencies. It prints one row per thing the skills read and exits non-zero on any
134
+ FAIL, so a CI job or a control plane can call it.
135
+
136
+ | Row | FAIL | DEGRADE | ok |
137
+ |---|---|---|---|
138
+ | each of the eight folders | missing | README missing | present |
139
+ | `_meta/docs-workflow.md`, `_meta/engineering-loop.md` | missing, or no stamp on line 1 | stamp behind the pack's version | stamp equals it |
140
+ | `_meta/doc-index.md` and its three slot sections | missing | a slot carries only the absent-line | filled |
141
+ | the five floor docs | missing | `status=draft`, or an `<!-- init: -->` left in | `status=current` |
142
+ | the four conditional docs | — | `status=draft` when present | present and current, or absent |
143
+ | `CLAUDE.md` / `AGENTS.md` stanza · `.claude/settings.json` pin | — | missing | present |
144
+ | `verify-staged` | — | not on PATH (the row carries the fallback) | resolvable |
145
+ | every `docs/research/*/00-ignition-brief.md` | the five problems the control plane's parser emits (no `## The kick`; the kick has no blockquote; no `## The sequence`; the sequence holds no table; a row names no kind from `coordinator build fixit review retro witness docs-process`), or a spec a row names is missing from disk | — | clean |
146
+ | every `docs/working/*.md` | — | lacks `## Living Docs to Update` or `## Archive` | carries both |
147
+ | anything else under `docs/` | — | — | "outside the loop's shape — not touched" |
148
+
149
+ **If `node` is absent**, walk the same rows by hand with your file tools, in that order, and print the
150
+ same table: the checks are file existence, a first-line stamp, a heading grep, a `status=` read, and the
151
+ brief's two sections.
152
+
153
+ A repository fresh from `init` reads DEGRADE on every draft and ok on the shape. That is the intended
154
+ state: the owner promotes each draft to `status=current` as they confirm it, section by section.
155
+
156
+ ## Notes
157
+
158
+ - `init` is the front door of the pack; `docs-status` is the pulse after it. Run `docs-status` once the
159
+ drafts are promoted to see the queue the way a session will.
160
+ - The floor is always drafted, even in a repository with no frontend or no codegen — the reading floor
161
+ must RESOLVE for every seat kind, and "No frontend detected — fill this in" is an honest doc.
162
+ - Counterpart skills: `spec-seat` (reads the floor), `spec-child` (reads the role rows), `spec-pipeline`
163
+ (writes the brief `doctor` checks), `docs-write` / `docs-process` (the queue `doctor` reads).
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // doctor — can the loop run in this repository? One row per thing the skills read, a verdict per row,
3
+ // non-zero exit on any FAIL. Node >= 22, no dependencies. Usage: node doctor.mjs [repo-root]
4
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
+ import { join, resolve, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { execFileSync } from 'node:child_process';
8
+
9
+ const repo = resolve(process.argv[2] || process.cwd());
10
+ const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
11
+ const rows = [];
12
+ const row = (verdict, subject, detail = '') => rows.push({ verdict, subject, detail });
13
+ const read = (p) => (existsSync(p) ? readFileSync(p, 'utf8').replace(/\r\n/g, '\n') : null);
14
+ const rel = (p) => p.slice(repo.length + 1).replace(/\\/g, '/');
15
+
16
+ // ── the pack's own version, for the stamp check ─────────────────────────────────────────────────
17
+ let packVersion = null;
18
+ try { packVersion = JSON.parse(readFileSync(join(packRoot, '.claude-plugin', 'plugin.json'), 'utf8')).version; } catch { /* reported below */ }
19
+
20
+ // ── 1. the eight folders ────────────────────────────────────────────────────────────────────────
21
+ const FOLDERS = ['_meta', 'research', 'working', 'archive', 'guides', 'patterns', 'infrastructure', 'features'];
22
+ if (!existsSync(join(repo, 'docs'))) row('FAIL', 'docs/', 'missing — run init');
23
+ for (const f of FOLDERS) {
24
+ const p = join(repo, 'docs', f);
25
+ if (!existsSync(p)) row('FAIL', `docs/${f}/`, 'missing');
26
+ else if (f !== '_meta') row(existsSync(join(p, 'README.md')) ? 'ok' : 'DEGRADE', `docs/${f}/README.md`, existsSync(join(p, 'README.md')) ? '' : 'missing (init writes it)');
27
+ }
28
+
29
+ // ── 2. the two method docs and their stamp ──────────────────────────────────────────────────────
30
+ const STAMP = /^<!-- naswerks-loop: version=([0-9]+\.[0-9]+\.[0-9]+[^ ]*) -->/m;
31
+ for (const name of ['docs-workflow.md', 'engineering-loop.md']) {
32
+ const p = join(repo, 'docs', '_meta', name);
33
+ const text = read(p);
34
+ if (text === null) { row('FAIL', `docs/_meta/${name}`, 'missing'); continue; }
35
+ const m = STAMP.exec(text.split('\n', 1)[0]);
36
+ if (!m) { row('FAIL', `docs/_meta/${name}`, 'no naswerks-loop stamp on line 1 — an unstamped copy cannot be told from a stale one'); continue; }
37
+ if (!packVersion) { row('DEGRADE', `docs/_meta/${name}`, `stamped ${m[1]}; the pack's own version could not be read`); continue; }
38
+ const cmp = compareVersions(m[1], packVersion);
39
+ if (cmp < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} — diff against the pack's references/${name}`);
40
+ else row('ok', `docs/_meta/${name}`, `stamp ${m[1]}`);
41
+ }
42
+
43
+ // ── 3. doc-index and its three repo slots ───────────────────────────────────────────────────────
44
+ {
45
+ const p = join(repo, 'docs', '_meta', 'doc-index.md');
46
+ const text = read(p);
47
+ if (text === null) row('FAIL', 'docs/_meta/doc-index.md', 'missing');
48
+ else {
49
+ for (const heading of ['## Role reading lists', '## Apps and their build commands', '## The evidence tools']) {
50
+ const section = sectionOf(text, heading);
51
+ if (section === null) row('FAIL', `doc-index.md ${heading}`, 'section missing');
52
+ else if (/fill this in|\(none detected\)/i.test(section)) row('DEGRADE', `doc-index.md ${heading}`, 'present, carries only the absent-line');
53
+ else row('ok', `doc-index.md ${heading}`, '');
54
+ }
55
+ }
56
+ }
57
+
58
+ // ── 4. the reading floor and the conditional docs ───────────────────────────────────────────────
59
+ const FLOOR = ['patterns/vertical-slice-anatomy.md', 'patterns/backend-patterns.md', 'patterns/frontend-patterns.md', 'patterns/testing.md', 'patterns/codegen.md'];
60
+ const CONDITIONAL = ['patterns/state-management.md', 'patterns/ui-style-guide.md', 'infrastructure/realtime-events.md', 'infrastructure/background-work.md'];
61
+ for (const d of FLOOR) docRow(d, true);
62
+ for (const d of CONDITIONAL) docRow(d, false);
63
+ function docRow(relPath, required) {
64
+ const text = read(join(repo, 'docs', relPath));
65
+ if (text === null) { row(required ? 'FAIL' : 'ok', `docs/${relPath}`, required ? 'missing' : 'absent (drafted only when the scan finds the concern)'); return; }
66
+ const meta = /<!-- meta:[^>]*status=([a-z]+)/.exec(text);
67
+ if (!meta) row('DEGRADE', `docs/${relPath}`, 'no <!-- meta --> line');
68
+ else if (meta[1] === 'draft') row('DEGRADE', `docs/${relPath}`, 'status=draft — scanned, not decided');
69
+ else row('ok', `docs/${relPath}`, `status=${meta[1]}`);
70
+ if (/<!-- init:/.test(text)) row('DEGRADE', `docs/${relPath}`, 'an <!-- init: --> instruction was left in the file');
71
+ }
72
+
73
+ // ── 5. CLAUDE.md / AGENTS.md stanza, settings pin ───────────────────────────────────────────────
74
+ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
75
+ const text = read(join(repo, name));
76
+ if (text === null) { if (name === 'CLAUDE.md') row('DEGRADE', name, 'missing (init creates it with the stanza)'); continue; }
77
+ row(text.includes('<!-- naswerks-loop:start-here -->') ? 'ok' : 'DEGRADE', `${name} stanza`, text.includes('<!-- naswerks-loop:start-here -->') ? '' : 'the Start-here stanza is not present');
78
+ }
79
+ {
80
+ const p = join(repo, '.claude', 'settings.json');
81
+ const text = read(p);
82
+ if (text === null) row('DEGRADE', '.claude/settings.json', 'missing (init writes the plugin pin)');
83
+ else {
84
+ try {
85
+ const j = JSON.parse(text);
86
+ const mk = j.extraKnownMarketplaces && j.extraKnownMarketplaces['engineering-loop'];
87
+ const en = j.enabledPlugins && j.enabledPlugins['naswerks@engineering-loop'] === true;
88
+ row(mk && en ? 'ok' : 'DEGRADE', '.claude/settings.json pin', mk && en ? '' : `${mk ? '' : 'extraKnownMarketplaces.engineering-loop missing; '}${en ? '' : 'enabledPlugins["naswerks@engineering-loop"] missing'}`);
89
+ } catch { row('DEGRADE', '.claude/settings.json', 'not valid JSON'); }
90
+ }
91
+ }
92
+
93
+ // ── 6. verify-staged resolvable ─────────────────────────────────────────────────────────────────
94
+ {
95
+ let ok = false;
96
+ for (const [cmd, args] of [['verify-staged', ['--help']], ['npx', ['--no-install', 'verify-staged', '--help']]]) {
97
+ try { execFileSync(cmd, args, { stdio: 'ignore', shell: process.platform === 'win32', timeout: 15000 }); ok = true; break; } catch { /* next */ }
98
+ }
99
+ const packBin = join(packRoot, 'bin', 'verify-staged.mjs');
100
+ if (ok) row('ok', 'verify-staged', 'on PATH');
101
+ else if (existsSync(packBin)) row('DEGRADE', 'verify-staged', `not on PATH; the skills' fallback is: node ${packBin.replace(/\\/g, '/')} {list}`);
102
+ else row('DEGRADE', 'verify-staged', 'not on PATH and the pack bin was not found — install the pack (npm i -g @naswerks/engineering-loop)');
103
+ }
104
+
105
+ // ── 7. every ignition brief, checked the way the control plane checks it ────────────────────────
106
+ const KINDS = new Set(['coordinator', 'build', 'fixit', 'review', 'retro', 'witness', 'docs-process']);
107
+ const researchDir = join(repo, 'docs', 'research');
108
+ if (existsSync(researchDir)) {
109
+ for (const topic of readdirSync(researchDir)) {
110
+ const briefPath = join(researchDir, topic, '00-ignition-brief.md');
111
+ if (!statSync(join(researchDir, topic)).isDirectory() || !existsSync(briefPath)) continue;
112
+ const text = read(briefPath);
113
+ const problems = [];
114
+ const kick = sectionOf(text, '## The kick');
115
+ if (kick === null) problems.push('the brief has no `## The kick` section, so the task text could not be read');
116
+ 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');
117
+ const seq = sectionOf(text, '## The sequence');
118
+ if (seq === null) problems.push('the brief has no `## The sequence` section, so no seats could be read');
119
+ else {
120
+ const tableRows = seq.split('\n').filter((l) => l.trim().startsWith('|'));
121
+ if (tableRows.length === 0) problems.push('the `## The sequence` section holds no table, so no seats could be read');
122
+ for (const line of tableRows.slice(1)) {
123
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim().replace(/^`|`$/g, '').trim());
124
+ if (cells.every((c) => /^[-: ]*$/.test(c))) continue;
125
+ if (!cells.some((c) => KINDS.has(c))) { problems.push(`a sequence row names no seat kind and composes nothing: \`${line.trim()}\``); continue; }
126
+ const spec = cells.map((c) => /\b(\d{2}-[A-Za-z0-9._-]*?\.md)\b/.exec(c)).find(Boolean);
127
+ if (spec && !existsSync(join(researchDir, topic, spec[1]))) problems.push(`the sequence names \`${spec[1]}\` and it is missing from disk`);
128
+ }
129
+ }
130
+ if (problems.length) for (const pr of problems) row('FAIL', `docs/research/${topic}/00-ignition-brief.md`, pr);
131
+ else row('ok', `docs/research/${topic}/00-ignition-brief.md`, 'the kick and the sequence read clean');
132
+ }
133
+ }
134
+
135
+ // ── 8. the working queue's shape ────────────────────────────────────────────────────────────────
136
+ const workingDir = join(repo, 'docs', 'working');
137
+ if (existsSync(workingDir)) {
138
+ for (const f of readdirSync(workingDir).filter((f) => f.endsWith('.md') && f !== 'README.md')) {
139
+ const text = read(join(workingDir, f));
140
+ const missing = ['## Living Docs to Update', '## Archive'].filter((h) => !text.includes(h));
141
+ row(missing.length ? 'DEGRADE' : 'ok', `docs/working/${f}`, missing.length ? `lacks ${missing.join(' and ')}` : 'carries its checklist and its archive target');
142
+ }
143
+ }
144
+
145
+ // ── 9. anything else under docs/ ────────────────────────────────────────────────────────────────
146
+ if (existsSync(join(repo, 'docs'))) {
147
+ for (const entry of readdirSync(join(repo, 'docs'))) {
148
+ if (FOLDERS.includes(entry)) continue;
149
+ row('ok', `docs/${entry}`, 'outside the loop\'s shape — not touched');
150
+ }
151
+ }
152
+
153
+ // ── report ──────────────────────────────────────────────────────────────────────────────────────
154
+ const width = Math.max(...rows.map((r) => r.subject.length), 10);
155
+ console.log(`doctor — ${repo}${packVersion ? ` — pack ${packVersion}` : ''}\n`);
156
+ console.log(`| verdict | subject${' '.repeat(width - 7)} | detail |`);
157
+ console.log(`|---------|${'-'.repeat(width + 2)}|--------|`);
158
+ for (const r of rows) console.log(`| ${r.verdict.padEnd(7)} | ${r.subject.padEnd(width)} | ${r.detail} |`);
159
+ const count = (v) => rows.filter((r) => r.verdict === v).length;
160
+ console.log(`\nFAIL ${count('FAIL')} · DEGRADE ${count('DEGRADE')} · ok ${count('ok')}`);
161
+ process.exit(count('FAIL') ? 1 : 0);
162
+
163
+ // ── helpers ─────────────────────────────────────────────────────────────────────────────────────
164
+ function sectionOf(text, heading) {
165
+ const lines = text.split('\n');
166
+ const start = lines.findIndex((l) => l.trimEnd().toLowerCase().startsWith(heading.toLowerCase()));
167
+ if (start < 0) return null;
168
+ const body = [];
169
+ for (let i = start + 1; i < lines.length; i++) { if (lines[i].startsWith('## ')) break; body.push(lines[i]); }
170
+ return body.join('\n');
171
+ }
172
+ function compareVersions(a, b) {
173
+ const pa = a.split(/[.-]/).map((x) => parseInt(x, 10) || 0), pb = b.split(/[.-]/).map((x) => parseInt(x, 10) || 0);
174
+ for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
175
+ return 0;
176
+ }
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: spec-child
3
+ description: The BUILD JOB for a pipeline seat — one spec, the role's reading list, the pin taxonomy, the suite protocol, and the fixit variant. Composed AFTER spec-seat, which carries the seat contract (blackboard, question lane, verdicts, FINAL TLDR, close-out). Use when a prompt says you are a build or fixit session, or run spec-child {role} {spec-path}. Roles: backend | frontend | mixed.
4
+ ---
5
+
6
+ # spec-child
7
+
8
+ **Your seat contract is `spec-seat` — invoke it first if you have not.** It carries the blackboard, the
9
+ question lane, the parked-call discriminator, the verdict vocabulary, the BELIEFS sha, git shapes, the
10
+ six-section FINAL TLDR, and the close-out order. **This file is the BUILD JOB only**, and it refines that
11
+ contract — where the two genuinely conflict, that is a finding to report, not a fork to resolve quietly.
12
+
13
+ You build exactly one spec. The coordinator judges and rules; you recon, plan, build, and report honestly.
14
+ Hand-cranked, a human relays messages; hosted inside a pipeline, your coordinator drives you itself and
15
+ the relay is machine-carried — **your contract is identical either way.**
16
+
17
+ Invoked as `spec-child {role} {spec-path}`, or told "you are a build session" with the spec path.
18
+
19
+ ## Step 1 — with your role and spec in hand
20
+
21
+ **If your prompt handed you a MANIFEST, that manifest is your reading list and this section does not
22
+ apply** (`spec-seat`, Step 0). This list is for a build seat working from a spec.
23
+
24
+ 1. **Your spec** — fully, INCLUDING any Addendum sections. It is self-contained.
25
+ 2. **The umbrella** (`01-{topic}.md`) — settled decisions; don't relitigate them.
26
+ 3. **The tracker** — `docs/research/{topic}/project-status.md`. The coordinator's live journal and
27
+ READ-ONLY to you. Prior session rows, deferred findings, and process incidents are your contract: build
28
+ against what ACTUALLY shipped, not the spec's guesses. **If anything in it disagrees with your spec or
29
+ doesn't make sense, say so in your PLAN TLDR** — asking is always allowed.
30
+ 4. **Your role's adds** (below) + any extra docs the prompt names.
31
+ 5. **Grep the living docs' `## Open Issues` for your subject.** One command, and it is the step efforts
32
+ keep skipping: a defect can sit dated and described in a living doc while several seats check the code
33
+ and conclude it is new.
34
+
35
+ ## Role — the adds, on top of spec-seat's Step 0 floor
36
+
37
+ | Role | Adds |
38
+ |---|---|
39
+ | **frontend** | `patterns/state-management.md` · `patterns/ui-style-guide.md` · `infrastructure/realtime-events.md` (the poke/refetch concept trips sessions up — read it even for "pure UI" work) · plus your role's rows in `docs/_meta/doc-index.md` § Role reading lists |
40
+ | **backend** | `infrastructure/realtime-events.md` (the poke/refetch concept trips sessions up — read it even for "pure API" work) · `infrastructure/background-work.md` as the task warrants · plus your role's rows in `docs/_meta/doc-index.md` § Role reading lists |
41
+ | **mixed** | **NOT every set by default** — read the adds for the surfaces your spec ACTUALLY touches, decided at recon once you have read the spec. **This is the default role and it covers every surface with no role of its own** — a surface the repo names only in `docs/_meta/doc-index.md` § Role reading lists (a sidecar, a CLI, a worker) reads the row there; docs work needs nothing beyond the Step 0 floor |
42
+
43
+ A role is *which docs does this build session read* — nothing else. Jobs are SEAT KINDS, not roles:
44
+ `docs-process` is a session type of its own, and a spec that touches a surface with no role of its own is
45
+ `mixed` with that surface's row. A doc-index row that says "no X detected" is an honest answer from `init`:
46
+ that concern is not in this repo yet, and there is nothing to read for it.
47
+
48
+ ## Recon — the build seat's first act
49
+
50
+ **Re-verify EVERY anchor your spec cites against HEAD** — specs age and prior sessions move line numbers,
51
+ so **report drift, don't trust**. **Recon's first act is to test whether the slice is already satisfied**:
52
+ specs do get overtaken between authoring and build, and discovering that at recon closes a session in
53
+ minutes while assuming the work is pending rebuilds what already exists.
54
+
55
+ Run `git status`; identify which dirty files are yours-to-be vs NOT yours (coordinator-owned docs appear
56
+ mid-run — never stage them). Answer any STANDING RECON QUESTION your spec or prompt carries.
57
+
58
+ **Measure your own baseline for any lane you touch, BEFORE you edit.** A same-day coordinator-verified
59
+ baseline is an acceptable substitute when cited, but measuring your own is the norm — and a number you
60
+ cannot reproduce is a finding.
61
+
62
+ ## Build
63
+
64
+ State your HEAD sha again in your FIRST implementation message (the coordinator diffs your beliefs
65
+ against its ledger — stale-tree catches come from this).
66
+
67
+ **If HEAD moves mid-build**: disclose FIRST in your next report — old sha to new sha, clean-ancestor or
68
+ not, `git diff --stat` overlap with YOUR files, and proof of coexistence if any overlap (marker greps +
69
+ suite on the combined tree). **Absorb-and-prove, never silently rebase your beliefs.**
70
+
71
+ **Before you delete a pin, ask what it encodes.** A pin that describes the **MECHANISM** you are removing
72
+ dies with it ("this verb is seat evidence"). A pin that encodes a **DECISION** — a boundary verdict, an
73
+ accepted residual, a known-positive control — dies with nothing, and removing it vacates a ruling nobody
74
+ made. **When in doubt it is a DECISION: say so and ask.**
75
+
76
+ **If ANY existing test requires modification to pass, STOP and report.** A test needing an edit is the
77
+ signal that a VERDICT changed, not that the test is stale.
78
+
79
+ ## The suite protocol
80
+
81
+ Run the suite lanes **SERIALLY** — the lanes and their exact invocations are `patterns/testing.md`
82
+ § Running tests; concurrent runs flake.
83
+
84
+ **Capture FULL runner output to a UTF-8 file and search failures by name** — redirect to a file, then
85
+ search `^failed ` / `Failed!`. Piping through a "last N lines" filter destroys failure names.
86
+
87
+ ### USE `rg`, NOT `grep`, FOR ANYTHING THAT MIGHT NOT BE ASCII — AND NEVER TRUST A LOCAL TEST
88
+
89
+ **Two instruments return `0` for text that is present.** Both are silent: they exit clean and read as
90
+ "not found", which is indistinguishable from a correct negative.
91
+
92
+ | instrument | the lie | measured |
93
+ |---|---|---|
94
+ | `grep` on a non-ASCII pattern | **locale-dependent** — the SAME binary on the SAME file | `LANG` unset: 64 · `LC_ALL=C`: 64 · `LANG=en_US.UTF-8`: 0 |
95
+ | PowerShell `Get-Content -Raw` | reads UTF-8 as the ANSI codepage | with `-Encoding utf8`: 83 · without: 0 |
96
+
97
+ **THE LOCALE ONE IS WHY "IT WORKS FOR ME" IS NOT EVIDENCE.** Whether your shell exports `LANG` is not
98
+ something you chose or can see, two seats on one machine can disagree, and an ASCII control passes in both
99
+ — so a test that "proves" grep is fine proves only that your environment is the lucky one. `rg` is
100
+ unaffected under every locale above.
101
+
102
+ **So:** `rg` for content search, always. `Get-Content` only with `-Encoding utf8`. And author regexes with
103
+ your file tools, never through a shell heredoc — a heredoc can collapse `\\s` to `\s` and break both a pin
104
+ and that pin's own control, so neither can announce the other.
105
+
106
+ **PowerShell 5.1 `Tee-Object` writes UTF-16** — `rg` detects the BOM and reads it; plain `grep` finds
107
+ nothing. Prefer a UTF-8 path anyway so the file is legible to everything.
108
+
109
+ **Read the build's exit code BEFORE trusting a green summary** — a stale binary passes every assertion
110
+ in it. And **require the summary block itself**: a run that printed no totals ran nothing, whatever its
111
+ exit code said. Disclose every red run WITH its rerun; never present a flake as clean.
112
+
113
+ **Any session that touched frontend code ALSO runs the production build** — a test transform does NOT
114
+ catch what the strict production compiler rejects: duplicate class members, cross-session member
115
+ collisions, private-in-template. A green test run can sit over a broken build. The build receipt goes in
116
+ FINAL TLDR section (b) alongside the suite.
117
+
118
+ **`cd` INTO THE APP YOU TOUCHED — a repo can hold several, and the wrong one builds green.** The apps
119
+ and their test and production-build commands are `docs/_meta/doc-index.md` § Apps and their build
120
+ commands. An instruction that named ONE app for years sent seats on other surfaces to run a production
121
+ build over a tree they had not touched, and they got a **passing receipt that proved nothing** — the
122
+ false-green genus, embedded in the manual instead of in a test. Name the app you edited and run its
123
+ commands from its root.
124
+
125
+ ## Fix-it variant (`spec-child fixit`)
126
+
127
+ No spec — your contract is the prompt + the coordinator's ledger (READ-ONLY). **STEP 0 — INVESTIGATE,
128
+ REPORT, STOP:** (1) reproduce/characterize cheaply, (2) find the cheapest reliable RECEIPT for the fixed
129
+ state, (3) report the verdict as plain text — root cause, evidence, proposed fix shape + blast radius —
130
+ then STOP for the coordinator's go. After the go: the minimal fix + MAKE THE FAILURE LOUD (log + trace row
131
+ — never silent) + a regression pin + an E2E receipt re-running the original failing scenario
132
+ + **SWEEP THE RECEIPTS YOUR FIX INVALIDATED** (`spec-seat`'s close-out step, and a fixit is the diff
133
+ most likely to need it — you are changing behaviour something already made a claim about). A fixit that
134
+ repairs a pane silently re-makes a `superseded` live-check TRUE, so its "do not run this" marker becomes
135
+ false. **Grep `live-checks.md`, the tracker and the working docs for every behaviour you changed**,
136
+ correct what is yours in the same commit, and hand back what is not.
137
+
138
+ **A fixit chartered from a review's findings inherits no escape hatch** — every numbered spec carries one
139
+ because `spec-pipeline` requires it, and you have no spec. If the charter you were handed did not state
140
+ what a truthful partial looks like per named item, **ask for it** before you start designing.
141
+
142
+ ## Notes
143
+
144
+ - **The spec's anchors are hypotheses; HEAD is the truth.** The biggest catches — stale trees, false
145
+ greens, dropped rulings — are all belief-vs-ledger mismatches. Your beliefs block exists so the
146
+ coordinator can catch them cheaply.
147
+ - Counterpart skills: `spec-seat` (your contract) · `spec-parent` (the seat that arms you) ·
148
+ `spec-pipeline` (how your spec came to exist). The hosted version of you runs THIS same pair — the
149
+ control plane's seed names both skills, in this order.