@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
package/README.md CHANGED
@@ -27,7 +27,7 @@ It is both deep **and** disciplined — and honest about what it deliberately is
27
27
  - **Surgical uninstall** — removes only its own keys from a shared host config (matched by JSON-pointer + SHA-256), never a neighbour tool's entries.<!-- claim:surgical-uninstall -->
28
28
  - **Pack-scoped install** — writes the active pack only, not a 500-artefact dump.
29
29
 
30
- **What it deliberately is *not*** — the core is a governance layer with optional, individually opt-in embedded engines (code intelligence, gated reach, the setup GUI, the bench lab — [ADR-124](docs/decisions/ADR-124-embedded-engine-doctrine.md)): **no background daemon, no separate state database, no self-rewriting memory, no auto-build pipeline.** Engines are never mandatory, never default-on without measured lift, and terminate with the command that invoked them. The host agent runs the loop; every learned change is human-reviewed; the same layer stays portable across tools. Capability without a process to babysit.
30
+ **What it deliberately is *not*** — the core is a governance layer with optional, individually opt-in embedded engines (code intelligence, gated reach, the setup GUI, the bench lab — [ADR-124](docs/decisions/ADR-124-embedded-engine-doctrine.md)): **no mandatory or always-on daemon, no separate state database, no self-rewriting memory, no auto-build pipeline.** Engines are never mandatory, never default-on without measured lift, and terminate with the command that invoked them. The host agent runs the loop; every learned change is human-reviewed; the same layer stays portable across tools. Capability without a process to babysit.
31
31
 
32
32
  > **Where this comes from (honest provenance).** The skills, rules and personas
33
33
  > are distilled from real production work on TypeScript and PHP codebases. The
@@ -487,6 +487,7 @@ The question is never "what does the file say" but **"what does it become here"*
487
487
  | reference material read on demand | a `guideline` or `context` |
488
488
  | a measured finding | a `decision-record`/ADR, not a rule |
489
489
  | a defect claim | a roadmap item, once verified |
490
+ | a claim that maps onto an existing stub | **not a fresh artifact — a stub-blocked finding, see below** |
490
491
  | a consolidation omitting a parent | **not an artifact — a discharge, see below** |
491
492
 
492
493
  **An omitted parent is discharged, never left silent.** When Phase 2's `lineage`
@@ -511,6 +512,41 @@ A `ghost:` reading — a declared parent with no matching file — has the same
511
512
  discharges plus a fourth that is really a correction: fix the name. A lineage
512
513
  naming a plan nobody can open is the cheaper half of the same defect.
513
514
 
515
+ **A survivor that maps onto an existing stub is never resolved into a verdict
516
+ line.** When a verified claim lands on something `agents/roadmaps/stubs/` already
517
+ holds, the mapping is neither "a defect claim → a roadmap item" nor "nothing to
518
+ do": the plan exists, and something is holding it. Before this row the table had
519
+ no entry for that state, so a run that found one had no prescribed output and
520
+ wrote a summary sentence instead. The round that produced this row closed exactly
521
+ so — *"Not a neglected guard: the cost of an unmade owner decision. On the owner's
522
+ desk."* That sentence is accurate, and it is why nine feedback rounds on one guard
523
+ produced no executable item: it names a state and hands nobody anything to do.
524
+
525
+ The run's output MUST carry all four of these, per stub matched. Three of them
526
+ are the difference between a finding and a shrug, and the fourth is already
527
+ computed upstream:
528
+
529
+ 1. **The stub path** — `agents/roadmaps/stubs/<slug>.md`, so a reader opens it
530
+ instead of searching for it.
531
+ 2. **Its blocker slug** — the `### blocker: <slug>` holding it, named. Where the
532
+ hold is recorded outside any blocker section, say so in those words and name
533
+ the section; "waiting on a decision" with nothing named is the verdict line
534
+ again, one word longer.
535
+ 3. **Its age in days** — days since the file first appeared under
536
+ `agents/roadmaps/stubs/`, from
537
+ `git log --diff-filter=A --format=%ad --date=short -- <path> | tail -1`. A
538
+ stub parked eleven days and one parked two hundred are different findings and
539
+ a reader cannot tell them apart from the path.
540
+ 4. **The recurrence count from Phase 4c** — how many earlier rounds raised the
541
+ same subject. A first arrival and a ninth are different findings, 4c has
542
+ already counted it, and dropping it here is what let one subject arrive nine
543
+ times as if each were new.
544
+
545
+ Missing any of the four leaves the survivor undischarged. This row does not
546
+ decide what happens to the stub — promotion is the estate's question and this
547
+ command does not answer it — it only forbids resolving the survivor into prose
548
+ that names no artifact.
549
+
514
550
  Three hard defaults, from this repo's own scar tissue:
