@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,296 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/os/site-kernel/src/rfc/handlers/list-create.ts as an authored site-kernel authored module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0303: extracted list and create handlers from handlers.ts into handlers/list-create.ts.</item>
10
+ <item>Post-refactor hardening: require explicit satisfies DNA traces for new architecture/contract RFC drafts.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { toKebabCase } from "../../../src/utils/string-utils.ts";
17
+ import { listRfcFiles, readAndParseRfc } from "../frontmatter-io.ts";
18
+ import type {
19
+ ForgeCommandInput,
20
+ ForgeCommandResult,
21
+ ForgeRuntimeContext,
22
+ } from "../../../src/types.ts";
23
+ import type {
24
+ RfcListEntry,
25
+ RfcListResult,
26
+ RfcCreateResult,
27
+ RfcNextIdResult,
28
+ RfcStatus,
29
+ RfcKind,
30
+ RfcScope,
31
+ } from "../types.ts";
32
+ import { RFC_DIR, RFC_KINDS, RFC_SCOPES, RFC_METADATA_CUTOFF } from "../types.ts";
33
+ import { DNA_DOCS, loadInvariantIds, toIsoDate, resolveRfcTemplate } from "./shared.ts";
34
+ import { collectDecisionLog, scoreDecisions } from "../decision-log.ts";
35
+ import type { ConsultedDecision } from "../types.ts";
36
+
37
+ function parseSatisfiesFlag(value: unknown): string[] {
38
+ if (typeof value !== "string") return [];
39
+ return value
40
+ .split(",")
41
+ .map((entry) => entry.trim())
42
+ .filter(Boolean);
43
+ }
44
+
45
+ async function assertSatisfiesIdsExist(workspaceRoot: string, satisfies: string[]): Promise<void> {
46
+ if (satisfies.length === 0) return;
47
+
48
+ const validDnaIds = await loadInvariantIds(workspaceRoot, DNA_DOCS, "DNA");
49
+ const errors: string[] = [];
50
+
51
+ for (const id of satisfies) {
52
+ if (!/^DNA-\d+$/.test(id)) {
53
+ errors.push(`Malformed satisfies entry: "${id}". Must match DNA-N.`);
54
+ continue;
55
+ }
56
+ if (!validDnaIds.has(parseInt(id.slice(4), 10))) {
57
+ errors.push(`Unknown DNA invariant: ${id} (not found in ${DNA_DOCS.join(", ")}).`);
58
+ }
59
+ }
60
+
61
+ if (errors.length > 0) {
62
+ throw new Error(errors.join(" "));
63
+ }
64
+ }
65
+
66
+ export async function runRfcList(
67
+ input: ForgeCommandInput,
68
+ context: ForgeRuntimeContext,
69
+ ): Promise<ForgeCommandResult<RfcListResult>> {
70
+ const { workspaceRoot, logger, outputFormat } = context;
71
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
72
+
73
+ const filterStatus = input.flags["status"] as string | undefined;
74
+ const filterKind = input.flags["kind"] as string | undefined;
75
+ const filterOwner = input.flags["owner"] as string | undefined;
76
+ const filterBatch = input.flags["batch"] as string | undefined;
77
+
78
+ const files = await listRfcFiles(rfcDirPath);
79
+ const entries: RfcListEntry[] = [];
80
+
81
+ for (const fileName of files) {
82
+ const result = await readAndParseRfc(rfcDirPath, fileName);
83
+ if (!result || "error" in result) continue;
84
+
85
+ const fm = result.parsed.frontmatter;
86
+ const id = String(fm["id"] ?? "");
87
+ const status = String(fm["status"] ?? "") as RfcStatus;
88
+ const kind = String(fm["kind"] ?? "") as RfcKind;
89
+ const scope = String(fm["scope"] ?? "") as RfcScope;
90
+ const owners = Array.isArray(fm["owners"]) ? (fm["owners"] as string[]) : [];
91
+
92
+ if (filterStatus && status !== filterStatus) continue;
93
+ if (filterKind && kind !== filterKind) continue;
94
+ if (filterOwner && !owners.includes(filterOwner)) continue;
95
+ if (filterBatch && String(fm["batch"] ?? "") !== filterBatch) continue;
96
+
97
+ entries.push({
98
+ id,
99
+ title: String(fm["title"] ?? ""),
100
+ status,
101
+ kind,
102
+ scope,
103
+ owners,
104
+ createdAt: String(fm["createdAt"] ?? ""),
105
+ updatedAt: String(fm["updatedAt"] ?? ""),
106
+ file: path.join(RFC_DIR, fileName),
107
+ batch: fm["batch"] ? String(fm["batch"]) : undefined,
108
+ dependsOn: Array.isArray(fm["dependsOn"]) ? (fm["dependsOn"] as string[]) : undefined,
109
+ });
110
+ }
111
+
112
+ if (outputFormat === "pretty") {
113
+ if (entries.length === 0) {
114
+ logger.info("No RFCs found matching the given filters.");
115
+ } else {
116
+ logger.section(`RFCs (${entries.length})`);
117
+ for (const entry of entries) {
118
+ const ownerStr = entry.owners.join(", ");
119
+ logger.info(
120
+ `${entry.id} ${entry.status.padEnd(12)} ${entry.kind.padEnd(14)} ${entry.title} [${ownerStr}]`,
121
+ );
122
+ }
123
+ }
124
+ }
125
+
126
+ return {
127
+ data: {
128
+ command: "rfc.list",
129
+ status: "ok",
130
+ count: entries.length,
131
+ entries,
132
+ },
133
+ summary: `${entries.length} RFC(s) found`,
134
+ };
135
+ }
136
+
137
+ export async function runRfcCreate(
138
+ input: ForgeCommandInput,
139
+ context: ForgeRuntimeContext,
140
+ ): Promise<ForgeCommandResult<RfcCreateResult>> {
141
+ const { workspaceRoot, logger, outputFormat } = context;
142
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
143
+
144
+ const title = input.flags["title"] as string | undefined;
145
+ if (!title) {
146
+ throw new Error(
147
+ 'rfc.create requires --title flag, e.g. --title "Add structure.validate command"',
148
+ );
149
+ }
150
+
151
+ const kind = (input.flags["kind"] as string | undefined) ?? "architecture";
152
+ const scope = (input.flags["scope"] as string | undefined) ?? "workspace";
153
+ const satisfies = parseSatisfiesFlag(input.flags["satisfies"]);
154
+
155
+ if (!RFC_KINDS.includes(kind as RfcKind)) {
156
+ throw new Error(`Invalid kind: ${kind}. Must be one of: ${RFC_KINDS.join(", ")}`);
157
+ }
158
+ if (!RFC_SCOPES.includes(scope as RfcScope)) {
159
+ throw new Error(`Invalid scope: ${scope}. Must be one of: ${RFC_SCOPES.join(", ")}`);
160
+ }
161
+
162
+ const today = toIsoDate(new Date());
163
+ const requiresSatisfies =
164
+ today >= RFC_METADATA_CUTOFF && (kind === "architecture" || kind === "contract");
165
+
166
+ if (requiresSatisfies && satisfies.length === 0) {
167
+ throw new Error(
168
+ `rfc.create requires --satisfies DNA-N for ${kind} RFCs created on or after ${RFC_METADATA_CUTOFF} (RFC-0331).`,
169
+ );
170
+ }
171
+ await assertSatisfiesIdsExist(workspaceRoot, satisfies);
172
+
173
+ const files = await listRfcFiles(rfcDirPath);
174
+ let maxId = 0;
175
+ for (const f of files) {
176
+ const basename = path.basename(f);
177
+ const match = basename.match(/^rfc-(\d{4})/);
178
+ if (match) {
179
+ const num = parseInt(match[1]!, 10);
180
+ if (num > maxId) maxId = num;
181
+ }
182
+ }
183
+ const nextNum = maxId + 1;
184
+ const paddedNum = String(nextNum).padStart(4, "0");
185
+ const nextSlug = `rfc-${paddedNum}`;
186
+ const nextId = `RFC-${paddedNum}`;
187
+
188
+ const templatePath = resolveRfcTemplate(workspaceRoot);
189
+ let templateContent: string;
190
+ try {
191
+ templateContent = await fs.readFile(templatePath, "utf-8");
192
+ } catch {
193
+ throw new Error(`RFC template not found at ${templatePath}.`);
194
+ }
195
+
196
+ const kebabTitle = toKebabCase(title);
197
+ const fileName = `${nextSlug}-${kebabTitle}.md`;
198
+
199
+ // RFC-0329: consult decision log before scaffolding
200
+ let consultedDecisions: ConsultedDecision[] = [];
201
+ try {
202
+ const logEntries = await collectDecisionLog(rfcDirPath);
203
+ consultedDecisions = scoreDecisions(title, logEntries);
204
+ } catch {
205
+ // graceful degradation — never block creation
206
+ }
207
+
208
+ if (outputFormat === "pretty") {
209
+ if (consultedDecisions.length > 0) {
210
+ logger.info("Prior decisions to consult:");
211
+ for (const d of consultedDecisions) {
212
+ logger.info(` ${d.rfcId} [${d.kind}] (score ${d.score}): ${d.title}`);
213
+ }
214
+ } else {
215
+ logger.info("No related prior decisions found.");
216
+ }
217
+ }
218
+
219
+ let content = templateContent;
220
+ content = content.replace(/^id: RFC-0000$/m, `id: ${nextId}`);
221
+ content = content.replace(/^title: ".*"$/m, `title: "${title}"`);
222
+ content = content.replace(/^kind: \w+$/m, `kind: ${kind}`);
223
+ content = content.replace(/^scope: \w+$/m, `scope: ${scope}`);
224
+ content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
225
+ content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
226
+ if (satisfies.length > 0) {
227
+ content = content.replace(
228
+ /^satisfies: \[\]$/m,
229
+ `satisfies:\n${satisfies.map((id) => ` - ${id}`).join("\n")}`,
230
+ );
231
+ }
232
+ content = content.replace(/^# RFC-0000: .+$/m, `# ${nextId}: ${title}`);
233
+
234
+ const targetPath = path.join(rfcDirPath, fileName);
235
+ await fs.writeFile(targetPath, content, "utf-8");
236
+
237
+ const relativeFile = path.join(RFC_DIR, fileName);
238
+
239
+ if (outputFormat === "pretty") {
240
+ logger.success(`Created ${nextId}: ${title}`);
241
+ logger.info(`File: ${relativeFile}`);
242
+ logger.info(`Template: full`);
243
+ logger.info("Status: draft (only a human with role 'architecture' may change status)");
244
+ }
245
+
246
+ return {
247
+ data: {
248
+ command: "rfc.create",
249
+ status: "ok",
250
+ file: relativeFile,
251
+ id: nextId,
252
+ satisfies,
253
+ consultedDecisions,
254
+ },
255
+ summary: `Created ${nextId}: ${title}`,
256
+ };
257
+ }
258
+
259
+ export async function runRfcNextId(
260
+ _input: ForgeCommandInput,
261
+ context: ForgeRuntimeContext,
262
+ ): Promise<ForgeCommandResult<RfcNextIdResult>> {
263
+ const { workspaceRoot, logger, outputFormat } = context;
264
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
265
+
266
+ const files = await listRfcFiles(rfcDirPath);
267
+ let maxId = 0;
268
+ for (const f of files) {
269
+ const basename = path.basename(f);
270
+ const match = basename.match(/^rfc-(\d{4})/);
271
+ if (match) {
272
+ const num = parseInt(match[1]!, 10);
273
+ if (num > maxId) maxId = num;
274
+ }
275
+ }
276
+
277
+ const nextNumber = maxId + 1;
278
+ const paddedNum = String(nextNumber).padStart(4, "0");
279
+ const nextId = `RFC-${paddedNum}`;
280
+ const maxExistingId = maxId > 0 ? `RFC-${String(maxId).padStart(4, "0")}` : "none";
281
+
282
+ if (outputFormat === "pretty") {
283
+ logger.info(`Next free RFC number: ${nextId} (scanned ${files.length} files)`);
284
+ }
285
+
286
+ return {
287
+ data: {
288
+ command: "rfc.next-id",
289
+ nextId,
290
+ nextNumber,
291
+ maxExistingId,
292
+ scannedFiles: files.length,
293
+ },
294
+ summary: `Next RFC number: ${nextId}`,
295
+ };
296
+ }
@@ -0,0 +1,173 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Reports the pipeline status of RFCs — which steps (audit, enhance, plan, implement) are complete or missing.</purpose>
4
+ <non-goals>
5
+ <item>Does not validate RFC content — use rfc.validate for that.</item>
6
+ <item>Does not mutate any files.</item>
7
+ <item>Does not gate skill execution — skills keep their own prerequisite checks.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>Add rfc.pipeline.status command for agents and humans to inspect where an RFC stands in the create → audit → enhance → plan → implement pipeline.</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import fs from "node:fs/promises";
16
+ import path from "node:path";
17
+
18
+ import { listRfcFiles, readAndParseRfc } from "../frontmatter-io.ts";
19
+ import type {
20
+ ForgeCommandInput,
21
+ ForgeCommandResult,
22
+ ForgeRuntimeContext,
23
+ } from "../../../src/types.ts";
24
+ import type { RfcStatus } from "../types.ts";
25
+ import { RFC_DIR } from "../types.ts";
26
+
27
+ export type PipelineStage = "audit" | "enhance" | "plan" | "implement";
28
+
29
+ export interface RfcPipelineStageInfo {
30
+ stage: PipelineStage;
31
+ done: boolean;
32
+ /** File or marker that confirms completion, when applicable. */
33
+ evidence?: string;
34
+ }
35
+
36
+ export interface RfcPipelineEntry {
37
+ id: string;
38
+ title: string;
39
+ status: RfcStatus;
40
+ file: string;
41
+ stages: RfcPipelineStageInfo[];
42
+ /** The immediate next missing step, or null if all are done. */
43
+ nextStep: PipelineStage | null;
44
+ }
45
+
46
+ export interface RfcPipelineStatusResult {
47
+ command: "rfc.pipeline.status";
48
+ status: "ok";
49
+ count: number;
50
+ entries: RfcPipelineEntry[];
51
+ }
52
+
53
+ const TERMINAL_STATUSES: ReadonlySet<string> = new Set(["implemented", "rejected", "superseded"]);
54
+
55
+ async function findAuditFile(workspaceRoot: string, rfcId: string): Promise<string | undefined> {
56
+ const auditsDir = path.join(workspaceRoot, "docs/audits");
57
+ try {
58
+ const entries = await fs.readdir(auditsDir);
59
+ const prefix = `audit-${rfcId.toLowerCase()}`;
60
+ const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
61
+ return match ? path.join("docs/audits", match) : undefined;
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+
67
+ async function findPlanFile(workspaceRoot: string, rfcId: string): Promise<string | undefined> {
68
+ const plansDir = path.join(workspaceRoot, "docs/plans");
69
+ try {
70
+ const entries = await fs.readdir(plansDir);
71
+ const prefix = `plan-${rfcId.toLowerCase()}`;
72
+ const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
73
+ return match ? path.join("docs/plans", match) : undefined;
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ export async function runRfcPipelineStatus(
80
+ input: ForgeCommandInput,
81
+ context: ForgeRuntimeContext,
82
+ ): Promise<ForgeCommandResult<RfcPipelineStatusResult>> {
83
+ const { workspaceRoot, logger, outputFormat } = context;
84
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
85
+
86
+ const targetId = input.flags["id"] as string | undefined;
87
+
88
+ let files = await listRfcFiles(rfcDirPath);
89
+ if (targetId) {
90
+ const lower = targetId.toLowerCase();
91
+ files = files.filter((f) => path.basename(f).toLowerCase().startsWith(lower));
92
+ if (files.length === 0) {
93
+ throw new Error(`No RFC file found for id ${targetId} in ${RFC_DIR}/`);
94
+ }
95
+ }
96
+
97
+ const entries: RfcPipelineEntry[] = [];
98
+
99
+ for (const fileName of files) {
100
+ const result = await readAndParseRfc(rfcDirPath, fileName);
101
+ if (!result || "error" in result) continue;
102
+ const fm = result.parsed.frontmatter;
103
+ const id = String(fm["id"] ?? "");
104
+ const title = String(fm["title"] ?? "");
105
+ const status = String(fm["status"] ?? "") as RfcStatus;
106
+ const enhancedAt = fm["enhancedAt"] ? String(fm["enhancedAt"]) : undefined;
107
+ const implementedAt = fm["implementedAt"] ? String(fm["implementedAt"]) : undefined;
108
+
109
+ const isTerminal = TERMINAL_STATUSES.has(status);
110
+
111
+ const auditFile = await findAuditFile(workspaceRoot, id);
112
+ const planFile = await findPlanFile(workspaceRoot, id);
113
+
114
+ const stages: RfcPipelineStageInfo[] = [
115
+ {
116
+ stage: "audit",
117
+ done: !!auditFile,
118
+ evidence: auditFile,
119
+ },
120
+ {
121
+ stage: "enhance",
122
+ done: !!enhancedAt,
123
+ evidence: enhancedAt ? `enhancedAt: ${enhancedAt}` : undefined,
124
+ },
125
+ {
126
+ stage: "plan",
127
+ done: !!planFile,
128
+ evidence: planFile,
129
+ },
130
+ {
131
+ stage: "implement",
132
+ done: status === "implemented" || !!implementedAt,
133
+ evidence: implementedAt ? `implementedAt: ${implementedAt}` : undefined,
134
+ },
135
+ ];
136
+
137
+ let nextStep: PipelineStage | null = null;
138
+ if (!isTerminal) {
139
+ for (const s of stages) {
140
+ if (!s.done) {
141
+ nextStep = s.stage;
142
+ break;
143
+ }
144
+ }
145
+ }
146
+
147
+ entries.push({
148
+ id,
149
+ title,
150
+ status,
151
+ file: path.join(RFC_DIR, fileName),
152
+ stages,
153
+ nextStep,
154
+ });
155
+ }
156
+
157
+ entries.sort((a, b) => a.id.localeCompare(b.id));
158
+
159
+ if (outputFormat === "pretty") {
160
+ logger.section(`RFC pipeline status (${entries.length} RFC(s))`);
161
+ for (const entry of entries) {
162
+ const stageStr = entry.stages.map((s) => `${s.stage}:${s.done ? "✓" : "—"}`).join(" ");
163
+ const next = entry.nextStep ? ` → next: ${entry.nextStep}` : "";
164
+ const terminal = TERMINAL_STATUSES.has(entry.status) ? " (terminal)" : "";
165
+ logger.info(`${entry.id} [${entry.status}${terminal}] ${stageStr}${next}`);
166
+ }
167
+ }
168
+
169
+ return {
170
+ data: { command: "rfc.pipeline.status", status: "ok", count: entries.length, entries },
171
+ summary: `Pipeline status for ${entries.length} RFC(s)`,
172
+ };
173
+ }
@@ -0,0 +1,121 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Shared helpers for RFC handler commands — date utils, invariant ID loading, FS path checks, command bucket parsing.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement command handlers; those live in sibling files.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0303: extracted shared helpers from handlers.ts into handlers/shared.ts.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import fs from "node:fs/promises";
14
+ import { existsSync } from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ import { resolveForgeRoot } from "../../../src/config/forge-config.ts";
19
+ import { RFC_TEMPLATE_FILE, RFC_TEMPLATE_FALLBACK_FILE } from "../types.ts";
20
+ import type { ParsedRfc } from "../frontmatter-io.ts";
21
+
22
+ export function toIsoDate(date: Date): string {
23
+ return date.toISOString().split("T")[0]!;
24
+ }
25
+
26
+ const DNA_DOC = "docs/architecture-dna.md";
27
+ const AP_DOC = "packages/os/site-kernel/docs/anti-patterns.md";
28
+ export const DNA_DOCS = [DNA_DOC];
29
+ export const AP_DOCS = [AP_DOC];
30
+
31
+ /** Parse `## <PREFIX>-N` headings from the given docs and return the set of N. */
32
+ export async function loadInvariantIds(
33
+ workspaceRoot: string,
34
+ relPaths: string[],
35
+ prefix: string,
36
+ ): Promise<Set<number>> {
37
+ const ids = new Set<number>();
38
+ const re = new RegExp(`^##\\s+${prefix}-(\\d+)\\b`, "gm");
39
+ for (const rel of relPaths) {
40
+ try {
41
+ const src = await fs.readFile(path.join(workspaceRoot, rel), "utf-8");
42
+ for (const m of src.matchAll(re)) ids.add(parseInt(m[1]!, 10));
43
+ } catch {
44
+ // Doc not present at this path — try the next candidate.
45
+ }
46
+ }
47
+ return ids;
48
+ }
49
+
50
+ /**
51
+ * Whether a "File system responsibilities" path is a concrete, checkable file.
52
+ * Skips glob/placeholder/prose entries that rfc.check must not `fs.access` verbatim:
53
+ * `apps/*`, `apps/<app>/…`, `{lang}`, `**`, or descriptive phrases with spaces.
54
+ */
55
+ export function isLiteralFsPath(p: string): boolean {
56
+ if (/[*<>{}]/.test(p)) return false; // globs / placeholders
57
+ if (/\s/.test(p)) return false; // prose ("Every generator module")
58
+ if (!p.includes("/") && !p.includes(".")) return false; // bare words
59
+ return true;
60
+ }
61
+
62
+ export function stringArray(value: unknown): string[] {
63
+ return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
64
+ }
65
+
66
+ export function commandBuckets(fm: Record<string, unknown>): {
67
+ proposed: string[];
68
+ added: string[];
69
+ changed: string[];
70
+ removed: string[];
71
+ } {
72
+ const commands = fm["commands"];
73
+ if (!commands || typeof commands !== "object" || Array.isArray(commands)) {
74
+ return { proposed: [], added: [], changed: [], removed: [] };
75
+ }
76
+ const record = commands as Record<string, unknown>;
77
+ return {
78
+ proposed: stringArray(record["proposed"]),
79
+ added: stringArray(record["added"]),
80
+ changed: stringArray(record["changed"]),
81
+ removed: stringArray(record["removed"]),
82
+ };
83
+ }
84
+
85
+ export type { ParsedRfc };
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // RFC template resolver — finds rfc-0000-template.md from project docs/rfcs/
89
+ // first (canonical location, mirroring ADR pattern), then falls back to the
90
+ // forge package copy (npm consumers without a project-level template).
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export function resolveRfcTemplate(workspaceRoot: string): string {
94
+ // 1. Try project-level docs/rfcs/rfc-0000-template.md (canonical, like ADR)
95
+ const projectPath = path.join(workspaceRoot, RFC_TEMPLATE_FILE);
96
+ if (existsSync(projectPath)) return projectPath;
97
+
98
+ // 2. Try forge root (monorepo or npm-installed) — fallback copy
99
+ try {
100
+ const forgeRoot = resolveForgeRoot(workspaceRoot);
101
+ const p = path.join(forgeRoot, RFC_TEMPLATE_FALLBACK_FILE);
102
+ if (existsSync(p)) return p;
103
+ } catch {
104
+ // forge root not resolvable — fall through to module-relative search
105
+ }
106
+
107
+ // 3. Fallback: walk up from this module's location to find os/rfc/rfc-0000-template.md
108
+ const here = path.dirname(fileURLToPath(import.meta.url));
109
+ let dir = here;
110
+ for (let i = 0; i < 10; i++) {
111
+ const candidate = path.join(dir, "os", "rfc", "rfc-0000-template.md");
112
+ if (existsSync(candidate)) return candidate;
113
+ const parent = path.dirname(dir);
114
+ if (parent === dir) break;
115
+ dir = parent;
116
+ }
117
+
118
+ throw new Error(
119
+ `RFC template not found. Tried ${RFC_TEMPLATE_FILE} relative to ${workspaceRoot}, forge root, and module-relative search from ${here}.`,
120
+ );
121
+ }