@event4u/agent-config 14.20.0 → 14.21.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 +243 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +15 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
- package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-643Z7FIC.js} +1 -1
- package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
- package/dist/cli-delegate/cmd_conformance.js +17 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +56 -25
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +479 -241
- package/dist/install/install.mjs +4 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +313 -63
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/llms.txt +14 -14
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +38 -1
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_pack_size.ts +1 -1
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +26 -71
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +34 -29
- package/src/scripts/self_review_gate.ts +409 -15
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a push that leaves this branch's OWN work uncommitted.
|
|
3
|
+
*
|
|
4
|
+
* The defect, measured on 2026-09-07 in `fix/release-obligation-answerable`:
|
|
5
|
+
* a merge-reconciliation baseline was computed, written into
|
|
6
|
+
* `gate-violation-baselines.json` AFTER `git add`, and then committed with
|
|
7
|
+
* `git commit --no-edit` — which captured the STAGED side. The reconciled
|
|
8
|
+
* number stayed in the working tree, the push went out without it, and the
|
|
9
|
+
* pushed branch asserted a baseline the tree contradicted. The operator found
|
|
10
|
+
* it, not the pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Nothing in CI can catch this class: CI checks out the pushed commit, where
|
|
13
|
+
* the working tree that holds the lost edit does not exist. The only place the
|
|
14
|
+
* evidence is available is the machine doing the push, which is why this gate
|
|
15
|
+
* is pre-push and local-only — the same reach `check_branch_freshness` has.
|
|
16
|
+
*
|
|
17
|
+
* The discriminator, whose work is it:
|
|
18
|
+
*
|
|
19
|
+
* A dirty tree at push time is not automatically a defect. Gate scripts write
|
|
20
|
+
* reports, a parallel session may touch a shared file, a test run leaves
|
|
21
|
+
* artifacts. Blocking on all of it would train the operator to set the skip
|
|
22
|
+
* variable, which is worse than no gate.
|
|
23
|
+
*
|
|
24
|
+
* So the split is by AUTHORSHIP, and it is decidable from git alone:
|
|
25
|
+
*
|
|
26
|
+
* 1. INDEX/WORKTREE DIVERGENCE (`MM`, `AM`, `RM`, …) — the file is staged
|
|
27
|
+
* AND changed again since. That is "edited after `git add`" as a fact, not
|
|
28
|
+
* an inference, and it is exactly the shape that produced the defect.
|
|
29
|
+
* 2. STAGED AT ALL — `git add` is a recorded intent to ship the file, and a
|
|
30
|
+
* push carries commits rather than the index, so a staged-but-uncommitted
|
|
31
|
+
* path is this branch's work being left behind by definition. No
|
|
32
|
+
* authorship inference is needed for this class.
|
|
33
|
+
* 3. A DIRTY FILE THIS BRANCH'S OWN COMMITS TOUCHED — the branch has been
|
|
34
|
+
* editing it deliberately, so an open change to it is this branch's
|
|
35
|
+
* unfinished work rather than someone else's noise.
|
|
36
|
+
* 4. EVERYTHING ELSE — a dirty file the branch never committed, or an
|
|
37
|
+
* untracked file. Reported by name, never blocking: this is where a
|
|
38
|
+
* parallel session's edit and a gate script's own report output land, and
|
|
39
|
+
* the branch has no claim on either.
|
|
40
|
+
*
|
|
41
|
+
* Class 4 is advisory, and the residual limit is stated rather than hidden: a
|
|
42
|
+
* file this branch created and has neither staged nor committed is
|
|
43
|
+
* indistinguishable from a foreign untracked file. Class 2 closes it at the
|
|
44
|
+
* first `git add` — which was itself a finding of testing this gate against the
|
|
45
|
+
* live tree, where its own source file sat in class 4 until the rule was
|
|
46
|
+
* widened.
|
|
47
|
+
*/
|
|
48
|
+
import * as path from 'node:path';
|
|
49
|
+
import process from 'node:process';
|
|
50
|
+
import { spawnSync } from 'node:child_process';
|
|
51
|
+
import { fileURLToPath } from 'node:url';
|
|
52
|
+
|
|
53
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
54
|
+
|
|
55
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
56
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
57
|
+
|
|
58
|
+
export interface WorkVerdict {
|
|
59
|
+
/** False when the push must be refused. */
|
|
60
|
+
ok: boolean;
|
|
61
|
+
/** Files that are this branch's own unfinished work, with the reason. */
|
|
62
|
+
blocking: { path: string; why: string }[];
|
|
63
|
+
/** Files that are dirty but not this branch's to answer for. */
|
|
64
|
+
advisory: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One `git status --porcelain` row. */
|
|
68
|
+
interface StatusRow {
|
|
69
|
+
index: string;
|
|
70
|
+
worktree: string;
|
|
71
|
+
path: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parsePorcelain(porcelain: string): StatusRow[] {
|
|
75
|
+
const out: StatusRow[] = [];
|
|
76
|
+
for (const line of porcelain.split('\n')) {
|
|
77
|
+
if (line.length < 4) continue;
|
|
78
|
+
const index = line[0] as string;
|
|
79
|
+
const worktree = line[1] as string;
|
|
80
|
+
let p = line.slice(3);
|
|
81
|
+
// Renames read `R old -> new`; the new path is the one that matters.
|
|
82
|
+
const arrow = p.indexOf(' -> ');
|
|
83
|
+
if (arrow !== -1) p = p.slice(arrow + 4);
|
|
84
|
+
out.push({ index, worktree, path: p.replace(/^"|"$/gu, '') });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The verdict, as a pure function of the two git readings — so the rules are
|
|
91
|
+
* unit-testable without a repository, the shape `confirmGate` and
|
|
92
|
+
* `preflightPosition` already use in this pipeline.
|
|
93
|
+
*/
|
|
94
|
+
export function classifyBranchWork(opts: {
|
|
95
|
+
porcelain: string;
|
|
96
|
+
/** Files this branch's own commits touched (`git diff --name-only base..HEAD`). */
|
|
97
|
+
branchFiles: readonly string[];
|
|
98
|
+
}): WorkVerdict {
|
|
99
|
+
const touched = new Set(opts.branchFiles);
|
|
100
|
+
const blocking: { path: string; why: string }[] = [];
|
|
101
|
+
const advisory: string[] = [];
|
|
102
|
+
|
|
103
|
+
for (const row of parsePorcelain(opts.porcelain)) {
|
|
104
|
+
const untracked = row.index === '?' && row.worktree === '?';
|
|
105
|
+
const staged = row.index !== ' ' && !untracked;
|
|
106
|
+
const dirty = row.worktree !== ' ' && !untracked;
|
|
107
|
+
|
|
108
|
+
if (staged && dirty) {
|
|
109
|
+
blocking.push({
|
|
110
|
+
path: row.path,
|
|
111
|
+
why: 'staged AND changed again since — edited after `git add`, so the commit would capture the stale side',
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (staged) {
|
|
116
|
+
blocking.push({
|
|
117
|
+
path: row.path,
|
|
118
|
+
why: 'staged but never committed — `git add` records the intent to ship it, and a push carries commits, not the index',
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (dirty && touched.has(row.path)) {
|
|
123
|
+
blocking.push({
|
|
124
|
+
path: row.path,
|
|
125
|
+
why: "this branch's own commits touch this file, so an open change to it is unfinished work from this branch",
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
advisory.push(row.path);
|
|
130
|
+
}
|
|
131
|
+
return { ok: blocking.length === 0, blocking, advisory };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function git(args: readonly string[]): string {
|
|
135
|
+
const r = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf-8' });
|
|
136
|
+
return r.status === 0 ? (r.stdout ?? '') : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Files this branch's commits touched, relative to where it left the base. */
|
|
140
|
+
export function branchOwnFiles(baseRef: string): string[] {
|
|
141
|
+
const base = git(['merge-base', baseRef, 'HEAD']).trim();
|
|
142
|
+
if (base === '') return [];
|
|
143
|
+
return git(['diff', '--name-only', `${base}..HEAD`])
|
|
144
|
+
.split('\n')
|
|
145
|
+
.filter((l) => l.trim() !== '');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function main(argv: readonly string[]): number {
|
|
149
|
+
const quiet = argv.includes('--quiet');
|
|
150
|
+
let baseRef = 'origin/main';
|
|
151
|
+
const bi = argv.indexOf('--base');
|
|
152
|
+
if (bi !== -1 && argv[bi + 1]) baseRef = argv[bi + 1] as string;
|
|
153
|
+
|
|
154
|
+
const porcelain = git(['status', '--porcelain']);
|
|
155
|
+
const verdict = classifyBranchWork({ porcelain, branchFiles: branchOwnFiles(baseRef) });
|
|
156
|
+
|
|
157
|
+
// Through the shared reporter, not a hand-written line: `check_gate_coverage`
|
|
158
|
+
// reads the CALL, and a printed `scanned:` string satisfies the eye while
|
|
159
|
+
// leaving the gate unhardened (measured — this gate reded that ratchet on
|
|
160
|
+
// its first CI run for exactly that reason).
|
|
161
|
+
//
|
|
162
|
+
// Zero rows is the GOOD state here, which inverts the usual dead-scope
|
|
163
|
+
// reasoning: for every other gate an empty corpus may mean the root moved,
|
|
164
|
+
// while for this one it means the operator has nothing outstanding. The
|
|
165
|
+
// scope cannot go dead either — `git status` is the corpus, and a reading it
|
|
166
|
+
// could not produce comes back as an empty string from a non-zero exit,
|
|
167
|
+
// which is indistinguishable from clean and is why the reason below is
|
|
168
|
+
// stated rather than left implicit.
|
|
169
|
+
reportScanned({
|
|
170
|
+
gate: 'check_branch_work_committed',
|
|
171
|
+
scanned: parsePorcelain(porcelain).length,
|
|
172
|
+
units: 'dirty path(s)',
|
|
173
|
+
roots: ['git status --porcelain'],
|
|
174
|
+
allowEmpty:
|
|
175
|
+
'EMPTY_VALID: a clean working tree is this gate\'s PASSING state, not a dead scope. ' +
|
|
176
|
+
'The corpus is git\'s own status output over the whole repository, so there is no ' +
|
|
177
|
+
'root to move; zero rows means the operator has committed everything, which is the ' +
|
|
178
|
+
'outcome the gate exists to produce.',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (verdict.ok) {
|
|
182
|
+
if (!quiet) {
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
`✅ check_branch_work_committed: nothing of this branch's own work is uncommitted` +
|
|
185
|
+
(verdict.advisory.length > 0
|
|
186
|
+
? ` (${String(verdict.advisory.length)} unrelated dirty path(s) left alone)\n`
|
|
187
|
+
: '\n'),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (verdict.advisory.length > 0 && !quiet) {
|
|
191
|
+
for (const p of verdict.advisory) {
|
|
192
|
+
process.stdout.write(` · ${p} — not touched by this branch's commits\n`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`❌ check_branch_work_committed: ${String(verdict.blocking.length)} file(s) carry this ` +
|
|
200
|
+
"branch's own uncommitted work:\n",
|
|
201
|
+
);
|
|
202
|
+
for (const b of verdict.blocking) {
|
|
203
|
+
process.stderr.write(` - ${b.path}\n ${b.why}\n`);
|
|
204
|
+
}
|
|
205
|
+
process.stderr.write(
|
|
206
|
+
' A push that leaves them behind ships a branch whose tree contradicts its own\n' +
|
|
207
|
+
' commits, and NO CI gate can see it — CI checks out the commit, never the tree\n' +
|
|
208
|
+
' the edit is sitting in.\n' +
|
|
209
|
+
' Commit them (or `git restore` what was not meant to land), then push again.\n' +
|
|
210
|
+
' Genuinely foreign changes are reported without blocking; if one of these is\n' +
|
|
211
|
+
" truly a parallel session's, commit around it with an explicit pathspec.\n" +
|
|
212
|
+
' WIP push: AGENT_CONFIG_SKIP_PREPUSH_WORKTREE=1.\n',
|
|
213
|
+
);
|
|
214
|
+
if (verdict.advisory.length > 0) {
|
|
215
|
+
process.stderr.write(
|
|
216
|
+
` Left alone (${String(verdict.advisory.length)}): ${verdict.advisory.join(', ')}\n`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const _isMain = (() => {
|
|
223
|
+
const entry = process.argv[1];
|
|
224
|
+
if (!entry) return false;
|
|
225
|
+
try {
|
|
226
|
+
return path.resolve(entry) === path.resolve(_HERE);
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
|
|
232
|
+
if (_isMain) {
|
|
233
|
+
process.exit(main(process.argv.slice(2)));
|
|
234
|
+
}
|
|
@@ -319,7 +319,7 @@ const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
|
|
|
319
319
|
{ fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
|
|
320
320
|
{ fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
321
321
|
{ fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
322
|
-
{ fingerprint: '
|
|
322
|
+
{ fingerprint: '47e4328e71652a0375ade7eb4dac8a3e4b6df1c6e4e74caa75287266676cc9a6', path: 'agents/templates/.ai-council.yml.example', size: 30561, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one. RE-PINNED 2026-09-07, 29,872 -> 30,561 bytes: the exception is path-AND-size-bound by design, so this branch adding a `disabled_reason` honest-null block to the gemini seat in the template invalidated it and the gate refused the entry as unaccounted. The re-pin excepts the SAME path for the SAME reason at its measured new size — it does not widen the class, raise a cap, or except anything the gate was not already carrying.' },
|
|
323
323
|
{ fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
|
|
324
324
|
{ fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
|
|
325
325
|
{ fingerprint: 'a4f51dba3a66c0f2c3548e77961271ed4d7b5ec8a6661ea359ff044758f0d921', path: 'src/templates/minimal/.agent-settings.yml', size: 1042, why: 'the minimal-install settings template; the dot is the consumer-side filename' },
|
|
@@ -71,18 +71,12 @@ import {
|
|
|
71
71
|
collect_span_commits,
|
|
72
72
|
derive_categories,
|
|
73
73
|
parse_git_log,
|
|
74
|
-
mix_response_blockers,
|
|
75
|
-
previous_promise,
|
|
76
74
|
previous_release_tag,
|
|
77
|
-
promise_readback_blockers,
|
|
78
75
|
stale_draft_labels,
|
|
79
76
|
} from './_lib/release_highlights.js';
|
|
80
|
-
import { loadTaxonomy, measureRange } from './measure_release_mix.js';
|
|
81
77
|
import {
|
|
82
78
|
CURATED_HEAD_INSTRUCTION,
|
|
83
|
-
PROMISE_READBACK_MARKER,
|
|
84
79
|
extract_changelog_section,
|
|
85
|
-
previous_changelog_version,
|
|
86
80
|
} from './_lib/release_material.js';
|
|
87
81
|
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
88
82
|
|
|
@@ -244,23 +238,6 @@ export function main(argv: readonly string[]): number {
|
|
|
244
238
|
);
|
|
245
239
|
return 1;
|
|
246
240
|
}
|
|
247
|
-
// Hoisted above the curated-head branches on purpose. It used to sit inside
|
|
248
|
-
// `contradictions.length === 0`, so CI reached it only for a section that
|
|
249
|
-
// HAS a head, carries no unrewritten draft and contradicts nothing — while
|
|
250
|
-
// the local guards call the same predicate unconditionally. A section
|
|
251
|
-
// without a curated head was therefore refused locally and waved through by
|
|
252
|
-
// CI: one predicate, two different gatekeepers, which is the drift this
|
|
253
|
-
// change exists to remove.
|
|
254
|
-
const mix = check_governance_mix_response(section.body, from, to, version);
|
|
255
|
-
if (mix !== 0) {
|
|
256
|
-
return mix;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const readback = check_previous_promise_readback(changelogText, section.body, version);
|
|
260
|
-
if (readback !== 0) {
|
|
261
|
-
return readback;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
241
|
const curated = parse_curated_head(section.body);
|
|
265
242
|
if (!curated) {
|
|
266
243
|
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
@@ -356,140 +333,6 @@ export function main(argv: readonly string[]): number {
|
|
|
356
333
|
return 1;
|
|
357
334
|
}
|
|
358
335
|
|
|
359
|
-
/**
|
|
360
|
-
* The governance-versus-product response, checked for PRESENCE, never for the
|
|
361
|
-
* ratio (ADR-253).
|
|
362
|
-
*
|
|
363
|
-
* `measure_release_mix` publishes a level. This is the one place that level has
|
|
364
|
-
* a consequence, and the consequence is deliberately about completeness of the
|
|
365
|
-
* written answer: when governance-only commits strictly outnumber consumer-only
|
|
366
|
-
* commits over the release span, the section under release must carry a written
|
|
367
|
-
* response naming either the next cycle's consumer work or a maintainer
|
|
368
|
-
* justification. No number is enforced, and no number can be — both council
|
|
369
|
-
* seats refused a threshold on fewer than two readings, and a gate with no
|
|
370
|
-
* threshold that blocks on the ratio is a smuggled threshold.
|
|
371
|
-
*
|
|
372
|
-
* The response lives OUTSIDE the curated head, as its own `> **Governance
|
|
373
|
-
* mix:**` line, so it does not consume the head's ten-line cap and does not
|
|
374
|
-
* become a sixth label every historical section would suddenly lack.
|
|
375
|
-
*
|
|
376
|
-
* Scope matches every other check here: only the section under release is read.
|
|
377
|
-
*
|
|
378
|
-
* A measurement failure DEGRADES to a printed warning rather than to a refusal.
|
|
379
|
-
* This is a governance signal, not a correctness or security control, and a
|
|
380
|
-
* shallow clone or a missing tag on the release path would otherwise turn an
|
|
381
|
-
* unrelated environment fact into a blocked release.
|
|
382
|
-
*/
|
|
383
|
-
export function check_governance_mix_response(
|
|
384
|
-
body: string,
|
|
385
|
-
from: string,
|
|
386
|
-
to: string,
|
|
387
|
-
version: string,
|
|
388
|
-
): number {
|
|
389
|
-
let reading;
|
|
390
|
-
try {
|
|
391
|
-
reading = measureRange(from, to, loadTaxonomy(), version, REPO_ROOT);
|
|
392
|
-
} catch (err) {
|
|
393
|
-
process.stdout.write(
|
|
394
|
-
`⚠️ governance mix not measured for ${version}: ${(err as Error).message}\n`,
|
|
395
|
-
);
|
|
396
|
-
return 0;
|
|
397
|
-
}
|
|
398
|
-
const o = reading.response_obligation;
|
|
399
|
-
const level = `governance-only ${o.governance_only} vs consumer-only ${o.consumer_only} (taxonomy ${reading.taxonomy_version})`;
|
|
400
|
-
if (!o.triggered) {
|
|
401
|
-
process.stdout.write(`✅ governance mix for ${version}: ${level} — no response owed\n`);
|
|
402
|
-
return 0;
|
|
403
|
-
}
|
|
404
|
-
// ONE predicate, shared with `guard_release_branch_push` — see
|
|
405
|
-
// `mix_response_blockers`. This side prints and exits; the local side dies
|
|
406
|
-
// before anything leaves the machine. Neither owns the rule.
|
|
407
|
-
const blockers = mix_response_blockers(body, version, `\`release/${version}\``, {
|
|
408
|
-
triggered: true,
|
|
409
|
-
level,
|
|
410
|
-
});
|
|
411
|
-
if (blockers.length === 0) {
|
|
412
|
-
process.stdout.write(`✅ governance mix for ${version}: ${level} — response present\n`);
|
|
413
|
-
return 0;
|
|
414
|
-
}
|
|
415
|
-
process.stderr.write(
|
|
416
|
-
`❌ the ${version} section owes a governance-versus-product response: ${level}.\n` +
|
|
417
|
-
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
418
|
-
' Governance-only commits outnumber consumer-only commits over the release span, so\n' +
|
|
419
|
-
' the section carries a written response naming either the next cycle\'s consumer work\n' +
|
|
420
|
-
' or a maintainer justification (docs/contracts/CHANGELOG-conventions.md § Governance-\n' +
|
|
421
|
-
' versus-product response; the decline this replaces is ADR-253).\n' +
|
|
422
|
-
' Only that section is read; historical sections are out of scope. No ratio is\n' +
|
|
423
|
-
' enforced — this blocks a MISSING answer, never a particular number.\n' +
|
|
424
|
-
' Reproduce locally, before any push:\n' +
|
|
425
|
-
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
426
|
-
);
|
|
427
|
-
return 1;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* The previous release's promise, read back at the next release.
|
|
432
|
-
*
|
|
433
|
-
* `_lib/release_material.ts` generates `Next cycle ships …` and, until this
|
|
434
|
-
* check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
|
|
435
|
-
* bridge repair, and no gate in the tree would have noticed the same promise
|
|
436
|
-
* being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
|
|
437
|
-
* free to restate, and one that is free to restate is not a commitment.
|
|
438
|
-
*
|
|
439
|
-
* Only the section under release is read, and only the previous section's
|
|
440
|
-
* governance-response block — the same scope every other check here uses, so a
|
|
441
|
-
* historical section cannot red a future release.
|
|
442
|
-
*
|
|
443
|
-
* The check is about ANSWERING, never about having kept the promise. A gate
|
|
444
|
-
* that refused an unkept promise would price honesty above silence and teach
|
|
445
|
-
* the next author to promise nothing.
|
|
446
|
-
*/
|
|
447
|
-
export function check_previous_promise_readback(
|
|
448
|
-
changelogText: string,
|
|
449
|
-
body: string,
|
|
450
|
-
version: string,
|
|
451
|
-
): number {
|
|
452
|
-
const previousVersion = previous_changelog_version(changelogText, version);
|
|
453
|
-
if (previousVersion === null) {
|
|
454
|
-
process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
|
|
455
|
-
return 0;
|
|
456
|
-
}
|
|
457
|
-
const promise = previous_promise(changelogText, previousVersion);
|
|
458
|
-
if (promise === null) {
|
|
459
|
-
process.stdout.write(
|
|
460
|
-
`✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
|
|
461
|
-
);
|
|
462
|
-
return 0;
|
|
463
|
-
}
|
|
464
|
-
const blockers = promise_readback_blockers(
|
|
465
|
-
body,
|
|
466
|
-
version,
|
|
467
|
-
previousVersion,
|
|
468
|
-
promise,
|
|
469
|
-
`\`release/${version}\``,
|
|
470
|
-
);
|
|
471
|
-
if (blockers.length === 0) {
|
|
472
|
-
process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
|
|
473
|
-
return 0;
|
|
474
|
-
}
|
|
475
|
-
process.stderr.write(
|
|
476
|
-
`❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
|
|
477
|
-
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
478
|
-
` The ${previousVersion} promise reads:\n` +
|
|
479
|
-
promise
|
|
480
|
-
.split('\n')
|
|
481
|
-
.map((l) => ` ${l}\n`)
|
|
482
|
-
.join('') +
|
|
483
|
-
` Answering it is one blockquote line under the curated head, e.g.\n` +
|
|
484
|
-
` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
|
|
485
|
-
' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
|
|
486
|
-
' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
|
|
487
|
-
' Reproduce locally, before any push:\n' +
|
|
488
|
-
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
489
|
-
);
|
|
490
|
-
return 1;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
336
|
const _isMain = (() => {
|
|
494
337
|
const entry = process.argv[1];
|
|
495
338
|
if (!entry) return false;
|
|
@@ -78,8 +78,11 @@ import {
|
|
|
78
78
|
} from './ai_council/config.js';
|
|
79
79
|
import { AuthCache, select_solo_member } from './ai_council/solo_dispatch.js';
|
|
80
80
|
import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
|
|
81
|
-
import { resolveMemberTransport } from './ai_council/transport_resolver.js';
|
|
82
|
-
import {
|
|
81
|
+
import { classifyCliFailure, resolveMemberTransport, type AbsentReason } from './ai_council/transport_resolver.js';
|
|
82
|
+
import {
|
|
83
|
+
disabledSeats as _disabledSeats,
|
|
84
|
+
resolvedTransportFor as _resolvedTransportFor,
|
|
85
|
+
} from './ai_council/status_surface.js';
|
|
83
86
|
import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
|
|
84
87
|
import {
|
|
85
88
|
_emitQuorumEvent,
|
|
@@ -2163,31 +2166,6 @@ function _emit_shadow_slo_banner(): void {
|
|
|
2163
2166
|
* leaves nothing to infer. Exit is always 0 — this reports a state, it does not
|
|
2164
2167
|
* gate — and the verdict line is machine-greppable in both directions.
|
|
2165
2168
|
*/
|
|
2166
|
-
/**
|
|
2167
|
-
* Resolve the concrete transport a member would use on THIS machine right now.
|
|
2168
|
-
*
|
|
2169
|
-
* `cmd_status` exists to answer "is the council reachable, and will it bill me".
|
|
2170
|
-
* Since the transport-mode setting was removed, the second half is not a value
|
|
2171
|
-
* anyone can look up in the config — it is a per-machine resolution. Printing a
|
|
2172
|
-
* configured mode would be the same class of stale answer the removed setting
|
|
2173
|
-
* produced. `mode` is pinned to `auto` because the loader no longer emits
|
|
2174
|
-
* anything else.
|
|
2175
|
-
*/
|
|
2176
|
-
function _resolvedTransportFor(
|
|
2177
|
-
name: string,
|
|
2178
|
-
member: { readonly binary: string | null; readonly api_key_ref: string | null },
|
|
2179
|
-
report?: EnvironmentReport,
|
|
2180
|
-
): ReturnType<typeof resolveMemberTransport> {
|
|
2181
|
-
return resolveMemberTransport({
|
|
2182
|
-
provider: name,
|
|
2183
|
-
report: report ?? detectEnvironment(),
|
|
2184
|
-
invocationMode: null,
|
|
2185
|
-
memberSettings: null,
|
|
2186
|
-
globalMode: 'auto',
|
|
2187
|
-
binaryOverride: member.binary,
|
|
2188
|
-
});
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
2169
|
/** Gather what `fallbackPostureFor` needs; the decision itself is pure. */
|
|
2192
2170
|
function _posture(
|
|
2193
2171
|
name: string,
|
|
@@ -2263,6 +2241,9 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2263
2241
|
transport: t.available ? t.transport : null,
|
|
2264
2242
|
billing: t.available ? t.billing : null,
|
|
2265
2243
|
reason: t.reason,
|
|
2244
|
+
absent_reason: t.absentReason,
|
|
2245
|
+
policy_exclusion: m.policy_exclusion,
|
|
2246
|
+
content_ceiling: m.content_ceiling,
|
|
2266
2247
|
},
|
|
2267
2248
|
];
|
|
2268
2249
|
}),
|
|
@@ -2273,6 +2254,7 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2273
2254
|
enabledMembers.map(([n, m]) => [n, _posture(n, m)]),
|
|
2274
2255
|
),
|
|
2275
2256
|
},
|
|
2257
|
+
disabled_seats: _disabledSeats(cfg),
|
|
2276
2258
|
ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
|
|
2277
2259
|
qualification: qualificationJson(qualifications),
|
|
2278
2260
|
qualified_members: countableSeats(qualifications),
|
|
@@ -2310,6 +2292,15 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2310
2292
|
const billing = t.available ? ` · ${t.billing}` : '';
|
|
2311
2293
|
const detail = t.available ? t.transport : `unavailable — ${t.reason ?? 'no usable transport'}`;
|
|
2312
2294
|
_stdout(` transport ${name}: ${detail}${billing}\n`);
|
|
2295
|
+
// 2.1: the machine-readable half of the refusal.
|
|
2296
|
+
if (!t.available && t.absentReason !== null && t.absentReason.startsWith('policy_')) {
|
|
2297
|
+
_stdout(` policy ${name}: ${t.absentReason}\n`);
|
|
2298
|
+
}
|
|
2299
|
+
_stdout(` content ceiling ${name}: ${member.content_ceiling}\n`);
|
|
2300
|
+
}
|
|
2301
|
+
// 1.2: a disabled seat was invisible here — `enabled` members only.
|
|
2302
|
+
for (const [name, reason] of Object.entries(_disabledSeats(cfg))) {
|
|
2303
|
+
_stdout(` disabled seat ${name}: ${reason}\n`);
|
|
2313
2304
|
}
|
|
2314
2305
|
for (const line of renderPostureLines(
|
|
2315
2306
|
enabledMembers.map(([n, m]) => [n, _posture(n, m)] as const),
|