@warpgogol/forge 5.3.3 → 6.0.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 (282) 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 +48 -16
  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 +39 -9
  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 +326 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
  23. package/dist/os/compass/handlers/compass-inventory.js +5 -2
  24. package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
  25. package/dist/os/compass/handlers/summary-record.d.ts +17 -3
  26. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  27. package/dist/os/compass/handlers/summary-record.js +107 -10
  28. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  29. package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts +18 -0
  30. package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts.map +1 -0
  31. package/dist/os/compass/handlers/tests/forge-yaml-fixture.js +64 -0
  32. package/dist/os/compass/handlers/tests/forge-yaml-fixture.js.map +1 -0
  33. package/dist/os/compass/policy.d.ts +6 -1
  34. package/dist/os/compass/policy.d.ts.map +1 -1
  35. package/dist/os/compass/policy.js +8 -2
  36. package/dist/os/compass/policy.js.map +1 -1
  37. package/dist/os/core/core.module.d.ts.map +1 -1
  38. package/dist/os/core/core.module.js +124 -73
  39. package/dist/os/core/core.module.js.map +1 -1
  40. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  41. package/dist/os/exploration/exploration.module.js +3 -0
  42. package/dist/os/exploration/exploration.module.js.map +1 -1
  43. package/dist/os/mission/handlers/archive.d.ts.map +1 -1
  44. package/dist/os/mission/handlers/archive.js +1 -2
  45. package/dist/os/mission/handlers/archive.js.map +1 -1
  46. package/dist/os/naming/naming.module.d.ts.map +1 -1
  47. package/dist/os/naming/naming.module.js +9 -2
  48. package/dist/os/naming/naming.module.js.map +1 -1
  49. package/dist/os/notes/notes.module.d.ts.map +1 -1
  50. package/dist/os/notes/notes.module.js +17 -6
  51. package/dist/os/notes/notes.module.js.map +1 -1
  52. package/dist/os/plan/plan.module.d.ts.map +1 -1
  53. package/dist/os/plan/plan.module.js +2 -0
  54. package/dist/os/plan/plan.module.js.map +1 -1
  55. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  56. package/dist/os/plugin/plugin.module.js +9 -2
  57. package/dist/os/plugin/plugin.module.js.map +1 -1
  58. package/dist/os/program/program.module.d.ts.map +1 -1
  59. package/dist/os/program/program.module.js +19 -4
  60. package/dist/os/program/program.module.js.map +1 -1
  61. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  62. package/dist/os/queue/handlers/queue-validate.js +154 -7
  63. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  64. package/dist/os/queue/manifest.d.ts +17 -1
  65. package/dist/os/queue/manifest.d.ts.map +1 -1
  66. package/dist/os/queue/manifest.js +127 -6
  67. package/dist/os/queue/manifest.js.map +1 -1
  68. package/dist/os/queue/queue.module.d.ts.map +1 -1
  69. package/dist/os/queue/queue.module.js +32 -4
  70. package/dist/os/queue/queue.module.js.map +1 -1
  71. package/dist/os/queue/types.d.ts +100 -1
  72. package/dist/os/queue/types.d.ts.map +1 -1
  73. package/dist/os/queue/types.js +44 -0
  74. package/dist/os/queue/types.js.map +1 -1
  75. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  76. package/dist/os/rfc/handlers/implement-stamp.js +28 -5
  77. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  78. package/dist/os/rfc/handlers/validate.js +1 -1
  79. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  80. package/dist/os/rfc/rfc.module.js +27 -3
  81. package/dist/os/rfc/rfc.module.js.map +1 -1
  82. package/dist/os/rfc/types.d.ts +11 -0
  83. package/dist/os/rfc/types.d.ts.map +1 -1
  84. package/dist/os/rfc/types.js +1 -0
  85. package/dist/os/rfc/types.js.map +1 -1
  86. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  87. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  88. package/dist/os/rfc/verification-evidence.js +140 -23
  89. package/dist/os/rfc/verification-evidence.js.map +1 -1
  90. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  91. package/dist/os/rfc/verification-refresh.js +72 -37
  92. package/dist/os/rfc/verification-refresh.js.map +1 -1
  93. package/dist/os/session/handlers/save.d.ts.map +1 -1
  94. package/dist/os/session/handlers/save.js +46 -5
  95. package/dist/os/session/handlers/save.js.map +1 -1
  96. package/dist/os/session/session.module.d.ts.map +1 -1
  97. package/dist/os/session/session.module.js +5 -0
  98. package/dist/os/session/session.module.js.map +1 -1
  99. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  100. package/dist/os/spec/live-spec-merge.js +55 -134
  101. package/dist/os/spec/live-spec-merge.js.map +1 -1
  102. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  103. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  104. package/dist/os/spec/live-spec-rebuild.js +272 -0
  105. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  106. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  107. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  108. package/dist/os/spec/live-spec-shared.js +315 -0
  109. package/dist/os/spec/live-spec-shared.js.map +1 -0
  110. package/dist/os/spec/live-spec-types.d.ts +17 -1
  111. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  112. package/dist/os/spec/live-spec-types.js +3 -0
  113. package/dist/os/spec/live-spec-types.js.map +1 -1
  114. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  115. package/dist/os/spec/live-spec-validate.js +105 -2
  116. package/dist/os/spec/live-spec-validate.js.map +1 -1
  117. package/dist/os/spec/spec-materialize.d.ts +2 -0
  118. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  119. package/dist/os/spec/spec-materialize.js +45 -5
  120. package/dist/os/spec/spec-materialize.js.map +1 -1
  121. package/dist/os/spec/spec-schema.d.ts +19 -0
  122. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  123. package/dist/os/spec/spec-schema.js +30 -0
  124. package/dist/os/spec/spec-schema.js.map +1 -1
  125. package/dist/os/spec/spec-status.d.ts +4 -0
  126. package/dist/os/spec/spec-status.d.ts.map +1 -1
  127. package/dist/os/spec/spec-status.js +19 -1
  128. package/dist/os/spec/spec-status.js.map +1 -1
  129. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  130. package/dist/os/spec/spec-validate.js +55 -8
  131. package/dist/os/spec/spec-validate.js.map +1 -1
  132. package/dist/os/spec/spec.module.d.ts.map +1 -1
  133. package/dist/os/spec/spec.module.js +84 -8
  134. package/dist/os/spec/spec.module.js.map +1 -1
  135. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  136. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  137. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  138. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  139. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  140. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  141. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  142. package/dist/os/workflow/workflow.module.js +13 -5
  143. package/dist/os/workflow/workflow.module.js.map +1 -1
  144. package/dist/src/cli-flags.d.ts.map +1 -1
  145. package/dist/src/cli-flags.js +25 -5
  146. package/dist/src/cli-flags.js.map +1 -1
  147. package/dist/src/config/forge-config.d.ts +24 -3
  148. package/dist/src/config/forge-config.d.ts.map +1 -1
  149. package/dist/src/config/forge-config.js +38 -7
  150. package/dist/src/config/forge-config.js.map +1 -1
  151. package/dist/src/onboarding/create.d.ts.map +1 -1
  152. package/dist/src/onboarding/create.js +26 -10
  153. package/dist/src/onboarding/create.js.map +1 -1
  154. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  155. package/dist/src/onboarding/doctor.js +49 -2
  156. package/dist/src/onboarding/doctor.js.map +1 -1
  157. package/dist/src/onboarding/init.d.ts.map +1 -1
  158. package/dist/src/onboarding/init.js +8 -3
  159. package/dist/src/onboarding/init.js.map +1 -1
  160. package/dist/src/onboarding/upgrade.d.ts.map +1 -1
  161. package/dist/src/onboarding/upgrade.js +24 -8
  162. package/dist/src/onboarding/upgrade.js.map +1 -1
  163. package/dist/src/pipeline-status.d.ts +14 -1
  164. package/dist/src/pipeline-status.d.ts.map +1 -1
  165. package/dist/src/pipeline-status.js +28 -8
  166. package/dist/src/pipeline-status.js.map +1 -1
  167. package/dist/src/types.d.ts +7 -1
  168. package/dist/src/types.d.ts.map +1 -1
  169. package/dist/src/types.js +6 -0
  170. package/dist/src/types.js.map +1 -1
  171. package/dist/src/utils/generated-marker.d.ts +8 -0
  172. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  173. package/dist/src/utils/generated-marker.js +25 -0
  174. package/dist/src/utils/generated-marker.js.map +1 -1
  175. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  176. package/dist/src/validators/skill-validate.js +42 -15
  177. package/dist/src/validators/skill-validate.js.map +1 -1
  178. package/os/adr/adr.module.ts +2 -0
  179. package/os/audit/audit.module.ts +2 -0
  180. package/os/compass/compass.module.ts +50 -16
  181. package/os/compass/handlers/compass-audit-handler.ts +50 -9
  182. package/os/compass/handlers/compass-docs-validate.ts +359 -0
  183. package/os/compass/handlers/compass-inventory.ts +6 -3
  184. package/os/compass/handlers/summary-record.ts +118 -10
  185. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  186. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  187. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  188. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  189. package/os/compass/handlers/tests/compass-policy.test.ts +15 -18
  190. package/os/compass/handlers/tests/compass-v2-contract.test.ts +38 -0
  191. package/os/compass/handlers/tests/forge-yaml-fixture.ts +69 -0
  192. package/os/compass/handlers/tests/summary-record.test.ts +152 -0
  193. package/os/compass/policy.ts +8 -2
  194. package/os/core/core.module.ts +147 -75
  195. package/os/core/docs-archive.test.ts +136 -0
  196. package/os/exploration/exploration.module.ts +3 -0
  197. package/os/mission/handlers/archive.ts +1 -2
  198. package/os/naming/naming.module.ts +9 -2
  199. package/os/notes/notes.module.ts +17 -6
  200. package/os/plan/plan-0000-template.md +150 -0
  201. package/os/plan/plan.module.ts +2 -0
  202. package/os/plugin/plugin.module.ts +9 -2
  203. package/os/program/program.module.ts +19 -4
  204. package/os/queue/decision-ledger.test.ts +163 -0
  205. package/os/queue/handlers/queue-validate.ts +190 -8
  206. package/os/queue/manifest.ts +201 -7
  207. package/os/queue/queue-validate.test.ts +303 -0
  208. package/os/queue/queue.module.ts +32 -4
  209. package/os/queue/types.ts +56 -1
  210. package/os/rfc/handlers/implement-stamp.ts +35 -5
  211. package/os/rfc/handlers/validate.ts +1 -1
  212. package/os/rfc/rfc.module.ts +29 -3
  213. package/os/rfc/types.ts +17 -1
  214. package/os/rfc/verification-emit.test.ts +305 -0
  215. package/os/rfc/verification-evidence.ts +155 -24
  216. package/os/rfc/verification-refresh.test.ts +222 -1
  217. package/os/rfc/verification-refresh.ts +80 -38
  218. package/os/session/handlers/save.ts +41 -5
  219. package/os/session/session.module.ts +5 -0
  220. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  221. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  222. package/os/spec/live-spec-merge.test.ts +187 -7
  223. package/os/spec/live-spec-merge.ts +71 -155
  224. package/os/spec/live-spec-rebuild.test.ts +317 -0
  225. package/os/spec/live-spec-rebuild.ts +356 -0
  226. package/os/spec/live-spec-shared.ts +378 -0
  227. package/os/spec/live-spec-types.ts +21 -1
  228. package/os/spec/live-spec-validate.test.ts +317 -0
  229. package/os/spec/live-spec-validate.ts +106 -2
  230. package/os/spec/spec-materialize.test.ts +192 -0
  231. package/os/spec/spec-materialize.ts +48 -4
  232. package/os/spec/spec-schema.ts +49 -0
  233. package/os/spec/spec-status.ts +24 -0
  234. package/os/spec/spec-validate.test.ts +66 -0
  235. package/os/spec/spec-validate.ts +62 -8
  236. package/os/spec/spec.module.ts +85 -8
  237. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  238. package/os/werkstatt/werkstatt.module.ts +13 -4
  239. package/os/workflow/workflow.module.ts +13 -5
  240. package/package.json +1 -1
  241. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  242. package/skills/fo/fo-compass-annotate/SKILL.md +1 -1
  243. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  244. package/skills/fo/fo-explore/SKILL.md +2 -1
  245. package/skills/fo/fo-fix/SKILL.md +11 -3
  246. package/skills/fo/fo-handoff/SKILL.md +6 -8
  247. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  248. package/skills/fo/fo-idea-create-rfc/SKILL.md +3 -2
  249. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  250. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  251. package/skills/fo/fo-idea-implement/SKILL.md +57 -13
  252. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  253. package/skills/fo/fo-review/SKILL.md +69 -8
  254. package/skills/fo/fo-session-retro/SKILL.md +4 -3
  255. package/skills/fo/fo-session-save/SKILL.md +1 -1
  256. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  257. package/skills/shared/grilling/SKILL.md +12 -0
  258. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  259. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  260. package/src/cli-flags.ts +23 -5
  261. package/src/config/forge-config.ts +106 -57
  262. package/src/onboarding/create.ts +25 -9
  263. package/src/onboarding/doctor.ts +51 -2
  264. package/src/onboarding/init.ts +8 -3
  265. package/src/onboarding/upgrade.ts +36 -6
  266. package/src/pipeline-status.ts +32 -8
  267. package/src/tests/bindings-schema.test.ts +53 -1
  268. package/src/tests/cli-flags.test.ts +20 -0
  269. package/src/tests/cli-output.test.ts +13 -7
  270. package/src/tests/doctor-bindings.test.ts +7 -2
  271. package/src/tests/doctor-skills-unmanaged.test.ts +145 -0
  272. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  273. package/src/tests/forge-config.test.ts +106 -2
  274. package/src/tests/implement-stamp.test.ts +38 -0
  275. package/src/tests/session-handlers.test.ts +33 -0
  276. package/src/tests/skill-validate.test.ts +180 -0
  277. package/src/tests/upgrade.test.ts +60 -0
  278. package/src/types.ts +13 -1
  279. package/src/utils/generated-marker.ts +28 -0
  280. package/src/validators/skill-validate.ts +43 -16
  281. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  282. package/skills/fo/fo-review/AXES.md +0 -70
