@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,289 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.create command — in-place scaffolding via scaffold + init + package-manager post-processing.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0544: initial forge.create tests.</item>
7
+ <item>RFC-0877: rewrite tests for --in-place mode, add strict empty-directory check and name-derivation tests.</item>
8
+ </CHANGE_SUMMARY>
9
+ */
10
+
11
+ import { test, expect, beforeEach, afterEach } from "vitest";
12
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
13
+ import { existsSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { runCreate } from "../onboarding/create.ts";
17
+ import { loadForgeConfig } from "../config/forge-config.ts";
18
+ import type { ForgeRuntimeContext } from "../types.ts";
19
+
20
+ const silentLogger = {
21
+ section: () => {},
22
+ info: () => {},
23
+ warn: () => {},
24
+ error: () => {},
25
+ success: () => {},
26
+ };
27
+
28
+ const FORGE_ROOT = join(import.meta.dirname, "..", "..");
29
+ const _WORKSPACE_ROOT = join(FORGE_ROOT, "..", "..");
30
+
31
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
32
+ return {
33
+ workspaceRoot,
34
+ logger: silentLogger as never,
35
+ dryRun: false,
36
+ outputFormat: "json",
37
+ forgeRoot: FORGE_ROOT,
38
+ };
39
+ }
40
+
41
+ let tempDir: string;
42
+
43
+ beforeEach(async () => {
44
+ tempDir = await mkdtemp(join(tmpdir(), "create-test-"));
45
+ });
46
+
47
+ afterEach(async () => {
48
+ await rm(tempDir, { recursive: true, force: true });
49
+ });
50
+
51
+ test("forge create --in-place scaffolds in cwd with forge.yaml and docs dirs", async () => {
52
+ const result = await runCreate(
53
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
54
+ makeContext(tempDir),
55
+ );
56
+ expect(result.exitCode).toBe(0);
57
+ expect(result.data?.status).toBe("pass");
58
+
59
+ expect(existsSync(join(tempDir, "forge.yaml"))).toBe(true);
60
+ expect(existsSync(join(tempDir, "docs", "rfcs"))).toBe(true);
61
+ expect(existsSync(join(tempDir, "docs", "adrs"))).toBe(true);
62
+ expect(existsSync(join(tempDir, "PREFERENCES.md"))).toBe(true);
63
+ expect(existsSync(join(tempDir, "package.json"))).toBe(true);
64
+ expect(existsSync(join(tempDir, "scripts", "clean.mjs"))).toBe(true);
65
+ }, 30000);
66
+
67
+ test("forge create --in-place refuses when forge artifacts already exist", async () => {
68
+ await writeFile(join(tempDir, "forge.yaml"), "existing: true", "utf8");
69
+
70
+ const result = await runCreate(
71
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
72
+ makeContext(tempDir),
73
+ );
74
+ expect(result.exitCode).toBe(1);
75
+ expect(result.data?.status).toBe("fail");
76
+ expect(result.data?.errors[0]).toContain("not empty");
77
+ });
78
+
79
+ test("forge create --in-place refuses when non-forge files exist in target directory", async () => {
80
+ await writeFile(join(tempDir, "some-file.txt"), "hello", "utf8");
81
+
82
+ const result = await runCreate(
83
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
84
+ makeContext(tempDir),
85
+ );
86
+ expect(result.exitCode).toBe(1);
87
+ expect(result.data?.status).toBe("fail");
88
+ expect(result.data?.errors[0]).toContain("not empty");
89
+ expect(result.data?.errors[0]).toContain("some-file.txt");
90
+ });
91
+
92
+ test("forge create --in-place tolerates only .git directory in target directory", async () => {
93
+ await mkdir(join(tempDir, ".git"), { recursive: true });
94
+
95
+ const result = await runCreate(
96
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
97
+ makeContext(tempDir),
98
+ );
99
+ expect(result.exitCode).toBe(0);
100
+ expect(result.data?.status).toBe("pass");
101
+ expect(existsSync(join(tempDir, "forge.yaml"))).toBe(true);
102
+ expect(existsSync(join(tempDir, ".git"))).toBe(true);
103
+ }, 30000);
104
+
105
+ test("forge create fails on non-kebab-case name", async () => {
106
+ const result = await runCreate(
107
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "MyProject" } },
108
+ makeContext(tempDir),
109
+ );
110
+ expect(result.exitCode).toBe(1);
111
+ expect(result.data?.status).toBe("fail");
112
+ expect(result.data?.errors[0]).toContain("kebab-case");
113
+ });
114
+
115
+ test("forge create fails on missing --in-place flag", async () => {
116
+ const result = await runCreate(
117
+ { argv: [], flags: { profile: "forge-shell", name: "my-project" } },
118
+ makeContext(tempDir),
119
+ );
120
+ expect(result.exitCode).toBe(1);
121
+ expect(result.data?.status).toBe("fail");
122
+ expect(result.data?.errors[0]).toContain("--in-place");
123
+ });
124
+
125
+ test("forge create fails on missing --profile flag", async () => {
126
+ const result = await runCreate(
127
+ { argv: [], flags: { "in-place": true, name: "my-project" } },
128
+ makeContext(tempDir),
129
+ );
130
+ expect(result.exitCode).toBe(1);
131
+ expect(result.data?.status).toBe("fail");
132
+ expect(result.data?.errors[0]).toContain("--profile");
133
+ });
134
+
135
+ test("forge create --in-place --profile forge-shell uses forge-shell profile", async () => {
136
+ const result = await runCreate(
137
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
138
+ makeContext(tempDir),
139
+ );
140
+ expect(result.exitCode).toBe(0);
141
+ expect(result.data?.profile).toBe("forge-shell");
142
+ }, 30000);
143
+
144
+ test("forge create --package-manager npm writes npm into forge.yaml with npx bindings", async () => {
145
+ const result = await runCreate(
146
+ {
147
+ argv: [],
148
+ flags: {
149
+ "in-place": true,
150
+ profile: "forge-shell",
151
+ name: "my-project",
152
+ "package-manager": "npm",
153
+ },
154
+ },
155
+ makeContext(tempDir),
156
+ );
157
+ expect(result.exitCode).toBe(0);
158
+
159
+ const config = loadForgeConfig(tempDir);
160
+ expect(config.project.packageManager).toBe("npm");
161
+ expect(config.bindings?.commands.validateRfc).toContain("npx");
162
+ expect(config.bindings?.commands.validateRfc).toContain("forge rfc.validate");
163
+ }, 30000);
164
+
165
+ test("forge create --package-manager pnpm writes pnpm into forge.yaml", async () => {
166
+ const result = await runCreate(
167
+ {
168
+ argv: [],
169
+ flags: {
170
+ "in-place": true,
171
+ profile: "forge-shell",
172
+ name: "my-project",
173
+ "package-manager": "pnpm",
174
+ },
175
+ },
176
+ makeContext(tempDir),
177
+ );
178
+ expect(result.exitCode).toBe(0);
179
+
180
+ const config = loadForgeConfig(tempDir);
181
+ expect(config.project.packageManager).toBe("pnpm");
182
+ expect(config.bindings?.commands.validateRfc).toContain("pnpm exec");
183
+ }, 30000);
184
+
185
+ test("forge.yaml has non-null forge-CLI bindings and null stack bindings", async () => {
186
+ await runCreate(
187
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
188
+ makeContext(tempDir),
189
+ );
190
+
191
+ const config = loadForgeConfig(tempDir);
192
+ expect(config.bindings?.commands.validateRfc).not.toBeNull();
193
+ expect(config.bindings?.commands.validateAdr).not.toBeNull();
194
+ expect(config.bindings?.commands.implementStamp).not.toBeNull();
195
+ expect(config.bindings?.commands.specValidate).not.toBeNull();
196
+ expect(config.bindings?.commands.sessionSave).not.toBeNull();
197
+ expect(config.bindings?.commands.typecheck).toBeNull();
198
+ expect(config.bindings?.commands.test).toBeNull();
199
+ expect(config.bindings?.commands.scopedBuild).toBeNull();
200
+ }, 30000);
201
+
202
+ test("forge.yaml has forge.syncedVersion set", async () => {
203
+ await runCreate(
204
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
205
+ makeContext(tempDir),
206
+ );
207
+
208
+ const config = loadForgeConfig(tempDir);
209
+ expect(config.forge?.syncedVersion).toBeTruthy();
210
+ }, 30000);
211
+
212
+ test("result includes nextSteps with forge-bootstrap", async () => {
213
+ const result = await runCreate(
214
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
215
+ makeContext(tempDir),
216
+ );
217
+ expect(result.exitCode).toBe(0);
218
+ expect(result.nextSteps).toBeDefined();
219
+ expect(result.nextSteps?.some((s) => s.action.includes("forge-bootstrap"))).toBe(true);
220
+ expect(result.nextSteps?.some((s) => s.kind === "required")).toBe(true);
221
+ }, 30000);
222
+
223
+ test("forge create generates AGENTS.md with behavioral layer (RFC-0548)", async () => {
224
+ const result = await runCreate(
225
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
226
+ makeContext(tempDir),
227
+ );
228
+ expect(result.exitCode).toBe(0);
229
+
230
+ const agentsMdPath = join(tempDir, "AGENTS.md");
231
+ expect(existsSync(agentsMdPath)).toBe(true);
232
+
233
+ const { readFile: readFileAsync } = await import("node:fs/promises");
234
+ const agentsMd = await readFileAsync(agentsMdPath, "utf8");
235
+ expect(agentsMd).toContain("<!-- forge:begin behavioral-layer -->");
236
+ expect(agentsMd).toContain("<!-- forge:end behavioral-layer -->");
237
+ expect(agentsMd).toContain("### Intent-to-skill routing");
238
+ }, 30000);
239
+
240
+ test("forge create writes NEXT_STEPS.md with greenfield and transplant guidance (RFC-0550)", async () => {
241
+ const result = await runCreate(
242
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
243
+ makeContext(tempDir),
244
+ );
245
+ expect(result.exitCode).toBe(0);
246
+
247
+ const nextStepsPath = join(tempDir, "NEXT_STEPS.md");
248
+ expect(existsSync(nextStepsPath)).toBe(true);
249
+
250
+ const { readFile: readFileAsync } = await import("node:fs/promises");
251
+ const nextSteps = await readFileAsync(nextStepsPath, "utf8");
252
+ expect(nextSteps).toContain("required");
253
+ expect(nextSteps).toContain("forge-bootstrap");
254
+ expect(nextSteps).toContain("language");
255
+ expect(result.data?.filesCreated).toContain("NEXT_STEPS.md");
256
+ }, 30000);
257
+
258
+ test("forge create root package.json has scripts and replaced project name", async () => {
259
+ const result = await runCreate(
260
+ { argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
261
+ makeContext(tempDir),
262
+ );
263
+ expect(result.exitCode).toBe(0);
264
+
265
+ const { readFile: readFileAsync } = await import("node:fs/promises");
266
+ const pkgJson = JSON.parse(await readFileAsync(join(tempDir, "package.json"), "utf8"));
267
+ expect(pkgJson.name).toBe("my-project");
268
+ expect(pkgJson.scripts.clean).toBe("node scripts/clean.mjs");
269
+ expect(pkgJson.scripts.format).toBe("prettier --write .");
270
+ expect(pkgJson.scripts["format:check"]).toBe("prettier --check .");
271
+ expect(pkgJson.scripts.test).toBe("vitest run --passWithNoTests");
272
+ expect(pkgJson.scripts["upgrade-packages"]).toBe("pnpm up");
273
+ }, 30000);
274
+
275
+ test("forge create --in-place derives name from folder name when --name omitted", async () => {
276
+ const namedDir = join(tempDir, "my-derived-project");
277
+ await mkdir(namedDir, { recursive: true });
278
+
279
+ const result = await runCreate(
280
+ { argv: [], flags: { "in-place": true, profile: "forge-shell" } },
281
+ makeContext(namedDir),
282
+ );
283
+ expect(result.exitCode).toBe(0);
284
+ expect(result.data?.status).toBe("pass");
285
+
286
+ const { readFile: readFileAsync } = await import("node:fs/promises");
287
+ const pkgJson = JSON.parse(await readFileAsync(join(namedDir, "package.json"), "utf8"));
288
+ expect(pkgJson.name).toBe("my-derived-project");
289
+ }, 30000);
@@ -0,0 +1,98 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.doctor autonomy guard — verifies @warpgogol/* import detection.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0391: initial doctor autonomy guard tests.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { runDoctor } from "../onboarding/doctor.ts";
15
+ import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
16
+
17
+ const mockLogger: ForgeLogger = {
18
+ section: () => {},
19
+ info: () => {},
20
+ warn: () => {},
21
+ error: () => {},
22
+ success: () => {},
23
+ };
24
+
25
+ const mockContext = (workspaceRoot: string): ForgeRuntimeContext => ({
26
+ workspaceRoot,
27
+ logger: mockLogger,
28
+ dryRun: false,
29
+ outputFormat: "json",
30
+ });
31
+
32
+ let tempDir: string;
33
+
34
+ beforeEach(async () => {
35
+ tempDir = await mkdtemp(join(tmpdir(), "forge-doctor-test-"));
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await rm(tempDir, { recursive: true, force: true });
40
+ });
41
+
42
+ test("doctor passes on clean tree with no @warpgogol/* imports", async () => {
43
+ await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
44
+ await writeFile(
45
+ join(tempDir, "packages", "forge", "package.json"),
46
+ '{"name":"@warpgogol/forge"}',
47
+ "utf8",
48
+ );
49
+ await writeFile(
50
+ join(tempDir, "packages", "forge", "src", "index.ts"),
51
+ "export const x = 1;",
52
+ "utf8",
53
+ );
54
+
55
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
56
+ const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
57
+ expect(autonomyCheck?.status).toBe("pass");
58
+ expect(result.data?.forbiddenImports).toEqual([]);
59
+ });
60
+
61
+ test("doctor detects @warpgogol/* import specifiers", async () => {
62
+ await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
63
+ await writeFile(
64
+ join(tempDir, "packages", "forge", "package.json"),
65
+ '{"name":"@warpgogol/forge"}',
66
+ "utf8",
67
+ );
68
+ await writeFile(
69
+ join(tempDir, "packages", "forge", "src", "bad.ts"),
70
+ 'import { foo } from "@warpgogol/werkstatt/kernel";\nexport const x = foo;',
71
+ "utf8",
72
+ );
73
+
74
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
75
+ const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
76
+ expect(autonomyCheck?.status).toBe("fail");
77
+ expect(result.data?.forbiddenImports.length).toBe(1);
78
+ expect(result.data?.forbiddenImports[0]?.specifier).toBe("@warpgogol/werkstatt/kernel");
79
+ });
80
+
81
+ test("doctor ignores @warpgogol/* in comments", async () => {
82
+ await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
83
+ await writeFile(
84
+ join(tempDir, "packages", "forge", "package.json"),
85
+ '{"name":"@warpgogol/forge"}',
86
+ "utf8",
87
+ );
88
+ await writeFile(
89
+ join(tempDir, "packages", "forge", "src", "clean.ts"),
90
+ "/* Do not import from @warpgogol/werkstatt/kernel */\nexport const x = 1;",
91
+ "utf8",
92
+ );
93
+
94
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
95
+ const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
96
+ expect(autonomyCheck?.status).toBe("pass");
97
+ expect(result.data?.forbiddenImports).toEqual([]);
98
+ });
@@ -0,0 +1,286 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.doctor defaultable-binding-null notices (RFC-0540).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0540: initial doctor binding notices tests.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { runDoctor } from "../onboarding/doctor.ts";
15
+ import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
16
+
17
+ const mockLogger: ForgeLogger = {
18
+ section: () => {},
19
+ info: () => {},
20
+ warn: () => {},
21
+ error: () => {},
22
+ success: () => {},
23
+ };
24
+
25
+ const mockContext = (workspaceRoot: string): ForgeRuntimeContext => ({
26
+ workspaceRoot,
27
+ logger: mockLogger,
28
+ dryRun: false,
29
+ outputFormat: "json",
30
+ });
31
+
32
+ let tempDir: string;
33
+
34
+ beforeEach(async () => {
35
+ tempDir = await mkdtemp(join(tmpdir(), "forge-doctor-bindings-test-"));
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await rm(tempDir, { recursive: true, force: true });
40
+ });
41
+
42
+ async function writeForgeYaml(dir: string, yaml: string): Promise<void> {
43
+ await writeFile(join(dir, "forge.yaml"), yaml, "utf8");
44
+ }
45
+
46
+ async function setupMinimalProject(dir: string): Promise<void> {
47
+ await mkdir(join(dir, "packages", "forge", "src"), { recursive: true });
48
+ await writeFile(
49
+ join(dir, "packages", "forge", "package.json"),
50
+ '{"name":"@warpgogol/forge"}',
51
+ "utf8",
52
+ );
53
+ await writeFile(join(dir, "packages", "forge", "src", "index.ts"), "export const x = 1;", "utf8");
54
+ await mkdir(join(dir, ".agents", "skills"), { recursive: true });
55
+ await mkdir(join(dir, "docs", "rfcs"), { recursive: true });
56
+ await mkdir(join(dir, "docs", "adrs"), { recursive: true });
57
+ await writeFile(join(dir, "AGENTS.md"), "# Test", "utf8");
58
+ await writeFile(join(dir, "PREFERENCES.md"), "# Test", "utf8");
59
+ }
60
+
61
+ test("doctor emits defaultable-binding-null notices for all 5 forge-CLI bindings when null", async () => {
62
+ await setupMinimalProject(tempDir);
63
+ await writeForgeYaml(
64
+ tempDir,
65
+ `schema: forge/config@1
66
+ project:
67
+ name: test
68
+ stack: []
69
+ packageManager: pnpm
70
+ paths:
71
+ rfcsDir: docs/rfcs
72
+ adrsDir: docs/adrs
73
+ plansDir: docs/plans
74
+ auditsDir: docs/audits
75
+ specsDir: docs/specs
76
+ skillsDir: .agents/skills
77
+ bindings:
78
+ schema: forge/bindings@1
79
+ commands:
80
+ validateRfc: null
81
+ validateAdr: null
82
+ implementStamp: null
83
+ typecheck: null
84
+ test: null
85
+ scopedBuild: null
86
+ specValidate: null
87
+ sessionSave: null
88
+ paths:
89
+ invariantsFile: null
90
+ compassDocs: []
91
+ reviewsDir: null
92
+ handoffsDir: null
93
+ sessionsDir: null
94
+ `,
95
+ );
96
+
97
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
98
+ const notices = result.data?.bindings?.notices ?? [];
99
+ expect(notices).toHaveLength(5);
100
+ const keys = notices.map((n) => n.key);
101
+ expect(keys).toContain("commands.validateRfc");
102
+ expect(keys).toContain("commands.validateAdr");
103
+ expect(keys).toContain("commands.implementStamp");
104
+ expect(keys).toContain("commands.specValidate");
105
+ expect(keys).toContain("commands.sessionSave");
106
+ for (const notice of notices) {
107
+ expect(notice.rule).toBe("defaultable-binding-null");
108
+ expect(notice.suggestion).toContain("pnpm exec forge");
109
+ }
110
+ });
111
+
112
+ test("doctor does NOT emit notices for non-null forge-CLI bindings (operator override)", async () => {
113
+ await setupMinimalProject(tempDir);
114
+ await writeForgeYaml(
115
+ tempDir,
116
+ `schema: forge/config@1
117
+ project:
118
+ name: test
119
+ stack: []
120
+ packageManager: pnpm
121
+ paths:
122
+ rfcsDir: docs/rfcs
123
+ adrsDir: docs/adrs
124
+ plansDir: docs/plans
125
+ auditsDir: docs/audits
126
+ specsDir: docs/specs
127
+ skillsDir: .agents/skills
128
+ bindings:
129
+ schema: forge/bindings@1
130
+ commands:
131
+ validateRfc: "custom rfc validate command"
132
+ validateAdr: "custom adr validate command"
133
+ implementStamp: "custom stamp command"
134
+ typecheck: null
135
+ test: null
136
+ scopedBuild: null
137
+ specValidate: "custom spec validate command"
138
+ sessionSave: "custom session save command"
139
+ paths:
140
+ invariantsFile: null
141
+ compassDocs: []
142
+ reviewsDir: null
143
+ handoffsDir: null
144
+ sessionsDir: null
145
+ `,
146
+ );
147
+
148
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
149
+ const notices = result.data?.bindings?.notices ?? [];
150
+ expect(notices).toHaveLength(0);
151
+ });
152
+
153
+ test("doctor emits zero notices when all forge-CLI bindings are non-null", async () => {
154
+ await setupMinimalProject(tempDir);
155
+ await writeForgeYaml(
156
+ tempDir,
157
+ `schema: forge/config@1
158
+ project:
159
+ name: test
160
+ stack: []
161
+ packageManager: npm
162
+ paths:
163
+ rfcsDir: docs/rfcs
164
+ adrsDir: docs/adrs
165
+ plansDir: docs/plans
166
+ auditsDir: docs/audits
167
+ specsDir: docs/specs
168
+ skillsDir: .agents/skills
169
+ bindings:
170
+ schema: forge/bindings@1
171
+ commands:
172
+ validateRfc: "npx forge rfc.validate --id {id} --json"
173
+ validateAdr: "npx forge adr.validate --id {id} --json"
174
+ implementStamp: "npx forge rfc.implement.stamp --id {id} --implementation-commit {commit}"
175
+ typecheck: null
176
+ test: null
177
+ scopedBuild: null
178
+ specValidate: "npx forge spec.validate --spec={id} --json"
179
+ sessionSave: "npx forge session.save --json"
180
+ paths:
181
+ invariantsFile: null
182
+ compassDocs: []
183
+ reviewsDir: null
184
+ handoffsDir: null
185
+ sessionsDir: null
186
+ `,
187
+ );
188
+
189
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
190
+ const notices = result.data?.bindings?.notices ?? [];
191
+ expect(notices).toHaveLength(0);
192
+ });
193
+
194
+ test("doctor notice suggestion uses npm runner when packageManager is npm", async () => {
195
+ await setupMinimalProject(tempDir);
196
+ await writeForgeYaml(
197
+ tempDir,
198
+ `schema: forge/config@1
199
+ project:
200
+ name: test
201
+ stack: []
202
+ packageManager: npm
203
+ paths:
204
+ rfcsDir: docs/rfcs
205
+ adrsDir: docs/adrs
206
+ plansDir: docs/plans
207
+ auditsDir: docs/audits
208
+ specsDir: docs/specs
209
+ skillsDir: .agents/skills
210
+ bindings:
211
+ schema: forge/bindings@1
212
+ commands:
213
+ validateRfc: null
214
+ validateAdr: null
215
+ implementStamp: null
216
+ typecheck: null
217
+ test: null
218
+ scopedBuild: null
219
+ specValidate: null
220
+ sessionSave: null
221
+ paths:
222
+ invariantsFile: null
223
+ compassDocs: []
224
+ reviewsDir: null
225
+ handoffsDir: null
226
+ sessionsDir: null
227
+ `,
228
+ );
229
+
230
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
231
+ const notices = result.data?.bindings?.notices ?? [];
232
+ expect(notices.length).toBeGreaterThan(0);
233
+ for (const notice of notices) {
234
+ expect(notice.suggestion).toContain("npx forge");
235
+ }
236
+ });
237
+
238
+ test("doctor detects directories as existing paths (regression: pathExists used readFile which throws EISDIR on dirs)", async () => {
239
+ await setupMinimalProject(tempDir);
240
+ await mkdir(join(tempDir, "docs", "sessions"), { recursive: true });
241
+ await writeForgeYaml(
242
+ tempDir,
243
+ `schema: forge/config@1
244
+ project:
245
+ name: test
246
+ stack: []
247
+ packageManager: pnpm
248
+ paths:
249
+ rfcsDir: docs/rfcs
250
+ adrsDir: docs/adrs
251
+ plansDir: docs/plans
252
+ auditsDir: docs/audits
253
+ specsDir: docs/specs
254
+ skillsDir: .agents/skills
255
+ sessionsDir: docs/sessions
256
+ bindings:
257
+ schema: forge/bindings@1
258
+ commands:
259
+ validateRfc: "pnpm exec forge rfc.validate --id {id} --json"
260
+ validateAdr: "pnpm exec forge adr.validate --id {id} --json"
261
+ implementStamp: "pnpm exec forge rfc.implement.stamp --id {id} --implementation-commit {commit}"
262
+ typecheck: null
263
+ test: null
264
+ scopedBuild: null
265
+ specValidate: "pnpm exec forge spec.validate --spec={id} --json"
266
+ sessionSave: "pnpm exec forge session.save --json"
267
+ paths:
268
+ invariantsFile: null
269
+ compassDocs: []
270
+ reviewsDir: null
271
+ handoffsDir: null
272
+ sessionsDir: docs/sessions
273
+ `,
274
+ );
275
+
276
+ const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
277
+ const checks = result.data?.checks ?? [];
278
+ const skillsCheck = checks.find((c) => c.name === ".agents/skills/");
279
+ expect(skillsCheck?.status).toBe("pass");
280
+ const rfcsCheck = checks.find((c) => c.name === "docs/rfcs/");
281
+ expect(rfcsCheck?.status).toBe("pass");
282
+ const adrsCheck = checks.find((c) => c.name === "docs/adrs/");
283
+ expect(adrsCheck?.status).toBe("pass");
284
+ const bindingsCheck = checks.find((c) => c.name === "bindings");
285
+ expect(bindingsCheck?.status).toBe("pass");
286
+ });