@warpgogol/forge 2.7.0 → 2.8.1

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 (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -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 +51 -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 +217 -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/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,158 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register the Architectural Decision Record (ADR) command domain with the forge kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement command logic here; implementations live in handlers/*.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0366: register adr.create, adr.validate, and adr.list commands.</item>
10
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/ as forgeAdrModule.</item>
11
+ <item>RFC-0727: register adr.implement.stamp command for atomic ADR status transition.</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import type { ForgeModule } from "../../src/forge-module.ts";
16
+
17
+ export const forgeAdrModule: ForgeModule = {
18
+ name: "forge-adr",
19
+ version: "0.1.0",
20
+
21
+ async register(registry) {
22
+ const { runAdrList, runAdrCreate } = await import("./handlers/list-create.ts");
23
+ const { runAdrValidate } = await import("./handlers/validate.ts");
24
+ const { runAdrArchive } = await import("./handlers/archive.ts");
25
+ const { runAdrImplementStamp } = await import("./handlers/implement-stamp.ts");
26
+
27
+ registry.registerCommand({
28
+ name: "adr.list",
29
+ description:
30
+ "List all ADRs. Filter with --status, --scope, --decider flags. " +
31
+ "Use --json for machine-readable output. " +
32
+ "Parses frontmatter on the fly — no index file needed.",
33
+ scope: "workspace",
34
+ flags: {
35
+ status: {
36
+ kind: "string",
37
+ description: "Filter by ADR status (e.g. proposed, accepted, superseded).",
38
+ },
39
+ scope: {
40
+ kind: "string",
41
+ description: "Filter by ADR scope (package, app, workspace).",
42
+ },
43
+ decider: { kind: "string", description: "Filter by decider string." },
44
+ },
45
+ reads: ["docs/adrs/**/*.md"],
46
+ execute: runAdrList,
47
+ });
48
+
49
+ registry.registerCommand({
50
+ name: "adr.create",
51
+ description:
52
+ "Create a new ADR draft from the template. " +
53
+ 'Pass --title "Short title" (required). ' +
54
+ "Optional: --scope, --decider, --status, --related. " +
55
+ "Always creates status: proposed unless overridden. " +
56
+ "AI agents are allowed to use this command.",
57
+ scope: "workspace",
58
+ mutatesState: true,
59
+ writes: ["docs/adrs/adr-*.md"],
60
+ reads: ["docs/adrs/**/*.md"],
61
+ cacheable: false,
62
+ flags: {
63
+ title: { kind: "string", required: true, description: "Short imperative ADR title." },
64
+ scope: {
65
+ kind: "string",
66
+ default: "package",
67
+ description: "ADR scope: package | app | workspace.",
68
+ },
69
+ decider: {
70
+ kind: "string",
71
+ default: "architecture",
72
+ description: "ADR decider, e.g. architecture or human:<handle>.",
73
+ },
74
+ status: {
75
+ kind: "string",
76
+ default: "proposed",
77
+ description: "ADR status: proposed | accepted | superseded | rejected.",
78
+ },
79
+ related: {
80
+ kind: "string",
81
+ description: "Comma-separated list of related RFC/ADR ids (e.g. RFC-0365,RFC-0001).",
82
+ },
83
+ },
84
+ execute: runAdrCreate,
85
+ });
86
+
87
+ registry.registerCommand({
88
+ name: "adr.validate",
89
+ description:
90
+ "Validate ADR frontmatter schema, required markdown sections, " +
91
+ "referential integrity (supersedes/supersededBy), and id/filename consistency. " +
92
+ "Pass --id to validate a single file, or run without arguments for all.",
93
+ scope: "workspace",
94
+ flags: {
95
+ id: { kind: "string", description: "Target a single ADR by id (e.g. ADR-0003)." },
96
+ },
97
+ reads: ["docs/adrs/**/*.md"],
98
+ execute: runAdrValidate,
99
+ });
100
+
101
+ registry.registerCommand({
102
+ name: "adr.archive",
103
+ description:
104
+ "Move terminal-status ADR files (implemented, rejected, superseded) into " +
105
+ "docs/adrs/archive/<status>/ subdirectories. Bidirectional: moves non-terminal " +
106
+ "files found in subdirectories back to root. Use --dry-run to preview. " +
107
+ "Use --status to filter to a single terminal status. " +
108
+ "Prefer the docs.archive umbrella command unless you need to archive only ADRs.",
109
+ scope: "workspace",
110
+ mutatesState: true,
111
+ writes: ["docs/adrs/*.md", "docs/adrs/archive/**"],
112
+ reads: ["docs/adrs/**/*.md"],
113
+ cacheable: false,
114
+ flags: {
115
+ "dry-run": {
116
+ kind: "boolean",
117
+ description: "Preview what would be moved without touching the filesystem.",
118
+ },
119
+ status: {
120
+ kind: "string",
121
+ description: "Filter to a single terminal status (implemented, rejected, superseded).",
122
+ },
123
+ },
124
+ execute: runAdrArchive,
125
+ });
126
+
127
+ registry.registerCommand({
128
+ name: "adr.implement.stamp",
129
+ description:
130
+ "Atomically transition an ADR from accepted/proposed to implemented. " +
131
+ "Validates preconditions (status, implementation commit, file cleanliness, " +
132
+ "concurrent safety) and mutates frontmatter in one atomic write. " +
133
+ "Use --dry-run to preview without mutating.",
134
+ scope: "workspace",
135
+ mutatesState: true,
136
+ writes: ["docs/adrs/*.md"],
137
+ reads: ["docs/adrs/**/*.md"],
138
+ cacheable: false,
139
+ flags: {
140
+ id: {
141
+ kind: "string",
142
+ required: true,
143
+ description: "Target ADR id (e.g. ADR-0003).",
144
+ },
145
+ "implementation-commit": {
146
+ kind: "string",
147
+ required: true,
148
+ description: "SHA of the implementation commit.",
149
+ },
150
+ "dry-run": {
151
+ kind: "boolean",
152
+ description: "Preview without mutating the ADR file.",
153
+ },
154
+ },
155
+ execute: runAdrImplementStamp,
156
+ });
157
+ },
158
+ };
@@ -0,0 +1,80 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Shared ADR frontmatter file I/O — list ADR files and parse their YAML frontmatter
5
+ without validating the shape.
6
+ </purpose>
7
+ <non-goals>
8
+ <item>Do not validate frontmatter shape — that is the validate handler.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0366: mirror the RFC frontmatter-io contract for ADRs.</item>
13
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import fs from "node:fs/promises";
18
+ import path from "node:path";
19
+ import YAML from "yaml";
20
+
21
+ export interface ParsedAdr {
22
+ frontmatter: Record<string, unknown>;
23
+ body: string;
24
+ }
25
+
26
+ export function parseAdrFile(source: string): ParsedAdr {
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 listAdrFiles(adrDirPath: 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("adr-0000") &&
51
+ entry.name !== "README.md"
52
+ ) {
53
+ results.push(relativePath);
54
+ }
55
+ }
56
+ } catch {
57
+ // Directory doesn't exist or is unreadable — return empty
58
+ }
59
+ }
60
+
61
+ await scanDir(adrDirPath, "");
62
+ return results.sort();
63
+ }
64
+
65
+ export function adrFileMatchesId(fileName: string, targetId: string): boolean {
66
+ return path.basename(fileName).toLowerCase().startsWith(targetId.toLowerCase());
67
+ }
68
+
69
+ export async function readAndParseAdr(
70
+ adrDirPath: string,
71
+ fileName: string,
72
+ ): Promise<{ fileName: string; parsed: ParsedAdr } | undefined> {
73
+ try {
74
+ const filePath = path.join(adrDirPath, fileName);
75
+ const content = await fs.readFile(filePath, "utf-8");
76
+ return { fileName, parsed: parseAdrFile(content) };
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
@@ -0,0 +1,244 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ ADR archive handler — moves terminal-status ADR files into status-specific
5
+ subdirectories under docs/adrs/archive/ and moves non-terminal files found
6
+ in subdirectories back to the root.
7
+ </purpose>
8
+ <non-goals>
9
+ <item>Does not validate ADR content — use adr.validate for that.</item>
10
+ <item>Does not change ADR frontmatter — only moves files on disk.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0367: implement adr.archive command for terminal-status file archiving.</item>
15
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
16
+ <item>RFC-0733: add pinned-files pre-check — skip pinned files with warning instead of moving them.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import fs from "node:fs/promises";
21
+ import path from "node:path";
22
+ import { listAdrFiles, readAndParseAdr } from "../frontmatter-io.ts";
23
+ import type {
24
+ ForgeCommandInput,
25
+ ForgeCommandResult,
26
+ ForgeRuntimeContext,
27
+ } from "../../../src/types.ts";
28
+ import { ADR_DIR } from "../types.ts";
29
+ import { loadPinnedManifest, isPinned, isIntraDirMove } from "../../core/handlers/pinned-check.ts";
30
+
31
+ export const ADR_TERMINAL_STATUSES = ["implemented", "rejected", "superseded"] as const;
32
+
33
+ export interface ArchiveMove {
34
+ id: string;
35
+ file: string;
36
+ status: string;
37
+ from: string;
38
+ to: string;
39
+ direction: "into-archive" | "out-of-archive";
40
+ }
41
+
42
+ export interface ArchiveSkip {
43
+ id: string;
44
+ file: string;
45
+ reason: string;
46
+ }
47
+
48
+ export interface AdrArchiveResult {
49
+ command: "adr.archive";
50
+ status: "ok";
51
+ moved: ArchiveMove[];
52
+ skipped: ArchiveSkip[];
53
+ dryRun: boolean;
54
+ }
55
+
56
+ export async function runAdrArchive(
57
+ input: ForgeCommandInput,
58
+ context: ForgeRuntimeContext,
59
+ ): Promise<ForgeCommandResult<AdrArchiveResult>> {
60
+ const { workspaceRoot, logger, outputFormat } = context;
61
+ const adrDirPath = path.join(workspaceRoot, ADR_DIR);
62
+
63
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
64
+ const statusFilter = input.flags["status"] as string | undefined;
65
+
66
+ if (statusFilter && !ADR_TERMINAL_STATUSES.includes(statusFilter as never)) {
67
+ throw new Error(
68
+ `Invalid --status "${statusFilter}". Must be one of: ${ADR_TERMINAL_STATUSES.join(", ")}`,
69
+ );
70
+ }
71
+
72
+ const files = await listAdrFiles(adrDirPath);
73
+ const moved: ArchiveMove[] = [];
74
+ const skipped: ArchiveSkip[] = [];
75
+
76
+ // RFC-0733: Load pinned manifest once per invocation
77
+ let pinnedManifest = null;
78
+ try {
79
+ pinnedManifest = await loadPinnedManifest(workspaceRoot);
80
+ } catch {
81
+ // Malformed manifest — skip pre-check
82
+ }
83
+
84
+ for (const fileName of files) {
85
+ const result = await readAndParseAdr(adrDirPath, fileName);
86
+ if (!result) {
87
+ skipped.push({
88
+ id: "UNKNOWN",
89
+ file: path.join(ADR_DIR, fileName),
90
+ reason: "unreadable frontmatter",
91
+ });
92
+ continue;
93
+ }
94
+
95
+ const fm = result.parsed.frontmatter;
96
+ const id = String(fm["id"] ?? "UNKNOWN");
97
+ const status = String(fm["status"] ?? "").trim();
98
+ const isTerminal = status === "implemented" || status === "rejected" || status === "superseded";
99
+ const isInArchive = fileName.includes("/");
100
+ const relFile = path.join(ADR_DIR, fileName);
101
+ const basename = path.basename(fileName);
102
+
103
+ if (statusFilter && status !== statusFilter) {
104
+ skipped.push({
105
+ id,
106
+ file: relFile,
107
+ reason: `status ${status} does not match --status ${statusFilter}`,
108
+ });
109
+ continue;
110
+ }
111
+
112
+ if (isTerminal && !isInArchive) {
113
+ const targetDir = path.join(adrDirPath, "archive", status);
114
+ const targetPath = path.join(targetDir, basename);
115
+ const targetRel = path.join(ADR_DIR, "archive", status, basename);
116
+
117
+ // RFC-0733: Check if file is pinned before moving
118
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
119
+ if (
120
+ pinnedManifest &&
121
+ isPinned(pinnedManifest, relFile) &&
122
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
123
+ ) {
124
+ skipped.push({ id, file: relFile, reason: "pinned (protected by .forge/pinned.yaml)" });
125
+ if (outputFormat === "pretty") {
126
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
127
+ }
128
+ continue;
129
+ }
130
+
131
+ try {
132
+ await fs.access(targetPath);
133
+ skipped.push({ id, file: relFile, reason: "destination exists" });
134
+ continue;
135
+ } catch {
136
+ // destination doesn't exist — proceed
137
+ }
138
+
139
+ if (!dryRun) {
140
+ await fs.mkdir(targetDir, { recursive: true });
141
+ try {
142
+ await fs.rename(path.join(adrDirPath, fileName), targetPath);
143
+ } catch (err) {
144
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
145
+ skipped.push({ id, file: relFile, reason: "already moved by another process" });
146
+ continue;
147
+ }
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ moved.push({
153
+ id,
154
+ file: targetRel,
155
+ status,
156
+ from: relFile,
157
+ to: targetRel,
158
+ direction: "into-archive",
159
+ });
160
+ } else if (!isTerminal && isInArchive) {
161
+ const targetPath = path.join(adrDirPath, basename);
162
+ const targetRel = path.join(ADR_DIR, basename);
163
+
164
+ // RFC-0733: Check if file is pinned before moving
165
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
166
+ if (
167
+ pinnedManifest &&
168
+ isPinned(pinnedManifest, relFile) &&
169
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
170
+ ) {
171
+ skipped.push({ id, file: relFile, reason: "pinned (protected by .forge/pinned.yaml)" });
172
+ if (outputFormat === "pretty") {
173
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
174
+ }
175
+ continue;
176
+ }
177
+
178
+ try {
179
+ await fs.access(targetPath);
180
+ skipped.push({ id, file: relFile, reason: "destination exists" });
181
+ continue;
182
+ } catch {
183
+ // destination doesn't exist — proceed
184
+ }
185
+
186
+ if (!dryRun) {
187
+ try {
188
+ await fs.rename(path.join(adrDirPath, fileName), targetPath);
189
+ } catch (err) {
190
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
191
+ skipped.push({ id, file: relFile, reason: "already moved by another process" });
192
+ continue;
193
+ }
194
+ throw err;
195
+ }
196
+ }
197
+
198
+ moved.push({
199
+ id,
200
+ file: targetRel,
201
+ status,
202
+ from: relFile,
203
+ to: targetRel,
204
+ direction: "out-of-archive",
205
+ });
206
+ } else if (isTerminal && isInArchive) {
207
+ skipped.push({ id, file: relFile, reason: `already archived (${status})` });
208
+ } else {
209
+ skipped.push({ id, file: relFile, reason: `status ${status} is non-terminal` });
210
+ }
211
+ }
212
+
213
+ if (outputFormat === "pretty") {
214
+ if (dryRun) {
215
+ logger.info(
216
+ `[dry-run] adr.archive: would move ${moved.length} file(s), skip ${skipped.length}`,
217
+ );
218
+ } else {
219
+ logger.success(`adr.archive: moved ${moved.length} file(s), skipped ${skipped.length}`);
220
+ }
221
+ for (const m of moved) {
222
+ logger.info(` ${m.direction}: ${m.id} (${m.status}) ${m.from} → ${m.to}`);
223
+ }
224
+ }
225
+
226
+ return {
227
+ data: {
228
+ command: "adr.archive",
229
+ status: "ok",
230
+ moved,
231
+ skipped,
232
+ dryRun,
233
+ },
234
+ summary: dryRun
235
+ ? `[dry-run] Would move ${moved.length} file(s), skip ${skipped.length}`
236
+ : `Moved ${moved.length} file(s), skipped ${skipped.length}`,
237
+ nextSteps: [
238
+ {
239
+ action: "Run: pnpm exec werkstatt run adr.list --json to verify archive status",
240
+ kind: "optional",
241
+ },
242
+ ],
243
+ };
244
+ }