@warpgogol/forge 5.3.4 → 6.0.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 (235) 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 +39 -9
  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 +326 -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 +93 -5
  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/exploration/exploration.module.d.ts.map +1 -1
  30. package/dist/os/exploration/exploration.module.js +3 -0
  31. package/dist/os/exploration/exploration.module.js.map +1 -1
  32. package/dist/os/naming/naming.module.d.ts.map +1 -1
  33. package/dist/os/naming/naming.module.js +9 -2
  34. package/dist/os/naming/naming.module.js.map +1 -1
  35. package/dist/os/notes/notes.module.d.ts.map +1 -1
  36. package/dist/os/notes/notes.module.js +17 -6
  37. package/dist/os/notes/notes.module.js.map +1 -1
  38. package/dist/os/plan/plan.module.d.ts.map +1 -1
  39. package/dist/os/plan/plan.module.js +2 -0
  40. package/dist/os/plan/plan.module.js.map +1 -1
  41. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  42. package/dist/os/plugin/plugin.module.js +9 -2
  43. package/dist/os/plugin/plugin.module.js.map +1 -1
  44. package/dist/os/program/program.module.d.ts.map +1 -1
  45. package/dist/os/program/program.module.js +19 -4
  46. package/dist/os/program/program.module.js.map +1 -1
  47. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  48. package/dist/os/queue/handlers/queue-validate.js +154 -7
  49. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  50. package/dist/os/queue/manifest.d.ts +17 -1
  51. package/dist/os/queue/manifest.d.ts.map +1 -1
  52. package/dist/os/queue/manifest.js +127 -6
  53. package/dist/os/queue/manifest.js.map +1 -1
  54. package/dist/os/queue/queue.module.d.ts.map +1 -1
  55. package/dist/os/queue/queue.module.js +32 -4
  56. package/dist/os/queue/queue.module.js.map +1 -1
  57. package/dist/os/queue/types.d.ts +100 -1
  58. package/dist/os/queue/types.d.ts.map +1 -1
  59. package/dist/os/queue/types.js +44 -0
  60. package/dist/os/queue/types.js.map +1 -1
  61. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  62. package/dist/os/rfc/handlers/implement-stamp.js +28 -5
  63. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  64. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  65. package/dist/os/rfc/rfc.module.js +27 -3
  66. package/dist/os/rfc/rfc.module.js.map +1 -1
  67. package/dist/os/rfc/types.d.ts +11 -0
  68. package/dist/os/rfc/types.d.ts.map +1 -1
  69. package/dist/os/rfc/types.js +1 -0
  70. package/dist/os/rfc/types.js.map +1 -1
  71. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  72. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  73. package/dist/os/rfc/verification-evidence.js +140 -23
  74. package/dist/os/rfc/verification-evidence.js.map +1 -1
  75. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  76. package/dist/os/rfc/verification-refresh.js +72 -37
  77. package/dist/os/rfc/verification-refresh.js.map +1 -1
  78. package/dist/os/session/handlers/save.d.ts.map +1 -1
  79. package/dist/os/session/handlers/save.js +46 -5
  80. package/dist/os/session/handlers/save.js.map +1 -1
  81. package/dist/os/session/session.module.d.ts.map +1 -1
  82. package/dist/os/session/session.module.js +5 -0
  83. package/dist/os/session/session.module.js.map +1 -1
  84. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  85. package/dist/os/spec/live-spec-merge.js +55 -134
  86. package/dist/os/spec/live-spec-merge.js.map +1 -1
  87. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  88. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  89. package/dist/os/spec/live-spec-rebuild.js +272 -0
  90. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  91. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  92. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  93. package/dist/os/spec/live-spec-shared.js +315 -0
  94. package/dist/os/spec/live-spec-shared.js.map +1 -0
  95. package/dist/os/spec/live-spec-types.d.ts +17 -1
  96. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  97. package/dist/os/spec/live-spec-types.js +3 -0
  98. package/dist/os/spec/live-spec-types.js.map +1 -1
  99. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  100. package/dist/os/spec/live-spec-validate.js +105 -2
  101. package/dist/os/spec/live-spec-validate.js.map +1 -1
  102. package/dist/os/spec/spec-materialize.d.ts +2 -0
  103. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  104. package/dist/os/spec/spec-materialize.js +45 -5
  105. package/dist/os/spec/spec-materialize.js.map +1 -1
  106. package/dist/os/spec/spec-schema.d.ts +19 -0
  107. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  108. package/dist/os/spec/spec-schema.js +30 -0
  109. package/dist/os/spec/spec-schema.js.map +1 -1
  110. package/dist/os/spec/spec-status.d.ts +4 -0
  111. package/dist/os/spec/spec-status.d.ts.map +1 -1
  112. package/dist/os/spec/spec-status.js +19 -1
  113. package/dist/os/spec/spec-status.js.map +1 -1
  114. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  115. package/dist/os/spec/spec-validate.js +55 -8
  116. package/dist/os/spec/spec-validate.js.map +1 -1
  117. package/dist/os/spec/spec.module.d.ts.map +1 -1
  118. package/dist/os/spec/spec.module.js +84 -8
  119. package/dist/os/spec/spec.module.js.map +1 -1
  120. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  121. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  122. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  123. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  124. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  125. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  126. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  127. package/dist/os/workflow/workflow.module.js +13 -5
  128. package/dist/os/workflow/workflow.module.js.map +1 -1
  129. package/dist/src/cli-flags.d.ts.map +1 -1
  130. package/dist/src/cli-flags.js +25 -5
  131. package/dist/src/cli-flags.js.map +1 -1
  132. package/dist/src/config/forge-config.d.ts +3 -0
  133. package/dist/src/config/forge-config.d.ts.map +1 -1
  134. package/dist/src/config/forge-config.js +2 -0
  135. package/dist/src/config/forge-config.js.map +1 -1
  136. package/dist/src/onboarding/init.d.ts.map +1 -1
  137. package/dist/src/onboarding/init.js +8 -3
  138. package/dist/src/onboarding/init.js.map +1 -1
  139. package/dist/src/pipeline-status.d.ts +14 -1
  140. package/dist/src/pipeline-status.d.ts.map +1 -1
  141. package/dist/src/pipeline-status.js +28 -8
  142. package/dist/src/pipeline-status.js.map +1 -1
  143. package/dist/src/types.d.ts +7 -1
  144. package/dist/src/types.d.ts.map +1 -1
  145. package/dist/src/types.js +6 -0
  146. package/dist/src/types.js.map +1 -1
  147. package/dist/src/utils/generated-marker.d.ts +8 -0
  148. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  149. package/dist/src/utils/generated-marker.js +25 -0
  150. package/dist/src/utils/generated-marker.js.map +1 -1
  151. package/os/adr/adr.module.ts +2 -0
  152. package/os/audit/audit.module.ts +2 -0
  153. package/os/compass/compass.module.ts +49 -15
  154. package/os/compass/handlers/compass-audit-handler.ts +50 -9
  155. package/os/compass/handlers/compass-docs-validate.ts +359 -0
  156. package/os/compass/handlers/summary-record.ts +104 -5
  157. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  158. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  159. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  160. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  161. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  162. package/os/core/core.module.ts +147 -75
  163. package/os/core/docs-archive.test.ts +136 -0
  164. package/os/exploration/exploration.module.ts +3 -0
  165. package/os/naming/naming.module.ts +9 -2
  166. package/os/notes/notes.module.ts +17 -6
  167. package/os/plan/plan-0000-template.md +150 -0
  168. package/os/plan/plan.module.ts +2 -0
  169. package/os/plugin/plugin.module.ts +9 -2
  170. package/os/program/program.module.ts +19 -4
  171. package/os/queue/decision-ledger.test.ts +163 -0
  172. package/os/queue/handlers/queue-validate.ts +190 -8
  173. package/os/queue/manifest.ts +201 -7
  174. package/os/queue/queue-validate.test.ts +303 -0
  175. package/os/queue/queue.module.ts +32 -4
  176. package/os/queue/types.ts +56 -1
  177. package/os/rfc/handlers/implement-stamp.ts +35 -5
  178. package/os/rfc/rfc.module.ts +29 -3
  179. package/os/rfc/types.ts +17 -1
  180. package/os/rfc/verification-emit.test.ts +305 -0
  181. package/os/rfc/verification-evidence.ts +155 -24
  182. package/os/rfc/verification-refresh.test.ts +222 -1
  183. package/os/rfc/verification-refresh.ts +80 -38
  184. package/os/session/handlers/save.ts +41 -5
  185. package/os/session/session.module.ts +5 -0
  186. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  187. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  188. package/os/spec/live-spec-merge.test.ts +187 -7
  189. package/os/spec/live-spec-merge.ts +71 -155
  190. package/os/spec/live-spec-rebuild.test.ts +317 -0
  191. package/os/spec/live-spec-rebuild.ts +356 -0
  192. package/os/spec/live-spec-shared.ts +378 -0
  193. package/os/spec/live-spec-types.ts +21 -1
  194. package/os/spec/live-spec-validate.test.ts +317 -0
  195. package/os/spec/live-spec-validate.ts +106 -2
  196. package/os/spec/spec-materialize.test.ts +192 -0
  197. package/os/spec/spec-materialize.ts +48 -4
  198. package/os/spec/spec-schema.ts +49 -0
  199. package/os/spec/spec-status.ts +24 -0
  200. package/os/spec/spec-validate.test.ts +66 -0
  201. package/os/spec/spec-validate.ts +62 -8
  202. package/os/spec/spec.module.ts +85 -8
  203. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  204. package/os/werkstatt/werkstatt.module.ts +13 -4
  205. package/os/workflow/workflow.module.ts +13 -5
  206. package/package.json +1 -1
  207. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  208. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  209. package/skills/fo/fo-explore/SKILL.md +2 -1
  210. package/skills/fo/fo-fix/SKILL.md +11 -3
  211. package/skills/fo/fo-handoff/SKILL.md +4 -6
  212. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  213. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  214. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  215. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  216. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  217. package/skills/fo/fo-review/SKILL.md +69 -8
  218. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  219. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  220. package/skills/shared/grilling/SKILL.md +12 -0
  221. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  222. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  223. package/src/cli-flags.ts +23 -5
  224. package/src/config/forge-config.ts +4 -0
  225. package/src/onboarding/init.ts +8 -3
  226. package/src/pipeline-status.ts +32 -8
  227. package/src/tests/cli-flags.test.ts +20 -0
  228. package/src/tests/cli-output.test.ts +13 -7
  229. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  230. package/src/tests/implement-stamp.test.ts +38 -0
  231. package/src/tests/session-handlers.test.ts +33 -0
  232. package/src/types.ts +13 -1
  233. package/src/utils/generated-marker.ts +28 -0
  234. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  235. package/skills/fo/fo-review/AXES.md +0 -70
