@event4u/agent-config 14.13.0 → 14.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +138 -376
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/analyze/inbox.md +36 -0
  7. package/dist/agent-src/commands/pr/create.md +16 -1
  8. package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
  9. package/dist/agent-src/rules/code-comment-discipline.md +6 -0
  10. package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
  11. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  12. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  13. package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
  14. package/dist/agent-src/scripts/stubs_due.ts +118 -2
  15. package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/cli/registry.js +1 -0
  18. package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
  19. package/dist/cli-delegate/cmd_conformance.js +39 -2
  20. package/dist/cli-delegate/cmd_doctor.js +1 -1
  21. package/dist/discovery/deprecation-report.md +1 -1
  22. package/dist/discovery/discovery-manifest.json +7 -7
  23. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  24. package/dist/discovery/discovery-manifest.summary.md +1 -1
  25. package/dist/discovery/dormancy-report.md +1 -1
  26. package/dist/discovery/orphan-report.md +1 -1
  27. package/dist/discovery/packs.json +2 -2
  28. package/dist/discovery/trust-report.md +1 -1
  29. package/dist/discovery/workspaces.json +2 -2
  30. package/dist/hooks/dispatch.js +1696 -812
  31. package/dist/mcp/registry-manifest.json +2 -2
  32. package/dist/mcp/server.json +2 -2
  33. package/dist/mcp/server.mjs +297 -115
  34. package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
  35. package/docs/guidelines/code-clarity.md +18 -0
  36. package/package.json +1 -1
  37. package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  38. package/src/agent-src/scripts/guarded_baseline.ts +22 -7
  39. package/src/agent-src/scripts/stubs_due.ts +118 -2
  40. package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
  41. package/src/config/assurance-capability-registry.json +1 -1
  42. package/src/config/estate-count-budget.json +20 -1
  43. package/src/config/evaluator-budgets.json +4 -4
  44. package/src/config/gate-coverage.yml +211 -1
  45. package/src/config/gate-reachability-exemptions.json +1 -0
  46. package/src/config/gate-violation-baselines.json +9 -3
  47. package/src/config/preamble-payload-budget.json +19 -11
  48. package/src/config/rule-stub-ceilings.json +14 -7
  49. package/src/config/web-launch-readiness.json +4 -2
  50. package/src/scripts/_cli/_doctor_emit.ts +71 -0
  51. package/src/scripts/_cli/_doctor_strict.ts +163 -0
  52. package/src/scripts/_cli/cmd_doctor.ts +16 -37
  53. package/src/scripts/_dispatch.bash +11 -4
  54. package/src/scripts/_lib/activation_ladder.ts +47 -3
  55. package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
  56. package/src/scripts/_lib/adr_frontmatter.ts +26 -0
  57. package/src/scripts/_lib/branch_convergence.ts +186 -0
  58. package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
  59. package/src/scripts/_lib/carrier_status.ts +118 -0
  60. package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
  61. package/src/scripts/_lib/context_observation.ts +132 -0
  62. package/src/scripts/_lib/continuation_ladder.ts +188 -0
  63. package/src/scripts/_lib/corpus_manifest.ts +726 -0
  64. package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
  65. package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
  66. package/src/scripts/_lib/evaluation_cascade.ts +452 -0
  67. package/src/scripts/_lib/evolution_roi.ts +562 -0
  68. package/src/scripts/_lib/experiment_binding.ts +95 -0
  69. package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
  70. package/src/scripts/_lib/ignored_blocker.ts +8 -1
  71. package/src/scripts/_lib/lexical_shortlist.ts +215 -0
  72. package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
  73. package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
  74. package/src/scripts/_lib/outcome_envelope.ts +6 -1
  75. package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
  76. package/src/scripts/_lib/pathology_archive.ts +393 -0
  77. package/src/scripts/_lib/promotion_capability.ts +315 -0
  78. package/src/scripts/_lib/promotion_evidence.ts +414 -0
  79. package/src/scripts/_lib/promotion_review.ts +294 -0
  80. package/src/scripts/_lib/prompt_shape.ts +92 -0
  81. package/src/scripts/_lib/release_highlights.ts +79 -1
  82. package/src/scripts/_lib/release_material.ts +125 -2
  83. package/src/scripts/_lib/repeated_failure.ts +19 -2
  84. package/src/scripts/_lib/routing_corpus.ts +307 -0
  85. package/src/scripts/_lib/routing_index_input.ts +104 -0
  86. package/src/scripts/_lib/rule_injection.ts +22 -1
  87. package/src/scripts/_lib/runtime_journal.ts +11 -4
  88. package/src/scripts/_lib/semantic_noop.ts +167 -0
  89. package/src/scripts/activation_receipt.ts +258 -0
  90. package/src/scripts/ai_council/cross_exam.ts +128 -0
  91. package/src/scripts/ai_council/explain_route.ts +157 -0
  92. package/src/scripts/ai_council/information_gain.ts +293 -0
  93. package/src/scripts/ai_council/leakage_corpus.ts +385 -0
  94. package/src/scripts/ai_council/leakage_patterns.ts +453 -0
  95. package/src/scripts/ai_council/minority_retention.ts +171 -0
  96. package/src/scripts/ai_council/recouncil_savings.ts +299 -0
  97. package/src/scripts/ai_council/replay_route.ts +223 -0
  98. package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
  99. package/src/scripts/ai_council/routing_training_row.ts +197 -0
  100. package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
  101. package/src/scripts/check_adr_frontmatter.ts +4 -9
  102. package/src/scripts/check_agent_artifact_location.ts +337 -0
  103. package/src/scripts/check_artefact_count_messaging.ts +74 -8
  104. package/src/scripts/check_enforcement_coverage.ts +40 -1
  105. package/src/scripts/check_estate_count.ts +205 -24
  106. package/src/scripts/check_release_highlights.ts +113 -14
  107. package/src/scripts/check_roadmap_trackable.ts +17 -2
  108. package/src/scripts/check_web_launch_readiness.ts +310 -20
  109. package/src/scripts/corpus_manifest.ts +235 -0
  110. package/src/scripts/evolution_lab.ts +234 -9
  111. package/src/scripts/generate_capability_matrix.ts +16 -9
  112. package/src/scripts/grade_target_readiness.ts +221 -2
  113. package/src/scripts/hook_effect_doctor.ts +47 -4
  114. package/src/scripts/hook_manifest.json +1 -1
  115. package/src/scripts/hook_manifest.yaml +28 -6
  116. package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
  117. package/src/scripts/hooks/concern_registry.ts +2 -0
  118. package/src/scripts/hooks/run_continuation_hook.ts +78 -88
  119. package/src/scripts/hooks/session_eol_hook.ts +12 -1
  120. package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
  121. package/src/scripts/install-hooks.sh +18 -0
  122. package/src/scripts/language_mirror_hook.ts +14 -2
  123. package/src/scripts/lint_carrier_integrity.ts +820 -0
  124. package/src/scripts/lint_code_comments.ts +748 -0
  125. package/src/scripts/lint_plan_risk_register.ts +39 -19
  126. package/src/scripts/lint_promotion_paths.ts +779 -0
  127. package/src/scripts/llm_propose.ts +256 -0
  128. package/src/scripts/measure_delivery_sets.ts +361 -0
  129. package/src/scripts/measure_routing_signal.ts +307 -0
  130. package/src/scripts/model_rule_injection.ts +19 -3
  131. package/src/scripts/probe_council_retention.ts +495 -0
  132. package/src/scripts/release.ts +21 -37
  133. package/src/scripts/release_drill.ts +199 -0
  134. package/src/scripts/release_env.ts +35 -0
  135. package/src/scripts/release_publication.ts +142 -0
  136. package/src/scripts/roadmap_context.ts +15 -0
  137. package/src/scripts/skill_scout.ts +633 -0
  138. package/src/scripts/sync_pr_branch.ts +521 -42
