@warpgogol/forge 2.7.0 → 2.8.1

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 (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -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 +51 -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 +217 -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/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,161 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import {
3
+ GENERATED_MARKER,
4
+ hasGeneratedMarker,
5
+ stripGeneratedMarker,
6
+ buildGeneratedHeader,
7
+ isGeneratedMarkerTextCandidate,
8
+ } from "../utils/generated-marker.ts";
9
+
10
+ describe("hasGeneratedMarker", () => {
11
+ test("returns true when marker is present", () => {
12
+ expect(hasGeneratedMarker(`// ${GENERATED_MARKER}`)).toBe(true);
13
+ expect(hasGeneratedMarker(`/* ${GENERATED_MARKER} */`)).toBe(true);
14
+ });
15
+
16
+ test("returns false when marker is absent", () => {
17
+ expect(hasGeneratedMarker("// some other comment")).toBe(false);
18
+ expect(hasGeneratedMarker("")).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe("stripGeneratedMarker", () => {
23
+ test("strips line-slash comment with marker", () => {
24
+ const input = `// ${GENERATED_MARKER}\n// DO NOT EDIT\ncontent`;
25
+ const result = stripGeneratedMarker(input);
26
+ expect(result.changed).toBe(true);
27
+ expect(result.content).not.toContain(GENERATED_MARKER);
28
+ expect(result.content).toContain("content");
29
+ });
30
+
31
+ test("strips block comment with marker", () => {
32
+ const input = `/*\n${GENERATED_MARKER}\n*/\ncontent`;
33
+ const result = stripGeneratedMarker(input);
34
+ expect(result.changed).toBe(true);
35
+ expect(result.content).toBe("content");
36
+ });
37
+
38
+ test("strips hash comment with marker", () => {
39
+ const input = `# ${GENERATED_MARKER}\ncontent`;
40
+ const result = stripGeneratedMarker(input);
41
+ expect(result.changed).toBe(true);
42
+ expect(result.content).toBe("content");
43
+ });
44
+
45
+ test("strips HTML comment with marker", () => {
46
+ const input = `<!-- ${GENERATED_MARKER} -->\ncontent`;
47
+ const result = stripGeneratedMarker(input);
48
+ expect(result.changed).toBe(true);
49
+ expect(result.content).toBe("content");
50
+ });
51
+
52
+ test("returns unchanged when no marker", () => {
53
+ const input = "// regular comment\ncontent";
54
+ const result = stripGeneratedMarker(input);
55
+ expect(result.changed).toBe(false);
56
+ expect(result.content).toBe(input);
57
+ });
58
+
59
+ test("strips full header block with advisory lines", () => {
60
+ const input = [
61
+ `// ${GENERATED_MARKER}`,
62
+ "// DO NOT EDIT THIS FILE. Changes are overwritten on the next build.",
63
+ "// Owner command: foo.generate",
64
+ "// Edit instead: template.ts",
65
+ "// Regenerate: forge foo.generate",
66
+ "",
67
+ "actual content",
68
+ ].join("\n");
69
+ const result = stripGeneratedMarker(input);
70
+ expect(result.changed).toBe(true);
71
+ expect(result.content).not.toContain(GENERATED_MARKER);
72
+ expect(result.content.trim()).toBe("actual content");
73
+ });
74
+ });
75
+
76
+ describe("buildGeneratedHeader", () => {
77
+ test("line-slash style for .ts files", () => {
78
+ const header = buildGeneratedHeader({
79
+ filePath: "src/generated.ts",
80
+ ownerCommand: "foo.generate",
81
+ });
82
+ expect(header).toContain(`// ${GENERATED_MARKER}`);
83
+ expect(header).toContain("// DO NOT EDIT");
84
+ expect(header).toContain("Owner command: foo.generate");
85
+ expect(header).toContain("Regenerate: forge foo.generate");
86
+ expect(header.endsWith("\n")).toBe(true);
87
+ });
88
+
89
+ test("line-hash style for .yml files", () => {
90
+ const header = buildGeneratedHeader({
91
+ filePath: "config/generated.yml",
92
+ ownerCommand: "foo.generate",
93
+ });
94
+ expect(header).toContain(`# ${GENERATED_MARKER}`);
95
+ expect(header).toContain("# DO NOT EDIT");
96
+ });
97
+
98
+ test("block-html style for .md files", () => {
99
+ const header = buildGeneratedHeader({
100
+ filePath: "docs/generated.md",
101
+ ownerCommand: "foo.generate",
102
+ });
103
+ expect(header).toContain("<!--");
104
+ expect(header).toContain(GENERATED_MARKER);
105
+ expect(header).toContain("-->");
106
+ });
107
+
108
+ test("block-css style for .css files", () => {
109
+ const header = buildGeneratedHeader({
110
+ filePath: "styles/generated.css",
111
+ ownerCommand: "foo.generate",
112
+ });
113
+ expect(header).toContain("/*");
114
+ expect(header).toContain(GENERATED_MARKER);
115
+ expect(header).toContain("*/");
116
+ });
117
+
118
+ test("includes --site when site is provided", () => {
119
+ const header = buildGeneratedHeader({
120
+ filePath: "src/generated.ts",
121
+ ownerCommand: "foo.generate",
122
+ site: "warpgogol-com",
123
+ });
124
+ expect(header).toContain("--site warpgogol-com");
125
+ });
126
+
127
+ test("uses custom commandPrefix", () => {
128
+ const header = buildGeneratedHeader({
129
+ filePath: "src/generated.ts",
130
+ ownerCommand: "foo.generate",
131
+ commandPrefix: "pnpm exec werkstatt run",
132
+ });
133
+ expect(header).toContain("pnpm exec werkstatt run foo.generate");
134
+ });
135
+
136
+ test("includes templatePath in Edit instead line", () => {
137
+ const header = buildGeneratedHeader({
138
+ filePath: "src/generated.ts",
139
+ ownerCommand: "foo.generate",
140
+ templatePath: "templates/foo.ts",
141
+ });
142
+ expect(header).toContain("Edit instead: templates/foo.ts");
143
+ });
144
+ });
145
+
146
+ describe("isGeneratedMarkerTextCandidate", () => {
147
+ test("returns true for text file extensions", () => {
148
+ expect(isGeneratedMarkerTextCandidate("file.ts")).toBe(true);
149
+ expect(isGeneratedMarkerTextCandidate("file.js")).toBe(true);
150
+ expect(isGeneratedMarkerTextCandidate("file.md")).toBe(true);
151
+ expect(isGeneratedMarkerTextCandidate("file.yml")).toBe(true);
152
+ expect(isGeneratedMarkerTextCandidate("file.json")).toBe(true);
153
+ expect(isGeneratedMarkerTextCandidate("file.astro")).toBe(true);
154
+ });
155
+
156
+ test("returns false for non-text extensions", () => {
157
+ expect(isGeneratedMarkerTextCandidate("file.png")).toBe(false);
158
+ expect(isGeneratedMarkerTextCandidate("file.webp")).toBe(false);
159
+ expect(isGeneratedMarkerTextCandidate("file.mp4")).toBe(false);
160
+ });
161
+ });
@@ -0,0 +1,37 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { byteHash } from "../utils/hash.ts";
3
+
4
+ describe("byteHash", () => {
5
+ test("returns sha256-prefixed hex string", () => {
6
+ const result = byteHash("hello");
7
+ expect(result).toMatch(/^sha256:[0-9a-f]{64}$/);
8
+ });
9
+
10
+ test("is deterministic for same input", () => {
11
+ expect(byteHash("test")).toBe(byteHash("test"));
12
+ });
13
+
14
+ test("differs for different input", () => {
15
+ expect(byteHash("a")).not.toBe(byteHash("b"));
16
+ });
17
+
18
+ test("accepts Uint8Array", () => {
19
+ const data = new Uint8Array([104, 101, 108, 108, 111]); // "hello"
20
+ expect(byteHash(data)).toBe(byteHash("hello"));
21
+ });
22
+
23
+ test("matches known SHA-256 value", () => {
24
+ // SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
25
+ expect(byteHash("hello")).toBe(
26
+ "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
27
+ );
28
+ });
29
+
30
+ test("empty string produces valid hash", () => {
31
+ expect(byteHash("")).toMatch(/^sha256:[0-9a-f]{64}$/);
32
+ // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
33
+ expect(byteHash("")).toBe(
34
+ "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
35
+ );
36
+ });
37
+ });
@@ -0,0 +1,505 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+
7
+ import { runRfcImplementStamp } from "../../os/rfc/handlers/implement-stamp.ts";
8
+ import type { ForgeCommandInput, ForgeRuntimeContext, ForgeLogger } from "../../src/types.ts";
9
+
10
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
11
+
12
+ const noopLogger: ForgeLogger = {
13
+ section: () => {},
14
+ info: () => {},
15
+ warn: () => {},
16
+ error: () => {},
17
+ success: () => {},
18
+ };
19
+
20
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
21
+ return {
22
+ workspaceRoot,
23
+ logger: noopLogger,
24
+ dryRun: false,
25
+ outputFormat: "json",
26
+ };
27
+ }
28
+
29
+ function makeInput(id: string, commit: string, dryRun?: boolean): ForgeCommandInput {
30
+ return {
31
+ argv: [],
32
+ flags: {
33
+ id,
34
+ "implementation-commit": commit,
35
+ ...(dryRun ? { "dry-run": true } : {}),
36
+ },
37
+ };
38
+ }
39
+
40
+ function makeInputAutoDetect(id: string, dryRun?: boolean): ForgeCommandInput {
41
+ return {
42
+ argv: [],
43
+ flags: {
44
+ id,
45
+ ...(dryRun ? { "dry-run": true } : {}),
46
+ },
47
+ };
48
+ }
49
+
50
+ const RFC_BODY = (id: string, status: string, criteriaChecked: boolean): string => `---
51
+ id: ${id}
52
+ title: "Test RFC"
53
+ status: ${status}
54
+ kind: policy
55
+ scope: workspace
56
+ owners:
57
+ - architecture
58
+ reviewers:
59
+ - human:test
60
+ createdAt: 2026-07-21
61
+ updatedAt: 2026-07-21
62
+ ---
63
+
64
+ # ${id}: Test RFC
65
+
66
+ ## Context
67
+
68
+ Test context.
69
+
70
+ ## Problem
71
+
72
+ Test problem.
73
+
74
+ ## Decision
75
+
76
+ Test decision.
77
+
78
+ ## Architectural fit
79
+
80
+ Test fit.
81
+
82
+ ## Design
83
+
84
+ Test design.
85
+
86
+ ## Rollout
87
+
88
+ Test rollout.
89
+
90
+ ## Alternatives considered
91
+
92
+ None.
93
+
94
+ ## Risks
95
+
96
+ None.
97
+
98
+ ## Acceptance criteria
99
+
100
+ - [${criteriaChecked ? "x" : " "}] First criterion (evidence: src/foo.ts:10, unit test)
101
+ - [${criteriaChecked ? "x" : " "}] Second criterion (evidence: src/bar.ts:20, integration test)
102
+ - [${criteriaChecked ? "x" : " "}] Third criterion (evidence: src/baz.ts:30, e2e test)
103
+
104
+ ## Implementation notes for agents
105
+
106
+ Test notes.
107
+ `;
108
+
109
+ const RFC_BODY_WITH_DEPS = (
110
+ id: string,
111
+ status: string,
112
+ criteriaChecked: boolean,
113
+ dependsOn: string[],
114
+ ): string => `---
115
+ id: ${id}
116
+ title: "Test RFC"
117
+ status: ${status}
118
+ kind: policy
119
+ scope: workspace
120
+ owners:
121
+ - architecture
122
+ reviewers:
123
+ - human:test
124
+ createdAt: 2026-07-21
125
+ updatedAt: 2026-07-21
126
+ dependsOn:
127
+ ${dependsOn.map((d) => ` - ${d}`).join("\n")}
128
+ ---
129
+
130
+ # ${id}: Test RFC
131
+
132
+ ## Context
133
+
134
+ Test context.
135
+
136
+ ## Problem
137
+
138
+ Test problem.
139
+
140
+ ## Decision
141
+
142
+ Test decision.
143
+
144
+ ## Architectural fit
145
+
146
+ Test fit.
147
+
148
+ ## Design
149
+
150
+ Test design.
151
+
152
+ ## Rollout
153
+
154
+ Test rollout.
155
+
156
+ ## Alternatives considered
157
+
158
+ None.
159
+
160
+ ## Risks
161
+
162
+ None.
163
+
164
+ ## Acceptance criteria
165
+
166
+ - [${criteriaChecked ? "x" : " "}] First criterion (evidence: src/foo.ts:10, unit test)
167
+ - [${criteriaChecked ? "x" : " "}] Second criterion (evidence: src/bar.ts:20, integration test)
168
+ - [${criteriaChecked ? "x" : " "}] Third criterion (evidence: src/baz.ts:30, e2e test)
169
+
170
+ ## Implementation notes for agents
171
+
172
+ Test notes.
173
+ `;
174
+
175
+ async function makeGitRepo(dir: string): Promise<void> {
176
+ execFileSync("git", ["init"], { cwd: dir, timeout: 5000 });
177
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir, timeout: 5000 });
178
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: dir, timeout: 5000 });
179
+ }
180
+
181
+ async function commitAll(dir: string, message: string): Promise<string> {
182
+ execFileSync("git", ["add", "-A"], { cwd: dir, timeout: 5000 });
183
+ execFileSync("git", ["commit", "-m", message], { cwd: dir, timeout: 5000 });
184
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, timeout: 5000 }).toString().trim();
185
+ }
186
+
187
+ // ─── Tests ────────────────────────────────────────────────────────────────────
188
+
189
+ describe("rfc.implement.stamp", () => {
190
+ let workspaceRoot: string;
191
+
192
+ beforeEach(async () => {
193
+ workspaceRoot = await mkdtemp(join(tmpdir(), "rfc-stamp-test-"));
194
+ await mkdir(join(workspaceRoot, "docs", "rfcs"), { recursive: true });
195
+ await makeGitRepo(workspaceRoot);
196
+ });
197
+
198
+ afterEach(async () => {
199
+ await rm(workspaceRoot, { recursive: true, force: true });
200
+ });
201
+
202
+ it("rejects an RFC not in accepted status (RFC-IMP-01)", async () => {
203
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
204
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "draft", true));
205
+ const commit = await commitAll(workspaceRoot, "Initial commit RFC-0001");
206
+
207
+ const result = await runRfcImplementStamp(
208
+ makeInput("RFC-0001", commit),
209
+ makeContext(workspaceRoot),
210
+ );
211
+
212
+ expect(result.exitCode).toBe(1);
213
+ expect(result.data?.status).toBe("fail");
214
+ expect(result.data?.violations).toHaveLength(1);
215
+ expect(result.data?.violations[0]?.rule).toBe("RFC-IMP-01");
216
+ });
217
+
218
+ it("rejects unchecked criteria (RFC-IMP-02)", async () => {
219
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
220
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", false));
221
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
222
+
223
+ const result = await runRfcImplementStamp(
224
+ makeInput("RFC-0001", commit),
225
+ makeContext(workspaceRoot),
226
+ );
227
+
228
+ expect(result.exitCode).toBe(1);
229
+ expect(result.data?.status).toBe("fail");
230
+ const rules = result.data?.violations.map((v) => v.rule);
231
+ expect(rules).toContain("RFC-IMP-02");
232
+ });
233
+
234
+ it("rejects an RFC file with uncommitted changes (RFC-IMP-04)", async () => {
235
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
236
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
237
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
238
+
239
+ // Make an uncommitted edit to the RFC file itself
240
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true) + "\nextra\n");
241
+
242
+ const result = await runRfcImplementStamp(
243
+ makeInput("RFC-0001", commit),
244
+ makeContext(workspaceRoot),
245
+ );
246
+
247
+ expect(result.exitCode).toBe(1);
248
+ expect(result.data?.status).toBe("fail");
249
+ const rules = result.data?.violations.map((v) => v.rule);
250
+ expect(rules).toContain("RFC-IMP-04");
251
+ });
252
+
253
+ it("ignores uncommitted changes in unrelated files (multi-agent)", async () => {
254
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
255
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
256
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
257
+
258
+ // Create uncommitted files from other agents — must NOT block stamping
259
+ await writeFile(join(workspaceRoot, "dirty.txt"), "dirty");
260
+ await mkdir(join(workspaceRoot, "src"), { recursive: true });
261
+ await writeFile(join(workspaceRoot, "src", "other-agent.ts"), "// other agent\n");
262
+
263
+ const result = await runRfcImplementStamp(
264
+ makeInput("RFC-0001", commit),
265
+ makeContext(workspaceRoot),
266
+ );
267
+
268
+ expect(result.exitCode).toBe(0);
269
+ expect(result.data?.status).toBe("pass");
270
+ expect(result.data?.violations).toEqual([]);
271
+ });
272
+
273
+ it("rejects a commit that does not reference the RFC (RFC-IMP-03)", async () => {
274
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
275
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
276
+ await commitAll(workspaceRoot, "Add RFC-0001");
277
+
278
+ // Make a separate commit that doesn't reference the RFC
279
+ await mkdir(join(workspaceRoot, "src"), { recursive: true });
280
+ await writeFile(join(workspaceRoot, "src", "change.ts"), "// unrelated\n");
281
+ const unrelatedCommit = await commitAll(workspaceRoot, "Some unrelated change");
282
+
283
+ const result = await runRfcImplementStamp(
284
+ makeInput("RFC-0001", unrelatedCommit),
285
+ makeContext(workspaceRoot),
286
+ );
287
+
288
+ expect(result.exitCode).toBe(1);
289
+ expect(result.data?.status).toBe("fail");
290
+ const rules = result.data?.violations.map((v) => v.rule);
291
+ expect(rules).toContain("RFC-IMP-03");
292
+ });
293
+
294
+ it("rejects a non-existent RFC (RFC-IMP-01)", async () => {
295
+ // Need at least one file to commit
296
+ await writeFile(join(workspaceRoot, "README.md"), "# test");
297
+ const commit = await commitAll(workspaceRoot, "Initial commit");
298
+
299
+ const result = await runRfcImplementStamp(
300
+ makeInput("RFC-9999", commit),
301
+ makeContext(workspaceRoot),
302
+ );
303
+
304
+ expect(result.exitCode).toBe(1);
305
+ expect(result.data?.status).toBe("fail");
306
+ expect(result.data?.violations[0]?.rule).toBe("RFC-IMP-01");
307
+ });
308
+
309
+ it("passes in dry-run mode for a valid accepted RFC", async () => {
310
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
311
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
312
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
313
+
314
+ const result = await runRfcImplementStamp(
315
+ makeInput("RFC-0001", commit, true),
316
+ makeContext(workspaceRoot),
317
+ );
318
+
319
+ expect(result.exitCode).toBe(0);
320
+ expect(result.data?.status).toBe("pass");
321
+ expect(result.data?.data?.rfcId).toBe("RFC-0001");
322
+ expect(result.data?.data?.criteriaChecked).toBe(3);
323
+ expect(result.data?.violations).toEqual([]);
324
+
325
+ // Verify the file was NOT mutated
326
+ const content = await readFile(rfcFile, "utf-8");
327
+ expect(content).toContain("status: accepted");
328
+ expect(content).not.toContain("status: implemented");
329
+ });
330
+
331
+ it("atomically stamps a valid accepted RFC to implemented", async () => {
332
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
333
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
334
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
335
+
336
+ const result = await runRfcImplementStamp(
337
+ makeInput("RFC-0001", commit),
338
+ makeContext(workspaceRoot),
339
+ );
340
+
341
+ expect(result.exitCode).toBe(0);
342
+ expect(result.data?.status).toBe("pass");
343
+ expect(result.data?.data?.rfcId).toBe("RFC-0001");
344
+ expect(result.data?.data?.implementationCommit).toBe(commit);
345
+ expect(result.data?.data?.criteriaChecked).toBe(3);
346
+ expect(result.data?.violations).toEqual([]);
347
+
348
+ // Verify the file WAS mutated
349
+ const content = await readFile(rfcFile, "utf-8");
350
+ expect(content).toContain("status: implemented");
351
+ expect(content).toMatch(/implementedAt: \d{4}-\d{2}-\d{2}/);
352
+ });
353
+
354
+ // ── RFC-0756: auto-detect tests ─────────────────────────────────────────────
355
+
356
+ it("auto-detects the implementation commit when --implementation-commit is omitted (RFC-0756)", async () => {
357
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
358
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
359
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
360
+
361
+ const result = await runRfcImplementStamp(
362
+ makeInputAutoDetect("RFC-0001"),
363
+ makeContext(workspaceRoot),
364
+ );
365
+
366
+ expect(result.exitCode).toBe(0);
367
+ expect(result.data?.status).toBe("pass");
368
+ expect(result.data?.data?.rfcId).toBe("RFC-0001");
369
+ expect(result.data?.data?.implementationCommit).toBe(commit);
370
+ expect(result.data?.violations).toEqual([]);
371
+
372
+ const content = await readFile(rfcFile, "utf-8");
373
+ expect(content).toContain("status: implemented");
374
+ });
375
+
376
+ it("lists multiple candidate commits when auto-detect finds more than one (RFC-0756)", async () => {
377
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
378
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
379
+ await commitAll(workspaceRoot, "Implement RFC-0001 step 1");
380
+ await writeFile(join(workspaceRoot, "src.txt"), "step 2");
381
+ await commitAll(workspaceRoot, "Implement RFC-0001 step 2");
382
+
383
+ const result = await runRfcImplementStamp(
384
+ makeInputAutoDetect("RFC-0001"),
385
+ makeContext(workspaceRoot),
386
+ );
387
+
388
+ expect(result.exitCode).toBe(1);
389
+ expect(result.data?.status).toBe("fail");
390
+ const imp03 = result.data?.violations.find((v) => v.rule === "RFC-IMP-03");
391
+ expect(imp03).toBeDefined();
392
+ expect(imp03?.message).toContain("Multiple commits reference RFC-0001");
393
+ expect(imp03?.message).toContain("Pass --implementation-commit");
394
+ });
395
+
396
+ it("errors with a clear message when no commit references the RFC (RFC-0756)", async () => {
397
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
398
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
399
+ await commitAll(workspaceRoot, "Initial commit without RFC ref");
400
+
401
+ const result = await runRfcImplementStamp(
402
+ makeInputAutoDetect("RFC-0001"),
403
+ makeContext(workspaceRoot),
404
+ );
405
+
406
+ expect(result.exitCode).toBe(1);
407
+ expect(result.data?.status).toBe("fail");
408
+ const imp03 = result.data?.violations.find((v) => v.rule === "RFC-IMP-03");
409
+ expect(imp03).toBeDefined();
410
+ expect(imp03?.message).toContain("No commit referencing RFC-0001 found");
411
+ expect(imp03?.message).toContain("Pass --implementation-commit");
412
+ });
413
+
414
+ it("explicit --implementation-commit overrides auto-detect when multiple commits exist (RFC-0756)", async () => {
415
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
416
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
417
+ await commitAll(workspaceRoot, "Implement RFC-0001 step 1");
418
+ await writeFile(join(workspaceRoot, "src.txt"), "step 2");
419
+ const secondCommit = await commitAll(workspaceRoot, "Implement RFC-0001 step 2");
420
+
421
+ const result = await runRfcImplementStamp(
422
+ makeInput("RFC-0001", secondCommit),
423
+ makeContext(workspaceRoot),
424
+ );
425
+
426
+ expect(result.exitCode).toBe(0);
427
+ expect(result.data?.status).toBe("pass");
428
+ expect(result.data?.data?.implementationCommit).toBe(secondCommit);
429
+ expect(result.data?.violations).toEqual([]);
430
+ });
431
+
432
+ // ── RFC-0795: RFC-IMP-07 dependsOn dependency gate tests ──────────────────
433
+
434
+ it("blocks stamping when a dependsOn RFC is not implemented (RFC-IMP-07)", async () => {
435
+ const depFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-dep.md");
436
+ await writeFile(depFile, RFC_BODY("RFC-0001", "accepted", true));
437
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
438
+ await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-0001"]));
439
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
440
+
441
+ const result = await runRfcImplementStamp(
442
+ makeInput("RFC-0002", commit),
443
+ makeContext(workspaceRoot),
444
+ );
445
+
446
+ expect(result.exitCode).toBe(1);
447
+ expect(result.data?.status).toBe("fail");
448
+ const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
449
+ expect(imp07).toBeDefined();
450
+ expect(imp07?.message).toContain("RFC-0001");
451
+ expect(imp07?.message).toContain("not implemented");
452
+ });
453
+
454
+ it("blocks stamping when a dependsOn RFC does not exist (RFC-IMP-07)", async () => {
455
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
456
+ await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-9999"]));
457
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
458
+
459
+ const result = await runRfcImplementStamp(
460
+ makeInput("RFC-0002", commit),
461
+ makeContext(workspaceRoot),
462
+ );
463
+
464
+ expect(result.exitCode).toBe(1);
465
+ expect(result.data?.status).toBe("fail");
466
+ const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
467
+ expect(imp07).toBeDefined();
468
+ expect(imp07?.message).toContain("RFC-9999");
469
+ expect(imp07?.message).toContain("does not exist");
470
+ });
471
+
472
+ it("passes when all dependsOn RFCs are implemented (RFC-IMP-07)", async () => {
473
+ const depFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-dep.md");
474
+ await writeFile(depFile, RFC_BODY("RFC-0001", "implemented", true));
475
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
476
+ await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-0001"]));
477
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
478
+
479
+ const result = await runRfcImplementStamp(
480
+ makeInput("RFC-0002", commit),
481
+ makeContext(workspaceRoot),
482
+ );
483
+
484
+ expect(result.exitCode).toBe(0);
485
+ expect(result.data?.status).toBe("pass");
486
+ const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
487
+ expect(imp07).toBeUndefined();
488
+ });
489
+
490
+ it("passes when dependsOn is absent (RFC-IMP-07)", async () => {
491
+ const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
492
+ await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
493
+ const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
494
+
495
+ const result = await runRfcImplementStamp(
496
+ makeInput("RFC-0001", commit),
497
+ makeContext(workspaceRoot),
498
+ );
499
+
500
+ expect(result.exitCode).toBe(0);
501
+ expect(result.data?.status).toBe("pass");
502
+ const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
503
+ expect(imp07).toBeUndefined();
504
+ });
505
+ });