@@ -105,15 +105,44 @@ async function setupWorkspace(opts: {
105
105
  };
106
106
  }
107
107
 
108
- function makeContext(workspaceRoot: string) {
108
+ function makeContext(workspaceRoot: string, overrides: Record<string, unknown> = {}) {
109
109
  return {
110
110
  workspaceRoot,
111
111
  logger: { info: () => {} },
112
112
  outputFormat: "json" as const,
113
113
  commandRegistry: undefined,
114
+ dryRun: false,
115
+ ...overrides,
114
116
  };
115
117
  }
116
118
 
119
+ /**
120
+ * RFC-1237: a minimal recording-style WorkspaceIO — writes land in `writes`,
121
+ * never on disk. Lets tests assert which writes the handler attempted through
122
+ * the port without touching fs.
123
+ */
124
+ function makeSpyIo() {
125
+ const writes: string[] = [];
126
+ const io = {
127
+ readFile: (p: string) => fs.readFile(p, "utf8"),
128
+ writeFile: async (p: string) => {
129
+ writes.push(p);
130
+ },
131
+ mkdir: async (p: string) => {
132
+ writes.push(`mkdir:${p}`);
133
+ },
134
+ exists: async (p: string) => {
135
+ try {
136
+ await fs.stat(p);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ },
142
+ };
143
+ return { io, writes };
144
+ }
145
+
117
146
  beforeEach(() => {
118
147
  mockRunProbe.mockReset();
119
148
  mockCaptureGitContext.mockReset();
@@ -124,6 +153,53 @@ afterEach(() => {
124
153
  vi.restoreAllMocks();
125
154
  });
126
155
 
156
+ describe("rfc.verification.refresh — sweep delta report", () => {
157
+ test("reports recovered (fail→pass) and regressed (pass→fail) transitions", async () => {
158
+ const { workspaceRoot, cleanup } = await setupWorkspace({
159
+ rfcs: [
160
+ { id: "RFC-0100", status: "implemented" },
161
+ { id: "RFC-0101", status: "implemented" },
162
+ { id: "RFC-0102", status: "implemented" },
163
+ ],
164
+ envelopes: [
165
+ { id: "RFC-0100", overall: "fail" },
166
+ { id: "RFC-0101", overall: "pass" },
167
+ { id: "RFC-0102", overall: "fail" },
168
+ ],
169
+ });
170
+
171
+ try {
172
+ // RFC-0100: fail → pass (recovered); RFC-0101: pass → fail (regressed);
173
+ // RFC-0102: fail → fail (still failing — not a regression).
174
+ mockRunProbe
175
+ .mockResolvedValueOnce({ probe: PROBE, ok: true, detail: "exists" })
176
+ .mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" })
177
+ .mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" });
178
+
179
+ const result = await runRfcVerificationRefresh(
180
+ { flags: { all: true } } as unknown as ForgeCommandInput,
181
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
182
+ );
183
+
184
+ expect(result.data!.delta).toEqual({
185
+ recovered: ["RFC-0100"],
186
+ regressed: ["RFC-0101"],
187
+ });
188
+ const entries = Object.fromEntries(result.data!.refreshed.map((r) => [r.rfcId, r]));
189
+ expect(entries["RFC-0100"]?.previousOverall).toBe("fail");
190
+ expect(entries["RFC-0101"]?.previousOverall).toBe("pass");
191
+ expect(entries["RFC-0102"]?.previousOverall).toBe("fail");
192
+ const r101Diag = result.data!.diagnostics.find((d) => d.file?.includes("rfc-0101"));
193
+ expect(r101Diag?.message).toContain("regressed: was pass");
194
+ const r102Diag = result.data!.diagnostics.find((d) => d.file?.includes("rfc-0102"));
195
+ expect(r102Diag?.message).not.toContain("regressed");
196
+ expect(result.summary).toContain("1 regressed, 1 recovered");
197
+ } finally {
198
+ await cleanup();
199
+ }
200
+ });
201
+ });
202
+
127
203
  describe("rfc.verification.refresh — AC-1: single-RFC refresh", () => {
128
204
  test("updates lastRefreshedAt, replaces probes[], preserves emittedAt", async () => {
129
205
  const { workspaceRoot, cleanup } = await setupWorkspace({
@@ -260,6 +336,151 @@ describe("rfc.verification.refresh — AC-4: --dry-run does not write files", ()
260
336
  });
261
337
  });
262
338
 
339
+ describe("rfc.verification.refresh — RFC-1237 AC-1: context.dryRun (the real CLI path)", () => {
340
+ test("werkstatt run --dry-run consumes --dry-run into context.dryRun — zero writes", async () => {
341
+ const { workspaceRoot, cleanup } = await setupWorkspace({
342
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
343
+ envelopes: [{ id: "RFC-0100", emittedAt: "2026-01-01T00:00:00.000Z" }],
344
+ });
345
+
346
+ try {
347
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
348
+ const envelopePath = path.join(
349
+ workspaceRoot,
350
+ "docs",
351
+ "rfcs",
352
+ "verification",
353
+ "rfc-0100.generated.yaml",
354
+ );
355
+ const before = await fs.readFile(envelopePath, "utf-8");
356
+
357
+ // The real CLI path: consumeCommonFlags strips --dry-run from argv into
358
+ // context.dryRun — input.flags carries no "dry-run" key at all.
359
+ const result = await runRfcVerificationRefresh(
360
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
361
+ makeContext(workspaceRoot, { dryRun: true }) as unknown as ForgeRuntimeContext,
362
+ );
363
+
364
+ expect(result.data!.refreshed).toHaveLength(1);
365
+ expect(await fs.readFile(envelopePath, "utf-8")).toBe(before);
366
+ } finally {
367
+ await cleanup();
368
+ }
369
+ });
370
+
371
+ test("recording-IO port sees zero write intents under context.dryRun", async () => {
372
+ const { workspaceRoot, cleanup } = await setupWorkspace({
373
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
374
+ envelopes: [{ id: "RFC-0100" }],
375
+ });
376
+
377
+ try {
378
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
379
+ const { io, writes } = makeSpyIo();
380
+
381
+ await runRfcVerificationRefresh(
382
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
383
+ makeContext(workspaceRoot, { dryRun: true, io }) as unknown as ForgeRuntimeContext,
384
+ );
385
+
386
+ expect(
387
+ writes,
388
+ "dry-run refresh must issue zero io writes — check the dryRun guard in runRfcVerificationRefresh",
389
+ ).toEqual([]);
390
+ } finally {
391
+ await cleanup();
392
+ }
393
+ });
394
+ });
395
+
396
+ describe("rfc.verification.refresh — RFC-1237 AC-2: emit-canonical header", () => {
397
+ test("writes the emit-owned structured marker header, not a refresh-owned one", async () => {
398
+ const { workspaceRoot, cleanup } = await setupWorkspace({
399
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
400
+ envelopes: [{ id: "RFC-0100" }],
401
+ });
402
+
403
+ try {
404
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
405
+
406
+ await runRfcVerificationRefresh(
407
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
408
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
409
+ );
410
+
411
+ const raw = await fs.readFile(
412
+ path.join(workspaceRoot, "docs", "rfcs", "verification", "rfc-0100.generated.yaml"),
413
+ "utf-8",
414
+ );
415
+ // Structured marker keys — identical-by-construction to what emit writes.
416
+ expect(raw).toMatch(/^generatedMarker: GENERATED\./);
417
+ expect(raw).toContain("ownerCommand: rfc.verification.emit");
418
+ expect(raw).toContain("regenerateCommand: pnpm exec werkstatt run rfc.verification.emit");
419
+ // Refresh never stamps itself as owner — it is a conditional secondary writer.
420
+ expect(raw).not.toContain("rfc.verification.refresh");
421
+ } finally {
422
+ await cleanup();
423
+ }
424
+ });
425
+
426
+ test("legacy '#'-comment input header normalizes to the canonical key format", async () => {
427
+ const { workspaceRoot, cleanup } = await setupWorkspace({
428
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
429
+ envelopes: [{ id: "RFC-0100" }],
430
+ });
431
+
432
+ try {
433
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
434
+ await runRfcVerificationRefresh(
435
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
436
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
437
+ );
438
+
439
+ const raw = await fs.readFile(
440
+ path.join(workspaceRoot, "docs", "rfcs", "verification", "rfc-0100.generated.yaml"),
441
+ "utf-8",
442
+ );
443
+ // setupWorkspace wrote the legacy `# Generated by ...` comment header —
444
+ // refresh output must carry no comment lines at all.
445
+ expect(raw.split("\n").every((line) => !line.startsWith("#"))).toBe(true);
446
+ expect(raw).toMatch(/^generatedMarker: GENERATED\./);
447
+ } finally {
448
+ await cleanup();
449
+ }
450
+ });
451
+
452
+ test("writes route through the resolved io port, not ambient fs", async () => {
453
+ const { workspaceRoot, cleanup } = await setupWorkspace({
454
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
455
+ envelopes: [{ id: "RFC-0100", emittedAt: "2026-01-01T00:00:00.000Z" }],
456
+ });
457
+
458
+ try {
459
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
460
+ const { io, writes } = makeSpyIo();
461
+ const envelopePath = path.join(
462
+ workspaceRoot,
463
+ "docs",
464
+ "rfcs",
465
+ "verification",
466
+ "rfc-0100.generated.yaml",
467
+ );
468
+
469
+ await runRfcVerificationRefresh(
470
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
471
+ makeContext(workspaceRoot, { io }) as unknown as ForgeRuntimeContext,
472
+ );
473
+
474
+ expect(writes).toEqual([envelopePath]);
475
+ // The spy io never touches disk — the on-disk file keeps its old content.
476
+ const raw = await fs.readFile(envelopePath, "utf-8");
477
+ expect(raw).not.toContain("lastRefreshedAt");
478
+ } finally {
479
+ await cleanup();
480
+ }
481
+ });
482
+ });
483
+
263
484
  describe("rfc.verification.refresh — AC-5: no evidence envelope", () => {
264
485
  test("skips RFC with reason 'no evidence envelope' and exits non-zero", async () => {
265
486
  const { workspaceRoot, cleanup } = await setupWorkspace({
@@ -14,6 +14,17 @@ lastRefreshedAt, replaces probes[] with fresh results. Supports --id,
14
14
  </MODULE_CONTRACT>
15
15
  <CHANGE_SUMMARY>
16
16
  <item>RFC-0999: initial implementation.</item>
17
+ <item>RFC-1237: dual-read dry-run (context.dryRun || flags["dry-run"]),
18
+ io.writeFile port routing, emit-canonical generated header — refresh owns
19
+ content freshness, never envelope identity (ownerCommand stays
20
+ rfc.verification.emit).</item>
21
+ <item>Session-retro 2026-10-08: sweep delta report — previousOverall per
22
+ envelope + recovered/regressed lists separate still-failing drift from fresh
23
+ regressions without manual triage.</item>
24
+ <item>Session-retro 2026-10-08: --concurrency flag (default 1, garbage values
25
+ warn + fall back to 1, >16 clamps) fans refresh out across RFC units via
26
+ mapWithConcurrency while probes inside one RFC stay sequential — envelope
27
+ ordering stays deterministic.</item>
17
28
  </CHANGE_SUMMARY>
18
29
  */
19
30
 
@@ -24,14 +35,14 @@ import { parse as yamlParse } from "yaml";
24
35
 
25
36
  import { runProbe } from "./acceptance.ts";
26
37
  import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
27
- import { writeFileAtomic } from "../../src/utils/fs-atomic.ts";
28
- import { buildGeneratedHeader } from "../../src/utils/generated-marker.ts";
29
- import { stringify as yamlStringify } from "yaml";
30
38
  import {
31
39
  captureGitContext,
32
40
  getKernelVersion,
41
+ mapWithConcurrency,
42
+ resolveConcurrency,
33
43
  VERIFICATION_DIR,
34
44
  buildEvidenceEnvelope,
45
+ serializeEvidenceEnvelope,
35
46
  } from "./verification-evidence.ts";
36
47
  import { RFC_DIR } from "./types.ts";
37
48
  import type {
@@ -57,7 +68,10 @@ export async function runRfcVerificationRefresh(
57
68
  const rfcDirPath = join(workspaceRoot, RFC_DIR);
58
69
  const targetId = input.flags["id"] as string | undefined;
59
70
  const allMode = input.flags["all"] === true;
60
- const dryRun = input.flags["dry-run"] === true;
71
+ // RFC-1237: consumeCommonFlags strips --dry-run into context.dryRun before
72
+ // dispatch — the input.flags read stays for direct/programmatic callers.
73
+ const dryRun = context.dryRun === true || input.flags["dry-run"] === true;
74
+ const concurrency = resolveConcurrency(input.flags["concurrency"], (m) => logger.warn(m));
61
75
 
62
76
  if (!targetId && !allMode) {
63
77
  return {
@@ -82,19 +96,24 @@ export async function runRfcVerificationRefresh(
82
96
  const gitContext = await captureGitContext(workspaceRoot);
83
97
  const kernelVersion = await getKernelVersion(workspaceRoot);
84
98
 
85
- for (const fileName of allFiles) {
99
+ const processFile = async (
100
+ fileName: string,
101
+ ): Promise<{
102
+ refreshedEntry?: NonNullable<RfcVerificationRefreshResult["refreshed"]>[number];
103
+ skippedEntry?: NonNullable<RfcVerificationRefreshResult["skipped"]>[number];
104
+ diagnostic?: Diagnostic;
105
+ }> => {
86
106
  const parsedFile = await readAndParseRfc(rfcDirPath, fileName);
87
- if (!parsedFile) continue;
88
- if ("error" in parsedFile) continue;
107
+ if (!parsedFile) return {};
108
+ if ("error" in parsedFile) return {};
89
109
  const fm = parsedFile.parsed.frontmatter;
90
110
  const rfcId = String(fm["id"] ?? "");
91
111
  const status = String(fm["status"] ?? "");
92
112
 
93
- if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) continue;
113
+ if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) return {};
94
114
 
95
115
  if (status !== "implemented") {
96
- skipped.push({ rfcId, reason: "not implemented" });
97
- continue;
116
+ return { skippedEntry: { rfcId, reason: "not implemented" } };
98
117
  }
99
118
 
100
119
  const slug = rfcId.toLowerCase();
@@ -113,15 +132,9 @@ export async function runRfcVerificationRefresh(
113
132
  // file doesn't exist or can't be parsed
114
133
  }
115
134
 
116
- if (!existingEnvelope) {
117
- skipped.push({ rfcId, reason: "no evidence envelope" });
118
- continue;
119
- }
120
-
121
135
  const acceptance = fm["acceptance"];
122
- if (!Array.isArray(acceptance) || acceptance.length === 0) {
123
- skipped.push({ rfcId, reason: "no evidence envelope" });
124
- continue;
136
+ if (!existingEnvelope || !Array.isArray(acceptance) || acceptance.length === 0) {
137
+ return { skippedEntry: { rfcId, reason: "no evidence envelope" } };
125
138
  }
126
139
 
127
140
  const probes = acceptance as AcceptanceProbe[];
@@ -156,33 +169,49 @@ export async function runRfcVerificationRefresh(
156
169
  envelope.lastRefreshedAt = now;
157
170
 
158
171
  if (!dryRun) {
159
- const yamlContent = `${buildGeneratedHeader({ filePath: evidenceRelPath, ownerCommand: "rfc.verification.refresh" })}${yamlStringify(envelope)}\n`;
160
- await writeFileAtomic(evidenceAbsPath, yamlContent);
172
+ // RFC-1237: emit-canonical serialization — emit owns these envelopes;
173
+ // refresh owns content freshness, never file identity (OWN-DUP-01).
174
+ // Port write routes through recording IO under kernel --dry-run.
175
+ await io.writeFile(evidenceAbsPath, serializeEvidenceEnvelope(envelope));
161
176
  }
162
177
 
163
178
  const probesFailed = probeRecords.filter((r) => !r.ok).length;
164
- refreshed.push({
165
- rfcId,
166
- file: evidenceRelPath,
167
- overall: envelope.overall,
168
- probesTotal: probeRecords.length,
169
- probesFailed,
170
- });
171
-
172
- if (envelope.overall === "fail") {
173
- diagnostics.push({
174
- ruleId: "RFC-REFRESH-01",
175
- severity: "error",
176
- file: evidenceRelPath,
177
- message: `${rfcId}: refresh overall is "fail" — ${probesFailed}/${probeRecords.length} probe(s) failed.`,
178
- });
179
- }
179
+ const previousOverall = existingEnvelope.overall;
180
180
 
181
181
  if (outputFormat === "pretty") {
182
+ const drift = previousOverall !== envelope.overall ? `, was ${previousOverall}` : "";
182
183
  logger.info(
183
- `[refresh] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${dryRun ? ", dry-run" : ""})`,
184
+ `[refresh] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${drift}${dryRun ? ", dry-run" : ""})`,
184
185
  );
185
186
  }
187
+
188
+ return {
189
+ refreshedEntry: {
190
+ rfcId,
191
+ file: evidenceRelPath,
192
+ overall: envelope.overall,
193
+ previousOverall,
194
+ probesTotal: probeRecords.length,
195
+ probesFailed,
196
+ },
197
+ ...(envelope.overall === "fail"
198
+ ? {
199
+ diagnostic: {
200
+ ruleId: "RFC-REFRESH-01",
201
+ severity: "error",
202
+ file: evidenceRelPath,
203
+ message: `${rfcId}: refresh overall is "fail" — ${probesFailed}/${probeRecords.length} probe(s) failed${previousOverall === "pass" ? " (regressed: was pass)" : ""}.`,
204
+ } satisfies Diagnostic,
205
+ }
206
+ : {}),
207
+ };
208
+ };
209
+
210
+ const results = await mapWithConcurrency(allFiles, concurrency, processFile);
211
+ for (const r of results) {
212
+ if (r.refreshedEntry) refreshed.push(r.refreshedEntry);
213
+ if (r.skippedEntry) skipped.push(r.skippedEntry);
214
+ if (r.diagnostic) diagnostics.push(r.diagnostic);
186
215
  }
187
216
 
188
217
  const passed = refreshed.filter((r) => r.overall === "pass").length;
@@ -190,6 +219,18 @@ export async function runRfcVerificationRefresh(
190
219
  const hasFailures = failed > 0;
191
220
  const hasSkippedNoEnvelope = skipped.some((s) => s.reason === "no evidence envelope");
192
221
  const status: RfcVerificationRefreshResult["status"] = hasFailures ? "fail" : "pass";
222
+ const delta = {
223
+ recovered: refreshed
224
+ .filter((r) => r.previousOverall === "fail" && r.overall === "pass")
225
+ .map((r) => r.rfcId),
226
+ regressed: refreshed
227
+ .filter((r) => r.previousOverall === "pass" && r.overall === "fail")
228
+ .map((r) => r.rfcId),
229
+ };
230
+ const deltaSummary =
231
+ delta.recovered.length + delta.regressed.length > 0
232
+ ? `, ${delta.regressed.length} regressed, ${delta.recovered.length} recovered`
233
+ : "";
193
234
 
194
235
  return {
195
236
  data: {
@@ -198,6 +239,7 @@ export async function runRfcVerificationRefresh(
198
239
  refreshed,
199
240
  skipped,
200
241
  diagnostics,
242
+ delta,
201
243
  summary: {
202
244
  total: refreshed.length,
203
245
  passed,
@@ -206,6 +248,6 @@ export async function runRfcVerificationRefresh(
206
248
  },
207
249
  },
208
250
  exitCode: hasFailures || hasSkippedNoEnvelope ? 1 : 0,
209
- summary: `rfc.verification.refresh: ${refreshed.length} refreshed, ${skipped.length} skipped`,
251
+ summary: `rfc.verification.refresh: ${refreshed.length} refreshed, ${skipped.length} skipped${deltaSummary}`,
210
252
  };
211
253
  }
@@ -19,6 +19,11 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
19
19
  <item>RFC-1097: sweep — werkstatt-engine clean
20
20
 
21
21
  Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
22
+ <item>Session-retro 2026-10-08: raw-file claiming — each .atif is claimed via
23
+ an atomic sibling `<name>.claim-<pid>-<token>` (io rename onto a nonexistent
24
+ target) before read/process/delete; another session's claim skips the file,
25
+ claims clean up in finally, dry-run creates none, keepRaw still preserves the
26
+ raw input. Closes the parallel-session read/delete race.</item>
22
27
  </CHANGE_SUMMARY>
23
28
  */
24
29
 
@@ -196,7 +201,9 @@ export async function runSessionSave(
196
201
  } else {
197
202
  try {
198
203
  const entries = await fs.readdir(rawDirPath);
199
- rawFiles = entries.filter((e) => e.isFile).map((e) => e.name);
204
+ // Only *.atif files are processed — claimed intermediates
205
+ // (*.atif.claim-<pid>) and in-progress writer temp files are skipped.
206
+ rawFiles = entries.filter((e) => e.isFile && e.name.endsWith(".atif")).map((e) => e.name);
200
207
  } catch {
201
208
  rawFiles = [];
202
209
  }
@@ -234,9 +241,25 @@ export async function runSessionSave(
234
241
 
235
242
  for (const rawFileName of rawFiles) {
236
243
  const rawFilePath = path.join(rawDirPath, rawFileName);
244
+ // Atomic claim: rename before reading so two concurrent session.save runs
245
+ // (parallel agent sessions) can't process the same file — the losing
246
+ // rename sees ENOENT and skips. Dry-run stays read-only by design.
247
+ let claimedPath = rawFilePath;
248
+ if (!dryRun) {
249
+ claimedPath = `${rawFilePath}.claim-${process.pid}`;
250
+ try {
251
+ await fs.rename(rawFilePath, claimedPath);
252
+ } catch (err) {
253
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
254
+ skipped.push({ rawFile: rawFileName, reason: "claimed by another process" });
255
+ continue;
256
+ }
257
+ throw err;
258
+ }
259
+ }
237
260
  let rawContent: string;
238
261
  try {
239
- rawContent = await fs.readFile(rawFilePath);
262
+ rawContent = await fs.readFile(claimedPath);
240
263
  } catch (err) {
241
264
  if ((err as NodeJS.ErrnoException).code === "ENOENT") {
242
265
  skipped.push({ rawFile: rawFileName, reason: "already processed by another process" });
@@ -279,12 +302,18 @@ export async function runSessionSave(
279
302
  // Delete raw file even when skipping — otherwise .atif files accumulate
280
303
  if (!dryRun && !keepRaw) {
281
304
  try {
282
- await trashPath(rawFilePath);
305
+ await trashPath(claimedPath);
283
306
  } catch (err) {
284
307
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
285
308
  throw err;
286
309
  }
287
310
  }
311
+ } else if (!dryRun && claimedPath !== rawFilePath) {
312
+ try {
313
+ await fs.rename(claimedPath, rawFilePath);
314
+ } catch {
315
+ // best-effort restore
316
+ }
288
317
  }
289
318
  continue;
290
319
  } catch {
@@ -320,15 +349,22 @@ export async function runSessionSave(
320
349
  );
321
350
  }
322
351
 
323
- // Delete raw file unless --keep-raw
352
+ // Delete raw file unless --keep-raw (then restore the original name —
353
+ // the claim suffix is an internal detail, not part of the file's name)
324
354
  if (!keepRaw) {
325
355
  try {
326
- await trashPath(rawFilePath);
356
+ await trashPath(claimedPath);
327
357
  } catch (err) {
328
358
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
329
359
  throw err;
330
360
  }
331
361
  }
362
+ } else if (claimedPath !== rawFilePath) {
363
+ try {
364
+ await fs.rename(claimedPath, rawFilePath);
365
+ } catch {
366
+ // best-effort restore — the claimed name remains, still recoverable
367
+ }
332
368
  }
333
369
  }
334
370
 
@@ -57,6 +57,7 @@ export async function createForgeSessionModule(): Promise<ForgeModule> {
57
57
  },
58
58
  {
59
59
  name: "session.archive",
60
+ coverage: "operator" as const,
60
61
  description:
61
62
  "Move session files older than --max-age-days (default 7) from " +
62
63
  "docs/sessions/ to docs/sessions/archive/. Bidirectional: files in " +
@@ -84,6 +85,8 @@ export async function createForgeSessionModule(): Promise<ForgeModule> {
84
85
  },
85
86
  {
86
87
  name: "session.validate",
88
+ coverage: "operator" as const,
89
+ coverageNote: "retrospective session-doc audit — pre-existing frontmatter debt",
87
90
  mutatesState: false,
88
91
  contract: "session",
89
92
  rules: [],
@@ -102,6 +105,7 @@ export async function createForgeSessionModule(): Promise<ForgeModule> {
102
105
  },
103
106
  {
104
107
  name: "session.list",
108
+ coverage: "operator" as const,
105
109
  mutatesState: false,
106
110
  description:
107
111
  "List all sessions. Filter with --date-from, --date-to, --rfc, --type flags. " +
@@ -132,6 +136,7 @@ export async function createForgeSessionModule(): Promise<ForgeModule> {
132
136
  },
133
137
  {
134
138
  name: "metrics.aggregate",
139
+ coverage: "operator" as const,
135
140
  mutatesState: true,
136
141
  description:
137
142
  "Query and aggregate skill effectiveness metrics from docs/metrics/ files. " +