@warpgogol/forge 5.3.4 → 6.2.0

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 (253) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.ts +7 -2
  3. package/dist/bin/cli.js +7 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/adr.module.d.ts.map +1 -1
  6. package/dist/os/adr/adr.module.js +2 -0
  7. package/dist/os/adr/adr.module.js.map +1 -1
  8. package/dist/os/audit/audit.module.d.ts.map +1 -1
  9. package/dist/os/audit/audit.module.js +2 -0
  10. package/dist/os/audit/audit.module.js.map +1 -1
  11. package/dist/os/compass/compass.module.d.ts.map +1 -1
  12. package/dist/os/compass/compass.module.js +47 -15
  13. package/dist/os/compass/compass.module.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
  15. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
  17. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
  19. package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
  20. package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/summary-record.d.ts +11 -2
  23. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  24. package/dist/os/compass/handlers/summary-record.js +95 -15
  25. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  26. package/dist/os/core/core.module.d.ts.map +1 -1
  27. package/dist/os/core/core.module.js +124 -73
  28. package/dist/os/core/core.module.js.map +1 -1
  29. package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
  30. package/dist/os/core/handlers/file-size-lint.js +2 -0
  31. package/dist/os/core/handlers/file-size-lint.js.map +1 -1
  32. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  33. package/dist/os/exploration/exploration.module.js +3 -0
  34. package/dist/os/exploration/exploration.module.js.map +1 -1
  35. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  36. package/dist/os/naming/naming-convention.js +11 -1
  37. package/dist/os/naming/naming-convention.js.map +1 -1
  38. package/dist/os/naming/naming.module.d.ts.map +1 -1
  39. package/dist/os/naming/naming.module.js +9 -2
  40. package/dist/os/naming/naming.module.js.map +1 -1
  41. package/dist/os/notes/notes.module.d.ts.map +1 -1
  42. package/dist/os/notes/notes.module.js +17 -6
  43. package/dist/os/notes/notes.module.js.map +1 -1
  44. package/dist/os/plan/plan.module.d.ts.map +1 -1
  45. package/dist/os/plan/plan.module.js +2 -0
  46. package/dist/os/plan/plan.module.js.map +1 -1
  47. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  48. package/dist/os/plugin/plugin.module.js +9 -2
  49. package/dist/os/plugin/plugin.module.js.map +1 -1
  50. package/dist/os/program/program.module.d.ts.map +1 -1
  51. package/dist/os/program/program.module.js +19 -4
  52. package/dist/os/program/program.module.js.map +1 -1
  53. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  54. package/dist/os/queue/handlers/queue-validate.js +154 -7
  55. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  56. package/dist/os/queue/manifest.d.ts +17 -1
  57. package/dist/os/queue/manifest.d.ts.map +1 -1
  58. package/dist/os/queue/manifest.js +127 -6
  59. package/dist/os/queue/manifest.js.map +1 -1
  60. package/dist/os/queue/queue.module.d.ts.map +1 -1
  61. package/dist/os/queue/queue.module.js +32 -4
  62. package/dist/os/queue/queue.module.js.map +1 -1
  63. package/dist/os/queue/types.d.ts +100 -1
  64. package/dist/os/queue/types.d.ts.map +1 -1
  65. package/dist/os/queue/types.js +44 -0
  66. package/dist/os/queue/types.js.map +1 -1
  67. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  68. package/dist/os/rfc/handlers/implement-stamp.js +30 -8
  69. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  70. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  71. package/dist/os/rfc/rfc.module.js +25 -6
  72. package/dist/os/rfc/rfc.module.js.map +1 -1
  73. package/dist/os/rfc/types.d.ts +12 -2
  74. package/dist/os/rfc/types.d.ts.map +1 -1
  75. package/dist/os/rfc/types.js +2 -2
  76. package/dist/os/rfc/types.js.map +1 -1
  77. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  78. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  79. package/dist/os/rfc/verification-evidence.js +134 -23
  80. package/dist/os/rfc/verification-evidence.js.map +1 -1
  81. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  82. package/dist/os/rfc/verification-refresh.js +66 -37
  83. package/dist/os/rfc/verification-refresh.js.map +1 -1
  84. package/dist/os/session/handlers/save.d.ts.map +1 -1
  85. package/dist/os/session/handlers/save.js +42 -5
  86. package/dist/os/session/handlers/save.js.map +1 -1
  87. package/dist/os/session/session.module.d.ts.map +1 -1
  88. package/dist/os/session/session.module.js +5 -0
  89. package/dist/os/session/session.module.js.map +1 -1
  90. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  91. package/dist/os/spec/live-spec-merge.js +55 -134
  92. package/dist/os/spec/live-spec-merge.js.map +1 -1
  93. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  94. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  95. package/dist/os/spec/live-spec-rebuild.js +272 -0
  96. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  97. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  98. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  99. package/dist/os/spec/live-spec-shared.js +315 -0
  100. package/dist/os/spec/live-spec-shared.js.map +1 -0
  101. package/dist/os/spec/live-spec-types.d.ts +17 -1
  102. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  103. package/dist/os/spec/live-spec-types.js +3 -0
  104. package/dist/os/spec/live-spec-types.js.map +1 -1
  105. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  106. package/dist/os/spec/live-spec-validate.js +105 -2
  107. package/dist/os/spec/live-spec-validate.js.map +1 -1
  108. package/dist/os/spec/spec-materialize.d.ts +2 -0
  109. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  110. package/dist/os/spec/spec-materialize.js +45 -5
  111. package/dist/os/spec/spec-materialize.js.map +1 -1
  112. package/dist/os/spec/spec-schema.d.ts +19 -0
  113. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  114. package/dist/os/spec/spec-schema.js +30 -0
  115. package/dist/os/spec/spec-schema.js.map +1 -1
  116. package/dist/os/spec/spec-status.d.ts +4 -0
  117. package/dist/os/spec/spec-status.d.ts.map +1 -1
  118. package/dist/os/spec/spec-status.js +19 -1
  119. package/dist/os/spec/spec-status.js.map +1 -1
  120. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  121. package/dist/os/spec/spec-validate.js +55 -8
  122. package/dist/os/spec/spec-validate.js.map +1 -1
  123. package/dist/os/spec/spec.module.d.ts.map +1 -1
  124. package/dist/os/spec/spec.module.js +84 -8
  125. package/dist/os/spec/spec.module.js.map +1 -1
  126. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  127. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  128. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  129. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  130. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  131. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  132. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  133. package/dist/os/workflow/workflow.module.js +13 -5
  134. package/dist/os/workflow/workflow.module.js.map +1 -1
  135. package/dist/src/cli-flags.d.ts.map +1 -1
  136. package/dist/src/cli-flags.js +25 -5
  137. package/dist/src/cli-flags.js.map +1 -1
  138. package/dist/src/config/forge-config.d.ts +3 -0
  139. package/dist/src/config/forge-config.d.ts.map +1 -1
  140. package/dist/src/config/forge-config.js +2 -0
  141. package/dist/src/config/forge-config.js.map +1 -1
  142. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  143. package/dist/src/onboarding/doctor.js +2 -3
  144. package/dist/src/onboarding/doctor.js.map +1 -1
  145. package/dist/src/onboarding/init.d.ts.map +1 -1
  146. package/dist/src/onboarding/init.js +8 -3
  147. package/dist/src/onboarding/init.js.map +1 -1
  148. package/dist/src/pipeline-status.d.ts +14 -1
  149. package/dist/src/pipeline-status.d.ts.map +1 -1
  150. package/dist/src/pipeline-status.js +28 -8
  151. package/dist/src/pipeline-status.js.map +1 -1
  152. package/dist/src/types.d.ts +7 -1
  153. package/dist/src/types.d.ts.map +1 -1
  154. package/dist/src/types.js +6 -0
  155. package/dist/src/types.js.map +1 -1
  156. package/dist/src/utils/generated-marker.d.ts +8 -0
  157. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  158. package/dist/src/utils/generated-marker.js +25 -0
  159. package/dist/src/utils/generated-marker.js.map +1 -1
  160. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  161. package/dist/src/validators/skill-validate.js +2 -4
  162. package/dist/src/validators/skill-validate.js.map +1 -1
  163. package/os/adr/adr.module.ts +2 -0
  164. package/os/audit/audit.module.ts +2 -0
  165. package/os/compass/compass.module.ts +49 -15
  166. package/os/compass/handlers/compass-audit-handler.ts +51 -10
  167. package/os/compass/handlers/compass-docs-validate.ts +361 -0
  168. package/os/compass/handlers/summary-record.ts +106 -15
  169. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  170. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  171. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  172. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  173. package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
  174. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  175. package/os/core/core.module.ts +147 -75
  176. package/os/core/docs-archive.test.ts +136 -0
  177. package/os/core/handlers/file-size-lint.ts +2 -0
  178. package/os/exploration/exploration.module.ts +3 -0
  179. package/os/naming/naming-convention.ts +11 -1
  180. package/os/naming/naming.module.ts +9 -2
  181. package/os/notes/notes.module.ts +17 -6
  182. package/os/plan/plan-0000-template.md +150 -0
  183. package/os/plan/plan.module.ts +2 -0
  184. package/os/plugin/plugin.module.ts +9 -2
  185. package/os/program/program.module.ts +19 -4
  186. package/os/queue/decision-ledger.test.ts +163 -0
  187. package/os/queue/handlers/queue-validate.ts +190 -8
  188. package/os/queue/manifest.ts +201 -7
  189. package/os/queue/queue-validate.test.ts +303 -0
  190. package/os/queue/queue.module.ts +32 -4
  191. package/os/queue/types.ts +56 -1
  192. package/os/rfc/handlers/implement-stamp.ts +37 -8
  193. package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
  194. package/os/rfc/rfc.module.ts +27 -6
  195. package/os/rfc/types.ts +18 -3
  196. package/os/rfc/verification-emit.test.ts +305 -0
  197. package/os/rfc/verification-evidence.ts +149 -24
  198. package/os/rfc/verification-refresh.test.ts +222 -1
  199. package/os/rfc/verification-refresh.ts +74 -38
  200. package/os/session/handlers/save.ts +37 -5
  201. package/os/session/session.module.ts +5 -0
  202. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  203. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  204. package/os/spec/live-spec-merge.test.ts +187 -7
  205. package/os/spec/live-spec-merge.ts +71 -155
  206. package/os/spec/live-spec-rebuild.test.ts +317 -0
  207. package/os/spec/live-spec-rebuild.ts +356 -0
  208. package/os/spec/live-spec-shared.ts +378 -0
  209. package/os/spec/live-spec-types.ts +21 -1
  210. package/os/spec/live-spec-validate.test.ts +317 -0
  211. package/os/spec/live-spec-validate.ts +106 -2
  212. package/os/spec/spec-materialize.test.ts +192 -0
  213. package/os/spec/spec-materialize.ts +48 -4
  214. package/os/spec/spec-schema.ts +49 -0
  215. package/os/spec/spec-status.ts +24 -0
  216. package/os/spec/spec-validate.test.ts +66 -0
  217. package/os/spec/spec-validate.ts +62 -8
  218. package/os/spec/spec.module.ts +85 -8
  219. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  220. package/os/werkstatt/werkstatt.module.ts +13 -4
  221. package/os/workflow/workflow.module.ts +13 -5
  222. package/package.json +1 -1
  223. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  224. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  225. package/skills/fo/fo-explore/SKILL.md +2 -1
  226. package/skills/fo/fo-fix/SKILL.md +11 -3
  227. package/skills/fo/fo-handoff/SKILL.md +4 -6
  228. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  229. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  230. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  231. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  232. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  233. package/skills/fo/fo-review/SKILL.md +69 -8
  234. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  235. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  236. package/skills/shared/grilling/SKILL.md +12 -0
  237. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  238. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  239. package/src/cli-flags.ts +23 -5
  240. package/src/config/forge-config.ts +4 -0
  241. package/src/onboarding/doctor.ts +2 -3
  242. package/src/onboarding/init.ts +8 -3
  243. package/src/pipeline-status.ts +32 -8
  244. package/src/tests/cli-flags.test.ts +20 -0
  245. package/src/tests/cli-output.test.ts +13 -7
  246. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  247. package/src/tests/implement-stamp.test.ts +38 -0
  248. package/src/tests/session-handlers.test.ts +33 -0
  249. package/src/types.ts +13 -1
  250. package/src/utils/generated-marker.ts +28 -0
  251. package/src/validators/skill-validate.ts +2 -4
  252. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  253. package/skills/fo/fo-review/AXES.md +0 -70
