@warpgogol/forge 5.3.4 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.ts +7 -2
  3. package/dist/bin/cli.js +7 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/adr.module.d.ts.map +1 -1
  6. package/dist/os/adr/adr.module.js +2 -0
  7. package/dist/os/adr/adr.module.js.map +1 -1
  8. package/dist/os/audit/audit.module.d.ts.map +1 -1
  9. package/dist/os/audit/audit.module.js +2 -0
  10. package/dist/os/audit/audit.module.js.map +1 -1
  11. package/dist/os/compass/compass.module.d.ts.map +1 -1
  12. package/dist/os/compass/compass.module.js +47 -15
  13. package/dist/os/compass/compass.module.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
  15. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
  17. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
  19. package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
  20. package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/summary-record.d.ts +11 -2
  23. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  24. package/dist/os/compass/handlers/summary-record.js +95 -15
  25. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  26. package/dist/os/core/core.module.d.ts.map +1 -1
  27. package/dist/os/core/core.module.js +124 -73
  28. package/dist/os/core/core.module.js.map +1 -1
  29. package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
  30. package/dist/os/core/handlers/file-size-lint.js +2 -0
  31. package/dist/os/core/handlers/file-size-lint.js.map +1 -1
  32. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  33. package/dist/os/exploration/exploration.module.js +3 -0
  34. package/dist/os/exploration/exploration.module.js.map +1 -1
  35. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  36. package/dist/os/naming/naming-convention.js +11 -1
  37. package/dist/os/naming/naming-convention.js.map +1 -1
  38. package/dist/os/naming/naming.module.d.ts.map +1 -1
  39. package/dist/os/naming/naming.module.js +9 -2
  40. package/dist/os/naming/naming.module.js.map +1 -1
  41. package/dist/os/notes/notes.module.d.ts.map +1 -1
  42. package/dist/os/notes/notes.module.js +17 -6
  43. package/dist/os/notes/notes.module.js.map +1 -1
  44. package/dist/os/plan/plan.module.d.ts.map +1 -1
  45. package/dist/os/plan/plan.module.js +2 -0
  46. package/dist/os/plan/plan.module.js.map +1 -1
  47. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  48. package/dist/os/plugin/plugin.module.js +9 -2
  49. package/dist/os/plugin/plugin.module.js.map +1 -1
  50. package/dist/os/program/program.module.d.ts.map +1 -1
  51. package/dist/os/program/program.module.js +19 -4
  52. package/dist/os/program/program.module.js.map +1 -1
  53. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  54. package/dist/os/queue/handlers/queue-validate.js +154 -7
  55. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  56. package/dist/os/queue/manifest.d.ts +17 -1
  57. package/dist/os/queue/manifest.d.ts.map +1 -1
  58. package/dist/os/queue/manifest.js +127 -6
  59. package/dist/os/queue/manifest.js.map +1 -1
  60. package/dist/os/queue/queue.module.d.ts.map +1 -1
  61. package/dist/os/queue/queue.module.js +32 -4
  62. package/dist/os/queue/queue.module.js.map +1 -1
  63. package/dist/os/queue/types.d.ts +100 -1
  64. package/dist/os/queue/types.d.ts.map +1 -1
  65. package/dist/os/queue/types.js +44 -0
  66. package/dist/os/queue/types.js.map +1 -1
  67. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  68. package/dist/os/rfc/handlers/implement-stamp.js +30 -8
  69. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  70. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  71. package/dist/os/rfc/rfc.module.js +25 -6
  72. package/dist/os/rfc/rfc.module.js.map +1 -1
  73. package/dist/os/rfc/types.d.ts +12 -2
  74. package/dist/os/rfc/types.d.ts.map +1 -1
  75. package/dist/os/rfc/types.js +2 -2
  76. package/dist/os/rfc/types.js.map +1 -1
  77. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  78. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  79. package/dist/os/rfc/verification-evidence.js +134 -23
  80. package/dist/os/rfc/verification-evidence.js.map +1 -1
  81. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  82. package/dist/os/rfc/verification-refresh.js +66 -37
  83. package/dist/os/rfc/verification-refresh.js.map +1 -1
  84. package/dist/os/session/handlers/save.d.ts.map +1 -1
  85. package/dist/os/session/handlers/save.js +42 -5
  86. package/dist/os/session/handlers/save.js.map +1 -1
  87. package/dist/os/session/session.module.d.ts.map +1 -1
  88. package/dist/os/session/session.module.js +5 -0
  89. package/dist/os/session/session.module.js.map +1 -1
  90. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  91. package/dist/os/spec/live-spec-merge.js +55 -134
  92. package/dist/os/spec/live-spec-merge.js.map +1 -1
  93. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  94. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  95. package/dist/os/spec/live-spec-rebuild.js +272 -0
  96. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  97. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  98. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  99. package/dist/os/spec/live-spec-shared.js +315 -0
  100. package/dist/os/spec/live-spec-shared.js.map +1 -0
  101. package/dist/os/spec/live-spec-types.d.ts +17 -1
  102. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  103. package/dist/os/spec/live-spec-types.js +3 -0
  104. package/dist/os/spec/live-spec-types.js.map +1 -1
  105. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  106. package/dist/os/spec/live-spec-validate.js +105 -2
  107. package/dist/os/spec/live-spec-validate.js.map +1 -1
  108. package/dist/os/spec/spec-materialize.d.ts +2 -0
  109. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  110. package/dist/os/spec/spec-materialize.js +45 -5
  111. package/dist/os/spec/spec-materialize.js.map +1 -1
  112. package/dist/os/spec/spec-schema.d.ts +19 -0
  113. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  114. package/dist/os/spec/spec-schema.js +30 -0
  115. package/dist/os/spec/spec-schema.js.map +1 -1
  116. package/dist/os/spec/spec-status.d.ts +4 -0
  117. package/dist/os/spec/spec-status.d.ts.map +1 -1
  118. package/dist/os/spec/spec-status.js +19 -1
  119. package/dist/os/spec/spec-status.js.map +1 -1
  120. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  121. package/dist/os/spec/spec-validate.js +55 -8
  122. package/dist/os/spec/spec-validate.js.map +1 -1
  123. package/dist/os/spec/spec.module.d.ts.map +1 -1
  124. package/dist/os/spec/spec.module.js +84 -8
  125. package/dist/os/spec/spec.module.js.map +1 -1
  126. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  127. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  128. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  129. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  130. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  131. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  132. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  133. package/dist/os/workflow/workflow.module.js +13 -5
  134. package/dist/os/workflow/workflow.module.js.map +1 -1
  135. package/dist/src/cli-flags.d.ts.map +1 -1
  136. package/dist/src/cli-flags.js +25 -5
  137. package/dist/src/cli-flags.js.map +1 -1
  138. package/dist/src/config/forge-config.d.ts +3 -0
  139. package/dist/src/config/forge-config.d.ts.map +1 -1
  140. package/dist/src/config/forge-config.js +2 -0
  141. package/dist/src/config/forge-config.js.map +1 -1
  142. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  143. package/dist/src/onboarding/doctor.js +2 -3
  144. package/dist/src/onboarding/doctor.js.map +1 -1
  145. package/dist/src/onboarding/init.d.ts.map +1 -1
  146. package/dist/src/onboarding/init.js +8 -3
  147. package/dist/src/onboarding/init.js.map +1 -1
  148. package/dist/src/pipeline-status.d.ts +14 -1
  149. package/dist/src/pipeline-status.d.ts.map +1 -1
  150. package/dist/src/pipeline-status.js +28 -8
  151. package/dist/src/pipeline-status.js.map +1 -1
  152. package/dist/src/types.d.ts +7 -1
  153. package/dist/src/types.d.ts.map +1 -1
  154. package/dist/src/types.js +6 -0
  155. package/dist/src/types.js.map +1 -1
  156. package/dist/src/utils/generated-marker.d.ts +8 -0
  157. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  158. package/dist/src/utils/generated-marker.js +25 -0
  159. package/dist/src/utils/generated-marker.js.map +1 -1
  160. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  161. package/dist/src/validators/skill-validate.js +2 -4
  162. package/dist/src/validators/skill-validate.js.map +1 -1
  163. package/os/adr/adr.module.ts +2 -0
  164. package/os/audit/audit.module.ts +2 -0
  165. package/os/compass/compass.module.ts +49 -15
  166. package/os/compass/handlers/compass-audit-handler.ts +51 -10
  167. package/os/compass/handlers/compass-docs-validate.ts +361 -0
  168. package/os/compass/handlers/summary-record.ts +106 -15
  169. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  170. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  171. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  172. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  173. package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
  174. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  175. package/os/core/core.module.ts +147 -75
  176. package/os/core/docs-archive.test.ts +136 -0
  177. package/os/core/handlers/file-size-lint.ts +2 -0
  178. package/os/exploration/exploration.module.ts +3 -0
  179. package/os/naming/naming-convention.ts +11 -1
  180. package/os/naming/naming.module.ts +9 -2
  181. package/os/notes/notes.module.ts +17 -6
  182. package/os/plan/plan-0000-template.md +150 -0
  183. package/os/plan/plan.module.ts +2 -0
  184. package/os/plugin/plugin.module.ts +9 -2
  185. package/os/program/program.module.ts +19 -4
  186. package/os/queue/decision-ledger.test.ts +163 -0
  187. package/os/queue/handlers/queue-validate.ts +190 -8
  188. package/os/queue/manifest.ts +201 -7
  189. package/os/queue/queue-validate.test.ts +303 -0
  190. package/os/queue/queue.module.ts +32 -4
  191. package/os/queue/types.ts +56 -1
  192. package/os/rfc/handlers/implement-stamp.ts +37 -8
  193. package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
  194. package/os/rfc/rfc.module.ts +27 -6
  195. package/os/rfc/types.ts +18 -3
  196. package/os/rfc/verification-emit.test.ts +305 -0
  197. package/os/rfc/verification-evidence.ts +149 -24
  198. package/os/rfc/verification-refresh.test.ts +222 -1
  199. package/os/rfc/verification-refresh.ts +74 -38
  200. package/os/session/handlers/save.ts +37 -5
  201. package/os/session/session.module.ts +5 -0
  202. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  203. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  204. package/os/spec/live-spec-merge.test.ts +187 -7
  205. package/os/spec/live-spec-merge.ts +71 -155
  206. package/os/spec/live-spec-rebuild.test.ts +317 -0
  207. package/os/spec/live-spec-rebuild.ts +356 -0
  208. package/os/spec/live-spec-shared.ts +378 -0
  209. package/os/spec/live-spec-types.ts +21 -1
  210. package/os/spec/live-spec-validate.test.ts +317 -0
  211. package/os/spec/live-spec-validate.ts +106 -2
  212. package/os/spec/spec-materialize.test.ts +192 -0
  213. package/os/spec/spec-materialize.ts +48 -4
  214. package/os/spec/spec-schema.ts +49 -0
  215. package/os/spec/spec-status.ts +24 -0
  216. package/os/spec/spec-validate.test.ts +66 -0
  217. package/os/spec/spec-validate.ts +62 -8
  218. package/os/spec/spec.module.ts +85 -8
  219. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  220. package/os/werkstatt/werkstatt.module.ts +13 -4
  221. package/os/workflow/workflow.module.ts +13 -5
  222. package/package.json +1 -1
  223. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  224. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  225. package/skills/fo/fo-explore/SKILL.md +2 -1
  226. package/skills/fo/fo-fix/SKILL.md +11 -3
  227. package/skills/fo/fo-handoff/SKILL.md +4 -6
  228. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  229. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  230. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  231. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  232. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  233. package/skills/fo/fo-review/SKILL.md +69 -8
  234. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  235. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  236. package/skills/shared/grilling/SKILL.md +12 -0
  237. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  238. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  239. package/src/cli-flags.ts +23 -5
  240. package/src/config/forge-config.ts +4 -0
  241. package/src/onboarding/doctor.ts +2 -3
  242. package/src/onboarding/init.ts +8 -3
  243. package/src/pipeline-status.ts +32 -8
  244. package/src/tests/cli-flags.test.ts +20 -0
  245. package/src/tests/cli-output.test.ts +13 -7
  246. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  247. package/src/tests/implement-stamp.test.ts +38 -0
  248. package/src/tests/session-handlers.test.ts +33 -0
  249. package/src/types.ts +13 -1
  250. package/src/utils/generated-marker.ts +28 -0
  251. package/src/validators/skill-validate.ts +2 -4
  252. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  253. package/skills/fo/fo-review/AXES.md +0 -70
