@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,428 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.upgrade handler (RFC-0543) and VERSION sourcing.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0543: initial upgrade handler and VERSION sourcing tests.</item>
7
+ <item>RFC-0552: add test for Forge-vs-pack skill name conflict detection in upgrade.</item>
8
+ </CHANGE_SUMMARY>
9
+ */
10
+
11
+ import { test, expect, beforeEach, afterEach, vi } from "vitest";
12
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
13
+ import { existsSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { runUpgrade } from "../onboarding/upgrade.ts";
17
+ import { runInit } from "../onboarding/init.ts";
18
+ import {
19
+ loadForgeConfig,
20
+ FORGE_CLI_BINDING_DEFAULTS,
21
+ resolvePmRunner,
22
+ } from "../config/forge-config.ts";
23
+
24
+ let tempDir: string;
25
+
26
+ beforeEach(async () => {
27
+ tempDir = await mkdtemp(join(tmpdir(), "forge-upgrade-test-"));
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await rm(tempDir, { recursive: true, force: true });
32
+ });
33
+
34
+ async function setupForgeSource(dir: string, version = "0.2.0"): Promise<void> {
35
+ await mkdir(join(dir, "packages", "forge", "skills", "fo", "fo-idea"), { recursive: true });
36
+ await writeFile(
37
+ join(dir, "packages", "forge", "skills", "fo", "fo-idea", "SKILL.md"),
38
+ "---\nname: fo-idea\n---\n# fo-idea\n",
39
+ "utf8",
40
+ );
41
+ await writeFile(
42
+ join(dir, "packages", "forge", "package.json"),
43
+ JSON.stringify({ name: "@warpgogol/forge", version }),
44
+ "utf8",
45
+ );
46
+ await mkdir(join(dir, "packages", "forge", "os", "rfc"), { recursive: true });
47
+ await writeFile(
48
+ join(dir, "packages", "forge", "os", "rfc", "rfc-0000-template.md"),
49
+ "# RFC Template",
50
+ "utf8",
51
+ );
52
+ await mkdir(join(dir, "packages", "forge", "profiles"), { recursive: true });
53
+ }
54
+
55
+ async function setupConsumerForgeYaml(
56
+ dir: string,
57
+ syncedVersion: string | null,
58
+ customBindings?: Record<string, string | null>,
59
+ ): Promise<void> {
60
+ const commands: Record<string, string | null> = {
61
+ validateRfc: null,
62
+ validateAdr: null,
63
+ implementStamp: null,
64
+ typecheck: null,
65
+ test: null,
66
+ scopedBuild: null,
67
+ specValidate: null,
68
+ sessionSave: null,
69
+ ...customBindings,
70
+ };
71
+ const forgeSection =
72
+ syncedVersion !== null ? `\nforge:\n syncedVersion: ${syncedVersion}\n` : "";
73
+
74
+ const yaml = `schema: forge/config@1
75
+ project:
76
+ name: consumer-project
77
+ stack: []
78
+ packageManager: pnpm
79
+ paths:
80
+ rfcsDir: docs/rfcs
81
+ adrsDir: docs/adrs
82
+ plansDir: docs/plans
83
+ auditsDir: docs/audits
84
+ specsDir: docs/specs
85
+ skillsDir: .agents/skills
86
+ sessionsDir: docs/sessions
87
+ bindings:
88
+ schema: forge/bindings@1
89
+ commands:
90
+ validateRfc: ${commands.validateRfc ? `"${commands.validateRfc}"` : "null"}
91
+ validateAdr: ${commands.validateAdr ? `"${commands.validateAdr}"` : "null"}
92
+ implementStamp: ${commands.implementStamp ? `"${commands.implementStamp}"` : "null"}
93
+ typecheck: ${commands.typecheck ? `"${commands.typecheck}"` : "null"}
94
+ test: ${commands.test ? `"${commands.test}"` : "null"}
95
+ scopedBuild: ${commands.scopedBuild ? `"${commands.scopedBuild}"` : "null"}
96
+ specValidate: ${commands.specValidate ? `"${commands.specValidate}"` : "null"}
97
+ sessionSave: ${commands.sessionSave ? `"${commands.sessionSave}"` : "null"}
98
+ paths:
99
+ invariantsFile: null
100
+ compassDocs: []
101
+ reviewsDir: null
102
+ handoffsDir: null
103
+ sessionsDir: null${forgeSection}`;
104
+ await writeFile(join(dir, "forge.yaml"), yaml, "utf8");
105
+ }
106
+
107
+ function makeContext(workspaceRoot: string, dryRun = false) {
108
+ return {
109
+ workspaceRoot,
110
+ logger: {
111
+ section: () => {},
112
+ info: () => {},
113
+ warn: () => {},
114
+ error: () => {},
115
+ success: () => {},
116
+ },
117
+ dryRun,
118
+ outputFormat: "json" as const,
119
+ commandRegistry: undefined,
120
+ };
121
+ }
122
+
123
+ test("forge.upgrade --dry-run writes no files", async () => {
124
+ await setupForgeSource(tempDir, "0.2.0");
125
+ await setupConsumerForgeYaml(tempDir, null);
126
+
127
+ const result = await runUpgrade(
128
+ { argv: [], flags: { "dry-run": true } },
129
+ makeContext(tempDir, true),
130
+ );
131
+
132
+ expect(result.data?.status).toBe("pass");
133
+ expect(result.data?.skillsUpdated.length).toBeGreaterThan(0);
134
+ // No skills directory should have been created
135
+ expect(existsSync(join(tempDir, ".agents", "skills"))).toBe(false);
136
+ // forge.yaml should still have no forge section
137
+ const yaml = await readFile(join(tempDir, "forge.yaml"), "utf8");
138
+ expect(yaml).not.toContain("syncedVersion: 0.2.0");
139
+ });
140
+
141
+ test("forge.upgrade with syncedVersion: null performs full sync", async () => {
142
+ await setupForgeSource(tempDir, "0.2.0");
143
+ await setupConsumerForgeYaml(tempDir, null);
144
+
145
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
146
+
147
+ expect(result.data?.status).toBe("pass");
148
+ expect(result.data?.fromVersion).toBe("never-synced");
149
+ expect(result.data?.toVersion).toBe("0.2.0");
150
+ expect(result.data?.skillsUpdated.length).toBeGreaterThan(0);
151
+
152
+ // syncedVersion should be updated in forge.yaml
153
+ const config = loadForgeConfig(tempDir);
154
+ expect(config.forge?.syncedVersion).toBe("0.2.0");
155
+ });
156
+
157
+ test("forge.upgrade with syncedVersion matching installed version returns noop", async () => {
158
+ await setupForgeSource(tempDir, "0.2.0");
159
+ await setupConsumerForgeYaml(tempDir, "0.2.0");
160
+
161
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
162
+
163
+ expect(result.data?.status).toBe("noop");
164
+ expect(result.data?.fromVersion).toBe("0.2.0");
165
+ expect(result.data?.toVersion).toBe("0.2.0");
166
+ expect(result.data?.skillsUpdated).toEqual([]);
167
+ expect(result.exitCode).toBe(0);
168
+ });
169
+
170
+ test("forge.upgrade never overwrites a non-null operator-set binding", async () => {
171
+ await setupForgeSource(tempDir, "0.2.0");
172
+ await setupConsumerForgeYaml(tempDir, null, {
173
+ validateRfc: "my-custom-validate-command",
174
+ });
175
+
176
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
177
+
178
+ expect(result.data?.status).toBe("pass");
179
+
180
+ // The custom binding should be preserved
181
+ const config = loadForgeConfig(tempDir);
182
+ expect(config.bindings?.commands.validateRfc).toBe("my-custom-validate-command");
183
+
184
+ // Other null bindings should have been filled with defaults
185
+ const runner = resolvePmRunner("pnpm");
186
+ expect(config.bindings?.commands.validateAdr).toBe(
187
+ `${runner} forge adr.validate --id {id} --json`,
188
+ );
189
+ expect(config.bindings?.commands.implementStamp).toBe(
190
+ `${runner} forge rfc.implement.stamp --id {id} --implementation-commit {commit}`,
191
+ );
192
+ expect(config.bindings?.commands.specValidate).toBe(
193
+ `${runner} forge spec.validate --spec={id} --json`,
194
+ );
195
+
196
+ // The added bindings should be reported
197
+ const addedKeys = result.data?.bindingsAdded.map((b) => b.key);
198
+ expect(addedKeys).not.toContain("commands.validateRfc");
199
+ expect(addedKeys).toContain("commands.validateAdr");
200
+ expect(addedKeys).toContain("commands.implementStamp");
201
+ expect(addedKeys).toContain("commands.specValidate");
202
+ expect(addedKeys).toContain("commands.sessionSave");
203
+ });
204
+
205
+ test("forge.upgrade refuses when forge.yaml is missing", async () => {
206
+ await setupForgeSource(tempDir, "0.2.0");
207
+
208
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
209
+
210
+ expect(result.data?.status).toBe("fail");
211
+ expect(result.exitCode).toBe(1);
212
+ expect(result.nextSteps?.[0]?.action).toContain("forge create");
213
+ });
214
+
215
+ test("forge.upgrade adds all FORGE_CLI_BINDING_DEFAULTS keys when all are null", async () => {
216
+ await setupForgeSource(tempDir, "0.2.0");
217
+ await setupConsumerForgeYaml(tempDir, null);
218
+
219
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
220
+
221
+ expect(result.data?.bindingsAdded.length).toBe(FORGE_CLI_BINDING_DEFAULTS.length);
222
+ });
223
+
224
+ test("forge.init writes forge.syncedVersion on first init", async () => {
225
+ await setupForgeSource(tempDir, "0.3.0");
226
+
227
+ runInit({ flags: {} }, { workspaceRoot: tempDir });
228
+
229
+ const config = loadForgeConfig(tempDir);
230
+ expect(config.forge?.syncedVersion).toBe("0.3.0");
231
+ });
232
+
233
+ test("forge.upgrade syncs skills to .agents/skills/", async () => {
234
+ await setupForgeSource(tempDir, "0.2.0");
235
+ await setupConsumerForgeYaml(tempDir, null);
236
+
237
+ await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
238
+
239
+ // Check that fo-idea skill was copied
240
+ const skillPath = join(tempDir, ".agents", "skills", "fo-idea", "SKILL.md");
241
+ expect(existsSync(skillPath)).toBe(true);
242
+ const content = await readFile(skillPath, "utf8");
243
+ expect(content).toContain("fo-idea");
244
+ });
245
+
246
+ test("RFC-0552: forge.upgrade returns skippedSkills field and syncs non-conflicting pack skills", async () => {
247
+ await setupForgeSource(tempDir, "0.2.0");
248
+
249
+ // Create a pack with prefix 'wg' containing a non-conflicting skill
250
+ await mkdir(join(tempDir, "packages", "wg-skills", "skills", "wg-custom"), { recursive: true });
251
+ await writeFile(
252
+ join(tempDir, "packages", "wg-skills", "skills", "wg-custom", "SKILL.md"),
253
+ "---\nname: wg-custom\n---\n# wg-custom (should be synced)\n",
254
+ "utf8",
255
+ );
256
+
257
+ // Write forge.yaml with skillPacks config and null syncedVersion
258
+ const yaml = `schema: forge/config@1
259
+ project:
260
+ name: consumer-project
261
+ stack: []
262
+ packageManager: pnpm
263
+ paths:
264
+ rfcsDir: docs/rfcs
265
+ adrsDir: docs/adrs
266
+ plansDir: docs/plans
267
+ auditsDir: docs/audits
268
+ specsDir: docs/specs
269
+ skillsDir: .agents/skills
270
+ sessionsDir: docs/sessions
271
+ skillPacks:
272
+ - prefix: wg
273
+ dir: packages/wg-skills/skills
274
+ bindings:
275
+ schema: forge/bindings@1
276
+ commands:
277
+ validateRfc: null
278
+ validateAdr: null
279
+ implementStamp: null
280
+ typecheck: null
281
+ test: null
282
+ scopedBuild: null
283
+ specValidate: null
284
+ paths:
285
+ invariantsFile: null
286
+ compassDocs: []
287
+ reviewsDir: null
288
+ handoffsDir: null
289
+ sessionsDir: null
290
+ `;
291
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
292
+
293
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
294
+
295
+ expect(result.data?.status).toBe("pass");
296
+
297
+ // skippedSkills field should exist and be empty (no conflicts)
298
+ expect(result.data?.skippedSkills).toBeDefined();
299
+ expect(result.data?.skippedSkills).toEqual([]);
300
+
301
+ // The non-conflicting pack skill should be synced
302
+ const wgSkillPath = join(tempDir, ".agents", "skills", "wg-custom", "SKILL.md");
303
+ expect(existsSync(wgSkillPath)).toBe(true);
304
+ });
305
+
306
+ async function setupNpmConsumerForge(dir: string, version = "0.2.0"): Promise<void> {
307
+ const forgeSrcDir = join(dir, "node_modules", "@warpgogol", "forge");
308
+ await mkdir(join(forgeSrcDir, "skills", "fo", "fo-idea"), { recursive: true });
309
+ await writeFile(
310
+ join(forgeSrcDir, "skills", "fo", "fo-idea", "SKILL.md"),
311
+ "---\nname: fo-idea\n---\n# fo-idea\n",
312
+ "utf8",
313
+ );
314
+ await writeFile(
315
+ join(forgeSrcDir, "package.json"),
316
+ JSON.stringify({ name: "@warpgogol/forge", version }),
317
+ "utf8",
318
+ );
319
+ await mkdir(join(forgeSrcDir, "os", "rfc"), { recursive: true });
320
+ await writeFile(join(forgeSrcDir, "os", "rfc", "rfc-0000-template.md"), "# RFC Template", "utf8");
321
+ await mkdir(join(forgeSrcDir, "profiles"), { recursive: true });
322
+ }
323
+
324
+ test("forge.upgrade --update-npm skips npm install in monorepo (packages/forge exists)", async () => {
325
+ await setupForgeSource(tempDir, "0.2.0");
326
+ await setupConsumerForgeYaml(tempDir, null);
327
+
328
+ const result = await runUpgrade(
329
+ { argv: [], flags: { "update-npm": true } },
330
+ makeContext(tempDir, false),
331
+ );
332
+
333
+ expect(result.data?.status).toBe("pass");
334
+ expect(result.data?.npmUpdated).toBe(false);
335
+ expect(result.data?.npmUpdateSkipped).toBe("monorepo (local package, not npm-installed)");
336
+ });
337
+
338
+ test("forge.upgrade --update-npm --dry-run skips npm install (dry-run)", async () => {
339
+ await setupNpmConsumerForge(tempDir, "0.2.0");
340
+ await setupConsumerForgeYaml(tempDir, null);
341
+
342
+ const result = await runUpgrade(
343
+ { argv: [], flags: { "update-npm": true, "dry-run": true } },
344
+ makeContext(tempDir, true),
345
+ );
346
+
347
+ expect(result.data?.status).toBe("pass");
348
+ expect(result.data?.npmUpdated).toBe(false);
349
+ expect(result.data?.npmUpdateSkipped).toBe("dry-run");
350
+ });
351
+
352
+ test("forge.upgrade --update-npm attempts npm install for npm consumer", async () => {
353
+ await setupNpmConsumerForge(tempDir, "0.2.0");
354
+ await setupConsumerForgeYaml(tempDir, null);
355
+
356
+ // Mock execSync to avoid real npm install
357
+ const execSyncMock = vi.fn(() => "");
358
+ vi.resetModules();
359
+ vi.doMock("node:child_process", () => ({
360
+ execSync: execSyncMock,
361
+ exec: vi.fn(),
362
+ execFile: vi.fn(),
363
+ spawn: vi.fn(),
364
+ fork: vi.fn(),
365
+ }));
366
+
367
+ // Re-import with mock
368
+ const { runUpgrade: runUpgradeMocked } = await import("../onboarding/upgrade.ts");
369
+
370
+ const result = await runUpgradeMocked(
371
+ { argv: [], flags: { "update-npm": true } },
372
+ makeContext(tempDir, false),
373
+ );
374
+
375
+ expect(result.data?.status).toBe("pass");
376
+ expect(execSyncMock).toHaveBeenCalled();
377
+ expect(result.data?.npmUpdated).toBe(true);
378
+ expect(result.data?.npmUpdateSkipped).toBe(null);
379
+
380
+ vi.doUnmock("node:child_process");
381
+ vi.resetModules();
382
+ });
383
+
384
+ test("forge.upgrade --update-npm reports failure when npm install throws", async () => {
385
+ await setupNpmConsumerForge(tempDir, "0.2.0");
386
+ await setupConsumerForgeYaml(tempDir, null);
387
+
388
+ // Mock execSync to throw (simulates network error / registry unavailable)
389
+ const execSyncMock = vi.fn(() => {
390
+ throw new Error("network error");
391
+ });
392
+ vi.resetModules();
393
+ vi.doMock("node:child_process", () => ({
394
+ execSync: execSyncMock,
395
+ exec: vi.fn(),
396
+ execFile: vi.fn(),
397
+ spawn: vi.fn(),
398
+ fork: vi.fn(),
399
+ }));
400
+
401
+ const { runUpgrade: runUpgradeMocked } = await import("../onboarding/upgrade.ts");
402
+
403
+ const result = await runUpgradeMocked(
404
+ { argv: [], flags: { "update-npm": true } },
405
+ makeContext(tempDir, false),
406
+ );
407
+
408
+ expect(result.data?.status).toBe("pass");
409
+ expect(execSyncMock).toHaveBeenCalled();
410
+ expect(result.data?.npmUpdated).toBe(false);
411
+ expect(result.data?.npmUpdateSkipped).toBe(
412
+ "install failed (network error or registry unavailable)",
413
+ );
414
+
415
+ vi.doUnmock("node:child_process");
416
+ vi.resetModules();
417
+ });
418
+
419
+ test("forge.upgrade without --update-npm does not attempt npm install", async () => {
420
+ await setupForgeSource(tempDir, "0.2.0");
421
+ await setupConsumerForgeYaml(tempDir, null);
422
+
423
+ const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
424
+
425
+ expect(result.data?.status).toBe("pass");
426
+ expect(result.data?.npmUpdated).toBe(false);
427
+ expect(result.data?.npmUpdateSkipped).toBe(null);
428
+ });