@@ -8,6 +8,7 @@
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>RFC-0396: initial spec.materialize handler — front computation, scaffold via rfc.create, materializedAs write-back.</item>
11
+ <item>RFC-1240: consumer-aware front — consumers ownership gate, foreign-dep satisfaction, foreignOwned result field, --consumer flag resolution.</item>
11
12
  </CHANGE_SUMMARY>
12
13
  */
13
14
 
@@ -29,6 +30,9 @@ import {
29
30
  type ForgeSpec,
30
31
  type SpecRfcNode,
31
32
  type SpecAmendment,
33
+ ownsSpecNode,
34
+ isForeignOwnedNode,
35
+ isForeignConsumerDep,
32
36
  resolveAmendedNode,
33
37
  } from "./spec-schema.ts";
34
38
  import { listRfcFiles } from "../rfc/frontmatter-io.ts";
@@ -55,6 +59,8 @@ interface SpecMaterializeResult {
55
59
  created: MaterializedNode[];
56
60
  skipped: SkippedNode[];
57
61
  front: string[];
62
+ /** RFC-1240: nodes excluded from the front by consumer ownership. */
63
+ foreignOwned: string[];
58
64
  }
59
65
 
60
66
  // ---------------------------------------------------------------------------
@@ -94,6 +100,7 @@ export async function runSpecMaterialize(
94
100
  created: [],
95
101
  skipped: [],
96
102
  front: [],
103
+ foreignOwned: [],
97
104
  },
