@event4u/agent-config 12.1.0 → 14.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +101 -164
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quorum seam between `quorum.ts` and the council CLI.
|
|
3
|
+
*
|
|
4
|
+
* `quorum.ts` is pure arithmetic: given n, present and a setting, it answers
|
|
5
|
+
* whether a pass concluded. Everything here is the plumbing around that answer
|
|
6
|
+
* — reading the setting out of a possibly hand-built config dict, re-deriving
|
|
7
|
+
* presence from a round of responses, emitting the attendance event, and
|
|
8
|
+
* rendering the operator-facing banner.
|
|
9
|
+
*
|
|
10
|
+
* **Extracted for the same reason its sibling `qualification_wiring.ts` was,
|
|
11
|
+
* and the reason is worth stating rather than implying.** Adding provider
|
|
12
|
+
* qualification to `council_cli.ts` pushed that file from 4,058 to 4,311
|
|
13
|
+
* lines, and `check_source_size_budget` scores `Σ max(0, lines(f) − 1500)` —
|
|
14
|
+
* so every added line in a file already 2,500 over the ceiling counts against
|
|
15
|
+
* the ratchet, while the new modules beside it count zero. Moving the
|
|
16
|
+
* qualification seam out recovered 164 of those lines; the residual 89 was
|
|
17
|
+
* call-site wiring that cannot leave `council_cli.ts`. This module is the
|
|
18
|
+
* operator's answer to that residual: pull the quorum plumbing — the same
|
|
19
|
+
* layer, and the code the qualification change already reaches into — into a
|
|
20
|
+
* file whose subject it actually is.
|
|
21
|
+
*
|
|
22
|
+
* That makes the extraction a real boundary rather than budget accounting. The
|
|
23
|
+
* discriminator: every function here is about a k-of-n verdict and its
|
|
24
|
+
* reporting, and none of them is about parsing argv, constructing clients, or
|
|
25
|
+
* writing an output payload — which is what `council_cli.ts` is for.
|
|
26
|
+
*
|
|
27
|
+
* `Dict` is redeclared locally rather than imported: it is `Record<string,
|
|
28
|
+
* unknown>` in the CLI too, and importing a type alias that shallow would
|
|
29
|
+
* couple this module to the file it was just separated from.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { CouncilResponse, ExternalAIClient } from './clients.js';
|
|
33
|
+
import type { QuorumSetting } from './config.js';
|
|
34
|
+
import {
|
|
35
|
+
appendQuorumEvent,
|
|
36
|
+
type QuorumAbsence,
|
|
37
|
+
type QuorumCommand,
|
|
38
|
+
type QuorumDispatch,
|
|
39
|
+
type QuorumEventPhase,
|
|
40
|
+
} from './events_log.js';
|
|
41
|
+
import { evaluateQuorum, SOLO_FLOOR_MIN_PRESENT, type QuorumResult } from './quorum.js';
|
|
42
|
+
import { absentReasonFromCliFailure, classifyCliFailure } from './transport_resolver.js';
|
|
43
|
+
|
|
44
|
+
type Dict = Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `ai['quorum']` as forwarded by `_synthesize_ai_council_block` (already
|
|
48
|
+
* validated by `config.ts::_build_quorum` on that path) — or, for a caller
|
|
49
|
+
* that hands `build_members` a hand-built dict bypassing the loader
|
|
50
|
+
* entirely, a defensive re-check that fails soft to `'majority'` rather
|
|
51
|
+
* than throwing. `build_members` is not the config-validation layer;
|
|
52
|
+
* `evaluateQuorum` degrades an unrecognisable setting toward
|
|
53
|
+
* `inconclusive`, never toward a false `concluded`, so failing soft here
|
|
54
|
+
* costs nothing on the safety side.
|
|
55
|
+
*/
|
|
56
|
+
export function _quorum_setting_from(ai: Dict): QuorumSetting {
|
|
57
|
+
const raw = ai['quorum'];
|
|
58
|
+
if (raw === 'majority') {
|
|
59
|
+
return 'majority';
|
|
60
|
+
}
|
|
61
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1) {
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
return 'majority';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `ai['quorum_min_present']` with the same fail-soft posture as
|
|
69
|
+
* `_quorum_setting_from`: the loader has already validated it on the normal
|
|
70
|
+
* path, and a hand-built dict that bypasses the loader falls back to the
|
|
71
|
+
* ADR-224 default rather than throwing.
|
|
72
|
+
*
|
|
73
|
+
* Failing soft costs nothing on the safety side here, and less than it does
|
|
74
|
+
* for `quorum`: this value feeds a counterfactual boolean that no gate reads,
|
|
75
|
+
* so the worst outcome of a wrong fallback is one mis-recorded telemetry line,
|
|
76
|
+
* never a pass that concluded when it should not have.
|
|
77
|
+
*/
|
|
78
|
+
export function _quorum_min_present_from(ai: Dict): number {
|
|
79
|
+
const raw = ai['quorum_min_present'];
|
|
80
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1) {
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
return SOLO_FLOOR_MIN_PRESENT;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Emit one `quorum_result` line for a resolved quorum.
|
|
88
|
+
*
|
|
89
|
+
* Both `evaluateQuorum` call sites route through here, tagged by `phase`, so
|
|
90
|
+
* a solo-concluded pass is distinguishable from a full-attendance one in the
|
|
91
|
+
* event log — the gap `road-to-always-on-orchestration`'s Risk 6 asserted was
|
|
92
|
+
* already closed and was not.
|
|
93
|
+
*
|
|
94
|
+
* The emit sits at the call sites rather than inside `_postRunQuorum` on
|
|
95
|
+
* purpose: that function is exported and unit-tested as a pure derivation,
|
|
96
|
+
* and a write buried in it would make every test of it a log writer.
|
|
97
|
+
*
|
|
98
|
+
* `absent` entries arrive as the CLI's own `{member, reason, detail}` dicts;
|
|
99
|
+
* `detail` is dropped by `appendQuorumEvent`'s input type, which cannot carry
|
|
100
|
+
* free-form text at all.
|
|
101
|
+
*/
|
|
102
|
+
export function _emitQuorumEvent(
|
|
103
|
+
phase: QuorumEventPhase,
|
|
104
|
+
quorum: QuorumResult,
|
|
105
|
+
absent: readonly Dict[],
|
|
106
|
+
ctx: {
|
|
107
|
+
command: QuorumCommand;
|
|
108
|
+
dispatch?: QuorumDispatch;
|
|
109
|
+
/** Enabled members before `--single` filtering; defaults to the roster that ran. */
|
|
110
|
+
configuredTotal?: number;
|
|
111
|
+
lens?: string;
|
|
112
|
+
invocation?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The ADR-224 shadow floor for this pass. `gateClass` is deliberately
|
|
115
|
+
* NOT in this context object: no CLI path declares itself gate-class,
|
|
116
|
+
* so a parameter here would be one no caller ever sets, and its
|
|
117
|
+
* absence is what the `false` on every line honestly records.
|
|
118
|
+
*/
|
|
119
|
+
minPresent?: number;
|
|
120
|
+
},
|
|
121
|
+
): void {
|
|
122
|
+
appendQuorumEvent({
|
|
123
|
+
lens: ctx.lens ?? '',
|
|
124
|
+
invocation: ctx.invocation ?? '',
|
|
125
|
+
phase,
|
|
126
|
+
command: ctx.command,
|
|
127
|
+
dispatch: ctx.dispatch ?? 'full',
|
|
128
|
+
configuredTotal: ctx.configuredTotal ?? quorum.total,
|
|
129
|
+
result: quorum,
|
|
130
|
+
...(ctx.minPresent !== undefined ? { minPresent: ctx.minPresent } : {}),
|
|
131
|
+
absent: absent.map(
|
|
132
|
+
(a): QuorumAbsence => ({
|
|
133
|
+
member: String(a['member'] ?? ''),
|
|
134
|
+
reason: (a['reason'] as QuorumAbsence['reason']) ?? 'unavailable',
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Re-derive quorum AFTER the provider calls, over what was actually USABLE —
|
|
142
|
+
* never what merely CONSTRUCTED (M3, independent-review finding). `members`
|
|
143
|
+
* and `responses` must be index-aligned (`consult()`'s own contract: one
|
|
144
|
+
* `CouncilResponse` per member, from the final round); `members.length` is
|
|
145
|
+
* the `n` — deliberately the roster that actually ran (post `--single` /
|
|
146
|
+
* `--siblings` filtering), never the full config roster a filtered run
|
|
147
|
+
* never attempted. A response carrying `.error` (or a missing response
|
|
148
|
+
* entry altogether) counts as absent, classified through the same
|
|
149
|
+
* `AbsentReason` vocabulary a static (pre-call) resolution failure uses, so
|
|
150
|
+
* one artefact buckets every absence — construction-time and mid-flight —
|
|
151
|
+
* under one taxonomy.
|
|
152
|
+
*/
|
|
153
|
+
export function _postRunQuorum(
|
|
154
|
+
members: ExternalAIClient[],
|
|
155
|
+
responses: CouncilResponse[],
|
|
156
|
+
ai_cfg: Dict,
|
|
157
|
+
): { quorum: QuorumResult; absent: Dict[] } {
|
|
158
|
+
const absent: Dict[] = [];
|
|
159
|
+
let present = 0;
|
|
160
|
+
for (let i = 0; i < members.length; i++) {
|
|
161
|
+
const m = members[i] as ExternalAIClient;
|
|
162
|
+
const r = responses[i];
|
|
163
|
+
// Round 7 § 5.2 — attendance is a NON-EMPTY answer, not the absence of an
|
|
164
|
+
// error. `!r.error` alone counted a member that returned zero bytes, and
|
|
165
|
+
// that is not hypothetical: a 290 s curl timeout returned an empty body
|
|
166
|
+
// with no error set in two sessions (`9fc9ba3e`, `4ac2f7ac`) and the
|
|
167
|
+
// banner printed `2/2 present` — a single-voice verdict presented as
|
|
168
|
+
// convergence, on a paid run. An empty answer contributes nothing to a
|
|
169
|
+
// quorum by definition, whatever the transport thought of it.
|
|
170
|
+
if (r !== undefined && !r.error && r.text.trim() !== '') {
|
|
171
|
+
present += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const raw = r?.error ?? (r !== undefined && r.text.trim() === '' ? 'empty response body' : 'no response');
|
|
175
|
+
const failure = classifyCliFailure(raw);
|
|
176
|
+
absent.push({
|
|
177
|
+
member: m.name,
|
|
178
|
+
reason: absentReasonFromCliFailure(failure) ?? 'unavailable',
|
|
179
|
+
detail: raw,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return { quorum: evaluateQuorum(members.length, present, _quorum_setting_from(ai_cfg)), absent };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* One-line k-of-n banner, mirrored from `orchestrator.ts::_render_quorum_line`
|
|
187
|
+
* but for the CLI's own stdout stream rather than the rendered report —
|
|
188
|
+
* "attendance is telemetry, never a silent drop" applies to the estimate
|
|
189
|
+
* preview too, not only to a completed pass.
|
|
190
|
+
*/
|
|
191
|
+
export function _format_quorum_line(q: QuorumResult, phase?: QuorumEventPhase): string {
|
|
192
|
+
// The phase tag is not decoration. A degraded run prints attendance TWICE —
|
|
193
|
+
// the pre-run reading before the estimate table, the post-run reading after
|
|
194
|
+
// the consult — and the two disagree by construction: `2/2 present …
|
|
195
|
+
// concluded` then `0/2 present … INCONCLUSIVE`. Same prefix, opposite
|
|
196
|
+
// content, and until 2026-08-12 neither line said which was which, so an
|
|
197
|
+
// operator skimming (or anything grepping `council:quorum ·`) could take the
|
|
198
|
+
// stale one. The event log had solved this from the start with an explicit
|
|
199
|
+
// `phase` field; stdout had not. Optional, because the estimate path prints
|
|
200
|
+
// exactly one line and a tag there would claim a distinction it does not make.
|
|
201
|
+
const tag = phase === undefined ? '' : `${phase === 'pre_run' ? 'before the run' : 'after the run'} · `;
|
|
202
|
+
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
203
|
+
// Round 7 § 5.3 — a degraded pass says so. `1/2 present, needed 1 —
|
|
204
|
+
// concluded` is literally true and reads as agreement; with a threshold of 1
|
|
205
|
+
// a solo answer concludes, and nothing in the line distinguishes "both
|
|
206
|
+
// members agreed" from "one member answered". The counts were always there;
|
|
207
|
+
// what was missing is the word that stops a reader inferring convergence.
|
|
208
|
+
const degraded =
|
|
209
|
+
q.present < q.total ? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.` : '';
|
|
210
|
+
return `council:quorum · ${tag}${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
|
|
211
|
+
}
|
|
@@ -43,9 +43,11 @@ import * as path from 'node:path';
|
|
|
43
43
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
44
44
|
|
|
45
45
|
import {
|
|
46
|
+
CLI_CONSUMER_TEAM,
|
|
46
47
|
type CouncilResponse,
|
|
47
48
|
load_cli_call_counts,
|
|
48
49
|
OpenAICliClient,
|
|
50
|
+
QUOTA_SOURCE_PROVIDER,
|
|
49
51
|
} from '../ai_council/clients.js';
|
|
50
52
|
import {
|
|
51
53
|
checkCodexAvailability,
|
|
@@ -361,7 +363,9 @@ export interface TeamReviewFinding {
|
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
export interface TeamReviewQuota {
|
|
364
|
-
/**
|
|
366
|
+
/** The shared-bucket key this run books into — read from the client, not assumed. */
|
|
367
|
+
readonly provider: string;
|
|
368
|
+
/** Today's call count in that bucket AFTER this run. */
|
|
365
369
|
readonly used: number;
|
|
366
370
|
/** `ai_team.max_calls_per_day` — the team-side ceiling on the shared bucket. */
|
|
367
371
|
readonly ceiling: number;
|
|
@@ -549,9 +553,23 @@ export interface RunTeamReviewOptions {
|
|
|
549
553
|
halted?: boolean;
|
|
550
554
|
}
|
|
551
555
|
|
|
552
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Read the bucket this run actually books into.
|
|
558
|
+
*
|
|
559
|
+
* `provider` comes from the CLIENT (`client.name`), never from a literal. It was
|
|
560
|
+
* hardcoded to `'openai'`, which happened to be correct because
|
|
561
|
+
* `TeamReviewCliClient extends OpenAICliClient` — and would have gone silently
|
|
562
|
+
* wrong the moment the team transport moved, reporting a bucket nobody was
|
|
563
|
+
* booking while the real one filled up. A report that names its own subject
|
|
564
|
+
* cannot drift away from it.
|
|
565
|
+
*/
|
|
566
|
+
function _quota_snapshot(
|
|
567
|
+
config: AiTeamConfig,
|
|
568
|
+
cli_calls_path: string | null,
|
|
569
|
+
provider: string,
|
|
570
|
+
): TeamReviewQuota {
|
|
553
571
|
const counts = load_cli_call_counts(cli_calls_path);
|
|
554
|
-
return { used: counts[
|
|
572
|
+
return { provider, used: counts[provider] ?? 0, ceiling: config.max_calls_per_day };
|
|
555
573
|
}
|
|
556
574
|
|
|
557
575
|
function _envelope_from_response(
|
|
@@ -571,9 +589,19 @@ function _envelope_from_response(
|
|
|
571
589
|
'codex CLI auth failed (auth_expired) — run `codex login`, then retry. ' +
|
|
572
590
|
'No review was produced.';
|
|
573
591
|
} else if (response.error === 'cli_quota_exhausted') {
|
|
592
|
+
// `cli_quota_exhausted` covers two events with opposite remedies, so
|
|
593
|
+
// the message must not assert one of them unconditionally. "Resets at
|
|
594
|
+
// UTC midnight" is true only of OUR counter; when the vendor refused
|
|
595
|
+
// us, their window governs and we do not know it.
|
|
574
596
|
blocking_reason =
|
|
575
|
-
|
|
576
|
-
|
|
597
|
+
response.metadata?.['quota_source'] === QUOTA_SOURCE_PROVIDER
|
|
598
|
+
? `the ${quota.provider} CLI refused the call as over its own quota ` +
|
|
599
|
+
`(local bucket ${quota.used}/${quota.ceiling}) — the provider's window ` +
|
|
600
|
+
'governs, not ours. No review was produced.'
|
|
601
|
+
: `daily cli-call quota exhausted for the shared ${quota.provider} bucket ` +
|
|
602
|
+
`(${quota.used}/${quota.ceiling}) — resets at UTC midnight, or clear it with ` +
|
|
603
|
+
'`agent-config council:quota --reset <provider> --confirm`. ' +
|
|
604
|
+
'No review was produced.';
|
|
577
605
|
} else {
|
|
578
606
|
blocking_reason = `codex CLI transport failed: ${response.error}. No review was produced.`;
|
|
579
607
|
}
|
|
@@ -649,10 +677,16 @@ export function run_team_review(opts: RunTeamReviewOptions = {}): TeamReviewRunR
|
|
|
649
677
|
model: config.model,
|
|
650
678
|
max_calls_per_day: config.max_calls_per_day,
|
|
651
679
|
cli_calls_path,
|
|
680
|
+
// The team half of the two enumerated consumers of the shared
|
|
681
|
+
// counter. Declared here because the client cannot know its
|
|
682
|
+
// caller — the bucket standing at 171 calls with zero recorded
|
|
683
|
+
// council exchanges is precisely what an unattributed counter
|
|
684
|
+
// cannot explain.
|
|
685
|
+
consumer: CLI_CONSUMER_TEAM,
|
|
652
686
|
});
|
|
653
687
|
|
|
654
688
|
const response = client.ask(TEAM_REVIEW_SYSTEM_PROMPT, render_review_user_prompt(bundle));
|
|
655
|
-
const quota = _quota_snapshot(config, cli_calls_path);
|
|
689
|
+
const quota = _quota_snapshot(config, cli_calls_path, client.name);
|
|
656
690
|
const envelope = _envelope_from_response(response, bundle, quota);
|
|
657
691
|
out(JSON.stringify(envelope, null, 2));
|
|
658
692
|
return { mode: 'call', header, bundle, envelope };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Offline re-scorer for the Phase-3 metric pair — S0.3 delta #11.
|
|
4
|
+
*
|
|
5
|
+
* Reads a finished `ab-v2` report, walks every trial's **preserved workspace**
|
|
6
|
+
* (delta #7 keyed `task|arm|seed`), and writes two endpoints into each trial's
|
|
7
|
+
* `metrics`:
|
|
8
|
+
*
|
|
9
|
+
* - `added_lines` — T1's endpoint. Added only, not added+removed: the
|
|
10
|
+
* discipline oracle's `max_lines_changed` wants the sum, the size claim
|
|
11
|
+
* wants what the run *wrote*.
|
|
12
|
+
* - `median_cognitive_complexity` — T2's endpoint, over the functions in the
|
|
13
|
+
* files that trial actually changed.
|
|
14
|
+
*
|
|
15
|
+
* WHY OFFLINE RATHER THAN IN THE RUNNER. Three reasons, in order of weight.
|
|
16
|
+
* The metric is **retro-fittable**: the workspaces from every completed sweep
|
|
17
|
+
* are on disk, so a run that already cost money can gain the endpoint without
|
|
18
|
+
* being re-run — which is what makes the anti-golfing gate nearly free, and what
|
|
19
|
+
* the roadmap's own text promises. It is **deterministic and model-free**, so
|
|
20
|
+
* re-scoring the same workspace twice cannot produce two numbers. And the
|
|
21
|
+
* grammar load is **async** while `trajectory_metrics` is synchronous and called
|
|
22
|
+
* from four places — threading async through the live path would be a larger
|
|
23
|
+
* diff than the endpoint itself, for no measurement it does not already get.
|
|
24
|
+
*
|
|
25
|
+
* The report is rewritten in place only with `--write`; the default prints the
|
|
26
|
+
* table and touches nothing, because silently rewriting a pinned artefact is how
|
|
27
|
+
* a report stops matching the numbers already quoted from it.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* bench_ab_v2_complexity.ts <report.json> [--write] [--json]
|
|
31
|
+
*/
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { parse as parseYaml } from 'yaml';
|
|
37
|
+
|
|
38
|
+
import { medianComplexityPerChangedFunction } from './_lib/bench_ab_complexity.js';
|
|
39
|
+
import { changed_files, diff_line_counts } from './_lib/bench_ab_scoring_v2.js';
|
|
40
|
+
|
|
41
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
42
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
43
|
+
const FIXTURES_ROOT = path.join(REPO_ROOT, 'internal', 'bench', 'ab');
|
|
44
|
+
const CORPUS_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'corpora', 'ab-trackb-v2.yaml');
|
|
45
|
+
|
|
46
|
+
type Dict = Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
function _dictOr(v: unknown): Dict {
|
|
49
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Dict) : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* task id → fixture path. The corpus is the ONLY resolver, deliberately.
|
|
54
|
+
*
|
|
55
|
+
* An earlier draft also had the runner stamp a `fixture` key onto each record so
|
|
56
|
+
* reports would be self-describing, and this function read it first. That is two
|
|
57
|
+
* mechanisms for one fact: the corpus lookup already covers every report,
|
|
58
|
+
* including every one written before the key would have existed. The extra field
|
|
59
|
+
* was dropped rather than kept as a fallback — a reader with no producer is the
|
|
60
|
+
* shape this branch's own completion review flagged elsewhere.
|
|
61
|
+
*
|
|
62
|
+
* A missing or unreadable corpus is not an error: it degrades every affected
|
|
63
|
+
* trial to `skipped_reason: 'task id not in the corpus'`, which is a reportable
|
|
64
|
+
* state, rather than aborting a re-score that may still cover most of the report.
|
|
65
|
+
*/
|
|
66
|
+
function loadCorpusFixtures(corpusPath: string | null): Map<string, string> {
|
|
67
|
+
const byId = new Map<string, string>();
|
|
68
|
+
if (!corpusPath || !fs.existsSync(corpusPath)) return byId;
|
|
69
|
+
try {
|
|
70
|
+
const corpus = _dictOr(parseYaml(fs.readFileSync(corpusPath, 'utf8'), { version: '1.1' }));
|
|
71
|
+
const tasks = Array.isArray(corpus['tasks']) ? (corpus['tasks'] as Dict[]) : [];
|
|
72
|
+
for (const t of tasks) {
|
|
73
|
+
if (t['id'] !== undefined && t['fixture'] !== undefined) {
|
|
74
|
+
byId.set(String(t['id']), String(t['fixture']));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
return byId;
|
|
79
|
+
}
|
|
80
|
+
return byId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TrialRescore {
|
|
84
|
+
task: string;
|
|
85
|
+
arm: string;
|
|
86
|
+
seed: unknown;
|
|
87
|
+
/** `null` when the workspace or the fixture is no longer on disk. */
|
|
88
|
+
added_lines: number | null;
|
|
89
|
+
median_cognitive_complexity: number | null;
|
|
90
|
+
n_functions: number;
|
|
91
|
+
unsupported_files: string[];
|
|
92
|
+
skipped_reason: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Re-score one report payload. Pure apart from the filesystem reads it needs;
|
|
97
|
+
* mutates `payload` only when `write` is true. The fixture each trial is diffed
|
|
98
|
+
* against is resolved from the corpus by task id — see `loadCorpusFixtures`.
|
|
99
|
+
*/
|
|
100
|
+
export async function rescoreReport(
|
|
101
|
+
payload: Dict,
|
|
102
|
+
opts: { fixturesRoot?: string; write?: boolean; corpusPath?: string | null } = {},
|
|
103
|
+
): Promise<TrialRescore[]> {
|
|
104
|
+
const fixturesRoot = opts.fixturesRoot ?? FIXTURES_ROOT;
|
|
105
|
+
let wrote = 0;
|
|
106
|
+
const byId = loadCorpusFixtures(opts.corpusPath === undefined ? CORPUS_PATH : opts.corpusPath);
|
|
107
|
+
const out: TrialRescore[] = [];
|
|
108
|
+
const records = Array.isArray(payload['records']) ? (payload['records'] as Dict[]) : [];
|
|
109
|
+
|
|
110
|
+
for (const rec of records) {
|
|
111
|
+
const taskId = String(rec['id'] ?? '<unknown>');
|
|
112
|
+
const fixtureRel = byId.get(taskId) ?? null;
|
|
113
|
+
const arms = _dictOr(rec['arms']);
|
|
114
|
+
for (const [arm, runsRaw] of Object.entries(arms)) {
|
|
115
|
+
const runs = Array.isArray(runsRaw) ? (runsRaw as Dict[]) : [];
|
|
116
|
+
for (const run of runs) {
|
|
117
|
+
const row: TrialRescore = {
|
|
118
|
+
task: taskId,
|
|
119
|
+
arm,
|
|
120
|
+
seed: run['seed'] ?? null,
|
|
121
|
+
added_lines: null,
|
|
122
|
+
median_cognitive_complexity: null,
|
|
123
|
+
n_functions: 0,
|
|
124
|
+
unsupported_files: [],
|
|
125
|
+
skipped_reason: null,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const workspace = run['workspace'] !== undefined ? String(run['workspace']) : '';
|
|
129
|
+
if (!workspace || !fs.existsSync(workspace)) {
|
|
130
|
+
// Not a zero — a trial whose workspace was pruned is
|
|
131
|
+
// unmeasurable, and `compare()` reads a missing metric as
|
|
132
|
+
// "not measured on this pair" rather than as a value.
|
|
133
|
+
row.skipped_reason = workspace ? 'workspace missing on disk' : 'no workspace recorded';
|
|
134
|
+
out.push(row);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (fixtureRel === null) {
|
|
138
|
+
row.skipped_reason = 'task id not in the corpus';
|
|
139
|
+
out.push(row);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const fixture = path.join(fixturesRoot, fixtureRel);
|
|
143
|
+
if (!fs.existsSync(fixture)) {
|
|
144
|
+
row.skipped_reason = 'fixture missing on disk';
|
|
145
|
+
out.push(row);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const changed = changed_files(fixture, workspace);
|
|
150
|
+
row.added_lines = diff_line_counts(fixture, workspace, changed).added;
|
|
151
|
+
const rollup = await medianComplexityPerChangedFunction(workspace, changed);
|
|
152
|
+
row.median_cognitive_complexity = rollup.median;
|
|
153
|
+
row.n_functions = rollup.n_functions;
|
|
154
|
+
row.unsupported_files = rollup.unsupported_files;
|
|
155
|
+
|
|
156
|
+
if (opts.write) {
|
|
157
|
+
const metrics = _dictOr(run['metrics']);
|
|
158
|
+
metrics['added_lines'] = row.added_lines;
|
|
159
|
+
// `median` is null when the diff contained no scorable
|
|
160
|
+
// function, and null is written on purpose: `compare()` reads
|
|
161
|
+
// a null as "not measured on this pair", while a 0 would claim
|
|
162
|
+
// the run produced maximally simple code. The count sits
|
|
163
|
+
// beside it so a reader can tell "nothing to score" from
|
|
164
|
+
// "scored, and it was trivial".
|
|
165
|
+
metrics['median_cognitive_complexity'] = row.median_cognitive_complexity;
|
|
166
|
+
metrics['complexity_n_functions'] = row.n_functions;
|
|
167
|
+
run['metrics'] = metrics;
|
|
168
|
+
wrote += 1;
|
|
169
|
+
}
|
|
170
|
+
out.push(row);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (opts.write) lastWriteCount = wrote;
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How many trials the last `rescoreReport({ write: true })` actually touched.
|
|
180
|
+
*
|
|
181
|
+
* The CLI uses it to skip rewriting the report when nothing was measured:
|
|
182
|
+
* re-serialising a pinned artefact for a zero-row pass changes its bytes (and
|
|
183
|
+
* its formatting) without changing a single number, which is exactly the kind of
|
|
184
|
+
* silent churn that makes a report stop matching the figures quoted from it.
|
|
185
|
+
*/
|
|
186
|
+
let lastWriteCount = 0;
|
|
187
|
+
export function trialsWrittenByLastRescore(): number {
|
|
188
|
+
return lastWriteCount;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderTable(rows: TrialRescore[]): string {
|
|
192
|
+
const L: string[] = [];
|
|
193
|
+
L.push('task | arm | seed | added | median cc | fns | note');
|
|
194
|
+
L.push('-----|-----|------|-------|-----------|-----|-----');
|
|
195
|
+
for (const r of rows) {
|
|
196
|
+
L.push(
|
|
197
|
+
[
|
|
198
|
+
r.task,
|
|
199
|
+
r.arm,
|
|
200
|
+
String(r.seed ?? ''),
|
|
201
|
+
r.added_lines === null ? '-' : String(r.added_lines),
|
|
202
|
+
r.median_cognitive_complexity === null ? '-' : String(r.median_cognitive_complexity),
|
|
203
|
+
String(r.n_functions),
|
|
204
|
+
r.skipped_reason ??
|
|
205
|
+
(r.unsupported_files.length > 0
|
|
206
|
+
? `unsupported: ${r.unsupported_files.join(',')}`
|
|
207
|
+
: ''),
|
|
208
|
+
].join(' | '),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const measured = rows.filter((r) => r.median_cognitive_complexity !== null).length;
|
|
212
|
+
L.push('');
|
|
213
|
+
L.push(`${measured}/${rows.length} trials carry a complexity observation.`);
|
|
214
|
+
return L.join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main(argv: string[]): Promise<number> {
|
|
218
|
+
const args = argv.filter((a) => !a.startsWith('--'));
|
|
219
|
+
const write = argv.includes('--write');
|
|
220
|
+
const asJson = argv.includes('--json');
|
|
221
|
+
const reportPath = args[0];
|
|
222
|
+
if (!reportPath) {
|
|
223
|
+
process.stderr.write(
|
|
224
|
+
'usage: bench_ab_v2_complexity.ts <report.json> [--write] [--json]\n',
|
|
225
|
+
);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
if (!fs.existsSync(reportPath)) {
|
|
229
|
+
process.stderr.write(`report not found: ${reportPath}\n`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
const payload = JSON.parse(fs.readFileSync(reportPath, 'utf8')) as Dict;
|
|
233
|
+
const rows = await rescoreReport(payload, { write });
|
|
234
|
+
const written = trialsWrittenByLastRescore();
|
|
235
|
+
if (write && written > 0) {
|
|
236
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
237
|
+
} else if (write) {
|
|
238
|
+
process.stderr.write('no trial could be measured — report left untouched\n');
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(asJson ? `${JSON.stringify(rows, null, 2)}\n` : `${renderTable(rows)}\n`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_HERE)) {
|
|
245
|
+
main(process.argv.slice(2)).then(
|
|
246
|
+
(code) => process.exit(code),
|
|
247
|
+
(err: unknown) => {
|
|
248
|
+
process.stderr.write(`${String(err)}\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
import { spawnSync } from 'node:child_process';
|
|
46
46
|
import { createHash } from 'node:crypto';
|
|
47
47
|
import * as fs from 'node:fs';
|
|
48
|
-
import * as os from 'node:os';
|
|
49
48
|
import * as path from 'node:path';
|
|
50
49
|
import process from 'node:process';
|
|
51
50
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -53,6 +52,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
53
52
|
|
|
54
53
|
import * as v1 from './bench_ab_task_runner.js';
|
|
55
54
|
import * as scoring from './_lib/bench_ab_scoring_v2.js';
|
|
55
|
+
import { preserve_transcript, reset_fixture } from './_lib/bench_ab_workspace.js';
|
|
56
56
|
import {
|
|
57
57
|
SweepBudget,
|
|
58
58
|
activation_verdict,
|
|
@@ -75,7 +75,6 @@ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
|
75
75
|
const ROUTER_PATH = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
76
76
|
const RULES_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
|
|
77
77
|
const CORPUS_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'corpora', 'ab-trackb-v2.yaml');
|
|
78
|
-
const FIXTURES_ROOT = path.join(REPO_ROOT, 'internal', 'bench', 'ab');
|
|
79
78
|
const REPORTS_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'reports', 'ab-v2');
|
|
80
79
|
const CHECKPOINTS_DIR = path.join(REPORTS_DIR, 'checkpoints');
|
|
81
80
|
const PRICING_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'pricing.yaml');
|
|
@@ -87,7 +86,6 @@ const PRICING_PATH = path.join(REPO_ROOT, 'internal', 'bench', 'pricing.yaml');
|
|
|
87
86
|
// vanilla ≈ package and invalidated every prior null. A /tmp clone has no
|
|
88
87
|
// agent-config ancestor → vanilla is truly plain; `package` still activates via
|
|
89
88
|
// the USER-scope global plugin regardless of cwd.
|
|
90
|
-
const WORK_ROOT = path.join(os.tmpdir(), 'agent-config-bench-v2-clones');
|
|
91
89
|
|
|
92
90
|
type Dict = Record<string, unknown>;
|
|
93
91
|
|
|
@@ -329,45 +327,6 @@ export function injected_text(inject: string | null, placebo_chars: number): str
|
|
|
329
327
|
return null;
|
|
330
328
|
}
|
|
331
329
|
|
|
332
|
-
/**
|
|
333
|
-
* Workspace directory for one trial — delta #7 of the S0.3 spike.
|
|
334
|
-
*
|
|
335
|
-
* Keyed by `task|arm|seed`, not by task alone. The old task-only key meant every
|
|
336
|
-
* arm and every seed of a task reused ONE directory that the next trial deleted,
|
|
337
|
-
* so at the end of a sweep exactly one workspace survived per task — the last one
|
|
338
|
-
* written, with no record of which arm or seed it belonged to.
|
|
339
|
-
*
|
|
340
|
-
* That is not a tidiness problem. Phase 3's anti-golfing gate is specified as
|
|
341
|
-
* retro-fittable onto already-completed runs by offline re-scoring, and the
|
|
342
|
-
* roadmap calls that gate cheap *because* the workspaces are preserved. Under the
|
|
343
|
-
* old key there was nothing to re-score, so the claim was false. A distinct
|
|
344
|
-
* directory per trial is what makes it true.
|
|
345
|
-
*
|
|
346
|
-
* `arm` is sanitised because arm names are used verbatim as a path segment and
|
|
347
|
-
* one of them would otherwise be free to escape the root.
|
|
348
|
-
*/
|
|
349
|
-
export function workspace_dir(task_id: string, arm: string, seed: number): string {
|
|
350
|
-
const safe_arm = arm.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
351
|
-
return path.join(WORK_ROOT, `${task_id}__${safe_arm}__seed${seed}`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Copy the task's pristine fixture into this trial's own working clone.
|
|
356
|
-
*
|
|
357
|
-
* The clone is re-created from the fixture on every call, so a resumed or
|
|
358
|
-
* repeated trial still starts pristine — the per-trial key changes *which*
|
|
359
|
-
* directory that is, never whether it is clean.
|
|
360
|
-
*/
|
|
361
|
-
export function reset_fixture(task: Dict, arm: string, seed: number): [string, string] {
|
|
362
|
-
const fixture = path.join(FIXTURES_ROOT, String(task['fixture']));
|
|
363
|
-
const dest = workspace_dir(String(task['id']), arm, seed);
|
|
364
|
-
if (fs.existsSync(dest)) {
|
|
365
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
366
|
-
}
|
|
367
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
368
|
-
fs.cpSync(fixture, dest, { recursive: true });
|
|
369
|
-
return [dest, fixture];
|
|
370
|
-
}
|
|
371
330
|
|
|
372
331
|
/** Map a run outcome to an AgentBench-style trajectory bucket. */
|
|
373
332
|
export function status_bucket(run: Dict): string {
|
|
@@ -689,6 +648,7 @@ export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
|
|
|
689
648
|
// rather than recomputed later, so an offline re-scorer reads the path the
|
|
690
649
|
// sweep actually used instead of re-deriving a key that may have changed.
|
|
691
650
|
workspace: clone,
|
|
651
|
+
transcript_path: preserve_transcript(clone, String(run['transcript'] ?? '')),
|
|
692
652
|
...integrity_fields(run, spec, sp_text ? sp_text.length : 0, opts.model),
|
|
693
653
|
};
|
|
694
654
|
}
|
|
@@ -803,6 +763,7 @@ export function selftest_run(
|
|
|
803
763
|
metrics: trajectory_metrics(run, score),
|
|
804
764
|
injected_chars: sp_text ? sp_text.length : 0,
|
|
805
765
|
workspace: clone,
|
|
766
|
+
// No transcript_path — a synthetic transcript would give T5 a score of the harness.
|
|
806
767
|
...integrity_fields(run, spec, sp_text ? sp_text.length : 0, opts.model),
|
|
807
768
|
};
|
|
808
769
|
}
|