@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +199 -0
- package/README.md +12 -7
- package/bin/verify-staged.mjs +136 -23
- package/bin/witness-acts.mjs +64 -0
- package/bin/witness-bodycheck.mjs +78 -0
- package/bin/witness-edits.mjs +60 -0
- package/bin/witness-extract-cmd.mjs +57 -0
- package/bin/witness-extract-seed.mjs +65 -0
- package/bin/witness-file-ledger.mjs +57 -0
- package/bin/witness-gaps.mjs +57 -0
- package/bin/witness-latency.mjs +55 -0
- package/bin/witness-lib.mjs +124 -0
- package/bin/witness-plane-window.mjs +57 -0
- package/bin/witness-procs.mjs +71 -0
- package/bin/witness-spawn-task.mjs +61 -0
- package/bin/witness-tx.mjs +60 -0
- package/bin/witness-watch.mjs +405 -0
- package/bin/witness-writes.mjs +64 -0
- package/package.json +1 -1
- package/references/docs-workflow.md +44 -24
- package/references/engineering-loop.md +42 -24
- package/skills/docs-audit-full/SKILL.md +2 -1
- package/skills/docs-process/SKILL.md +13 -7
- package/skills/docs-write/SKILL.md +1 -1
- package/skills/init/scripts/doctor.mjs +43 -5
- package/skills/init/scripts/owned.mjs +10 -0
- package/skills/spec-build/SKILL.md +12 -4
- package/skills/spec-coordinator/SKILL.md +149 -80
- package/skills/spec-ignite/SKILL.md +25 -21
- package/skills/spec-pipeline/SKILL.md +151 -82
- package/skills/spec-retro/SKILL.md +14 -9
- package/skills/spec-review/SKILL.md +32 -14
- package/skills/spec-seat/SKILL.md +107 -48
- package/skills/spec-witness/SKILL.md +267 -212
- 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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
200
|
-
specs/ <- the spec pipeline: 00-ignition-brief · 01
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
|
434
|
-
written whole at authoring, verified at ignition) · `01
|
|
435
|
-
|
|
436
|
-
section notes) · `live-checks.md` (the
|
|
437
|
-
Each per-
|
|
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: [{
|
|
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
|
|
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 (
|
|
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,
|
|
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
|
|
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.
|
|
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}/:
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
70
|
-
|
|
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
|
|
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,
|
|
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
|
|
126
|
-
| **Witness** *(optional, `spec-witness`)* | a
|
|
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
|
|
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
|
|
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,
|
|
188
|
-
|
|
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)
|
|
191
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
22
|
-
— never infer membership from the folder.** A seat that
|
|
23
|
-
undo it. If your prompt did not name them, ask before you move
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
232
|
+
const cells = cellsOf(line);
|
|
218
233
|
if (cells.every((c) => /^[-: ]*$/.test(c))) continue;
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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.
|