98
105
  exitCode: 1,
99
106
  summary: "spec.materialize: --spec=<id> is required",
@@ -101,12 +108,16 @@ export async function runSpecMaterialize(
101
108
  }
102
109
 
103
110
  let specsDir = "docs/specs";
111
+ let configConsumer: string | undefined;
104
112
  try {
105
113
  const config = loadForgeConfig(workspaceRoot);
106
114
  specsDir = config.paths.specsDir;
115
+ configConsumer = config.project.consumer;
107
116
  } catch {
108
117
  // Use default
109
118
  }
119
+ // RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
120
+ const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
110
121
 
111
122
  const specDir = path.join(workspaceRoot, specsDir, specId);
112
123
  const forgeSpecPath = path.join(specDir, "forge-spec.yaml");
@@ -120,6 +131,7 @@ export async function runSpecMaterialize(
120
131
  created: [],
121
132
  skipped: [],
122
133
  front: [],
134
+ foreignOwned: [],
123
135
  },
124
136
  exitCode: 1,
125
137
  summary: `spec.materialize: forge-spec.yaml not found for spec '${specId}'`,
@@ -136,6 +148,7 @@ export async function runSpecMaterialize(
136
148
  created: [],
137
149
  skipped: [],
138
150
  front: [],
151
+ foreignOwned: [],
139
152
  },