@@ -0,0 +1,356 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>spec.live.rebuild handler — regenerates a living spec by deterministic
4
+ replay of its deduplicated history[] (RFC-1230). The repair path for duplicated
5
+ namespaced sections and duplicate history entries produced before spec.live.merge
6
+ became idempotent. Replaying each unique RFC's current ## Design in first-occurrence
7
+ order yields the canonical spec; hand-applied edits are dropped by design.</purpose>
8
+ <non-goals>
9
+ <item>Do not merge new RFCs — that is spec.live.merge.</item>
10
+ <item>Do not validate — spec.live.validate reports what rebuild must repair.</item>
11
+ <item>Do not delete spec files — empty-history or missing specs are skipped, never removed.</item>
12
+ </non-goals>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-1230: initial spec.live.rebuild handler — dedupe history, single RFC scan, replay via shared section mechanics, unchanged-detection modulo updatedAt.</item>
15
+ <item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
16
+ <item>RFC-1231: step 1 — rename supportsAllSites to acceptsAllFlag
17
+
18
+ Mechanical sweep: the field only ever gated --all argv acceptance; fan-out
19
+ follows the parsed selector. Guard renamed assertAllSitesAllowed ->
20
+ assertAllFlagAccepted, message updated. 417 declaration sites + type
21
+ surfaces (KernelCommandMetadata, ForgeCommandMetadata) in one atomic pass.</item>
22
+ <item>RFC-1234: extracted projectLiveSpec — the pure projection core (dedupe history, replay Design sections, serialize modulo updatedAt) — consumable without writes or logging; spec.live.validate V-LS-09 reuses it so the validator sees exactly what rebuild would write.</item>
23
+ </CHANGE_SUMMARY>
24
+ */
25
+
26
+ import { ambientIo as fs } from "../../src/utils/io.ts";
27
+ import { existsSync } from "../../src/utils/sync-fs.ts";
28
+ import path from "node:path";
29
+ import type {
30
+ ForgeCommandInput,
31
+ ForgeCommandResult,
32
+ ForgeRuntimeContext,
33
+ } from "../../src/types.ts";
34
+ import { writeFileIfChanged, buildGeneratedHeader } from "../../src/utils/index.ts";
35
+ import { listRfcFiles, readAndParseRfc, rfcFileMatchesId } from "../rfc/frontmatter-io.ts";
36
+ import { RFC_DIR } from "../rfc/types.ts";
37
+ import type {
38
+ DeltaOperation,
39
+ LivingSpec,
40
+ LivingSpecHistoryEntry,
41
+ SpecLiveRebuildResult,
42
+ } from "./live-spec-types.ts";
43
+ import {
44
+ LIVE_SPECS_DIR,
45
+ extractDesignSection,
46
+ parseHeadings,
47
+ parseLivingSpec,
48
+ serializeLivingSpec,
49
+ applyDeltasToSpecBody,
50
+ namespaceHeadings,
51
+ seedSpecPrefix,
52
+ } from "./live-spec-shared.ts";
53
+
54
+ interface RebuildSummary {
55
+ command: "spec.live.rebuild";
56
+ domains: SpecLiveRebuildResult[];
57
+ dryRun: boolean;
58
+ }
59
+
60
+ /** Pure projection core (RFC-1234): reads the spec and replays its history
61
+ * into the canonical regenerated bytes — no writes, no logging. The
62
+ * `projection` field is serialized with the spec's own `updatedAt`, so
63
+ * byte-equality with the committed file means "no drift". Consumed by
64
+ * rebuild (write path) and by spec.live.validate V-LS-09 (compare only). */
65
+ export type LiveSpecProjection =
66
+ | { kind: "unreadable" }
67
+ | { kind: "invalid-frontmatter" }
68
+ | { kind: "empty-history" }
69
+ | {
70
+ kind: "projected";
71
+ spec: LivingSpec;
72
+ content: string;
73
+ projection: string;
74
+ regenerated: LivingSpec;
75
+ uniqueIds: string[];
76
+ unreadableRfcs: string[];
77
+ droppedSections: number;
78
+ };
79
+
80
+ export async function projectLiveSpec(
81
+ specFilePath: string,
82
+ domain: string,
83
+ rfcFiles: readonly string[],
84
+ rfcDirPath: string,
85
+ ): Promise<LiveSpecProjection> {
86
+ const content = await fs.readFile(specFilePath).catch(() => null);
87
+ if (content === null) return { kind: "unreadable" };
88
+ const spec = parseLivingSpec(content);
89
+ if (!spec) return { kind: "invalid-frontmatter" };
90
+ if (spec.history.length === 0) return { kind: "empty-history" };
91
+
92
+ const uniqueIds: string[] = [];
93
+ const normalizedHistory: LivingSpecHistoryEntry[] = [];
94
+ for (const entry of spec.history) {
95
+ if (!entry?.rfc || uniqueIds.includes(entry.rfc)) continue;
96
+ uniqueIds.push(entry.rfc);
97
+ normalizedHistory.push(entry);
98
+ }
99
+
100
+ // Count only namespaced duplicates — the metric is "duplicate (RFC-XXXX)
101
+ // sections removed"; non-namespaced heading repeats are out of contract.
102
+ const existingHeadings = parseHeadings(spec.body);
103
+ const seenTexts = new Set<string>();
104
+ let droppedSections = 0;
105
+ for (const h of existingHeadings) {
106
+ if (!/\(RFC-\d{4}\)$/.test(h.text)) continue;
107
+ if (seenTexts.has(h.text)) droppedSections++;
108
+ else seenTexts.add(h.text);
109
+ }
110
+
111
+ const header = buildGeneratedHeader({
112
+ filePath: specFilePath,
113
+ ownerCommand: "spec.live.merge",
114
+ commandPrefix: "pnpm exec werkstatt run",
115
+ });
116
+ // trimEnd so applyDeltas' own "\n\n" separator reproduces the creation path's
117
+ // `seed + designSection` layout exactly — otherwise rebuild inserts an extra
118
+ // blank line after "## Overview" and unchanged-detection never fires.
119
+ let body = seedSpecPrefix(domain, header).trimEnd();
120
+ const unreadableRfcs: string[] = [];
121
+
122
+ for (const rfcId of uniqueIds) {
123
+ const file = rfcFiles.find((f) => rfcFileMatchesId(f, rfcId));
124
+ const parsedResult = file
125
+ ? await readAndParseRfc(rfcDirPath, file)
126
+ : undefined;
127
+ const fm =
128
+ parsedResult && "parsed" in parsedResult ? parsedResult.parsed.frontmatter : undefined;
129
+ const status = fm ? String(fm["status"] ?? "").trim() : "";
130
+ const design = parsedResult && "parsed" in parsedResult
131
+ ? extractDesignSection(parsedResult.parsed.body)
132
+ : "";
133
+ if (!fm || status !== "implemented" || !design) {
134
+ unreadableRfcs.push(rfcId);
135
+ continue;
136
+ }
137
+ const namespaced = namespaceHeadings(design, rfcId);
138
+ const headings = parseHeadings(namespaced);
139
+ const ops: DeltaOperation[] = headings.map((h) => ({
140
+ type: "added",
141
+ heading: h.text,
142
+ rfc: rfcId,
143
+ }));
144
+ body = applyDeltasToSpecBody(body, headings, ops);
145
+ }
146
+
147
+ const regenerated: LivingSpec = {
148
+ ...spec,
149
+ lastMergedRfc: uniqueIds[uniqueIds.length - 1]!,
150
+ history: normalizedHistory,
151
+ body: body.trim(),
152
+ };
153
+
154
+ // Compare modulo updatedAt — a clean spec rebuilt on another day must still
155
+ // report unchanged, otherwise every rebuild produces a one-line date diff.
156
+ const projection = serializeLivingSpec({ ...regenerated, updatedAt: spec.updatedAt });
157
+
158
+ return {
159
+ kind: "projected",
160
+ spec,
161
+ content,
162
+ projection,
163
+ regenerated,
164
+ uniqueIds,
165
+ unreadableRfcs,
166
+ droppedSections,
167
+ };
168
+ }
169
+
170
+ async function rebuildOneSpec(
171
+ specFilePath: string,
172
+ domain: string,
173
+ rfcFiles: readonly string[],
174
+ rfcDirPath: string,
175
+ today: string,
176
+ dryRun: boolean,
177
+ context: ForgeRuntimeContext,
178
+ ): Promise<{ result: SpecLiveRebuildResult; exitCode: number }> {
179
+ const { logger } = context;
180
+
181
+ const base: SpecLiveRebuildResult = {
182
+ command: "spec.live.rebuild",
183
+ domain,
184
+ operation: "skipped",
185
+ uniqueRfcs: 0,
186
+ droppedHistoryEntries: 0,
187
+ droppedSections: 0,
188
+ unreadableRfcs: [],
189
+ dryRun,
190
+ };
191
+
192
+ const proj = await projectLiveSpec(specFilePath, domain, rfcFiles, rfcDirPath);
193
+ if (proj.kind === "unreadable") {
194
+ logger.warn(`spec.live.rebuild: ${domain} unreadable at ${specFilePath} — skipping`);
195
+ return { result: base, exitCode: 1 };
196
+ }
197
+ if (proj.kind === "invalid-frontmatter") {
198
+ logger.warn(`spec.live.rebuild: ${domain} has no valid frontmatter — skipping`);
199
+ return { result: base, exitCode: 1 };
200
+ }
201
+ if (proj.kind === "empty-history") {
202
+ return { result: base, exitCode: 0 };
203
+ }
204
+
205
+ for (const rfcId of proj.unreadableRfcs) {
206
+ logger.warn(
207
+ `spec.live.rebuild: ${rfcId} unreadable, not implemented, or lacks ## Design — skipping`,
208
+ );
209
+ }
210
+
211
+ const replayed = proj.uniqueIds.length - proj.unreadableRfcs.length;
212
+ if (replayed === 0) {
213
+ return {
214
+ result: {
215
+ ...base,
216
+ operation: "skipped",
217
+ uniqueRfcs: proj.uniqueIds.length,
218
+ unreadableRfcs: proj.unreadableRfcs,
219
+ },
220
+ exitCode: 2,
221
+ };
222
+ }
223
+
224
+ const droppedHistoryEntries = proj.spec.history.length - proj.regenerated.history.length;
225
+ if (proj.projection === proj.content) {
226
+ return {
227
+ result: {
228
+ ...base,
229
+ operation: "unchanged",
230
+ uniqueRfcs: proj.uniqueIds.length,
231
+ droppedHistoryEntries,
232
+ droppedSections: proj.droppedSections,
233
+ unreadableRfcs: proj.unreadableRfcs,
234
+ },
235
+ exitCode: 0,
236
+ };
237
+ }
238
+
239
+ const finalSpec = serializeLivingSpec({ ...proj.regenerated, updatedAt: today });
240
+ if (!dryRun) {
241
+ await writeFileIfChanged(specFilePath, finalSpec);
242
+ }
243
+
244
+ return {
245
+ result: {
246
+ ...base,
247
+ operation: "rebuilt",
248
+ uniqueRfcs: proj.uniqueIds.length,
249
+ droppedHistoryEntries,
250
+ droppedSections: proj.droppedSections,
251
+ unreadableRfcs: proj.unreadableRfcs,
252
+ },
253
+ exitCode: 0,
254
+ };
255
+ }
256
+
257
+ export async function runSpecLiveRebuild(
258
+ input: ForgeCommandInput,
259
+ context: ForgeRuntimeContext,
260
+ ): Promise<ForgeCommandResult<RebuildSummary | SpecLiveRebuildResult>> {
261
+ const { workspaceRoot, logger, outputFormat } = context;
262
+ const domain = String(input.flags["domain"] ?? "");
263
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
264
+
265
+ // The kernel consumes `--all` as its own site-selector before argv reaches
266
+ // the command (acceptsAllFlag: true lets it through). Under `werkstatt run`
267
+ // the flag never lands in input.flags — so "no --domain" IS the kernel-level
268
+ // --all signal. The explicit flag still works via the standalone forge CLI.
269
+ const explicitAll = input.flags["all"] === true;
270
+ if (domain && explicitAll) {
271
+ return {
272
+ data: {
273
+ command: "spec.live.rebuild",
274
+ domains: [],
275
+ dryRun,
276
+ },
277
+ exitCode: 1,
278
+ summary: "spec.live.rebuild: --domain and --all are mutually exclusive",
279
+ };
280
+ }
281
+ const all = explicitAll || !domain;
282
+
283
+ const liveSpecsDir = path.join(workspaceRoot, LIVE_SPECS_DIR);
284
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
285
+ const today = new Date().toISOString().slice(0, 10);
286
+
287
+ const specPaths: Array<{ domain: string; filePath: string }> = [];
288
+ if (all) {
289
+ if (existsSync(liveSpecsDir)) {
290
+ const files = (await fs.readdir(liveSpecsDir)).map((e) => e.name);
291
+ for (const f of files.filter((f) => f.endsWith(".md") && f !== "README.md").sort()) {
292
+ specPaths.push({ domain: f.slice(0, -3), filePath: path.join(liveSpecsDir, f) });
293
+ }
294
+ }
295
+ } else {
296
+ const filePath = path.join(liveSpecsDir, `${domain}.md`);
297
+ if (!existsSync(filePath)) {
298
+ return {
299
+ data: {
300
+ command: "spec.live.rebuild",
301
+ domains: [],
302
+ dryRun,
303
+ },
304
+ exitCode: 1,
305
+ summary: `spec.live.rebuild: living spec not found for domain "${domain}" (${LIVE_SPECS_DIR}/${domain}.md)`,
306
+ };
307
+ }
308
+ specPaths.push({ domain, filePath });
309
+ }
310
+
311
+ if (specPaths.length === 0) {
312
+ return {
313
+ data: { command: "spec.live.rebuild", domains: [], dryRun },
314
+ exitCode: 0,
315
+ summary: "spec.live.rebuild: no living specs found",
316
+ };
317
+ }
318
+
319
+ // Single recursive RFC scan reused for every spec — a corrupted spec can carry
320
+ // ~170 history entries, and a per-id listRfcFiles rescan is O(n²).
321
+ const rfcFiles = await listRfcFiles(rfcDirPath);
322
+
323
+ const results: SpecLiveRebuildResult[] = [];
324
+ let worstExit = 0;
325
+ for (const { domain: d, filePath } of specPaths) {
326
+ const { result, exitCode } = await rebuildOneSpec(
327
+ filePath, d, rfcFiles, rfcDirPath, today, dryRun, context,
328
+ );
329
+ results.push(result);
330
+ if (exitCode > worstExit) worstExit = exitCode;
331
+ if (outputFormat === "pretty") {
332
+ const suffix = dryRun ? " [dry-run]" : "";
333
+ if (result.operation === "rebuilt") {
334
+ logger.success(
335
+ `spec.live.rebuild: ${d} rebuilt — ${result.uniqueRfcs} unique RFC(s), dropped ${result.droppedHistoryEntries} history entr(ies), ${result.droppedSections} duplicate section(s)${suffix}`,
336
+ );
337
+ } else {
338
+ logger.info(`spec.live.rebuild: ${d} — ${result.operation}${suffix}`);
339
+ }
340
+ }
341
+ }
342
+
343
+ if (!all) {
344
+ return {
345
+ data: results[0]!,
346
+ exitCode: worstExit,
347
+ summary: `spec.live.rebuild: ${domain} — ${results[0]!.operation}`,
348
+ };
349
+ }
350
+
351
+ return {
352
+ data: { command: "spec.live.rebuild", domains: results, dryRun },
353
+ exitCode: worstExit,
354
+ summary: `spec.live.rebuild: ${results.length} spec(s) processed${dryRun ? " [dry-run]" : ""}`,
355
+ };
356
+ }