@@ -28,11 +28,13 @@ Sweep batch 4: 73 Compass headers on headerless engine files (certification, com
28
28
 
29
29
  compass-migrate-handler hint used a consumer-specific run command — switched to generic 'pnpm exec forge run' convention. Reworded recorded CHANGE_SUMMARY items in 3 handlers to drop the consumer-specific literal. compass-policy AC-4 test green (65/65).</item>
30
30
  <item>RFC-1220: step 2 — fail-closed guards in history paths</item>
31
+ <item>RFC-1233: stripGitTrailers at the record point + sanitizeItemText star-slash guard — commit-message trailers (Token: value / Token #value, plus markdown-link attribution lines like "Generated with [X](url)") no longer pollute injected items.</item>
32
+ <item>Session-retro 2026-10-08: id-in-diff dedup — auto-inject skips when the worktree diff already carries a same-ID item (HEAD-vs-worktree via git show HEAD:path), closing the descriptive+generic duplicate class seen on RFC-1237.</item>
31
33
  <history>RFC-1095</history>
32
34
  </CHANGE_SUMMARY>
33
35
  */
34
36
 
35
- import { existsSync } from "../../../src/utils/sync-fs.ts";
37
+ import { execFile, existsSync } from "../../../src/utils/sync-fs.ts";
36
38
  import { ambientIo, resolveIo } from "../../../src/utils/io.ts";
37
39
 
38
40
  import { resolve, relative } from "node:path";
@@ -69,7 +71,7 @@ export interface SummaryRecordInput {
69
71
  workpiece?: string;
70
72
  }
71
73
 
72
- export type SummaryRecordSkipReason = "no-block" | "duplicate" | "unparseable";
74
+ export type SummaryRecordSkipReason = "no-block" | "duplicate" | "unparseable" | "id-in-diff";
73
75
 
74
76
  export interface SummaryRecordResult {
75
77
  command: "compass.summary.record";
@@ -167,6 +169,41 @@ export function stripConventionalPrefix(subject: string): string {
167
169
  return subject.replace(CONVENTIONAL_PREFIX_RE, "").trim();
168
170
  }
169
171
 
172
+ // RFC-1233: git-trailer tail block — strict `Token: value` / `Token #value`
173
+ // lines (interpret-trailers shape, empty value allowed), plus lines ending
174
+ // in a markdown link (the observed "Generated with [X](url)" attribution
175
+ // boilerplate has no separator). A link-shaped line is admitted only when
176
+ // the block also contains at least one strict trailer line — a lone link
177
+ // tail is prose.
178
+ const TRAILER_LINE_RE = /^[A-Za-z0-9-]+(:| #)\s*.*$/;
179
+ const MARKDOWN_LINK_TAIL_RE = /^.*\[[^\]]*\]\([^)]+\)\s*$/;
180
+
181
+ /**
182
+ * Remove a trailing git-trailer block: the contiguous tail of strict
183
+ * trailer lines and admitted markdown-link lines. A blank line ends the
184
+ * tail paragraph. Returns the message without the block; when the tail
185
+ * holds no strict trailer line the message is returned unchanged.
186
+ */
187
+ export function stripGitTrailers(message: string): string {
188
+ const lines = message.trimEnd().split("\n");
189
+ let start = lines.length;
190
+ let sawStrict = false;
191
+ while (start > 0) {
192
+ const line = lines[start - 1]!.trim();
193
+ if (line === "") break;
194
+ if (TRAILER_LINE_RE.test(line)) {
195
+ sawStrict = true;
196
+ start--;
197
+ } else if (MARKDOWN_LINK_TAIL_RE.test(line)) {
198
+ start--;
199
+ } else {
200
+ break;
201
+ }
202
+ }
203
+ if (!sawStrict || start === lines.length) return message;
204
+ return lines.slice(0, start).join("\n").trimEnd();
205
+ }
206
+
170
207
  // Literal Compass block tags inside an item corrupt block parsing — a commit
171
208
  // message saying "collapsed into history" would be matched by HISTORY_RE and
172
209
  // produce phantom non-ID tokens (CS-07). Strip the angle brackets on record.
@@ -174,7 +211,14 @@ const COMPASS_TAG_LITERAL_RE =
174
211
  /<\/?(?:CHANGE_SUMMARY|MODULE_CONTRACT|KEY_DECISIONS|history|item|purpose|non-goals)>/g;
175
212
 
176
213
  export function sanitizeItemText(text: string): string {
177
- return text.replace(COMPASS_TAG_LITERAL_RE, (tag) => tag.replace(/[<>]/g, ""));
214
+ return (
215
+ text
216
+ .replace(COMPASS_TAG_LITERAL_RE, (tag) => tag.replace(/[<>]/g, ""))
217
+ // RFC-1233: a literal star-slash sequence would terminate the
218
+ // surrounding block comment when the item lands in /* ... */ headers.
219
+ // ∕ (U+2215) is visually near-identical, idempotent, and never re-forms.
220
+ .replace(/\*\//g, "*\u2215")
221
+ );
178
222
  }
179
223
 
180
224
  export function isValidGovernanceId(id: string, policy: CompassPolicy): boolean {
@@ -190,6 +234,8 @@ interface RecordOutcome {
190
234
  /**
191
235
  * Append `ID: text` to one file's CHANGE_SUMMARY and collapse the window.
192
236
  * Returns the outcome; writes the file only when mutated and not dry-run.
237
+ * `headSource` is the file's content at HEAD (when resolvable) — it enables
238
+ * the id-in-diff dedup; pass undefined outside git workspaces.
193
239
  */
194
240
  export async function recordSummaryItem(
195
241
  absPath: string,
@@ -198,6 +244,7 @@ export async function recordSummaryItem(
198
244
  text: string,
199
245
  dryRun: boolean,
200
246
  policy: CompassPolicy,
247
+ headSource?: string,
201
248
  ): Promise<RecordOutcome> {
202
249
  const source = await io.readFile(absPath);
203
250
  const blockMatch = source.match(CHANGE_SUMMARY_BLOCK_RE);
@@ -228,6 +275,22 @@ export async function recordSummaryItem(
228
275
  return { recorded: false, collapsed: false, skipReason: "duplicate" };
229
276
  }
230
277
 
278
+ // A same-ID item already in the worktree but absent from HEAD means the
279
+ // pending change already carries a hand-authored record for this governance
280
+ // ID — the auto-injected subject-derived item would duplicate it at lower
281
+ // specificity (observed 2026-10-08: 27 files carried descriptive + generic
282
+ // RFC-1237 items). HEAD-resident same-ID items do NOT suppress: consecutive
283
+ // commits under one RFC legitimately stack step items.
284
+ if (headSource !== undefined) {
285
+ const headBlock = headSource.match(CHANGE_SUMMARY_BLOCK_RE);
286
+ const headItems = headBlock ? parseChangeSummary(headBlock[0]).items : [];
287
+ const idInWorktree = items.some((item) => leadingGovernanceId(item, policy) === id);
288
+ const idInHead = headItems.some((item) => leadingGovernanceId(item, policy) === id);
289
+ if (idInWorktree && !idInHead) {
290
+ return { recorded: false, collapsed: false, skipReason: "id-in-diff" };
291
+ }
292
+ }
293
+
231
294
  const nextItems = [...items, newItem];
232
295
  const collapsedIds: string[] = [];
233
296
  while (nextItems.length > CHANGE_SUMMARY_WINDOW) {
@@ -246,6 +309,27 @@ export async function recordSummaryItem(
246
309
  return { recorded: true, collapsed };
247
310
  }
248
311
 
312
+ function execGit(cwd: string, args: string[]): Promise<string | null> {
313
+ return new Promise((resolve) => {
314
+ execFile("git", args, { cwd, timeout: 5000 }, (err, stdout) => {
315
+ resolve(err ? null : stdout);
316
+ });
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Read `path`'s content at HEAD — the baseline for the id-in-diff dedup.
322
+ * Returns undefined when the file is untracked or the workspace is not a git
323
+ * repo (the dedup then falls back to exact-text matching only).
324
+ */
325
+ async function readHeadSource(gitTop: string | null, absPath: string): Promise<string | undefined> {
326
+ if (!gitTop) return undefined;
327
+ const rel = relative(gitTop, absPath).replace(/\\/g, "/");
328
+ if (rel === "" || rel.startsWith("..")) return undefined;
329
+ const source = await execGit(gitTop, ["show", `HEAD:${rel}`]);
330
+ return source ?? undefined;
331
+ }
332
+
249
333
  function riskReminderNeeded(relPath: string, source: string, policy: CompassPolicy): boolean {
250
334
  const segments = relPath.split("/").filter(Boolean);
251
335
  const workspaceRel = getWorkspaceRelativeSegments(segments, policy).join("/");
@@ -278,7 +362,10 @@ export async function runCompassSummaryRecord(
278
362
  : [];
279
363
 
280
364
  const rawText = input.flags["text"];
281
- const text = typeof rawText === "string" && rawText.trim().length > 0 ? rawText.trim() : id;
365
+ // RFC-1233: strip the git-trailer block at the single injection point —
366
+ // ecosystem.commit and mission.git.commit both pipe the raw message here.
367
+ const stripped = typeof rawText === "string" ? stripGitTrailers(rawText).trim() : "";
368
+ const text = stripped.length > 0 ? stripped : id;
282
369
 
283
370
  const result: SummaryRecordResult = {
284
371
  command: "compass.summary.record",
@@ -289,6 +376,9 @@ export async function runCompassSummaryRecord(
289
376
  keyDecisionsReminders: [],
290
377
  };
291
378
 
379
+ const gitTopRaw = await execGit(baseRoot, ["rev-parse", "--show-toplevel"]);
380
+ const gitTop = gitTopRaw?.trim() || null;
381
+
292
382
  for (const file of files) {
293
383
  const absPath = resolve(baseRoot, file);
294
384
  const relPath = relative(baseRoot, absPath).replace(/\\/g, "/");
@@ -298,7 +388,16 @@ export async function runCompassSummaryRecord(
298
388
  continue;
299
389
  }
300
390
  try {
301
- const outcome = await recordSummaryItem(absPath, relPath, id, text, context.dryRun, policy);
391
+ const headSource = await readHeadSource(gitTop, absPath);
392
+ const outcome = await recordSummaryItem(
393
+ absPath,
394
+ relPath,
395
+ id,
396
+ text,
397
+ context.dryRun,
398
+ policy,
399
+ headSource,
400
+ );
302
401
  if (outcome.skipReason) {
303
402
  result.skipped.push({ file, reason: outcome.skipReason });
304
403
  continue;
@@ -16,6 +16,7 @@ import { execFile } from "node:child_process";
16
16
  import { promisify } from "node:util";
17
17
  import { parse as yamlParse } from "yaml";
18
18
  import { runCompassAuditRecord } from "../compass-audit-handler.ts";
19
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
20
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -42,7 +43,7 @@ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
42
43
  logger: logger as never,
43
44
  dryRun: false,
44
45
  outputFormat: "json",
45
- io: {} as never,
46
+ io: ambientIo,
46
47
  actualState: undefined as never,
47
48
  fileIntents: [],
48
49
  } as unknown as ForgeRuntimeContext;
@@ -16,6 +16,7 @@ import { join } from "node:path";
16
16
  import { execFile } from "node:child_process";
17
17
  import { promisify } from "node:util";
18
18
  import { runCompassAuditValidate } from "../compass-audit-handler.ts";
19
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
20
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -31,15 +32,15 @@ const logger = {
31
32
  },
32
33
  };
33
34
 
34
- function makeContext(workspaceRoot: string): ForgeRuntimeContext {
35
+ function makeContext(workspaceRoot: string, dryRun = false): ForgeRuntimeContext {
35
36
  return {
36
37
  workspaceRoot,
37
38
  site: undefined,
38
39
  siteExplicit: false,
39
40
  logger: logger as never,
40
- dryRun: false,
41
+ dryRun,
41
42
  outputFormat: "json",
42
- io: {} as never,
43
+ io: ambientIo,
43
44
  actualState: undefined as never,
44
45
  fileIntents: [],
45
46
  } as unknown as ForgeRuntimeContext;
@@ -131,3 +132,91 @@ describe("compass.audit.validate — RFC-1143 ledger-ineligible skips", () => {
131
132
  expect(data.skippedIneligible).toBeGreaterThan(0);
132
133
  });
133
134
  });
135
+
136
+ // RFC-1242 AC-5/AC-6: --prune drops ledger entries for paths that no longer
137
+ // exist while preserving every surviving entry's verdict fields.
138
+ const DEAD_PATH = "packages/gone/deleted.ts";
139
+ const LEDGER_ENTRY = (path: string) =>
140
+ ` - path: ${path}\n entityId: "eid-${path}"\n auditedRevision: 7\n auditedHash: "hash-${path}"\n auditedAt: "2026-01-01T00:00:00Z"\n verdict: pass\n agent: human:test\n`;
141
+
142
+ describe("compass.audit.validate --prune — RFC-1242", () => {
143
+ let root: string;
144
+
145
+ beforeEach(async () => {
146
+ root = await mkdtemp(join(tmpdir(), "compass-prune-"));
147
+ await mkdir(join(root, "packages", "foo"), { recursive: true });
148
+ await mkdir(join(root, "docs"), { recursive: true });
149
+ await writeFile(join(root, "packages", "foo", "index.ts"), "export const x = 1;\n", "utf8");
150
+ await writeFile(
151
+ join(root, "docs", "compass-audit-ledger.generated.yaml"),
152
+ `entries:\n${LEDGER_ENTRY(ELIGIBLE_FILE)}${LEDGER_ENTRY(DEAD_PATH)}`,
153
+ "utf8",
154
+ );
155
+ await execFileAsync("git", ["init"], { cwd: root });
156
+ await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
157
+ await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
158
+ await execFileAsync("git", ["add", "."], { cwd: root });
159
+ await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
160
+ });
161
+
162
+ afterEach(async () => {
163
+ await rm(root, { recursive: true, force: true });
164
+ });
165
+
166
+ it("AC-5: ledger contains no entry whose path does not exist after prune", async () => {
167
+ const result = await runCompassAuditValidate(
168
+ makeInput({ prune: true, root: ["packages"] }),
169
+ makeContext(root),
170
+ );
171
+ const data = result.data as { pruned: number; prunedPaths: string[] };
172
+ expect(data.pruned).toBe(1);
173
+ expect(data.prunedPaths).toEqual([DEAD_PATH]);
174
+ const { readFile } = await import("node:fs/promises");
175
+ const ledgerText = await readFile(
176
+ join(root, "docs", "compass-audit-ledger.generated.yaml"),
177
+ "utf8",
178
+ );
179
+ expect(ledgerText).not.toContain(DEAD_PATH);
180
+ expect(ledgerText).toContain(ELIGIBLE_FILE);
181
+ });
182
+
183
+ it("AC-6: surviving entries keep their verdict fields unchanged", async () => {
184
+ await runCompassAuditValidate(
185
+ makeInput({ prune: true, root: ["packages"] }),
186
+ makeContext(root),
187
+ );
188
+ const { readFile } = await import("node:fs/promises");
189
+ const { parse } = await import("yaml");
190
+ const ledger = parse(
191
+ await readFile(join(root, "docs", "compass-audit-ledger.generated.yaml"), "utf8"),
192
+ ) as { entries: Array<Record<string, unknown>> };
193
+ const survivor = ledger.entries.find((e) => e.path === ELIGIBLE_FILE);
194
+ expect(
195
+ survivor,
196
+ "surviving ledger entry must keep its verdict fields — check the keep/drop partition",
197
+ ).toMatchObject({
198
+ entityId: `eid-${ELIGIBLE_FILE}`,
199
+ auditedRevision: 7,
200
+ auditedHash: `hash-${ELIGIBLE_FILE}`,
201
+ auditedAt: "2026-01-01T00:00:00Z",
202
+ verdict: "pass",
203
+ agent: "human:test",
204
+ });
205
+ });
206
+
207
+ it("--prune --dry-run reports the drop set without rewriting the ledger", async () => {
208
+ const { readFile } = await import("node:fs/promises");
209
+ const ledgerPath = join(root, "docs", "compass-audit-ledger.generated.yaml");
210
+ const before = await readFile(ledgerPath, "utf8");
211
+ const result = await runCompassAuditValidate(
212
+ makeInput({ prune: true, root: ["packages"] }),
213
+ makeContext(root, true),
214
+ );
215
+ const data = result.data as { pruned: number };
216
+ expect(data.pruned).toBe(1);
217
+ expect(
218
+ await readFile(ledgerPath, "utf8"),
219
+ "dry-run must not rewrite the ledger — writes must route through context.io honoring context.dryRun",
220
+ ).toBe(before);
221
+ });
222
+ });
@@ -0,0 +1,203 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-1242 AC-1..AC-4: compass.docs.validate scans the docs/*.xml corpus and
5
+ must fail on unresolvable <path> entries (COMPASS-DOC-01) and dead workspace
6
+ ids (COMPASS-DOC-02), warn on unresolvable link targets (COMPASS-DOC-03),
7
+ and flag malformed XML (COMPASS-DOC-00). A green corpus exits 0 with
8
+ status-shaped data.
9
+ </purpose>
10
+ </MODULE_CONTRACT>
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
14
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { runCompassDocsValidate } from "../compass-docs-validate.ts";
18
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
+ import type { Diagnostic, ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
+
21
+ const logger = {
22
+ section() {},
23
+ info() {},
24
+ warn() {},
25
+ error() {},
26
+ success() {},
27
+ getEvents() {
28
+ return [];
29
+ },
30
+ };
31
+
32
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
33
+ return {
34
+ workspaceRoot,
35
+ site: undefined,
36
+ siteExplicit: false,
37
+ logger: logger as never,
38
+ dryRun: false,
39
+ outputFormat: "json",
40
+ io: ambientIo,
41
+ actualState: undefined as never,
42
+ fileIntents: [],
43
+ } as unknown as ForgeRuntimeContext;
44
+ }
45
+
46
+ function makeInput(): ForgeCommandInput {
47
+ return { argv: [], flags: {} } as unknown as ForgeCommandInput;
48
+ }
49
+
50
+ const GREEN_DOC = `<root>
51
+ <path>\`docs/other.xml\`</path>
52
+ <workspace id="pkg-foo"><path>\`packages/foo\`</path></workspace>
53
+ <node id="packages.foo" type="package"><path>\`packages/foo\`</path></node>
54
+ <node id="root.a" type="compass-doc"><link rel="binds-to" target="root.b" /></node>
55
+ <node id="root.b" type="compass-doc" />
56
+ </root>
57
+ `;
58
+
59
+ describe("compass.docs.validate — RFC-1242", () => {
60
+ let root: string;
61
+
62
+ beforeEach(async () => {
63
+ root = await mkdtemp(join(tmpdir(), "compass-docs-"));
64
+ await mkdir(join(root, "docs"), { recursive: true });
65
+ await mkdir(join(root, "packages", "foo"), { recursive: true });
66
+ await writeFile(join(root, "packages", "foo", "index.ts"), "export {};\n", "utf8");
67
+ });
68
+
69
+ afterEach(async () => {
70
+ await rm(root, { recursive: true, force: true });
71
+ });
72
+
73
+ it("AC-2: green corpus exits 0 and reports scanned counts", async () => {
74
+ await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
75
+ await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
76
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
77
+ expect(result.exitCode).toBe(0);
78
+ const data = result.data as {
79
+ scanned: { xmlFiles: number; pathsChecked: number; idsChecked: number };
80
+ diagnostics: Diagnostic[];
81
+ };
82
+ expect(data.scanned.xmlFiles).toBe(2);
83
+ expect(data.diagnostics).toEqual([]);
84
+ });
85
+
86
+ it("AC-3: dead <path> emits COMPASS-DOC-01 error", async () => {
87
+ await writeFile(
88
+ join(root, "docs", "a.xml"),
89
+ `<root><path>\`packages/gone\`</path></root>`,
90
+ "utf8",
91
+ );
92
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
93
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
94
+ expect(
95
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01" && d.severity === "error"),
96
+ "dead <path> must emit COMPASS-DOC-01 — check pathTokens/pathCheckTarget resolution",
97
+ ).toBe(true);
98
+ });
99
+
100
+ it("AC-4: dead <path> produces a non-zero exit", async () => {
101
+ await writeFile(
102
+ join(root, "docs", "a.xml"),
103
+ `<root><path>\`packages/gone\`</path></root>`,
104
+ "utf8",
105
+ );
106
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
107
+ expect(result.exitCode).toBe(1);
108
+ });
109
+
110
+ it("dead workspace id emits COMPASS-DOC-02 error", async () => {
111
+ await writeFile(join(root, "docs", "a.xml"), `<root><workspace id="pkg-gone"/></root>`, "utf8");
112
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
113
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
114
+ expect(
115
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-02" && d.message.includes("pkg-gone")),
116
+ "dead workspace id must emit COMPASS-DOC-02 — check workspaceIdTarget mapping",
117
+ ).toBe(true);
118
+ expect(result.exitCode).toBe(1);
119
+ });
120
+
121
+ it("glob-tail <path> resolves against the parent directory", async () => {
122
+ await writeFile(
123
+ join(root, "docs", "a.xml"),
124
+ `<root><path>\`packages/*\`</path></root>`,
125
+ "utf8",
126
+ );
127
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
128
+ expect(result.exitCode).toBe(0);
129
+ });
130
+
131
+ it("unresolvable link target emits COMPASS-DOC-03 warning, never an error", async () => {
132
+ await writeFile(
133
+ join(root, "docs", "a.xml"),
134
+ `<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-9999" /></node></root>`,
135
+ "utf8",
136
+ );
137
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
138
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
139
+ expect(
140
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03" && d.severity === "warning"),
141
+ "dangling link target must warn via COMPASS-DOC-03",
142
+ ).toBe(true);
143
+ expect(result.exitCode).toBe(0);
144
+ });
145
+
146
+ it("link target resolving to an archived RFC file passes", async () => {
147
+ await mkdir(join(root, "docs", "rfcs", "archive"), { recursive: true });
148
+ await writeFile(join(root, "docs", "rfcs", "archive", "rfc-0097-old.md"), "x", "utf8");
149
+ await writeFile(
150
+ join(root, "docs", "a.xml"),
151
+ `<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-0097" /></node></root>`,
152
+ "utf8",
153
+ );
154
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
155
+ expect(result.exitCode).toBe(0);
156
+ });
157
+
158
+ it("malformed XML emits COMPASS-DOC-00 and skips ref checks for that file", async () => {
159
+ await writeFile(join(root, "docs", "a.xml"), `<root><name>oops</root>`, "utf8");
160
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
161
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
162
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(true);
163
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01")).toBe(false);
164
+ expect(result.exitCode).toBe(1);
165
+ });
166
+
167
+ it("quoted attribute value ending in / is not misread as self-closing", async () => {
168
+ await writeFile(
169
+ join(root, "docs", "a.xml"),
170
+ `<root><node id="x" type="compass-doc" path="docs/"><link rel="binds-to" target="x" /></node></root>`,
171
+ "utf8",
172
+ );
173
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
174
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
175
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(false);
176
+ expect(result.exitCode).toBe(0);
177
+ });
178
+
179
+ it("docs.plans.plan-rfc-* link target resolves to a plan-*.md basename", async () => {
180
+ await mkdir(join(root, "docs", "plans"), { recursive: true });
181
+ await writeFile(join(root, "docs", "plans", "plan-rfc-1242-x.md"), "x", "utf8");
182
+ await writeFile(
183
+ join(root, "docs", "a.xml"),
184
+ `<root><node id="x"><link rel="binds-to" target="docs.plans.plan-rfc-1242-x" /></node></root>`,
185
+ "utf8",
186
+ );
187
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
188
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
189
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03")).toBe(false);
190
+ });
191
+
192
+ it("AC-1: result data carries command-shaped payload fields", async () => {
193
+ await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
194
+ await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
195
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
196
+ const data = result.data as Record<string, unknown>;
197
+ expect(data).toHaveProperty("command", "compass.docs.validate");
198
+ expect(data).toHaveProperty("status", "pass");
199
+ expect(data).toHaveProperty("scanned");
200
+ expect(data).toHaveProperty("diagnostics");
201
+ expect(Array.isArray(data.diagnostics)).toBe(true);
202
+ });
203
+ });
@@ -16,6 +16,7 @@ import { execFile } from "node:child_process";
16
16
  import { promisify } from "node:util";
17
17
  import { parse as yamlParse } from "yaml";
18
18
  import { runCompassAuditBaseline } from "../compass-audit-handler.ts";
19
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
20
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -39,7 +40,7 @@ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
39
40
  logger: logger as never,
40
41
  dryRun: false,
41
42
  outputFormat: "json",
42
- io: {} as never,
43
+ io: ambientIo,
43
44
  actualState: undefined as never,
44
45
  fileIntents: [],
45
46
  } as unknown as ForgeRuntimeContext;