140
153
  exitCode: 1,
141
154
  summary: `spec.materialize: forge-spec.yaml schema violation for spec '${specId}'`,
@@ -153,6 +166,7 @@ export async function runSpecMaterialize(
153
166
  created: [],
154
167
  skipped: [],
155
168
  front: [],
169
+ foreignOwned: [],
156
170
  },
157
171
  exitCode: 1,
158
172
  summary: `spec.materialize: spec '${specId}' is not accepted (status: ${spec.status}). Run /fo-spec-ingest to complete acceptance.`,
@@ -220,10 +234,27 @@ export async function runSpecMaterialize(
220
234
  }
221
235
  }
222
236
 
223
- // Compute front: unmaterialized nodes whose deps are all implemented
237
+ // RFC-1240: consumer ownership — a node with `consumers` is ours only when the
238
+ // resolved local consumer identity is a member. Without local identity every
239
+ // consumer-declared node is treated as not locally owned.
240
+ const nodeById = new Map(spec.rfcs.map((n) => [n.id, n]));
241
+ const ownsNode = (node: SpecRfcNode): boolean =>
242
+ ownsSpecNode(node, localConsumer);
243
+ const depSatisfied = (depId: string): boolean => {
244
+ if (nodeState.get(depId) === "implemented") return true;
245
+ // Foreign-consumer dependency: an implementation duty of another consumer.
246
+ return isForeignConsumerDep(nodeById.get(depId), localConsumer);
247
+ };
248
+ const foreignOwned = spec.rfcs
249
+ .filter((n) => isForeignOwnedNode(n, localConsumer))
250
+ .map((n) => n.id);
251
+
252
+ // Compute front: unmaterialized, locally-owned nodes whose deps are all
253
+ // implemented or foreign-owned (RFC-1240).
224
254
  function isFront(node: SpecRfcNode): boolean {
225
255
  if (node.materializedAs) return false;
226
- return node.dependsOn.every((dep) => nodeState.get(dep) === "implemented");
256
+ if (!ownsNode(node)) return false;
257
+ return node.dependsOn.every(depSatisfied);
227
258
  }
228
259
 
229
260
  const frontNodes = spec.rfcs.filter(isFront);
