@event4u/agent-config 9.28.0 → 9.29.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/AGENTS.md +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- 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 +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- 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_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -7
- 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 +1 -1
- 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 +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -4,23 +4,36 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the fail-closed validation posture of the council loader
|
|
6
6
|
* (`src/scripts/ai_council/config.ts`) at a fraction of its surface: the
|
|
7
|
-
* `ai_team` block has
|
|
8
|
-
* loader is deliberately lean — defaults,
|
|
9
|
-
* unknown-key rejection. Contract:
|
|
7
|
+
* `ai_team` block has a handful of flat keys and no nested sub-schemas
|
|
8
|
+
* beyond `review_gate`, so this loader is deliberately lean — defaults,
|
|
9
|
+
* per-key type checks, and hard unknown-key rejection. Contract:
|
|
10
|
+
* `docs/contracts/ai-team-config.md`.
|
|
10
11
|
*
|
|
11
12
|
* Unlike the council (ALWAYS user-global `.ai-council.yml`, ADR-104),
|
|
12
13
|
* `ai_team` lives in the PROJECT settings cascade (`.agent-settings.yml`
|
|
13
|
-
* via `load_agent_settings`) — team mode is a per-project posture
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* via `load_agent_settings`) — team mode is a per-project posture, not a
|
|
15
|
+
* per-developer credential store; the codex CLI carries the subscription
|
|
16
|
+
* auth itself.
|
|
17
|
+
*
|
|
18
|
+
* `enabled` — the former master on/off switch — was DELETED
|
|
19
|
+
* (road-to-always-on-orchestration Phase 1, Step 1.3): `/team`'s
|
|
20
|
+
* availability is now a FACT resolved from the machine (codex CLI on
|
|
21
|
+
* `$PATH` + detectable auth — `src/scripts/ai_team/availability.ts`), not
|
|
22
|
+
* a settings flag, matching the doctrine already applied to the
|
|
23
|
+
* subagent/council layers. A leftover `enabled` key from an older install
|
|
24
|
+
* is accepted-and-ignored here (never rejected as unknown, never applied
|
|
25
|
+
* by any reader) so an existing project's `.agent-settings.yml` does not
|
|
26
|
+
* start failing `/team` outright; `load_agent_settings` still surfaces the
|
|
27
|
+
* one-line deprecation warning for it (`_lib/agent_settings.ts`).
|
|
16
28
|
*
|
|
17
29
|
* Validation contract (all enforced at load time, fail-closed):
|
|
18
30
|
*
|
|
19
|
-
* 1. `ai_team` absent → defaults
|
|
31
|
+
* 1. `ai_team` absent → defaults. Non-mapping → error.
|
|
20
32
|
* 2. Unknown keys under `ai_team` are rejected — a typo must never
|
|
21
33
|
* silently disable a gate (`allow_delegate` misspelled = delegation
|
|
22
|
-
* stays off AND the load fails loudly).
|
|
23
|
-
*
|
|
34
|
+
* stays off AND the load fails loudly). `enabled` is the one
|
|
35
|
+
* deliberate exception: accepted, untyped, ignored (see above).
|
|
36
|
+
* 3. `allow_delegate`, `suppress_setup_hint` are booleans.
|
|
24
37
|
* 4. `model` is a non-empty string. `'auto'` = pass no `--model` flag
|
|
25
38
|
* (the codex CLI default applies); any other value passes through
|
|
26
39
|
* verbatim.
|
|
@@ -47,7 +60,6 @@ export interface AiTeamReviewGateConfig {
|
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
export interface AiTeamConfig {
|
|
50
|
-
readonly enabled: boolean;
|
|
51
63
|
readonly model: string;
|
|
52
64
|
readonly allow_delegate: boolean;
|
|
53
65
|
readonly max_calls_per_day: number;
|
|
@@ -63,7 +75,6 @@ export const AI_TEAM_REVIEW_GATE_DEFAULTS: AiTeamReviewGateConfig = Object.freez
|
|
|
63
75
|
|
|
64
76
|
/** Shipped defaults — byte-parity with `src/config/agent-settings.template.yml`. */
|
|
65
77
|
export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
|
|
66
|
-
enabled: false,
|
|
67
78
|
model: 'auto',
|
|
68
79
|
allow_delegate: false,
|
|
69
80
|
max_calls_per_day: 50,
|
|
@@ -79,7 +90,6 @@ export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
|
|
|
79
90
|
export const AI_TEAM_MODEL_AUTO = 'auto';
|
|
80
91
|
|
|
81
92
|
const _KNOWN_KEYS: ReadonlySet<string> = new Set([
|
|
82
|
-
'enabled',
|
|
83
93
|
'model',
|
|
84
94
|
'allow_delegate',
|
|
85
95
|
'max_calls_per_day',
|
|
@@ -87,6 +97,14 @@ const _KNOWN_KEYS: ReadonlySet<string> = new Set([
|
|
|
87
97
|
'review_gate',
|
|
88
98
|
]);
|
|
89
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Deleted key accepted-and-ignored for migration grace (see module docstring).
|
|
102
|
+
* Never added to `_KNOWN_KEYS` — it carries no validated type and is never
|
|
103
|
+
* read into `AiTeamConfig`; it exists in this set only so the unknown-key
|
|
104
|
+
* loop below does not fail-closed an existing project's leftover value.
|
|
105
|
+
*/
|
|
106
|
+
const _DELETED_KEYS: ReadonlySet<string> = new Set(['enabled']);
|
|
107
|
+
|
|
90
108
|
const _KNOWN_REVIEW_GATE_KEYS: ReadonlySet<string> = new Set([
|
|
91
109
|
'managed',
|
|
92
110
|
'max_consecutive_blocks',
|
|
@@ -175,6 +193,9 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
|
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
for (const key of Object.keys(raw)) {
|
|
196
|
+
if (_DELETED_KEYS.has(key)) {
|
|
197
|
+
continue; // deleted, accepted-and-ignored — see module docstring.
|
|
198
|
+
}
|
|
178
199
|
if (!_KNOWN_KEYS.has(key)) {
|
|
179
200
|
throw new TeamConfigError(
|
|
180
201
|
`ai_team.${key}: unknown key; valid: ` +
|
|
@@ -183,7 +204,6 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
|
|
|
183
204
|
}
|
|
184
205
|
}
|
|
185
206
|
|
|
186
|
-
const enabled = _requireBool(raw, 'enabled', AI_TEAM_DEFAULTS.enabled);
|
|
187
207
|
const allow_delegate = _requireBool(raw, 'allow_delegate', AI_TEAM_DEFAULTS.allow_delegate);
|
|
188
208
|
const suppress_setup_hint = _requireBool(
|
|
189
209
|
raw,
|
|
@@ -216,7 +236,7 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
|
|
|
216
236
|
|
|
217
237
|
const review_gate = _build_review_gate('review_gate' in raw ? raw['review_gate'] : undefined);
|
|
218
238
|
|
|
219
|
-
return {
|
|
239
|
+
return { model, allow_delegate, max_calls_per_day, suppress_setup_hint, review_gate };
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
/**
|
|
@@ -18,11 +18,20 @@
|
|
|
18
18
|
* instead of live repo access, no background jobs, explicitly worse than the
|
|
19
19
|
* plugin — pointing Claude-Code users back to the Phase 2 plugin path.
|
|
20
20
|
*
|
|
21
|
-
* Governance reuse (Phase 3 Step 3): same `ai_team` config
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* Governance reuse (Phase 3 Step 3): same `ai_team` config, same
|
|
22
|
+
* `cli_call_budget` openai quota bucket (the gate and the recording both
|
|
23
|
+
* live in `CliClient.ask` — no new counter), same `_AUTH_FAILURE_PATTERNS`
|
|
24
|
+
* classification. `--manual` renders the bundle between `═` rules for
|
|
25
|
+
* paste-into-web usage — no call, no quota.
|
|
26
|
+
*
|
|
27
|
+
* Availability (road-to-always-on-orchestration Phase 1, Step 1.3):
|
|
28
|
+
* `ai_team.enabled` was DELETED. Every dispatch entry point below is
|
|
29
|
+
* fail-closed on TWO facts instead — `emergency.orchestration_halt` (the
|
|
30
|
+
* one audited incident switch over the always-on stack) and
|
|
31
|
+
* `checkCodexAvailability()` (codex CLI + auth presence,
|
|
32
|
+
* `./availability.js`) — never a settings flag. `ai_team.allow_delegate`
|
|
33
|
+
* is unchanged: a second, write-access opt-in that stacks ON TOP of
|
|
34
|
+
* availability in `assert_delegate_allowed`.
|
|
26
35
|
*
|
|
27
36
|
* The review system prompt is derived from the adversarial-review findings
|
|
28
37
|
* shape of an Apache-2.0 upstream — attribution in the repo-root NOTICE file.
|
|
@@ -38,6 +47,12 @@ import {
|
|
|
38
47
|
load_cli_call_counts,
|
|
39
48
|
OpenAICliClient,
|
|
40
49
|
} from '../ai_council/clients.js';
|
|
50
|
+
import {
|
|
51
|
+
checkCodexAvailability,
|
|
52
|
+
isOrchestrationHalted,
|
|
53
|
+
ORCHESTRATION_HALT_MESSAGE,
|
|
54
|
+
type TeamAvailability,
|
|
55
|
+
} from './availability.js';
|
|
41
56
|
import { AI_TEAM_MODEL_AUTO, type AiTeamConfig, load_ai_team_config } from './config.js';
|
|
42
57
|
|
|
43
58
|
// ── errors ──────────────────────────────────────────────────────────────
|
|
@@ -51,50 +66,67 @@ export class TeamDispatchError extends Error {
|
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
|
-
* Raised when `
|
|
55
|
-
*
|
|
69
|
+
* Raised when `/team` is unavailable — either `emergency.orchestration_halt`
|
|
70
|
+
* is set, or the codex CLI / auth availability check
|
|
71
|
+
* (`checkCodexAvailability`) failed. `reason` is the one-line message the
|
|
72
|
+
* relevant check produced; callers print it verbatim. There is no more
|
|
73
|
+
* settings flag behind this — see the module docstring.
|
|
56
74
|
*/
|
|
57
75
|
export class TeamDisabledError extends TeamDispatchError {
|
|
58
|
-
constructor() {
|
|
59
|
-
super(
|
|
60
|
-
'team mode is disabled (`ai_team.enabled: false`, the shipped default). ' +
|
|
61
|
-
'Set `ai_team.enabled: true` in `.agent-settings.yml` to opt in — ' +
|
|
62
|
-
'see docs/contracts/ai-team-config.md.',
|
|
63
|
-
);
|
|
76
|
+
constructor(reason: string) {
|
|
77
|
+
super(reason);
|
|
64
78
|
this.name = 'TeamDisabledError';
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
/**
|
|
69
|
-
* Thrown by `assert_delegate_allowed` when `
|
|
83
|
+
* Thrown by `assert_delegate_allowed` when `/team` is available but the
|
|
70
84
|
* second opt-in (`ai_team.allow_delegate`) is not. `/team delegate` is the
|
|
71
85
|
* only wrapper that hands WRITE access to the second model, so it is
|
|
72
|
-
* double-gated;
|
|
86
|
+
* double-gated; availability alone is never delegate authorization.
|
|
73
87
|
*/
|
|
74
88
|
export class TeamDelegateDisabledError extends TeamDispatchError {
|
|
75
89
|
constructor() {
|
|
76
90
|
super(
|
|
77
91
|
'`/team delegate` is disabled (`ai_team.allow_delegate: false`, the shipped ' +
|
|
78
92
|
'default). It is the only team-mode wrapper that hands write access to ' +
|
|
79
|
-
'the second model, so it needs its own opt-in: set
|
|
80
|
-
'`ai_team.
|
|
81
|
-
'
|
|
93
|
+
'the second model, so it needs its own opt-in: set ' +
|
|
94
|
+
'`ai_team.allow_delegate: true` in `.agent-settings.yml` (once /team ' +
|
|
95
|
+
'itself is available — codex CLI installed and authenticated) — see ' +
|
|
96
|
+
'docs/contracts/ai-team-config.md.',
|
|
82
97
|
);
|
|
83
98
|
this.name = 'TeamDelegateDisabledError';
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
/** Test seams for the two availability facts (`assert_delegate_allowed`, `run_team_review`). */
|
|
103
|
+
export interface TeamAvailabilityOverrides {
|
|
104
|
+
/** Default: `checkCodexAvailability()` — the real, cached, zero-spend probe. */
|
|
105
|
+
availability?: TeamAvailability;
|
|
106
|
+
/** Default: `isOrchestrationHalted(undefined, cwd)` — reads the real settings cascade. */
|
|
107
|
+
halted?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
/**
|
|
88
111
|
* Deterministic mirror of the `/team delegate` double gate: throws
|
|
89
|
-
* `TeamDisabledError`
|
|
112
|
+
* `TeamDisabledError` when halted or `/team` is unavailable, then
|
|
90
113
|
* `TeamDelegateDisabledError` unless `ai_team.allow_delegate`. The command
|
|
91
114
|
* doc's prose gates instruct the agent; this guard is the machine-checkable
|
|
92
115
|
* contract (`--delegate-gate` CLI mode) tests pin against.
|
|
93
116
|
*/
|
|
94
|
-
export function assert_delegate_allowed(
|
|
117
|
+
export function assert_delegate_allowed(
|
|
118
|
+
config?: AiTeamConfig,
|
|
119
|
+
cwd?: string | null,
|
|
120
|
+
overrides: TeamAvailabilityOverrides = {},
|
|
121
|
+
): void {
|
|
95
122
|
const cfg = config ?? load_ai_team_config({ cwd: cwd ?? null });
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
const halted = overrides.halted ?? isOrchestrationHalted(undefined, cwd ?? null);
|
|
124
|
+
if (halted) {
|
|
125
|
+
throw new TeamDisabledError(ORCHESTRATION_HALT_MESSAGE);
|
|
126
|
+
}
|
|
127
|
+
const availability = overrides.availability ?? checkCodexAvailability(cwd ?? undefined);
|
|
128
|
+
if (!availability.available) {
|
|
129
|
+
throw new TeamDisabledError(availability.reason ?? 'codex CLI not available.');
|
|
98
130
|
}
|
|
99
131
|
if (!cfg.allow_delegate) {
|
|
100
132
|
throw new TeamDelegateDisabledError();
|
|
@@ -511,6 +543,10 @@ export interface RunTeamReviewOptions {
|
|
|
511
543
|
cli_calls_path?: string | null;
|
|
512
544
|
/** Sink for user-facing output; default `process.stdout`. */
|
|
513
545
|
out?: (line: string) => void;
|
|
546
|
+
/** Test seam: override the availability check (default: the real, cached probe). */
|
|
547
|
+
availability?: TeamAvailability;
|
|
548
|
+
/** Test seam: override the halted flag (default: read from the settings cascade). */
|
|
549
|
+
halted?: boolean;
|
|
514
550
|
}
|
|
515
551
|
|
|
516
552
|
function _quota_snapshot(config: AiTeamConfig, cli_calls_path: string | null): TeamReviewQuota {
|
|
@@ -564,16 +600,22 @@ function _envelope_from_response(
|
|
|
564
600
|
/**
|
|
565
601
|
* Run the multi-host team-review fallback.
|
|
566
602
|
*
|
|
567
|
-
* Order of operations is the contract: (1) fail-closed
|
|
568
|
-
* (2) capability-delta header at the top,
|
|
569
|
-
* (4) manual render OR one synchronous `codex exec`
|
|
570
|
-
* quota/auth machinery, (5) envelope emission.
|
|
571
|
-
*
|
|
603
|
+
* Order of operations is the contract: (1) fail-closed availability gate —
|
|
604
|
+
* halted, then codex CLI/auth — (2) capability-delta header at the top,
|
|
605
|
+
* (3) read-only bundle, (4) manual render OR one synchronous `codex exec`
|
|
606
|
+
* call through the shared quota/auth machinery, (5) envelope emission.
|
|
607
|
+
* Throws `TeamDisabledError` when halted or unavailable — never a silent
|
|
608
|
+
* no-op.
|
|
572
609
|
*/
|
|
573
610
|
export function run_team_review(opts: RunTeamReviewOptions = {}): TeamReviewRunResult {
|
|
574
611
|
const config = opts.config ?? load_ai_team_config({ cwd: opts.cwd ?? null });
|
|
575
|
-
|
|
576
|
-
|
|
612
|
+
const halted = opts.halted ?? isOrchestrationHalted(undefined, opts.cwd ?? null);
|
|
613
|
+
if (halted) {
|
|
614
|
+
throw new TeamDisabledError(ORCHESTRATION_HALT_MESSAGE);
|
|
615
|
+
}
|
|
616
|
+
const availability = opts.availability ?? checkCodexAvailability(opts.cwd ?? undefined);
|
|
617
|
+
if (!availability.available) {
|
|
618
|
+
throw new TeamDisabledError(availability.reason ?? 'codex CLI not available.');
|
|
577
619
|
}
|
|
578
620
|
const out = opts.out ?? ((line: string): void => void process.stdout.write(`${line}\n`));
|
|
579
621
|
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critic-protocol A/B runner (road-to-judgment-and-forensic-evidence Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Measures the `load_bearing` critic protocol against the shipped `legacy`
|
|
5
|
+
* adversarial-skeptic posture on the frozen corpus
|
|
6
|
+
* (internal/bench/adversarial-council/corpus.json) — the corpus on which the
|
|
7
|
+
* legacy posture scored a measured 100% false-positive rate on the 3
|
|
8
|
+
* controversial-but-correct clean controls (run 2026-07-21).
|
|
9
|
+
*
|
|
10
|
+
* Protocol (pre-registered in docs/CLAIMS.md `critic-protocol-load-bearing-ab`
|
|
11
|
+
* BEFORE any paid run; thresholds, prompts, and scorer semantics frozen there):
|
|
12
|
+
*
|
|
13
|
+
* Both arms run as ONE independent single-shot review per vendor
|
|
14
|
+
* (anthropic + openai, direct client calls — NEVER council_cli transport)
|
|
15
|
+
* over all 15 corpus items (12 defect fixtures + 3 clean controls).
|
|
16
|
+
*
|
|
17
|
+
* PER VENDOR, the load_bearing arm promotes iff BOTH:
|
|
18
|
+
* (1) FP rate on the 3 clean controls < 50%, AND
|
|
19
|
+
* (2) TP count on the 12 defect fixtures >= 80% of the legacy arm's
|
|
20
|
+
* TP count in the same run.
|
|
21
|
+
*
|
|
22
|
+
* Scoring is the existing deterministic scorer applied IDENTICALLY to both
|
|
23
|
+
* arms on findings only (caughtDefect / isFalsePositive). The protocol's
|
|
24
|
+
* `verdict` field is a secondary published signal; "flawed" with empty
|
|
25
|
+
* findings counts as `incoherent` (2026-08-09 council design pass) — never
|
|
26
|
+
* as a catch, never as an FP.
|
|
27
|
+
*
|
|
28
|
+
* `--mock` validates the pipeline in BOTH directions with zero spend (a
|
|
29
|
+
* passing arm must read PASS, a failing arm must read FAIL) — the roadmap's
|
|
30
|
+
* pre-spend tripwire. `--run` fires the paid cross-vendor calls
|
|
31
|
+
* (maintainer spend-gated). `--out <file>` writes the report JSON.
|
|
32
|
+
*
|
|
33
|
+
* The `legacy` default and the `critic_protocol` config key live in
|
|
34
|
+
* scripts/ai_council/config.ts; this runner takes the arms from the corpus
|
|
35
|
+
* loop directly (an A/B needs both regardless of the configured value).
|
|
36
|
+
*/
|
|
37
|
+
// ledger-exempt: bench runner, not a CI gate — spend-gated on-demand measurement
|
|
38
|
+
// with a fixed 15-item corpus; the report itself carries the full denominator.
|
|
39
|
+
import * as fs from 'node:fs';
|
|
40
|
+
import * as path from 'node:path';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
AnthropicClient,
|
|
44
|
+
OpenAIClient,
|
|
45
|
+
load_anthropic_key,
|
|
46
|
+
load_openai_key,
|
|
47
|
+
type ExternalAIClient,
|
|
48
|
+
} from './ai_council/clients.js';
|
|
49
|
+
import {
|
|
50
|
+
caughtDefect,
|
|
51
|
+
isFalsePositive,
|
|
52
|
+
categoryFamily,
|
|
53
|
+
type Finding,
|
|
54
|
+
type GroundTruth,
|
|
55
|
+
} from './_lib/adversarial_bench_score.js';
|
|
56
|
+
|
|
57
|
+
const REPO_ROOT = process.cwd();
|
|
58
|
+
const CORPUS_PATH = path.join(REPO_ROOT, 'internal/bench/adversarial-council/corpus.json');
|
|
59
|
+
|
|
60
|
+
/** Pre-registered thresholds — frozen with the CLAIMS entry, never adjusted. */
|
|
61
|
+
const FP_CEILING = 0.5; // strict: FP rate must be < 0.5 on the 3 controls
|
|
62
|
+
const TP_RETENTION_FLOOR = 0.8; // load_bearing TP >= 0.8 × legacy TP, per vendor
|
|
63
|
+
|
|
64
|
+
interface CorpusItem {
|
|
65
|
+
id: string;
|
|
66
|
+
class: string;
|
|
67
|
+
defect_category: string;
|
|
68
|
+
is_clean: boolean;
|
|
69
|
+
files: { path: string; content: string }[];
|
|
70
|
+
ground_truth: { defect_files: string[]; defect_summary: string };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadCorpus(): CorpusItem[] {
|
|
74
|
+
return JSON.parse(fs.readFileSync(CORPUS_PATH, 'utf8')).items as CorpusItem[];
|
|
75
|
+
}
|
|
76
|
+
function groundTruth(item: CorpusItem): GroundTruth {
|
|
77
|
+
return {
|
|
78
|
+
id: item.id,
|
|
79
|
+
is_clean: item.is_clean,
|
|
80
|
+
defect_files: item.ground_truth.defect_files,
|
|
81
|
+
defect_category: item.is_clean ? 'none' : categoryFamily(item.defect_category),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function codeBlock(item: CorpusItem): string {
|
|
85
|
+
return item.files.map((f) => `FILE: ${f.path}\n\`\`\`ts\n${f.content}\`\`\``).join('\n\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const FINDINGS_SHAPE =
|
|
89
|
+
'{"findings":[{"file":"<file path>","category":"<defect category>","confidence":"high|medium|low","summary":"<one line>"}]}';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* LEGACY arm — byte-identical to the prompt that produced the measured 100%
|
|
93
|
+
* FP on the clean controls (bench_adversarial_council.ts skepticPrompt).
|
|
94
|
+
*/
|
|
95
|
+
function legacyPrompt(item: CorpusItem): string {
|
|
96
|
+
return [
|
|
97
|
+
'You are an adversarial security/correctness skeptic. Assume this code may hide a subtle, non-obvious defect that a normal review missed — a masked access-control/SSRF/traversal flaw, a boundary/logic inversion, a concurrency or resource-leak edge, a cross-file contract mismatch. Hunt for it. Report only defects you can concretely justify; if after hard scrutiny it is genuinely correct, report an empty array (do not fabricate).',
|
|
98
|
+
`Respond with STRICT JSON only, exactly: ${FINDINGS_SHAPE}`,
|
|
99
|
+
'',
|
|
100
|
+
codeBlock(item),
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PROTOCOL_SHAPE =
|
|
105
|
+
'{"invariant":"<one sentence>","load_bearing_assumption":"<one sentence, names file+function>","failure_scenario_if_broken":"<one sentence>","cost_of_avoidance":"<one sentence>","succeeded_would_do":"<one sentence or \'nothing\'>","verdict":"holds|flawed","findings":[{"file":"<file path>","category":"<defect category>","confidence":"high|medium|low","summary":"<one line>"}]}';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* LOAD_BEARING arm — the fixed protocol. Design integrated the 2026-08-09
|
|
109
|
+
* council convergence: the assumption must be tied to a named file+function
|
|
110
|
+
* (no platitudes), a concrete failure scenario forces the assumption to be
|
|
111
|
+
* load-bearing, and "holds" is an explicitly permitted, completed review.
|
|
112
|
+
*/
|
|
113
|
+
function loadBearingPrompt(item: CorpusItem): string {
|
|
114
|
+
return [
|
|
115
|
+
'You are a rigorous correctness critic running a fixed protocol. Do not free-associate defects. Work strictly in this order:',
|
|
116
|
+
'1. State the invariant this code must maintain to be correct, then name the SINGLE load-bearing assumption it rests on — the one whose failure would break that invariant. Tie it to a named file and function; a platitude ("inputs are valid") is not an assumption.',
|
|
117
|
+
'2. State the concrete failure scenario if that assumption broke: which input or state leads to which wrong behaviour. If you cannot state one, the assumption is not load-bearing — pick again.',
|
|
118
|
+
'3. State the cost of what this code is avoiding: what failure mode or complexity does its current shape avert?',
|
|
119
|
+
"4. State what someone who has already built a comparable thing successfully would do differently here, if anything ('nothing' is acceptable).",
|
|
120
|
+
'5. Now inspect the assumption against the code. If it survives inspection and the failure scenario is unreachable, your verdict is "holds" and findings is []. A defensible "holds" with the assumption and scenario stated is a completed review, not a refusal. If the assumption breaks, or the failure scenario is reachable, or you find another concrete defect you can justify against specific lines, your verdict is "flawed" and findings lists each one.',
|
|
121
|
+
`Respond with STRICT JSON only, exactly: ${PROTOCOL_SHAPE}`,
|
|
122
|
+
'',
|
|
123
|
+
codeBlock(item),
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SYSTEM_JSON =
|
|
128
|
+
'You are a code reviewer. Output ONLY one JSON object — no prose, no markdown, no code fences. ' +
|
|
129
|
+
'If the code is correct, the "findings" array is empty. Do not include any text before or after the JSON object.';
|
|
130
|
+
|
|
131
|
+
interface ArmResponse {
|
|
132
|
+
findings: Finding[];
|
|
133
|
+
verdict: 'holds' | 'flawed' | null; // null for the legacy arm (no verdict field)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractJsonObject(text: string): Record<string, unknown> | null {
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
const start = text.indexOf('{');
|
|
139
|
+
if (start < 0) return null;
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let end = -1;
|
|
142
|
+
for (let i = start; i < text.length; i++) {
|
|
143
|
+
if (text[i] === '{') depth++;
|
|
144
|
+
else if (text[i] === '}' && --depth === 0) {
|
|
145
|
+
end = i;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (end < 0) return null;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text.slice(start, end + 1)) as Record<string, unknown>;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseArmResponse(text: string): ArmResponse {
|
|
158
|
+
const obj = extractJsonObject(text);
|
|
159
|
+
if (!obj) return { findings: [], verdict: null };
|
|
160
|
+
const arr = Array.isArray(obj.findings) ? obj.findings : [];
|
|
161
|
+
const findings = arr
|
|
162
|
+
.filter((f): f is Record<string, unknown> => typeof f === 'object' && f !== null)
|
|
163
|
+
.map((f) => ({
|
|
164
|
+
file: String(f.file ?? ''),
|
|
165
|
+
category: String(f.category ?? ''),
|
|
166
|
+
confidence: (['high', 'medium', 'low'].includes(String(f.confidence))
|
|
167
|
+
? String(f.confidence)
|
|
168
|
+
: 'high') as 'high' | 'medium' | 'low',
|
|
169
|
+
}))
|
|
170
|
+
.filter((f) => f.file.length > 0);
|
|
171
|
+
const verdict = obj.verdict === 'holds' || obj.verdict === 'flawed' ? obj.verdict : null;
|
|
172
|
+
return { findings, verdict };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type Arm = 'legacy' | 'load_bearing';
|
|
176
|
+
type Vendor = 'anthropic' | 'openai';
|
|
177
|
+
const ARMS: readonly Arm[] = ['legacy', 'load_bearing'];
|
|
178
|
+
const VENDORS: readonly Vendor[] = ['anthropic', 'openai'];
|
|
179
|
+
|
|
180
|
+
/** fixtureId → arm → vendor → response */
|
|
181
|
+
type Reviews = Record<string, Record<Arm, Record<Vendor, ArmResponse>>>;
|
|
182
|
+
|
|
183
|
+
let _clients: Record<Vendor, ExternalAIClient> | null = null;
|
|
184
|
+
function getClients(): Record<Vendor, ExternalAIClient> {
|
|
185
|
+
if (!_clients) {
|
|
186
|
+
_clients = {
|
|
187
|
+
anthropic: new AnthropicClient({ model: 'claude-sonnet-4-5', api_key: load_anthropic_key() }),
|
|
188
|
+
openai: new OpenAIClient({ model: 'gpt-4o', api_key: load_openai_key() }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return _clients;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function realReviews(items: CorpusItem[]): Promise<Reviews> {
|
|
195
|
+
const cs = getClients();
|
|
196
|
+
const out: Reviews = {};
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
const perArm = { legacy: {}, load_bearing: {} } as Reviews[string];
|
|
199
|
+
out[item.id] = perArm;
|
|
200
|
+
for (const arm of ARMS) {
|
|
201
|
+
const prompt = arm === 'legacy' ? legacyPrompt(item) : loadBearingPrompt(item);
|
|
202
|
+
for (const vendor of VENDORS) {
|
|
203
|
+
let text = '';
|
|
204
|
+
try {
|
|
205
|
+
const resp = await cs[vendor].ask(SYSTEM_JSON, prompt, 1600);
|
|
206
|
+
text = resp.text ?? '';
|
|
207
|
+
} catch (e) {
|
|
208
|
+
process.stderr.write(` ${vendor} error on ${item.id}/${arm}: ${(e as Error).message}\n`);
|
|
209
|
+
}
|
|
210
|
+
const r = parseArmResponse(text);
|
|
211
|
+
perArm[arm][vendor] = r;
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
` ${arm} ${item.id} ${vendor}: findings=${String(r.findings.length)} verdict=${r.verdict ?? '-'}\n`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Mock reviews for pipeline validation in a chosen direction.
|
|
223
|
+
* `direction: 'pass'` — load_bearing keeps TP parity with legacy and stays
|
|
224
|
+
* silent on the controls. `direction: 'fail'` — load_bearing rubber-stamps
|
|
225
|
+
* (misses defects below the retention floor) so the verdict must read FAIL.
|
|
226
|
+
*/
|
|
227
|
+
function mockReviews(items: CorpusItem[], direction: 'pass' | 'fail'): Reviews {
|
|
228
|
+
const hit = (i: CorpusItem): Finding[] => [
|
|
229
|
+
{
|
|
230
|
+
file: i.ground_truth.defect_files[0] ?? i.files[0]?.path ?? '',
|
|
231
|
+
category: i.defect_category,
|
|
232
|
+
confidence: 'high' as const,
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
const fpHit = (i: CorpusItem): Finding[] => [
|
|
236
|
+
{ file: i.files[0]?.path ?? '', category: 'correctness', confidence: 'high' as const },
|
|
237
|
+
];
|
|
238
|
+
const out: Reviews = {};
|
|
239
|
+
const defects = items.filter((i) => !i.is_clean);
|
|
240
|
+
const keepIds = new Set(
|
|
241
|
+
// In 'fail' direction the load_bearing arm catches only half the
|
|
242
|
+
// defects legacy catches — below the 0.8 retention floor.
|
|
243
|
+
defects.filter((_, idx) => idx % 2 === 0).map((d) => d.id),
|
|
244
|
+
);
|
|
245
|
+
for (const item of items) {
|
|
246
|
+
const legacyResp: ArmResponse = item.is_clean
|
|
247
|
+
? { findings: fpHit(item), verdict: null } // legacy FPs every control (the measured defect)
|
|
248
|
+
: { findings: hit(item), verdict: null };
|
|
249
|
+
const lbCatches = item.is_clean ? false : direction === 'pass' || keepIds.has(item.id);
|
|
250
|
+
const lbResp: ArmResponse = item.is_clean
|
|
251
|
+
? { findings: [], verdict: 'holds' }
|
|
252
|
+
: lbCatches
|
|
253
|
+
? { findings: hit(item), verdict: 'flawed' }
|
|
254
|
+
: { findings: [], verdict: 'holds' };
|
|
255
|
+
out[item.id] = {
|
|
256
|
+
legacy: { anthropic: legacyResp, openai: legacyResp },
|
|
257
|
+
load_bearing: { anthropic: lbResp, openai: lbResp },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface VendorArmScore {
|
|
264
|
+
tp: number;
|
|
265
|
+
fp: number;
|
|
266
|
+
fp_rate: number;
|
|
267
|
+
incoherent: number;
|
|
268
|
+
holds_on_clean: number;
|
|
269
|
+
caught_ids: string[];
|
|
270
|
+
fp_ids: string[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface VendorVerdict {
|
|
274
|
+
legacy: VendorArmScore;
|
|
275
|
+
load_bearing: VendorArmScore;
|
|
276
|
+
tp_retention: number | null;
|
|
277
|
+
fp_pass: boolean;
|
|
278
|
+
tp_pass: boolean;
|
|
279
|
+
promotes: boolean;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function scoreVendor(items: CorpusItem[], reviews: Reviews, vendor: Vendor): VendorVerdict {
|
|
283
|
+
const defects = items.filter((i) => !i.is_clean);
|
|
284
|
+
const controls = items.filter((i) => i.is_clean);
|
|
285
|
+
const armScore = (arm: Arm): VendorArmScore => {
|
|
286
|
+
const caught: string[] = [];
|
|
287
|
+
let incoherent = 0;
|
|
288
|
+
let holdsOnClean = 0;
|
|
289
|
+
for (const d of defects) {
|
|
290
|
+
const r = reviews[d.id]?.[arm]?.[vendor] ?? { findings: [], verdict: null };
|
|
291
|
+
if (caughtDefect(groundTruth(d), r.findings)) caught.push(d.id);
|
|
292
|
+
if (r.verdict === 'flawed' && r.findings.length === 0) incoherent++;
|
|
293
|
+
}
|
|
294
|
+
const fpIds: string[] = [];
|
|
295
|
+
for (const c of controls) {
|
|
296
|
+
const r = reviews[c.id]?.[arm]?.[vendor] ?? { findings: [], verdict: null };
|
|
297
|
+
if (isFalsePositive(groundTruth(c), r.findings)) fpIds.push(c.id);
|
|
298
|
+
if (r.verdict === 'flawed' && r.findings.length === 0) incoherent++;
|
|
299
|
+
if (r.verdict === 'holds') holdsOnClean++;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
tp: caught.length,
|
|
303
|
+
fp: fpIds.length,
|
|
304
|
+
fp_rate: controls.length ? fpIds.length / controls.length : 0,
|
|
305
|
+
incoherent,
|
|
306
|
+
holds_on_clean: holdsOnClean,
|
|
307
|
+
caught_ids: caught,
|
|
308
|
+
fp_ids: fpIds,
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
const legacy = armScore('legacy');
|
|
312
|
+
const load_bearing = armScore('load_bearing');
|
|
313
|
+
const tp_retention = legacy.tp > 0 ? load_bearing.tp / legacy.tp : null;
|
|
314
|
+
const fp_pass = load_bearing.fp_rate < FP_CEILING;
|
|
315
|
+
// A legacy TP of 0 leaves retention undefined. NOT part of the frozen
|
|
316
|
+
// registration (which is silent on a zero baseline) — a conservative
|
|
317
|
+
// in-code rule, unexercised in the recorded run (legacy TP was 10 and 5):
|
|
318
|
+
// retention unshowable against a zero baseline → tp_pass false.
|
|
319
|
+
const tp_pass = tp_retention !== null && tp_retention >= TP_RETENTION_FLOOR;
|
|
320
|
+
return { legacy, load_bearing, tp_retention, fp_pass, tp_pass, promotes: fp_pass && tp_pass };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildReport(items: CorpusItem[], reviews: Reviews, mode: string): Record<string, unknown> {
|
|
324
|
+
const perVendor: Record<string, VendorVerdict> = {};
|
|
325
|
+
for (const vendor of VENDORS) perVendor[vendor] = scoreVendor(items, reviews, vendor);
|
|
326
|
+
const promotes = VENDORS.every((v) => perVendor[v]?.promotes === true);
|
|
327
|
+
const detail: Record<string, unknown> = {};
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
const perItem: Record<string, unknown> = { is_clean: item.is_clean };
|
|
330
|
+
for (const arm of ARMS) {
|
|
331
|
+
for (const vendor of VENDORS) {
|
|
332
|
+
const r = reviews[item.id]?.[arm]?.[vendor];
|
|
333
|
+
perItem[`${arm}_${vendor}`] = r
|
|
334
|
+
? { findings: r.findings.length, verdict: r.verdict }
|
|
335
|
+
: null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
detail[item.id] = perItem;
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
mode,
|
|
342
|
+
corpus: 'internal/bench/adversarial-council/corpus.json',
|
|
343
|
+
claim: 'critic-protocol-load-bearing-ab (docs/CLAIMS.md, pre-registered 2026-08-09)',
|
|
344
|
+
protocol:
|
|
345
|
+
'both arms single-shot per vendor over all 15 items; FP on findings only, identical scorer both arms; verdict secondary; flawed+empty findings = incoherent',
|
|
346
|
+
thresholds: { fp_ceiling: FP_CEILING, tp_retention_floor: TP_RETENTION_FLOOR },
|
|
347
|
+
per_vendor: perVendor,
|
|
348
|
+
verdict: {
|
|
349
|
+
promotes,
|
|
350
|
+
direction: promotes ? 'both thresholds met per vendor' : 'at least one threshold missed',
|
|
351
|
+
default_unchanged: 'legacy stays the default regardless (config critic_protocol)',
|
|
352
|
+
},
|
|
353
|
+
detail,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function main(argv: string[]): Promise<number> {
|
|
358
|
+
const mock = argv.includes('--mock');
|
|
359
|
+
const run = argv.includes('--run');
|
|
360
|
+
const outIdx = argv.indexOf('--out');
|
|
361
|
+
const outFile = outIdx >= 0 ? argv[outIdx + 1] : null;
|
|
362
|
+
if (!mock && !run) {
|
|
363
|
+
process.stderr.write('usage: bench_critic_protocol (--mock | --run) [--out <file>]\n');
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
const items = loadCorpus();
|
|
367
|
+
if (mock) {
|
|
368
|
+
// Tripwire (roadmap pre-mortem): the pipeline must discriminate in BOTH
|
|
369
|
+
// directions before any paid call.
|
|
370
|
+
const passReport = buildReport(items, mockReviews(items, 'pass'), 'mock-pass');
|
|
371
|
+
const failReport = buildReport(items, mockReviews(items, 'fail'), 'mock-fail');
|
|
372
|
+
const passOk = (passReport.verdict as { promotes: boolean }).promotes === true;
|
|
373
|
+
const failOk = (failReport.verdict as { promotes: boolean }).promotes === false;
|
|
374
|
+
process.stdout.write(JSON.stringify({ mock_pass: passReport, mock_fail: failReport }, null, 2) + '\n');
|
|
375
|
+
if (!passOk || !failOk) {
|
|
376
|
+
process.stderr.write(
|
|
377
|
+
`mock discrimination FAILED: pass-direction promotes=${String(passOk)}, fail-direction rejects=${String(failOk)} — do NOT run paid calls.\n`,
|
|
378
|
+
);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
process.stderr.write('mock discrimination OK in both directions.\n');
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
const reviews = await realReviews(items);
|
|
385
|
+
const report = buildReport(items, reviews, 'run');
|
|
386
|
+
const json = JSON.stringify(report, null, 2) + '\n';
|
|
387
|
+
if (outFile) fs.writeFileSync(outFile, json);
|
|
388
|
+
process.stdout.write(json);
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
393
|
+
void main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export { parseArmResponse, scoreVendor, buildReport, mockReviews, legacyPrompt, loadBearingPrompt };
|