@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,391 @@
1
+ import { test, expect, describe, beforeEach, afterEach, vi } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { runMissionArchive } from "./archive.ts";
7
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../src/types.ts";
8
+
9
+ vi.mock("../../../src/utils/fs-trash.ts", () => ({
10
+ trashPath: (targetPath: string) => fs.rm(targetPath, { recursive: true, force: true }),
11
+ }));
12
+
13
+ const execSyncMock = vi.fn<(cmd: string, opts?: Record<string, unknown>) => string | Buffer>();
14
+
15
+ vi.mock("node:child_process", () => ({
16
+ execSync: (cmd: string, opts?: Record<string, unknown>) => execSyncMock(cmd, opts),
17
+ }));
18
+
19
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
20
+ return {
21
+ workspaceRoot,
22
+ logger: {
23
+ info: () => {},
24
+ success: () => {},
25
+ warn: () => {},
26
+ error: () => {},
27
+ debug: () => {},
28
+ },
29
+ outputFormat: "json",
30
+ dryRun: false,
31
+ } as unknown as ForgeRuntimeContext;
32
+ }
33
+
34
+ function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
35
+ return {
36
+ commandName: "mission.archive",
37
+ flags,
38
+ } as unknown as ForgeCommandInput;
39
+ }
40
+
41
+ function unwrap<T>(result: { data?: T }): T {
42
+ if (!result.data) throw new Error("result.data is undefined");
43
+ return result.data;
44
+ }
45
+
46
+ async function writeMissionManifest(
47
+ missionsDir: string,
48
+ missionId: string,
49
+ state: string,
50
+ ): Promise<void> {
51
+ const missionDir = path.join(missionsDir, missionId);
52
+ await fs.mkdir(missionDir, { recursive: true });
53
+ await fs.writeFile(
54
+ path.join(missionDir, "mission.yaml"),
55
+ `missionId: ${missionId}\nstate: ${state}\n`,
56
+ );
57
+ }
58
+
59
+ let tmpDir: string;
60
+ let missionsDir: string;
61
+
62
+ beforeEach(async () => {
63
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mission-archive-test-"));
64
+ missionsDir = path.join(tmpDir, "missions");
65
+ await fs.mkdir(missionsDir, { recursive: true });
66
+ });
67
+
68
+ afterEach(async () => {
69
+ await fs.rm(tmpDir, { recursive: true, force: true });
70
+ });
71
+
72
+ describe("mission.archive", () => {
73
+ test("terminal mission (closed) → moved to archive/closed/", async () => {
74
+ await writeMissionManifest(missionsDir, "test-m001", "closed");
75
+
76
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
77
+
78
+ expect(data.moved).toHaveLength(1);
79
+ expect(data.moved[0].missionId).toBe("test-m001");
80
+ expect(data.moved[0].state).toBe("closed");
81
+ expect(data.moved[0].direction).toBe("into-archive");
82
+ expect(existsSync(path.join(missionsDir, "test-m001"))).toBe(false);
83
+ expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m001"))).toBe(true);
84
+ });
85
+
86
+ test("terminal mission (aborted) → moved to archive/aborted/", async () => {
87
+ await writeMissionManifest(missionsDir, "test-m002", "aborted");
88
+
89
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
90
+
91
+ expect(data.moved).toHaveLength(1);
92
+ expect(data.moved[0].missionId).toBe("test-m002");
93
+ expect(data.moved[0].state).toBe("aborted");
94
+ expect(existsSync(path.join(missionsDir, "archive", "aborted", "test-m002"))).toBe(true);
95
+ });
96
+
97
+ test("open mission → skipped (non-terminal)", async () => {
98
+ await writeMissionManifest(missionsDir, "test-m003", "open");
99
+
100
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
101
+
102
+ expect(data.moved).toHaveLength(0);
103
+ expect(data.skipped).toHaveLength(1);
104
+ expect(data.skipped[0].reason).toContain("non-terminal");
105
+ expect(existsSync(path.join(missionsDir, "test-m003"))).toBe(true);
106
+ });
107
+
108
+ test("--status closed filter → only closed missions moved", async () => {
109
+ await writeMissionManifest(missionsDir, "test-m004", "closed");
110
+ await writeMissionManifest(missionsDir, "test-m005", "aborted");
111
+
112
+ const data = unwrap(
113
+ await runMissionArchive(makeInput({ status: "closed" }), makeContext(tmpDir)),
114
+ );
115
+
116
+ expect(data.moved).toHaveLength(1);
117
+ expect(data.moved[0].missionId).toBe("test-m004");
118
+ expect(existsSync(path.join(missionsDir, "test-m005"))).toBe(true);
119
+ });
120
+
121
+ test("--dry-run → reports moves without touching filesystem", async () => {
122
+ await writeMissionManifest(missionsDir, "test-m006", "closed");
123
+
124
+ const data = unwrap(
125
+ await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
126
+ );
127
+
128
+ expect(data.moved).toHaveLength(1);
129
+ expect(data.dryRun).toBe(true);
130
+ expect(existsSync(path.join(missionsDir, "test-m006"))).toBe(true);
131
+ expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m006"))).toBe(false);
132
+ });
133
+
134
+ test("destination exists → skipped with 'destination exists' reason", async () => {
135
+ await writeMissionManifest(missionsDir, "test-m007", "closed");
136
+ const archiveDir = path.join(missionsDir, "archive", "closed", "test-m007");
137
+ await fs.mkdir(archiveDir, { recursive: true });
138
+ await fs.writeFile(
139
+ path.join(archiveDir, "mission.yaml"),
140
+ "missionId: test-m007\nstate: closed\n",
141
+ );
142
+
143
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
144
+
145
+ expect(data.moved).toHaveLength(0);
146
+ expect(data.skipped).toHaveLength(2);
147
+ expect(data.skipped.some((s) => s.reason === "destination exists")).toBe(true);
148
+ });
149
+
150
+ test("unreadable manifest → skipped with 'unreadable manifest' reason", async () => {
151
+ const missionDir = path.join(missionsDir, "test-m008");
152
+ await fs.mkdir(missionDir, { recursive: true });
153
+ // No mission.yaml — unreadable
154
+
155
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
156
+
157
+ expect(data.moved).toHaveLength(0);
158
+ expect(data.skipped).toHaveLength(1);
159
+ expect(data.skipped[0].reason).toBe("unreadable manifest");
160
+ });
161
+
162
+ test("open mission in archive/ → moved back to missions/ (bidirectional)", async () => {
163
+ await writeMissionManifest(missionsDir, "test-m009", "open");
164
+ const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m010");
165
+ await fs.mkdir(archiveMissionDir, { recursive: true });
166
+ await fs.writeFile(
167
+ path.join(archiveMissionDir, "mission.yaml"),
168
+ "missionId: test-m010\nstate: open\n",
169
+ );
170
+
171
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
172
+
173
+ const outOfArchive = data.moved.filter((m) => m.direction === "out-of-archive");
174
+ expect(outOfArchive).toHaveLength(1);
175
+ expect(outOfArchive[0].missionId).toBe("test-m010");
176
+ expect(existsSync(path.join(missionsDir, "test-m010"))).toBe(true);
177
+ expect(existsSync(archiveMissionDir)).toBe(false);
178
+ });
179
+
180
+ test("resurrected source path (e.g. .astro/ cache) → cleaned up after rename", async () => {
181
+ await writeMissionManifest(missionsDir, "test-m011", "closed");
182
+ const missionDir = path.join(missionsDir, "test-m011");
183
+
184
+ // Simulate a watcher recreating a stale cache dir after rename by
185
+ // monkey-patching fs.rename to re-create .astro/ at the source.
186
+ const originalRename = fs.rename;
187
+ const astroCacheDir = path.join(missionDir, "workpiece", ".astro");
188
+ vi.spyOn(fs, "rename").mockImplementation(async (src, dest) => {
189
+ await originalRename(src, dest);
190
+ // Simulate IDE/watcher recreating stale cache at old path
191
+ await fs.mkdir(path.join(missionDir, "workpiece", ".astro"), { recursive: true });
192
+ await fs.writeFile(path.join(astroCacheDir, "content.d.ts"), "// stale cache\n");
193
+ });
194
+
195
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
196
+
197
+ vi.mocked(fs.rename).mockRestore();
198
+
199
+ expect(data.moved).toHaveLength(1);
200
+ expect(data.moved[0].missionId).toBe("test-m011");
201
+ expect(existsSync(missionDir)).toBe(false);
202
+ expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m011"))).toBe(true);
203
+ });
204
+
205
+ test("invalid --status value → throws error", async () => {
206
+ await expect(
207
+ runMissionArchive(makeInput({ status: "invalid" }), makeContext(tmpDir)),
208
+ ).rejects.toThrow('Invalid --status "invalid"');
209
+ });
210
+
211
+ test("no missions/ directory → empty result", async () => {
212
+ const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), "mission-archive-empty-"));
213
+ try {
214
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(emptyDir)));
215
+ expect(data.moved).toHaveLength(0);
216
+ expect(data.skipped).toHaveLength(0);
217
+ } finally {
218
+ await fs.rm(emptyDir, { recursive: true, force: true });
219
+ }
220
+ });
221
+
222
+ test("RFC-0796: stale symlink in missions/ root → trashed", async () => {
223
+ // Create an archived mission
224
+ const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m012");
225
+ await fs.mkdir(archiveMissionDir, { recursive: true });
226
+ await fs.writeFile(
227
+ path.join(archiveMissionDir, "mission.yaml"),
228
+ "missionId: test-m012\nstate: closed\n",
229
+ );
230
+
231
+ // Create a stale symlink pointing to the archived mission
232
+ const symlinkPath = path.join(missionsDir, "test-m012");
233
+ await fs.symlink(archiveMissionDir, symlinkPath);
234
+
235
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
236
+
237
+ // Symlink should be trashed
238
+ expect(existsSync(symlinkPath)).toBe(false);
239
+ // Archived mission should still exist
240
+ expect(existsSync(archiveMissionDir)).toBe(true);
241
+ // Should be reported as skipped with stale symlink reason
242
+ expect(data.skipped.some((s) => s.reason === "stale symlink — trashed")).toBe(true);
243
+ });
244
+
245
+ test("RFC-0796: stale symlink in --dry-run → not trashed, reported", async () => {
246
+ const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m013");
247
+ await fs.mkdir(archiveMissionDir, { recursive: true });
248
+ await fs.writeFile(
249
+ path.join(archiveMissionDir, "mission.yaml"),
250
+ "missionId: test-m013\nstate: closed\n",
251
+ );
252
+
253
+ const symlinkPath = path.join(missionsDir, "test-m013");
254
+ await fs.symlink(archiveMissionDir, symlinkPath);
255
+
256
+ const data = unwrap(
257
+ await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
258
+ );
259
+
260
+ // In dry-run, symlink should NOT be trashed
261
+ expect(existsSync(symlinkPath)).toBe(true);
262
+ // But should be reported as skipped
263
+ expect(data.skipped.some((s) => s.reason === "stale symlink — trashed")).toBe(true);
264
+ });
265
+
266
+ test("RFC-0801: service folders deleted before move", async () => {
267
+ await writeMissionManifest(missionsDir, "test-m014", "closed");
268
+ const missionDir = path.join(missionsDir, "test-m014");
269
+ const workpieceDir = path.join(missionDir, "workpiece");
270
+ await fs.mkdir(workpieceDir, { recursive: true });
271
+
272
+ // Create service folders with dummy content
273
+ for (const folder of ["node_modules", "dist", ".astro", ".wrangler", ".cache", ".turbo"]) {
274
+ const folderPath = path.join(workpieceDir, folder);
275
+ await fs.mkdir(folderPath, { recursive: true });
276
+ await fs.writeFile(path.join(folderPath, "dummy.txt"), `content of ${folder}\n`);
277
+ }
278
+
279
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
280
+
281
+ expect(data.moved).toHaveLength(1);
282
+ expect(data.moved[0].missionId).toBe("test-m014");
283
+
284
+ const archivedWorkpiece = path.join(missionsDir, "archive", "closed", "test-m014", "workpiece");
285
+ for (const folder of ["node_modules", "dist", ".astro", ".wrangler", ".cache", ".turbo"]) {
286
+ expect(existsSync(path.join(archivedWorkpiece, folder))).toBe(false);
287
+ }
288
+ });
289
+
290
+ test("RFC-0801: mission without workpiece — cleanup skipped gracefully", async () => {
291
+ await writeMissionManifest(missionsDir, "test-m015", "closed");
292
+ // No workpiece/ directory created — simulates aborted mission
293
+
294
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
295
+
296
+ expect(data.moved).toHaveLength(1);
297
+ expect(data.moved[0].missionId).toBe("test-m015");
298
+ expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m015"))).toBe(true);
299
+ });
300
+
301
+ test("RFC-0801: --dry-run does not delete service folders", async () => {
302
+ await writeMissionManifest(missionsDir, "test-m016", "closed");
303
+ const missionDir = path.join(missionsDir, "test-m016");
304
+ const workpieceDir = path.join(missionDir, "workpiece");
305
+ await fs.mkdir(workpieceDir, { recursive: true });
306
+ await fs.mkdir(path.join(workpieceDir, "node_modules"), { recursive: true });
307
+ await fs.writeFile(path.join(workpieceDir, "node_modules", "pkg.txt"), "content\n");
308
+
309
+ const data = unwrap(
310
+ await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
311
+ );
312
+
313
+ expect(data.dryRun).toBe(true);
314
+ // Service folders should still exist at source in dry-run
315
+ expect(existsSync(path.join(workpieceDir, "node_modules"))).toBe(true);
316
+ expect(existsSync(path.join(workpieceDir, "node_modules", "pkg.txt"))).toBe(true);
317
+ });
318
+
319
+ test("RFC-0804: pnpm install called after moves", async () => {
320
+ execSyncMock.mockReset();
321
+ execSyncMock.mockReturnValue("");
322
+ await writeMissionManifest(missionsDir, "test-m017", "closed");
323
+
324
+ await runMissionArchive(makeInput(), makeContext(tmpDir));
325
+
326
+ const installCall = execSyncMock.mock.calls.find(
327
+ (c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
328
+ );
329
+ expect(installCall).toBeDefined();
330
+ expect(installCall![1]).toMatchObject({ cwd: tmpDir });
331
+ });
332
+
333
+ test("RFC-0804: lockfile committed when dirty after install", async () => {
334
+ execSyncMock.mockReset();
335
+ // pnpm install succeeds, git status shows dirty lockfile, git add/commit succeed
336
+ execSyncMock.mockImplementation((cmd: string) => {
337
+ if (cmd.startsWith("pnpm install")) return "";
338
+ if (cmd.startsWith("git status --porcelain pnpm-lock.yaml")) return "M pnpm-lock.yaml";
339
+ return "";
340
+ });
341
+ await writeMissionManifest(missionsDir, "test-m018", "closed");
342
+
343
+ await runMissionArchive(makeInput(), makeContext(tmpDir));
344
+
345
+ const commitCall = execSyncMock.mock.calls.find(
346
+ (c) => typeof c[0] === "string" && c[0].startsWith("git commit"),
347
+ );
348
+ expect(commitCall).toBeDefined();
349
+ expect(commitCall![0]).toContain("refresh pnpm-lock.yaml after mission.archive");
350
+ });
351
+
352
+ test("RFC-0804: pnpm install failure is non-fatal", async () => {
353
+ execSyncMock.mockReset();
354
+ execSyncMock.mockImplementation((cmd: string) => {
355
+ if (cmd.startsWith("pnpm install")) throw new Error("ECONNREFUSED");
356
+ return "";
357
+ });
358
+ await writeMissionManifest(missionsDir, "test-m019", "closed");
359
+
360
+ const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
361
+
362
+ expect(data.moved).toHaveLength(1);
363
+ expect(data.moved[0].missionId).toBe("test-m019");
364
+ expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m019"))).toBe(true);
365
+ });
366
+
367
+ test("RFC-0804: dry-run skips lockfile refresh", async () => {
368
+ execSyncMock.mockReset();
369
+ execSyncMock.mockReturnValue("");
370
+ await writeMissionManifest(missionsDir, "test-m020", "closed");
371
+
372
+ await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir));
373
+
374
+ const installCall = execSyncMock.mock.calls.find(
375
+ (c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
376
+ );
377
+ expect(installCall).toBeUndefined();
378
+ });
379
+
380
+ test("RFC-0804: no moves skips lockfile refresh", async () => {
381
+ execSyncMock.mockReset();
382
+ execSyncMock.mockReturnValue("");
383
+
384
+ await runMissionArchive(makeInput(), makeContext(tmpDir));
385
+
386
+ const installCall = execSyncMock.mock.calls.find(
387
+ (c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
388
+ );
389
+ expect(installCall).toBeUndefined();
390
+ });
391
+ });