515
551
 
516
552
  - **Extend before you create.** Run the four-surface overlap scan from
@@ -149,10 +149,25 @@ than less: the gate can now read the PR's real `baseRefName` from the forge, so
149
149
  a stacked or release-line PR is measured against the branch it will actually
150
150
  merge into instead of against the repo default.
151
151
 
152
+ **The whole SEQUENCE is executable now, not just this step.** All six steps —
153
+ fetch, integrate the base set, regenerate, verify, re-check freshness, then hand
154
+ the push back to you — sit behind one runner, and a dry mode prints each step
155
+ with its outcome without touching the tree. In this package that runner is the
156
+ `push-ready` target in `taskfiles/dev.yml`; a consumer project wires the same
157
+ six steps behind whatever its own task runner is, with steps 3 and 4 being its
158
+ own generator and verify commands. The pre-push hook refuses and points at the
159
+ runner; the hook never merges, because a merge inside `pre-push` rewrites the
160
+ tree at the moment you believe your work is finished.
161
+
152
162
  **The resolution is executable now, not just described.**
153
163
  `./scripts-run src/scripts/sync_pr_branch` resolves the base from the open PR
154
164
  (so a stacked or release-line PR is measured against what it actually merges
155
- into), fetches, and merges it in when the branch is behind. On a conflict it
165
+ into), fetches, and merges it in when the branch is behind. The base is a
166
+ **set**: when the PR targets something other than the default branch, whether
167
+ the default branch joins the set is a per-target policy read from the *target's
168
+ own commit*, never from this branch — see `src/scripts/_lib/branch_convergence.ts`.
169
+ A non-default target with no policy entry is refused, not guessed. The default
170
+ is merged first, so a conflict surfaces against the broader base first. On a conflict it
156
171
  STOPS and splits the conflicted paths into generated, remeasured and authored —
157
172
  the first has one correct resolution (regenerate), the second's is to re-run the
