@event4u/agent-config 12.0.0 → 13.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 +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- 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/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- 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/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- 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/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- 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 +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +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 +44 -23
- 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 +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/MIGRATION.md +20 -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/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -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/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/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 +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -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/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -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/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
|
@@ -81,6 +81,7 @@ import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
|
81
81
|
// `import type` and is erased at build, so there is no runtime cycle today.
|
|
82
82
|
// Converting it to a value import — or adding any other runtime import from
|
|
83
83
|
// quorum into config — creates a real ESM cycle in the loader's init path.
|
|
84
|
+
import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
|
|
84
85
|
import { SOLO_FLOOR_MIN_PRESENT } from './quorum.js';
|
|
85
86
|
|
|
86
87
|
const _VALID_PROVIDERS: ReadonlySet<string> = new Set([
|
|
@@ -1843,7 +1844,21 @@ function _build_member(
|
|
|
1843
1844
|
}
|
|
1844
1845
|
entries.push(entry);
|
|
1845
1846
|
}
|
|
1846
|
-
|
|
1847
|
+
// `codex-default` is a SENTINEL, not a model id — "let the transport
|
|
1848
|
+
// choose". Requiring it to appear on a downgrade ladder of concrete ids
|
|
1849
|
+
// is a category error, and it is the check that blocks the one config
|
|
1850
|
+
// that actually works on a subscription account: the shipped template
|
|
1851
|
+
// cannot pin an id (every id this package shipped is on
|
|
1852
|
+
// `CODEX_MEASURED_UNSERVABLE`), and its ladder is an api-transport
|
|
1853
|
+
// concern the CLI's own default cannot be ordered against. Exempting
|
|
1854
|
+
// the sentinel is narrower than dropping the check — a real pin is
|
|
1855
|
+
// still required to be on its own ladder.
|
|
1856
|
+
if (
|
|
1857
|
+
member_enabled &&
|
|
1858
|
+
model &&
|
|
1859
|
+
model !== OPENAI_CLI_VENDOR_DEFAULT &&
|
|
1860
|
+
!entries.includes(model)
|
|
1861
|
+
) {
|
|
1847
1862
|
throw new CouncilConfigError(
|
|
1848
1863
|
`members.${name}.model_ladder must include the active ` +
|
|
1849
1864
|
`\`model\` (${_pyReprStr(model)}); got ${_pyRepr(entries)}.`,
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// - dict insertion order + `dict(sorted(...))` for the stats aggregation.
|
|
36
36
|
|
|
37
37
|
import { pyRound } from '../_lib/value_ladder.js';
|
|
38
|
+
import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
|
|
38
39
|
import type { CouncilResponse, ExternalAIClient } from './clients.js';
|
|
39
40
|
import type { LowImpactFastPathConfig, MemberConfig } from './config.js';
|
|
40
41
|
import type { ImpactVerdict } from './necessity.js';
|
|
@@ -630,7 +631,19 @@ export function resolve_low_impact(
|
|
|
630
631
|
const pinned = model_downgrade.model_tier_override?.[member.name];
|
|
631
632
|
const cheapest = member.model_ladder.length > 0 ? member.model_ladder[0] : null;
|
|
632
633
|
const target = pinned && pinned.length > 0 ? pinned : cheapest;
|
|
633
|
-
|
|
634
|
+
// A member left on the vendor-default sentinel is NOT downgradable
|
|
635
|
+
// off the ladder. There is nothing to downgrade FROM — the
|
|
636
|
+
// transport picks the model — and writing a rung onto it re-creates
|
|
637
|
+
// the pinned-id shape the seat repair removed. Concretely: the
|
|
638
|
+
// shipped ladder's cheapest rung is `gpt-4o-mini` and its
|
|
639
|
+
// neighbours are `gpt-4o` / `gpt-4.1`, so this line could hand the
|
|
640
|
+
// codex transport an id `CODEX_MEASURED_UNSERVABLE` already records
|
|
641
|
+
// as refused — on a path whose `model_downgrade.enabled` defaults
|
|
642
|
+
// to true. An explicit per-run `model_tier_override` still wins,
|
|
643
|
+
// because that is a human naming a model for this run.
|
|
644
|
+
const unpinned = client.model === OPENAI_CLI_VENDOR_DEFAULT;
|
|
645
|
+
const overrideWins = pinned !== undefined && pinned.length > 0;
|
|
646
|
+
if (target && target !== client.model && (!unpinned || overrideWins)) {
|
|
634
647
|
client.model = target;
|
|
635
648
|
}
|
|
636
649
|
}
|
|
@@ -2029,7 +2029,20 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
|
|
|
2029
2029
|
function _render_quorum_line(q: QuorumResult): string {
|
|
2030
2030
|
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
2031
2031
|
const solo = isSoloConcluded(q) ? ' · **solo** — one voice concluded this pass' : '';
|
|
2032
|
-
|
|
2032
|
+
// The DEGRADED marker shipped on stdout (`council_cli.ts::_format_quorum_line`,
|
|
2033
|
+
// round 7 § 5.3) and did NOT ship here, although this function is the one
|
|
2034
|
+
// the mirror was copied FROM. The two then diverged in the worse direction:
|
|
2035
|
+
// stdout scrolls away, this string is what gets committed as evidence and
|
|
2036
|
+
// cited months later. `1/2 present, needed 1 — concluded` is literally true
|
|
2037
|
+
// and reads as agreement; the solo marker alone covers only present === 1,
|
|
2038
|
+
// so 2-of-4 rendered as plain "concluded" with nothing to stop a reader
|
|
2039
|
+
// inferring convergence. Same sentence as stdout on purpose — one wording,
|
|
2040
|
+
// so neither surface can drift into being the softer one again.
|
|
2041
|
+
const degraded =
|
|
2042
|
+
q.present < q.total
|
|
2043
|
+
? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.`
|
|
2044
|
+
: '';
|
|
2045
|
+
return `**Quorum:** ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${solo}${degraded}`;
|
|
2033
2046
|
}
|
|
2034
2047
|
|
|
2035
2048
|
/**
|
|
@@ -356,6 +356,7 @@ export type CliFailureClass =
|
|
|
356
356
|
| 'binary_missing'
|
|
357
357
|
| 'auth_rejected'
|
|
358
358
|
| 'cli_unsupported'
|
|
359
|
+
| 'model_unservable'
|
|
359
360
|
| 'timeout'
|
|
360
361
|
| 'server_error'
|
|
361
362
|
| 'quota_exhausted'
|
|
@@ -376,6 +377,20 @@ const FALLBACK_ELIGIBLE: ReadonlySet<CliFailureClass> = new Set([
|
|
|
376
377
|
'binary_missing',
|
|
377
378
|
'auth_rejected',
|
|
378
379
|
'cli_unsupported',
|
|
380
|
+
// `model_unservable` satisfies the same no-double-charge property in both
|
|
381
|
+
// of its shapes: the pre-spend gate refuses before any subprocess exists,
|
|
382
|
+
// and the call-time shape is a provider 400 — rejected at the request
|
|
383
|
+
// boundary, no generation performed.
|
|
384
|
+
//
|
|
385
|
+
// HONEST SCOPE: this set is not consumed by any shipped path today.
|
|
386
|
+
// `isFallbackEligible` and `MidFlightFallback` have zero call sites outside
|
|
387
|
+
// this module and its tests, so membership here is a stated property of the
|
|
388
|
+
// class, NOT a behaviour a caller performs. Said plainly because the
|
|
389
|
+
// opposite reading is the attractive one — the constraint really is the
|
|
390
|
+
// subscription transport's rather than the model's, so the api rung really
|
|
391
|
+
// would serve the same id, and it would be easy to write that as if it
|
|
392
|
+
// happened. It does not happen yet.
|
|
393
|
+
'model_unservable',
|
|
379
394
|
]);
|
|
380
395
|
|
|
381
396
|
/** Normalise a raw client error / skip reason into a `CliFailureClass`. */
|
|
@@ -388,6 +403,15 @@ export function classifyCliFailure(raw: string): CliFailureClass {
|
|
|
388
403
|
if (s === 'cli_unsupported' || s === 'unsupported' || s === 'parse_failed') {
|
|
389
404
|
return 'cli_unsupported';
|
|
390
405
|
}
|
|
406
|
+
// Prefix match, not equality: the detail carries the operator-facing reason
|
|
407
|
+
// after the code, and that sentence IS the value of this class — collapsing
|
|
408
|
+
// it back to a bare token would restore the opaque `exit_1` this replaced.
|
|
409
|
+
// Also matched on the provider's own wording, so a model refused at call
|
|
410
|
+
// time (one the deny-list has never seen) classifies the same as one the
|
|
411
|
+
// pre-spend gate caught.
|
|
412
|
+
if (s.startsWith('model_unservable') || s.includes('is not supported when using codex')) {
|
|
413
|
+
return 'model_unservable';
|
|
414
|
+
}
|
|
391
415
|
if (s === 'timeout') return 'timeout';
|
|
392
416
|
if (s === 'cli_quota_exhausted' || s === 'quota_exhausted') return 'quota_exhausted';
|
|
393
417
|
if (/^exit_5\d\d$/.test(s) || s === 'server_error' || s === 'os') return 'server_error';
|
|
@@ -406,8 +430,14 @@ export function isFallbackEligible(failure: CliFailureClass): boolean {
|
|
|
406
430
|
* can bucket every absent member — whether it never got a transport at all
|
|
407
431
|
* or lost one partway through — under one vocabulary. Returns `null` for a
|
|
408
432
|
* failure class outside the four-value enum (`cli_unsupported`,
|
|
409
|
-
* `server_error`, `other`); a caller falls back to the raw
|
|
410
|
-
* for those rather than mis-classifying them.
|
|
433
|
+
* `server_error`, `model_unservable`, `other`); a caller falls back to the raw
|
|
434
|
+
* failure detail for those rather than mis-classifying them.
|
|
435
|
+
*
|
|
436
|
+
* `model_unservable` is deliberately unmapped rather than folded into
|
|
437
|
+
* `no_auth`: the credential is fine, the model is not, and the `detail` string
|
|
438
|
+
* carries the specific reason. So the bucket a reader sees is still the generic
|
|
439
|
+
* `unavailable`, while the line beside it now names the cause — an incomplete
|
|
440
|
+
* improvement, stated as one.
|
|
411
441
|
*/
|
|
412
442
|
export function absentReasonFromCliFailure(failure: CliFailureClass): AbsentReason | null {
|
|
413
443
|
switch (failure) {
|
|
@@ -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
|
}
|