@@ -241,8 +272,15 @@ export async function runSpecMaterialize(
241
272
  if (!node) {
242
273
  skipped.push({ node: nodeId, reason: "node not found in spec" });
243
274
  } else if (!isFront(node)) {
244
- const blockers = node.dependsOn.filter((d) => nodeState.get(d) !== "implemented");
245
- skipped.push({ node: nodeId, reason: `blocked by ${blockers.join(", ")}` });
275
+ if (!ownsSpecNode(node, localConsumer)) {
276
+ const reason = localConsumer == null
277
+ ? "consumer identity not configured"
278
+ : `foreign-owned by ${(node.consumers ?? []).join(", ")}`;
279
+ skipped.push({ node: nodeId, reason });
280
+ } else {
281
+ const blockers = node.dependsOn.filter((d) => !depSatisfied(d));
282
+ skipped.push({ node: nodeId, reason: `blocked by ${blockers.join(", ")}` });
283
+ }
246
284
  } else {
247
285
  toMaterialize.push(node);
248
286
  }
@@ -256,6 +294,7 @@ export async function runSpecMaterialize(
256
294
  created: [],
257
295
  skipped,
258
296
  front: frontNodes.map((n) => n.id),
297
+ foreignOwned,
259
298
  },
260
299
  exitCode: 1,
261
300
  summary: `spec.materialize: all requested nodes blocked or not found`,
@@ -293,6 +332,7 @@ export async function runSpecMaterialize(
293
332
  created: [],
294
333
  skipped: [],
295
334
  front: frontNodes.map((n) => n.id),
335
+ foreignOwned,
296
336
  },
297
337
  exitCode: 1,
298
338
  summary: `spec.materialize: RFC template not found at ${templatePath}`,
@@ -373,6 +413,9 @@ export async function runSpecMaterialize(
373
413
  if (remainingFront.length > 0) {
374
414
  logger.info(`Remaining front: ${remainingFront.join(", ")}`);
375
415
  }
416
+ if (foreignOwned.length > 0) {
417
+ logger.info(`Foreign-owned nodes (excluded from front): ${foreignOwned.join(", ")}`);
418
+ }
376
419
  }
377
420
 
378
421
  return {
@@ -383,6 +426,7 @@ export async function runSpecMaterialize(
383
426
  created,
384
427
  skipped,
385
428
  front: remainingFront,
429
+ foreignOwned,
386
430
  },
387
431
  exitCode: 0,
388
432
  summary: `spec.materialize: created ${created.length} RFC(s) from spec '${specId}'`,
@@ -8,6 +8,7 @@
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>RFC-0394: initial spec schema — forge/spec@1, integrity manifest, decision/rfc/wave types.</item>
11
+ <item>RFC-1240: consumers field on roadmap nodes (consumer ownership filtering); severity field on SpecViolation for warning diagnostics.</item>
11
12
  </CHANGE_SUMMARY>
12
13
  */
13
14
 
@@ -38,6 +39,8 @@ export const specRfcNodeSchema = z.object({
38
39
  wave: z.number().int().min(1),
39
40
  sources: z.array(z.string()).default([]),
40
41
  materializedAs: z.string().optional(),
42
+ /** RFC-1240: consumer ownership — absent means the node materializes everywhere. */
43
+ consumers: z.array(z.string().min(1)).optional(),
41
44
  });
42
45
 
43
46
  export type SpecRfcNode = z.infer<typeof specRfcNodeSchema>;
@@ -77,6 +80,50 @@ export const forgeSpecSchema = z.object({
77
80
 
78
81
  export type ForgeSpec = z.infer<typeof forgeSpecSchema>;
79
82
 
83
+ // ---------------------------------------------------------------------------
84
+ // Consumer ownership predicates (RFC-1240)
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * True when the node may materialize for this workspace's consumer identity.
89
+ * A node without `consumers` is owned by every consumer.
90
+ */
91
+ export function ownsSpecNode(
92
+ node: SpecRfcNode,
93
+ localConsumer: string | undefined,
94
+ ): boolean {
95
+ return (
96
+ !node.consumers ||
97
+ (localConsumer != null && node.consumers.includes(localConsumer))
98
+ );
99
+ }
100
+
101
+ /**
102
+ * True when the node declares `consumers` but is not locally owned —
103
+ * either the local identity is absent or it is not a member.
104
+ */
105
+ export function isForeignOwnedNode(
106
+ node: SpecRfcNode,
107
+ localConsumer: string | undefined,
108
+ ): boolean {
109
+ return node.consumers != null && !ownsSpecNode(node, localConsumer);
110
+ }
111
+
112
+ /**
113
+ * True when a dependency node's implementation duty belongs to other
114
+ * consumers — it satisfies dependents without a local implementation.
115
+ */
116
+ export function isForeignConsumerDep(
117
+ depNode: SpecRfcNode | undefined,
118
+ localConsumer: string | undefined,
119
+ ): boolean {
120
+ return (
121
+ depNode?.consumers != null &&
122
+ localConsumer != null &&
123
+ !depNode.consumers.includes(localConsumer)
124
+ );
125
+ }
126
+
80
127
  // ---------------------------------------------------------------------------
81
128
  // Integrity manifest (forge/spec-integrity@1)
82
129
  // ---------------------------------------------------------------------------
@@ -95,6 +142,8 @@ export type SpecIntegrity = z.infer<typeof specIntegritySchema>;
95
142
  export interface SpecViolation {
96
143
  rule: string;
97
144
  message: string;
145
+ /** RFC-1240: warnings (e.g. missing consumer identity) do not fail validation. */
146
+ severity?: "error" | "warning";
98
147
  }
99
148
 
100
149
  export interface SpecValidateResult {
@@ -7,6 +7,7 @@
7
7
  </MODULE_CONTRACT>
8
8
  <CHANGE_SUMMARY>
9
9
  <item>RFC-0396: initial spec.status handler — per-node states, blockers, front, progress.</item>
10
+ <item>RFC-1240: consumer-aware front — foreignOwned flag/list, foreign deps are not local blockers, --consumer flag resolution.</item>
10
11
  </CHANGE_SUMMARY>
11
12
  */
12
13
 
@@ -23,6 +24,8 @@ import type {
23
24
  import {
24
25
  forgeSpecSchema,
25
26
  specAmendmentSchema,
27
+ isForeignConsumerDep,
28
+ isForeignOwnedNode,
26
29
  type ForgeSpec,
27
30
  type SpecAmendment,
28
31
  } from "./spec-schema.ts";
@@ -37,6 +40,8 @@ interface SpecNodeStatus {
37
40
  materializedAs?: string;
38
41
  wave: number;
39
42
  blockedBy: string[];
43
+ /** RFC-1240: true when the node is owned by other consumers (excluded from front). */
44
+ foreignOwned: boolean;
40
45
  }
41
46
 
42
47
  interface SpecProgress {
@@ -49,6 +54,8 @@ interface SpecStatusEntry {
49
54
  status: string;
50
55
  nodes: SpecNodeStatus[];
51
56
  front: string[];
57
+ /** RFC-1240: nodes excluded from the front by consumer ownership. */
58
+ foreignOwned: string[];
52
59
  progress: SpecProgress;
53
60
  amendments?: {
54
61
  proposed: number;
@@ -138,6 +145,8 @@ function computeFront(
138
145
  ): string[] {
139
146
  return nodes
140
147
  .filter((n) => n.state === "unmaterialized")
148
+ // RFC-1240: foreign-owned nodes are never on the local front.
149
+ .filter((n) => !n.foreignOwned)
141
150
  .filter((n) => n.blockedBy.every((dep) => {
142
151
  const depNode = nodeMap.get(dep);
143
152
  return depNode?.state === "implemented";
@@ -156,12 +165,16 @@ export async function runSpecStatus(
156
165
  const { workspaceRoot, logger, outputFormat } = context;
157
166
 
158
167
  let specsDir = "docs/specs";
168
+ let configConsumer: string | undefined;
159
169
  try {
160
170
  const config = loadForgeConfig(workspaceRoot);
161
171
  specsDir = config.paths.specsDir;
172
+ configConsumer = config.project.consumer;
162
173
  } catch {
163
174
  // Use default
164
175
  }
176
+ // RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
177
+ const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
165
178
 
166
179
  const specsRoot = path.join(workspaceRoot, specsDir);
167
180
  const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
@@ -198,9 +211,15 @@ export async function runSpecStatus(
198
211
  else state = "draft";
199
212
  }
200
213
 
214
+ // RFC-1240: a node with `consumers` not containing the local consumer
215
+ // identity (or any `consumers` when identity is absent) is foreign-owned.
216
+ const foreignOwned = isForeignOwnedNode(node, localConsumer);
217
+
201
218
  const blockedBy = node.dependsOn.filter((dep) => {
202
219
  const depNode = spec.rfcs.find((n) => n.id === dep);
203
220
  if (!depNode) return false;
221
+ // RFC-1240: a dependency owned by other consumers is not a local blocker.
222
+ if (isForeignConsumerDep(depNode, localConsumer)) return false;
204
223
  if (!depNode.materializedAs) return true;
205
224
  const depStatus = rfcStatuses.get(depNode.materializedAs);
206
225
  return depStatus !== "implemented";
@@ -212,6 +231,7 @@ export async function runSpecStatus(
212
231
  materializedAs: node.materializedAs,
213
232
  wave: node.wave,
214
233
  blockedBy,
234
+ foreignOwned,
215
235
  };
216
236
  nodes.push(nodeStatus);
217
237
  nodeMap.set(node.id, nodeStatus);
@@ -240,6 +260,7 @@ export async function runSpecStatus(
240
260
  status: spec.status,
241
261
  nodes,
242
262
  front,
263
+ foreignOwned: nodes.filter((n) => n.foreignOwned).map((n) => n.id),
243
264
  progress,
244
265
  amendments: {
245
266
  proposed: amendments.filter((a) => a.status === "proposed").length,
@@ -261,6 +282,9 @@ export async function runSpecStatus(
261
282
  `Progress: ${entry.progress.implemented}/${entry.progress.total} implemented`,
262
283
  );
263
284
  logger.info(`Front: ${entry.front.join(", ") || "(none)"}`);
285
+ if (entry.foreignOwned.length > 0) {
286
+ logger.info(`Foreign-owned: ${entry.foreignOwned.join(", ")}`);
287
+ }
264
288
  for (const node of entry.nodes) {
265
289
  const blocked = node.blockedBy.length > 0
266
290
  ? ` [blocked by ${node.blockedBy.join(", ")}]`
@@ -19,6 +19,72 @@ afterEach(async () => {
19
19
  await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
20
20
  });
21
21
 
22
+ describe("spec.validate consumer rules (RFC-1240)", () => {
23
+ async function writeConsumerSpec(workspaceRoot: string, consumersYaml: string): Promise<void> {
24
+ const specDir = path.join(workspaceRoot, "docs/specs/example");
25
+ await fs.mkdir(specDir, { recursive: true });
26
+ await fs.writeFile(path.join(specDir, "forge-spec.yaml"), `schema: forge/spec@1
27
+ id: example
28
+ title: Example
29
+ version: 1.0.0
30
+ status: accepted
31
+ reviewers: []
32
+ sourceNote: test fixture
33
+ vendoredAt: 2026-10-09
34
+ documents: {}
35
+ decisions: []
36
+ rfcs:
37
+ - id: EX-001
38
+ title: Consumer node
39
+ dependsOn: []
40
+ wave: 1
41
+ sources: []
42
+ ${consumersYaml}waves:
43
+ - id: 1
44
+ name: Test
45
+ goal: Exercise consumer rules
46
+ `);
47
+ await fs.writeFile(path.join(specDir, "integrity.yaml"), "schema: forge/spec-integrity@1\nfiles: {}\n");
48
+ }
49
+
50
+ function ctx(workspaceRoot: string): ForgeRuntimeContext {
51
+ return { workspaceRoot, dryRun: false, outputFormat: "json",
52
+ logger: { section() {}, info() {}, warn() {}, error() {}, success() {} } } as ForgeRuntimeContext;
53
+ }
54
+
55
+ it("emits SPEC-13 warning without failing when consumers exist but no local identity is configured", async () => {
56
+ const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
57
+ roots.push(workspaceRoot);
58
+ await writeConsumerSpec(workspaceRoot, " consumers:\n - personal-agent\n");
59
+ const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
60
+ expect(result.exitCode).toBe(0);
61
+ expect(result.data.status).toBe("pass");
62
+ const warnings = result.data.specs[0]!.violations.filter((v) => v.severity === "warning");
63
+ expect(warnings.map((v) => v.rule)).toEqual(["SPEC-13"]);
64
+ expect(warnings[0]!.message).toContain("EX-001");
65
+ });
66
+
67
+ it("does not emit SPEC-13 when project.consumer is configured", async () => {
68
+ const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
69
+ roots.push(workspaceRoot);
70
+ await fs.writeFile(path.join(workspaceRoot, "forge.yaml"),
71
+ "schema: forge/config@1\nproject:\n name: test-ws\n consumer: werkstatt\npaths: {}\n");
72
+ await writeConsumerSpec(workspaceRoot, " consumers:\n - personal-agent\n");
73
+ const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
74
+ expect(result.exitCode).toBe(0);
75
+ expect(result.data.specs[0]!.violations).toEqual([]);
76
+ });
77
+
78
+ it("fails with SPEC-12 on duplicate consumer entries", async () => {
79
+ const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
80
+ roots.push(workspaceRoot);
81
+ await writeConsumerSpec(workspaceRoot, " consumers:\n - werkstatt\n - werkstatt\n");
82
+ const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
83
+ expect(result.exitCode).toBe(1);
84
+ expect(result.data.specs[0]!.violations.map((v) => v.rule)).toContain("SPEC-12");
85
+ });
86
+ });
87
+
22
88
  describe("spec.validate SPEC-07", () => {
23
89
  it("accepts a materialized RFC after docs.archive moves it below archive/implemented", async () => {
24
90
  const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-archive-"));
@@ -10,6 +10,7 @@
10
10
  <CHANGE_SUMMARY>
11
11
  <item>RFC-0394: initial spec.validate handler with SPEC-01..07 rules.</item>
12
12
  <item>Gap fix: resolve materialized RFCs recursively so terminal RFC archival does not invalidate accepted specs.</item>
13
+ <item>RFC-1240: SPEC-12 consumers list hygiene, SPEC-13 consumer-identity warning; warnings no longer fail validation.</item>
13
14
  </CHANGE_SUMMARY>
14
15
  */
15
16
 
@@ -280,6 +281,45 @@ async function checkMaterializedAs(
280
281
  }
281
282
  }
282
283
 
284
+ // ---------------------------------------------------------------------------
285
+ // SPEC-12: consumers list hygiene / SPEC-13: consumer identity configured (RFC-1240)
286
+ // ---------------------------------------------------------------------------
287
+
288
+ function checkConsumers(
289
+ spec: ForgeSpec,
290
+ localConsumer: string | undefined,
291
+ violations: SpecViolation[],
292
+ ): void {
293
+ const identityNodes: string[] = [];
294
+ for (const node of spec.rfcs) {
295
+ if (!node.consumers) continue;
296
+ if (node.consumers.length === 0) {
297
+ violations.push({
298
+ rule: "SPEC-12",
299
+ message: `node ${node.id} declares an empty consumers list`,
300
+ });
301
+ }
302
+ const seen = new Set<string>();
303
+ for (const consumer of node.consumers) {
304
+ if (seen.has(consumer)) {
305
+ violations.push({
306
+ rule: "SPEC-12",
307
+ message: `node ${node.id} declares duplicate consumer '${consumer}'`,
308
+ });
309
+ }
310
+ seen.add(consumer);
311
+ }
312
+ if (localConsumer == null) identityNodes.push(node.id);
313
+ }
314
+ if (identityNodes.length > 0) {
315
+ violations.push({
316
+ rule: "SPEC-13",
317
+ severity: "warning",
318
+ message: `consumer identity not configured — nodes declaring consumers cannot be owned locally: ${identityNodes.join(", ")} (set project.consumer in forge.yaml or pass --consumer)`,
319
+ });
320
+ }
321
+ }
322
+
283
323
  // ---------------------------------------------------------------------------
284
324
  // SPEC-08..11: Amendment validation (RFC-0397)
285
325
  // ---------------------------------------------------------------------------
@@ -401,12 +441,16 @@ export async function runSpecValidate(
401
441
 
402
442
  // Resolve specs directory from forge.yaml
403
443
  let specsDir = "docs/specs";
444
+ let configConsumer: string | undefined;
404
445
  try {
405
446
  const config = loadForgeConfig(workspaceRoot);
406
447
  specsDir = config.paths.specsDir;
448
+ configConsumer = config.project.consumer;
407
449
  } catch {
408
450
  // Use default
409
451
  }
452
+ // RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
453
+ const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
410
454
 
411
455
  const specsRoot = path.join(workspaceRoot, specsDir);
412
456
  const filterSpec = input.flags["spec"] as string | undefined;
@@ -475,6 +519,9 @@ export async function runSpecValidate(
475
519
  const amendments = await loadAmendments(specDir);
476
520
  checkAmendments(spec, amendments, violations);
477
521
 
522
+ // SPEC-12/13: consumers hygiene + identity warning (RFC-1240)
523
+ checkConsumers(spec, localConsumer, violations);
524
+
478
525
  results.push({
479
526
  id: spec.id,
480
527
  status: spec.status,
@@ -482,20 +529,27 @@ export async function runSpecValidate(
482
529
  });
483
530
  }
484
531
 
485
- const hasFailures = results.some((r) => r.violations.length > 0);
532
+ const hasFailures = results.some((r) =>
533
+ r.violations.some((v) => v.severity !== "warning"),
534
+ );
486
535
 
487
536
  if (outputFormat === "pretty") {
488
537
  if (results.length === 0) {
489
538
  logger.info("No vendored specs found.");
490
539
  } else {
491
540
  for (const result of results) {
492
- if (result.violations.length === 0) {
493
- logger.success(`spec.validate: ${result.id} — pass`);
541
+ const errors = result.violations.filter((v) => v.severity !== "warning");
542
+ const warnings = result.violations.filter((v) => v.severity === "warning");
543
+ if (errors.length === 0) {
544
+ logger.success(`spec.validate: ${result.id} — pass${warnings.length > 0 ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""}`);
494
545
  } else {
495
- logger.error(`spec.validate: ${result.id} — ${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}`);
496
- for (const v of result.violations) {
497
- logger.error(` ${v.rule}: ${v.message}`);
498
- }
546
+ logger.error(`spec.validate: ${result.id} — ${errors.length} violation${errors.length === 1 ? "" : "s"}`);
547
+ }
548
+ for (const v of errors) {
549
+ logger.error(` ${v.rule}: ${v.message}`);
550
+ }
551
+ for (const v of warnings) {
552
+ logger.warn(` ${v.rule}: ${v.message}`);
499
553
  }
500
554
  }
501
555
  }
@@ -509,7 +563,7 @@ export async function runSpecValidate(
509
563
  },
510
564
  exitCode: hasFailures ? 1 : 0,
511
565
  summary: hasFailures
512
- ? `spec.validate: ${results.filter((r) => r.violations.length > 0).length} spec(s) with violations`
566
+ ? `spec.validate: ${results.filter((r) => r.violations.some((v) => v.severity !== "warning")).length} spec(s) with violations`
513
567
  : `spec.validate: all ${results.length} spec(s) pass`,
514
568
  };
515
569
  }