@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,163 @@
1
+ /**
2
+ * `doctor --strict` — the failing exit mode, its threshold, and its kill switch.
3
+ *
4
+ * A separate module rather than more lines in `cmd_doctor.ts`, which is already
5
+ * far past the source-size budget: every line added there is counted excess by
6
+ * `check_source_size_budget`, and a ratchet that only turns one way is a
7
+ * standing instruction to put new code somewhere else.
8
+ */
9
+ /** Structural, so this module needs no import from its caller. */
10
+ type Dict = Record<string, unknown>;
11
+ interface StrictOptions {
12
+ strict: boolean;
13
+ strict_level: string;
14
+ }
15
+
16
+ function eprint(msg: string): void {
17
+ process.stderr.write(`${msg}\n`);
18
+ }
19
+
20
+ /** Raised on an unknown `--strict-level`; the caller maps it to its argparse exit. */
21
+ export class StrictLevelError extends Error {}
22
+
23
+ /**
24
+ * `--strict` — the failing exit mode, and the ONE decision behind it.
25
+ *
26
+ * `doctor-exit-contract` was resolved by AI council on 2026-09-03 (members
27
+ * anthropic, openai), unanimously **(b)**: `doctor` keeps its exit contract and
28
+ * a separate strict mode carries the failing exit, leaving current callers
29
+ * untouched at the cost of one more surface. ADR-041 governs verb growth, so a
30
+ * flag rather than a verb; and the existing contract is load-bearing for callers
31
+ * that run `doctor` for information rather than for permission.
32
+ *
33
+ * WHAT WAS ALREADY TRUE, measured rather than assumed. The originating roadmap
34
+ * says `doctor` "writes nothing and always exits zero". Half of that is wrong:
35
+ * the default run already returns 1 on DRIFT (`return drift_present ? 1 : 0`),
36
+ * and `--ci` already folds check failures in on top. What is genuinely missing
37
+ * is a failing exit for check failures OUTSIDE the `--ci` JSON contract — `--ci`
38
+ * forces `opts.json = true`, so there is no human-readable strict run — and that
39
+ * gap is what `--strict` closes. Recorded here because a comment claiming to
40
+ * implement a premise the code contradicts is worse than no comment.
41
+ *
42
+ * SEVERITY IS CONFIGURABLE, per a condition both council seats attached:
43
+ * `--strict-level fail` (default) fails on `fail` rows; `--strict-level warn`
44
+ * also fails on `warn`. A caller adopting strict picks its own bar rather than
45
+ * inheriting one.
46
+ *
47
+ * THE KILL SWITCH is `AGENT_CONFIG_DOCTOR_NO_FAIL=1`, the other condition both
48
+ * seats attached: an immediate way to disable failing exits that is not a code
49
+ * rollback. When set, `--strict` still REPORTS every finding and still emits
50
+ * `can_proceed: false`; only the exit code is suppressed, and the suppression is
51
+ * announced on stderr so a green run cannot be silently bought. A switch that
52
+ * hid the finding as well would be the "warning ignored eighteen times" shape
53
+ * this repository already documents.
54
+ */
55
+ export const STRICT_DEFAULT_LEVEL = 'fail';
56
+ export const STRICT_LEVELS: ReadonlySet<string> = new Set(['fail', 'warn']);
57
+ /** Env var that suppresses the failing exit without suppressing the finding. */
58
+ export const STRICT_KILL_SWITCH = 'AGENT_CONFIG_DOCTOR_NO_FAIL';
59
+
60
+ /** Statuses that trip `--strict` at the given level. */
61
+ export function strictTripping(level: string): ReadonlySet<string> {
62
+ return level === 'warn' ? new Set(['fail', 'warn']) : new Set(['fail']);
63
+ }
64
+
65
+ /**
66
+ * The machine-readable "may I proceed" answer — nothing in this tree carried
67
+ * one before (`grep -rc can_proceed src` returned zero hits tree-wide).
68
+ *
69
+ * Deliberately independent of `--strict`: a caller asking the JSON whether it
70
+ * may proceed gets the same answer whether or not the exit code was configured
71
+ * to fail, because the two questions are different. The exit code is a policy;
72
+ * `can_proceed` is a fact.
73
+ */
74
+ export function canProceed(driftPresent: boolean, checks: readonly Dict[]): boolean {
75
+ return !driftPresent && !checks.some((c) => c['status'] === 'fail');
76
+ }
77
+
78
+ export function validateStrictLevel(level: string): void {
79
+ if (!STRICT_LEVELS.has(level)) {
80
+ throw new StrictLevelError(
81
+ `argument --strict-level: invalid choice: '${level}' (choose from 'fail', 'warn')`,
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The `--strict` verdict, or `null` when strict is off.
88
+ *
89
+ * Factored out because `main` has TWO exit paths and only one of them was
90
+ * obvious: a project with no install manifest returns through
91
+ * `_run_no_manifest` long before the drift branch, so a strict check written
92
+ * only in the drift branch silently never fires there. Measured, not guessed —
93
+ * the first implementation exited 0 on a tree carrying a `fail` check row and a
94
+ * `can_proceed: false` payload, because this repository has no manifest of its
95
+ * own. Both paths call this now, so the two cannot diverge again.
96
+ */
97
+ export function strictExit(
98
+ opts: StrictOptions,
99
+ checks: readonly Dict[],
100
+ driftPresent: boolean,
101
+ ): number | null {
102
+ if (!opts.strict) {
103
+ return null;
104
+ }
105
+ const tripping = strictTripping(opts.strict_level);
106
+ const tripped = checks.filter((c) => tripping.has(c['status'] as string));
107
+ if (!driftPresent && tripped.length === 0) {
108
+ return 0;
109
+ }
110
+ if (process.env[STRICT_KILL_SWITCH] === '1') {
111
+ // The finding is NEVER suppressed — only the exit code is, and the
112
+ // suppression announces itself. A switch that hid the finding as well
113
+ // would be the ignored-warning shape this repository already documents.
114
+ eprint(
115
+ `⚠️ doctor --strict: ${String(tripped.length)} finding(s) at or above ` +
116
+ `'${opts.strict_level}'` +
117
+ (driftPresent ? ' plus manifest drift' : '') +
118
+ `, but ${STRICT_KILL_SWITCH}=1 suppressed the failing exit. ` +
119
+ 'The findings above still stand; unset it to restore the gate.',
120
+ );
121
+ return 0;
122
+ }
123
+ return 1;
124
+ }
125
+
126
+ /**
127
+ * The `can_proceed` / `status` pair, written onto a `--json` payload.
128
+ *
129
+ * Lives here rather than inline in `_emit_json` for the source-budget reason
130
+ * this whole module exists for. `status` is the roll-up a human reads;
131
+ * `can_proceed` is the boolean a caller branches on without parsing prose. Both
132
+ * are facts about the tree and never about the exit policy — a caller running
133
+ * with the kill switch set still gets `can_proceed: false`.
134
+ */
135
+ export function attachProceedFields(
136
+ payload: Dict,
137
+ driftPresent: boolean,
138
+ checks: readonly Dict[],
139
+ ): void {
140
+ const proceed = canProceed(driftPresent, checks);
141
+ payload['can_proceed'] = proceed;
142
+ payload['status'] = proceed
143
+ ? checks.some((c) => c['status'] === 'warn')
144
+ ? 'warn'
145
+ : 'ok'
146
+ : 'fail';
147
+ }
148
+
149
+ /** Argv wiring, exported as fragments so the caller spends one line each. */
150
+ export const STRICT_STORE_TRUE = { '--strict': 'strict' } as const;
151
+ export const STRICT_VALUE_FLAGS = { '--strict-level': 'strict_level' } as const;
152
+ export const STRICT_DEFAULTS = { strict: false, strict_level: STRICT_DEFAULT_LEVEL } as const;
153
+ export const STRICT_USAGE = ' [--strict] [--strict-level {fail,warn}]\n';
154
+
155
+ /** Validate, mapping the module's error onto the caller's argparse exit. */
156
+ export function validateStrictLevelOr(level: string, onError: (msg: string) => never): void {
157
+ try {
158
+ validateStrictLevel(level);
159
+ } catch (e) {
160
+ if (e instanceof StrictLevelError) onError(e.message);
161
+ throw e;
162
+ }
163
+ }
@@ -3049,6 +3049,8 @@ function _run_no_manifest(
3049
3049
  }
3050
3050
  return fail_check ? 1 : 0;
3051
3051
  }
3052
+ const sx = strictExit(opts, checks, false); // no manifest: keep the 2, never soften to 1
3053
+ if (sx !== null) return bridge_present ? sx : 2;
3052
3054
  if (opts.ci && bridge_present) {
3053
3055
  // Same fold as the manifest path: check failures are red under --ci.
3054
3056
  return fail_check ? 1 : 0;
@@ -3077,6 +3079,8 @@ function _emit_json(
3077
3079
  }
3078
3080
  if (checks !== null) {
3079
3081
  payload['checks'] = checks;
3082
+ const drift = missing.length + modified.length + foreign.length + tag_drift.length;
3083
+ attachProceedFields(payload, drift > 0, checks);
3080
3084
  // The detection section rides along whenever checks ran, so the agent /
3081
3085
  // GUI contract carries the full provider → transport → billing table
3082
3086
  // and not just the one-line check row.
@@ -3117,48 +3121,16 @@ function _emit_detection_text(project_root: string, checks: Dict[]): void {
3117
3121
  print('');
3118
3122
  }
3119
3123
 
3120
- function _emit_text(
3121
- project_root: string,
3122
- missing: Dict[],
3123
- modified: Dict[],
3124
- foreign: Dict[],
3125
- tag_drift: Dict[],
3126
- ): void {
3127
- const total = missing.length + modified.length + foreign.length + tag_drift.length;
3128
- if (total === 0) {
3129
- print(`✅ doctor: manifest matches filesystem under ${project_root}`);
3130
- return;
3131
- }
3132
- print(`⚠️ doctor: ${total} drift item(s) under ${project_root}`);
3133
- const groups: ReadonlyArray<[string, Dict[]]> = [
3134
- ['missing', missing],
3135
- ['modified', modified],
3136
- ['foreign', foreign],
3137
- ['tag-drift', tag_drift],
3138
- ];
3139
- for (const [label, items] of groups) {
3140
- if (items.length === 0) {
3141
- continue;
3142
- }
3143
- print(`\n ${label} (${items.length}):`);
3144
- for (const it of items) {
3145
- const tool = (it['tool'] as string) || '?';
3146
- print(` · [${tool}] ${it['path']}`);
3147
- if (label === 'tag-drift') {
3148
- const found = (it['found'] as string) || '(missing)';
3149
- const expected = 'expected' in it ? it['expected'] : PACKAGE_TAG_ID;
3150
- print(` expected: ${expected}`);
3151
- print(` found: ${found}`);
3152
- }
3153
- print(` fix: ${it['fix']}`);
3154
- }
3155
- }
3156
- }
3124
+
3125
+ // `--strict` lives in `_doctor_strict.ts`; the drift renderer in `_doctor_emit.ts`.
3126
+ import { STRICT_DEFAULTS, STRICT_STORE_TRUE, STRICT_USAGE, STRICT_VALUE_FLAGS, attachProceedFields, strictExit, validateStrictLevelOr } from './_doctor_strict.js';
3127
+ import { _emit_text } from './_doctor_emit.js';
3157
3128
 
3158
3129
  interface Options {
3159
3130
  project: string | null;
3160
3131
  json: boolean;
3161
3132
  ci: boolean;
3133
+ strict: boolean; strict_level: string; // see `_doctor_strict.ts`
3162
3134
  check: string | null;
3163
3135
  trace_root: boolean;
3164
3136
  context: boolean;
@@ -3173,18 +3145,21 @@ const PROG = 'agent-config doctor';
3173
3145
  // terminal width; golden tests assert the `usage:` token + exit code only.
3174
3146
  const USAGE =
3175
3147
  `usage: ${PROG} [-h] [--project PROJECT] [--json] [--ci] [--check ID]\n` +
3148
+ STRICT_USAGE +
3176
3149
  ' [--trace-root] [--context] [--anatomy]\n' +
3177
3150
  ' [--repair ID]\n';
3178
3151
 
3179
3152
  const _STORE_TRUE_FLAGS: Record<string, keyof Options> = {
3180
3153
  '--json': 'json',
3181
3154
  '--ci': 'ci',
3155
+ ...STRICT_STORE_TRUE,
3182
3156
  '--trace-root': 'trace_root',
3183
3157
  '--context': 'context',
3184
3158
  '--anatomy': 'anatomy',
3185
3159
  };
3186
3160
 
3187
3161
  const _VALUE_FLAGS: Record<string, keyof Options> = {
3162
+ ...STRICT_VALUE_FLAGS,
3188
3163
  '--project': 'project',
3189
3164
  '--check': 'check',
3190
3165
  '--repair': 'repair',
@@ -3201,6 +3176,7 @@ function _parse(argv: string[]): Options {
3201
3176
  project: null,
3202
3177
  json: false,
3203
3178
  ci: false,
3179
+ ...STRICT_DEFAULTS,
3204
3180
  check: null,
3205
3181
  trace_root: false,
3206
3182
  context: false,
@@ -3430,6 +3406,7 @@ function _run_repair(opts: Options): number {
3430
3406
 
3431
3407
  function main(argv: string[] | null = null): number {
3432
3408
  const opts = _parse(argv !== null ? Array.from(argv) : process.argv.slice(2));
3409
+ validateStrictLevelOr(opts.strict_level, _argError);
3433
3410
  if (opts.ci) {
3434
3411
  // `--ci` = machine-readable consumer-CI contract: JSON payload on
3435
3412
  // stdout, zero interactive output, and check failures fold into the
@@ -3502,6 +3479,8 @@ function main(argv: string[] | null = null): number {
3502
3479
  // · 2 unresolvable environment (handled above via ProjectRootError).
3503
3480
  return drift_present || fail_check ? 1 : 0;
3504
3481
  }
3482
+ const strict = strictExit(opts, checks, drift_present);
3483
+ if (strict !== null) return strict;
3505
3484
  return drift_present ? 1 : 0;
3506
3485
  }
3507
3486
 
@@ -290,6 +290,12 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
290
290
  last dispatcher feedback per concern, missing trampolines.
291
291
  Wraps hooks:status. Read-only.
292
292
  Flags: --format json|table, --strict (CI), --project-root <path>
293
+ hooks:effect Does a bound concern actually FIRE on this host? Dispatches
294
+ each eligible concern against a scratch root and reports the
295
+ observed state. Not hooks:doctor: that reads the manifest,
296
+ this reads what happened. Read-only.
297
+ Flags: --host <name>, --format text|json, --root <path>,
298
+ --limit <n>, --help (prints both verdict vocabularies)
293
299
  routing:doctor Live routing diagnosis: per-gate ACTIVE/INACTIVE with the
294
300
  concern's own reason (read-only probes), session_start chain,
295
301
  router + projection freshness, host bridge status.
@@ -764,10 +770,6 @@ cmd_roadmap_progress() {
764
770
  # The parked estate's front door — read-only, and deliberately so. It authors
765
771
  # nothing under `agents/roadmaps/stubs/`, which is what keeps it clear of the
766
772
  # 2026-08-21 council verdict that deleted the hand-maintained index there.
767
- cmd_stubs_due() {
768
- exec_ts "$PACKAGE_ROOT/src/scripts/stubs_due.ts" "$@"
769
- }
770
-
771
773
  cmd_roadmap_progress_check() {
772
774
  local script
773
775
  script="$(resolve_script "dist/agent-src/scripts/update_roadmap_progress.ts" ".augment/scripts/update_roadmap_progress.ts")"
@@ -980,6 +982,10 @@ cmd_hooks_doctor() {
980
982
  exec_hook "src/scripts/hooks_doctor" "$@"
981
983
  }
982
984
 
985
+ cmd_hooks_effect() {
986
+ exec_hook "src/scripts/hook_effect_doctor" "$@"
987
+ }
988
+
983
989
  cmd_routing_doctor() {
984
990
  exec_hook "src/scripts/routing_doctor" "$@"
985
991
  }
@@ -1537,6 +1543,7 @@ main() {
1537
1543
  dispatch:hook) cmd_dispatch_hook "$@" ;;
1538
1544
  hooks:status) cmd_hooks_status "$@" ;;
1539
1545
  hooks:doctor) cmd_hooks_doctor "$@" ;;
1546
+ hooks:effect) cmd_hooks_effect "$@" ;;
1540
1547
  routing:doctor) cmd_routing_doctor "$@" ;;
1541
1548
  workspace:doctor) cmd_workspace_doctor "$@" ;;
1542
1549
  route:explain) cmd_route_explain "$@" ;;
@@ -43,6 +43,26 @@ export const LADDER_RUNGS = [
43
43
  ] as const;
44
44
  export type LadderRung = (typeof LADDER_RUNGS)[number];
45
45
 
46
+ /**
47
+ * The receipt-bearing cascade stage that reads each rung, in ladder order.
48
+ *
49
+ * Derived from {@link LADDER_RUNGS} rather than written out, so a rung cannot
50
+ * exist without its stage and the two orders cannot drift. Named here rather
51
+ * than in the cascade because the RUNGS are this module's, and a second module
52
+ * spelling `receipt-<rung>` would be a second source of truth for the same
53
+ * enum.
54
+ *
55
+ * The cascade consumes these; nothing here imports the cascade. That direction
56
+ * is the one `docs/contracts/activation-receipt-trust-boundary.md` TB-1 fixes.
57
+ */
58
+ export const RECEIPT_STAGES = LADDER_RUNGS.map((r) => `receipt-${r}` as const);
59
+ export type ReceiptStage = (typeof RECEIPT_STAGES)[number];
60
+
61
+ /** The rung a receipt-bearing stage reads. Total over {@link ReceiptStage}. */
62
+ export function rungForReceiptStage(stage: ReceiptStage): LadderRung {
63
+ return stage.slice('receipt-'.length) as LadderRung;
64
+ }
65
+
46
66
  /**
47
67
  * Which failure family a stall at each rung belongs to.
48
68
  *
@@ -171,10 +191,34 @@ export function rungState(receipt: ActivationReceipt, rung: LadderRung): RungSta
171
191
  * and the caller gets `null`.
172
192
  */
173
193
  export function classifyFailure(receipt: ActivationReceipt): FailureFamily | null {
174
- for (const spec of LADDER) {
194
+ return firstStall(receipt)?.family ?? null;
195
+ }
196
+
197
+ /** Where a receipt stopped climbing, and the family that stall belongs to. */
198
+ export interface LadderStall {
199
+ /** The rung the walk stopped at. */
200
+ readonly rung: LadderRung;
201
+ /** The receipt-bearing cascade stage that reads {@link rung}. */
202
+ readonly stage: ReceiptStage;
203
+ /** `unknown` when the rung was never observed; the rung's family otherwise. */
204
+ readonly family: FailureFamily;
205
+ }
206
+
207
+ /**
208
+ * The first rung a receipt did not reach, with its stage and its family.
209
+ *
210
+ * {@link classifyFailure} is a projection of this, and delegates to it rather
211
+ * than walking the ladder a second time. Two walks of the same ladder applying
212
+ * the same short-circuit rule is two places to change it — and the cascade needs
213
+ * the STAGE as well as the family, which is the only reason this exists
214
+ * separately at all.
215
+ */
216
+ export function firstStall(receipt: ActivationReceipt): LadderStall | null {
217
+ for (const [i, spec] of LADDER.entries()) {
175
218
  const state = rungState(receipt, spec.rung);
176
- if (state === 'unknown') return 'unknown';
177
- if (state === 'not-reached') return spec.family;
219
+ const stage = RECEIPT_STAGES[i] as ReceiptStage;
220
+ if (state === 'unknown') return { rung: spec.rung, stage, family: 'unknown' };
221
+ if (state === 'not-reached') return { rung: spec.rung, stage, family: spec.family };
178
222
  }
179
223
  return null;
180
224
  }