@event4u/agent-config 14.13.0 → 14.14.0
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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +116 -387
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +38 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1438 -711
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +112 -17
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/gate-coverage.yml +150 -0
- package/src/config/gate-violation-baselines.json +8 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/scripts/_dispatch.bash +0 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +70 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +181 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +643 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
|
@@ -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
|
+
}
|