@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,453 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Mission archive handler — moves terminal-state mission directories (state: closed
5
+ or state: aborted in mission.yaml) into status-specific subdirectories under
6
+ missions/archive/<state>/ and moves non-terminal (open) directories found in
7
+ archive subdirectories back to missions/.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Does not validate mission manifest schema — reads only the state field.</item>
11
+ <item>Does not change mission.yaml — only moves directories on disk.</item>
12
+ <item>Does not import from @warpgogol/* packages — uses node:fs, node:child_process, and yaml only.</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0573: implement mission.archive command.</item>
17
+ <item>RFC-0573: extract moveMissionDir helper to eliminate Phase 1/2 duplication.</item>
18
+ <item>RFC-0801: add service-folder cleanup (node_modules, dist, .astro, .wrangler, .cache, .turbo) before archive move.</item>
19
+ <item>RFC-0733: add pinned-files pre-check — skip pinned mission directories with warning instead of moving them.</item>
20
+ <item>RFC-0804: auto-refresh pnpm-lock.yaml after directory moves.</item>
21
+ </CHANGE_SUMMARY>
22
+ */
23
+
24
+ import fs from "node:fs/promises";
25
+ import { existsSync } from "node:fs";
26
+ import path from "node:path";
27
+ import { execSync } from "node:child_process";
28
+ import { parse as parseYaml } from "yaml";
29
+ import type {
30
+ ForgeCommandInput,
31
+ ForgeCommandResult,
32
+ ForgeRuntimeContext,
33
+ } from "../../../src/types.ts";
34
+ import { trashPath } from "../../../src/utils/fs-trash.ts";
35
+ import {
36
+ MISSIONS_DIR,
37
+ ARCHIVE_DIR_NAME,
38
+ MISSION_TERMINAL_STATUSES,
39
+ type MissionArchiveMove,
40
+ type MissionArchiveSkip,
41
+ type MissionArchiveResult,
42
+ } from "../types.ts";
43
+ import { loadPinnedManifest, isPinned, isIntraDirMove } from "../../core/handlers/pinned-check.ts";
44
+
45
+ const SERVICE_FOLDERS = [
46
+ "node_modules",
47
+ "dist",
48
+ ".astro",
49
+ ".wrangler",
50
+ ".cache",
51
+ ".turbo",
52
+ ] as const;
53
+
54
+ async function cleanServiceFolders(workpieceDir: string): Promise<string[]> {
55
+ const removed: string[] = [];
56
+ for (const folder of SERVICE_FOLDERS) {
57
+ const target = path.join(workpieceDir, folder);
58
+ if (existsSync(target)) {
59
+ await fs.rm(target, { recursive: true, force: true });
60
+ removed.push(folder);
61
+ }
62
+ }
63
+ return removed;
64
+ }
65
+
66
+ async function readMissionState(missionDir: string): Promise<string | null> {
67
+ const manifestPath = path.join(missionDir, "mission.yaml");
68
+ try {
69
+ const raw = await fs.readFile(manifestPath, "utf8");
70
+ const parsed = parseYaml(raw) as Record<string, unknown>;
71
+ const state = parsed?.state;
72
+ if (typeof state === "string") return state.trim();
73
+ return null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ interface MoveAttempt {
80
+ moved: MissionArchiveMove | null;
81
+ skip: MissionArchiveSkip | null;
82
+ }
83
+
84
+ async function moveMissionDir(
85
+ sourcePath: string,
86
+ targetPath: string,
87
+ targetParentDir: string,
88
+ missionId: string,
89
+ state: string,
90
+ sourceRel: string,
91
+ targetRel: string,
92
+ direction: "into-archive" | "out-of-archive",
93
+ dryRun: boolean,
94
+ logger?: { info: (msg: string) => void; warn: (msg: string) => void },
95
+ ): Promise<MoveAttempt> {
96
+ if (existsSync(targetPath)) {
97
+ return { moved: null, skip: { missionId, dir: sourceRel, reason: "destination exists" } };
98
+ }
99
+
100
+ if (!dryRun) {
101
+ if (targetParentDir) {
102
+ await fs.mkdir(targetParentDir, { recursive: true });
103
+ }
104
+
105
+ // RFC-0801: Clean service folders from workpiece before move.
106
+ // node_modules/, dist/, .astro/, .wrangler/, .cache/, .turbo/ contain
107
+ // broken symlinks and stale build artifacts that waste disk space in archive.
108
+ // Aborted missions may not have a workpiece/ directory — skip gracefully.
109
+ const workpieceDir = path.join(sourcePath, "workpiece");
110
+ if (existsSync(workpieceDir)) {
111
+ try {
112
+ const removed = await cleanServiceFolders(workpieceDir);
113
+ if (removed.length > 0 && logger) {
114
+ logger.info(` cleaned service folders: ${removed.join(", ")}`);
115
+ }
116
+ } catch (cleanupErr) {
117
+ // Non-fatal: log warning and continue with move
118
+ if (logger) {
119
+ logger.warn(
120
+ ` Warning: service folder cleanup failed for ${missionId}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
121
+ );
122
+ }
123
+ }
124
+ }
125
+
126
+ try {
127
+ await fs.rename(sourcePath, targetPath);
128
+ } catch (err) {
129
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
130
+ return {
131
+ moved: null,
132
+ skip: { missionId, dir: sourceRel, reason: "already moved by another process" },
133
+ };
134
+ }
135
+ throw err;
136
+ }
137
+
138
+ // Post-rename cleanup: a watcher or IDE may recreate stale cache
139
+ // directories (e.g. .astro/) at the source path after the rename.
140
+ // Remove the resurrected source so it doesn't leave orphan directories.
141
+ if (existsSync(sourcePath)) {
142
+ await trashPath(sourcePath);
143
+ }
144
+ }
145
+
146
+ return {
147
+ moved: { missionId, state, from: sourceRel, to: targetRel, direction },
148
+ skip: null,
149
+ };
150
+ }
151
+
152
+ export async function runMissionArchive(
153
+ input: ForgeCommandInput,
154
+ context: ForgeRuntimeContext,
155
+ ): Promise<ForgeCommandResult<MissionArchiveResult>> {
156
+ const { workspaceRoot, logger, outputFormat } = context;
157
+ const missionsPath = path.join(workspaceRoot, MISSIONS_DIR);
158
+
159
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
160
+ const statusFilter = input.flags["status"] as string | undefined;
161
+
162
+ if (statusFilter && !MISSION_TERMINAL_STATUSES.includes(statusFilter as never)) {
163
+ throw new Error(
164
+ `Invalid --status "${statusFilter}". Must be one of: ${MISSION_TERMINAL_STATUSES.join(", ")}`,
165
+ );
166
+ }
167
+
168
+ const moved: MissionArchiveMove[] = [];
169
+ const skipped: MissionArchiveSkip[] = [];
170
+
171
+ // RFC-0733: Load pinned manifest once per invocation
172
+ let pinnedManifest = null;
173
+ try {
174
+ pinnedManifest = await loadPinnedManifest(workspaceRoot);
175
+ } catch {
176
+ // Malformed manifest — skip pre-check
177
+ }
178
+
179
+ if (!existsSync(missionsPath)) {
180
+ if (outputFormat === "pretty") {
181
+ if (dryRun) {
182
+ logger.info("[dry-run] mission.archive: no missions/ directory — nothing to do");
183
+ } else {
184
+ logger.info("mission.archive: no missions/ directory — nothing to do");
185
+ }
186
+ }
187
+ return {
188
+ data: { command: "mission.archive", status: "ok", moved, skipped, dryRun },
189
+ summary: dryRun
190
+ ? "[dry-run] No missions/ directory — nothing to do"
191
+ : "No missions/ directory — nothing to do",
192
+ };
193
+ }
194
+
195
+ // Phase 1: Scan missions/ for terminal-state missions to move into archive
196
+ const rootEntries = await fs.readdir(missionsPath, { withFileTypes: true });
197
+
198
+ // Stale symlink cleanup: IDEs or file watchers may recreate symlinks to
199
+ // archived mission directories after mission.archive has moved them.
200
+ // These symlinks break pnpm workspace resolution and mission number derivation.
201
+ // Trash them before they can cause downstream issues.
202
+ for (const e of rootEntries) {
203
+ if (e.isSymbolicLink() && e.name !== ARCHIVE_DIR_NAME) {
204
+ const symlinkPath = path.join(missionsPath, e.name);
205
+ const sourceRel = `${MISSIONS_DIR}/${e.name}`;
206
+ if (!dryRun) {
207
+ await trashPath(symlinkPath);
208
+ }
209
+ skipped.push({
210
+ missionId: e.name,
211
+ dir: sourceRel,
212
+ reason: "stale symlink — trashed",
213
+ });
214
+ if (outputFormat === "pretty") {
215
+ logger.warn(` stale symlink: trashed ${sourceRel}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ const rootDirs = rootEntries
221
+ .filter((e) => e.isDirectory() && !e.isSymbolicLink() && e.name !== ARCHIVE_DIR_NAME)
222
+ .map((e) => e.name);
223
+
224
+ for (const missionId of rootDirs) {
225
+ const missionDir = path.join(missionsPath, missionId);
226
+ const state = await readMissionState(missionDir);
227
+
228
+ if (state === null) {
229
+ skipped.push({
230
+ missionId,
231
+ dir: `${MISSIONS_DIR}/${missionId}`,
232
+ reason: "unreadable manifest",
233
+ });
234
+ continue;
235
+ }
236
+
237
+ const isTerminal = MISSION_TERMINAL_STATUSES.includes(state as never);
238
+
239
+ if (!isTerminal) {
240
+ skipped.push({
241
+ missionId,
242
+ dir: `${MISSIONS_DIR}/${missionId}`,
243
+ reason: `state ${state} is non-terminal`,
244
+ });
245
+ continue;
246
+ }
247
+
248
+ if (statusFilter && state !== statusFilter) {
249
+ skipped.push({
250
+ missionId,
251
+ dir: `${MISSIONS_DIR}/${missionId}`,
252
+ reason: `state ${state} does not match --status ${statusFilter}`,
253
+ });
254
+ continue;
255
+ }
256
+
257
+ const targetDir = path.join(missionsPath, ARCHIVE_DIR_NAME, state);
258
+ const targetPath = path.join(targetDir, missionId);
259
+ const targetRel = `${MISSIONS_DIR}/${ARCHIVE_DIR_NAME}/${state}/${missionId}`;
260
+ const sourceRel = `${MISSIONS_DIR}/${missionId}`;
261
+
262
+ // RFC-0733: Check if mission directory is pinned before moving
263
+ // Gap fix: exempt intra-directory moves (dir stays within the same pinned parent)
264
+ if (
265
+ pinnedManifest &&
266
+ isPinned(pinnedManifest, sourceRel) &&
267
+ !isIntraDirMove(pinnedManifest, sourceRel, targetRel)
268
+ ) {
269
+ skipped.push({
270
+ missionId,
271
+ dir: sourceRel,
272
+ reason: "pinned (protected by .forge/pinned.yaml)",
273
+ });
274
+ if (outputFormat === "pretty") {
275
+ logger.warn(` pinned: skipping ${sourceRel} (protected)`);
276
+ }
277
+ continue;
278
+ }
279
+
280
+ const result = await moveMissionDir(
281
+ missionDir,
282
+ targetPath,
283
+ targetDir,
284
+ missionId,
285
+ state,
286
+ sourceRel,
287
+ targetRel,
288
+ "into-archive",
289
+ dryRun,
290
+ logger,
291
+ );
292
+ if (result.skip) {
293
+ skipped.push(result.skip);
294
+ continue;
295
+ }
296
+ if (result.moved) moved.push(result.moved);
297
+ }
298
+
299
+ // Phase 2: Scan missions/archive/<state>/ for non-terminal missions to move back
300
+ const archivePath = path.join(missionsPath, ARCHIVE_DIR_NAME);
301
+ if (existsSync(archivePath)) {
302
+ const stateDirs = await fs.readdir(archivePath, { withFileTypes: true });
303
+ const stateDirNames = stateDirs.filter((e) => e.isDirectory()).map((e) => e.name);
304
+
305
+ for (const stateDirName of stateDirNames) {
306
+ const stateDirPath = path.join(archivePath, stateDirName);
307
+ const archivedMissions = await fs.readdir(stateDirPath, { withFileTypes: true });
308
+ const archivedDirNames = archivedMissions.filter((e) => e.isDirectory()).map((e) => e.name);
309
+
310
+ for (const missionId of archivedDirNames) {
311
+ const archivedMissionDir = path.join(stateDirPath, missionId);
312
+ const state = await readMissionState(archivedMissionDir);
313
+
314
+ if (state === null) {
315
+ skipped.push({
316
+ missionId,
317
+ dir: `${MISSIONS_DIR}/${ARCHIVE_DIR_NAME}/${stateDirName}/${missionId}`,
318
+ reason: "unreadable manifest",
319
+ });
320
+ continue;
321
+ }
322
+
323
+ const isTerminal = MISSION_TERMINAL_STATUSES.includes(state as never);
324
+
325
+ if (isTerminal) {
326
+ skipped.push({
327
+ missionId,
328
+ dir: `${MISSIONS_DIR}/${ARCHIVE_DIR_NAME}/${stateDirName}/${missionId}`,
329
+ reason: `already archived (${state})`,
330
+ });
331
+ continue;
332
+ }
333
+
334
+ // Non-terminal in archive — move back to missions/
335
+ const targetPath = path.join(missionsPath, missionId);
336
+ const targetRel = `${MISSIONS_DIR}/${missionId}`;
337
+ const sourceRel = `${MISSIONS_DIR}/${ARCHIVE_DIR_NAME}/${stateDirName}/${missionId}`;
338
+
339
+ // RFC-0733: Check if mission directory is pinned before moving
340
+ // Gap fix: exempt intra-directory moves (dir stays within the same pinned parent)
341
+ if (
342
+ pinnedManifest &&
343
+ isPinned(pinnedManifest, sourceRel) &&
344
+ !isIntraDirMove(pinnedManifest, sourceRel, targetRel)
345
+ ) {
346
+ skipped.push({
347
+ missionId,
348
+ dir: sourceRel,
349
+ reason: "pinned (protected by .forge/pinned.yaml)",
350
+ });
351
+ if (outputFormat === "pretty") {
352
+ logger.warn(` pinned: skipping ${sourceRel} (protected)`);
353
+ }
354
+ continue;
355
+ }
356
+
357
+ const result = await moveMissionDir(
358
+ archivedMissionDir,
359
+ targetPath,
360
+ "",
361
+ missionId,
362
+ state,
363
+ sourceRel,
364
+ targetRel,
365
+ "out-of-archive",
366
+ dryRun,
367
+ logger,
368
+ );
369
+ if (result.skip) {
370
+ skipped.push(result.skip);
371
+ continue;
372
+ }
373
+ if (result.moved) moved.push(result.moved);
374
+ }
375
+ }
376
+ }
377
+
378
+ if (outputFormat === "pretty") {
379
+ if (dryRun) {
380
+ logger.info(
381
+ `[dry-run] mission.archive: would move ${moved.length} mission(s), skip ${skipped.length}`,
382
+ );
383
+ } else {
384
+ logger.success(
385
+ `mission.archive: moved ${moved.length} mission(s), skipped ${skipped.length}`,
386
+ );
387
+ }
388
+ for (const m of moved) {
389
+ logger.info(` ${m.direction}: ${m.missionId} (${m.state}) ${m.from} → ${m.to}`);
390
+ }
391
+ }
392
+
393
+ // RFC-0804: Post-move lockfile refresh
394
+ if (!dryRun && moved.length > 0) {
395
+ try {
396
+ execSync("pnpm install", {
397
+ cwd: workspaceRoot,
398
+ stdio: ["pipe", "pipe", "pipe"],
399
+ timeout: 120_000,
400
+ });
401
+ const lockfileStatus = execSync("git status --porcelain pnpm-lock.yaml", {
402
+ cwd: workspaceRoot,
403
+ encoding: "utf-8",
404
+ stdio: ["pipe", "pipe", "pipe"],
405
+ }).trim();
406
+ if (lockfileStatus) {
407
+ execSync("git add pnpm-lock.yaml", {
408
+ cwd: workspaceRoot,
409
+ stdio: ["pipe", "pipe", "pipe"],
410
+ });
411
+ for (const m of moved) {
412
+ execSync(`git add -A ${JSON.stringify(m.to)}`, {
413
+ cwd: workspaceRoot,
414
+ stdio: ["pipe", "pipe", "pipe"],
415
+ });
416
+ if (existsSync(m.from)) continue;
417
+ execSync(`git rm -r --cached --ignore-unmatch ${JSON.stringify(m.from)}`, {
418
+ cwd: workspaceRoot,
419
+ stdio: ["pipe", "pipe", "pipe"],
420
+ });
421
+ }
422
+ execSync(
423
+ `git commit -m ${JSON.stringify(`chore: refresh pnpm-lock.yaml after mission.archive (${moved.length} moved)`)}`,
424
+ { cwd: workspaceRoot, stdio: ["pipe", "pipe", "pipe"] },
425
+ );
426
+ logger.info(` Committed pnpm-lock.yaml refresh (${moved.length} missions archived)`);
427
+ }
428
+ } catch (refreshErr) {
429
+ logger.warn(
430
+ ` Lockfile refresh failed after archive — lockfile may be stale. Run 'pnpm install' manually: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`,
431
+ );
432
+ }
433
+ }
434
+
435
+ return {
436
+ data: {
437
+ command: "mission.archive",
438
+ status: "ok",
439
+ moved,
440
+ skipped,
441
+ dryRun,
442
+ },
443
+ summary: dryRun
444
+ ? `[dry-run] Would move ${moved.length} mission(s), skip ${skipped.length}`
445
+ : `Moved ${moved.length} mission(s), skipped ${skipped.length}`,
446
+ nextSteps: [
447
+ {
448
+ action: "Run: pnpm exec werkstatt run mission.list --json to verify active missions",
449
+ kind: "optional",
450
+ },
451
+ ],
452
+ };
453
+ }
@@ -0,0 +1 @@
1
+ export { forgeMissionModule } from "./mission.module.ts";
@@ -0,0 +1,48 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register the mission archive command with the forge kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement handler logic here — delegate to handlers/archive.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0573: initial forgeMissionModule registering mission.archive command.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+
15
+ export const forgeMissionModule: ForgeModule = {
16
+ name: "forge-mission",
17
+ version: "0.1.0",
18
+ async register(registry) {
19
+ const { runMissionArchive } = await import("./handlers/archive.ts");
20
+
21
+ registry.registerCommand({
22
+ name: "mission.archive",
23
+ description:
24
+ "Move terminal-state mission directories (state: closed or aborted in " +
25
+ "mission.yaml) into missions/archive/<state>/<missionId>/ subdirectories. " +
26
+ "Bidirectional: moves open missions found in archive subdirectories back " +
27
+ "to missions/. Use --dry-run to preview. Use --status to filter to a " +
28
+ "single terminal status (closed, aborted). " +
29
+ "Prefer the docs.archive umbrella command unless you need to archive only missions.",
30
+ scope: "workspace",
31
+ mutatesState: true,
32
+ writes: ["missions/*", "missions/archive/**"],
33
+ reads: ["missions/**"],
34
+ cacheable: false,
35
+ flags: {
36
+ "dry-run": {
37
+ kind: "boolean",
38
+ description: "Preview what would be moved without touching the filesystem.",
39
+ },
40
+ status: {
41
+ kind: "string",
42
+ description: "Filter to a single terminal status (closed, aborted).",
43
+ },
44
+ },
45
+ execute: runMissionArchive,
46
+ });
47
+ },
48
+ };
@@ -0,0 +1,41 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Type definitions for the mission archive domain — mission directory discovery,
5
+ manifest state extraction, and archive result shapes.
6
+ </purpose>
7
+ <non-goals>
8
+ <item>Do not define archive handler logic here — that lives in handlers/archive.ts.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0573: initial mission archive types.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ export const MISSIONS_DIR = "missions";
17
+ export const ARCHIVE_DIR_NAME = "archive";
18
+
19
+ export const MISSION_TERMINAL_STATUSES = ["closed", "aborted"] as const;
20
+
21
+ export interface MissionArchiveMove {
22
+ missionId: string;
23
+ state: string;
24
+ from: string;
25
+ to: string;
26
+ direction: "into-archive" | "out-of-archive";
27
+ }
28
+
29
+ export interface MissionArchiveSkip {
30
+ missionId: string;
31
+ dir: string;
32
+ reason: string;
33
+ }
34
+
35
+ export interface MissionArchiveResult {
36
+ command: "mission.archive";
37
+ status: "ok";
38
+ moved: MissionArchiveMove[];
39
+ skipped: MissionArchiveSkip[];
40
+ dryRun: boolean;
41
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Expose forge naming lint command handlers through a stable package subpath.</purpose>
4
+ <non-goals>
5
+ <item>Do not register project-specific naming commands.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0374: initial naming module barrel.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ export { forgeNamingModule } from "./naming.module.ts";
14
+ export { runNamingConventionLint } from "./naming-convention.ts";