@event4u/agent-config 9.35.0 → 10.0.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 +95 -142
- package/agents/templates/.ai-council.yml.example +31 -51
- package/dist/agent-src/commands/analyze/conformance.md +15 -4
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/session-canary.md +28 -0
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
- package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
- package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
- package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
- package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
- package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1038 -197
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +9 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- 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 +654 -411
- package/dist/install/install.mjs +39 -29
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1580 -617
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +39 -25
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +15 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +23 -3
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +11 -6
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +16 -9
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/config/agent-settings.template.yml +38 -36
- package/src/config/ci-local-parity.yml +44 -0
- package/src/config/gate-coverage.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_lib/agent_settings.ts +39 -25
- package/src/scripts/_lib/environment_detector.ts +52 -1
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +62 -24
- package/src/scripts/before_complete_hook.ts +39 -0
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/check_bridge_derivation.ts +89 -3
- package/src/scripts/check_ci_local_parity.ts +74 -0
- package/src/scripts/check_references.ts +15 -1
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/conformance_scan.ts +200 -9
- package/src/scripts/council_cli.ts +95 -3
- package/src/scripts/hook_manifest.yaml +30 -9
- package/src/scripts/hooks/block_no_verify.ts +82 -9
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/probe_promissory_closing.ts +180 -0
- package/src/scripts/probe_session_canary.ts +180 -0
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/server/schemas/settings.ts +14 -13
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
|
@@ -38,6 +38,27 @@ const HOOK = path.join(REPO, 'src', 'scripts', 'install-hooks.sh');
|
|
|
38
38
|
/** Roots of the local closure. `ci` is the local mirror; the hook is what a push runs. */
|
|
39
39
|
const LOCAL_ROOTS = ['ci', 'consistency'] as const;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Round 7 § Phase 3 — the third dimension, and why two were not enough.
|
|
43
|
+
*
|
|
44
|
+
* `task ci` INCLUDES `preflight` (`Taskfile.yml:87`), so the two roots above
|
|
45
|
+
* answer "does the local MIRROR match CI". The pass an agent is actually told to
|
|
46
|
+
* run before pushing is `task preflight`, a deliberate subset of `ci` — its own
|
|
47
|
+
* docstring excludes `check_enforcement_coverage` at a measured 30.7 s because
|
|
48
|
+
* "a pre-push gate that doubles the hook teaches people to skip it".
|
|
49
|
+
*
|
|
50
|
+
* That makes `CI ∩ ci \ preflight` invisible to this gate by construction, and it
|
|
51
|
+
* is exactly where round 7's measured "green locally, red remotely" cycles came
|
|
52
|
+
* from: the Thin-Root char cap, the ratchet living in the Node-Tests shard, and
|
|
53
|
+
* the derived-page freshness check.
|
|
54
|
+
*
|
|
55
|
+
* Reported, not gated. The subset is intentional, so failing on it would demand
|
|
56
|
+
* that `preflight` grow into `ci` — the opposite of what its author decided. What
|
|
57
|
+
* this dimension removes is the IMPLICATION that a green preflight means CI will
|
|
58
|
+
* be green.
|
|
59
|
+
*/
|
|
60
|
+
const PREFLIGHT_ROOTS = ['preflight'] as const;
|
|
61
|
+
|
|
41
62
|
export interface Declared {
|
|
42
63
|
id: string;
|
|
43
64
|
class: string;
|
|
@@ -197,6 +218,19 @@ export interface ParityReport {
|
|
|
197
218
|
undeclared_ci_only: string[];
|
|
198
219
|
undeclared_local_only: string[];
|
|
199
220
|
stale_declarations: string[];
|
|
221
|
+
/** Round 7 § Phase 3 — `task preflight`'s own closure. */
|
|
222
|
+
preflight_gates: string[];
|
|
223
|
+
/**
|
|
224
|
+
* `CI ∩ local \ (preflight ∪ pre-push hook)` — gates a push's remote run
|
|
225
|
+
* enforces, the local mirror covers, and NOTHING a push runs locally reaches.
|
|
226
|
+
* Report-only: the measured size of "green locally, red remotely", not a
|
|
227
|
+
* failure.
|
|
228
|
+
*
|
|
229
|
+
* The hook's own closure is in the subtrahend per R2 finding 4: subtracting
|
|
230
|
+
* preflight alone counted gates the pre-push hook already runs, which cannot
|
|
231
|
+
* fail after a push. That read 221; the honest figure is 209.
|
|
232
|
+
*/
|
|
233
|
+
ci_not_in_preflight: string[];
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
export function analyse(repo = REPO, manifest?: Manifest): ParityReport {
|
|
@@ -231,12 +265,38 @@ export function analyse(repo = REPO, manifest?: Manifest): ParityReport {
|
|
|
231
265
|
...[...declaredLocal].filter((g) => localGates.has(g) && ciGates.has(g)),
|
|
232
266
|
].sort();
|
|
233
267
|
|
|
268
|
+
// Round 7 § 3.1 — the third dimension. Derived the same way as the other two,
|
|
269
|
+
// so it cannot drift from them by construction.
|
|
270
|
+
//
|
|
271
|
+
// R2 finding 4 (medium): the first version subtracted only the `preflight`
|
|
272
|
+
// closure from `localGates`, and `localGates` includes every gate reachable
|
|
273
|
+
// from `install-hooks.sh` — which the PRE-PUSH hook runs. A gate that runs
|
|
274
|
+
// before the push lands cannot produce a "green locally, red remotely" cycle,
|
|
275
|
+
// so counting it inflated the number and mislabelled it, in a step whose own
|
|
276
|
+
// text calls the number "the deliverable, not an adjective". The subtrahend is
|
|
277
|
+
// therefore everything a push already runs: preflight PLUS the hook's own
|
|
278
|
+
// closure.
|
|
279
|
+
const preflightGates = local_closure(PREFLIGHT_ROOTS, tasks);
|
|
280
|
+
const prePush = new Set(preflightGates);
|
|
281
|
+
if (fs.existsSync(HOOK)) {
|
|
282
|
+
const hook = fs.readFileSync(HOOK, 'utf-8');
|
|
283
|
+
for (const g of extract_gates(hook)) prePush.add(g);
|
|
284
|
+
for (const t of extract_tasks(hook)) {
|
|
285
|
+
for (const g of local_closure([t], tasks)) prePush.add(g);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const ci_not_in_preflight = [...ciGates]
|
|
289
|
+
.filter((g) => localGates.has(g) && !prePush.has(g))
|
|
290
|
+
.sort();
|
|
291
|
+
|
|
234
292
|
return {
|
|
235
293
|
ci_gates: [...ciGates].sort(),
|
|
236
294
|
local_gates: [...localGates].sort(),
|
|
237
295
|
undeclared_ci_only,
|
|
238
296
|
undeclared_local_only,
|
|
239
297
|
stale_declarations,
|
|
298
|
+
preflight_gates: [...preflightGates].sort(),
|
|
299
|
+
ci_not_in_preflight,
|
|
240
300
|
};
|
|
241
301
|
}
|
|
242
302
|
|
|
@@ -292,6 +352,20 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
292
352
|
for (const g of r.stale_declarations) process.stdout.write(` - ${g}\n`);
|
|
293
353
|
}
|
|
294
354
|
|
|
355
|
+
// Round 7 § 3.2 — printed on EVERY path, pass or fail, because the number is
|
|
356
|
+
// the deliverable. A green parity verdict above says the local MIRROR matches
|
|
357
|
+
// CI; it says nothing about the pass a contributor is told to run, and reading
|
|
358
|
+
// the green line as if it did is the implication this block removes.
|
|
359
|
+
process.stdout.write(
|
|
360
|
+
`ℹ️ preflight: ${String(r.preflight_gates.length)} gate(s) of the ${String(r.local_gates.length)} local ones; ` +
|
|
361
|
+
`${String(r.ci_not_in_preflight.length)} CI-enforced gate(s) run NEITHER in \`task preflight\` NOR\n` +
|
|
362
|
+
' anywhere else the pre-push hook reaches — i.e. the set that can only fail after\n' +
|
|
363
|
+
' a push. `task ci` INCLUDES preflight, so the parity verdict above cannot see it.\n' +
|
|
364
|
+
' Report-only: the subset is deliberate (see the preflight docstring). A green\n' +
|
|
365
|
+
' preflight is therefore not a prediction that CI will be green.\n',
|
|
366
|
+
);
|
|
367
|
+
for (const g of r.ci_not_in_preflight) process.stdout.write(` · ${g}\n`);
|
|
368
|
+
|
|
295
369
|
if (!failed) {
|
|
296
370
|
process.stdout.write(
|
|
297
371
|
`✅ CI ↔ local parity: ${String(r.ci_gates.length)} CI gate(s), ${String(r.local_gates.length)} local, ` +
|
|
@@ -57,7 +57,21 @@ function makeBrokenRef(
|
|
|
57
57
|
|
|
58
58
|
const SCAN_DIRS = ['dist/agent-src', 'agents'];
|
|
59
59
|
const SKIP_DIRS = [
|
|
60
|
-
|
|
60
|
+
// Archived roadmaps have historical refs — a link to a file that existed when
|
|
61
|
+
// the plan was written is not drift, so the exclusion is right on the merits.
|
|
62
|
+
//
|
|
63
|
+
// ROUND 7, DOWNGRADE (2026-08-12): it also means this checker is the ONLY thing
|
|
64
|
+
// that would see a class of real rot, and cannot. Measured: **530 dead relative
|
|
65
|
+
// links across 147 of 466 archived roadmaps**. The cause is not history — it is
|
|
66
|
+
// that `archive_completed_roadmaps` moves a file one directory deeper and never
|
|
67
|
+
// re-depths its outgoing relative links, so a link that resolved before the move
|
|
68
|
+
// is broken BY the move. Two tools, one silence: the writer does not rewrite, and
|
|
69
|
+
// the only checker that would notice is scoped away from where it lands.
|
|
70
|
+
//
|
|
71
|
+
// Not repaired here: the fix is a bulk edit across 147 files plus a re-depth pass
|
|
72
|
+
// in the sweep, which is its own change. Recorded so the exclusion is read as a
|
|
73
|
+
// known gap with a number rather than as coverage.
|
|
74
|
+
'agents/roadmaps/archive',
|
|
61
75
|
'agents/runtime/council/sessions', // per-user audit trail (gitignored), captured provider output
|
|
62
76
|
'agents/runtime/council/responses', // paired council output (gitignored), captured provider output
|
|
63
77
|
'agents/runtime/council/questions', // design Q&A trail — forward-refs to planned artifacts
|
package/src/scripts/condense.ts
CHANGED
|
@@ -359,8 +359,18 @@ const _ALL_TOOLS: ReadonlySet<string> = new Set([
|
|
|
359
359
|
]);
|
|
360
360
|
void _ALL_TOOLS;
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Tool ids selected by `agents/.agent-tools.yml` under `root`, or `null` when
|
|
364
|
+
* the file is absent / malformed / carries no `tools:` list — `null` means
|
|
365
|
+
* "all tools active", never "none".
|
|
366
|
+
*
|
|
367
|
+
* Root-parameterised and exported so a consumer auditing a checkout other than
|
|
368
|
+
* `MODULE_STATE.PROJECT_ROOT` (a gate run with `--root`) reads the selection
|
|
369
|
+
* this generator actually honours, instead of re-implementing these four
|
|
370
|
+
* fallbacks and drifting from them.
|
|
371
|
+
*/
|
|
372
|
+
export function active_tools_at(root: string): ReadonlySet<string> | null {
|
|
373
|
+
const tools_file = path.join(root, 'agents', '.agent-tools.yml');
|
|
364
374
|
if (!_exists(tools_file)) {
|
|
365
375
|
return null;
|
|
366
376
|
}
|
|
@@ -380,6 +390,10 @@ function _active_tools(): ReadonlySet<string> | null {
|
|
|
380
390
|
return new Set(tools.filter((t): t is string => typeof t === 'string'));
|
|
381
391
|
}
|
|
382
392
|
|
|
393
|
+
function _active_tools(): ReadonlySet<string> | null {
|
|
394
|
+
return active_tools_at(MODULE_STATE.PROJECT_ROOT);
|
|
395
|
+
}
|
|
396
|
+
|
|
383
397
|
function _tool_active(tool_id: string): boolean {
|
|
384
398
|
const active = _active_tools();
|
|
385
399
|
return active === null ? true : active.has(tool_id);
|
|
@@ -41,6 +41,9 @@ import { isSyntheticPrompt } from "./_lib/prompt_shape.js";
|
|
|
41
41
|
import { classifyAuthorization, type GitOp } from "./git_authorization_hook.js";
|
|
42
42
|
import { BLOCK_OPS, commandOp } from "./hooks/block_unauthorized_git.js";
|
|
43
43
|
import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.js";
|
|
44
|
+
// Round 7 § 1.5 — the SAME predicate `turn-end-gate` refuses on, imported rather
|
|
45
|
+
// than reimplemented, so the measured rate and the gate cannot disagree.
|
|
46
|
+
import { detectCompletionClaim } from "./hooks/turn_end_gate_hook.js";
|
|
44
47
|
import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evidence_independence.js";
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -57,7 +60,12 @@ import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evid
|
|
|
57
60
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
58
61
|
|
|
59
62
|
export interface Violation {
|
|
60
|
-
check:
|
|
63
|
+
check:
|
|
64
|
+
| "language-pin"
|
|
65
|
+
| "git-authorization"
|
|
66
|
+
| "vacuous-evidence"
|
|
67
|
+
| "evidence-steering"
|
|
68
|
+
| "completion-claim";
|
|
61
69
|
session: string;
|
|
62
70
|
at: string;
|
|
63
71
|
detail: string;
|
|
@@ -78,6 +86,17 @@ export interface SessionReport {
|
|
|
78
86
|
user_turns: number;
|
|
79
87
|
assistant_turns: number;
|
|
80
88
|
violations: Violation[];
|
|
89
|
+
/**
|
|
90
|
+
* Round 7 § 6.2 — assistant turns that stood under a GERMAN pin, i.e. the only
|
|
91
|
+
* turns on which `language-pin` can fire at all. The rate was published over
|
|
92
|
+
* ALL assistant turns, which understates it by whatever fraction of a corpus is
|
|
93
|
+
* English-pinned: measured on round 7's own corpus, 1 655 of 2 354 (70 %), and
|
|
94
|
+
* the same 152 violations read 6.5 % on the old denominator and 9.2 % on this
|
|
95
|
+
* one — enough to move the band verdict from OUTSIDE to INSIDE.
|
|
96
|
+
*/
|
|
97
|
+
de_pin_turns: number;
|
|
98
|
+
/** First entry timestamp, so a reader can era-split without a second pass. */
|
|
99
|
+
first_at: string;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
/**
|
|
@@ -134,7 +153,7 @@ export const OBSERVED_BAND = { low: 9.1, high: 39.2 } as const;
|
|
|
134
153
|
*/
|
|
135
154
|
export const BAND_MIN_TURNS = 1978;
|
|
136
155
|
|
|
137
|
-
export type BandVerdict = "inside" | "outside" | "corpus-too-small";
|
|
156
|
+
export type BandVerdict = "inside" | "outside" | "corpus-too-small" | "era-spanning";
|
|
138
157
|
|
|
139
158
|
/** Round-6 Phase 4.5 — the forward-capture record for one scan run. */
|
|
140
159
|
export interface RateRecord {
|
|
@@ -144,7 +163,29 @@ export interface RateRecord {
|
|
|
144
163
|
assistant_turns: number;
|
|
145
164
|
language_pin: number;
|
|
146
165
|
rate_pct: number;
|
|
147
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Round 7 § 6.2 / 6.1 — OPTIONAL, and not for convenience.
|
|
168
|
+
*
|
|
169
|
+
* `--record` appends one line per run to a series whose whole purpose is
|
|
170
|
+
* comparability over time, and every line written before round 7 carries none
|
|
171
|
+
* of these four. Declaring them required would make the type say something
|
|
172
|
+
* false about the records on disk, and the renderer already reads them
|
|
173
|
+
* defensively for exactly that reason. `analyse()` always populates them, so a
|
|
174
|
+
* fresh run is never partial — the optionality describes the archive, not the
|
|
175
|
+
* producer.
|
|
176
|
+
*/
|
|
177
|
+
de_pin_turns?: number;
|
|
178
|
+
/** The same numerator over `de_pin_turns`. Reported beside `rate_pct`, never instead. */
|
|
179
|
+
rate_pct_de_pin?: number;
|
|
180
|
+
/** The corpus straddles a carrier landing, so no band verdict. */
|
|
181
|
+
era_spanning?: boolean;
|
|
182
|
+
/** Which carrier, when `era_spanning` — empty otherwise. */
|
|
183
|
+
era_reason?: string;
|
|
184
|
+
/**
|
|
185
|
+
* `outside` is the falsifier firing; `corpus-too-small` and `era-spanning` are
|
|
186
|
+
* not readings — the first is about corpus size, the second about the corpus
|
|
187
|
+
* containing two different systems.
|
|
188
|
+
*/
|
|
148
189
|
band: BandVerdict;
|
|
149
190
|
/**
|
|
150
191
|
* `null` when the scanned store is NOT this repo's own — `measureDelivered` is
|
|
@@ -157,7 +198,49 @@ export interface RateRecord {
|
|
|
157
198
|
delivered_global_tokens: number | null;
|
|
158
199
|
}
|
|
159
200
|
|
|
160
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Round 7 § 6.1 — dates on which a carrier for a mechanised check LANDED.
|
|
203
|
+
*
|
|
204
|
+
* A rate pooled across one of these is not a reading about behaviour; it is a
|
|
205
|
+
* weighted average of two different systems. The band's falsifier is "a fourth
|
|
206
|
+
* PROJECT outside the band", and on round 7's first run it fired on the project
|
|
207
|
+
* that helped DEFINE the band — because the corpus straddled the language-pin
|
|
208
|
+
* carrier. 23.1 % before it, 0.0 % after, 6.5 % pooled, and the pooled figure was
|
|
209
|
+
* announced as the falsifier.
|
|
210
|
+
*
|
|
211
|
+
* Declared as data with provenance rather than inferred: nothing in a transcript
|
|
212
|
+
* records which carriers were bound while it was written.
|
|
213
|
+
*/
|
|
214
|
+
export const CARRIER_CHANGES: readonly { at: string; what: string }[] = [
|
|
215
|
+
{ at: "2026-08-07T00:00:00Z", what: "language-pin bound on user_prompt_submit (round 5)" },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
/** True when the corpus spans a carrier change, i.e. two systems in one average. */
|
|
219
|
+
export function spansCarrierChange(firstAts: readonly string[]): { spans: boolean; what: string } {
|
|
220
|
+
const stamps = firstAts.filter((s) => s !== "").sort();
|
|
221
|
+
if (stamps.length === 0) {
|
|
222
|
+
return { spans: false, what: "" };
|
|
223
|
+
}
|
|
224
|
+
const lo = stamps[0] as string;
|
|
225
|
+
const hi = stamps[stamps.length - 1] as string;
|
|
226
|
+
for (const c of CARRIER_CHANGES) {
|
|
227
|
+
if (lo < c.at && hi >= c.at) {
|
|
228
|
+
return { spans: true, what: c.what };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { spans: false, what: "" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function bandVerdict(
|
|
235
|
+
rate_pct: number,
|
|
236
|
+
assistant_turns: number,
|
|
237
|
+
opts: { spansCarrier?: boolean } = {},
|
|
238
|
+
): BandVerdict {
|
|
239
|
+
// The era guard is checked FIRST and for the same reason the turn floor is: a
|
|
240
|
+
// verdict about corpus shape must not be reported as a verdict about behaviour.
|
|
241
|
+
if (opts.spansCarrier === true) {
|
|
242
|
+
return "era-spanning";
|
|
243
|
+
}
|
|
161
244
|
if (assistant_turns < BAND_MIN_TURNS) {
|
|
162
245
|
return "corpus-too-small";
|
|
163
246
|
}
|
|
@@ -181,6 +264,7 @@ export const CHECK_IDS: readonly CheckId[] = [
|
|
|
181
264
|
"git-authorization",
|
|
182
265
|
"vacuous-evidence",
|
|
183
266
|
"evidence-steering",
|
|
267
|
+
"completion-claim",
|
|
184
268
|
];
|
|
185
269
|
|
|
186
270
|
/**
|
|
@@ -207,6 +291,12 @@ export const CHECK_MEANINGS: Record<CheckId, string> = {
|
|
|
207
291
|
"evidence-steering":
|
|
208
292
|
"A self-commissioned evaluation whose prompt pre-loaded its own verdict, " +
|
|
209
293
|
"or a second self-scoped evaluation of the same subject within one turn.",
|
|
294
|
+
"completion-claim":
|
|
295
|
+
"An assistant turn claiming the work is complete while the last CI read in " +
|
|
296
|
+
"that session was NOT settled. Round 7 measured 14 instances by reading, " +
|
|
297
|
+
"each followed by the user handing the work back; this is the same " +
|
|
298
|
+
"predicate `turn-end-gate`'s completion detector refuses on, so the rate " +
|
|
299
|
+
"and the gate cannot disagree. A session that never polled CI can never hit.",
|
|
210
300
|
};
|
|
211
301
|
|
|
212
302
|
/**
|
|
@@ -317,7 +407,11 @@ export function isInjectedBody(text: string): boolean {
|
|
|
317
407
|
* `API Error: 529 Overloaded` retry banners. Counting the harness as the model
|
|
318
408
|
* inflates the very number this scan exists to report honestly.
|
|
319
409
|
*/
|
|
320
|
-
|
|
410
|
+
// Exported since round 7 § 6.3: `probe_session_canary` needs the SAME predicate,
|
|
411
|
+
// and its first version had its own copy that omitted `You've hit your` — which
|
|
412
|
+
// turned a spend-limit banner into a missed greeting and reported 24/28 for a
|
|
413
|
+
// corpus that is 25/28.
|
|
414
|
+
export const HARNESS_TEXT =
|
|
321
415
|
/^(API Error:|Request (timed out|was aborted)|\[Request interrupted|Error: |Credit balance is too low|You've hit your|Prompt is too long)/i;
|
|
322
416
|
|
|
323
417
|
/** Rough English-opener test for the language check (first prose line only). */
|
|
@@ -330,6 +424,8 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
330
424
|
user_turns: 0,
|
|
331
425
|
assistant_turns: 0,
|
|
332
426
|
violations: [],
|
|
427
|
+
de_pin_turns: 0,
|
|
428
|
+
first_at: "",
|
|
333
429
|
};
|
|
334
430
|
|
|
335
431
|
let pinned: "de" | "en" | null = null;
|
|
@@ -343,6 +439,10 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
343
439
|
// that split reproducible against a future corpus instead of re-argued.
|
|
344
440
|
let turnsSincePrompt = 0;
|
|
345
441
|
let compactionSincePrompt = false;
|
|
442
|
+
// Round 7 § 1.5 — session-scoped, deliberately NOT reset per prompt: the
|
|
443
|
+
// measured failure is a completion claim in a LATER turn than the poll.
|
|
444
|
+
let ciSeen = false;
|
|
445
|
+
let ciSettled = false;
|
|
346
446
|
|
|
347
447
|
for (const line of lines) {
|
|
348
448
|
if (!line.trim()) {
|
|
@@ -354,6 +454,12 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
354
454
|
} catch {
|
|
355
455
|
continue;
|
|
356
456
|
}
|
|
457
|
+
// Round 7 § 6.1 — captured for the era guard. The FIRST timestamp in the
|
|
458
|
+
// file, not the file mtime: mtime moves when a session is resumed, which
|
|
459
|
+
// would put a July session in the August era.
|
|
460
|
+
if (report.first_at === "" && typeof entry["timestamp"] === "string") {
|
|
461
|
+
report.first_at = entry["timestamp"];
|
|
462
|
+
}
|
|
357
463
|
|
|
358
464
|
// Tool results arrive as `user` entries carrying `toolUseResult`.
|
|
359
465
|
if (entry["type"] === "user" && entry["toolUseResult"] !== undefined && pendingPoll !== null) {
|
|
@@ -370,6 +476,14 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
370
476
|
detail: `CI poll read as settled without an in-flight observation: ${out.slice(0, 90)}`,
|
|
371
477
|
});
|
|
372
478
|
}
|
|
479
|
+
// Round 7 § 1.5 — the session-scoped CI-settle state the completion-claim
|
|
480
|
+
// check reads. R2 finding 3: this copied `pending === 0` AND the wrong
|
|
481
|
+
// comment claiming it matched the producer's discrimination. It did not —
|
|
482
|
+
// a stale all-pass table read as a settle and silenced the check. The
|
|
483
|
+
// in-flight witness is required here too, so the measurement and the gate
|
|
484
|
+
// agree on what a settle is.
|
|
485
|
+
ciSeen = true;
|
|
486
|
+
ciSettled = pending === 0 && sawPending && !isVacuousOutput(out);
|
|
373
487
|
pendingPoll = null;
|
|
374
488
|
continue;
|
|
375
489
|
}
|
|
@@ -424,6 +538,26 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
424
538
|
if (prose !== null) {
|
|
425
539
|
report.assistant_turns += 1;
|
|
426
540
|
turnsSincePrompt += 1;
|
|
541
|
+
// Round 7 § 6.2 — the denominator the language check can actually fire on.
|
|
542
|
+
// Counted here, beside the numerator, so the two cannot be derived from
|
|
543
|
+
// different populations later.
|
|
544
|
+
if (pinned === "de") {
|
|
545
|
+
report.de_pin_turns += 1;
|
|
546
|
+
}
|
|
547
|
+
// Round 7 § 1.5 — placed BEFORE the language block on purpose: that block
|
|
548
|
+
// `continue`s on a harness banner, and a check placed after it would be
|
|
549
|
+
// skipped for every such turn without saying so.
|
|
550
|
+
if (!HARNESS_TEXT.test((prose.split("\n").find((l) => l.trim()) ?? "").trim())) {
|
|
551
|
+
const cc = detectCompletionClaim(prose, { seen: ciSeen, settled: ciSettled });
|
|
552
|
+
if (cc !== null) {
|
|
553
|
+
report.violations.push({
|
|
554
|
+
check: "completion-claim",
|
|
555
|
+
session: sessionId,
|
|
556
|
+
at,
|
|
557
|
+
detail: `completion claimed over an unsettled CI read: "${cc.evidence.slice(0, 110)}"`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
427
561
|
if (pinned === "de") {
|
|
428
562
|
const first = prose.split("\n").find((l) => l.trim()) ?? "";
|
|
429
563
|
if (HARNESS_TEXT.test(first.trim())) {
|
|
@@ -585,6 +719,7 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
585
719
|
"git-authorization": 0,
|
|
586
720
|
"vacuous-evidence": 0,
|
|
587
721
|
"evidence-steering": 0,
|
|
722
|
+
"completion-claim": 0,
|
|
588
723
|
};
|
|
589
724
|
for (const s of per_session) {
|
|
590
725
|
for (const v of s.violations) {
|
|
@@ -600,6 +735,14 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
600
735
|
// band" — a rounding artefact announcing the declared falsifier.
|
|
601
736
|
const rate_pct =
|
|
602
737
|
assistant_turns === 0 ? 0 : Number(((100 * language_pin) / assistant_turns).toFixed(1));
|
|
738
|
+
// Round 7 § 6.2 — the second denominator, reported beside the first rather than
|
|
739
|
+
// instead of it: replacing the figure silently would break comparability with
|
|
740
|
+
// every recorded run, and the band's own three reference values were computed
|
|
741
|
+
// on the all-turns denominator.
|
|
742
|
+
const de_pin_turns = per_session.reduce((n, s) => n + s.de_pin_turns, 0);
|
|
743
|
+
const rate_pct_de_pin =
|
|
744
|
+
de_pin_turns === 0 ? 0 : Number(((100 * language_pin) / de_pin_turns).toFixed(1));
|
|
745
|
+
const era = spansCarrierChange(per_session.map((s) => s.first_at));
|
|
603
746
|
const delivered = measureDelivered(
|
|
604
747
|
path.join(REPO_ROOT, ".claude", "rules"),
|
|
605
748
|
path.join(process.env["HOME"] ?? os.homedir(), ".claude", "rules"),
|
|
@@ -619,7 +762,11 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
619
762
|
assistant_turns,
|
|
620
763
|
language_pin,
|
|
621
764
|
rate_pct,
|
|
622
|
-
|
|
765
|
+
de_pin_turns,
|
|
766
|
+
rate_pct_de_pin,
|
|
767
|
+
era_spanning: era.spans,
|
|
768
|
+
era_reason: era.what,
|
|
769
|
+
band: bandVerdict(rate_pct, assistant_turns, { spansCarrier: era.spans }),
|
|
623
770
|
// Only attach the carrier figures when the rate and the carriers describe
|
|
624
771
|
// the same project.
|
|
625
772
|
delivered_project_tokens: ownStore ? delivered.project.tokens : null,
|
|
@@ -680,7 +827,36 @@ export function render(report: ScanReport): string {
|
|
|
680
827
|
` language-pin rate ${report.rate.rate_pct.toFixed(1)}% ` +
|
|
681
828
|
`(${report.rate.language_pin} of ${report.rate.assistant_turns} assistant turns)`,
|
|
682
829
|
);
|
|
683
|
-
|
|
830
|
+
// Round 7 § 6.2 — both denominators, always. The band's three reference values
|
|
831
|
+
// were computed on the all-turns one, so that stays the headline; the second is
|
|
832
|
+
// the only one the check can actually fire on, and on round 7's own corpus the
|
|
833
|
+
// two differ by enough to move the verdict (6.5 % vs 9.2 %, band low 9.1 %).
|
|
834
|
+
// Optional-read, not `report.rate.x.toFixed(...)`: a `rate` block RECORDED
|
|
835
|
+
// before round 7 carries neither field, and `--record` exists to be read back
|
|
836
|
+
// over time. A renderer that throws on last month's line makes the series
|
|
837
|
+
// unreadable to prove a point about denominators.
|
|
838
|
+
const dePinTurns = report.rate.de_pin_turns;
|
|
839
|
+
const dePinRate = report.rate.rate_pct_de_pin;
|
|
840
|
+
if (typeof dePinTurns === 'number' && typeof dePinRate === 'number') {
|
|
841
|
+
lines.push(
|
|
842
|
+
` ${dePinRate.toFixed(1)}% ` +
|
|
843
|
+
`(${report.rate.language_pin} of ${dePinTurns} turns under a GERMAN pin — the only` +
|
|
844
|
+
' turns this check can fire on)',
|
|
845
|
+
);
|
|
846
|
+
lines.push(
|
|
847
|
+
' The band (M5, n=3) was computed on the ALL-turns denominator, so the first figure',
|
|
848
|
+
);
|
|
849
|
+
lines.push(' is the comparable one and the second is the honest one. Both are printed.');
|
|
850
|
+
}
|
|
851
|
+
if (report.rate.band === "era-spanning") {
|
|
852
|
+
lines.push(
|
|
853
|
+
` Not compared: the corpus SPANS a carrier landing — ${report.rate.era_reason || 'see CARRIER_CHANGES'}.`,
|
|
854
|
+
);
|
|
855
|
+
lines.push(' A rate pooled across it is a weighted average of two different systems, not');
|
|
856
|
+
lines.push(' a reading about behaviour. Round 7 measured 23.1% before and 0.0% after that');
|
|
857
|
+
lines.push(' exact carrier; the pooled 6.5% was announced as the falsifier firing.');
|
|
858
|
+
lines.push(' Split the window at the carrier date and read each era on its own.');
|
|
859
|
+
} else if (report.rate.band === "corpus-too-small") {
|
|
684
860
|
lines.push(
|
|
685
861
|
` Not compared: ${report.rate.assistant_turns} turns is below the ${BAND_MIN_TURNS} of the`,
|
|
686
862
|
);
|
|
@@ -702,8 +878,23 @@ export function render(report: ScanReport): string {
|
|
|
702
878
|
lines.push(" as new. Store novelty is not project novelty.");
|
|
703
879
|
}
|
|
704
880
|
lines.push("");
|
|
705
|
-
|
|
706
|
-
|
|
881
|
+
// Round 7 § 6.4 — this footer used to lump four classes together as "NOT
|
|
882
|
+
// measured", and two of the four were wrong. Stated per class instead, because
|
|
883
|
+
// "not measured here" and "not measured anywhere" are different facts and only
|
|
884
|
+
// one of them is a gap.
|
|
885
|
+
lines.push(" Scanned checks are exactly the mechanised ones. What is NOT in the counts above:");
|
|
886
|
+
lines.push(" ask-shape (trailing free-text offer) — measured NOWHERE. ~11 findings in round 7's");
|
|
887
|
+
lines.push(" reading half; the discriminator needs judgement, so it stays advisory on purpose.");
|
|
888
|
+
lines.push(" session-canary (opening greeting) — not here, but PROBEABLE:");
|
|
889
|
+
lines.push(" `./scripts-run src/scripts/probe_session_canary`. Round 7: 25/28 sessions,");
|
|
890
|
+
lines.push(" 24/25 post-carrier. The per-TASK instance is undecidable — no task boundary");
|
|
891
|
+
lines.push(" is recorded in a transcript.");
|
|
892
|
+
lines.push(" promissory closings — not here, but ALREADY GATED: `turn-end-gate`");
|
|
893
|
+
lines.push(" ships a blocking `detectPromissory`. Round 7 probe: 1 of 120 hand-back turns");
|
|
894
|
+
lines.push(" (the 163 this line first printed was the retracted denominator — it counted");
|
|
895
|
+
lines.push(" synthetic user turns as hand-back closers).");
|
|
896
|
+
lines.push(" `./scripts-run src/scripts/probe_promissory_closing`.");
|
|
897
|
+
lines.push(" checkbox batching — measured NOWHERE. ~5 low-severity findings.");
|
|
707
898
|
return lines.join("\n");
|
|
708
899
|
}
|
|
709
900
|
|
|
@@ -749,11 +749,18 @@ function _postRunQuorum(
|
|
|
749
749
|
for (let i = 0; i < members.length; i++) {
|
|
750
750
|
const m = members[i] as ExternalAIClient;
|
|
751
751
|
const r = responses[i];
|
|
752
|
-
|
|
752
|
+
// Round 7 § 5.2 — attendance is a NON-EMPTY answer, not the absence of an
|
|
753
|
+
// error. `!r.error` alone counted a member that returned zero bytes, and
|
|
754
|
+
// that is not hypothetical: a 290 s curl timeout returned an empty body
|
|
755
|
+
// with no error set in two sessions (`9fc9ba3e`, `4ac2f7ac`) and the
|
|
756
|
+
// banner printed `2/2 present` — a single-voice verdict presented as
|
|
757
|
+
// convergence, on a paid run. An empty answer contributes nothing to a
|
|
758
|
+
// quorum by definition, whatever the transport thought of it.
|
|
759
|
+
if (r !== undefined && !r.error && r.text.trim() !== '') {
|
|
753
760
|
present += 1;
|
|
754
761
|
continue;
|
|
755
762
|
}
|
|
756
|
-
const raw = r?.error ?? 'no response';
|
|
763
|
+
const raw = r?.error ?? (r !== undefined && r.text.trim() === '' ? 'empty response body' : 'no response');
|
|
757
764
|
const failure = classifyCliFailure(raw);
|
|
758
765
|
absent.push({
|
|
759
766
|
member: m.name,
|
|
@@ -772,7 +779,14 @@ function _postRunQuorum(
|
|
|
772
779
|
*/
|
|
773
780
|
function _format_quorum_line(q: QuorumResult): string {
|
|
774
781
|
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
775
|
-
|
|
782
|
+
// Round 7 § 5.3 — a degraded pass says so. `1/2 present, needed 1 —
|
|
783
|
+
// concluded` is literally true and reads as agreement; with a threshold of 1
|
|
784
|
+
// a solo answer concludes, and nothing in the line distinguishes "both
|
|
785
|
+
// members agreed" from "one member answered". The counts were always there;
|
|
786
|
+
// what was missing is the word that stops a reader inferring convergence.
|
|
787
|
+
const degraded =
|
|
788
|
+
q.present < q.total ? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.` : '';
|
|
789
|
+
return `council:quorum · ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
|
|
776
790
|
}
|
|
777
791
|
|
|
778
792
|
function build_members(settings: Dict, opts: BuildMembersOptions = {}): ExternalAIClient[] {
|
|
@@ -2328,6 +2342,31 @@ function _emit_shadow_slo_banner(): void {
|
|
|
2328
2342
|
* leaves nothing to infer. Exit is always 0 — this reports a state, it does not
|
|
2329
2343
|
* gate — and the verdict line is machine-greppable in both directions.
|
|
2330
2344
|
*/
|
|
2345
|
+
/**
|
|
2346
|
+
* Resolve the concrete transport a member would use on THIS machine right now.
|
|
2347
|
+
*
|
|
2348
|
+
* `cmd_status` exists to answer "is the council reachable, and will it bill me".
|
|
2349
|
+
* Since the transport-mode setting was removed, the second half is not a value
|
|
2350
|
+
* anyone can look up in the config — it is a per-machine resolution. Printing a
|
|
2351
|
+
* configured mode would be the same class of stale answer the removed setting
|
|
2352
|
+
* produced. `mode` is pinned to `auto` because the loader no longer emits
|
|
2353
|
+
* anything else.
|
|
2354
|
+
*/
|
|
2355
|
+
function _resolvedTransportFor(
|
|
2356
|
+
name: string,
|
|
2357
|
+
member: { readonly binary: string | null; readonly api_key_ref: string | null },
|
|
2358
|
+
report?: EnvironmentReport,
|
|
2359
|
+
): ReturnType<typeof resolveMemberTransport> {
|
|
2360
|
+
return resolveMemberTransport({
|
|
2361
|
+
provider: name,
|
|
2362
|
+
report: report ?? detectEnvironment(),
|
|
2363
|
+
invocationMode: null,
|
|
2364
|
+
memberSettings: null,
|
|
2365
|
+
globalMode: 'auto',
|
|
2366
|
+
binaryOverride: member.binary,
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2331
2370
|
export function cmd_status(args: Args, opts: { env?: Record<string, string | undefined> } = {}): number {
|
|
2332
2371
|
const env = opts.env ?? process.env;
|
|
2333
2372
|
const override = env[COUNCIL_CONFIG_ENV];
|
|
@@ -2363,6 +2402,21 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2363
2402
|
members_total: cfg === null ? null : cfg.members.size,
|
|
2364
2403
|
members_enabled: cfg === null ? null : enabledMembers.length,
|
|
2365
2404
|
member_names: enabledMembers.map(([n]) => n),
|
|
2405
|
+
transports: Object.fromEntries(
|
|
2406
|
+
enabledMembers.map(([n, m]) => {
|
|
2407
|
+
const t = _resolvedTransportFor(n, m);
|
|
2408
|
+
return [
|
|
2409
|
+
n,
|
|
2410
|
+
{
|
|
2411
|
+
available: t.available,
|
|
2412
|
+
transport: t.available ? t.transport : null,
|
|
2413
|
+
billing: t.available ? t.billing : null,
|
|
2414
|
+
reason: t.reason,
|
|
2415
|
+
},
|
|
2416
|
+
];
|
|
2417
|
+
}),
|
|
2418
|
+
),
|
|
2419
|
+
ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
|
|
2366
2420
|
parse_error,
|
|
2367
2421
|
project_tree_consulted: false,
|
|
2368
2422
|
})}\n`,
|
|
@@ -2383,6 +2437,25 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2383
2437
|
if (enabledMembers.length > 0) {
|
|
2384
2438
|
_stdout(` ${enabledMembers.map(([n]) => n).join(', ')}\n`);
|
|
2385
2439
|
}
|
|
2440
|
+
// Transport is resolved per machine, so the only honest way to answer
|
|
2441
|
+
// "will this cost money" is to resolve it here rather than print a
|
|
2442
|
+
// configured value. The billing class comes from the detected auth
|
|
2443
|
+
// source, never from the transport name — a community CLI wrapper
|
|
2444
|
+
// shells out to the same paid API and stays metered.
|
|
2445
|
+
for (const [name, member] of enabledMembers) {
|
|
2446
|
+
const t = _resolvedTransportFor(name, member);
|
|
2447
|
+
const billing = t.available ? ` · ${t.billing}` : '';
|
|
2448
|
+
const detail = t.available ? t.transport : `unavailable — ${t.reason ?? 'no usable transport'}`;
|
|
2449
|
+
_stdout(` transport ${name}: ${detail}${billing}\n`);
|
|
2450
|
+
}
|
|
2451
|
+
if (cfg.ignored_transport_keys.length > 0) {
|
|
2452
|
+
_stdout('\n');
|
|
2453
|
+
_stdout(' Ignored transport keys — transport is resolved, not configured:\n');
|
|
2454
|
+
for (const key of cfg.ignored_transport_keys) {
|
|
2455
|
+
_stdout(` ${key}\n`);
|
|
2456
|
+
}
|
|
2457
|
+
_stdout(' Safe to delete from the config file; they change nothing.\n');
|
|
2458
|
+
}
|
|
2386
2459
|
}
|
|
2387
2460
|
_stdout('\n');
|
|
2388
2461
|
_stdout(' The project tree is NEVER consulted (ADR-104). No project file —\n');
|
|
@@ -2593,6 +2666,25 @@ function cmd_run(
|
|
|
2593
2666
|
return 0;
|
|
2594
2667
|
}
|
|
2595
2668
|
|
|
2669
|
+
// Round 7 § 5.1 — VALIDATE THE OUTPUT PATH BEFORE THE FIRST BILLABLE CALL.
|
|
2670
|
+
//
|
|
2671
|
+
// The only validation used to sit immediately before the WRITE, i.e. after
|
|
2672
|
+
// every member had been paid, so a wrong directory discarded a completed run.
|
|
2673
|
+
// Measured across round 7's corpus: ~$1.30 in three sessions (`291f827b`
|
|
2674
|
+
// ~$0.42, `9502795e` $0.44, plus a discarded run each in `d6154522` and
|
|
2675
|
+
// `3d50d0df`). The trap sat in the operator's notes for weeks and was still
|
|
2676
|
+
// paid twice, because the defect is the ORDERING, not the caller.
|
|
2677
|
+
//
|
|
2678
|
+
// Placed HERE, after the `--confirm` gate, and not at function entry: the
|
|
2679
|
+
// estimate-only path neither writes nor bills, and the first attempt at this
|
|
2680
|
+
// fix validated at entry and broke it — caught by
|
|
2681
|
+
// `council_cli.test.ts`'s no-confirm case. Nothing above this line spends.
|
|
2682
|
+
//
|
|
2683
|
+
// The validator is pure, so the pre-write call stays: it returns the string
|
|
2684
|
+
// the write uses, and removing it to avoid "duplicate work" would put the
|
|
2685
|
+
// guarantee back in a single place that runs late.
|
|
2686
|
+
_validate_council_output_path(args.output as string, { kind: 'responses', subcommand: 'run' });
|
|
2687
|
+
|
|
2596
2688
|
const cost_cfg = (ai_cfg['cost_budget'] as Dict) || {};
|
|
2597
2689
|
const budget = new CostBudget({
|
|
2598
2690
|
max_input_tokens: _pyInt(cost_cfg['max_input_tokens'] ?? 50_000, 50_000),
|