@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
@@ -47,7 +47,7 @@
47
47
  * or merges one in the same change — or the added file carries an explicit
48
48
  * `estate_offset_exempt:` reason in its frontmatter, which costs a visible line
49
49
  * in the diff of the very commit that claims it. This half is diff-scoped: it
50
- * reads `<base>...HEAD` over `agents/roadmaps/`, so on a branch that adds no
50
+ * reads `<base>...HEAD` over `agents/roadmaps/`, so on a branch that adds no code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
51
51
  * roadmap it legitimately finds nothing to weigh.
52
52
  *
53
53
  * THE THREE WAYS THE ESTATE MAY LEGITIMATELY GROW
@@ -65,7 +65,7 @@
65
65
  * estate rather than creating it, so it needs no authorisation; what the
66
66
  * allowance does NOT cover is a `later/` file appearing from nowhere.
67
67
  * 3. **Anything else** — `estate_growth_exempt: <reason>` added, in this change,
68
- * to the frontmatter of any roadmap under `agents/roadmaps/`. The canonical
68
+ * to the frontmatter of any roadmap under `agents/roadmaps/`. The canonical code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
69
69
  * case is a blocker discovered while doing the work: `open_blockers` rises,
70
70
  * nothing was archived, and the reason belongs next to the blocker.
71
71
  *
@@ -117,7 +117,7 @@
117
117
  * which one lied.
118
118
  *
119
119
  * The same function measures the BASE side, over a scratch copy of the base
120
- * ref's `agents/roadmaps/` (`_lib/base_tree.ts`). Re-deriving the floor with a
120
+ * ref's `agents/roadmaps/` (`_lib/base_tree.ts`). Re-deriving the floor with a code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
121
121
  * second implementation would let the two sides disagree about one tree, which
122
122
  * is the failure this section already argues against, one level up.
123
123
  *
@@ -143,7 +143,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
143
143
 