158
173
  measurement on the merged tree (a ratchet baseline, where picking a side is how
@@ -10,7 +10,7 @@ identically to a win: a loop that hit its iteration cap, a gate that could not
10
10
  run, an approval that never came — each ending with a summary that says the work
11
11
  is done.
12
12
 
13
- ## The six states
13
+ ## The seven states
14
14
 
15
15
  | state | what it means | what it is NOT |
16
16
  |---|---|---|
@@ -20,6 +20,23 @@ is done.
20
20
  | `approval-required` | The work is ready and the next action crosses a gate the agent may not cross alone. | Not `blocked`: nothing is missing, and the run produced a deliverable. |
21
21
  | `exhausted` | A declared budget ran out — iterations, tokens, spend, wall-clock. | **Never** `success`, whatever partial progress exists. The cap firing IS the outcome. |
22
22
  | `stagnated` | The budget has not run out and progress has stopped: the same failure signature is repeating. | Not `exhausted`. Stagnation is detected by a NO-PROGRESS signal, not by a counter. |
23
+ | `premise-invalidated` | The plan premise moved: the situational-awareness fingerprint the run engaged under (`origin/main` plus every open PR head) was re-observed and differs. | Not `exhausted` — the budget is untouched. Not `stagnated` — progress may have been fine. Not `blocked` — nothing is missing; what the run knew went stale. |
24
+
25
+ **Why `premise-invalidated` is not `blocked` with a reason field.** Added
26
+ 2026-09-03 (AI council, unanimous) when the continuation ladder needed a word for
27
+ a run whose base ref and open-PR set had moved under it. Reusing `blocked` keeps
28
+ the arity at six and loses two things that are not decorative: the ability to
29
+ COUNT premise invalidation separately from a missing precondition, and
30
+ type-enforced exhaustive handling in the consumers that switch on the state. The
31
+ remedies differ as sharply as `exhausted` and `stagnated` do — `blocked` says
32
+ obtain the missing thing, `premise-invalidated` says re-probe and re-plan against
33
+ the world as it now is.
34
+
35
+ A consumer that cannot represent it reports `blocked`, which is the DECLARED
36
+ downgrade in `_lib/outcome_vocabularies.ts` and not an ad-hoc substitution. The
37
+ value domain is versioned (`RUN_TERMINAL_VOCABULARY_VERSION`, v2), and a reader
38
+ meeting an unrecognised persisted value reports it as not-recorded rather than
39
+ crashing or guessing.
23
40
 
24
41
  **Why `exhausted` and `stagnated` are separate**, when both stop a loop: they
25
42
  have different remedies and different next actions. `exhausted` says *the work
@@ -40,8 +57,8 @@ budget-exhausted stop indistinguishable from a completed one in a dashboard.
40
57
  | `[~]` | deferred | `blocked` or `approval-required`, undistinguished |
41
58
  | `[-]` | cancelled | *(a decision, not a run outcome)* |
42
59
 
43
- **Three states have no glyph, and they are exactly the ones a validation budget
44
- and a hard-blocker class produce:**
60
+ **Four states have no glyph, and they are exactly the ones a validation budget,
61
+ a hard-blocker class and a drift detector produce:**
45
62
 
46
63
  1. **`exhausted`** — the run stopped at its cap with the step still open. The
47
64
  glyph stays `[ ]`, which is identical to *never attempted*. A reader cannot
@@ -51,6 +68,8 @@ and a hard-blocker class produce:**
51
68
  3. **`approval-required`** — collapses into `[~]` alongside `blocked`, so a
52
69
  roadmap cannot show that the work is FINISHED and waiting on a human, versus
53
70
  not startable at all.
71
+ 4. **`premise-invalidated`** — same `[ ]` again, and the reader cannot see that
72
+ the step was not attempted because the plan it belonged to went stale.
54
73
 
55
74
  **This is recorded rather than fixed.** Adding glyphs would change a format that
56
75
  `update_roadmap_progress`, `check_roadmap_trackable`, `lint_empty_roadmaps` and
@@ -75,11 +94,13 @@ applies unchanged.
75
94
 
76
95
  ## Reporting contract
77
96
 
78
- A closing report names the state **by one of the six words above**, and where the
97
+ A closing report names the state **by one of the seven words above**, and where the
79
98
  state is not `success` it names what would change it:
80
99
 
81
100
  - `exhausted` — the budget that ran out, its value, and what was achieved inside it.
82
101
  - `stagnated` — the repeating failure signature, and the attempts it survived.
102
+ - `premise-invalidated` — which fingerprint was claimed, which was observed, and
103
+ that the next act is a re-probe rather than another iteration.
83
104
  - `blocked` — the missing thing, by name.
84
105
  - `approval-required` — the exact action awaiting approval.
85
106
  - `clean-no-op` — what was scanned, so "nothing to do" is distinguishable from
@@ -22,6 +22,9 @@ collision_ok:
22
22
  "implement": "fresh implementation is where over-commenting lands"
23
23
  # obligation: line 5
24
24
  obligation_frequency: "per-edit"
25
+ enforced_by:
26
+ - "validator:src/scripts/lint_code_comments.ts"
27
+ - "hook:comment-discipline"
25
28
  ---
26
29
 
27
30
  # Code Comment Discipline
@@ -37,6 +40,9 @@ NO DOCBLOCK THAT ONLY MIRRORS THE NATIVE SIGNATURE.
37
40
  DOCBLOCKS EARN THEIR PLACE ONLY WITH MACHINE-RELEVANT PRECISION
38
41
  (GENERICS, ARRAY SHAPES, NON-TRIVIAL UNIONS) OR GENUINE WHY-CONTEXT.
39
42
  WHEN IN DOUBT: NO COMMENT. SHORTER IS BETTER. NONE IS OFTEN BEST.
43
+ A COMMENT NAMING WHERE THE CODE CAME FROM — A ROADMAP PATH, A PHASE AND STEP,
44
+ A PROTOTYPE — IS EVIDENCE IN THE WRONG PLACE. PROVENANCE IS NOT A WHY.
45
+ A ROADMAP CANNOT LICENSE A COMMENT THIS RULE FORBIDS.
40
46
  ```
41
47
 
42
48
  ## What a comment is FOR
@@ -31,8 +31,8 @@ and dropped **402 entries** from the model-visible skills list
31
31
  projection moved that to 330 and stripped the descriptions anyway.
32
32
 
33
33
  402 is the **host's own count against the host's own denominator**, which is not
34
- this package's: the same install projects 297 skills, so the figure is a
35
- magnitude, never a subtraction anyone can perform. What it establishes is that
34
+ this package's: that install projected 297 skills <!-- count: dated -->, so the figure
35
+ is a magnitude, never a subtraction anyone can perform. What it establishes is that
36
36
  most of the catalogue is not model-visible — which is all this rule needs.
37
37
 
38
38
  So "I do not see a skill for this" is evidence about the **delivery**, never
@@ -57,8 +57,8 @@ A skill claiming `rich` MUST satisfy ALL three:
57
57
  carry a `load_context` pointer instead.
58
58
 
59
59
  3. **≤ 15 % of the suite's skills may claim `rich`** — the CI linter counts
60
- rich-tagged skills and hard-fails if the ratio is exceeded. With ~290 skills
61
- (288 as-of 2026-08), the cap is ~43. Do not claim `rich` for convenience; claim it only for
60
+ rich-tagged skills and hard-fails if the ratio is exceeded. With 299 skills
61
+ the cap is ~44. Do not claim `rich` for convenience; claim it only for
62
62
  irreducible-complexity skills.
63
63
 
64
64
  ## Telegraph-speak amendment
@@ -12,8 +12,8 @@
12
12
  * `urp.collect()` completion criterion, and exit code 0. snake_case kept.
13
13
  *
14
14
  * A roadmap that has reached `count_open == 0` and `count_deferred == 0` is
15
- * **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites
16
- * inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`)
15
+ * **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites code-comment-allow provenance-comment -- the sweep's own operand
16
+ * inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`) code-comment-allow provenance-comment -- the rewrite this sweep performs
17
17
  * across tracked files so links never break, and regenerates the dashboard.
18
18
  *
19
19
  * It replaces the old **merge-gate** (keep one item open + a manual post-merge
@@ -136,7 +136,7 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
136
136
  *
137
137
  * Two classes, and the second is why this predicate exists at all:
138
138
  *
139
- * - `agents/evidence/**` — a committed record of what was true, or of what a
139
+ * - `agents/evidence/**` — a committed record of what was true, or of what a code-comment-allow provenance-comment -- the predicate's operand set
140
140
  * reviewer was shown, at one point in time. Retroactively editing it makes it
141
141
  * claim something that did not happen, which is the failure
142
142
  * `evaluator-independence` exists to prevent ("the prompt ships with the
@@ -146,7 +146,7 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
146
146
  * AND makes `git apply` target a path the diff never created.
147
147
  *
148
148
  * Measured 2026-08-11: archiving one roadmap rewrote four frozen artifacts
149
- * under `agents/evidence/reviews/`, including the `diff.patch` headers of a
149
+ * under `agents/evidence/reviews/`, including the `diff.patch` headers of a code-comment-allow provenance-comment -- names what the predicate protects
150
150
  * recorded review input. Nothing caught it — the sweep reported it as
151
151
  * "4 ref(s) migrated", which reads like success.
152
152
  */
