@event4u/agent-config 12.0.0 → 12.1.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +122 -0
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -7
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-K34QTSD5.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-LKZ5DOYW.js} +557 -297
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-O52B4F5S.js} +123 -2338
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -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 +2574 -1080
- package/dist/install/install.mjs +744 -482
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +113 -10
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +20 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +39 -9
- package/src/config/gate-violation-baselines.json +6 -0
- package/src/config/hook-token-budget.json +7 -1
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +862 -0
- package/src/scripts/ai_council/clients.ts +182 -3
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/capture_skill_catalogue.ts +185 -328
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/hook_manifest.yaml +47 -13
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +95 -1
- package/src/scripts/schemas/command.schema.json +2 -2
- package/src/scripts/schemas/skill.schema.json +2 -1
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
|
@@ -115,7 +115,37 @@ export const DEFAULT_PERPLEXITY_MODEL = 'sonar-pro';
|
|
|
115
115
|
// divergence is a one-line change, not a hunt through inline literals.
|
|
116
116
|
// (`xai` / `perplexity` CLI reuse their API constants: their CLIs are
|
|
117
117
|
// community wrappers around the same paid API, so the values do not diverge.)
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Sentinel: let the codex CLI pick its own model, by omitting `--model`.
|
|
120
|
+
*
|
|
121
|
+
* This is not a placeholder for a value nobody looked up. Measured
|
|
122
|
+
* 2026-08-15 against `codex exec --json` on a ChatGPT-account (subscription)
|
|
123
|
+
* transport, every explicitly named candidate was refused with
|
|
124
|
+
* `400 invalid_request_error: The '<model>' model is not supported when using
|
|
125
|
+
* Codex with a ChatGPT account.` — `gpt-4o`, `gpt-5` (this constant's previous
|
|
126
|
+
* value) and `gpt-5.1-codex` alike. Omitting the flag answered normally.
|
|
127
|
+
*
|
|
128
|
+
* So the only value known to work on this transport is "whatever the CLI
|
|
129
|
+
* chooses", and pinning ANY name here would re-break the seat the next time
|
|
130
|
+
* the vendor rotates its lineup. The label is carried into records as-is so a
|
|
131
|
+
* reader can see that no model was pinned, rather than a name being implied
|
|
132
|
+
* that nobody verified.
|
|
133
|
+
*/
|
|
134
|
+
export const OPENAI_CLI_VENDOR_DEFAULT = 'codex-default';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Models MEASURED to be refused by the codex subscription transport, with the
|
|
138
|
+
* date they were measured. This is a deny-list on purpose: the CLI publishes
|
|
139
|
+
* no allow-list, so the honest claim is "these were rejected when we tried",
|
|
140
|
+
* never "only these are allowed".
|
|
141
|
+
*/
|
|
142
|
+
export const CODEX_MEASURED_UNSERVABLE: ReadonlyMap<string, string> = new Map([
|
|
143
|
+
['gpt-4o', '2026-08-15'],
|
|
144
|
+
['gpt-5', '2026-08-15'],
|
|
145
|
+
['gpt-5.1-codex', '2026-08-15'],
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
|
|
119
149
|
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'claude-sonnet-4-5';
|
|
120
150
|
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
121
151
|
|
|
@@ -665,7 +695,18 @@ export class OpenAIClient extends ExternalAIClient {
|
|
|
665
695
|
|
|
666
696
|
constructor(opts: ApiClientOptions = {}) {
|
|
667
697
|
super();
|
|
668
|
-
|
|
698
|
+
// `codex-default` is the CLI transport's "let the host choose" sentinel,
|
|
699
|
+
// and ONE `model:` field in `.ai-council.yml` feeds both transports —
|
|
700
|
+
// which one a member resolves to is decided at run time, not in the
|
|
701
|
+
// config. The API has no such sentinel (there is no endpoint named
|
|
702
|
+
// `codex-default`), so it resolves to this client's own default instead
|
|
703
|
+
// of being sent verbatim and rejected. Without this, making the shipped
|
|
704
|
+
// template CLI-safe would break every api-transport user in the same
|
|
705
|
+
// commit.
|
|
706
|
+
this.model =
|
|
707
|
+
opts.model === undefined || opts.model === OPENAI_CLI_VENDOR_DEFAULT
|
|
708
|
+
? DEFAULT_OPENAI_MODEL
|
|
709
|
+
: opts.model;
|
|
669
710
|
if (opts.client !== undefined && opts.client !== null) {
|
|
670
711
|
this._client = opts.client;
|
|
671
712
|
return;
|
|
@@ -1252,6 +1293,20 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1252
1293
|
}
|
|
1253
1294
|
}
|
|
1254
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* Refuse a call this transport cannot serve, BEFORE spawning.
|
|
1298
|
+
*
|
|
1299
|
+
* `null` means "nothing known to be wrong" — deliberately not "verified
|
|
1300
|
+
* servable". No vendor CLI here publishes the set of models its
|
|
1301
|
+
* subscription tier accepts, so an allow-list would be invented and would
|
|
1302
|
+
* reject valid pins the day the vendor adds one. What CAN be stated is the
|
|
1303
|
+
* complement: models this estate has MEASURED being rejected. Subclasses
|
|
1304
|
+
* override; the base transport knows of none.
|
|
1305
|
+
*/
|
|
1306
|
+
protected _preflight_transport(): { code: string; detail: string } | null {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1255
1310
|
/** Return text to send on stdin, or `null` to inherit caller's stdin. */
|
|
1256
1311
|
protected _stdin_payload(system_prompt: string, user_prompt: string): string | null {
|
|
1257
1312
|
void system_prompt;
|
|
@@ -1391,6 +1446,26 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1391
1446
|
}
|
|
1392
1447
|
}
|
|
1393
1448
|
|
|
1449
|
+
// 1b. transport pre-flight — refuse BEFORE spending.
|
|
1450
|
+
//
|
|
1451
|
+
// A subscription transport can be structurally unable to serve the
|
|
1452
|
+
// configured model. Discovering that by spawning costs a call against
|
|
1453
|
+
// the daily cap and returns an opaque `exit_1`, which reads as "the CLI
|
|
1454
|
+
// is flaky" rather than "this pin can never work here". A refusal that
|
|
1455
|
+
// names the model and the transport is the difference between a config
|
|
1456
|
+
// bug someone fixes in a minute and a seat that quietly stays dead.
|
|
1457
|
+
const preflight = this._preflight_transport();
|
|
1458
|
+
if (preflight !== null) {
|
|
1459
|
+
return new CouncilResponse({
|
|
1460
|
+
provider: this.name,
|
|
1461
|
+
model: this.model,
|
|
1462
|
+
text: '',
|
|
1463
|
+
latency_ms: _elapsedMs(t0),
|
|
1464
|
+
error: preflight.code,
|
|
1465
|
+
metadata: { cli: true, transport: this.transport, detail: preflight.detail },
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1394
1469
|
// 2. build command + spawn.
|
|
1395
1470
|
const cmd = this._build_command(system_prompt, user_prompt, max_tokens);
|
|
1396
1471
|
const stdinPayload = this._stdin_payload(system_prompt, user_prompt);
|
|
@@ -1726,9 +1801,46 @@ export class OpenAICliClient extends CliClient {
|
|
|
1726
1801
|
void system_prompt;
|
|
1727
1802
|
void user_prompt;
|
|
1728
1803
|
void max_tokens;
|
|
1804
|
+
// `--skip-git-repo-check` is not a convenience flag. Without it codex
|
|
1805
|
+
// refuses any directory it does not consider trusted — captured
|
|
1806
|
+
// `stderr_tail`: `Not inside a trusted directory and
|
|
1807
|
+
// --skip-git-repo-check was not specified.` A worktree path is never
|
|
1808
|
+
// trusted, which is the whole explanation for the standing observation
|
|
1809
|
+
// that the openai seat dies in a worktree while working from the main
|
|
1810
|
+
// checkout. The council reads a prompt and writes nothing through this
|
|
1811
|
+
// process, so the trust gate protects nothing here that the council's
|
|
1812
|
+
// own gates do not already cover.
|
|
1813
|
+
//
|
|
1729
1814
|
// `-` is codex's documented "read the prompt from stdin" positional;
|
|
1730
1815
|
// the payload itself is assembled in `_stdin_payload`.
|
|
1731
|
-
|
|
1816
|
+
const modelArgs =
|
|
1817
|
+
this.model === OPENAI_CLI_VENDOR_DEFAULT ? [] : ['--model', this.model];
|
|
1818
|
+
return [this.binary, 'exec', '--json', '--skip-git-repo-check', ...modelArgs, '-'];
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Refuse a pin this estate has measured the transport rejecting.
|
|
1823
|
+
*
|
|
1824
|
+
* The remedy is named because it is not guessable: removing the `model:`
|
|
1825
|
+
* line is the fix, and "remove a setting" is the last thing an operator
|
|
1826
|
+
* tries when a member is silent.
|
|
1827
|
+
*/
|
|
1828
|
+
protected override _preflight_transport(): { code: string; detail: string } | null {
|
|
1829
|
+
const measuredOn = CODEX_MEASURED_UNSERVABLE.get(this.model);
|
|
1830
|
+
if (measuredOn === undefined) return null;
|
|
1831
|
+
return {
|
|
1832
|
+
code: 'model_unsupported_on_transport',
|
|
1833
|
+
detail:
|
|
1834
|
+
`openai member pins model \`${this.model}\`, which the codex CLI ` +
|
|
1835
|
+
`(subscription transport) refused when measured on ${measuredOn}: ` +
|
|
1836
|
+
`"The '${this.model}' model is not supported when using Codex with a ` +
|
|
1837
|
+
'ChatGPT account." The CLI publishes no list of models it DOES accept, ' +
|
|
1838
|
+
'so there is no supported set to offer here — the one configuration ' +
|
|
1839
|
+
'measured to work is no pin at all. Fix: remove the `model:` line from ' +
|
|
1840
|
+
'the openai member (or set it to `codex-default`) to use the CLI\'s own ' +
|
|
1841
|
+
'default. A pinned model is only reachable on the API transport ' +
|
|
1842
|
+
'(`api_key_ref:` without `mode: cli`).',
|
|
1843
|
+
};
|
|
1732
1844
|
}
|
|
1733
1845
|
|
|
1734
1846
|
/**
|
|
@@ -1752,6 +1864,7 @@ export class OpenAICliClient extends CliClient {
|
|
|
1752
1864
|
let text = '';
|
|
1753
1865
|
let input_tokens = 0;
|
|
1754
1866
|
let output_tokens = 0;
|
|
1867
|
+
let failure: string | null = null;
|
|
1755
1868
|
const meta: Record<string, unknown> = {};
|
|
1756
1869
|
for (let line of _splitlines(stdout)) {
|
|
1757
1870
|
line = line.trim();
|
|
@@ -1794,6 +1907,31 @@ export class OpenAICliClient extends CliClient {
|
|
|
1794
1907
|
text = chunks.join('\n').trim();
|
|
1795
1908
|
}
|
|
1796
1909
|
}
|
|
1910
|
+
// The FLAT shape, and it is the one the CLI emits today:
|
|
1911
|
+
// {"item":{"type":"agent_message","text":"OK"}}
|
|
1912
|
+
// The `content[]` branch above is the older nested form. Both
|
|
1913
|
+
// are read because this adapter has no way to pin a CLI
|
|
1914
|
+
// version, and reading only the nested one is why the seat
|
|
1915
|
+
// returned an empty string with NO error for every call —
|
|
1916
|
+
// measured 2026-08-15, and indistinguishable downstream from
|
|
1917
|
+
// a model that answered with nothing. Only an `agent_message`
|
|
1918
|
+
// is taken: an `error` item also carries `text`-adjacent
|
|
1919
|
+
// fields, and treating one as an answer would turn the
|
|
1920
|
+
// skills-budget warning into the member's response.
|
|
1921
|
+
//
|
|
1922
|
+
// LAST message wins, never the first. A codex turn emits
|
|
1923
|
+
// several `agent_message` items — a preamble ("I'll check X
|
|
1924
|
+
// first…") and then the answer — so keeping the first one
|
|
1925
|
+
// truncates the response to its throat-clearing. Measured
|
|
1926
|
+
// 2026-08-15 on a real council run: 1,479 output tokens
|
|
1927
|
+
// billed, 134 characters captured. A one-word probe cannot
|
|
1928
|
+
// catch this, which is why it survived the first fix.
|
|
1929
|
+
if (_dictGet(itemObj, 'type', undefined) === 'agent_message') {
|
|
1930
|
+
const flat = _pyStr(_dictGet(itemObj, 'text', '')).trim();
|
|
1931
|
+
if (flat) {
|
|
1932
|
+
text = flat;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1797
1935
|
if (_pyTruthy(_dictGet(itemObj, 'id', undefined))) {
|
|
1798
1936
|
meta['item_id'] = _pyStr(itemObj['id']);
|
|
1799
1937
|
}
|
|
@@ -1812,8 +1950,49 @@ export class OpenAICliClient extends CliClient {
|
|
|
1812
1950
|
if (_pyTruthy(_dictGet(ev, 'session_id', undefined))) {
|
|
1813
1951
|
meta['session_id'] = _pyStr(ev['session_id']);
|
|
1814
1952
|
}
|
|
1953
|
+
} else if (event_type === 'turn.failed') {
|
|
1954
|
+
// The turn's real cause lives ONLY here, and codex still exits
|
|
1955
|
+
// 0. Without this branch the response is an empty text, which
|
|
1956
|
+
// the caller reports as `no response` — indistinguishable from
|
|
1957
|
+
// a model that answered with nothing. Measured 2026-08-15: a
|
|
1958
|
+
// pinned `gpt-4o` produced exactly this shape, and the seat
|
|
1959
|
+
// read as flaky for weeks instead of as misconfigured.
|
|
1960
|
+
let err = _dictGet(ev, 'error', null);
|
|
1961
|
+
if (!_pyTruthy(err)) err = {};
|
|
1962
|
+
if (_isPlainObject(err)) {
|
|
1963
|
+
const raw = _pyStr(_dictGet(err as Record<string, unknown>, 'message', ''));
|
|
1964
|
+
if (raw) failure = raw;
|
|
1965
|
+
}
|
|
1815
1966
|
}
|
|
1816
1967
|
}
|
|
1968
|
+
|
|
1969
|
+
if (failure !== null && !text) {
|
|
1970
|
+
const unsupported = /not supported when using Codex/i.test(failure);
|
|
1971
|
+
return new CouncilResponse({
|
|
1972
|
+
provider: this.name,
|
|
1973
|
+
model: this.model,
|
|
1974
|
+
text: '',
|
|
1975
|
+
input_tokens,
|
|
1976
|
+
output_tokens,
|
|
1977
|
+
error: unsupported ? 'model_unsupported_on_transport' : 'turn_failed',
|
|
1978
|
+
metadata: {
|
|
1979
|
+
...meta,
|
|
1980
|
+
// The vendor's own sentence, kept because it names the
|
|
1981
|
+
// model and the account type — the two facts an operator
|
|
1982
|
+
// needs and neither of which the error code carries.
|
|
1983
|
+
detail: failure.slice(0, 400),
|
|
1984
|
+
...(unsupported
|
|
1985
|
+
? {
|
|
1986
|
+
hint:
|
|
1987
|
+
'the codex subscription transport serves no explicitly pinned ' +
|
|
1988
|
+
'model measured so far — remove the `model:` line from the ' +
|
|
1989
|
+
'openai member to use the CLI default',
|
|
1990
|
+
}
|
|
1991
|
+
: {}),
|
|
1992
|
+
},
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1817
1996
|
return new CouncilResponse({
|
|
1818
1997
|
provider: this.name,
|
|
1819
1998
|
model: this.model,
|
|
@@ -81,6 +81,7 @@ import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
|
81
81
|
// `import type` and is erased at build, so there is no runtime cycle today.
|
|
82
82
|
// Converting it to a value import — or adding any other runtime import from
|
|
83
83
|
// quorum into config — creates a real ESM cycle in the loader's init path.
|
|
84
|
+
import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
|
|
84
85
|
import { SOLO_FLOOR_MIN_PRESENT } from './quorum.js';
|
|
85
86
|
|
|
86
87
|
const _VALID_PROVIDERS: ReadonlySet<string> = new Set([
|
|
@@ -1843,7 +1844,21 @@ function _build_member(
|
|
|
1843
1844
|
}
|
|
1844
1845
|
entries.push(entry);
|
|
1845
1846
|
}
|
|
1846
|
-
|
|
1847
|
+
// `codex-default` is a SENTINEL, not a model id — "let the transport
|
|
1848
|
+
// choose". Requiring it to appear on a downgrade ladder of concrete ids
|
|
1849
|
+
// is a category error, and it is the check that blocks the one config
|
|
1850
|
+
// that actually works on a subscription account: the shipped template
|
|
1851
|
+
// cannot pin an id (every id this package shipped is on
|
|
1852
|
+
// `CODEX_MEASURED_UNSERVABLE`), and its ladder is an api-transport
|
|
1853
|
+
// concern the CLI's own default cannot be ordered against. Exempting
|
|
1854
|
+
// the sentinel is narrower than dropping the check — a real pin is
|
|
1855
|
+
// still required to be on its own ladder.
|
|
1856
|
+
if (
|
|
1857
|
+
member_enabled &&
|
|
1858
|
+
model &&
|
|
1859
|
+
model !== OPENAI_CLI_VENDOR_DEFAULT &&
|
|
1860
|
+
!entries.includes(model)
|
|
1861
|
+
) {
|
|
1847
1862
|
throw new CouncilConfigError(
|
|
1848
1863
|
`members.${name}.model_ladder must include the active ` +
|
|
1849
1864
|
`\`model\` (${_pyReprStr(model)}); got ${_pyRepr(entries)}.`,
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// - dict insertion order + `dict(sorted(...))` for the stats aggregation.
|
|
36
36
|
|
|
37
37
|
import { pyRound } from '../_lib/value_ladder.js';
|
|
38
|
+
import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
|
|
38
39
|
import type { CouncilResponse, ExternalAIClient } from './clients.js';
|
|
39
40
|
import type { LowImpactFastPathConfig, MemberConfig } from './config.js';
|
|
40
41
|
import type { ImpactVerdict } from './necessity.js';
|
|
@@ -630,7 +631,19 @@ export function resolve_low_impact(
|
|
|
630
631
|
const pinned = model_downgrade.model_tier_override?.[member.name];
|
|
631
632
|
const cheapest = member.model_ladder.length > 0 ? member.model_ladder[0] : null;
|
|
632
633
|
const target = pinned && pinned.length > 0 ? pinned : cheapest;
|
|
633
|
-
|
|
634
|
+
// A member left on the vendor-default sentinel is NOT downgradable
|
|
635
|
+
// off the ladder. There is nothing to downgrade FROM — the
|
|
636
|
+
// transport picks the model — and writing a rung onto it re-creates
|
|
637
|
+
// the pinned-id shape the seat repair removed. Concretely: the
|
|
638
|
+
// shipped ladder's cheapest rung is `gpt-4o-mini` and its
|
|
639
|
+
// neighbours are `gpt-4o` / `gpt-4.1`, so this line could hand the
|
|
640
|
+
// codex transport an id `CODEX_MEASURED_UNSERVABLE` already records
|
|
641
|
+
// as refused — on a path whose `model_downgrade.enabled` defaults
|
|
642
|
+
// to true. An explicit per-run `model_tier_override` still wins,
|
|
643
|
+
// because that is a human naming a model for this run.
|
|
644
|
+
const unpinned = client.model === OPENAI_CLI_VENDOR_DEFAULT;
|
|
645
|
+
const overrideWins = pinned !== undefined && pinned.length > 0;
|
|
646
|
+
if (target && target !== client.model && (!unpinned || overrideWins)) {
|
|
634
647
|
client.model = target;
|
|
635
648
|
}
|
|
636
649
|
}
|
|
@@ -2029,7 +2029,20 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
|
|
|
2029
2029
|
function _render_quorum_line(q: QuorumResult): string {
|
|
2030
2030
|
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
2031
2031
|
const solo = isSoloConcluded(q) ? ' · **solo** — one voice concluded this pass' : '';
|
|
2032
|
-
|
|
2032
|
+
// The DEGRADED marker shipped on stdout (`council_cli.ts::_format_quorum_line`,
|
|
2033
|
+
// round 7 § 5.3) and did NOT ship here, although this function is the one
|
|
2034
|
+
// the mirror was copied FROM. The two then diverged in the worse direction:
|
|
2035
|
+
// stdout scrolls away, this string is what gets committed as evidence and
|
|
2036
|
+
// cited months later. `1/2 present, needed 1 — concluded` is literally true
|
|
2037
|
+
// and reads as agreement; the solo marker alone covers only present === 1,
|
|
2038
|
+
// so 2-of-4 rendered as plain "concluded" with nothing to stop a reader
|
|
2039
|
+
// inferring convergence. Same sentence as stdout on purpose — one wording,
|
|
2040
|
+
// so neither surface can drift into being the softer one again.
|
|
2041
|
+
const degraded =
|
|
2042
|
+
q.present < q.total
|
|
2043
|
+
? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.`
|
|
2044
|
+
: '';
|
|
2045
|
+
return `**Quorum:** ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${solo}${degraded}`;
|
|
2033
2046
|
}
|
|
2034
2047
|
|
|
2035
2048
|
/**
|
|
@@ -356,6 +356,7 @@ export type CliFailureClass =
|
|
|
356
356
|
| 'binary_missing'
|
|
357
357
|
| 'auth_rejected'
|
|
358
358
|
| 'cli_unsupported'
|
|
359
|
+
| 'model_unservable'
|
|
359
360
|
| 'timeout'
|
|
360
361
|
| 'server_error'
|
|
361
362
|
| 'quota_exhausted'
|
|
@@ -376,6 +377,20 @@ const FALLBACK_ELIGIBLE: ReadonlySet<CliFailureClass> = new Set([
|
|
|
376
377
|
'binary_missing',
|
|
377
378
|
'auth_rejected',
|
|
378
379
|
'cli_unsupported',
|
|
380
|
+
// `model_unservable` satisfies the same no-double-charge property in both
|
|
381
|
+
// of its shapes: the pre-spend gate refuses before any subprocess exists,
|
|
382
|
+
// and the call-time shape is a provider 400 — rejected at the request
|
|
383
|
+
// boundary, no generation performed.
|
|
384
|
+
//
|
|
385
|
+
// HONEST SCOPE: this set is not consumed by any shipped path today.
|
|
386
|
+
// `isFallbackEligible` and `MidFlightFallback` have zero call sites outside
|
|
387
|
+
// this module and its tests, so membership here is a stated property of the
|
|
388
|
+
// class, NOT a behaviour a caller performs. Said plainly because the
|
|
389
|
+
// opposite reading is the attractive one — the constraint really is the
|
|
390
|
+
// subscription transport's rather than the model's, so the api rung really
|
|
391
|
+
// would serve the same id, and it would be easy to write that as if it
|
|
392
|
+
// happened. It does not happen yet.
|
|
393
|
+
'model_unservable',
|
|
379
394
|
]);
|
|
380
395
|
|
|
381
396
|
/** Normalise a raw client error / skip reason into a `CliFailureClass`. */
|
|
@@ -388,6 +403,15 @@ export function classifyCliFailure(raw: string): CliFailureClass {
|
|
|
388
403
|
if (s === 'cli_unsupported' || s === 'unsupported' || s === 'parse_failed') {
|
|
389
404
|
return 'cli_unsupported';
|
|
390
405
|
}
|
|
406
|
+
// Prefix match, not equality: the detail carries the operator-facing reason
|
|
407
|
+
// after the code, and that sentence IS the value of this class — collapsing
|
|
408
|
+
// it back to a bare token would restore the opaque `exit_1` this replaced.
|
|
409
|
+
// Also matched on the provider's own wording, so a model refused at call
|
|
410
|
+
// time (one the deny-list has never seen) classifies the same as one the
|
|
411
|
+
// pre-spend gate caught.
|
|
412
|
+
if (s.startsWith('model_unservable') || s.includes('is not supported when using codex')) {
|
|
413
|
+
return 'model_unservable';
|
|
414
|
+
}
|
|
391
415
|
if (s === 'timeout') return 'timeout';
|
|
392
416
|
if (s === 'cli_quota_exhausted' || s === 'quota_exhausted') return 'quota_exhausted';
|
|
393
417
|
if (/^exit_5\d\d$/.test(s) || s === 'server_error' || s === 'os') return 'server_error';
|
|
@@ -406,8 +430,14 @@ export function isFallbackEligible(failure: CliFailureClass): boolean {
|
|
|
406
430
|
* can bucket every absent member — whether it never got a transport at all
|
|
407
431
|
* or lost one partway through — under one vocabulary. Returns `null` for a
|
|
408
432
|
* failure class outside the four-value enum (`cli_unsupported`,
|
|
409
|
-
* `server_error`, `other`); a caller falls back to the raw
|
|
410
|
-
* for those rather than mis-classifying them.
|
|
433
|
+
* `server_error`, `model_unservable`, `other`); a caller falls back to the raw
|
|
434
|
+
* failure detail for those rather than mis-classifying them.
|
|
435
|
+
*
|
|
436
|
+
* `model_unservable` is deliberately unmapped rather than folded into
|
|
437
|
+
* `no_auth`: the credential is fine, the model is not, and the `detail` string
|
|
438
|
+
* carries the specific reason. So the bucket a reader sees is still the generic
|
|
439
|
+
* `unavailable`, while the line beside it now names the cause — an incomplete
|
|
440
|
+
* improvement, stated as one.
|
|
411
441
|
*/
|
|
412
442
|
export function absentReasonFromCliFailure(failure: CliFailureClass): AbsentReason | null {
|
|
413
443
|
switch (failure) {
|