@warpgogol/forge 2.8.0 → 2.8.2

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 (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +54 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +221 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,515 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0476: rfc.implement.stamp — the exclusive atomic path for every
5
+ accepted → implemented RFC transition. Validates preconditions, records
6
+ verification evidence, and atomically mutates RFC frontmatter.
7
+ </purpose>
8
+ <non-goals>
9
+ <item>Does not transition non-accepted RFCs.</item>
10
+ <item>Does not bypass V-26/V-27 criterion semantics — reuses evaluateAcceptanceCriteria.</item>
11
+ <item>Does not call the GitHub API or require network access.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>RFC-0476: initial implementation.</item>
16
+ <item>RFC-0756: auto-detect implementation commit when --implementation-commit is omitted.</item>
17
+ <item>RFC-0795: add RFC-IMP-07 dependsOn dependency gate — blocks stamping when any dependsOn entry is not implemented.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import { execFile } from "node:child_process";
22
+ import { readFile, unlink, mkdir } from "node:fs/promises";
23
+ import { join, dirname } from "node:path";
24
+
25
+ import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
26
+ import { parse as yamlParse } from "yaml";
27
+
28
+ import {
29
+ listRfcFiles,
30
+ readAndParseRfc,
31
+ rfcFileMatchesId,
32
+ loadRfcStatusMap,
33
+ } from "../frontmatter-io.ts";
34
+ import { evaluateAcceptanceCriteria } from "./validate-rules.ts";
35
+ import { toIsoDate } from "./shared.ts";
36
+ import { RFC_DIR, RFC_METADATA_CUTOFF } from "../types.ts";
37
+ import type { RfcStatus, RfcImplementStampViolation, RfcImplementStampResult } from "../types.ts";
38
+ import type {
39
+ ForgeCommandInput,
40
+ ForgeCommandResult,
41
+ ForgeRuntimeContext,
42
+ } from "../../../src/types.ts";
43
+
44
+ const VERIFICATION_DIR = join(RFC_DIR, "verification");
45
+
46
+ // ─── Git helpers ─────────────────────────────────────────────────────────────
47
+
48
+ function execGit(workspaceRoot: string, args: string[]): Promise<string> {
49
+ return new Promise((resolve) => {
50
+ execFile("git", args, { cwd: workspaceRoot, timeout: 10000 }, (err, stdout) => {
51
+ if (err) {
52
+ resolve("");
53
+ return;
54
+ }
55
+ resolve(stdout.trim());
56
+ });
57
+ });
58
+ }
59
+
60
+ async function isRfcFileClean(workspaceRoot: string, rfcRelPath: string): Promise<boolean> {
61
+ const status = await execGit(workspaceRoot, ["status", "--porcelain", "--", rfcRelPath]);
62
+ return status.length === 0;
63
+ }
64
+
65
+ async function commitReachableFromHead(workspaceRoot: string, commitSha: string): Promise<boolean> {
66
+ const _result = await execGit(workspaceRoot, ["merge-base", "--is-ancestor", commitSha, "HEAD"]);
67
+ // merge-base --is-ancestor exits 0 if ancestor, 1 if not, >1 on error
68
+ // execGit returns "" on error (non-zero exit), trimmed stdout on success
69
+ // So we need a different approach: check exit code directly
70
+ return new Promise((resolve) => {
71
+ execFile(
72
+ "git",
73
+ ["merge-base", "--is-ancestor", commitSha, "HEAD"],
74
+ { cwd: workspaceRoot, timeout: 10000 },
75
+ (err) => {
76
+ resolve(!err);
77
+ },
78
+ );
79
+ });
80
+ }
81
+
82
+ async function autoDetectImplementationCommit(
83
+ workspaceRoot: string,
84
+ rfcId: string,
85
+ ): Promise<
86
+ { sha: string } | { multiple: Array<{ sha: string; message: string }> } | { none: true }
87
+ > {
88
+ const hashes = await execGit(workspaceRoot, [
89
+ "log",
90
+ "--fixed-strings",
91
+ "--no-merges",
92
+ `--grep=${rfcId}`,
93
+ "--format=%H",
94
+ ]);
95
+
96
+ const hashList = hashes.split("\n").filter(Boolean);
97
+
98
+ if (hashList.length === 0) return { none: true };
99
+ if (hashList.length === 1) return { sha: hashList[0]! };
100
+
101
+ const details = await execGit(workspaceRoot, [
102
+ "log",
103
+ "--fixed-strings",
104
+ "--no-merges",
105
+ `--grep=${rfcId}`,
106
+ "--format=%h %s",
107
+ ]);
108
+
109
+ const candidates = details
110
+ .split("\n")
111
+ .filter(Boolean)
112
+ .map((line) => {
113
+ const spaceIdx = line.indexOf(" ");
114
+ const sha = spaceIdx >= 0 ? line.slice(0, spaceIdx) : line;
115
+ const message = spaceIdx >= 0 ? line.slice(spaceIdx + 1) : "";
116
+ return { sha, message };
117
+ });
118
+
119
+ return { multiple: candidates };
120
+ }
121
+
122
+ async function commitReferencesRfc(
123
+ workspaceRoot: string,
124
+ commitSha: string,
125
+ rfcId: string,
126
+ ): Promise<boolean> {
127
+ // Check the commit message and the diff for the RFC ID
128
+ const commitMsg = await execGit(workspaceRoot, ["log", "-1", "--format=%B", commitSha]);
129
+ if (commitMsg.includes(rfcId)) return true;
130
+
131
+ // Also check if the commit touched a file whose name contains the RFC id
132
+ const changedFiles = await execGit(workspaceRoot, [
133
+ "show",
134
+ "--name-only",
135
+ "--format=",
136
+ commitSha,
137
+ ]);
138
+ const slug = rfcId.toLowerCase();
139
+ if (changedFiles.toLowerCase().includes(slug)) return true;
140
+
141
+ return false;
142
+ }
143
+
144
+ // ─── RFC-specific exclusive lock ─────────────────────────────────────────────
145
+
146
+ function lockFilePath(workspaceRoot: string, rfcId: string): string {
147
+ const slug = rfcId.toLowerCase();
148
+ return join(workspaceRoot, ".rfc-locks", `${slug}.lock`);
149
+ }
150
+
151
+ async function acquireRfcLock(
152
+ workspaceRoot: string,
153
+ rfcId: string,
154
+ ): Promise<{ acquired: boolean; lockPath: string }> {
155
+ const lockPath = lockFilePath(workspaceRoot, rfcId);
156
+ const lockDir = dirname(lockPath);
157
+ await mkdir(lockDir, { recursive: true });
158
+
159
+ // Atomic lock acquisition via O_EXCL file creation
160
+ const { open } = await import("node:fs/promises");
161
+ try {
162
+ const handle = await open(lockPath, "wx");
163
+ await handle.writeFile(
164
+ JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }),
165
+ );
166
+ await handle.close();
167
+ return { acquired: true, lockPath };
168
+ } catch {
169
+ return { acquired: false, lockPath };
170
+ }
171
+ }
172
+
173
+ async function releaseRfcLock(lockPath: string): Promise<void> {
174
+ try {
175
+ await unlink(lockPath);
176
+ } catch {
177
+ // Lock file already removed — safe
178
+ }
179
+ }
180
+
181
+ // ─── Evidence helpers ────────────────────────────────────────────────────────
182
+
183
+ async function checkExistingEvidence(
184
+ workspaceRoot: string,
185
+ rfcId: string,
186
+ ): Promise<{ exists: boolean; overall: string }> {
187
+ const slug = rfcId.toLowerCase();
188
+ const evidencePath = join(workspaceRoot, VERIFICATION_DIR, `${slug}.generated.yaml`);
189
+ try {
190
+ const content = await readFile(evidencePath, "utf-8");
191
+ const evidence = yamlParse(content) as { overall?: string };
192
+ return { exists: true, overall: String(evidence.overall ?? "") };
193
+ } catch {
194
+ return { exists: false, overall: "missing" };
195
+ }
196
+ }
197
+
198
+ // ─── Frontmatter mutation ────────────────────────────────────────────────────
199
+
200
+ function mutateFrontmatter(
201
+ source: string,
202
+ status: "implemented",
203
+ implementedAt: string,
204
+ updatedAt: string,
205
+ ): string {
206
+ let content = source;
207
+
208
+ // Replace status
209
+ content = content.replace(/^status: \w+$/m, `status: ${status}`);
210
+
211
+ // Replace implementedAt (may be empty or already set)
212
+ if (/^implementedAt:.*$/m.test(content)) {
213
+ content = content.replace(/^implementedAt:.*$/m, `implementedAt: ${implementedAt}`);
214
+ } else {
215
+ // Insert after updatedAt
216
+ content = content.replace(
217
+ /^updatedAt: .*$/m,
218
+ `updatedAt: ${updatedAt}\nimplementedAt: ${implementedAt}`,
219
+ );
220
+ }
221
+
222
+ // Replace updatedAt
223
+ content = content.replace(/^updatedAt: .*$/m, `updatedAt: ${updatedAt}`);
224
+
225
+ return content;
226
+ }
227
+
228
+ // ─── Main handler ────────────────────────────────────────────────────────────
229
+
230
+ export async function runRfcImplementStamp(
231
+ input: ForgeCommandInput,
232
+ context: ForgeRuntimeContext,
233
+ ): Promise<ForgeCommandResult<RfcImplementStampResult>> {
234
+ const { workspaceRoot, logger, outputFormat, dryRun } = context;
235
+ const rfcDirPath = join(workspaceRoot, RFC_DIR);
236
+
237
+ const targetId = input.flags["id"] as string | undefined;
238
+ let implementationCommit = input.flags["implementation-commit"] as string | undefined;
239
+
240
+ // ── Flag validation ───────────────────────────────────────────────────────
241
+ if (!targetId) {
242
+ throw new Error("rfc.implement.stamp requires --id flag (e.g. --id RFC-0476).");
243
+ }
244
+
245
+ const violations: RfcImplementStampViolation[] = [];
246
+ const isDryRun = dryRun || input.flags["dry-run"] === true;
247
+
248
+ // ── Find the target RFC ───────────────────────────────────────────────────
249
+ const files = await listRfcFiles(rfcDirPath);
250
+ const targetFile = files.find((f) => rfcFileMatchesId(f, targetId));
251
+
252
+ if (!targetFile) {
253
+ violations.push({
254
+ rule: "RFC-IMP-01",
255
+ message: `RFC ${targetId} not found in ${RFC_DIR}.`,
256
+ });
257
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
258
+ }
259
+
260
+ const targetParsed = await readAndParseRfc(rfcDirPath, targetFile);
261
+ if (!targetParsed) {
262
+ violations.push({
263
+ rule: "RFC-IMP-01",
264
+ message: `Could not parse target RFC ${targetId}.`,
265
+ });
266
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
267
+ }
268
+ if ("error" in targetParsed) {
269
+ violations.push({
270
+ rule: "RFC-IMP-01",
271
+ message: `Could not parse target RFC ${targetId}: ${targetParsed.error}`,
272
+ });
273
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
274
+ }
275
+
276
+ const fm = targetParsed.parsed.frontmatter;
277
+ const body = targetParsed.parsed.body;
278
+ const currentStatus = String(fm["status"] ?? "") as RfcStatus;
279
+
280
+ // ── RFC-IMP-01: must be in accepted status ────────────────────────────────
281
+ if (currentStatus !== "accepted") {
282
+ violations.push({
283
+ rule: "RFC-IMP-01",
284
+ message: `RFC ${targetId} has status "${currentStatus}" — only "accepted" RFCs can be stamped to "implemented".`,
285
+ });
286
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
287
+ }
288
+
289
+ // ── RFC-IMP-02: criterion checks (V-26/V-27 semantics) ────────────────────
290
+ const criteriaEval = evaluateAcceptanceCriteria(body);
291
+ if (criteriaEval.totalUnchecked > 0) {
292
+ violations.push({
293
+ rule: "RFC-IMP-02",
294
+ message: `${criteriaEval.totalUnchecked} acceptance criteria are unchecked. All criteria must be checked before stamping: ${criteriaEval.uncheckedLines.join("; ")}`,
295
+ });
296
+ }
297
+ if (criteriaEval.checkedWithoutEvidence.length > 0) {
298
+ violations.push({
299
+ rule: "RFC-IMP-02",
300
+ message: `${criteriaEval.checkedWithoutEvidence.length} checked criteria lack inline (evidence: ...) annotation: ${criteriaEval.checkedWithoutEvidence.join("; ")}`,
301
+ });
302
+ }
303
+
304
+ // ── RFC-IMP-07: dependsOn dependency gate (RFC-0795) ──────────────────────
305
+ const dependsOn = fm["dependsOn"];
306
+ if (Array.isArray(dependsOn) && dependsOn.length > 0) {
307
+ const statusMap = await loadRfcStatusMap(rfcDirPath);
308
+ for (const depRef of dependsOn) {
309
+ const depId = String(depRef);
310
+ if (!depId) continue;
311
+ const depStatus = statusMap.get(depId);
312
+ if (!depStatus) {
313
+ violations.push({
314
+ rule: "RFC-IMP-07",
315
+ message: `RFC ${targetId} depends on ${depId}, which does not exist in ${RFC_DIR}.`,
316
+ });
317
+ } else if (depStatus !== "implemented") {
318
+ violations.push({
319
+ rule: "RFC-IMP-07",
320
+ message: `RFC ${targetId} depends on ${depId}, which is not implemented (status: ${depStatus}). Implement ${depId} first.`,
321
+ });
322
+ }
323
+ }
324
+ }
325
+
326
+ // ── RFC-0756: auto-detect implementation commit when --implementation-commit is omitted ──
327
+ if (!implementationCommit) {
328
+ const detected = await autoDetectImplementationCommit(workspaceRoot, targetId);
329
+ if ("sha" in detected) {
330
+ implementationCommit = detected.sha;
331
+ } else if ("none" in detected) {
332
+ violations.push({
333
+ rule: "RFC-IMP-03",
334
+ message: `No commit referencing ${targetId} found in git history. Pass --implementation-commit <SHA> explicitly.`,
335
+ });
336
+ } else {
337
+ const candidateList = detected.multiple.map((c) => ` ${c.sha} — ${c.message}`).join("\n");
338
+ violations.push({
339
+ rule: "RFC-IMP-03",
340
+ message: `Multiple commits reference ${targetId}:\n${candidateList}\nPass --implementation-commit <SHA> to specify which one.`,
341
+ });
342
+ }
343
+ }
344
+
345
+ // ── RFC-IMP-04: RFC file must be clean (no uncommitted edits to the target RFC) ──
346
+ // Only the RFC file being stamped is checked, not the entire working tree.
347
+ // This allows multi-agent workflows where other agents may have uncommitted
348
+ // changes in unrelated files.
349
+ const rfcRelPath = join(RFC_DIR, targetFile);
350
+ const rfcFileClean = await isRfcFileClean(workspaceRoot, rfcRelPath);
351
+ if (!rfcFileClean) {
352
+ violations.push({
353
+ rule: "RFC-IMP-04",
354
+ message: `RFC file ${rfcRelPath} has uncommitted changes. Commit the RFC file before stamping.`,
355
+ });
356
+ }
357
+
358
+ // ── RFC-IMP-03: implementation commit validation ──────────────────────────
359
+ if (implementationCommit) {
360
+ const reachable = await commitReachableFromHead(workspaceRoot, implementationCommit);
361
+ if (!reachable) {
362
+ violations.push({
363
+ rule: "RFC-IMP-03",
364
+ message: `Implementation commit ${implementationCommit} is not reachable from HEAD. The commit must be an ancestor of the current HEAD.`,
365
+ });
366
+ } else {
367
+ const referencesRfc = await commitReferencesRfc(
368
+ workspaceRoot,
369
+ implementationCommit,
370
+ targetId,
371
+ );
372
+ if (!referencesRfc) {
373
+ violations.push({
374
+ rule: "RFC-IMP-03",
375
+ message: `Implementation commit ${implementationCommit} does not reference ${targetId} in its message or changed files.`,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ // ── RFC-IMP-06: probe-bearing RFCs require passing evidence ───────────────
382
+ const acceptance = fm["acceptance"];
383
+ const hasProbes = Array.isArray(acceptance) && (acceptance as unknown[]).length > 0;
384
+ const createdAtStr = String(fm["createdAt"] ?? "");
385
+ const requiresEvidence = createdAtStr >= RFC_METADATA_CUTOFF && hasProbes;
386
+
387
+ let evidenceRelPath: string | undefined;
388
+
389
+ if (requiresEvidence) {
390
+ const evidenceCheck = await checkExistingEvidence(workspaceRoot, targetId);
391
+ if (!evidenceCheck.exists || evidenceCheck.overall !== "pass") {
392
+ violations.push({
393
+ rule: "RFC-IMP-06",
394
+ message: `RFC ${targetId} declares acceptance probes but evidence file is missing or overall is not "pass" (got "${evidenceCheck.overall}"). Run: site-kernel run rfc.verification.emit --id ${targetId}`,
395
+ });
396
+ } else {
397
+ const slug = targetId.toLowerCase();
398
+ evidenceRelPath = join(VERIFICATION_DIR, `${slug}.generated.yaml`);
399
+ }
400
+ }
401
+
402
+ // If any violations found, return without mutation
403
+ if (violations.length > 0) {
404
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
405
+ }
406
+
407
+ // ── RFC-IMP-05: acquire RFC-specific exclusive lock ───────────────────────
408
+ const { acquired, lockPath } = await acquireRfcLock(workspaceRoot, targetId);
409
+ if (!acquired) {
410
+ violations.push({
411
+ rule: "RFC-IMP-05",
412
+ message: `A concurrent stamp operation is in progress for ${targetId}. Wait for it to complete and retry.`,
413
+ });
414
+ return stampFailResult(violations, isDryRun, outputFormat, logger);
415
+ }
416
+
417
+ try {
418
+ // ── Atomic mutation ──────────────────────────────────────────────────────
419
+ const rfcFilePath = join(rfcDirPath, targetFile);
420
+ const rfcSource = await readFile(rfcFilePath, "utf-8");
421
+
422
+ const today = toIsoDate(new Date());
423
+ const stampedAt = new Date().toISOString();
424
+ const mutatedSource = mutateFrontmatter(rfcSource, "implemented", today, today);
425
+
426
+ if (isDryRun) {
427
+ if (outputFormat === "pretty") {
428
+ logger.success(`[dry-run] ${targetId} would be stamped as implemented`);
429
+ logger.info(` implementation commit: ${implementationCommit}`);
430
+ logger.info(` criteria checked: ${criteriaEval.totalChecked}`);
431
+ if (evidenceRelPath) {
432
+ logger.info(` evidence: ${evidenceRelPath}`);
433
+ }
434
+ }
435
+ return {
436
+ data: {
437
+ command: "rfc.implement.stamp",
438
+ status: "pass",
439
+ data: {
440
+ rfcId: targetId,
441
+ implementationCommit: implementationCommit!,
442
+ stampedAt,
443
+ criteriaChecked: criteriaEval.totalChecked,
444
+ ...(evidenceRelPath ? { evidencePath: evidenceRelPath } : {}),
445
+ },
446
+ violations: [],
447
+ },
448
+ exitCode: 0,
449
+ summary: `rfc.implement.stamp: dry-run — ${targetId} would be stamped`,
450
+ };
451
+ }
452
+
453
+ // Write the mutated RFC file atomically
454
+ await writeFileAtomic(rfcFilePath, mutatedSource);
455
+
456
+ // ── Re-emit evidence if probes exist (to update with new status) ─────────
457
+ if (hasProbes && requiresEvidence && evidenceRelPath) {
458
+ // Evidence already exists and passed — no need to re-emit on stamp
459
+ // The evidence was generated before stamping; the status change is
460
+ // recorded in the RFC frontmatter, not in the evidence file.
461
+ }
462
+
463
+ if (outputFormat === "pretty") {
464
+ logger.success(`${targetId} stamped as implemented`);
465
+ logger.info(` implementation commit: ${implementationCommit}`);
466
+ logger.info(` criteria checked: ${criteriaEval.totalChecked}`);
467
+ if (evidenceRelPath) {
468
+ logger.info(` evidence: ${evidenceRelPath}`);
469
+ }
470
+ logger.info(` stamped at: ${stampedAt}`);
471
+ }
472
+
473
+ return {
474
+ data: {
475
+ command: "rfc.implement.stamp",
476
+ status: "pass",
477
+ data: {
478
+ rfcId: targetId,
479
+ implementationCommit: implementationCommit!,
480
+ stampedAt,
481
+ criteriaChecked: criteriaEval.totalChecked,
482
+ ...(evidenceRelPath ? { evidencePath: evidenceRelPath } : {}),
483
+ },
484
+ violations: [],
485
+ },
486
+ exitCode: 0,
487
+ summary: `rfc.implement.stamp: ${targetId} stamped as implemented`,
488
+ };
489
+ } finally {
490
+ await releaseRfcLock(lockPath);
491
+ }
492
+ }
493
+
494
+ function stampFailResult(
495
+ violations: RfcImplementStampViolation[],
496
+ isDryRun: boolean,
497
+ outputFormat: string,
498
+ logger: { warn: (msg: string) => void; info: (msg: string) => void },
499
+ ): ForgeCommandResult<RfcImplementStampResult> {
500
+ const prefix = isDryRun ? "[dry-run] " : "";
501
+ if (outputFormat === "pretty") {
502
+ for (const v of violations) {
503
+ logger.warn(`${prefix}${v.rule}: ${v.message}`);
504
+ }
505
+ }
506
+ return {
507
+ data: {
508
+ command: "rfc.implement.stamp",
509
+ status: "fail",
510
+ violations,
511
+ },
512
+ exitCode: 1,
513
+ summary: `${prefix}rfc.implement.stamp: ${violations.length} violation(s)`,
514
+ };
515
+ }