144
144
  import {
145
145
  collect,
146
- is_draft as isDraft,
146
+ is_unscheduled as isUnscheduled,
147
147
  is_roadmap_candidate as isRoadmapCandidate,
148
148
  parse_frontmatter as parseFrontmatter,
149
149
  parse_roadmap as parseRoadmap,
@@ -155,6 +155,8 @@ import { materialiseSubtree } from './_lib/base_tree.js';
155
155
  import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
156
156
  import { CONCERN_MANIFEST_POSIX, countConcerns } from './_lib/concern_estate.js';
157
157
  import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
158
+ import { countDeclaredCarriers, declaresCarrier } from './_lib/carrier_status.js';
159
+ import { carrierEstateCases } from './_lib/estate_carrier_cases.js';
158
160
 
159
161
  const GATE = 'check_estate_count';
160
162
  const BUDGET_REL = path.join('src', 'config', 'estate-count-budget.json');
@@ -232,6 +234,63 @@ export interface EstateBudget {
232
234
  * number here is how it gets a threshold.
233
235
  */
234
236
  one_in_one_out: { applies_above_active: number | null };
237
+ /**
238
+ * The capped provisional-promotion path — specified, and DECLINED.
239
+ *
240
+ * READ ITS NULLS THE OTHER WAY ROUND FROM THE KEY ABOVE. There `null` means
241
+ * UNCONDITIONAL; here `max_live: null` and `expires_after_days: null` mean
242
+ * NOT REGISTERED, PATH INACTIVE. The two keys sit a few lines apart in one
243
+ * file and their nulls mean opposite things, so a reader who transfers the
244
+ * sibling's semantics reads a declined path as an uncapped one.
245
+ */
246
+ provisional_promotion: ProvisionalPromotion;
247
+ }
248
+
249
+ /**
250
+ * The `provisional_promotion` object as the budget file carries it.
251
+ *
252
+ * `status` is optional so that a file which registered the two integers without
253
+ * restating a status is still readable — the integers are what activate the
254
+ * path, and a declination that dropped its own marker would fall to
255
+ * `unregistered`, which is the safe direction.
256
+ */
257
+ export interface ProvisionalPromotion {
258
+ status?: string;
259
+ max_live: number | null;
260
+ expires_after_days: number | null;
261
+ }
262
+
263
+ /**
264
+ * Three states, and the reason there are three rather than two.
265
+ *
266
+ * The 2026-09-01 council that declined the path attached one obligation to the
267
+ * decline: the checker must distinguish an intentional declination from missing
268
+ * configuration. Two states cannot — `null` would read the same either way, and
269
+ * "nobody has written this yet" would be indistinguishable from "this was
270
+ * specified in full and refused on 2026-09-01".
271
+ */
272
+ export type ProvisionalPromotionState = 'declined' | 'unregistered' | 'registered';
273
+
274
+ /**
275
+ * Classify the path's state. NEVER activates it, in any state.
276
+ *
277
+ * A half-registered object — one integer, one null — throws rather than
278
+ * resolving to either neighbour: a cap with no expiry and an expiry with no cap
279
+ * are different mechanisms, and guessing which half was meant is how a bounded
280
+ * path becomes an unbounded one by omission.
281
+ */
282
+ export function provisionalPromotionState(pp: ProvisionalPromotion): ProvisionalPromotionState {
283
+ const live = pp.max_live;
284
+ const days = pp.expires_after_days;
285
+ if (typeof live === 'number' && typeof days === 'number') return 'registered';
286
+ if ((live === null) !== (days === null)) {
287
+ throw new Error(
288
+ `${BUDGET_REL} "provisional_promotion" is half-registered ` +
289
+ `(max_live=${JSON.stringify(live)}, expires_after_days=${JSON.stringify(days)}). ` +
290
+ 'Both integers activate the path; both null leave it inactive. One of each is neither.',
291
+ );
292
+ }
293
+ return pp.status === 'declined' ? 'declined' : 'unregistered';
235
294
  }
236
295
 
237
296
  /** One gated count that rose above the floor measured at the base ref. */
@@ -305,6 +364,15 @@ export interface EstateVerdict {
305
364
  offsets: OffsetLedger | null;
306
365
  offsetSkipReason: string | null;
307
366
  unpaid: number;
367
+ /**
368
+ * The provisional-promotion path's state, reported and never acted on.
369
+ *
370
+ * Carried in the verdict so a `--json` consumer can tell a declined path
371
+ * from an unwritten one without re-reading the budget file, which is the
372
+ * distinction the 2026-09-01 decline was conditioned on. It contributes
373
+ * nothing to `withinBudget` in any state: reading is not activation.
374
+ */
375
+ provisionalPromotion: ProvisionalPromotionState;
308
376
  withinBudget: boolean;
309
377
  }
310
378
 
@@ -370,7 +438,39 @@ function countIn(roadmapRoot: string, sub: string): number {
370
438
  }
371
439
  }
372
440
 
373
- /** Non-draft roadmaps parked in `later/`, parsed for their blockers. */
441
+ /**
442
+ * Top-level roadmaps `collect()` drops for declaring `status: carrier`, added
443
+ * back so that FLIPPING a status is count-neutral.
444
+ *
445
+ * Measured before this: flipping one file to `carrier` moved `active_roadmaps
446
+ * 3 → 2` and the gate printed "estate within its ratchet", because a count
447
+ * below the floor is a drawdown and free. The floor is measured at the base
448
+ * ref, so once such a flip merges the lowered number becomes the next floor —
449
+ * the file cannot be flipped back without paying, and any roadmap can be
450
+ * laundered out of the count by adding one word.
451
+ *
452
+ * A reclassification is not a disposal, so the metric counts the file on both
453
+ * sides. Same argument `open_blockers` makes below for spanning `later/`.
454
+ * Deleting a carrier is still a shrink and still earns no offset
455
+ * (`classifyDiff`); adding one is growth that takes the claim path.
456
+ *
457
+ * This is therefore the one roadmap metric that does NOT equal the dashboard
458
+ * header, stated here rather than left to be tripped over.
459
+ */
460
+ function countActiveCarriers(roadmapRoot: string): number {
461
+ return countDeclaredCarriers(roadmapRoot, isRoadmapCandidate, fs, path.join);
462
+ }
463
+
464
+ /**
465
+ * Roadmaps parked in `later/` whose blockers count, parsed for them.
466
+ *
467
+ * Skips a draft AND a carrier, which is the same pair `collect()` skips on the
468
+ * active side. It used to skip only the draft, so parking a carrier moved its
469
+ * blockers INTO the gated count while the identical file at the top level kept
470
+ * them out — a ratchet verdict that moved on a relocation, for no stated
471
+ * reason. Three of the four status readers learned `carrier` when the status
472
+ * shipped; this is the fourth.
473
+ */
374
474
  function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unknown[] }> {
375
475
  const dir = path.join(roadmapRoot, 'later');
376
476
  let names: string[];
@@ -386,7 +486,7 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
386
486
  // `collect()` cannot be reused here: it filters through the same
387
487
  // path-based predicate and would reject its own root.
388
488
  const text = fs.readFileSync(abs, 'utf-8');
389
- if (isDraft(parseFrontmatter(text))) continue;
489
+ if (isUnscheduled(parseFrontmatter(text))) continue;
390
490
  const stats = parseRoadmap(abs, dir);
391
491
  if (stats !== null) out.push(stats as unknown as { open_blockers: readonly unknown[] });
392
492
  }