@@ -0,0 +1,820 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Standing validator for deferral carries whose parent is already archived.
4
+ *
5
+ * A `[~]` step carried to a follow-up roadmap is checked once, at the moment
6
+ * the parent is archived, and never again. The check that performs it runs
7
+ * inside a sweep over active roadmaps, so once the parent moves under
8
+ * `archive/` neither end of the pair is ever looked at together again. The
9
+ * receiver can then be deleted, renamed, or stripped of its back-link and
10
+ * nothing goes red.
11
+ *
12
+ * This gate closes that by walking from the ARCHIVED side. An archived roadmap
13
+ * is a durable record: nothing rewrites it, and it names its destination by
14
+ * slug. So the expected set of live receivers is derivable at any time without
15
+ * a registry to maintain, and whole-file deletion is detected on the first
16
+ * pass — which a validator enumerating only the surviving receivers cannot do.
17
+ *
18
+ * Annotation parsing is delegated to `parseDeferredItems`, the same function the
19
+ * archival sweep uses, so the syntax has one implementation and cannot drift.
20
+ * The RESOLUTION POLICY is deliberately not shared, and the reason is temporal
21
+ * rather than accidental. At archival time a destination under `archive/` is a
22
+ * refusal: carrying an item into an already-dead roadmap loses it on arrival.
23
+ * Afterwards, a destination that completed its own work and archived is the
24
+ * SUCCESS case, not a loss. Sharing that branch would red two carries in this
25
+ * repository whose receivers did exactly what they were asked to do — measured,
26
+ * not predicted: both were reported on the first run against real data.
27
+ *
28
+ * So the standing policy is: the destination must still exist somewhere (a
29
+ * missing file is the deletion this gate is for), it must still back-link, and
30
+ * if it is archived it must carry no OPEN steps. An archived roadmap with open
31
+ * steps was moved by hand and its obligations are stranded. A destination under
32
+ * `skipped/` is red in either policy — skipping is not fulfilment.
33
+ *
34
+ * Unsupported transitions fail closed rather than being inferred. A rename
35
+ * breaks destination resolution. A re-parent breaks the back-link. An onward
36
+ * carry that removes the intermediate breaks the original parent's link. All
37
+ * three are red here, deliberately, until a disposition vocabulary exists to
38
+ * express them.
39
+ *
40
+ * A SECOND ENUMERATION runs beside the first, over the live carriers rather
41
+ * than over the archived parents, and it exists because the walk direction that
42
+ * makes whole-file deletion detectable also makes a self-declared carrier
43
+ * invisible. `status: carrier` buys exclusion from the active count, from
44
+ * trackability and from the plan risk register; enumerating only archived
45
+ * parents means a file nobody carried anything to wears the status for free.
46
+ * So every live carrier must be named by some dead roadmap's `carried-to=`
47
+ * annotation, and one that is not is a hard failure like any other lost
48
+ * obligation — the status is legitimate for a receiver of a real carry, and for
49
+ * nothing else.
50
+ *
51
+ * Exit codes: 0 = every carry resolves, 1 = at least one does not.
52
+ */
53
+
54
+ import * as fs from 'node:fs';
55
+ import * as os from 'node:os';
56
+ import * as path from 'node:path';
57
+ import { fileURLToPath, pathToFileURL } from 'node:url';
58
+
59
+ import {
60
+ parseDeferredItems,
61
+ _relatesTo as relatesTo,
62
+ type DeferredItem,
63
+ } from '../agent-src/scripts/archive_completed_roadmaps.js';
64
+ import { checkRatchet } from './_lib/gate_baseline.js';
65
+ import { GateLedger } from './_lib/gate_ledger.js';
66
+ import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
67
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
68
+
69
+ const _HERE = fileURLToPath(import.meta.url);
70
+ const _DEFAULT_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
71
+
72
+ /** Directories holding roadmaps that can no longer be re-checked by the sweep. */
73
+ const DEAD_DIRS: readonly string[] = ['archive', 'skipped'];
74
+
75
+ /**
76
+ * Cheap pre-filter. It must match a BARE `[~]` too, not only the annotation:
77
+ * a deferred step with no annotation at all is the loss case, and filtering on
78
+ * the annotation alone made the gate blind to exactly the shape it exists for.
79
+ * Its own self-test caught that, which is the difference between a `scanned:`
80
+ * floor and a proof that the reading changes the verdict.
81
+ */
82
+ const CARRY_CANDIDATE_RE = /deferred-resolution:|^[ \t]*[-*][ \t]*\[~\]/m;
83
+
84
+ const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
85
+ 'README.md',
86
+ 'INDEX.md',
87
+ 'template.md',
88
+ 'progress.md',
89
+ ]);
90
+
91
+ /**
92
+ * Two classes, and the split is the whole design.
93
+ *
94
+ * `unannotated` — a `[~]` step with no `deferred-resolution:` comment at all.
95
+ * Every one of the 243 in this repository predates the annotation contract,
96
+ * which was settled by council three months after the newest of them. It is
97
+ * real debt, it is shrink-only, and it is not what this gate was built for.
98
+ *
99
+ * `broken-destination` — an annotation that named a receiver which no longer
100
+ * holds up its end: deleted, renamed, re-parented, skipped, or archived with
101
+ * open steps. This class is a hard failure at zero, because every member of it
102
+ * is an obligation that had a recorded home and lost it. Deleting a live
103
+ * receiver produces this class, not the other one.
104
+ */
105
+ export type CarrierProblemClass = 'unannotated' | 'broken-destination';
106
+
107
+ export interface CarrierProblem {
108
+ /** Repo-relative path of the archived roadmap whose carry is broken. */
109
+ source: string;
110
+ cls: CarrierProblemClass;
111
+ detail: string;
112
+ }
113
+
114
+ /** Roadmap files under the dead directories, sorted for stable output. */
115
+ export function deadRoadmaps(root: string): string[] {
116
+ const out: string[] = [];
117
+ for (const dir of DEAD_DIRS) {
118
+ const abs = path.join(root, 'agents', 'roadmaps', dir);
119
+ let entries: string[];
120
+ try {
121
+ entries = fs.readdirSync(abs);
122
+ } catch {
123
+ continue;
124
+ }
125
+ for (const name of entries.sort()) {
126
+ if (!name.endsWith('.md') || EXCLUDE_NAMES.has(name)) {
127
+ continue;
128
+ }
129
+ const full = path.join(abs, name);
130
+ if (!fs.statSync(full).isFile()) {
131
+ continue;
132
+ }
133
+ out.push(path.posix.join('agents/roadmaps', dir, name));
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /** Escape a slug for literal use inside a RegExp. */
140
+ function _escapeRe(v: string): string {
141
+ return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142
+ }
143
+
144
+ /**
145
+ * The frontmatter block's body, or `''` when the file opens without one.
146
+ *
147
+ * Every structural test below reads this rather than the whole file. A roadmap
148
+ * body legitimately quotes roadmap syntax — that is what the stub recording the
149
+ * deferred transition vocabulary does — so a whole-file regex answers a question
150
+ * about documentation as if it were a question about declarations.
151
+ */
152
+ export function frontmatter(text: string): string {
153
+ const m = /^---\n([\s\S]*?)\n---/.exec(text);
154
+ return m === null ? '' : (m[1] ?? '');
155
+ }
156
+
157
+ /**
158
+ * True when `text` DECLARES `parent_roadmap: <slug>` or a `relates:` row naming
159
+ * it, in its frontmatter.
160
+ *
161
+ * Frontmatter-scoped, and the scope is the check. The first version matched
162
+ * `- slug: <parent>` anywhere in the file, which re-opened the defect a prior
163
+ * review closed in `deferralProblems`: a mention in prose, in an example, or
164
+ * inside a fenced block is not a declaration, and a back-link that prose can
165
+ * satisfy proves nothing about either end of the pair. The `relates:` walk is
166
+ * imported from the archival sweep rather than re-implemented, for the same
167
+ * one-implementation reason the annotation parser is.
168
+ */
169
+ function _linksBackTo(text: string, slug: string): boolean {
170
+ const fm = frontmatter(text);
171
+ if (fm === '') {
172
+ return false;
173
+ }
174
+ if (new RegExp(`^parent_roadmap:[ \t]*${_escapeRe(slug)}[ \t]*$`, 'm').test(fm)) {
175
+ return true;
176
+ }
177
+ return relatesTo(fm, slug);
178
+ }
179
+
180
+ /** Where a destination slug resolves, and under which directory. */
181
+ function _locate(root: string, slug: string): { file: string; dir: 'active' | 'later' | 'archive' | 'skipped' } | null {
182
+ const candidates: ReadonlyArray<readonly [string, 'active' | 'later' | 'archive' | 'skipped']> = [
183
+ [path.join(root, 'agents', 'roadmaps', `${slug}.md`), 'active'],
184
+ [path.join(root, 'agents', 'roadmaps', 'later', `${slug}.md`), 'later'],
185
+ [path.join(root, 'agents', 'roadmaps', 'archive', `${slug}.md`), 'archive'],
186
+ [path.join(root, 'agents', 'roadmaps', 'skipped', `${slug}.md`), 'skipped'],
187
+ ];
188
+ for (const [file, dir] of candidates) {
189
+ if (fs.existsSync(file)) {
190
+ return { file, dir };
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ /** An OPEN checkbox on one line — `-` or `*`, matching the canonical grammar. */
197
+ const OPEN_STEP_RE = /^[ \t]*[-*][ \t]*\[[ ][ \t]*\][ \t]/;
198
+ /** A fenced block's delimiter, in either of the two fence styles in use here. */
199
+ const FENCE_RE = /^[ \t]*(?:```|~~~)/;
200
+ const CARRIER_STATUS_RE = /^status:[ \t]*carrier[ \t]*$/m;
201
+
202
+ /**
203
+ * True when an OPEN step appears OUTSIDE every fenced block.
204
+ *
205
+ * Fence-aware because this branch hard-fails at zero with no baseline, and a
206
+ * roadmap that documents step syntax inside a fenced block is not a roadmap
207
+ * with an open step. Measured before the fix: a fenced `markdown` example
208
+ * containing one `- [ ]` line reddened CI with a diagnostic naming the wrong
209
+ * cause and offering remediation that did not apply.
210
+ */
211
+ export function hasOpenStep(text: string): boolean {
212
+ let inFence = false;
213
+ for (const line of text.split('\n')) {
214
+ if (FENCE_RE.test(line)) {
215
+ inFence = !inFence;
216
+ continue;
217
+ }
218
+ if (!inFence && OPEN_STEP_RE.test(line)) {
219
+ return true;
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+
225
+ /** True when the frontmatter — not the body — declares `status: carrier`. */
226
+ export function declaresCarrier(text: string): boolean {
227
+ return CARRIER_STATUS_RE.test(frontmatter(text));
228
+ }
229
+
230
+ /**
231
+ * The standing carry policy for one archived roadmap. See the module header for
232
+ * why this is not `deferralProblems`.
233
+ */
234
+ export function carryProblems(
235
+ root: string,
236
+ sourceSlug: string,
237
+ text: string,
238
+ ): Array<{ cls: CarrierProblemClass; detail: string }> {
239
+ return carryProblemsFor(root, sourceSlug, parseDeferredItems(text));
240
+ }
241
+
242
+ /**
243
+ * The same policy over an ALREADY-PARSED item list.
244
+ *
245
+ * Split out so the walk parses each candidate once and reuses the result for
246
+ * both this check and the destination census the live-carrier enumeration
247
+ * needs, rather than parsing the same file twice per run.
248
+ */
249
+ export function carryProblemsFor(
250
+ root: string,
251
+ sourceSlug: string,
252
+ items: readonly DeferredItem[],
253
+ ): Array<{ cls: CarrierProblemClass; detail: string }> {
254
+ const problems: Array<{ cls: CarrierProblemClass; detail: string }> = [];
255
+ for (const item of items) {
256
+ const label = item.text === '' ? '(unnamed step)' : item.text;
257
+ if (item.kind === null || item.destination === null) {
258
+ problems.push({
259
+ cls: 'unannotated',
260
+ detail:
261
+ `deferred step ${JSON.stringify(label)} carries no ` +
262
+ '`<!-- deferred-resolution: carried-to=<slug> -->` annotation, so nothing records where it went',
263
+ });
264
+ continue;
265
+ }
266
+ if (item.destination === sourceSlug) {
267
+ problems.push({
268
+ cls: 'broken-destination',
269
+ detail: `deferred step ${JSON.stringify(label)} names its OWN roadmap as the destination`,
270
+ });
271
+ continue;
272
+ }
273
+ const found = _locate(root, item.destination);
274
+ if (found === null) {
275
+ problems.push({
276
+ cls: 'broken-destination',
277
+ detail: `deferred step ${JSON.stringify(label)} names destination \`${item.destination}\`, ` +
278
+ 'which no longer exists anywhere under `agents/roadmaps/` — the receiver was deleted ' +
279
+ 'and the obligation went with it',
280
+ });
281
+ continue;
282
+ }
283
+ if (found.dir === 'skipped') {
284
+ problems.push({
285
+ cls: 'broken-destination',
286
+ detail: `destination \`${item.destination}\` is under \`skipped/\` — skipping a receiver is not ` +
287
+ 'fulfilling the carry',
288
+ });
289
+ continue;
290
+ }
291
+ const destText = fs.readFileSync(found.file, 'utf-8');
292
+ // A carrier exists to stay live. Archiving one is the terminal-archival
293
+ // transition that has no vocabulary yet, and it strands every item the
294
+ // carrier itself holds — none of which is an OPEN step, so the check
295
+ // below would not see it.
296
+ if (found.dir === 'archive' && declaresCarrier(destText)) {
297
+ problems.push({
298
+ cls: 'broken-destination',
299
+ detail:
300
+ `destination \`${item.destination}\` is a \`status: carrier\` roadmap that has been ` +
301
+ 'archived — a carrier holds obligations whose triggers are unmet, so archiving it ' +
302
+ 'strands them. Terminal archival of a live carrier is not expressible yet and fails closed.',
303
+ });
304
+ continue;
305
+ }
306
+ if (found.dir === 'archive' && hasOpenStep(destText)) {
307
+ problems.push({
308
+ cls: 'broken-destination',
309
+ detail: `destination \`${item.destination}\` is archived but still has OPEN steps — it was moved ` +
310
+ 'by hand rather than completed, so the carried obligation is stranded',
311
+ });
312
+ continue;
313
+ }
314
+ if (!_linksBackTo(destText, sourceSlug)) {
315
+ problems.push({
316
+ cls: 'broken-destination',
317
+ detail: `destination \`${item.destination}\` carries no \`parent_roadmap: ${sourceSlug}\` ` +
318
+ 'back-link and no `relates:` row naming it — the link must verify from both ends',
319
+ });
320
+ }
321
+ }
322
+ return problems;
323
+ }
324
+
325
+ /** The live locations a carrier may occupy — both are live per `_locate`. */
326
+ const LIVE_SUBDIRS: readonly string[] = ['', 'later'];
327
+
328
+ /**
329
+ * Every roadmap that DECLARES `status: carrier` and is still live.
330
+ *
331
+ * The enumeration the walk-from-the-archive direction cannot produce. Sorted,
332
+ * repo-relative, and `later/` is included because `_locate` treats a parked
333
+ * receiver as live — a carrier parked there is equally excluded from the active
334
+ * count, so it needs the same justification.
335
+ */
336
+ export function liveCarriers(root: string): string[] {
337
+ const out: string[] = [];
338
+ for (const sub of LIVE_SUBDIRS) {
339
+ const abs = path.join(root, 'agents', 'roadmaps', sub);
340
+ let entries: string[];
341
+ try {
342
+ entries = fs.readdirSync(abs);
343
+ } catch {
344
+ continue;
345
+ }
346
+ for (const name of entries.sort()) {
347
+ if (!name.endsWith('.md') || EXCLUDE_NAMES.has(name)) {
348
+ continue;
349
+ }
350
+ const full = path.join(abs, name);
351
+ let text: string;
352
+ try {
353
+ if (!fs.statSync(full).isFile()) {
354
+ continue;
355
+ }
356
+ text = fs.readFileSync(full, 'utf-8');
357
+ } catch {
358
+ continue;
359
+ }
360
+ if (declaresCarrier(text)) {
361
+ out.push(path.posix.join('agents/roadmaps', sub, name));
362
+ }
363
+ }
364
+ }
365
+ return out;
366
+ }
367
+
368
+ /**
369
+ * Validate every carry annotation in the dead directories, then every live
370
+ * carrier against the destinations those annotations actually name.
371
+ *
372
+ * @param walked the dead-roadmap list, when the caller already has it. The walk
373
+ * `statSync`s every file in the archive, so taking it as a parameter is what
374
+ * stops the run doing that twice — once for the ledger's plan and once here.
375
+ * @returns the problems found, how many dead roadmaps were read, and how many
376
+ * live carriers were checked.
377
+ */
378
+ export function auditCarries(
379
+ root: string,
380
+ walked: readonly string[] = deadRoadmaps(root),
381
+ ): { problems: CarrierProblem[]; scanned: number; carriers: string[] } {
382
+ const problems: CarrierProblem[] = [];
383
+ let scanned = 0;
384
+ /** Every slug some dead roadmap named with `carried-to=`. */
385
+ const carriedTo = new Set<string>();
386
+ for (const rel of walked) {
387
+ let text: string;
388
+ try {
389
+ text = fs.readFileSync(path.join(root, rel), 'utf-8');
390
+ } catch {
391
+ continue;
392
+ }
393
+ scanned += 1;
394
+ if (!CARRY_CANDIDATE_RE.test(text)) {
395
+ continue;
396
+ }
397
+ const items = parseDeferredItems(text);
398
+ for (const item of items) {
399
+ if (item.kind === 'carried-to' && item.destination !== null) {
400
+ carriedTo.add(item.destination);
401
+ }
402
+ }
403
+ const sourceSlug = path.basename(rel).replace(/\.md$/, '');
404
+ for (const found of carryProblemsFor(root, sourceSlug, items)) {
405
+ problems.push({ source: rel, ...found });
406
+ }
407
+ }
408
+ const carriers = liveCarriers(root);
409
+ for (const rel of carriers) {
410
+ const slug = path.basename(rel).replace(/\.md$/, '');
411
+ if (carriedTo.has(slug)) {
412
+ continue;
413
+ }
414
+ problems.push({
415
+ source: rel,
416
+ cls: 'broken-destination',
417
+ detail:
418
+ 'declares `status: carrier`, but no roadmap under `agents/roadmaps/archive/` or ' +
419
+ '`agents/roadmaps/skipped/` names it with a `<!-- deferred-resolution: ' +
420
+ `carried-to=${slug} -->\` annotation. That status is only legitimate for the ` +
421
+ 'RECEIVER of a real carry: it buys exclusion from the active roadmap count, from ' +
422
+ '`check_roadmap_trackable` and from the plan risk register, so a file no archived ' +
423
+ 'parent carried anything to would take all three exclusions for nothing. Either ' +
424
+ 'record the carry in the parent that deferred the work, or drop `status: carrier`.',
425
+ });
426
+ }
427
+ return { problems, scanned, carriers };
428
+ }
429
+
430
+ const USAGE = `usage: lint_carrier_integrity [--root <dir>] [--self-test] [--quiet]
431
+
432
+ --root <dir> audit another checkout (default: this repository)
433
+ --self-test run the built-in discrimination suite and exit
434
+ --quiet suppress the green summary line
435
+ `;
436
+
437
+ function _writeFixture(dir: string, rel: string, body: string): void {
438
+ const full = path.join(dir, rel);
439
+ fs.mkdirSync(path.dirname(full), { recursive: true });
440
+ fs.writeFileSync(full, body, 'utf-8');
441
+ }
442
+
443
+ const PARENT_WITH_CARRY = '---\ncomplexity: bounded\n---\n# Parent\n\n## Phase 1 - a phase\n\n- [~] **1.1 A carried step.**\n <!-- deferred-resolution: carried-to=road-to-receiver -->\n';
444
+
445
+ function _receiver(backlink: string): string {
446
+ return `---
447
+ complexity: bounded
448
+ ${backlink}
449
+ ---
450
+ # Receiver
451
+
452
+ Body.
453
+ `;
454
+ }
455
+
456
+ /**
457
+ * A body that DOCUMENTS roadmap syntax inside a fence. Both structural tests
458
+ * used to match it, so this fixture is the one that fails in two ways at once.
459
+ */
460
+ const FENCED_SYNTAX_DOC = 'Syntax this file documents rather than declares:\n\n```markdown\n---\nstatus: carrier\n---\n- [ ] **1.1 An example open step.**\n```\n';
461
+
462
+ /** A back-link that exists only in the body — a mention, never a declaration. */
463
+ const FENCED_BODY_BACKLINK = 'What a back-link looks like, quoted:\n\n```yaml\nparent_roadmap: road-to-parent\nrelates:\n - slug: road-to-parent\n relation: extends\n```\n';
464
+
465
+ /** A receiver that declares itself a carrier — used only where that is the point. */
466
+ function _carrierReceiver(backlink: string): string {
467
+ return _receiver(backlink).replace('---\n# Receiver', 'status: carrier\n---\n# Receiver');
468
+ }
469
+
470
+ /**
471
+ * Each case builds a throwaway tree, runs the real CLI against it with
472
+ * `--root`, and asserts the exit code. Shelling out rather than calling
473
+ * `auditCarries` directly keeps argv parsing and the exit-code mapping inside
474
+ * what is proven.
475
+ *
476
+ * Every case here is a `broken-destination` case, because that is the class
477
+ * this CLI hard-fails on. The `unannotated` class cannot be proven by exit code
478
+ * at all: it is ratcheted against a baseline of 243, so one planted fixture
479
+ * finding sits far below the floor and correctly exits 0. Its detection is
480
+ * proven instead by a unit test over `carryProblems`, which asserts the class
481
+ * directly, plus by the 243 it reports against the real tree on every run.
482
+ */
483
+ export function selfTestCases(root: string): SelfTestCase[] {
484
+ const build = (name: string, plant: (dir: string) => void): (() => number) => {
485
+ return () => {
486
+ const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), name));
487
+ try {
488
+ plant(dir);
489
+ return runGateCli(
490
+ root,
491
+ 'src/scripts/lint_carrier_integrity.ts',
492
+ ['--root', dir, '--quiet'],
493
+ root,
494
+ );
495
+ } finally {
496
+ fs.rmSync(dir, { recursive: true, force: true });
497
+ }
498
+ };
499
+ };
500
+ const parent = (dir: string): void =>
501
+ _writeFixture(dir, 'agents/roadmaps/archive/road-to-parent.md', PARENT_WITH_CARRY);
502
+ return [
503
+ {
504
+ name: 'a live receiver with a back-link is accepted',
505
+ expect: 'accept',
506
+ run: build('carrier-ok-', (dir) => {
507
+ parent(dir);
508
+ _writeFixture(
509
+ dir,
510
+ 'agents/roadmaps/road-to-receiver.md',
511
+ _receiver('parent_roadmap: road-to-parent'),
512
+ );
513
+ }),
514
+ },
515
+ {
516
+ name: 'a receiver parked under later/ is equally live and is accepted',
517
+ expect: 'accept',
518
+ run: build('carrier-later-', (dir) => {
519
+ parent(dir);
520
+ _writeFixture(
521
+ dir,
522
+ 'agents/roadmaps/later/road-to-receiver.md',
523
+ _receiver('parent_roadmap: road-to-parent'),
524
+ );
525
+ }),
526
+ },
527
+ {
528
+ name: 'a receiver that completed its work and archived is accepted — fulfilment, not loss',
529
+ expect: 'accept',
530
+ run: build('carrier-done-', (dir) => {
531
+ parent(dir);
532
+ _writeFixture(
533
+ dir,
534
+ 'agents/roadmaps/archive/road-to-receiver.md',
535
+ `${_receiver('parent_roadmap: road-to-parent')}\n- [x] **1.1 Done.**\n`,
536
+ );
537
+ }),
538
+ },
539
+ {
540
+ name: 'a back-link supplied as a relates: row instead of parent_roadmap is accepted',
541
+ expect: 'accept',
542
+ run: build('carrier-relates-', (dir) => {
543
+ parent(dir);
544
+ _writeFixture(
545
+ dir,
546
+ 'agents/roadmaps/road-to-receiver.md',
547
+ _receiver('relates:\n - slug: road-to-parent\n relation: extends'),
548
+ );
549
+ }),
550
+ },
551
+ {
552
+ name: 'a live carrier an archived parent actually names is accepted',
553
+ expect: 'accept',
554
+ run: build('carrier-named-', (dir) => {
555
+ parent(dir);
556
+ _writeFixture(
557
+ dir,
558
+ 'agents/roadmaps/road-to-receiver.md',
559
+ _carrierReceiver('parent_roadmap: road-to-parent'),
560
+ );
561
+ }),
562
+ },
563
+ {
564
+ name: 'an archived receiver that only documents roadmap syntax in a fence is accepted',
565
+ expect: 'accept',
566
+ run: build('carrier-fenced-', (dir) => {
567
+ parent(dir);
568
+ _writeFixture(
569
+ dir,
570
+ 'agents/roadmaps/archive/road-to-receiver.md',
571
+ `${_receiver('parent_roadmap: road-to-parent')}\n${FENCED_SYNTAX_DOC}`,
572
+ );
573
+ }),
574
+ },
575
+ {
576
+ name: 'a live carrier no archived parent names is rejected — the status is not free',
577
+ expect: 'reject',
578
+ run: build('carrier-orphan-', (dir) => {
579
+ parent(dir);
580
+ _writeFixture(
581
+ dir,
582
+ 'agents/roadmaps/road-to-receiver.md',
583
+ _receiver('parent_roadmap: road-to-parent'),
584
+ );
585
+ _writeFixture(
586
+ dir,
587
+ 'agents/roadmaps/road-to-self-declared.md',
588
+ _carrierReceiver('owner: maintainer'),
589
+ );
590
+ }),
591
+ },
592
+ {
593
+ name: 'an orphan carrier parked under later/ is rejected too',
594
+ expect: 'reject',
595
+ run: build('carrier-orphan-later-', (dir) => {
596
+ parent(dir);
597
+ _writeFixture(
598
+ dir,
599
+ 'agents/roadmaps/road-to-receiver.md',
600
+ _receiver('parent_roadmap: road-to-parent'),
601
+ );
602
+ _writeFixture(
603
+ dir,
604
+ 'agents/roadmaps/later/road-to-self-declared.md',
605
+ _carrierReceiver('owner: maintainer'),
606
+ );
607
+ }),
608
+ },
609
+ {
610
+ name: 'an archived receiver with an asterisk-bulleted OPEN step is rejected',
611
+ expect: 'reject',
612
+ run: build('carrier-asterisk-', (dir) => {
613
+ parent(dir);
614
+ _writeFixture(
615
+ dir,
616
+ 'agents/roadmaps/archive/road-to-receiver.md',
617
+ `${_receiver('parent_roadmap: road-to-parent')}\n* [ ] **1.1 Still open.**\n`,
618
+ );
619
+ }),
620
+ },
621
+ {
622
+ name: 'a back-link that only appears in the body, not the frontmatter, is rejected',
623
+ expect: 'reject',
624
+ run: build('carrier-bodylink-', (dir) => {
625
+ parent(dir);
626
+ _writeFixture(
627
+ dir,
628
+ 'agents/roadmaps/road-to-receiver.md',
629
+ `${_receiver('owner: maintainer')}\n${FENCED_BODY_BACKLINK}`,
630
+ );
631
+ }),
632
+ },
633
+ {
634
+ name: 'a receiver that is itself a carrier and has been archived is rejected',
635
+ expect: 'reject',
636
+ run: build('carrier-archived-carrier-', (dir) => {
637
+ parent(dir);
638
+ _writeFixture(
639
+ dir,
640
+ 'agents/roadmaps/archive/road-to-receiver.md',
641
+ _carrierReceiver('parent_roadmap: road-to-parent'),
642
+ );
643
+ }),
644
+ },
645
+ {
646
+ name: 'the receiver deleted entirely is rejected',
647
+ expect: 'reject',
648
+ run: build('carrier-deleted-', parent),
649
+ },
650
+ {
651
+ name: 'a receiver archived while still carrying OPEN steps is rejected — stranded, not completed',
652
+ expect: 'reject',
653
+ run: build('carrier-stranded-', (dir) => {
654
+ parent(dir);
655
+ _writeFixture(
656
+ dir,
657
+ 'agents/roadmaps/archive/road-to-receiver.md',
658
+ `${_receiver('parent_roadmap: road-to-parent')}\n- [ ] **1.1 Still open.**\n`,
659
+ );
660
+ }),
661
+ },
662
+ {
663
+ name: 'a receiver under skipped/ is rejected — skipping is not fulfilling',
664
+ expect: 'reject',
665
+ run: build('carrier-skipped-', (dir) => {
666
+ parent(dir);
667
+ _writeFixture(
668
+ dir,
669
+ 'agents/roadmaps/skipped/road-to-receiver.md',
670
+ _receiver('parent_roadmap: road-to-parent'),
671
+ );
672
+ }),
673
+ },
674
+ {
675
+ name: 'the back-link removed is rejected — the link must verify from both ends',
676
+ expect: 'reject',
677
+ run: build('carrier-nolink-', (dir) => {
678
+ parent(dir);
679
+ _writeFixture(
680
+ dir,
681
+ 'agents/roadmaps/road-to-receiver.md',
682
+ _receiver('owner: maintainer'),
683
+ );
684
+ }),
685
+ },
686
+ {
687
+ name: 'a back-link naming a different parent is rejected',
688
+ expect: 'reject',
689
+ run: build('carrier-reparent-', (dir) => {
690
+ parent(dir);
691
+ _writeFixture(
692
+ dir,
693
+ 'agents/roadmaps/road-to-receiver.md',
694
+ _receiver('parent_roadmap: road-to-someone-else'),
695
+ );
696
+ }),
697
+ },
698
+ ];
699
+ }
700
+
701
+ export function main(argv: string[]): number {
702
+ let root = _DEFAULT_ROOT;
703
+ let quiet = false;
704
+ for (let i = 0; i < argv.length; i += 1) {
705
+ const arg = argv[i] as string;
706
+ if (arg === '-h' || arg === '--help') {
707
+ process.stdout.write(USAGE);
708
+ return 0;
709
+ } else if (arg === '--quiet') {
710
+ quiet = true;
711
+ } else if (arg === '--root') {
712
+ const next = argv[i + 1];
713
+ if (next === undefined) {
714
+ process.stderr.write('lint_carrier_integrity: --root needs a directory\n');
715
+ return 2;
716
+ }
717
+ root = path.resolve(next);
718
+ i += 1;
719
+ } else if (arg === '--self-test') {
720
+ return runSelfTest({
721
+ gate: 'lint_carrier_integrity',
722
+ cases: selfTestCases(_DEFAULT_ROOT),
723
+ minCases: 16,
724
+ minRejectCases: 10,
725
+ });
726
+ } else {
727
+ process.stderr.write(`lint_carrier_integrity: unknown argument ${JSON.stringify(arg)}\n`);
728
+ process.stderr.write(USAGE);
729
+ return 2;
730
+ }
731
+ }
732
+
733
+ const ledger = new GateLedger('lint_carrier_integrity');
734
+ const walked = deadRoadmaps(root);
735
+ // Walked ONCE and handed to the audit. The two enumerations used to each
736
+ // call `deadRoadmaps`, so a run cost two readdirs and two `statSync` per
737
+ // archived file for one reading.
738
+ const { problems, scanned, carriers } = auditCarries(root, walked);
739
+ ledger.plan([...walked, ...carriers]);
740
+ const failed = new Set(problems.map((p) => p.source));
741
+ for (const rel of [...walked, ...carriers]) {
742
+ if (failed.has(rel)) {
743
+ ledger.fail(rel, problems.find((p) => p.source === rel)?.detail ?? 'broken carry');
744
+ } else {
745
+ ledger.complete(rel);
746
+ }
747
+ }
748
+ try {
749
+ reportScanned({
750
+ gate: 'lint_carrier_integrity',
751
+ scanned,
752
+ units: 'archived roadmap(s)',
753
+ roots: DEAD_DIRS.map((d) => `agents/roadmaps/${d}`),
754
+ allowEmpty: 'a checkout with no archived roadmaps has no carry to break',
755
+ });
756
+ } catch (e) {
757
+ if (e instanceof DeadScopeError) {
758
+ process.stderr.write(`${e.message}\n`);
759
+ ledger.report();
760
+ return 1;
761
+ }
762
+ throw e;
763
+ }
764
+
765
+ const broken = problems.filter((p) => p.cls === 'broken-destination');
766
+ const unannotated = problems.filter((p) => p.cls === 'unannotated');
767
+
768
+ // Hard failure, at zero, no baseline. Every member of this class is an
769
+ // obligation that had a recorded destination and lost it.
770
+ if (broken.length > 0) {
771
+ process.stderr.write(
772
+ `\n❌ ${String(broken.length)} broken deferral carr${broken.length === 1 ? 'y' : 'ies'}. ` +
773
+ 'An archived roadmap named a receiver that no longer holds up its end, so the ' +
774
+ 'obligations it carried have no live destination.\n\n',
775
+ );
776
+ for (const p of broken) {
777
+ process.stderr.write(` ❌ ${p.source}\n ${p.detail}\n`);
778
+ }
779
+ process.stderr.write(
780
+ '\n Restore the receiver, restore its `parent_roadmap:` back-link, or move the\n' +
781
+ ' carried items back to open in the archived parent. A rename or a re-parent is\n' +
782
+ ' not expressible yet and fails closed on purpose.\n' +
783
+ ' A file listed for declaring `status: carrier` needs neither: record the carry\n' +
784
+ ' in the archived parent, or drop the status and let the file be ordinary estate.\n',
785
+ );
786
+ ledger.report();
787
+ return 1;
788
+ }
789
+
790
+ const verdict = checkRatchet({
791
+ gate: 'lint_carrier_integrity',
792
+ actual: unannotated.length,
793
+ repoRoot: _DEFAULT_ROOT,
794
+ });
795
+ if (!verdict.ok) {
796
+ process.stderr.write(`\n❌ ${verdict.message}\n\n`);
797
+ for (const p of unannotated.slice(0, 20)) {
798
+ process.stderr.write(` ❌ ${p.source}\n ${p.detail}\n`);
799
+ }
800
+ if (unannotated.length > 20) {
801
+ process.stderr.write(` … and ${String(unannotated.length - 20)} more.\n`);
802
+ }
803
+ ledger.report();
804
+ return 1;
805
+ }
806
+
807
+ if (!quiet) {
808
+ process.stdout.write(`✅ ${verdict.message}\n`);
809
+ process.stdout.write(
810
+ `✅ lint_carrier_integrity: ${String(scanned)} dead roadmap(s) scanned, ` +
811
+ `${String(carriers.length)} live carrier(s) justified, every annotated carry resolves.\n`,
812
+ );
813
+ }
814
+ ledger.report();
815
+ return 0;
816
+ }
817
+
818
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
819
+ process.exit(main(process.argv.slice(2)));
820
+ }