@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,315 @@
1
+ /**
2
+ * The ONE guarded capability every promotion-capable write must pass through.
3
+ *
4
+ * `road-to-harness-promotion-bridge` — discharge route 1 of the carried
5
+ * blocking condition.
6
+ *
7
+ * ## Why this module exists
8
+ *
9
+ * The carried condition covers *"a verb, a state transition into `promoted`, or
10
+ * any write into `src/` derived from a candidate"*. Two of those three were
11
+ * gated before this change: `verbPromote` refuses unconditionally
12
+ * (`src/scripts/evolution_lab.ts`), and `assertTransition(_, 'promoted')`
13
+ * refuses without a named approver (`_lib/candidate_record.ts`). The AI council
14
+ * of 2026-08-31 ruled the condition NOT DISCHARGED because nothing proved
15
+ * repository-wide that a THIRD path could not exist — a direct approval
16
+ * synthesis, a record written with `lifecycle: 'promoted'` outside the verb, or
17
+ * a candidate-derived write into the source tree.
18
+ *
19
+ * The route the same verdict named is a structural invariant with **one guarded
20
+ * capability** that is **unobtainable while `blocker: merge-authority` is open**.
21
+ * This module is that capability; `src/scripts/lint_promotion_paths.ts` is the
22
+ * invariant that proves nothing bypasses it.
23
+ *
24
+ * ## This module creates no promotion path
25
+ *
26
+ * It performs no filesystem write, no transition, and no promotion. It mints an
27
+ * opaque token, and refuses unless the blocker reads GRANTED. The carried
28
+ * condition requires the enforcement to land *before or in* the first commit
29
+ * that creates a promotion path; this is the enforcement, and it arrives with no
30
+ * path attached.
31
+ *
32
+ * ## Fail-closed in every direction
33
+ *
34
+ * An absent roadmap, an unreadable file, a missing blocker heading and a Status
35
+ * field this module cannot parse all resolve to {@link BlockerStatus} values
36
+ * that REFUSE. There is deliberately no environment variable, no flag and no
37
+ * override argument: a capability that can be unlocked by configuration is not a
38
+ * capability, it is a default.
39
+ */
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+ import { fileURLToPath } from 'node:url';
43
+
44
+ import type { HumanApproval } from './candidate_record.js';
45
+
46
+ const _HERE = fileURLToPath(import.meta.url);
47
+ /** `src/scripts/_lib/` → repo root. */
48
+ export const CAPABILITY_REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
49
+
50
+ /**
51
+ * The three literals `lint_roadmap_blockers` uses to find a blocker, copied so
52
+ * the two readers cannot drift: `:37`, `:38` and `:39` there. They are literals
53
+ * rather than an import because that module is a CLI gate with side effects at
54
+ * load; `tests/scripts/lint_promotion_paths.test.ts` pins them equal.
55
+ */
56
+ const FENCED_CODE_RE = /^[ \t]*```[^\n]*\n[\s\S]*?^[ \t]*```[ \t]*$/gm;
57
+ const BLOCKERS_SECTION_RE = /^##[ \t]+Blockers[ \t]*$/im;
58
+ const NEXT_H2_RE = /^##[ \t]+\S/m;
59
+
60
+ /** The blocker whose disposition decides whether this capability is obtainable. */
61
+ export const MERGE_AUTHORITY_BLOCKER_ID = 'merge-authority';
62
+
63
+ /**
64
+ * The roadmap that owns the blocker entry, repo-relative.
65
+ *
66
+ * **It is in `archive/` since 2026-09-01, and that is correct rather than
67
+ * stale.** The roadmap closed at its PR boundary once `blocker: merge-authority`
68
+ * was settled — `Status: resolved`, `Disposition: refused` — and the archival
69
+ * sweep rewrote this constant itself, since it rewrites inbound
70
+ * `agents/roadmaps/<x>.md` references when it moves a file. An archived roadmap
71
+ * is a permanent tracked record, so the settled decision stays readable.
72
+ *
73
+ * **Nothing about the refusal weakened in the move.** `refused` is not
74
+ * `resolved`, so `isRefusingStatus` is true and the capability is unobtainable
75
+ * exactly as it was while the blocker read `open`. And if this path ever stops
76
+ * resolving, `readMergeAuthorityStatus` returns `roadmap-unreadable`, which is
77
+ * also refusing — every failure mode of this pointer fails closed.
78
+ *
79
+ * **What a future GRANT would need, named rather than left to be discovered.**
80
+ * Flipping the disposition would mean editing an archived roadmap, which is a
81
+ * poor home for a live authority record. The grant path is an owner ruling in
82
+ * `docs/decisions/ADR-239-drain-command-surface-and-merge-authority.md`
83
+ * § Decision 3, and wiring this reader to that record instead of to a roadmap is
84
+ * the change that should accompany it. It is deliberately NOT made here: it
85
+ * would be a redesign of the guarded capability on a change whose whole point is
86
+ * that the capability does not move.
87
+ */
88
+ export const MERGE_AUTHORITY_ROADMAP = 'agents/roadmaps/archive/road-to-harness-promotion-bridge.md';
89
+
90
+ /**
91
+ * The source tree a candidate may never be written into.
92
+ *
93
+ * Promotion means a candidate's bytes reaching canonical `agent-config`, and
94
+ * `src/` is where canonical lives (ADR-051 flat-`src/`). A candidate-derived
95
+ * write anywhere under it IS the promotion the condition names, whatever the
96
+ * lifecycle field says.
97
+ */
98
+ export const REPO_SOURCE_ROOT = 'src';
99
+
100
+ /**
101
+ * Blocker states, as the roadmap gate reads them.
102
+ *
103
+ * `resolved` is the ONLY value that is not a refusal. The literal that decides
104
+ * *closedness* is the same one `lint_roadmap_blockers.ts:193` uses, so a blocker
105
+ * this repository calls open and a blocker this module calls open cannot
106
+ * diverge — but closedness alone is no longer sufficient to mint, and the reason
107
+ * is a defect this module used to carry.
108
+ *
109
+ * ## Why `Status: resolved` is not, by itself, a grant
110
+ *
111
+ * `blocker: merge-authority` asks *"is preauthorized merge authority granted or
112
+ * refused?"*, and its own `What to do` offers BOTH directions. But `resolved` is
113
+ * the only closed token this repository recognises, so before this change the
114
+ * two directions were indistinguishable here: settling the blocker in the
115
+ * **refusing** direction — writing down that unattended promotion is forbidden —
116
+ * would have set `Status: resolved` and thereby MINTED the capability that
117
+ * performs unattended promotion. The refusal would have granted the thing it
118
+ * refused.
119
+ *
120
+ * That is the same failure ADR-239 Decision 3 names from the other side, where
121
+ * an authorization read out of agent-writable state *"would let the agent
122
+ * consent on the user's behalf — which is the thing the abort exists to prevent,
123
+ * reimplemented as a feature"*. Here the consent would have been synthesised out
124
+ * of a bookkeeping token rather than out of runtime state, which is worse: no
125
+ * one would have written the word "granted" anywhere.
126
+ *
127
+ * So the closed blocker must additionally carry an explicit **disposition**, and
128
+ * only one of its values mints. This is strictly stricter than the previous
129
+ * behaviour in every direction — a body that granted before and still grants
130
+ * must now say so — so it cannot widen the capability, only narrow it.
131
+ */
132
+ export type BlockerStatus =
133
+ | 'resolved'
134
+ | 'refused'
135
+ | 'resolved-unclassified'
136
+ | 'open'
137
+ | 'blocker-absent'
138
+ | 'roadmap-unreadable';
139
+
140
+ /** Every status except `resolved`. Named so callers do not re-derive the polarity. */
141
+ export function isRefusingStatus(s: BlockerStatus): boolean {
142
+ return s !== 'resolved';
143
+ }
144
+
145
+ /**
146
+ * The disposition line a CLOSED `merge-authority` blocker must carry.
147
+ *
148
+ * `granted` is the only value that mints. `refused` is a first-class closed
149
+ * state — the blocker is answered and no longer blocks archival, and the answer
150
+ * is "no". Anything else, including an absent line, reads as
151
+ * `resolved-unclassified` and refuses.
152
+ */
153
+ const DISPOSITION_GRANTED_RE = /^-[ \t]*\*\*Disposition:\*\*[ \t]*granted[ \t]*$/im;
154
+ const DISPOSITION_REFUSED_RE = /^-[ \t]*\*\*Disposition:\*\*[ \t]*refused[ \t]*(?:$|[-—:.,(])/im;
155
+
156
+ /**
157
+ * Blank out fenced code, preserving line count.
158
+ *
159
+ * The same transform `lint_roadmap_blockers.ts:137` applies before its own read,
160
+ * and applying it here is what makes the "cannot diverge" claim true rather than
161
+ * merely intended. Without it a fenced EXAMPLE of the syntax — which is exactly
162
+ * what a `What to do:` field contains, since that field's job is to tell a
163
+ * maintainer which line to write — is read as the live value.
164
+ */
165
+ function stripFencedCode(text: string): string {
166
+ return text.replace(FENCED_CODE_RE, (m) => '\n'.repeat((m.match(/\n/g) ?? []).length));
167
+ }
168
+
169
+ /**
170
+ * Extract the `### blocker: <id>` section body from a roadmap's text.
171
+ *
172
+ * Scoped and stripped to match `lint_roadmap_blockers` exactly: fenced code is
173
+ * blanked first, the search is confined to the `## Blockers` section, and only a
174
+ * `###` heading opens a blocker. A `#### blocker: merge-authority` under some
175
+ * other section is not a blocker to the linter and must not be one here either —
176
+ * otherwise a history note could carry a status the repository's own gate does
177
+ * not see.
178
+ */
179
+ export function blockerSection(markdown: string, id: string): string | null {
180
+ const stripped = stripFencedCode(markdown);
181
+ const sectionMatch = BLOCKERS_SECTION_RE.exec(stripped);
182
+ if (sectionMatch === null) {
183
+ return null;
184
+ }
185
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
186
+ const rest = stripped.slice(sectionStart);
187
+ const h2 = NEXT_H2_RE.exec(rest);
188
+ const scoped = rest.slice(0, h2 ? h2.index : undefined);
189
+
190
+ const lines = scoped.split('\n');
191
+ const head = new RegExp(`^###\\s+blocker:\\s*${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'i');
192
+ let start = -1;
193
+ for (let i = 0; i < lines.length; i += 1) {
194
+ if (head.test(lines[i] as string)) {
195
+ start = i + 1;
196
+ break;
197
+ }
198
+ }
199
+ if (start < 0) {
200
+ return null;
201
+ }
202
+ const out: string[] = [];
203
+ for (let i = start; i < lines.length; i += 1) {
204
+ // `#{1,6}` rather than `#{1,4}`: an h5/h6 sub-heading ends the section
205
+ // too. With the old bound a `##### ` line matched nothing (greedy 4 then
206
+ // requiring `\s`, backtracking into `#`), so everything after it bled
207
+ // into the body.
208
+ if (/^#{1,6}\s/.test(lines[i] as string)) {
209
+ break;
210
+ }
211
+ out.push(lines[i] as string);
212
+ }
213
+ return out.join('\n');
214
+ }
215
+
216
+ /**
217
+ * Read the live status of `blocker: merge-authority`.
218
+ *
219
+ * Fail-closed: anything this function cannot read positively as `resolved`
220
+ * comes back as a refusing status. It never throws — the caller decides, and
221
+ * every caller in this module refuses.
222
+ */
223
+ export function readMergeAuthorityStatus(repoRoot: string = CAPABILITY_REPO_ROOT): BlockerStatus {
224
+ let text: string;
225
+ try {
226
+ text = fs.readFileSync(path.join(repoRoot, MERGE_AUTHORITY_ROADMAP), 'utf-8');
227
+ } catch {
228
+ return 'roadmap-unreadable';
229
+ }
230
+ const body = blockerSection(text, MERGE_AUTHORITY_BLOCKER_ID);
231
+ if (body === null) {
232
+ return 'blocker-absent';
233
+ }
234
+ // The same literal `lint_roadmap_blockers.ts:193` matches. `resolved` is the
235
+ // only closed token this repository recognises — so closedness is read with
236
+ // that literal and NOTHING else, and the grant/refuse direction is then read
237
+ // separately from the body's own `Disposition` line. See the BlockerStatus
238
+ // docblock for why a closed blocker is not automatically a grant.
239
+ if (!/^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body)) {
240
+ return 'open';
241
+ }
242
+ if (DISPOSITION_REFUSED_RE.test(body)) {
243
+ return 'refused';
244
+ }
245
+ if (DISPOSITION_GRANTED_RE.test(body)) {
246
+ return 'resolved';
247
+ }
248
+ return 'resolved-unclassified';
249
+ }
250
+
251
+ /** Raised when the capability cannot be minted. There is no way around it. */
252
+ export class PromotionCapabilityUnobtainableError extends Error {
253
+ readonly status: BlockerStatus;
254
+ constructor(status: BlockerStatus, why: string) {
255
+ super(
256
+ `promotion capability unobtainable (blocker '${MERGE_AUTHORITY_BLOCKER_ID}' status: ${status}): ${why}`,
257
+ );
258
+ this.name = 'PromotionCapabilityUnobtainableError';
259
+ this.status = status;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * The token. Holding one means the blocker reads GRANTED AND a human was named.
265
+ *
266
+ * Deliberately opaque: `blockerStatusAtGrant` is a LITERAL type — its value
267
+ * `'resolved'` is the status enum's GRANTED member, not merely "closed" — so a caller
268
+ * cannot construct a value of this shape with any other status, and there is no
269
+ * exported constructor other than {@link acquirePromotionCapability}.
270
+ */
271
+ export interface PromotionCapability {
272
+ readonly approval: HumanApproval;
273
+ readonly blockerStatusAtGrant: 'resolved';
274
+ readonly roadmap: string;
275
+ }
276
+
277
+ /**
278
+ * Mint the capability, or refuse.
279
+ *
280
+ * Two conjuncts, both required, checked in this order so the message names the
281
+ * governance reason first:
282
+ *
283
+ * 1. `blocker: merge-authority` reads `resolved` in the live roadmap — which
284
+ * now means BOTH `Status: resolved` AND `Disposition: granted`. While it
285
+ * reads anything else — including "the roadmap is missing", "the blocker was
286
+ * closed as refused" and "the blocker was closed without saying which" — the
287
+ * capability is unobtainable, which is the property the council's route 1
288
+ * specified.
289
+ * 2. A NAMED human approver. Empty, whitespace-only and absent are refused, so
290
+ * the cheapest way to satisfy the gate stays "name someone".
291
+ *
292
+ * @throws {PromotionCapabilityUnobtainableError} on either.
293
+ */
294
+ export function acquirePromotionCapability(
295
+ approval: HumanApproval,
296
+ repoRoot: string = CAPABILITY_REPO_ROOT,
297
+ ): PromotionCapability {
298
+ const status = readMergeAuthorityStatus(repoRoot);
299
+ if (isRefusingStatus(status)) {
300
+ throw new PromotionCapabilityUnobtainableError(
301
+ status,
302
+ 'promotion into canonical agent-config is gated on the owner-reserved blocker ' +
303
+ '(ADR-239 Decision 3), which does not read as GRANTED. No flag, environment ' +
304
+ 'variable or argument lifts it.',
305
+ );
306
+ }
307
+ if (approval.approver.trim() === '' || approval.approvedAt.trim() === '') {
308
+ throw new PromotionCapabilityUnobtainableError(
309
+ status,
310
+ 'the blocker is resolved, and promotion still requires a NAMED human approver with a ' +
311
+ 'date. A blank name is refused rather than defaulted.',
312
+ );
313
+ }
314
+ return { approval, blockerStatusAtGrant: 'resolved', roadmap: MERGE_AUTHORITY_ROADMAP };
315
+ }