@pilotspace/add 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,110 @@ All notable changes to the ADD method (`@pilotspace/add` on npm,
4
4
  `pilotspace-add` on PyPI) are documented here. The format follows
5
5
  [Keep a Changelog](https://keepachangelog.com/); versions follow semver.
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [1.11.0] — 2026-06-26
10
+
11
+ Engine-modularization + audit-hardening release — the 7k-line monolith becomes a
12
+ navigable package, the verify/atomicity/coverage gates get five fixes, and the
13
+ standalone (milestone-free) lane becomes first-class. **No behavior change** to the
14
+ CLI, the flow, or any output: this is an internal architecture + hardening release.
15
+
16
+ ### Changed
17
+ - **The engine is now a focused package.** `add-method/tooling/add.py` (7049 → 5640
18
+ lines, −20%) split into **13 `add_engine/*.py` modules** — `constants`, `io_state`,
19
+ `accessors`, `predicates`, `identity`, `guidelines`, `render`, `milestones`,
20
+ `components`, `version`, `release`, `taskdoc`, `autonomy` — behind a stable
21
+ re-export surface (`import add; add.<name>` still resolves, public AND `_`-prefixed,
22
+ so every existing test is untouched). `add.py` stays the runnable **orchestrator
23
+ entry** (the `load_state`/`save_state`/`report_data`/`cmd_*`/`main` spine). Each of
24
+ the 16 extractions was its own CI-green PR, proven verbatim by an AST source-segment
25
+ diff and gated by the full suite (1815 → 1959 green throughout, no test weakened).
26
+ - **Two-pin integrity model.** Alongside `ENGINE_MD5` (md5 of `add.py`),
27
+ `ENGINE_PKG_MD5` is a manifest digest over every `add_engine/*.py`; both are literal
28
+ pins (never self-hashed) and are checked byte-identical across the 3-tree mirror
29
+ (canonical · `.add` · `_bundled`). `prepare_bundle` + both installers + the `.add`
30
+ mirror now ship the whole `add_engine/` package.
31
+
32
+ ### Fixed (audit-hardening)
33
+ - Five gate/atomicity/coverage fixes: a phase-build guard, hardened save-state
34
+ atomicity, force-preserve self-healing, setup-tests-before-build ordering, and a
35
+ consumer-stale gate. A security finding remains an un-forceable HARD-STOP.
36
+
37
+ ### Added (the standalone / lightweight lane)
38
+ - **`add.py todo`** — capture a milestone-free backlog item without scaffolding a task.
39
+ - **`loose tasks:` release attribution** — a release now attributes *any* done,
40
+ milestone-free task on its `RELEASES.md` row (not only milestone membership), with a
41
+ separate status cue for releasable loose work.
42
+ - **`fast` (task) + `auto` (mode) flag modes** — the standalone lane is first-class:
43
+ a minimal `TASK.fast.md`, freeze-gated under any milestone, with a flag-mode quick
44
+ reference.
45
+
46
+ This release bundles **2 closed milestones** (`audit-hardening`,
47
+ `engine-modularization`) and **27 loose tasks** since 1.10.0.
48
+
49
+ ## [1.10.0] — 2026-06-25
50
+
51
+ Component-aware major — ADD now models every codebase as a **graph of components**,
52
+ each owning a source root, its own green bar, and the contracts it produces or
53
+ consumes, so **one milestone can ship a vertical slice across components** in a
54
+ monorepo or across repos. Bundles the closed `component-aware-add` major plus the
55
+ `docs-site` (the book as a live MkDocs site) and `loop-steering` milestones, and the
56
+ ccsk `--rule-file` mode. Every new gate is **opt-in or grandfathered** — a project
57
+ that declares no components is byte-identical to 1.9.0.
58
+
59
+ ### Added
60
+ - **Component registry (`component-aware-add`)** — declare parts of a multi-part
61
+ codebase in `.add/components.toml` under `[component.<name>]` (a `root` + a
62
+ `green-bar`). A task binds to one with a `component:` header line, which anchors its
63
+ §5 Scope to that component's root. Declared, never inferred; zero components ⇒
64
+ byte-identical to today.
65
+ - **Per-component verify** — a bound task is held to ITS component's green-bar at the
66
+ verify gate (the cite-gate refuses `component_green_bar_uncited`), so two tasks in
67
+ one milestone can pass on two different toolchains (e.g. `pytest` and `vitest`). The
68
+ engine still never runs a suite — the AI runs it, the gate checks the right bar was
69
+ cited.
70
+ - **Cross-component contracts (`produces:` / `consumes:`)** — declare `[contract.<id>]`
71
+ (producer + consumers). A producer's §3 freeze writes an immutable snapshot at
72
+ `.add/contracts/<id>.json`; a consumer pins its hash; a changed re-freeze flags every
73
+ consumer stale. A missing/malformed snapshot HARD-STOPS — never build against a
74
+ guessed shape.
75
+ - **One milestone, full-stack slice** — a `consumes:` task is HELD from entering §3
76
+ (`producer_contract_unfrozen`) until its producer's contract is frozen, so a BE→FE
77
+ vertical slice ships in one milestone, ordered by the frozen contract.
78
+ - **Multi-repo federation (`add.py federate pull <id>`)** — a consumer repo declares
79
+ `[federation.<id>]` (a `source` path + optional `pin`) and pulls a byte-for-byte copy
80
+ of a producer repo's published snapshot into its local `.add/contracts/`, where the
81
+ rest of ADD treats mono- and multi-repo identically. Fail-loud: unknown id /
82
+ unreadable source / invalid snapshot / version mismatch each HARD-STOPS and lands
83
+ nothing.
84
+ - **Component pillar docs** — a new book chapter `17 · Components`, a skill guide
85
+ (`components.md`), and glossary terms (Component · Cross-component contract ·
86
+ Federation) teach the whole loop.
87
+ - **The book as a live site (`docs-site`)** — the AIDD book ships to GitHub Pages as a
88
+ MkDocs-Material site (`mkdocs.yml` + `requirements-docs.txt`, build-time only — the
89
+ published packages stay zero-dependency); `mkdocs build --strict` fails the deploy on
90
+ a broken intra-book link.
91
+ - **Rule-file mode for ccsk projects (`--rule-file`)** — when a project keeps its
92
+ workflow rules under `.claude/rules/` (the ccsk convention, detected by a `.ccsk/`
93
+ dir), ADD relocates CLAUDE.md's block to `.claude/rules/add-workflows.md` and leaves a
94
+ single reference bullet, instead of inlining. Triggers three ways (the explicit
95
+ `--rule-file` flag, a `.ccsk/` directory, or a rule file already present),
96
+ **CLAUDE-only**, migrates a prior inline block out (`.bak` on change), idempotent and
97
+ fail-soft. Mirrored across all three installers (engine `add.py`, pip `_installer.py`,
98
+ npm `cli.js`).
99
+
100
+ ### Changed
101
+ - **Guided dynamic loop (`loop-steering`)** — `status` and `guide` now STEER into the
102
+ build→verify loop at the loop juncture rather than only reporting it, so an agent is
103
+ pointed at the next loop step instead of having to infer it.
104
+
105
+ ### Compatibility
106
+ - Python 3.11+ is required to USE the component pillar (it parses `components.toml` with
107
+ the stdlib `tomllib`); on 3.10 the engine runs unchanged but a declared
108
+ `components.toml` fails loud (`components_malformed`). The published packages remain
109
+ zero-dependency.
110
+
7
111
  ## [1.9.0] — 2026-06-24
8
112
 
9
113
  Lean-pass major — make ADD's own surface (skill · flow · engine) the most-effective
package/bin/cli.js CHANGED
@@ -39,7 +39,7 @@ function parseArgs(argv) {
39
39
  // defaults the stage and infers the name from the folder, so the manual-init
40
40
  // hint only echoes flags the user actually chose (shortest true command).
41
41
  const args = { _: [], force: false, check: false, noSkill: false, stage: null, name: null,
42
- yes: false, nonInteractive: false, global: false, globalData: false };
42
+ yes: false, nonInteractive: false, global: false, globalData: false, ruleFile: false };
43
43
  for (let i = 0; i < argv.length; i++) {
44
44
  const a = argv[i];
45
45
  if (a === "--force") args.force = true;
@@ -58,6 +58,9 @@ function parseArgs(argv) {
58
58
  // already provides the `add` skill, so a plugin bootstrap uses this to materialize
59
59
  // .add/tooling/ + .add/docs/ into the project without a duplicate .claude/skills/add.
60
60
  else if (a === "--no-skill") args.noSkill = true;
61
+ // --rule-file: write the ADD block to .claude/rules/add-workflows.md + reference it from
62
+ // CLAUDE.md instead of inlining (auto-on for ccsk projects with a .ccsk/ dir).
63
+ else if (a === "--rule-file") args.ruleFile = true;
61
64
  else if (a === "--stage" || a === "--name") {
62
65
  const v = argv[++i];
63
66
  // fail loudly on a trailing/abutting flag — never silently drop a value
@@ -249,6 +252,126 @@ function writeAgentPointer(target, profile) {
249
252
  }
250
253
  }
251
254
 
255
+ // --- rule-file mode: ccsk-style relocation of the CLAUDE.md block ---------------------
256
+ // Mirror of add.py / _installer.py rule-file logic (twins by duplication). For ccsk projects
257
+ // (.ccsk/ dir) or an explicit --rule-file, the ADD pointer goes to .claude/rules/add-workflows.md
258
+ // and CLAUDE.md keeps only a reference bullet. CLAUDE-only — AGENTS.md/.clinerules stay inline.
259
+ const RULES_FILE_REL = path.join(".claude", "rules", "add-workflows.md");
260
+ const WORKFLOW_HEADINGS = ["Rules & Workflows", "Workflows", "Rules"];
261
+ const RULE_REF_LINE = "- ADD (AI-Driven Development) Workflows rules: ./.claude/rules/add-workflows.md";
262
+
263
+ // True when the ADD block belongs in .claude/rules/add-workflows.md instead of inline.
264
+ // Re-derived from disk: explicit flag, a ccsk project (.ccsk/), or a rule file already present.
265
+ function ruleFileMode(root, flag) {
266
+ if (flag) return true;
267
+ try {
268
+ if (fs.existsSync(path.join(root, ".ccsk")) &&
269
+ fs.statSync(path.join(root, ".ccsk")).isDirectory()) return true;
270
+ if (fs.existsSync(path.join(root, RULES_FILE_REL))) return true;
271
+ } catch (_e) { /* fail-soft */ }
272
+ return false;
273
+ }
274
+
275
+ // Remove an inline ADD:BEGIN..END region (migration), collapsing the gap. An unterminated
276
+ // BEGIN is left as-is rather than eating the rest of the file.
277
+ function stripInlineBlock(text) {
278
+ const begin = text.indexOf(GUIDE_BEGIN);
279
+ if (begin === -1) return text;
280
+ let end = text.indexOf(GUIDE_END, begin);
281
+ if (end === -1) return text;
282
+ end += GUIDE_END.length;
283
+ const head = text.slice(0, begin).replace(/\n+$/, "");
284
+ const tail = text.slice(end).replace(/^\n+/, "");
285
+ if (head && tail) return head + "\n\n" + tail;
286
+ return head || tail;
287
+ }
288
+
289
+ // Insert the ADD rule-file bullet under an existing Workflows/Rules heading, or append a fresh
290
+ // '## Workflows' section. Caller guarantees the bullet is absent.
291
+ function insertRuleReference(text) {
292
+ const lines = text.split("\n");
293
+ let headingIdx = -1;
294
+ for (let i = 0; i < lines.length; i++) {
295
+ const m = lines[i].match(/^(#{1,6})\s+(.*?)\s*$/);
296
+ if (m && WORKFLOW_HEADINGS.some((h) => m[2].trim().toLowerCase() === h.toLowerCase())) {
297
+ headingIdx = i;
298
+ break;
299
+ }
300
+ }
301
+ if (headingIdx === -1) {
302
+ const body = text.replace(/\n+$/, "");
303
+ const sep = body ? "\n\n" : "";
304
+ return body + sep + "## Workflows\n\n" + RULE_REF_LINE + "\n";
305
+ }
306
+ const level = lines[headingIdx].match(/^(#{1,6})/)[1].length;
307
+ let end = lines.length;
308
+ for (let j = headingIdx + 1; j < lines.length; j++) {
309
+ const m = lines[j].match(/^(#{1,6})\s+/);
310
+ if (m && m[1].length <= level) { end = j; break; }
311
+ }
312
+ let insertAt = headingIdx + 1;
313
+ for (let j = headingIdx + 1; j < end; j++) {
314
+ if (lines[j].trim()) insertAt = j + 1;
315
+ }
316
+ lines.splice(insertAt, 0, RULE_REF_LINE);
317
+ return lines.join("\n");
318
+ }
319
+
320
+ // Make CLAUDE.md reference the ADD rule file under a Workflows/Rules heading, migrating any prior
321
+ // inline ADD block out. created|updated|unchanged|skipped; .bak on change; fail-soft.
322
+ function ensureClaudeReference(claudeMd) {
323
+ try {
324
+ const existed = fs.existsSync(claudeMd);
325
+ const current = existed ? fs.readFileSync(claudeMd, "utf8") : "";
326
+ let next = stripInlineBlock(current);
327
+ if (next.indexOf("add-workflows.md") === -1) next = insertRuleReference(next);
328
+ if (!next.endsWith("\n")) next += "\n";
329
+ if (next === current) return "unchanged";
330
+ if (existed) fs.writeFileSync(claudeMd + ".bak", current);
331
+ fs.writeFileSync(claudeMd, next);
332
+ return existed ? "updated" : "created";
333
+ } catch (e) {
334
+ warn("could not write CLAUDE.md — " + (e && e.message ? e.message : e) + "; skipped");
335
+ return "skipped";
336
+ }
337
+ }
338
+
339
+ // Rule-file variant of writeAgentPointer for the Claude profile: write the ADD pointer block to
340
+ // .claude/rules/add-workflows.md and leave a reference in CLAUDE.md. Fail-soft.
341
+ function writeRuleFilePointer(target, profile) {
342
+ const rulesPath = path.join(target, RULES_FILE_REL);
343
+ const block = agentPointerBlock(profile);
344
+ try {
345
+ if (fs.existsSync(rulesPath)) {
346
+ const current = fs.readFileSync(rulesPath, "utf8");
347
+ const begin = current.indexOf(GUIDE_BEGIN);
348
+ let next;
349
+ if (begin !== -1) {
350
+ const endIdx = current.indexOf(GUIDE_END, begin);
351
+ if (endIdx !== -1) {
352
+ next = current.slice(0, begin) + block + current.slice(endIdx + GUIDE_END.length);
353
+ } else {
354
+ next = current.replace(/\n+$/, "") + "\n\n" + block + "\n";
355
+ }
356
+ } else {
357
+ next = current.replace(/\n+$/, "") + "\n\n" + block + "\n";
358
+ }
359
+ if (next !== current) {
360
+ fs.writeFileSync(rulesPath + ".bak", current);
361
+ fs.writeFileSync(rulesPath, next);
362
+ }
363
+ } else {
364
+ fs.mkdirSync(path.dirname(rulesPath), { recursive: true });
365
+ fs.writeFileSync(rulesPath, block + "\n");
366
+ }
367
+ } catch (e) {
368
+ warn("could not write " + RULES_FILE_REL + " — " + (e && e.message ? e.message : e) + "; skipped");
369
+ return "skipped";
370
+ }
371
+ ensureClaudeReference(path.join(target, "CLAUDE.md"));
372
+ return "ok";
373
+ }
374
+
252
375
  // --- interactive layer (clack on a real TTY; plain text everywhere else) -----
253
376
  // Designed-for-failure: any doubt (non-TTY, CI, --yes, a failed import, an
254
377
  // un-promptable stream) degrades to the EXACT plain-text path below. The clack
@@ -439,6 +562,57 @@ function writeGeminiSettings(target) {
439
562
  }
440
563
  }
441
564
 
565
+ // Seed .add/SOUL.md from the bundled template if it does not yet exist. Mirror of
566
+ // _installer.py:_seed_soul_md (npm <-> pip parity): skip-if-exists (SOUL.md is
567
+ // user-owned — never clobber); fail-soft (warn + return, never abort install/update).
568
+ function seedSoulMd(target) {
569
+ const dest = path.join(target, ".add", "SOUL.md");
570
+ if (fs.existsSync(dest)) return; // skip-if-exists (never clobber)
571
+ const source = path.join(PKG_ROOT, "tooling", "templates", "SOUL.md.tmpl");
572
+ if (!fs.existsSync(source)) {
573
+ warn("soul_seed_skipped: SOUL.md.tmpl not found in bundled tooling/templates/");
574
+ return;
575
+ }
576
+ try {
577
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
578
+ fs.writeFileSync(dest, fs.readFileSync(source, "utf8"));
579
+ } catch (e) {
580
+ warn("soul_seed_skipped: could not write .add/SOUL.md — " + (e && e.message ? e.message : e));
581
+ }
582
+ }
583
+
584
+ // Ensure .add/.gitignore lists the engine's transient artifacts. Seed it from the bundled
585
+ // tooling/templates/gitignore.tmpl if absent; else APPEND-IF-ABSENT each pattern line the
586
+ // template carries that the file lacks — additive only, never reorders/removes user lines,
587
+ // idempotent; comment/blank lines are not appended to an existing file. Fail-soft. Twin of
588
+ // _installer.py:_seed_gitignore.
589
+ function seedGitignore(target) {
590
+ const source = path.join(PKG_ROOT, "tooling", "templates", "gitignore.tmpl");
591
+ if (!fs.existsSync(source)) {
592
+ warn("gitignore_seed_skipped: gitignore.tmpl not found in bundled tooling/templates/");
593
+ return;
594
+ }
595
+ const dest = path.join(target, ".add", ".gitignore");
596
+ try {
597
+ const body = fs.readFileSync(source, "utf8");
598
+ if (!fs.existsSync(dest)) {
599
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
600
+ fs.writeFileSync(dest, body); // seed-if-missing
601
+ return;
602
+ }
603
+ const current = fs.readFileSync(dest, "utf8");
604
+ const have = new Set(current.split("\n").map((l) => l.trim()));
605
+ const missing = body.split("\n").filter(
606
+ (l) => l.trim() && !l.trim().startsWith("#") && !have.has(l.trim())
607
+ );
608
+ if (missing.length === 0) return; // idempotent — nothing to add
609
+ const suffix = current === "" || current.endsWith("\n") ? "" : "\n";
610
+ fs.writeFileSync(dest, current + suffix + missing.join("\n") + "\n");
611
+ } catch (e) {
612
+ warn("gitignore_seed_skipped: could not update .add/.gitignore — " + (e && e.message ? e.message : e));
613
+ }
614
+ }
615
+
442
616
  // The drop — now a RECONCILE: restore missing managed trees + refresh present ones
443
617
  // (sweep orphans) + report per-tree status. Byte-compatible handoff with the prior
444
618
  // installer. The interactive path resolves a target then calls straight into this.
@@ -446,11 +620,22 @@ function dropFiles(args, target, profile, intent) {
446
620
  profile = profile || detectAgent(process.env);
447
621
  log("Installing ADD into " + target);
448
622
  reconcile(args, target);
623
+ seedSoulMd(target); // pip parity: re-seed a missing user-owned SOUL.md (never clobber)
624
+ seedGitignore(target); // pip parity: seed/append-if-absent the engine-transient ignore lines
449
625
 
450
626
  // Agent detection: write THE detected agent's integration file (a marker-delimited
451
627
  // pointer init's sync-guidelines later supersedes) + tailor the closing next-step.
452
628
  // Best-effort + fail-soft — never aborts the successful drop above.
453
- writeAgentPointer(target, profile);
629
+ // Rule-file mode (ccsk projects / --rule-file) relocates the CLAUDE.md pointer to
630
+ // .claude/rules/add-workflows.md + a reference — CLAUDE-only; other agents stay inline.
631
+ if (profile.integration_file === "CLAUDE.md" && ruleFileMode(target, args.ruleFile)) {
632
+ if (fs.existsSync(path.join(target, ".ccsk"))) {
633
+ log(" ccsk detected — ADD rules go to .claude/rules/add-workflows.md");
634
+ }
635
+ writeRuleFilePointer(target, profile);
636
+ } else {
637
+ writeAgentPointer(target, profile);
638
+ }
454
639
 
455
640
  // Gemini CLI auto-loads GEMINI.md, not AGENTS.md — so for the gemini profile we ALSO merge
456
641
  // .gemini/settings.json (context.fileName) to load the AGENTS.md pointer. Fail-soft + idempotent.
@@ -800,6 +985,8 @@ function cmdUpdate(args) {
800
985
  fs.copyFileSync(stateFile, path.join(addDir, "pre-update-state.bak.json"));
801
986
  }
802
987
  reconcile(args, target);
988
+ seedSoulMd(target); // pip parity: re-seed a missing user-owned SOUL.md (never clobber)
989
+ seedGitignore(target); // pip parity: seed/append-if-absent the engine-transient ignore lines
803
990
  writeStamp(addDir, version);
804
991
  log("ADD updated " + (cur || "(unstamped)") + " -> " + version +
805
992
  " · managed layer reconciled · your project state untouched.");
@@ -847,4 +1034,7 @@ module.exports = {
847
1034
  scopeOptions: scopeOptions,
848
1035
  writeIntentNote: writeIntentNote,
849
1036
  writeGeminiSettings: writeGeminiSettings,
1037
+ ruleFileMode: ruleFileMode,
1038
+ ensureClaudeReference: ensureClaudeReference,
1039
+ writeRuleFilePointer: writeRuleFilePointer,
850
1040
  };
@@ -51,7 +51,7 @@ Each delta is one tagged entry — `- [COMPETENCY · status] the learning (evide
51
51
 
52
52
  **The consolidation.** At milestone close (or on demand, when open deltas pile up), a person runs the retrospective consolidation: **gather** every `open` delta across the milestone's tasks, **group** them by competency, **propose** the exact foundation edit for each, **confirm** with the human one by one, then **write** — append-only, newest-first (the newest record prepended at the top) — flipping each delta to `folded` (merged) or `rejected` (considered and deliberately not merged, left in place so the trail survives), and bumping the `foundation-version:` marker. `DDD`/`SDD`/`UDD` deltas consolidate into the matching section of `PROJECT.md`; `TDD`/`ADD` consolidate into `CONVENTIONS.md` (they sharpen the engine, not the product); and **every** consolidation also prepends one row at the top of `PROJECT.md` §Key Decisions — the universal, auditable record of what the foundation learned.
53
53
 
54
- **Tooling.** `add.py deltas` lists every open delta across the project (so nothing waiting to be consolidated is invisible); `add.py check` lints each delta's well-formedness — known competency tag, valid status, non-empty evidence. There is deliberately **no `add.py fold`**: the engine stays judgment-free, and the ritual lives with the human who owns it.
54
+ **Tooling.** `add.py deltas` lists every open delta across the project (so nothing waiting to be consolidated is invisible); `add.py check` lints each delta's well-formedness — known competency tag, valid status, non-empty evidence. `add.py fold [--task <slug>] [--comp <TAG>]` mechanizes the consolidation flip + transcribe + route + bump, validate-all-then-write so a reject leaves the tree byte-unchanged; running it **is** the human's confirmation, and the engine never self-approves *which* lessons to keep.
55
55
 
56
56
  **Foundation compaction.** The consolidation *prepends* new learnings; **foundation compaction** is the separate, later step that *shrinks* the result. At milestone close (or on demand, once a foundation spec grows past one screen), the AI proposes collapsing the stable, shipped, zero-residue tail of each foundation spec into ONE per-spec **rolled-up settled line** at the bottom — and the human confirms, one line at a time, exactly as with the consolidation. It never deletes: a settled line summarizes the prose and keeps a `see git` pointer, so the record is lossy on wording but lossless on traceability, and any OPEN residue always stays live. Each spec collapses in its own **per-spec shape** — `PROJECT.md` §Spec version bullets, §Key-Decisions rows, `CONVENTIONS.md` learnings, the `GLOSSARY.md` definition, the `MODEL_REGISTRY.md` rows — under one shared eligibility rule. Because every append-only sequence is **newest-first** (newest record on top), compaction collapses *upward from the bottom*: the settled line anchors at the tail (the oldest end). Like the consolidation it is convention-guided — there is deliberately no `add.py` command for it (read `compact-foundation.md`) — and it is distinct from the engine's `add.py compact <slug>`, which archives a finished milestone's files rather than shrinking a living spec.
57
57
 
@@ -31,7 +31,7 @@ Every checkpoint produces three short reports — **Test** (does it pass?), **Qu
31
31
 
32
32
  - **`PASS`** — criteria met; proceed.
33
33
  - **`RISK-ACCEPTED`** — proceed with a signed waiver carrying a named owner, a linked ticket, and an expiry. Allowed for non-security gaps only.
34
- - **`HARD-STOP`** — cannot proceed. Triggered by any failing test or any security finding; overridable only by the most senior accountable owner, and never for security.
34
+ - **`HARD-STOP`** — cannot proceed. Triggered by any failing test or any security finding. A non-security limitation may proceed only with a signed `RISK-ACCEPTED` record carrying an owner and an expiry; security is never waved through.
35
35
 
36
36
  The rule behind the protocol is *no silent skips.* A report nobody is accountable for approving is just a document; an outcome with an owner is governance.
37
37
 
@@ -56,7 +56,7 @@ then render a real screen and **capture** it. That capture is the **design-confi
56
56
  evidence — a real image the person approves *before* implementation, so the build
57
57
  matches the layout instead of discovering it. The book keeps the *why*; the
58
58
  operational recipe (the wireframe format, the token-bound mock, the capture engines)
59
- lives in the `add` skill's `design.md` and `udd-wireframe.md`.
59
+ lives in the `add` skill's `design.md` and the `udd-wireframe.md` template (`tooling/templates/`).
60
60
 
61
61
  These three foundation competencies, together with the **TDD ⇄ ADD** engine of
62
62
  [Part II](./02-the-flow.md), are ADD's five. The first four feed context to the
@@ -1,6 +1,6 @@
1
1
  # 16 · Releasing
2
2
 
3
- [← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
3
+ [← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [17 Components →](./17-components.md)
4
4
 
5
5
  ---
6
6
 
@@ -11,8 +11,8 @@ them *ship*. This chapter names the act every project eventually performs and th
11
11
  now, never formalized: bundling closed milestones into a versioned, user-facing release whose notes
12
12
  are evidence-backed, whose risk is disclosed, and whose behaviour is then watched.
13
13
 
14
- Releasing is the **fifth scope level** — after the task, the milestone, the foundation/setup level,
15
- and stage graduation. Like every scope level it runs the same shape: **gather → propose → the human
14
+ Releasing is the **fifth scope level** — after setup, intake, the milestone loop, and stage graduation.
15
+ Like every scope level it runs the same shape: **gather → propose → the human
16
16
  confirms → the engine records and enforces a floor.** And like graduation, it ends with an outward
17
17
  act the human owns. The operational recipe lives in the `release.md` skill guide; this chapter is the
18
18
  *why* behind it.
@@ -179,4 +179,4 @@ the risk, record the marker, and let a person make the outward call.
179
179
 
180
180
  ---
181
181
 
182
- [← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
182
+ [← 15 Foundations & Lineage](./15-foundations-and-lineage.md) · [Contents](./README.md) · Next: [17 Components →](./17-components.md)
@@ -0,0 +1,125 @@
1
+ # 17 · Components — monorepo and multi-repo
2
+
3
+ [← 16 Releasing](./16-releasing.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
4
+
5
+ ---
6
+
7
+ Most of this book treats a project as one codebase with one green bar. Real
8
+ systems are rarely that tidy: a backend and a frontend, a shared library and two
9
+ apps, or three services across three repos. ADD models all of these the same
10
+ way — as a **graph of components**. A component owns a source root, its own
11
+ green bar, and the contracts it produces or consumes. With that, **one milestone
12
+ can ship a vertical slice across components** — a backend endpoint and the
13
+ frontend that calls it — instead of splitting the slice across milestones.
14
+
15
+ This pillar is **opt-in and additive**: a project that declares no components
16
+ behaves exactly as the rest of the book describes. You reach for it only when a
17
+ milestone genuinely spans more than one green bar.
18
+
19
+ ## Declare the components
20
+
21
+ Components are **declared, never inferred** — ADD does not scan `apps/*` and
22
+ guess. You name them in `.add/components.toml`:
23
+
24
+ ```toml
25
+ [component.gateway]
26
+ root = "apps/gateway"
27
+ green-bar = "pytest + pyright"
28
+
29
+ [component.dashboard]
30
+ root = "apps/web"
31
+ green-bar = "vitest + a11y"
32
+ ```
33
+
34
+ Each component owns a **root** (the source subtree it governs) and a
35
+ **green-bar** (the suite + checks that prove *that* component healthy). A task
36
+ binds to a component with a `component:` header line; the component's root is
37
+ then added to the task's §5 Scope automatically. A project with no
38
+ `components.toml`, or a task with no `component:` line, is byte-identical to a
39
+ single-component project.
40
+
41
+ ## Verify each task against its own green bar
42
+
43
+ In a mixed milestone, a backend task and a frontend task pass on **different
44
+ toolchains**. The verify gate enforces this per component: a task bound to
45
+ `gateway` must cite its component's green-bar (`pytest + pyright`) in the §6
46
+ Build-expectations evidence, or the gate refuses `component_green_bar_uncited`.
47
+ The engine never *runs* the suite — that invariant holds here too. The AI runs
48
+ the right suite for the bound component; the gate checks the **right bar was
49
+ cited** in the evidence. Two tasks, one milestone, two green bars — each held to
50
+ its own.
51
+
52
+ ## Freeze a contract between components
53
+
54
+ When one component produces an interface another consumes, that boundary needs a
55
+ **frozen, machine-checkable contract**. Declare it, name its producer and
56
+ consumers:
57
+
58
+ ```toml
59
+ [contract.gateway-api]
60
+ producer = "gateway"
61
+ consumers = ["dashboard"]
62
+ ```
63
+
64
+ A task declares its role with a header line — `produces: gateway-api` or
65
+ `consumes: gateway-api`. When the **producer** task freezes its §3 and crosses
66
+ contract→tests, the engine writes an immutable snapshot at
67
+ `.add/contracts/gateway-api.json` (id, producer, version, frozen date, and a
68
+ hash over the frozen §3 shape). When a **consumer** task crosses contract→tests,
69
+ it **pins** that live hash. If the producer later re-freezes a *changed* shape,
70
+ `add.py check` flags every consumer `contract_consumer_stale` — a §7 delta to
71
+ re-pin against the new shape. A missing or malformed snapshot is a HARD-STOP, not
72
+ a guess: the consumer never builds against a shape that was never frozen.
73
+
74
+ ## One milestone, a full-stack slice
75
+
76
+ The reason to put a producer and a consumer in the *same* milestone is to ship a
77
+ vertical slice — but the frontend must not commit to an endpoint the backend has
78
+ not frozen yet. ADD enforces that ordering with a **hold**: a `consumes:` task
79
+ cannot advance scenarios→contract (it cannot write its §3) while its producer's
80
+ snapshot does not yet exist. The engine refuses `producer_contract_unfrozen` and
81
+ the task stays at `scenarios`. Once the backend freezes its contract, the
82
+ frontend proceeds and pins it. The slice is **ordered by the frozen contract**,
83
+ all inside one milestone — the FE stays downstream of the BE endpoint, not split
84
+ into a later milestone.
85
+
86
+ ## Across repositories: federation
87
+
88
+ Components in separate repositories work the same way; only the
89
+ **snapshot transport** differs. A consumer repo declares where a producer repo
90
+ publishes its frozen contract:
91
+
92
+ ```toml
93
+ [federation.gateway-api]
94
+ source = "../gateway/.add/contracts/gateway-api.json"
95
+ pin = "v1" # optional — the version this repo expects
96
+ ```
97
+
98
+ `add.py federate pull gateway-api` reads that source, validates it (valid JSON,
99
+ matching id, a hash, and — if `pin` is set — a matching version), and lands a
100
+ **byte-for-byte copy** at the local `.add/contracts/gateway-api.json`. From
101
+ there, the consuming repo's task holds and pins exactly as in a monorepo. The
102
+ pull is **fail-loud by design**: an unknown id, an unreadable source, an invalid
103
+ snapshot, or a version mismatch each HARD-STOPS and lands nothing — federation
104
+ never builds an FE against a guessed or stale endpoint. The producer's snapshot
105
+ is the published artifact; "publishing" is committing that file in the producer
106
+ repo. Each repo keeps its own git-native `state.json`; federation transports only
107
+ the immutable frozen shape, never shared mutable state.
108
+
109
+ ## What this pillar is not
110
+
111
+ - **Not auto-discovery.** Components are declared in `components.toml`, not
112
+ inferred from the directory tree.
113
+ - **Not a central server.** Federation copies an immutable snapshot between
114
+ repos; there is no shared service and no shared mutable state.
115
+ - **Not a new approval.** The component machinery rides the existing six-step
116
+ flow and its single contract-freeze approval — it adds gates the engine
117
+ enforces, not human checkpoints.
118
+
119
+ The whole pillar is structure, not policy: who *owns* a component and how
120
+ autonomy is set per component is the identity/governance story (chapters 11–12),
121
+ layered on top of this graph.
122
+
123
+ ---
124
+
125
+ [← 16 Releasing](./16-releasing.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
@@ -16,6 +16,12 @@
16
16
 
17
17
  **Co-specification** — how a spec is made in ADD: the AI and the human **brainstorm the shape together** (diverge), the AI **drafts** it, and the human **validates with the AI's advice** (validate). The AI's decisive advice is the *lowest-confidence flag*. It replaces dictation-by-one-side — the human owns the decision, the AI owns surfacing what it does not yet know. See [03 Specify](./03-step-1-specify.md).
18
18
 
19
+ **Component** — a declared part of a multi-part codebase that owns a source `root` and its own `green-bar` (suite + checks), named in `.add/components.toml` under `[component.<name>]`. A task binds to one with a `component:` header line, which adds that root to its §5 Scope and holds it to that component's green bar at verify. Declared, never inferred; a project with no components is byte-identical to a single-codebase project. See [17 Components](./17-components.md).
20
+
21
+ **Cross-component contract** — the frozen, machine-checkable interface between a producer component and its consumers, declared under `[contract.<id>]` (producer + consumers). A task names its role with a `produces: <id>` or `consumes: <id>` header. On the producer's freeze the engine writes an immutable snapshot at `.add/contracts/<id>.json`; a consumer pins its hash and is flagged `contract_consumer_stale` if the producer later re-freezes a changed shape. Inside one milestone a `consumes:` task is HELD from writing its §3 until the producer's snapshot exists — the intra-milestone BE→FE ordering. See [17 Components](./17-components.md).
22
+
23
+ **Federation (multi-repo)** — the transport that carries a frozen cross-component contract between separate repositories. A consumer repo declares `[federation.<id>]` with a `source` (and optional `pin`); `add.py federate pull <id>` validates the producer repo's published snapshot and lands a byte-for-byte copy locally, where it behaves exactly as in a monorepo. Fail-loud: an unknown id, unreadable source, invalid snapshot, or version mismatch HARD-STOPS and lands nothing. Each repo keeps its own git-native `state.json`; only the immutable snapshot crosses. See [17 Components](./17-components.md).
24
+
19
25
  **Disposable code** — the view that code is one regenerable implementation of the artifacts, not a durable asset to be preserved.
20
26
 
21
27
  **Evidence bundle** — the proof attached to a change (passing tests, clean security scan, no coverage loss) that justifies trusting it and may unlock more AI autonomy.
@@ -78,7 +84,7 @@
78
84
 
79
85
  **Baseline approval** (formerly "the lock-down") — the single human gate ending autonomous setup: an explicit yes that freezes the foundation, first scope, and first contract together; runs as `add.py lock --by <name>`.
80
86
 
81
- **Scope level** (formerly "altitude") — the granularity a decision lives at: intake level (request → versioned scope) · milestone level · setup/foundation level · task level · release level (≥1 closed milestone → a versioned, watched cut; see **Release scope level**). (A cross-stage decision lives one level out, at the **stage-graduation** loop — which `graduate.md` also numbers as a scope level; see **Stage graduation**.) One ⚠-assumption notation is shared across every scope level.
87
+ **Scope level** (formerly "altitude") — the granularity a decision lives at, as one ordered ladder of five: **setup/foundation** · **intake** (request → versioned scope) · **the milestone loop** (the task is its inner unit) · **stage graduation** (changes rigor, not version; see **Stage graduation**) · **release** (≥1 closed milestone → a versioned, watched cut; see **Release scope level**). One ⚠-assumption notation is shared across every scope level.
82
88
 
83
89
  **Autonomy level** (formerly "autonomy dial") — the explicit per-task setting (`autonomy: manual | conservative | auto`, an ordered ladder manual < conservative < auto) choosing who resolves Verify: `auto` auto-PASSes on complete evidence, `conservative` keeps a human at the gate, `manual` is the strict floor (the human owns the gate; nothing auto-resolves). A high-risk scope refuses an unguarded `auto` — it must be lowered to `manual` or `conservative`. New tasks seed a visible, overridable `autonomy: auto`; a live task with no level warns (`implicit_autonomy`), a token outside the set is rejected (`unknown_autonomy_level`).
84
90
 
@@ -128,9 +134,9 @@
128
134
 
129
135
  **Newest-first append-only** — every append-only foundation sequence prepends the newest record at the top; the rolled-up settled line anchors at the bottom (the oldest end), so compaction collapses upward.
130
136
 
131
- **Wireframe** — the Stage-A low-fidelity, *structural* map of one screen: its regions and the component **slots** inside them, derived from `prototypes/<name>.json` *before* any color, type, or spacing — it answers "what goes where", not "what it looks like". Beat 3 of the UDD **design-definition loop**; the low-fi half of the two-stage fidelity that ends in a confirmed capture. See the `add` skill's `udd-wireframe.md` (Stage A).
137
+ **Wireframe** — the Stage-A low-fidelity, *structural* map of one screen: its regions and the component **slots** inside them, derived from `prototypes/<name>.json` *before* any color, type, or spacing — it answers "what goes where", not "what it looks like". Beat 3 of the UDD **design-definition loop**; the low-fi half of the two-stage fidelity that ends in a confirmed capture. See the `udd-wireframe.md` template (`tooling/templates/`, Stage A).
132
138
 
133
- **Design mock** — the Stage-B high-fidelity, **self-contained** HTML render of a screen: the `catalog.json` components as a reusable token-bound kit, bound to `tokens.json` and populated with mock data, openable offline and screenshot-able. The human-facing *visible* evidence the human confirms (the frozen `prototypes/<name>.json` tree is its machine-checkable twin). Beat 4's hi-fi artifact; the recipe lives in the `add` skill's `udd-wireframe.md` (Stage B).
139
+ **Design mock** — the Stage-B high-fidelity, **self-contained** HTML render of a screen: the `catalog.json` components as a reusable token-bound kit, bound to `tokens.json` and populated with mock data, openable offline and screenshot-able. The human-facing *visible* evidence the human confirms (the frozen `prototypes/<name>.json` tree is its machine-checkable twin). Beat 4's hi-fi artifact; the recipe lives in the `udd-wireframe.md` template (`tooling/templates/`, Stage B).
134
140
 
135
141
  **Capture** — the real rendered image (PNG/SVG) of a design mock: the **design-confirm evidence** artifact. Captures live at `.add/design/captures/<name>.<ext>` (one per prototype) and are attached or mentioned in the feature's `TASK.md`; `@json-render/image` (Satori → PNG/SVG, no browser) is the named default capture engine, otherwise the self-contained mock is screenshot headless. The engine never renders — it only MEASURES presence: `add.py check` raises a never-red `missing_capture` WARN for a prototype with no capture.
136
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotspace/add",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "ADD (AI-Driven Development). One skill. Eight steps. Five disciplines. Every feature ships through the loop — a minimal, state-tracked Claude Code skill that ships the AIDD book as its trust layer.",
5
5
  "bin": {
6
6
  "add": "bin/cli.js"
@@ -19,6 +19,9 @@
19
19
  "bin/",
20
20
  "skill/",
21
21
  "tooling/add.py",
22
+ "tooling/add_engine/",
23
+ "!tooling/add_engine/__pycache__",
24
+ "!**/*.pyc",
22
25
  "tooling/templates/",
23
26
  "docs/",
24
27
  "README.md",
@@ -46,7 +49,7 @@
46
49
  "type": "git",
47
50
  "url": "git+https://github.com/pilotspace/ADD.git"
48
51
  },
49
- "homepage": "https://github.com/pilotspace/ADD#readme",
52
+ "homepage": "https://pilotspace.github.io/ADD/",
50
53
  "bugs": {
51
54
  "url": "https://github.com/pilotspace/ADD/issues"
52
55
  }
@@ -62,7 +62,13 @@ self-improving via the `soul-self-improve` path). Then branch on state:
62
62
  - **No active task** → first SIZE the request (Intake below), then `add.py new-task <slug> --title "..."`.
63
63
 
64
64
  **Quick ref** — `status` resume · `init` bootstrap · `advance` continue · `gate PASS` at verify.
65
- **Opt-in flags** (human picks, never auto): `new-task --fast` = fast lane (minimal template, freeze-gated) · `new-milestone --await-confirm` = confirm-gate the milestone's tasks.
65
+ **Flag mode** — two human-owned settings (never auto-picked): **fast** (task) · **auto** (mode).
66
+ - **fast** — `new-task --fast`: minimal template, freeze-gated; a milestone-free `--fast` task is a
67
+ blessed standalone low-ceremony lane. Jot ideas first with `add.py todo "<text>"` (then `todo` to
68
+ list · `todo --done <id>`).
69
+ - **auto** — `autonomy: auto` (default) auto-gates verify on evidence; lower the autonomy level with
70
+ `add.py autonomy set conservative|manual` for a human gate · `new-milestone --await-confirm`
71
+ confirm-gates a milestone's tasks.
66
72
 
67
73
  ## Intake — size a request before creating scope
68
74
 
@@ -130,6 +136,10 @@ once the human confirms, rewrites `SOUL.md` (the human is the only writer) — `
130
136
  (security HARD-STOP is un-forceable) → human confirms → `add.py release <version>` records the cut
131
137
  (CHANGELOG + `RELEASES.md` ledger + milestone attribution). The engine records; the human runs the
132
138
  tag / publish / deploy. A release bundles ≥1 milestone and is orthogonal to stage.
139
+ - **Monorepo / multi-repo** — a milestone spans more than one green bar (a BE + its FE, services across
140
+ repos) → the **component pillar**: declare components in `.add/components.toml`, gate each task on its
141
+ component's green-bar, freeze cross-component contracts (`produces:`/`consumes:`), hold the FE until the
142
+ BE freezes, and `federate pull` a contract across repos — `components.md`. Opt-in; no components = today.
133
143
 
134
144
  ## Non-negotiable rules (from the method)
135
145