@@ -346,7 +346,7 @@ function _escapeRe(v: string): string {
346
346
  * bare `- <name>` item inside the `relates:` block, which is a declaration the
347
347
  * author made deliberately. A mention anywhere else in the file does not count.
348
348
  */
349
- function _relatesTo(text: string, slug: string): boolean {
349
+ export function _relatesTo(text: string, slug: string): boolean {
350
350
  const lines = text.split('\n');
351
351
  const start = lines.findIndex((l) => /^relates:/.test(l));
352
352
  if (start === -1) return false;
@@ -654,7 +654,7 @@ function _regen_dashboard(root: string, dry_run: boolean): void {
654
654
  /**
655
655
  * Rebuild the archive index after a move, when the builder is present.
656
656
  *
657
- * The sweep is the one operation that changes what `agents/roadmaps/archive/`
657
+ * The sweep is the one operation that changes what `agents/roadmaps/archive/` code-comment-allow provenance-comment -- the directory this function writes
658
658
  * contains, so it is the one operation that can leave the generated index
659
659
  * stale — and the drift gate on that index runs in the same PR the sweep runs
660
660
  * in. Regenerating here keeps the two from disagreeing by construction rather
@@ -226,9 +226,18 @@ export function guardedBaselineProblems(items: readonly GuardedBaselineItem[]):
226
226
  *
227
227
  * HONEST SCOPE. Only a PATH trigger is machine-checkable: a token containing `/`
228
228
  * is resolved against the repo root and stale iff it exists. A bare symbol name
229
- * cannot be decided from a path, so it is returned as `unverifiable` rather than
230
- * silently reading as "not stale" — an unchecked trigger that looks checked is
231
- * the failure this whole state exists to avoid.
229
+ * cannot be decided from a path, so a record carrying ONLY symbol tokens is
230
+ * returned as `unverifiable` rather than silently reading as "not stale" — an
231
+ * unchecked trigger that looks checked is the failure this whole state exists
232
+ * to avoid.
233
+ *
234
+ * A record whose trigger carries AT LEAST ONE path token is decided by that
235
+ * path and is NOT reported as unverifiable, even when a companion symbol token
236
+ * sits beside it. Reporting it would be the mirror of the failure above: a
237
+ * checked trigger that looks unchecked. Measured 2026-09-01 — three of the four
238
+ * lines the dashboard printed as "not machine-checkable" carried a path token
239
+ * and were therefore already decidable, which is noise that trains a reader to
240
+ * skip the section where the one genuinely undecidable trigger also lives.
232
241
  */
233
242
  export function guardedBaselineStaleness(
234
243
  items: readonly GuardedBaselineItem[],
@@ -241,16 +250,22 @@ export function guardedBaselineStaleness(
241
250
  if (trigger === '') {
242
251
  continue;
243
252
  }
244
- for (const token of trigger.split(/[\s,]+/).filter((t) => t !== '')) {
245
- if (!token.includes('/')) {
246
- unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
247
- } else if (fs.existsSync(path.join(repo_root, token))) {
253
+ const tokens = trigger.split(/[\s,]+/).filter((t) => t !== '');
254
+ const paths = tokens.filter((t) => t.includes('/'));
255
+ for (const token of paths) {
256
+ if (fs.existsSync(path.join(repo_root, token))) {
248
257
  stale.push(
249
258
  `line ${it.line} (${it.label}): recheck_when \`${token}\` now exists — ` +
250
259
  're-verify against the real mechanism',
251
260
  );
252
261
  }
253
262
  }
263
+ if (paths.length > 0) {
264
+ continue;
265
+ }
266
+ for (const token of tokens) {
267
+ unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
268
+ }
254
269
  }
255
270
  return { stale, unverifiable };
256
271
  }
@@ -66,6 +66,48 @@ export interface StubRecord {
66
66
  owner_decisions: number;
67
67
  overdue: boolean;
68
68
  days_overdue: number | null;
69
+ /** Frontmatter `design_validated:` — a citation, or null when absent. */
70
+ design_validated: string | null;
71
+ /** Frontmatter `capability_gap:` — `none` means the stub declares no gap. */
72
+ capability_gap: string | null;
73
+ /** Frontmatter `blocker_class:` — `estate` | `budget` | `product`. */
74
+ blocker_class: string | null;
75
+ /** Frontmatter `blocker_opened:` — ISO date the hold began, or null. */
76
+ blocker_opened: string | null;
77
+ /** Releases published since `blocker_opened`, or null when it is absent. */
78
+ releases_since_blocked: number | null;
79
+ /** All three membership conditions hold. See {@link BLOCKED_QUICKWIN_FIELDS}. */
80
+ blocked_quickwin: boolean;
81
+ }
82
+
83
+ /**
84
+ * The three frontmatter keys that decide fourth-bucket membership.
85
+ *
86
+ * Read as SCALARS, never as substrings of the body. A substring match over
87
+ * prose is what put a validated fix and a waiting preference into one bucket
88
+ * in the first place; doing it with three substrings instead of one would be
89
+ * worse, not better. A stub missing any of the three is not a member and falls
90
+ * through to the OWNER bucket exactly as before — absence is never a member.
91
+ */
92
+ export const BLOCKED_QUICKWIN_FIELDS = ['design_validated', 'capability_gap', 'blocker_class'] as const;
93
+
94
+ /** Blocker classes that make a stub a BLOCKED QUICK-WIN rather than a product wait. */
95
+ export const QUICKWIN_BLOCKER_CLASSES = ['estate', 'budget'] as const;
96
+
97
+ /**
98
+ * All three conditions, read from frontmatter only.
99
+ *
100
+ * A recorded design validation, an explicit `capability_gap: none`, and an open
101
+ * blocker that is an estate or budget decision rather than a product one.
102
+ */
103
+ export function is_blocked_quickwin(
104
+ design_validated: string | null,
105
+ capability_gap: string | null,
106
+ blocker_class: string | null,
107
+ ): boolean {
108
+ if (design_validated === null || design_validated === '') return false;
109
+ if (capability_gap !== 'none') return false;
110
+ return (QUICKWIN_BLOCKER_CLASSES as readonly string[]).includes(blocker_class ?? '');
69
111
  }
70
112
 
71
113
  function _isDir(p: string): boolean {
@@ -188,7 +230,48 @@ function _parseIso(s: string): Date | null {
188
230
 
189
231
  /** Read every stub and classify it. `today` is injectable so tests are stable. */
190
232
  export function scan(root: string, today: string): StubRecord[] {
191
- return scan_dir(path.join(root, STUB_DIR), today);
233
+ return scan_dir(path.join(root, STUB_DIR), today, release_dates(root));
234
+ }
235
+
236
+ /**
237
+ * Changelog files that carry released sections, newest era first.
238
+ *
239
+ * BOTH are read, and that is the whole point of the pair. The era split moved
240
+ * older releases into the archive, so a count taken over the current file alone
241
+ * silently reports zero for anything before the split — measured: 14.11.0 lives
242
+ * only in the archive, and grepping `CHANGELOG.md` for it returns nothing.
243
+ */
244
+ export const CHANGELOG_ERAS = ['CHANGELOG.md', 'docs/archive/CHANGELOG-pre-14.12.0.md'] as const;
245
+
246
+ /** `## [x.y.z](…) (YYYY-MM-DD)` — a released section heading with its date. */
247
+ const RELEASE_HEADING = /^## \[\d+\.\d+\.\d+\][^\n]*\((\d{4}-\d{2}-\d{2})\)\s*$/gm;
248
+
249
+ /** Every released section's date across both eras, unsorted. */
250
+ export function release_dates(root: string): string[] {
251
+ const out: string[] = [];
252
+ for (const rel of CHANGELOG_ERAS) {
253
+ let text: string;
254
+ try {
255
+ text = fs.readFileSync(path.join(root, rel), 'utf8');
256
+ } catch {
257
+ continue;
258
+ }
259
+ for (const m of text.matchAll(RELEASE_HEADING)) {
260
+ if (m[1] !== undefined) out.push(m[1]);
261
+ }
262
+ }
263
+ return out;
264
+ }
265
+
266
+ /**
267
+ * Releases published strictly AFTER the day the hold began.
268
+ *
269
+ * Strictly after, because a release cut on the same day cannot have carried a
270
+ * fix the hold was blocking — counting it would inflate the figure the
271
+ * council's falsifier is read against.
272
+ */
273
+ export function releases_since(opened: string, dates: readonly string[]): number {
274
+ return dates.filter((d) => d > opened).length;
192
275
  }
193
276
 
194
277
  /**
@@ -198,7 +281,11 @@ export function scan(root: string, today: string): StubRecord[] {
198
281
  * immediately re-join — a fragile round trip past exactly the directory both
199
282
  * callers care about.
200
283
  */
201
- export function scan_dir(base: string, today: string): StubRecord[] {
284
+ export function scan_dir(
285
+ base: string,
286
+ today: string,
287
+ releaseDates: readonly string[] = [],
288
+ ): StubRecord[] {
202
289
  if (!_isDir(base)) return [];
203
290
  const now = _parseIso(today);
204
291
  const out: StubRecord[] = [];
@@ -222,6 +309,10 @@ export function scan_dir(base: string, today: string): StubRecord[] {
222
309
  const review_by = _scalar(front, 'review_by');
223
310
  const reviewed_at = _scalar(front, 'reviewed_at');
224
311
  const probeVal = _scalar(front, 'probe');
312
+ const design_validated = _scalar(front, 'design_validated');
313
+ const capability_gap = _scalar(front, 'capability_gap');
314
+ const blocker_class = _scalar(front, 'blocker_class');
315
+ const blocker_opened = _scalar(front, 'blocker_opened');
225
316
  const shape: StubShape = text.includes(TRANSFER_MARKER) ? 'transfer' : 'orgmode';
226
317
  let overdue = false;
227
318
  let days_overdue: number | null = null;
@@ -242,6 +333,13 @@ export function scan_dir(base: string, today: string): StubRecord[] {
242
333
  owner_decisions: count_owner_decisions(text),
243
334
  overdue,
244
335
  days_overdue,
336
+ design_validated,
337
+ capability_gap,
338
+ blocker_class,
339
+ blocker_opened,
340
+ releases_since_blocked:
341
+ blocker_opened === null ? null : releases_since(blocker_opened, releaseDates),
342
+ blocked_quickwin: is_blocked_quickwin(design_validated, capability_gap, blocker_class),
245
343
  });
246
344
  }
247
345
  return out;
@@ -251,6 +349,8 @@ export interface Counts {
251
349
  overdue: number;
252
350
  owner_decisions: number;
253
351
  missing_review_by: number;
352
+ /** ADDITIVE 2026-09-01. Existing keys are unchanged in name, type and meaning. */
353
+ blocked_quickwin: number;
254
354
  total: number;
255
355
  }
256
356
 
@@ -259,6 +359,7 @@ export function counts(records: StubRecord[]): Counts {
259
359
  overdue: records.filter((r) => r.overdue).length,
260
360
  owner_decisions: records.reduce((s, r) => s + r.owner_decisions, 0),
261
361
  missing_review_by: records.filter((r) => r.review_by === null).length,
362
+ blocked_quickwin: records.filter((r) => r.blocked_quickwin).length,
262
363
  total: records.length,
263
364
  };
264
365
  }
@@ -328,6 +429,21 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
328
429
  }
329
430
  }
330
431
 
432
+ const quickwins = records.filter((r) => r.blocked_quickwin);
433
+ if (quickwins.length > 0) {
434
+ process.stdout.write(
435
+ `\n 🔓 ${quickwins.length} BLOCKED QUICK-WIN(S) — design validated, no capability gap,\n` +
436
+ ' held by an estate or budget decision rather than a product one:\n',
437
+ );
438
+ for (const r of quickwins) {
439
+ const since =
440
+ r.releases_since_blocked === null
441
+ ? 'no `blocker_opened:`'
442
+ : `${String(r.releases_since_blocked)} release(s) since ${r.blocker_opened}`;
443
+ process.stdout.write(` ${r.file} (${r.blocker_class}, ${since})\n`);
444
+ }
445
+ }
446
+
331
447
  const noProbe = records.filter((r) => !r.probe_none && !r.has_probe_heading);
332
448
  if (noProbe.length > 0) {
333
449
  process.stdout.write(
@@ -12,7 +12,7 @@
12
12
  * `archive_completed_roadmaps` can consume it the same way Python does. snake_case
13
13
  * is kept on the public surface.
14
14
  *
15
- * Scans every roadmap under `agents/roadmaps/` (excluding `archive/`, `skipped/`,
15
+ * Scans every roadmap under `agents/roadmaps/` (excluding `archive/`, `skipped/`, code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
16
16
  * `template.md`, `README.md`, `open-questions*.md`), counts checkbox states per
17
17
  * phase, and writes a dashboard at `agents/roadmaps-progress.md` (outside the
18
18
  * `roadmaps/` folder to keep it clean) with:
@@ -96,7 +96,7 @@ const EXCLUDE_DIRS: ReadonlySet<string> = new Set(['archive', 'skipped', 'stubs'
96
96
 
97
97
  // FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
98
98
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---[ \t\n\r\f\v]*\n/;
99
- const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
99
+ const UNSCHEDULED_VALUES: ReadonlySet<string> = new Set(['draft', 'carrier']);
100
100
 
101
101
  const MERGE_GATED_RE = /merge-gated/i;
102
102
  // PR_NUM_RE = re.compile(r"pr\s*[=#:]?\s*#?\s*(\d+)", re.IGNORECASE)
@@ -289,8 +289,8 @@ function _stripQuotes(s: string): string {
289
289
  return out;
290
290
  }
291
291
 
292
- function is_draft(fm: Record<string, string>): boolean {
293
- return DRAFT_VALUES.has((fm['status'] ?? '').toLowerCase());
292
+ function is_unscheduled(fm: Record<string, string>): boolean {
293
+ return UNSCHEDULED_VALUES.has((fm['status'] ?? '').toLowerCase());
294
294
  }
295
295
 
296
296
  /**
@@ -752,7 +752,7 @@ function collect(roadmap_root: string): RoadmapStats[] {
752
752
  continue;
753
753
  }
754
754
  const text = fs.readFileSync(p, { encoding: 'utf-8' });
755
- if (is_draft(parse_frontmatter(text))) {
755
+ if (is_unscheduled(parse_frontmatter(text))) {
756
756
  continue;
757
757
  }
758
758
  const stats = parse_roadmap(p, roadmap_root);
@@ -820,7 +820,7 @@ function collect_parked(roadmap_root: string): ParkedRoadmap[] {
820
820
  } catch {
821
821
  continue;
822
822
  }
823
- if (is_draft(parse_frontmatter(text))) {
823
+ if (is_unscheduled(parse_frontmatter(text))) {
824
824
  continue;
825
825
  }
826
826
  const cell = resume_cell(text);
@@ -1305,7 +1305,7 @@ function _parseArgs(argv: readonly string[]): Args {
1305
1305
  }
1306
1306
 
1307
1307
  /**
1308
- * When the default cwd carries no `agents/roadmaps/`, fall back to the git
1308
+ * When the default cwd carries no `agents/roadmaps/`, fall back to the git code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
1309
1309
  * toplevel — hook and IDE invocations often run from a subdirectory, and a
1310
1310
  * cwd-only resolution silently skipped the dashboard there. An explicit
1311
1311
  * `--repo-root` always wins; a cwd that has the directory is used as-is
@@ -1479,7 +1479,7 @@ export {
1479
1479
  TITLE_RE,
1480
1480
  FRONTMATTER_RE,
1481
1481
  parse_frontmatter,
1482
- is_draft,
1482
+ is_unscheduled,
1483
1483
  is_roadmap_candidate,
1484
1484
  count_checkboxes,
1485
1485
  parse_blockers,
@@ -39,7 +39,7 @@ schema_version: 1
39
39
  # CI guard: a release bump of `package.json` must update this value
40
40
  # in lockstep — see scripts/check_template_pin_drift.ts (road-to-
41
41
  # portable-runtime-and-update-check P3.3).
42
- agent_config_version: "14.13.0"
42
+ agent_config_version: "14.14.1"
43
43
 
44
44
  # --- Project identity ---
45
45
  project:
@@ -77,6 +77,7 @@ export const REGISTRY = [
77
77
  { name: 'hooks:install', disposition: 'delegate', synopsis: 'Install the combined pre-commit hook.' },
78
78
  { name: 'hooks:status', disposition: 'delegate', synopsis: 'Print the runtime hook matrix (per-platform install + bindings).' },
79
79
  { name: 'hooks:doctor', disposition: 'delegate', synopsis: 'Diagnose hook health: concerns, posture, missing trampolines (read-only).' },
80
+ { name: 'hooks:effect', disposition: 'delegate', synopsis: 'Does a bound concern actually FIRE here? Sandboxed synthetic dispatch per concern; five per-concern states, four host verdicts (read-only).' },
80
81
  { name: 'routing:doctor', disposition: 'delegate', synopsis: 'Live routing diagnosis: per-gate ACTIVE/INACTIVE with reason, chain, freshness, orchestration state (read-only).' },
81
82
  { name: 'workspace:doctor', disposition: 'delegate', synopsis: 'Workspace identity with provenance (repo root, main worktree, current worktree, branch, PR base) + session claim and worktree pressure (read-only).' },
82
83
  { name: 'route:explain', disposition: 'delegate', synopsis: 'Deterministic rule-routing trace for one prompt: matched triggers, tier, disposition, budget, rejected candidates (trigger-match level only).' },