@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,82 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Plan frontmatter file I/O — recursive file discovery and YAML frontmatter
5
+ parsing for plan files under docs/plans/.
6
+ </purpose>
7
+ <non-goals>
8
+ <item>Do not validate plan content — that is a future concern.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0521: initial plan frontmatter I/O.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import fs from "node:fs/promises";
17
+ import path from "node:path";
18
+ import YAML from "yaml";
19
+ import { PLAN_FILE_PATTERN } from "./types.ts";
20
+
21
+ export interface ParsedPlan {
22
+ frontmatter: Record<string, unknown>;
23
+ body: string;
24
+ }
25
+
26
+ export function parsePlanFile(source: string): ParsedPlan {
27
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
28
+ if (!match) {
29
+ return { frontmatter: {}, body: source };
30
+ }
31
+ return {
32
+ frontmatter: (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>,
33
+ body: match[2] ?? "",
34
+ };
35
+ }
36
+
37
+ export async function listPlanFiles(planDirPath: string): Promise<string[]> {
38
+ const results: string[] = [];
39
+
40
+ async function scanDir(dirPath: string, relativePrefix: string): Promise<void> {
41
+ try {
42
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
45
+ if (entry.isDirectory()) {
46
+ await scanDir(path.join(dirPath, entry.name), relativePath);
47
+ } else if (
48
+ entry.isFile() &&
49
+ entry.name.endsWith(".md") &&
50
+ !entry.name.startsWith("plan-0000") &&
51
+ entry.name !== "README.md" &&
52
+ PLAN_FILE_PATTERN.test(entry.name)
53
+ ) {
54
+ results.push(relativePath);
55
+ }
56
+ }
57
+ } catch {
58
+ // Directory doesn't exist or is unreadable — return empty
59
+ }
60
+ }
61
+
62
+ await scanDir(planDirPath, "");
63
+ return results.sort();
64
+ }
65
+
66
+ export async function readAndParsePlan(
67
+ planDirPath: string,
68
+ fileName: string,
69
+ ): Promise<{ fileName: string; parsed: ParsedPlan } | undefined> {
70
+ try {
71
+ const filePath = path.join(planDirPath, fileName);
72
+ const content = await fs.readFile(filePath, "utf-8");
73
+ return { fileName, parsed: parsePlanFile(content) };
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ export function extractRfcIdFromPlanFile(fileName: string): string | undefined {
80
+ const match = path.basename(fileName).match(/^plan-(rfc-\d{4})-/i);
81
+ return match ? match[1]!.toUpperCase() : undefined;
82
+ }
@@ -0,0 +1,238 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Plan archive handler — moves plan files whose parent RFC has terminal status
5
+ into status-specific subdirectories under docs/plans/archive/ and moves
6
+ non-terminal files found in subdirectories back to the root.
7
+ </purpose>
8
+ <non-goals>
9
+ <item>Does not validate plan content.</item>
10
+ <item>Does not change plan frontmatter — only moves files on disk.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0521: implement plan.archive command.</item>
15
+ <item>RFC-0733: add pinned-files pre-check — skip pinned files with warning instead of moving them.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import fs from "node:fs/promises";
20
+ import path from "node:path";
21
+ import { listPlanFiles, extractRfcIdFromPlanFile } from "../frontmatter-io.ts";
22
+ import { loadRfcStatusMap } from "../../rfc/frontmatter-io.ts";
23
+ import type { ArchiveMove, ArchiveSkip } from "../../rfc/handlers/archive.ts";
24
+ import type {
25
+ ForgeCommandInput,
26
+ ForgeCommandResult,
27
+ ForgeRuntimeContext,
28
+ } from "../../../src/types.ts";
29
+ import { PLAN_DIR } from "../types.ts";
30
+ import type { PlanArchiveResult } from "../types.ts";
31
+ import { loadPinnedManifest, isPinned, isIntraDirMove } from "../../core/handlers/pinned-check.ts";
32
+
33
+ const TERMINAL_STATUSES = ["implemented", "rejected", "superseded"] as const;
34
+
35
+ export async function runPlanArchive(
36
+ input: ForgeCommandInput,
37
+ context: ForgeRuntimeContext,
38
+ ): Promise<ForgeCommandResult<PlanArchiveResult>> {
39
+ const { workspaceRoot, logger, outputFormat } = context;
40
+ const planDirPath = path.join(workspaceRoot, PLAN_DIR);
41
+ const rfcDirPath = path.join(workspaceRoot, "docs", "rfcs");
42
+
43
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
44
+ const statusFilter = input.flags["status"] as string | undefined;
45
+
46
+ if (statusFilter && !TERMINAL_STATUSES.includes(statusFilter as never)) {
47
+ throw new Error(
48
+ `Invalid --status "${statusFilter}". Must be one of: ${TERMINAL_STATUSES.join(", ")}`,
49
+ );
50
+ }
51
+
52
+ const files = await listPlanFiles(planDirPath);
53
+ const rfcStatusMap = await loadRfcStatusMap(rfcDirPath);
54
+ const moved: ArchiveMove[] = [];
55
+ const skipped: ArchiveSkip[] = [];
56
+
57
+ // RFC-0733: Load pinned manifest once per invocation
58
+ let pinnedManifest = null;
59
+ try {
60
+ pinnedManifest = await loadPinnedManifest(workspaceRoot);
61
+ } catch {
62
+ // Malformed manifest — skip pre-check
63
+ }
64
+
65
+ for (const fileName of files) {
66
+ const rfcId = extractRfcIdFromPlanFile(fileName);
67
+ const relFile = path.join(PLAN_DIR, fileName);
68
+ const basename = path.basename(fileName);
69
+
70
+ if (!rfcId) {
71
+ skipped.push({ id: "UNKNOWN", file: relFile, reason: "no RFC id in filename" });
72
+ continue;
73
+ }
74
+
75
+ const rfcStatus = rfcStatusMap.get(rfcId);
76
+ if (!rfcStatus) {
77
+ skipped.push({ id: rfcId, file: relFile, reason: "parent RFC not found" });
78
+ continue;
79
+ }
80
+
81
+ const isTerminal =
82
+ rfcStatus === "implemented" || rfcStatus === "rejected" || rfcStatus === "superseded";
83
+ const isInArchive = fileName.includes("/");
84
+
85
+ if (statusFilter && rfcStatus !== statusFilter) {
86
+ skipped.push({
87
+ id: rfcId,
88
+ file: relFile,
89
+ reason: `parent RFC status ${rfcStatus} does not match --status ${statusFilter}`,
90
+ });
91
+ continue;
92
+ }
93
+
94
+ if (isTerminal && !isInArchive) {
95
+ const targetDir = path.join(planDirPath, "archive", rfcStatus);
96
+ const targetPath = path.join(targetDir, basename);
97
+ const targetRel = path.join(PLAN_DIR, "archive", rfcStatus, basename);
98
+
99
+ // RFC-0733: Check if file is pinned before moving
100
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
101
+ if (
102
+ pinnedManifest &&
103
+ isPinned(pinnedManifest, relFile) &&
104
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
105
+ ) {
106
+ skipped.push({
107
+ id: rfcId,
108
+ file: relFile,
109
+ reason: "pinned (protected by .forge/pinned.yaml)",
110
+ });
111
+ if (outputFormat === "pretty") {
112
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ try {
118
+ await fs.access(targetPath);
119
+ skipped.push({ id: rfcId, file: relFile, reason: "destination exists" });
120
+ continue;
121
+ } catch {
122
+ // destination doesn't exist — proceed
123
+ }
124
+
125
+ if (!dryRun) {
126
+ await fs.mkdir(targetDir, { recursive: true });
127
+ try {
128
+ await fs.rename(path.join(planDirPath, fileName), targetPath);
129
+ } catch (err) {
130
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
131
+ skipped.push({ id: rfcId, file: relFile, reason: "already moved by another process" });
132
+ continue;
133
+ }
134
+ throw err;
135
+ }
136
+ }
137
+
138
+ moved.push({
139
+ id: rfcId,
140
+ file: targetRel,
141
+ status: rfcStatus,
142
+ from: relFile,
143
+ to: targetRel,
144
+ direction: "into-archive",
145
+ });
146
+ } else if (!isTerminal && isInArchive) {
147
+ const targetPath = path.join(planDirPath, basename);
148
+ const targetRel = path.join(PLAN_DIR, basename);
149
+
150
+ // RFC-0733: Check if file is pinned before moving
151
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
152
+ if (
153
+ pinnedManifest &&
154
+ isPinned(pinnedManifest, relFile) &&
155
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
156
+ ) {
157
+ skipped.push({
158
+ id: rfcId,
159
+ file: relFile,
160
+ reason: "pinned (protected by .forge/pinned.yaml)",
161
+ });
162
+ if (outputFormat === "pretty") {
163
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
164
+ }
165
+ continue;
166
+ }
167
+
168
+ try {
169
+ await fs.access(targetPath);
170
+ skipped.push({ id: rfcId, file: relFile, reason: "destination exists" });
171
+ continue;
172
+ } catch {
173
+ // destination doesn't exist — proceed
174
+ }
175
+
176
+ if (!dryRun) {
177
+ try {
178
+ await fs.rename(path.join(planDirPath, fileName), targetPath);
179
+ } catch (err) {
180
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
181
+ skipped.push({ id: rfcId, file: relFile, reason: "already moved by another process" });
182
+ continue;
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ moved.push({
189
+ id: rfcId,
190
+ file: targetRel,
191
+ status: rfcStatus,
192
+ from: relFile,
193
+ to: targetRel,
194
+ direction: "out-of-archive",
195
+ });
196
+ } else if (isTerminal && isInArchive) {
197
+ skipped.push({ id: rfcId, file: relFile, reason: `already archived (${rfcStatus})` });
198
+ } else {
199
+ skipped.push({
200
+ id: rfcId,
201
+ file: relFile,
202
+ reason: `parent RFC status ${rfcStatus} is non-terminal`,
203
+ });
204
+ }
205
+ }
206
+
207
+ if (outputFormat === "pretty") {
208
+ if (dryRun) {
209
+ logger.info(
210
+ `[dry-run] plan.archive: would move ${moved.length} file(s), skip ${skipped.length}`,
211
+ );
212
+ } else {
213
+ logger.success(`plan.archive: moved ${moved.length} file(s), skipped ${skipped.length}`);
214
+ }
215
+ for (const m of moved) {
216
+ logger.info(` ${m.direction}: ${m.id} (${m.status}) ${m.from} → ${m.to}`);
217
+ }
218
+ }
219
+
220
+ return {
221
+ data: {
222
+ command: "plan.archive",
223
+ status: "ok",
224
+ moved,
225
+ skipped,
226
+ dryRun,
227
+ },
228
+ summary: dryRun
229
+ ? `[dry-run] Would move ${moved.length} file(s), skip ${skipped.length}`
230
+ : `Moved ${moved.length} file(s), skipped ${skipped.length}`,
231
+ nextSteps: [
232
+ {
233
+ action: "Run: pnpm exec werkstatt run plan.list --json to verify archive status",
234
+ kind: "optional",
235
+ },
236
+ ],
237
+ };
238
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Barrel export for the plan domain — types, module, and handlers.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement plan logic here; delegate to handlers/ and plan.module.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0521: initial plan module barrel.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ export { forgePlanModule } from "./plan.module.ts";
14
+ export { runPlanArchive } from "./handlers/archive.ts";
15
+ export {
16
+ listPlanFiles,
17
+ parsePlanFile,
18
+ readAndParsePlan,
19
+ extractRfcIdFromPlanFile,
20
+ type ParsedPlan,
21
+ } from "./frontmatter-io.ts";
22
+ export type { PlanArchiveResult, PlanArchiveMove, PlanArchiveSkip } from "./types.ts";
23
+ export { PLAN_DIR, PLAN_FILE_PATTERN } from "./types.ts";
@@ -0,0 +1,48 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register the plan 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-0521: initial forgePlanModule registering plan.archive command.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+
15
+ export const forgePlanModule: ForgeModule = {
16
+ name: "forge-plan",
17
+ version: "0.1.0",
18
+ async register(registry) {
19
+ const { runPlanArchive } = await import("./handlers/archive.ts");
20
+
21
+ registry.registerCommand({
22
+ name: "plan.archive",
23
+ description:
24
+ "Move plan files whose parent RFC has terminal status " +
25
+ "(implemented, rejected, superseded) into docs/plans/archive/<status>/ " +
26
+ "subdirectories. Bidirectional: moves non-terminal files found in " +
27
+ "subdirectories back to root. Use --dry-run to preview. " +
28
+ "Use --status to filter to a single terminal status. " +
29
+ "Prefer the docs.archive umbrella command unless you need to archive only plans.",
30
+ scope: "workspace",
31
+ mutatesState: true,
32
+ writes: ["docs/plans/*.md", "docs/plans/archive/**"],
33
+ reads: ["docs/plans/**/*.md", "docs/rfcs/**/*.md"],
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 (implemented, rejected, superseded).",
43
+ },
44
+ },
45
+ execute: runPlanArchive,
46
+ });
47
+ },
48
+ };
@@ -0,0 +1,29 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Type definitions for the plan archive domain — plan file discovery, parsing,
5
+ 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-0521: initial plan archive types.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import type { ArchiveMove, ArchiveSkip } from "../rfc/handlers/archive.ts";
17
+
18
+ export const PLAN_DIR = "docs/plans";
19
+ export const PLAN_FILE_PATTERN = /^plan-rfc-\d{4}-/;
20
+
21
+ export interface PlanArchiveResult {
22
+ command: "plan.archive";
23
+ status: "ok";
24
+ moved: ArchiveMove[];
25
+ skipped: ArchiveSkip[];
26
+ dryRun: boolean;
27
+ }
28
+
29
+ export type { ArchiveMove as PlanArchiveMove, ArchiveSkip as PlanArchiveSkip };