@naswerks/engineering-loop 0.2.1 → 0.5.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 +2 -6
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +94 -0
- package/README.md +49 -11
- package/package.json +1 -1
- package/references/docs-workflow.md +119 -10
- package/references/engineering-loop.md +27 -2
- package/skills/docs-audit-feature/SKILL.md +24 -20
- package/skills/docs-audit-full/SKILL.md +17 -9
- package/skills/docs-backlog/SKILL.md +6 -3
- package/skills/docs-process/SKILL.md +67 -66
- package/skills/docs-status/SKILL.md +25 -15
- package/skills/docs-write/SKILL.md +9 -6
- package/skills/init/SKILL.md +88 -22
- package/skills/init/scripts/doctor.mjs +131 -18
- package/skills/init/scripts/owned.mjs +96 -0
- package/skills/init/scripts/refresh.mjs +129 -0
- package/skills/spec-child/SKILL.md +1 -1
- package/skills/spec-parent/SKILL.md +20 -9
- package/skills/spec-pipeline/SKILL.md +23 -13
- package/skills/spec-retro/SKILL.md +5 -2
- package/skills/spec-review/SKILL.md +10 -6
- package/skills/spec-seat/SKILL.md +11 -6
- package/templates/docs/_meta/doc-index.md +7 -1
- package/templates/docs/infrastructure/background-work.md +1 -1
- package/templates/docs/infrastructure/realtime-events.md +1 -1
- package/templates/docs/patterns/backend-patterns.md +1 -1
- package/templates/docs/patterns/code-organization.md +1 -1
- package/templates/docs/patterns/codegen.md +1 -1
- package/templates/docs/patterns/design-tokens.md +1 -1
- package/templates/docs/patterns/frontend-patterns.md +1 -1
- package/templates/docs/patterns/long-running-workflows.md +1 -1
- package/templates/docs/patterns/state-management.md +1 -1
- package/templates/docs/patterns/testing.md +1 -1
- package/templates/docs/patterns/ui-style-guide.md +1 -1
|
@@ -5,6 +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, compareVersions, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
|
|
8
9
|
|
|
9
10
|
const repo = resolve(process.argv[2] || process.cwd());
|
|
10
11
|
const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
@@ -27,17 +28,54 @@ for (const f of FOLDERS) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// ── 2. the two method docs and their stamp ──────────────────────────────────────────────────────
|
|
31
|
+
// A repository rewrites the repository-owned sections of these two files to its own conventions, and the
|
|
32
|
+
// loop text around them is its copy from whenever it was taken — the doctor grades neither. It reads the
|
|
33
|
+
// version stamp (line 1, or the first line after a leading frontmatter block when the repository's own gate
|
|
34
|
+
// requires one on every file under docs/) and whether each repository-owned heading is where a skill looks.
|
|
30
35
|
const STAMP = /^<!-- naswerks-loop: version=([0-9]+\.[0-9]+\.[0-9]+[^ ]*) -->/m;
|
|
31
36
|
for (const name of ['docs-workflow.md', 'engineering-loop.md']) {
|
|
32
37
|
const p = join(repo, 'docs', '_meta', name);
|
|
33
38
|
const text = read(p);
|
|
34
39
|
if (text === null) { row('FAIL', `docs/_meta/${name}`, 'missing'); continue; }
|
|
35
|
-
const m = STAMP.exec(text
|
|
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; }
|
|
40
|
+
const m = STAMP.exec(firstBodyLine(text));
|
|
41
|
+
if (!m) { row('FAIL', `docs/_meta/${name}`, 'no naswerks-loop stamp on line 1 (or on the first line after a frontmatter block) — an unstamped copy cannot be told from a stale one'); continue; }
|
|
37
42
|
if (!packVersion) { row('DEGRADE', `docs/_meta/${name}`, `stamped ${m[1]}; the pack's own version could not be read`); continue; }
|
|
38
43
|
const cmp = compareVersions(m[1], packVersion);
|
|
39
|
-
if (cmp < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} —
|
|
44
|
+
if (cmp < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} — run init refresh: it adds this version's repository-owned sections and restamps, and never touches the loop text`);
|
|
40
45
|
else row('ok', `docs/_meta/${name}`, `stamp ${m[1]}`);
|
|
46
|
+
|
|
47
|
+
// ── 2b. the repository-owned sections: present under the heading a skill looks them up by ─────────
|
|
48
|
+
const lines = linesOf(text);
|
|
49
|
+
const headings = h2Index(lines).map((h) => h.heading);
|
|
50
|
+
for (const space of OWNED.filter((o) => o.file === name)) {
|
|
51
|
+
if (headings.includes(space.heading)) row('ok', `docs/_meta/${name} ${space.heading}`, 'present');
|
|
52
|
+
else {
|
|
53
|
+
const was = space.earlier.find((h) => headings.includes(h));
|
|
54
|
+
row('DEGRADE', `docs/_meta/${name} ${space.heading}`, was
|
|
55
|
+
? `unmarked (the copy says "${was}") — a skill finds this section by its heading; init refresh marks it`
|
|
56
|
+
: 'missing — a skill finds this section by its heading; init refresh adds it');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── 2c. the rails: every item the pack names is present, and none still carries its absent-line ───
|
|
61
|
+
const rails = OWNED.find((o) => o.file === name && o.heading.includes('rails'));
|
|
62
|
+
const sec = rails ? findSection(lines, rails.heading) : null;
|
|
63
|
+
if (sec) {
|
|
64
|
+
const section = lines.slice(sec.start, sec.end);
|
|
65
|
+
const items = railsItems(section);
|
|
66
|
+
const packItems = (() => {
|
|
67
|
+
const t = read(join(packRoot, 'references', name));
|
|
68
|
+
if (t === null) return [];
|
|
69
|
+
const pl = linesOf(t);
|
|
70
|
+
const ps = findSection(pl, rails.heading);
|
|
71
|
+
return ps ? railsItems(pl.slice(ps.start, ps.end)).map((it) => it.name) : [];
|
|
72
|
+
})();
|
|
73
|
+
for (const n of packItems.filter((n) => !items.some((it) => it.name.toLowerCase() === n.toLowerCase()))) {
|
|
74
|
+
row('DEGRADE', `rails: ${n}`, 'missing — init refresh adds it as its absent-line');
|
|
75
|
+
}
|
|
76
|
+
for (const it of items) row(/fill this in/i.test(it.text) ? 'DEGRADE' : 'ok', `rails: ${it.name}`, /fill this in/i.test(it.text) ? 'still the absent-line — fill it in (init drafts it from the scan)' : 'recorded');
|
|
77
|
+
if (!items.length && /fill this in/i.test(section.map(bare).join('\n'))) row('DEGRADE', 'rails', 'carries only the absent-line');
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
// ── 3. doc-index and its three repo slots ───────────────────────────────────────────────────────
|
|
@@ -56,6 +94,9 @@ for (const name of ['docs-workflow.md', 'engineering-loop.md']) {
|
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
// ── 4. the reading floor and the conditional docs ───────────────────────────────────────────────
|
|
97
|
+
// A doc's status is read from whichever header the repository writes: the loop's own <!-- meta --> line,
|
|
98
|
+
// or a YAML frontmatter block with a status key. The one word the doctor needs is `draft`, which both forms
|
|
99
|
+
// spell the same way.
|
|
59
100
|
const FLOOR = ['patterns/code-organization.md', 'patterns/backend-patterns.md', 'patterns/frontend-patterns.md', 'patterns/testing.md'];
|
|
60
101
|
const STANDARD = ['patterns/codegen.md', 'patterns/state-management.md', 'patterns/ui-style-guide.md', 'patterns/design-tokens.md', 'patterns/long-running-workflows.md', 'infrastructure/realtime-events.md', 'infrastructure/background-work.md'];
|
|
61
102
|
const roleRows = (() => { const t = read(join(repo, 'docs', '_meta', 'doc-index.md')); return t === null ? null : sectionOf(t, '## Role reading lists'); })();
|
|
@@ -70,20 +111,53 @@ function docRow(relPath, required) {
|
|
|
70
111
|
else row('FAIL', `docs/${relPath}`, 'absent with no doc-index row saying it was not detected — a seat meeting this path has no answer');
|
|
71
112
|
return;
|
|
72
113
|
}
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
else if (
|
|
76
|
-
else row('ok', `docs/${relPath}`, `status=${
|
|
114
|
+
const status = statusOf(text);
|
|
115
|
+
if (status === null) row('DEGRADE', `docs/${relPath}`, 'no status in either header form (<!-- meta --> line or frontmatter status key)');
|
|
116
|
+
else if (status.value === 'draft') row('DEGRADE', `docs/${relPath}`, `status=draft (${status.layer}) — scanned, not decided`);
|
|
117
|
+
else row('ok', `docs/${relPath}`, `status=${status.value} (${status.layer})`);
|
|
77
118
|
if (/<!-- init:/.test(text)) row('DEGRADE', `docs/${relPath}`, 'an <!-- init: --> instruction was left in the file');
|
|
78
119
|
}
|
|
79
120
|
|
|
80
|
-
// ── 4b.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
// ── 4b. the menu: its shape, and every doc it names ────────────────────────────────────────────
|
|
122
|
+
// doc-index.md declares one of two shapes (docs-workflow.md § The repository-owned sections): menu=all lists
|
|
123
|
+
// every living doc; menu=curated lists the docs the seats read and names the index that lists every doc. A
|
|
124
|
+
// menu with no shape line is menu=all. Every doc a menu names must exist (a row saying `not detected` is
|
|
125
|
+
// exempt); a curated menu's docs must also meet the loop's standard: a status in either header form, not
|
|
126
|
+
// draft, and a `## Key Files` section.
|
|
127
|
+
{
|
|
128
|
+
const menu = read(join(repo, 'docs', '_meta', 'doc-index.md'));
|
|
129
|
+
if (menu !== null) {
|
|
130
|
+
const line = /<!-- naswerks-loop: menu=([A-Za-z-]+)(?:;\s*index=(\S+?))?\s*-->/.exec(menu);
|
|
131
|
+
const shape = line ? line[1] : 'all';
|
|
132
|
+
if (!['all', 'curated'].includes(shape)) row('FAIL', 'menu shape', `menu=${shape} is not a shape the loop knows (all | curated)`);
|
|
133
|
+
else if (shape === 'curated') {
|
|
134
|
+
const index = line[2];
|
|
135
|
+
if (!index) row('DEGRADE', 'menu shape', 'menu=curated names no index of every doc, so docs-process cannot say where a new doc is registered');
|
|
136
|
+
else if (!existsSync(join(repo, index))) row('DEGRADE', 'menu shape', `menu=curated; its index ${index} does not exist`);
|
|
137
|
+
else row('ok', 'menu shape', `menu=curated; index=${index}`);
|
|
138
|
+
} else row('ok', 'menu shape', line ? 'menu=all' : 'menu=all (no shape line: the default)');
|
|
139
|
+
|
|
140
|
+
const named = new Set();
|
|
141
|
+
for (const l of menu.split('\n')) {
|
|
142
|
+
if (/not detected/i.test(l)) continue;
|
|
143
|
+
for (const m of l.matchAll(/(?:\]\(|`)(?:\.\.\/)?((?:guides|patterns|infrastructure|features)\/[A-Za-z0-9_./-]+\.md)/g)) named.add(m[1]);
|
|
144
|
+
}
|
|
145
|
+
const missing = [...named].filter((p) => !existsSync(join(repo, 'docs', p))).sort();
|
|
146
|
+
for (const p of missing) row('FAIL', `menu: docs/${p}`, 'the menu names a doc that does not exist');
|
|
147
|
+
if (shape === 'curated') {
|
|
148
|
+
for (const p of [...named].filter((q) => !missing.includes(q)).sort()) {
|
|
149
|
+
const text = read(join(repo, 'docs', p));
|
|
150
|
+
const short = [];
|
|
151
|
+
const st = statusOf(text);
|
|
152
|
+
if (st === null) short.push('no status in either header form');
|
|
153
|
+
else if (st.value === 'draft') short.push('status draft');
|
|
154
|
+
if (!/^## Key Files\b/m.test(text)) short.push('no ## Key Files');
|
|
155
|
+
row(short.length ? 'DEGRADE' : 'ok', `menu: docs/${p}`, short.length ? `below the loop's standard: ${short.join(', ')}` : "meets the loop's standard");
|
|
156
|
+
}
|
|
157
|
+
} else if (shape === 'all') {
|
|
158
|
+
const unnamed = livingDocs().filter((p) => !named.has(p) && !menu.includes(p));
|
|
159
|
+
for (const p of unnamed) row('DEGRADE', `menu: docs/${p}`, 'a living doc the menu does not name (menu=all) — add its row, or declare the menu curated');
|
|
160
|
+
if (!unnamed.length && !missing.length) row('ok', 'menu entries', `names every living doc (${named.size}), each present`);
|
|
87
161
|
}
|
|
88
162
|
}
|
|
89
163
|
}
|
|
@@ -187,8 +261,47 @@ function sectionOf(text, heading) {
|
|
|
187
261
|
for (let i = start + 1; i < lines.length; i++) { if (lines[i].startsWith('## ')) break; body.push(lines[i]); }
|
|
188
262
|
return body.join('\n');
|
|
189
263
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
264
|
+
// A doc's status from whichever header it carries: the <!-- meta --> line first, then a frontmatter
|
|
265
|
+
// `status:` key. Returns { value, layer } or null.
|
|
266
|
+
function statusOf(text) {
|
|
267
|
+
const m = /<!-- meta:[^>]*\bstatus=([A-Za-z-]+)/.exec(text);
|
|
268
|
+
if (m) return { value: m[1], layer: 'meta line' };
|
|
269
|
+
const fm = frontmatterOf(text);
|
|
270
|
+
if (fm && fm.status !== undefined) return { value: fm.status, layer: 'frontmatter' };
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
// The leading YAML frontmatter block as flat key: value pairs (scalars only — that is all the doctor reads).
|
|
274
|
+
function frontmatterOf(text) {
|
|
275
|
+
if (!text.startsWith('---\n')) return null;
|
|
276
|
+
const end = text.indexOf('\n---', 4);
|
|
277
|
+
if (end < 0) return null;
|
|
278
|
+
const out = {};
|
|
279
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
280
|
+
const m = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
|
|
281
|
+
if (m) out[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
// The living docs a menu=all menu lists: every .md directly under the four living folders, and each
|
|
286
|
+
// feature folder's own docs one level down (a filed effort's notes sit deeper and are history, not living).
|
|
287
|
+
function livingDocs() {
|
|
288
|
+
const out = [];
|
|
289
|
+
for (const f of ['guides', 'patterns', 'infrastructure', 'features']) {
|
|
290
|
+
const dir = join(repo, 'docs', f);
|
|
291
|
+
if (!existsSync(dir)) continue;
|
|
292
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
293
|
+
if (e.isFile() && e.name.endsWith('.md') && e.name !== 'README.md') out.push(`${f}/${e.name}`);
|
|
294
|
+
else if (f === 'features' && e.isDirectory()) {
|
|
295
|
+
for (const g of readdirSync(join(dir, e.name), { withFileTypes: true })) {
|
|
296
|
+
if (g.isFile() && g.name.endsWith('.md') && g.name !== 'README.md') out.push(`features/${e.name}/${g.name}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out.sort();
|
|
302
|
+
}
|
|
303
|
+
function firstBodyLine(text) {
|
|
304
|
+
let body = text;
|
|
305
|
+
if (text.startsWith('---\n')) { const end = text.indexOf('\n---', 4); if (end >= 0) body = text.slice(end + 4).replace(/^\n+/, ''); }
|
|
306
|
+
return body.split('\n', 1)[0];
|
|
194
307
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The repository-owned sections: the four spaces in the two method docs where a repository writes its own
|
|
2
|
+
// formats in place of the loop's. One list, read by the doctor, by refresh and by the pins, so the three can
|
|
3
|
+
// never disagree about what a space is. Everything else a space means (what the skills read from it, where
|
|
4
|
+
// the method docs echo it, the rails items) is read from the pack's own references/, never restated here.
|
|
5
|
+
|
|
6
|
+
// The one grep that lists every space, and nothing else, with its line number.
|
|
7
|
+
export const GREP = "rg -n '^#+ .*\\(repository-owned\\)' docs/_meta/";
|
|
8
|
+
export const MARKED = /^#+ .*\(repository-owned\)/;
|
|
9
|
+
|
|
10
|
+
// The line every skill that needs a format carries, verbatim, near its top.
|
|
11
|
+
export const FIND_FORMAT = "**Find the format first.** This repository writes its formats (the doc header and status words, where an effort is filed, its rails) in the sections `rg -n '^#+ .*\\(repository-owned\\)' docs/_meta/` lists; read the one a step names before that step writes. This skill names the section, never the format.";
|
|
12
|
+
|
|
13
|
+
// Each space by its heading, in the order the pack writes them, with the spellings a copy taken before the
|
|
14
|
+
// sections were marked still carries. `earlier` is the only fact here that the references cannot supply.
|
|
15
|
+
export const OWNED = [
|
|
16
|
+
{ file: 'docs-workflow.md', heading: '## The archive convention (repository-owned)', title: 'The archive convention', earlier: ['## The archive convention'] },
|
|
17
|
+
{ file: 'docs-workflow.md', heading: '## Doc metadata (repository-owned)', title: 'Doc metadata', earlier: ['## Doc metadata (header + lineage)', '## Doc metadata'] },
|
|
18
|
+
{ file: 'docs-workflow.md', heading: '## Templates (repository-owned)', title: 'Templates', earlier: ['## Templates'] },
|
|
19
|
+
{ file: 'engineering-loop.md', heading: "## This repository's rails (repository-owned)", title: "This repository's rails", earlier: [] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const STAMP = /^(<!-- naswerks-loop: version=)([0-9]+\.[0-9]+\.[0-9]+[^ ]*)( -->)/;
|
|
23
|
+
|
|
24
|
+
export function compareVersions(a, b) {
|
|
25
|
+
const pa = a.split(/[.-]/).map((x) => parseInt(x, 10) || 0), pb = b.split(/[.-]/).map((x) => parseInt(x, 10) || 0);
|
|
26
|
+
for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A file as lines that keep their own terminators, so an untouched line is written back byte for byte.
|
|
31
|
+
export function linesOf(text) {
|
|
32
|
+
return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
33
|
+
}
|
|
34
|
+
export const bare = (line) => line.replace(/\r?\n$/, '').replace(/^/, '');
|
|
35
|
+
export const eolOf = (text) => (text.includes('\r\n') ? '\r\n' : '\n');
|
|
36
|
+
|
|
37
|
+
// The index of every H2 heading outside a fenced block. The Templates space holds fenced examples whose
|
|
38
|
+
// own `## ` lines are template text, not sections.
|
|
39
|
+
export function h2Index(lines) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let fence = null;
|
|
42
|
+
lines.forEach((raw, i) => {
|
|
43
|
+
const line = bare(raw);
|
|
44
|
+
const f = /^\s*(```|~~~)/.exec(line);
|
|
45
|
+
if (f) { fence = fence === null ? f[1] : (fence === f[1] ? null : fence); return; }
|
|
46
|
+
if (fence === null && /^## /.test(line)) out.push({ i, heading: line.trimEnd() });
|
|
47
|
+
});
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A section by heading: the heading line's index and the index of the next H2 (or the end of the file).
|
|
52
|
+
export function findSection(lines, heading) {
|
|
53
|
+
const hs = h2Index(lines);
|
|
54
|
+
const k = hs.findIndex((h) => h.heading === heading);
|
|
55
|
+
if (k < 0) return null;
|
|
56
|
+
return { start: hs[k].i, end: k + 1 < hs.length ? hs[k + 1].i : lines.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The rails items a section carries, by the bold lead-in of each top-level bullet: `- **Policy.** ...`.
|
|
60
|
+
export function railsItems(sectionLines) {
|
|
61
|
+
const items = [];
|
|
62
|
+
for (let i = 0; i < sectionLines.length; i++) {
|
|
63
|
+
const m = /^- \*\*(.+?)\.?\*\*/.exec(bare(sectionLines[i]));
|
|
64
|
+
if (!m) continue;
|
|
65
|
+
let j = i + 1;
|
|
66
|
+
while (j < sectionLines.length && /^\s+\S/.test(bare(sectionLines[j]))) j++;
|
|
67
|
+
items.push({ name: m[1].replace(/\.$/, ''), text: sectionLines.slice(i, j).join(''), first: i, last: j - 1 });
|
|
68
|
+
}
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The stamp line: line 1, or the first non-blank line after a leading frontmatter block.
|
|
73
|
+
export function stampIndex(lines) {
|
|
74
|
+
let i = 0;
|
|
75
|
+
if (bare(lines[0] ?? '') === '---') {
|
|
76
|
+
i = 1;
|
|
77
|
+
while (i < lines.length && bare(lines[i]) !== '---') i++;
|
|
78
|
+
i++;
|
|
79
|
+
while (i < lines.length && bare(lines[i]).trim() === '') i++;
|
|
80
|
+
}
|
|
81
|
+
return i < lines.length && STAMP.test(bare(lines[i])) ? i : -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The map's rows in the pack's docs-workflow.md: what the skills read from each space, and where the method
|
|
85
|
+
// docs echo it. Keyed by the space's title.
|
|
86
|
+
export function mapOf(workflowText) {
|
|
87
|
+
const lines = linesOf(workflowText.replace(/\r\n/g, '\n'));
|
|
88
|
+
const sec = findSection(lines, '## The repository-owned sections');
|
|
89
|
+
const out = {};
|
|
90
|
+
if (!sec) return out;
|
|
91
|
+
for (const raw of lines.slice(sec.start, sec.end)) {
|
|
92
|
+
const m = /^\|\s*\*\*(.+?)\*\*[^|]*\|([^|]*)\|([^|]*)\|/.exec(bare(raw));
|
|
93
|
+
if (m) out[m[1].trim()] = { reads: m[2].trim(), echoed: m[3].trim() };
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// refresh — bring a repository's copies of the two method docs up to this pack version by writing ONLY their
|
|
3
|
+
// repository-owned sections and the stamp. Every other line is left exactly as it is: the loop text in a
|
|
4
|
+
// copy is the repository's, whatever it has done to it. Dry run by default; --write applies the rows it
|
|
5
|
+
// printed. Node >= 22, no dependencies. Usage: node refresh.mjs [repo-root] [--write]
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, resolve, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { OWNED, STAMP, compareVersions, linesOf, bare, eolOf, h2Index, findSection, railsItems, stampIndex, mapOf } from './owned.mjs';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const write = args.includes('--write');
|
|
13
|
+
const repo = resolve(args.find((a) => !a.startsWith('--')) || process.cwd());
|
|
14
|
+
const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
15
|
+
const packVersion = JSON.parse(readFileSync(join(packRoot, '.claude-plugin', 'plugin.json'), 'utf8')).version;
|
|
16
|
+
const packText = (file) => readFileSync(join(packRoot, 'references', file), 'utf8').replace(/\r\n/g, '\n');
|
|
17
|
+
|
|
18
|
+
const rows = [];
|
|
19
|
+
const row = (file, subject, action) => rows.push({ file, subject, action });
|
|
20
|
+
let changedFiles = 0;
|
|
21
|
+
|
|
22
|
+
console.log(`init refresh — ${repo} — pack ${packVersion}`);
|
|
23
|
+
console.log('writes only the repository-owned sections and the stamp; every other line is left as it is.\n');
|
|
24
|
+
|
|
25
|
+
for (const file of ['docs-workflow.md', 'engineering-loop.md']) {
|
|
26
|
+
const path = join(repo, 'docs', '_meta', file);
|
|
27
|
+
const shown = `docs/_meta/${file}`;
|
|
28
|
+
if (!existsSync(path)) { row(shown, '(file)', 'missing — `init` writes it; refresh only updates a copy that exists'); continue; }
|
|
29
|
+
const original = readFileSync(path, 'utf8');
|
|
30
|
+
const eol = eolOf(original);
|
|
31
|
+
let lines = linesOf(original);
|
|
32
|
+
const sIdx = stampIndex(lines);
|
|
33
|
+
if (sIdx < 0) { row(shown, '(file)', 'no naswerks-loop stamp on its first line — not recognised as a copy of the method doc; nothing written'); continue; }
|
|
34
|
+
|
|
35
|
+
const packLines = linesOf(packText(file));
|
|
36
|
+
const withEol = (text) => text.replace(/\r?\n/g, eol);
|
|
37
|
+
let changed = false;
|
|
38
|
+
|
|
39
|
+
for (const space of OWNED.filter((o) => o.file === file)) {
|
|
40
|
+
const hs = h2Index(lines);
|
|
41
|
+
const present = hs.find((h) => h.heading === space.heading);
|
|
42
|
+
if (present) {
|
|
43
|
+
row(shown, space.heading, 'kept — yours, byte for byte');
|
|
44
|
+
} else {
|
|
45
|
+
const was = hs.find((h) => space.earlier.includes(h.heading));
|
|
46
|
+
if (was) {
|
|
47
|
+
const raw = lines[was.i];
|
|
48
|
+
lines[was.i] = space.heading + raw.slice(bare(raw).length);
|
|
49
|
+
row(shown, space.heading, `heading marked — was "${was.heading}"; only that line changes`);
|
|
50
|
+
changed = true;
|
|
51
|
+
} else {
|
|
52
|
+
// Insert the pack's own section before the first pack heading after it that this copy also has.
|
|
53
|
+
const pack = findSection(packLines, space.heading);
|
|
54
|
+
const packHs = h2Index(packLines);
|
|
55
|
+
const after = packHs.filter((h) => h.i > pack.start).map((h) => h.heading);
|
|
56
|
+
const anchor = after.map((h) => hs.find((c) => c.heading === h)).find(Boolean);
|
|
57
|
+
const chunk = linesOf(withEol(packLines.slice(pack.start, pack.end).join('')));
|
|
58
|
+
if (anchor) {
|
|
59
|
+
lines.splice(anchor.i, 0, ...chunk);
|
|
60
|
+
row(shown, space.heading, `added — the pack's default, before "${anchor.heading}"`);
|
|
61
|
+
} else {
|
|
62
|
+
if (lines.length && !/\n$/.test(lines[lines.length - 1])) lines[lines.length - 1] += eol;
|
|
63
|
+
lines.push(eol, ...chunk);
|
|
64
|
+
row(shown, space.heading, 'added — the pack\'s default, at the end of the file');
|
|
65
|
+
}
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The rails: a bullet this version names that the copy's section lacks is appended as its absent-line.
|
|
71
|
+
if (space.heading.includes('rails')) {
|
|
72
|
+
const packSec = findSection(packLines, space.heading);
|
|
73
|
+
const packItems = railsItems(packLines.slice(packSec.start, packSec.end));
|
|
74
|
+
const sec = findSection(lines, space.heading);
|
|
75
|
+
const have = railsItems(lines.slice(sec.start, sec.end)).map((it) => it.name.toLowerCase());
|
|
76
|
+
const missing = packItems.filter((it) => !have.includes(it.name.toLowerCase()));
|
|
77
|
+
if (missing.length) {
|
|
78
|
+
let at = sec.end;
|
|
79
|
+
while (at > sec.start + 1 && bare(lines[at - 1]).trim() === '') at--;
|
|
80
|
+
if (!/\n$/.test(lines[at - 1])) lines[at - 1] += eol;
|
|
81
|
+
const add = [];
|
|
82
|
+
if (!have.length) add.push(eol);
|
|
83
|
+
for (const it of missing) add.push(...linesOf(withEol(it.text)));
|
|
84
|
+
lines.splice(at, 0, ...add);
|
|
85
|
+
for (const it of missing) row(shown, `rails item: ${it.name}`, 'item added — its absent-line, at the end of the section');
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The stamp names the pack version whose repository-owned sections this copy carries.
|
|
92
|
+
const stampAt = stampIndex(lines);
|
|
93
|
+
const current = STAMP.exec(bare(lines[stampAt]))[2];
|
|
94
|
+
const cmp = compareVersions(current, packVersion);
|
|
95
|
+
if (cmp < 0) {
|
|
96
|
+
const raw = lines[stampAt];
|
|
97
|
+
const bom = raw.startsWith('') ? '' : '';
|
|
98
|
+
lines[stampAt] = bom + raw.slice(bom.length).replace(STAMP, `$1${packVersion}$3`);
|
|
99
|
+
row(shown, 'stamp', `${current} to ${packVersion}`);
|
|
100
|
+
changed = true;
|
|
101
|
+
} else if (cmp > 0) row(shown, 'stamp', `${current} — ahead of this pack; left as it is`);
|
|
102
|
+
else row(shown, 'stamp', `${current} — current`);
|
|
103
|
+
|
|
104
|
+
if (changed) {
|
|
105
|
+
changedFiles++;
|
|
106
|
+
if (write) writeFileSync(path, lines.join(''));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const width = Math.max(...rows.map((r) => r.subject.length), 7);
|
|
111
|
+
const fw = Math.max(...rows.map((r) => r.file.length), 4);
|
|
112
|
+
console.log(`| ${'file'.padEnd(fw)} | ${'section'.padEnd(width)} | action |`);
|
|
113
|
+
console.log(`|${'-'.repeat(fw + 2)}|${'-'.repeat(width + 2)}|--------|`);
|
|
114
|
+
for (const r of rows) console.log(`| ${r.file.padEnd(fw)} | ${r.subject.padEnd(width)} | ${r.action} |`);
|
|
115
|
+
|
|
116
|
+
const map = mapOf(packText('docs-workflow.md'));
|
|
117
|
+
if (Object.keys(map).length) {
|
|
118
|
+
console.log('\nyours — what this version\'s skills read from each section (check that yours answers each), and the');
|
|
119
|
+
console.log('loop passages that echo it (rewrite them too if you replaced the section):');
|
|
120
|
+
for (const space of OWNED) {
|
|
121
|
+
const m = map[space.title];
|
|
122
|
+
if (m) console.log(`- ${space.title}: reads ${m.reads}${m.echoed && m.echoed !== '—' ? `; echoed in ${m.echoed}` : ''}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\nloop text — not touched. The pack's own is in ${join(packRoot, 'references').replace(/\\/g, '/')}/ to compare.`);
|
|
127
|
+
if (!changedFiles) console.log('\nnothing to do — every section present, every stamp current.');
|
|
128
|
+
else if (write) console.log(`\nwritten — ${changedFiles} file(s), exactly the rows above.`);
|
|
129
|
+
else console.log('\ndry run — nothing written. Run again with --write to apply exactly the rows above.');
|
|
@@ -42,7 +42,7 @@ not a missing-file finding. `patterns/codegen.md` is read by any seat that touch
|
|
|
42
42
|
|---|---|
|
|
43
43
|
| **frontend** | `patterns/state-management.md` · `patterns/ui-style-guide.md` · `patterns/design-tokens.md` · `infrastructure/realtime-events.md` (the poke/refetch concept trips sessions up — read it even for "pure UI" work) · `patterns/codegen.md` when the client consumes generated types |
|
|
44
44
|
| **backend** | `infrastructure/realtime-events.md` (the poke/refetch concept trips sessions up — read it even for "pure API" work) · `infrastructure/background-work.md` · `patterns/long-running-workflows.md` as the task warrants · `patterns/codegen.md` when the server side emits generated code |
|
|
45
|
-
| **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 CLI, a sidecar, a worker project; docs work needs nothing beyond the Step 0 floor |
|
|
45
|
+
| **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 CLI, a sidecar, a worker project — and its reading list is the rows for those surfaces in `docs/_meta/doc-index.md` § Role reading lists; docs work needs nothing beyond the Step 0 floor |
|
|
46
46
|
|
|
47
47
|
**This repository's own docs reach you as paths too, not as an index to consult:** the coordinator copies
|
|
48
48
|
your role's rows from `docs/_meta/doc-index.md` § Role reading lists into your prompt's `EXTRA DOCS` line.
|
|
@@ -9,6 +9,8 @@ Turn this session into the **coordinator** for a `docs/research/{topic}/` spec p
|
|
|
9
9
|
phase 3 of the backlog model (`docs-backlog`, `spec-pipeline`, **`spec-parent`**, seats build,
|
|
10
10
|
`docs-process`).
|
|
11
11
|
|
|
12
|
+
**Find the format first.** This repository writes its formats (the doc header and status words, where an effort is filed, its rails) in the sections `rg -n '^#+ .*\(repository-owned\)' docs/_meta/` lists; read the one a step names before that step writes. This skill names the section, never the format.
|
|
13
|
+
|
|
12
14
|
**You are one of the three kinds that do NOT compose `spec-seat`** — that file is the contract for the
|
|
13
15
|
seats you ARM, and it says so itself. This skill is your whole contract: you write rulings rather than
|
|
14
16
|
receive them, you own the tracker, and you hold the close. The control plane's seed says so out loud
|
|
@@ -285,7 +287,7 @@ read the board and answer on first receipt rather than spending a round-trip.
|
|
|
285
287
|
`.md` files (`live-checks.md`, the numbered specs, the umbrella) are inside your scoped write path —
|
|
286
288
|
same gate, same receipt as your ledger. **This also means a line you have PROVEN false in a spec or
|
|
287
289
|
a live-check row is yours to strike at the source.** Research stage only — after docs-process
|
|
288
|
-
|
|
290
|
+
files the folder, only the ledger remains writable.
|
|
289
291
|
Every session's TLDR section (e) row belongs there, plus any status only you observed. Unaggregated
|
|
290
292
|
rows are the most common thing to fall through, and the human's sitting is what they are for.
|
|
291
293
|
3. **Run `spec-retro`.** It is written FROM the tracker, which is why step 1 comes first — a retro
|
|
@@ -294,8 +296,10 @@ read the board and answer on first receipt rather than spending a round-trip.
|
|
|
294
296
|
grades stay independent.** Do not reconcile them or soften yours to match; two accounts of one run
|
|
295
297
|
are the point, and whoever reads them both does the reconciling.
|
|
296
298
|
4. **Commit the retro** by explicit path and **write `BUILD COMPLETE`** into `project-status.md`.
|
|
297
|
-
5. **Push the effort's branch. Create the PR if none is open**
|
|
298
|
-
|
|
299
|
+
5. **Push the effort's branch. Create the PR if none is open** — where and how
|
|
300
|
+
`docs/_meta/engineering-loop.md` § This repository's rails (repository-owned) says (when it names no
|
|
301
|
+
route, ask the human once and suggest recording the answer there); if one is already open the push is
|
|
302
|
+
enough. These are your own git — gated and receipted like any other publish. **The
|
|
299
303
|
push command must be the ENTIRE command** (a leading `cd` disqualifies it), and keep git-verb words
|
|
300
304
|
out of any echo label around it — the deny layer matches TEXT, not parsed verbs, and a decorative
|
|
301
305
|
`=== remote tip ===` header over a read-only block will park.
|
|
@@ -306,9 +310,12 @@ read the board and answer on first receipt rather than spending a round-trip.
|
|
|
306
310
|
are **contingent on the merge**, so say so in the close: the docs describe what the PR will land.
|
|
307
311
|
|
|
308
312
|
**P MOVES THE EFFORT HOME OUT OF YOUR REACH — the tracker pass in step 1 is your LAST chance at
|
|
309
|
-
the specs and live-checks.** P `git mv`s `docs/research/{topic}/` to
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
the specs and live-checks.** P `git mv`s `docs/research/{topic}/` to the archive home
|
|
314
|
+
`docs/_meta/docs-workflow.md` § The archive convention (repository-owned) names, or to the umbrella's
|
|
315
|
+
`Archive:` line when it carries one. The ledger survives the
|
|
316
|
+
rename (its write predicate admits `docs/{research|archive}/*/project-status.md`; a home elsewhere
|
|
317
|
+
is outside it, so there P's write to the tracker is the last one), but the effort-home lane
|
|
318
|
+
(`live-checks.md`, the specs) is research only — the archive is frozen
|
|
312
319
|
testimony. So finish those before P runs, and record P's own outcome in P's TLDR rather than
|
|
313
320
|
planning to journal it afterwards.
|
|
314
321
|
|
|
@@ -364,6 +371,9 @@ so read them as run-time obligations, not close-out ones:
|
|
|
364
371
|
- Plan TLDR as **plain text BEFORE any plan-approval mode**; questions as **plain numbered text, then
|
|
365
372
|
STOP** — never an interactive question tool (the human can't copy from it).
|
|
366
373
|
- **No commit/push without your relayed call.** Explicit staging only — never `git add -A`.
|
|
374
|
+
- **This repository's rails add to these rules.** `docs/_meta/engineering-loop.md` § This repository's rails
|
|
375
|
+
(repository-owned) names the policy, the gates a commit passes, the suite before a PR, the PR route and
|
|
376
|
+
any stricter staging — bake every rail in beside these, and never let one subtract from them.
|
|
367
377
|
- **The unattended git form, and it binds YOU as much as the seats: one git command per call, no shell
|
|
368
378
|
metacharacters.** The FORM check reads the whole command string, so chaining, redirects or substitution
|
|
369
379
|
disqualify it — `git add … && git commit … && git push …` parks, while the same three sent separately
|
|
@@ -488,7 +498,8 @@ REVIEW row (R):
|
|
|
488
498
|
|
|
489
499
|
DOCS-PROCESS row (P):
|
|
490
500
|
You are the DOCS-PROCESS seat: invoke the docs-process skill before anything else.
|
|
491
|
-
Effort: {topic} · archive target: docs/archive
|
|
501
|
+
Effort: {topic} · archive target: {per docs/_meta/docs-workflow.md § The archive convention
|
|
502
|
+
(repository-owned), or the umbrella's Archive: line when it carries one} · shape: {as that section says}
|
|
492
503
|
Working docs for THIS effort, and ONLY these: {explicit list — docs/working/ is a shared
|
|
493
504
|
queue and may hold other efforts' docs; do not infer membership from the folder}
|
|
494
505
|
Research/spec docs to archive: docs/research/{topic}/
|
|
@@ -563,8 +574,8 @@ PARENT RESPONSE — FINAL TLDR ACCEPTED {(+ what I verified myself)}.
|
|
|
563
574
|
VERIFY before committing: verify-staged {same list} — commit only on exit 0. Non-zero means
|
|
564
575
|
git add silently dropped files (usually a .gitignore rule); STOP, report the output verbatim,
|
|
565
576
|
and wait — do not force-add, do not widen .gitignore.
|
|
566
|
-
Message: {
|
|
567
|
-
Trailer: {the trailer
|
|
577
|
+
Message: {a message the rails' commit gates accept, naming the spec}
|
|
578
|
+
Trailer: {the trailer the rails' commit convention requires, if any — name YOUR model}
|
|
568
579
|
Then push. Post-commit status must show only the known coordinator-owned/unrelated files, and
|
|
569
580
|
the close-out report carries the one-line STAGING VERIFIED receipt — a commit report
|
|
570
581
|
without it is incomplete (the whole defect class is silence; an unrequired receipt is
|
|
@@ -10,6 +10,8 @@ the two-phase backlog model: `docs-backlog` parks an idea; `spec-pipeline` gradu
|
|
|
10
10
|
into specs. **The plan is throwaway — the specs carry every crumb.** A future session reads ONE spec + the
|
|
11
11
|
umbrella and can build without the plan or the original conversation.
|
|
12
12
|
|
|
13
|
+
**Find the format first.** This repository writes its formats (the doc header and status words, where an effort is filed, its rails) in the sections `rg -n '^#+ .*\(repository-owned\)' docs/_meta/` lists; read the one a step names before that step writes. This skill names the section, never the format.
|
|
14
|
+
|
|
13
15
|
## The two-phase model
|
|
14
16
|
|
|
15
17
|
| Phase | When | Skill | Output |
|
|
@@ -111,8 +113,8 @@ sessions/ WHO PUTS IT THERE
|
|
|
111
113
|
**"Do you want DOCS-PROCESS as a row?"** In-pipeline gives you a zero-touch close: living docs patched
|
|
112
114
|
and the effort archived without a second sitting. **State the trade honestly:** `docs-process` is
|
|
113
115
|
designed as a fresh session over *merged* code, so in-pipeline it patches living docs against code that
|
|
114
|
-
is only **about to be** merged, and the `research/{topic}`
|
|
115
|
-
PR. If the PR is not merged, or is partly reverted, those docs describe something absent. **Say in the
|
|
116
|
+
is only **about to be** merged, and the move from `research/{topic}` into the archive home rides the
|
|
117
|
+
same PR. If the PR is not merged, or is partly reverted, those docs describe something absent. **Say in the
|
|
116
118
|
brief that the living-doc patches are contingent on the merge.** Out-of-pipeline is equally valid and
|
|
117
119
|
cheap — every artifact is durable, so it runs cold, later, over several efforts at once.
|
|
118
120
|
|
|
@@ -125,7 +127,7 @@ sessions/ WHO PUTS IT THERE
|
|
|
125
127
|
|
|
126
128
|
```
|
|
127
129
|
02..NN the build seats
|
|
128
|
-
{R} REVIEW — spec-review, if chosen. Runs on `git diff
|
|
130
|
+
{R} REVIEW — spec-review, if chosen. Runs on `git diff {base}...<branch>` (the rails' base branch); no PR yet
|
|
129
131
|
{F} FIXIT — MAY materialise, one maximum, only if the review classifies an (a)+(b)
|
|
130
132
|
finding AND the coordinator rules it in. Not authored up front; the
|
|
131
133
|
coordinator charters it from the review's named list
|
|
@@ -146,7 +148,8 @@ sessions/ WHO PUTS IT THERE
|
|
|
146
148
|
|
|
147
149
|
3. **Write the specs**, using the spec-doc template in `docs/_meta/docs-workflow.md`. Each per-seat
|
|
148
150
|
spec opens with the spec-doc blockquote (`Builds on [prior]` + `Source: [umbrella / research]`) and the
|
|
149
|
-
|
|
151
|
+
machine header `docs/_meta/docs-workflow.md` § Doc metadata (repository-owned) prescribes, at the word for
|
|
152
|
+
`draft`, so the set stays navigable.
|
|
150
153
|
|
|
151
154
|
**Six things every slice carries**, each earned by a session that did not have it:
|
|
152
155
|
|
|
@@ -178,7 +181,7 @@ sessions/ WHO PUTS IT THERE
|
|
|
178
181
|
bans, and the builder is then forced to choose between the letter and the point. State the property
|
|
179
182
|
and the letter can never contradict it.
|
|
180
183
|
- **A `last verified against` line — the date AND the sha the spec was checked against.** The
|
|
181
|
-
|
|
184
|
+
header's verified date records authoring; this tells the BUILDER what the spec's facts are relative
|
|
182
185
|
to. **Recon's first act is to test whether the slice is already satisfied** — specs do get overtaken
|
|
183
186
|
between authoring and build, and a session that discovers this at recon closes in minutes while one
|
|
184
187
|
that assumes the work is pending rebuilds what already exists.
|
|
@@ -199,7 +202,7 @@ sessions/ WHO PUTS IT THERE
|
|
|
199
202
|
which spec moves it, and have the others leave it alone.
|
|
200
203
|
|
|
201
204
|
4. **Name the RESEARCH the effort came from, in the umbrella, by path.** The idea that fed this work
|
|
202
|
-
archives *with* it — that is how
|
|
205
|
+
archives *with* it — that is how its archive home ends up holding the whole arc from "thing we'd like to
|
|
203
206
|
do" to "done." When the research doc already lives at `research/{topic}/`, it travels for free. **When
|
|
204
207
|
it does not** — a standalone `research/{something}.md`, or several — the umbrella's `Source:` / Lineage
|
|
205
208
|
is the only record that it belongs to this effort.
|
|
@@ -208,7 +211,13 @@ sessions/ WHO PUTS IT THERE
|
|
|
208
211
|
list, and a research doc nobody named is one that stays behind in the running tab after its effort has
|
|
209
212
|
shipped. Two lines in the umbrella now saves a hunt through a shared folder later.
|
|
210
213
|
|
|
211
|
-
5. **
|
|
214
|
+
5. **Name the archive destination when only you can.** Read `docs/_meta/docs-workflow.md` § The archive
|
|
215
|
+
convention (repository-owned). If where a pipeline files depends on something only this session knows
|
|
216
|
+
(which living feature the effort belongs to, say), resolve it now and write the path into the
|
|
217
|
+
umbrella's blockquote as an `Archive:` line; `spec-parent` hands it to the docs-process row and
|
|
218
|
+
`docs-process` files there. A convention that needs nothing from you needs no line.
|
|
219
|
+
|
|
220
|
+
6. **Stop.** The specs stay in `research/{topic}/`. Do NOT implement, do NOT touch living docs, do NOT
|
|
212
221
|
archive — those are later phases. Output the new files only.
|
|
213
222
|
|
|
214
223
|
## The `00-ignition-brief.md` template
|
|
@@ -231,7 +240,7 @@ requires no authoring at all.
|
|
|
231
240
|
|
|
232
241
|
> You are the COORDINATOR of the {topic} pipeline. Read this, then the umbrella (01-*.md), then the
|
|
233
242
|
> tracker (project-status.md — yours), then EVERY numbered spec before arming the first seat.
|
|
234
|
-
|
|
243
|
+
{the machine header docs/_meta/docs-workflow.md § Doc metadata (repository-owned) prescribes, at the word for `draft`}
|
|
235
244
|
|
|
236
245
|
**Last verified against:** {date}, `{sha}`. Ignition re-verifies this brief against live HEAD and
|
|
237
246
|
corrects drift; it does not re-author it.
|
|
@@ -320,7 +329,8 @@ stays short because a task buried in a run row cannot be diffed or reviewed the
|
|
|
320
329
|
```
|
|
321
330
|
|
|
322
331
|
**Do not restate the ceremony here.** The coordinator's procedure lives in `spec-parent` and its policy
|
|
323
|
-
|
|
332
|
+
is the one § This repository's rails (repository-owned) names, which a hosted run also carries in the
|
|
333
|
+
un-skippable policy suffix the control plane supplies; a brief that re-teaches them accretes
|
|
324
334
|
into prompt-law that breaks the coordinator it was meant to steer. The brief carries THIS RUN's
|
|
325
335
|
choreography and nothing else.
|
|
326
336
|
|
|
@@ -338,7 +348,7 @@ viewer because they are instructions to the coordinator, not content for the hum
|
|
|
338
348
|
# {Topic} — Project Status
|
|
339
349
|
|
|
340
350
|
> The coordinator's ledger. Advanced at every session boundary, never reconstructed at close.
|
|
341
|
-
|
|
351
|
+
{the machine header docs/_meta/docs-workflow.md § Doc metadata (repository-owned) prescribes, at the word for `wip`}
|
|
342
352
|
|
|
343
353
|
## What this is
|
|
344
354
|
{one paragraph}
|
|
@@ -434,9 +444,9 @@ to edit it directly.
|
|
|
434
444
|
folder), the chat transcript, and any subagent reports vanish. Inline their load-bearing content; never
|
|
435
445
|
point at ephemeral state as the source of truth. Cross-links to *other durable docs* are fine.
|
|
436
446
|
- **Lifecycle.** Specs live in `research/{topic}/` through the build. When the effort ships, `docs-process`
|
|
437
|
-
archives the umbrella + every spec + the working docs together into
|
|
438
|
-
*is* the design history.
|
|
439
|
-
- **Anti-fabrication.** A spec's
|
|
447
|
+
archives the umbrella + every spec + the working docs together into the home `docs-workflow.md` § The
|
|
448
|
+
archive convention (repository-owned) names, in its shape — the spec set *is* the design history.
|
|
449
|
+
- **Anti-fabrication.** A spec's header verified date is the date it was *authored against the code it
|
|
440
450
|
cites*, not a promise the code stays unchanged. Only write facts you actually checked this run.
|
|
441
451
|
- **Author against the HEAD the run will start from — and know what that does not buy you.** Authoring close
|
|
442
452
|
to HEAD reduces drift; it does not substitute for a seat reading the code. Specs authored against the
|