@@ -396,8 +496,11 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
396
496
  /**
397
497
  * Count the estate, three ways.
398
498
  *
399
- * `active_roadmaps` comes from `collect()` so it cannot disagree with the
400
- * dashboard. `later_roadmaps` is a directory listing because parked files are
499
+ * `active_roadmaps` is `collect()` PLUS the top-level carriers `collect()`
500
+ * drops, so the parser is shared with the dashboard while a status flip stays
501
+ * count-neutral — see `countActiveCarriers` for why that beats parity here, and
502
+ * for the one number on this gate that the dashboard header does not print.
503
+ * `later_roadmaps` is a directory listing because parked files are
401
504
  * outside that corpus by design — filtered through the same `is_roadmap_candidate`
402
505
  * predicate as the active side, so `later/README.md` is not a roadmap here either.
403
506
  *
@@ -421,7 +524,7 @@ export function countEstate(repoRoot: string): EstateCounts {
421
524
  rows.reduce((n, r) => n + r.open_blockers.length, 0);
422
525
  const skills = measureSkillEstate(repoRoot);
423
526
  return {
424
- active_roadmaps: stats.length,
527
+ active_roadmaps: stats.length + countActiveCarriers(roadmapRoot),
425
528
  later_roadmaps: countIn(roadmapRoot, 'later'),
426
529
  open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
427
530
  skill_count: skills.skill_count,
@@ -440,7 +543,7 @@ export function skillTokensExact(root: string): boolean {
440
543
  return measureSkillEstate(root).skill_description_tokens !== null;
441
544
  }
442
545
 
443
- /** `agents/roadmaps/<name>.md` — the active top level, never a subdirectory. */
546
+ /** `agents/roadmaps/<name>.md` — the active top level. code-comment-allow provenance-comment -- operand, not provenance */
444
547
  function isActiveTopLevel(rel: string): boolean {
445
548
  const norm = rel.split(path.sep).join('/');
446
549
  if (!norm.startsWith('agents/roadmaps/') || !norm.endsWith('.md')) {
@@ -487,6 +590,9 @@ export function exemptionReason(text: string): string | null {
487
590
  return reason === '' ? null : reason;
488
591
  }
489
592
 
593
+ /** Re-export; `_lib/carrier_status.ts` holds the one implementation. */
594
+ export const isCarrierText = declaresCarrier;
595
+
490
596
  /**
491
597
  * Classify the change's effect on the active roadmap tree.
492
598
  *
@@ -495,7 +601,17 @@ export function exemptionReason(text: string): string | null {
495
601
  * `later/road-to-x.md` → `road-to-x.md` is an un-parking and counts as an
496
602
  * addition. A top-level-to-top-level rename is neither.
497
603
  */
498
- export function classifyDiff(nameStatus: string, readFile: (rel: string) => string | null): OffsetLedger {
604
+ export function classifyDiff(
605
+ nameStatus: string,
606
+ readFile: (rel: string) => string | null,
607
+ /**
608
+ * The file's content at the BASE ref, for paths this change removed — a
609
+ * deleted carrier cannot be read from the working tree and its status
610
+ * decides whether the deletion earns an offset. Defaults to null, which
611
+ * reproduces the previous scoring for any caller without a pre-image.
612
+ */
613
+ readBase: (rel: string) => string | null = () => null,
614
+ ): OffsetLedger {
499
615
  const added: string[] = [];
500
616
  const offsets: string[] = [];
501
617
  const exempt: Array<{ file: string; reason: string }> = [];
@@ -508,8 +624,10 @@ export function classifyDiff(nameStatus: string, readFile: (rel: string) => stri
508
624
  const from = cols[1] ?? '';
509
625
  const to = cols[2] ?? '';
510
626
  if (isActiveTopLevel(from) && isDisposed(to)) {
511
- offsets.push(from);
512
- if (isParked(to)) parked.push(to);
627
+ if (!isCarrierText(readBase(from))) {
628
+ offsets.push(from);
629
+ if (isParked(to)) parked.push(to);
630
+ }
513
631
  } else if (isDisposed(from) && isActiveTopLevel(to)) {
514
632
  added.push(to);
515
633
  }
@@ -520,7 +638,10 @@ export function classifyDiff(nameStatus: string, readFile: (rel: string) => stri
520
638
  if (status === 'A') {
521
639
  added.push(file);
522
640
  } else if (status === 'D') {
523
- offsets.push(file);
641
+ // A carrier's removal is never an offset. See `readBase`.
642
+ if (!isCarrierText(readBase(file))) {
643
+ offsets.push(file);
644
+ }
524
645
  }
525
646
  }
526
647
  for (const file of added) {
@@ -587,6 +708,18 @@ export function evaluate(
587
708
  if (budget.one_in_one_out === undefined) {
588
709
  throw new Error(`${BUDGET_REL} carries no "one_in_one_out" object.`);
589
710
  }
711
+ // Same shape, same reason: an absent policy object is a could-not-run, not a
712
+ // pass. Here it also carries a second job — with the key required, "the path
713
+ // was declined on 2026-09-01" and "nobody has configured this" cannot
714
+ // collapse into the same reading, which is the condition the council
715
+ // attached to the decline.
716
+ if (budget.provisional_promotion === undefined) {
717
+ throw new Error(
718
+ `${BUDGET_REL} carries no "provisional_promotion" object. ` +
719
+ 'Absent is misconfiguration, never an inactive path — a declination is recorded, not implied.',
720
+ );
721
+ }
722
+ const provisionalPromotion = provisionalPromotionState(budget.provisional_promotion);
590
723
 
591
724
  const counts = countEstate(repoRoot);
592
725
  const baseRef = opts.baseRef ?? resolveBaseRef(repoRoot);
@@ -699,13 +832,20 @@ export function evaluate(
699
832
  if (!diff.ok) {
700
833
  offsetSkipReason = `git diff against ${baseRef} failed — one-in-one-out not evaluated`;
701
834
  } else {
702
- offsets = classifyDiff(diff.stdout, (rel) => {
703
- try {
704
- return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
705
- } catch {
706
- return null;
707
- }
708
- });
835
+ offsets = classifyDiff(
836
+ diff.stdout,
837
+ (rel) => {
838
+ try {
839
+ return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
840
+ } catch {
841
+ return null;
842
+ }
843
+ },
844
+ (rel) => {
845
+ const show = git(['show', `${baseRef}:${rel}`], repoRoot);
846
+ return show.ok ? show.stdout : null;
847
+ },
848
+ );
709
849
  }
710
850
  const patch = git(
711
851
  ['diff', '--unified=0', '--find-renames', `${baseRef}...HEAD`, '--', ROADMAPS_REL],
@@ -790,6 +930,7 @@ export function evaluate(
790
930
  offsets,
791
931
  offsetSkipReason,
792
932
  unpaid,
933
+ provisionalPromotion,
793
934
  // No floor is a FAILURE, not a skip, and it is the one place this gate
794
935
  // convicts on a missing input. The other halves can report "unproven" and
795
936
  // still leave a meaningful verdict behind; the count half IS the verdict,
@@ -800,7 +941,7 @@ export function evaluate(
800
941
  }
801
942
 
802
943
  /** Floors for `--self-test`, declared here so a truncation is a visible diff. */
803
- const SELF_TEST_MIN_CASES = 12;
944
+ const SELF_TEST_MIN_CASES = 13;
804
945
  const SELF_TEST_MIN_REJECT = 8;
805
946
 
806
947
  /**
@@ -825,11 +966,32 @@ function selfTest(): number {
825
966
  * the baseline with no reason" is not a state a change can reach.
826
967
  */
827
968
  const budgetJson = (): string =>
828
- `${JSON.stringify({ one_in_one_out: { applies_above_active: null } }, null, 4)}\n`;
969
+ `${JSON.stringify(
970
+ {
971
+ one_in_one_out: { applies_above_active: null },
972
+ // Required since the path was specified and declined: the gate
973
+ // refuses a budget that carries no `provisional_promotion`, so a
974
+ // fixture without it exits 2 on every case rather than on the one
975
+ // case testing that refusal.
976
+ provisional_promotion: { status: 'declined', max_live: null, expires_after_days: null },
977
+ },
978
+ null,
979
+ 4,
980
+ )}\n`;
829
981
 
830
982
  const roadmap = (name: string, extra = ''): string =>
831
983
  `# Roadmap: ${name}\n\n## Phase 1\n\n- [ ] **1.1** s\n${extra}`;
832
984
 
985
+ /** A `status: carrier` roadmap — counted as estate, and never an offset. */
986
+ const CARRIER = `---\nstatus: carrier\n---\n# Roadmap: carrier\n\n## Phase 1\n\n- [~] **1.1** s\n`;
987
+
988
+ /**
989
+ * A `status: draft` roadmap — invisible to `collect()`, so adding one cannot
990
+ * move `active_roadmaps`. That is what makes it the fixture for a case that
991
+ * must be decided by the offset scoring and by nothing else.
992
+ */
993
+ const DRAFT = `---\nstatus: draft\n---\n# Roadmap: draft\n\n## Phase 1\n\n- [ ] **1.1** s\n`;
994
+
833
995
  /** An open blocker — no `Status: resolved` prefix, so it counts as open. */
834
996
  const BLOCKER = '\n## Blockers\n\n### Blocker: fixture\n\n- **Status:** open\n';
835
997
 
@@ -1035,6 +1197,14 @@ function selfTest(): number {
1035
1197
  after: (dir) => write(dir, 'agents/roadmaps/later/road-to-parked.md', roadmap('P')),
1036
1198
  }),
1037
1199
  },
1200
+ ...carrierEstateCases({
1201
+ fixture,
1202
+ write,
1203
+ remove: (dir, rel) => fs.rmSync(path.join(dir, rel)),
1204
+ roadmap,
1205
+ CARRIER,
1206
+ DRAFT,
1207
+ }),
1038
1208
  {
1039
1209
  name: 'a new open blocker with no claim → reject',
1040
1210
  expect: 'reject',
@@ -1160,7 +1330,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
1160
1330
  return 2;
1161
1331
  }
1162
1332
 
1163
- // A ratchet over an empty estate always passes. Move `agents/roadmaps/` and
1333
+ // A ratchet over an empty estate always passes. Move `agents/roadmaps/` and code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
1164
1334
  // every count is 0, which is trivially under any baseline — exit 2 (could
1165
1335
  // not run), never 1, which would assert the estate actually grew.
1166
1336
  try {
@@ -1237,6 +1407,17 @@ export function main(argv: string[] = process.argv.slice(2)): number {
1237
1407
  } else if (verdict.offsetSkipReason !== null) {
1238
1408
  process.stdout.write(` ⚠️ ${verdict.offsetSkipReason}\n`);
1239
1409
  }
1410
+ // Printed unconditionally so the state is visible without `--json`. It never
1411
+ // changes the exit code — a declined path and a registered one both leave
1412
+ // the ratchet exactly as it was, and the allowance a registered path would
1413
+ // grant is deliberately unwired (the owner registers the numbers; wiring
1414
+ // what they buy is a separate, separately authorised change).
1415
+ process.stdout.write(
1416
+ ` ${'provisional'.padEnd(18)} ${verdict.provisionalPromotion.padStart(5)}` +
1417
+ `${verdict.provisionalPromotion === 'declined' ? ' (specified and declined 2026-09-01 — path inactive by decision)' : ''}` +
1418
+ `${verdict.provisionalPromotion === 'unregistered' ? ' (no numbers registered — path inactive)' : ''}` +
1419
+ `${verdict.provisionalPromotion === 'registered' ? ' (numbers registered by the owner; this gate grants no allowance for them)' : ''}\n`,
1420
+ );
1240
1421
  for (const c of verdict.claims) {
1241
1422
  process.stdout.write(` ↑ growth claimed in ${c.file}: ${c.reason.slice(0, 120)}\n`);
1242
1423
  }
@@ -18,7 +18,33 @@
18
18
  * `_none_` everywhere. That removes the guaranteed first-run red (9.17.0,
19
19
  * 9.18.0) without blunting this gate: a machine-written `_none_` was never the
20
20
  * failure it was built for — a HUMAN writing one was (9.13.0, 9.14.0), and that
21
- * still fails here. An unrewritten derived line warns; it never blocks.
21
+ * still fails here.
22
+ *
23
+ * ## REVERSED 2026-09-01 — an unrewritten derived line now BLOCKS
24
+ *
25
+ * This header used to end the paragraph above with *"An unrewritten derived
26
+ * line warns; it never blocks."* That is no longer the behaviour, and the
27
+ * sentence is replaced rather than deleted so the reversal is legible.
28
+ *
29
+ * **The premise was never wrong; the conclusion drawn from it was.** A derived
30
+ * line does carry true evidence — the deriving reason plus citing SHAs — so it
31
+ * is not false-if-shipped. What the advisory conclusion assumed is that
32
+ * "unpolished-if-unedited" would in fact get edited. Measured at `b50b27281`,
33
+ * it did not: `_auto-derived, rewrite before merge:_` shipped in FIVE
34
+ * consecutive released sections — 14.9.0 (4 lines), 14.10.0 (2), 14.11.0 (4),
35
+ * 14.12.0 (4), 14.13.0 (4), eighteen lines in total. A warning that has been
36
+ * ignored eighteen times is not a warning; it is a comment.
37
+ *
38
+ * **Instructing authority:** the maintainer, in the closing instruction of the
39
+ * 2026-09-a inbox round, requiring that the release-placeholder defect be taken
40
+ * into a roadmap and fixed. Carried into
41
+ * `agents/roadmaps/archive/road-to-publication-integrity-hard-fail.md` § Phase 1, which
42
+ * is the record of this change.
43
+ *
44
+ * **What did NOT change.** The read stays scoped to the section under release
45
+ * (`extract_changelog_section` below), so the eighteen historical lines cannot
46
+ * red a future release; a filled field is still never judged for prose quality;
47
+ * and the `_none_` contradiction check is untouched.
22
48
  *
23
49
  * Derivation is deliberately conservative (documented per-label below):
24
50
  * a false red makes every release annoying; a miss only returns the head to
@@ -29,7 +55,8 @@
29
55
  * [--changelog <path>]
30
56
  *
31
57
  * `--from` defaults to the latest reachable release tag before `--to`
32
- * (default HEAD). Exit codes: 0 plausible · 1 contradiction · 2 usage error.
58
+ * (default HEAD). Exit codes: 0 plausible · 1 refusal (an unrewritten
59
+ * auto-derived head line, or a `_none_` contradiction) · 2 usage error.
33
60
  */
34
61
  import * as fs from 'node:fs';
35
62
  import * as path from 'node:path';
@@ -37,6 +64,7 @@ import process from 'node:process';
37
64
  import { fileURLToPath } from 'node:url';
38
65
 
39
66
  import {
67
+ DERIVED_MARKER,
40
68
  HEAD_LABELS,
41
69
  HEAD_NONE,
42
70
  type SpanCommit,
@@ -46,7 +74,10 @@ import {
46
74
  previous_release_tag,
47
75
  stale_draft_labels,
48
76
  } from './_lib/release_highlights.js';
49
- import { extract_changelog_section } from './_lib/release_material.js';
77
+ import {
78
+ CURATED_HEAD_INSTRUCTION,
79
+ extract_changelog_section,
80
+ } from './_lib/release_material.js';
50
81
  import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
51
82
 
52
83
  const _HERE = fileURLToPath(import.meta.url);
@@ -116,9 +147,17 @@ export function highlight_contradictions(
116
147
 
117
148
  /**
118
149
  * Exported so the advisory-vs-blocking decision is a fixture, not a claim.
150
+ *
151
+ * CORRECTED 2026-09-01 with the reversal. This comment used to say that
119
152
  * `stale_draft_labels` firing while the exit code stays 0 is the behaviour the
120
- * cadence blocker adjudicates; pinning it here makes either branch of that
121
- * decision a one-line diff with a test that notices.
153
+ * cadence blocker adjudicates. That is no longer what happens — the branch at
154
+ * the call site returns 1 — and the sentence is replaced rather than deleted
155
+ * because it was the load-bearing claim on the other side.
156
+ *
157
+ * What it got RIGHT is worth keeping: the decision was a one-line diff with a
158
+ * test that notices, and the reversal proved it. The two fixtures that pinned
159
+ * the advisory branch were updated in place, not deleted, so the change of
160
+ * contract reads off the diff.
122
161
  */
123
162
  export function main(argv: readonly string[]): number {
124
163
  let version: string | null = null;
@@ -171,6 +210,33 @@ export function main(argv: readonly string[]): number {
171
210
  process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
172
211
  return 2;
173
212
  }
213
+ // A LEAKED AUTHORING INSTRUCTION REFUSES THE RELEASE (roadmap § 2.2).
214
+ //
215
+ // A different mechanism from the marker check below: that one catches an
216
+ // unpolished claim, this one catches a reminder to the releaser that was
217
+ // never meant to be release content at all. `CHANGELOG.md` is the bare
218
+ // entry in `package.json` `files`, so a comment surviving here is
219
+ // published to npm — it did, twice, before Option A stopped the writer
220
+ // emitting it.
221
+ //
222
+ // SAME SCOPE as the marker check, and for the same reason: `section` is
223
+ // the ONE section `extract_changelog_section` cut for `--version`. A
224
+ // historical section carrying the identical comment is not read here and
225
+ // does not block this release. Detection is the named sentinel, never a
226
+ // shape match over comment prose.
227
+ if (section.body.includes(CURATED_HEAD_INSTRUCTION)) {
228
+ process.stderr.write(
229
+ `❌ the ${version} section still carries the generator's authoring instruction ` +
230
+ `(\`${CURATED_HEAD_INSTRUCTION}\`).\n` +
231
+ ' It is a reminder to the releaser, not release content, and CHANGELOG.md ' +
232
+ 'is published to npm.\n' +
233
+ ` Delete that comment line from the ${version} section. Only that section is ` +
234
+ 'read; historical\n' +
235
+ ' sections carrying the same comment are out of scope and do not block this ' +
236
+ 'release.\n',
237
+ );
238
+ return 1;
239
+ }
174
240
  const curated = parse_curated_head(section.body);
175
241
  if (!curated) {
176
242
  process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
@@ -199,19 +265,52 @@ export function main(argv: readonly string[]): number {
199
265
  'could contradict. Deleting the scan root is not the same state — that is the ' +
200
266
  'throwing branch, not this one.',
201
267
  });
202
- // A generator-derived line that nobody rewrote is a prose gap, never a
203
- // contradiction: the line's evidence is true, it is just unpolished. Warn
204
- // so the omission is visible, and keep the exit code owned solely by the
205
- // `_none_` check — a warning that reds the build is the guaranteed-red
206
- // failure mode this whole change exists to remove.
268
+ // A generator-derived line that nobody rewrote REFUSES the release.
269
+ //
270
+ // REVERSED 2026-09-01. This block used to warn and return the exit code to
271
+ // the `_none_` check alone, on the reasoning that a derived line is true
272
+ // and merely unpolished. The reasoning still holds and the conclusion does
273
+ // not: the warning was emitted and ignored through five consecutive
274
+ // released sections. See this file's header for the full record and the
275
+ // instructing authority.
276
+ //
277
+ // SCOPE, and it is the load-bearing half (roadmap § 1.3): `curated` comes
278
+ // from `parse_curated_head(section.body)` above, and `section` is the ONE
279
+ // section `extract_changelog_section` cut for `--version`. No read here
280
+ // reaches another section, another era file, or `docs/archive/`. That is
281
+ // what keeps eighteen historical marker lines from turning this into a
282
+ // permanent red on every future release.
283
+ const derived = derive_categories(span);
207
284
  const drafts = stale_draft_labels(curated);
208
285
  if (drafts.length > 0) {
209
- process.stdout.write(
210
- `⚠️ auto-derived head line(s) not yet rewritten for ${version}: ` +
211
- `${drafts.join(', ')} — advisory, not blocking.\n`,
286
+ process.stderr.write(
287
+ `❌ unrewritten auto-derived head line(s) in the ${version} section: ` +
288
+ `${drafts.join(', ')}\n` +
289
+ ` Each still carries \`${DERIVED_MARKER}\` — the generator's draft, not a ` +
290
+ 'curated claim.\n' +
291
+ ` Rewrite those line(s) in the ${version} section of the changelog. Only that ` +
292
+ 'section is read;\n' +
293
+ ' historical sections carrying the same marker are out of scope and do not ' +
294
+ 'block this release.\n',
212
295
  );
296
+ // The evidence, not just the verdict. The draft line cites SHAs and a
297
+ // deriving reason; what a releaser needs in order to write the real
298
+ // claim is the SUBJECT behind each SHA, and looking eleven of them up
299
+ // by hand is the friction that made this refusal cheaper to ignore
300
+ // than to satisfy across five releases. The `_none_` branch below has
301
+ // printed its evidence since it was built; this one now matches it.
302
+ for (const label of drafts) {
303
+ const hits = derived[label] ?? [];
304
+ if (hits.length === 0) {
305
+ continue;
306
+ }
307
+ process.stderr.write(` - **${label}:** derived from\n`);
308
+ for (const e of hits) {
309
+ process.stderr.write(` ${e}\n`);
310
+ }
311
+ }
312
+ return 1;
213
313
  }
214
- const derived = derive_categories(span);
215
314
  const contradictions = highlight_contradictions(curated, derived);
216
315
  if (contradictions.length === 0) {
217
316
  process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
@@ -15,7 +15,7 @@
15
15
  * (Python-only); these pure regex/parse helpers are replicated here
16
16
  * byte-for-byte from that module. Keep in lock-step with it.
17
17
  *
18
- * Every non-draft file under `agents/roadmaps/` (excluding `archive/`,
18
+ * Every non-draft file under `agents/roadmaps/` (excluding `archive/`, code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
19
19
  * `skipped/`, template/README/open-questions) MUST:
20
20
  * 1. Be parseable by the dashboard's `PHASE_RE` (>= one `## Phase <id>`).
21
21
  * 2. Have at least one trackable checkbox under every parsed phase.
@@ -49,6 +49,15 @@ const PHASE_RE =
49
49
  // FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
50
50
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
51
51
  const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
52
+ /**
53
+ * A carrier holds obligations deferred out of an archived parent. It is not
54
+ * schedulable work — every item in it has an unmet resumption trigger — and it
55
+ * has no `## Phase` headings by construction, so it is skipped exactly where a
56
+ * draft is. It differs from a draft in what protects it: a draft may be deleted
57
+ * freely, a carrier may not, and `lint_carrier_integrity` reds when one loses
58
+ * its receiver.
59
+ */
60
+ const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
52
61
 
53
62
  const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
54
63
  'template.md',
@@ -92,6 +101,11 @@ function is_draft(fm: Map<string, string>): boolean {
92
101
  return DRAFT_VALUES.has((fm.get('status') ?? '').toLowerCase());
93
102
  }
94
103
 
104
+ /** True for a `status: carrier` roadmap — see CARRIER_VALUES. */
105
+ function is_carrier(fm: Map<string, string>): boolean {
106
+ return CARRIER_VALUES.has((fm.get('status') ?? '').toLowerCase());
107
+ }
108
+
95
109
  /** Mirror update_roadmap_progress.is_roadmap_candidate (POSIX path parts). */
96
110
  function is_roadmap_candidate(p: string): boolean {
97
111
  const name = path.basename(p);
@@ -168,7 +182,7 @@ function find_active_roadmaps(root: string): string[] {
168
182
  continue;
169
183
  }
170
184
  const text = fs.readFileSync(p, 'utf-8');
171
- if (is_draft(parse_frontmatter(text))) {
185
+ if (is_draft(parse_frontmatter(text)) || is_carrier(parse_frontmatter(text))) {
172
186
  continue;
173
187
  }
174
188
  out.push(p);
@@ -345,6 +359,7 @@ export {
345
359
  CHECKBOX_RE,
346
360
  PHASE_RE,
347
361
  is_draft,
362
+ is_carrier,
348
363
  is_roadmap_candidate,
349
364
  parse_frontmatter,
350
365
  find_active_roadmaps,