amicus 4.6.3 → 4.7.1
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/plugin.json +1 -1
- package/CHANGELOG.md +349 -0
- package/README.md +19 -7
- package/bin/amicus.js +31 -0
- package/docs/ROADMAP.md +143 -36
- package/docs/configuration.md +52 -4
- package/docs/council.md +65 -12
- package/docs/doc-system.md +8 -7
- package/docs/schemas.md +10 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +33 -5
- package/docs/usage.md +64 -14
- package/electron/workspace-ui/index.html +3 -0
- package/electron/workspace-ui/live-model.js +52 -14
- package/electron/workspace-ui/workspace-app.js +14 -3
- package/electron/workspace-ui/workspace-lazy.js +233 -0
- package/electron/workspace-ui/workspace-matrix.js +12 -1
- package/electron/workspace-ui/workspace-panels.js +24 -171
- package/electron/workspace-ui/workspace-render.js +6 -2
- package/electron/workspace-ui/workspace-seats.js +68 -0
- package/electron/workspace-ui/workspace.css +6 -0
- package/package.json +8 -4
- package/schemas/council-run.schema.json +1 -0
- package/schemas/council-stats.schema.json +9 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
- package/skills/second-opinion/MODEL-NOTES.md +5 -4
- package/skills/sidecar/SKILL.md +9 -2
- package/src/cli-council-run-bench.js +86 -0
- package/src/cli-handlers-council-run.js +65 -81
- package/src/cli-handlers-council.js +17 -5
- package/src/cli-handlers-fanout.js +179 -0
- package/src/cli-handlers-pack.js +24 -10
- package/src/cli-handlers-resume-continue.js +20 -0
- package/src/cli-handlers-run.js +19 -161
- package/src/cli-template-args.js +48 -0
- package/src/cli.js +39 -46
- package/src/council/debate.js +89 -10
- package/src/council/ledger.js +72 -11
- package/src/council/report.js +17 -6
- package/src/council/run-assemble.js +15 -3
- package/src/council/run-budget.js +2 -2
- package/src/council/run-chair.js +61 -5
- package/src/council/run-debate.js +51 -67
- package/src/council/run-launch.js +20 -2
- package/src/council/run-retry.js +17 -2
- package/src/council/run-stage1-launch.js +94 -0
- package/src/council/run-stage2.js +25 -4
- package/src/council/run-stages.js +79 -86
- package/src/council/run-state.js +10 -2
- package/src/council/run.js +26 -2
- package/src/council/tally.js +6 -2
- package/src/headless.js +69 -6
- package/src/mcp-council-awareness.js +1 -0
- package/src/mcp-council-bench.js +4 -0
- package/src/mcp-council-run.js +10 -0
- package/src/mcp-server.js +114 -54
- package/src/mcp-tools.js +12 -5
- package/src/pack/pack-cli.js +1 -1
- package/src/pack/pack-forward.js +12 -4
- package/src/pack/pack-resolve.js +3 -0
- package/src/pack/pack-store.js +20 -3
- package/src/pack/pack-validate.js +5 -1
- package/src/sidecar/budget.js +38 -4
- package/src/sidecar/continue.js +8 -23
- package/src/sidecar/fanout-budget.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +7 -3
- package/src/sidecar/fanout-retry.js +15 -3
- package/src/sidecar/fanout-wave-io.js +13 -1
- package/src/sidecar/fanout.js +11 -9
- package/src/sidecar/list-limit.js +50 -0
- package/src/sidecar/list-search.js +69 -0
- package/src/sidecar/read.js +90 -5
- package/src/sidecar/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- package/src/sidecar/start-metadata.js +58 -0
- package/src/sidecar/start.js +8 -43
- package/src/sidecar/workspace-auto-open.js +2 -2
- package/src/spend-query.js +2 -1
- package/src/template/apply.js +7 -4
- package/src/template/render.js +6 -2
- package/src/template/store.js +1 -1
- package/src/utils/cli-preflight.js +27 -1
- package/src/utils/config.js +15 -0
- package/src/utils/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/remediation-hints.js +29 -0
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +6 -1
- package/src/utils/session-index-tmp-sweep.js +18 -3
- package/src/utils/session-index.js +1 -0
- package/src/utils/session-metadata-tmp-sweep.js +24 -4
- package/src/utils/spend-ledger.js +11 -4
- package/src/utils/validators.js +16 -0
package/src/council/ledger.js
CHANGED
|
@@ -3,11 +3,55 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { getConfigDir } = require('../utils/config');
|
|
6
|
-
const { DEBATE_ROLES } = require('./debate');
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
// v4.7 GOA-7 D9: v2 rows may carry `resolvedModel` (the executable id that
|
|
8
|
+
// served, copied from the joined runStats row, emit-only-when-set). Absent
|
|
9
|
+
// resolvedModel ⇒ legacy row, aggregated under its alias (spec R2) — this
|
|
10
|
+
// covers ALL pre-v2 history AND leg-less v2 rows (give-up chair, dead seats,
|
|
11
|
+
// claude, hand-assembled tally input), whose resolution is genuinely
|
|
12
|
+
// unknowable. Legacy-READ only: readers never inspect schemaVersion, rows are
|
|
13
|
+
// never migrated.
|
|
14
|
+
const LEDGER_SCHEMA_VERSION = 2;
|
|
9
15
|
const LEDGER_FILE = 'council-ledger.jsonl';
|
|
10
16
|
|
|
17
|
+
// v4.7 D4/E1/E2/E6 (Task-7, task-6/task-7 adjudications): fail-closed
|
|
18
|
+
// ALLOWLIST of runStats roles the ledger join (below) may consume as a
|
|
19
|
+
// model's ledger identity. 'council' is the legacy default role (pre-#83
|
|
20
|
+
// runs, and the av-receiver golden fixture — errata E2, must stay green).
|
|
21
|
+
// 'redteam' is the second-opinion skill's documented primary-seat role
|
|
22
|
+
// (skills/second-opinion/MANUAL-ORCHESTRATION.md:147; red-team runs record
|
|
23
|
+
// to the ledger per COUNCIL-DESIGN.md:266 — errata E6, task-7 review: without
|
|
24
|
+
// it a red-team row's role/wasChair/conformance never join, silently
|
|
25
|
+
// fabricating conformance:'clean' via the `|| 'clean'` fallback below).
|
|
26
|
+
// 'judge' stays excluded (#83's overwrite-guard: judges ARE bench models, and
|
|
27
|
+
// their Stage-2 cost-attribution row must never win over the seat row). Every
|
|
28
|
+
// OTHER non-primary row-per-launch producer — 'chair-attempt', 'repair',
|
|
29
|
+
// 'superseded', and the debate pair 'rebuttal'/'revote' — shares a model with
|
|
30
|
+
// that model's real bench row and must never join either: skipped by
|
|
31
|
+
// omission, including any future role never added here (fail-closed, not a
|
|
32
|
+
// skip-list that a new producer could silently slip past). This is the E6
|
|
33
|
+
// trade, made explicit: any free-form/custom role label a future producer
|
|
34
|
+
// invents is rejected BY DESIGN until someone deliberately adds it here —
|
|
35
|
+
// the allowlist would rather silently drop a legitimate new role's
|
|
36
|
+
// role/wasChair/conformance (falling back to 'council'/false/'clean') than
|
|
37
|
+
// ever let an unreviewed role win the model-keyed join.
|
|
38
|
+
//
|
|
39
|
+
// Final-review consolidated wave (owner-ruled): the ABSENCE of a role
|
|
40
|
+
// (null/undefined) is a DIFFERENT case from a NAMED-unknown role and joins
|
|
41
|
+
// too — this is the docs/council.md:562-blessed hand-assembled tally-input
|
|
42
|
+
// shape ("the legacy default `council` … pre-#83 rows, or hand-assembled
|
|
43
|
+
// tally input that never set a role"), and mirrors GOA-7's absent-field⇒
|
|
44
|
+
// legacy pattern elsewhere in this codebase: a field that was never set gets
|
|
45
|
+
// treated as the oldest/legacy shape, not silently dropped like an
|
|
46
|
+
// unreviewed value would be. A NAMED custom label (e.g. 'custom-thing') is
|
|
47
|
+
// still rejected exactly as E6 describes — only the missing-field case is
|
|
48
|
+
// legacy; an actively wrong or unreviewed one is not.
|
|
49
|
+
const LEDGER_JOIN_ROLES = new Set(['seat', 'critic', 'chair', 'claude', 'council', 'redteam']);
|
|
50
|
+
function joinsLedger(role) {
|
|
51
|
+
return role === null || role === undefined ||
|
|
52
|
+
LEDGER_JOIN_ROLES.has(role) || (typeof role === 'string' && role.startsWith('lens:'));
|
|
53
|
+
}
|
|
54
|
+
|
|
11
55
|
function countSeverity(findings) {
|
|
12
56
|
const c = { blocker: 0, major: 0, minor: 0, nit: 0 };
|
|
13
57
|
for (const f of findings) { if (c[f.severity] !== undefined) { c[f.severity] += 1; } }
|
|
@@ -18,10 +62,10 @@ function countSeverity(findings) {
|
|
|
18
62
|
function buildLedgerRows(record) {
|
|
19
63
|
const { meta, findings, streetCred, runStats, judged } = record;
|
|
20
64
|
const sc = new Map(streetCred.map(s => [s.model, s]));
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const rs = new Map(runStats.filter(r =>
|
|
65
|
+
// The join below is keyed by MODEL — only allowlisted roles (joinsLedger,
|
|
66
|
+
// above) may win it, so a non-primary row-per-launch row can never
|
|
67
|
+
// silently overwrite a model's real bench (seat) row.
|
|
68
|
+
const rs = new Map(runStats.filter(r => joinsLedger(r.role))
|
|
25
69
|
.map(r => [r.model, r]));
|
|
26
70
|
return meta.models.map(model => {
|
|
27
71
|
const raised = findings.filter(f => f.raiser === model);
|
|
@@ -39,6 +83,7 @@ function buildLedgerRows(record) {
|
|
|
39
83
|
confirmRate: judged && denom ? raised.filter(f => f.tier === 'Confirmed').length / denom : null,
|
|
40
84
|
factErrorRate: judged && denom ? raised.filter(f => f.tier === 'Disputed').length / denom : null,
|
|
41
85
|
conformance: r.conformance || 'clean',
|
|
86
|
+
...(r.resolvedModel ? { resolvedModel: r.resolvedModel } : {}),
|
|
42
87
|
};
|
|
43
88
|
});
|
|
44
89
|
}
|
|
@@ -61,25 +106,41 @@ function readRows(dir) {
|
|
|
61
106
|
|
|
62
107
|
function avg(nums) { return nums.length ? nums.reduce((s, x) => s + x, 0) / nums.length : null; }
|
|
63
108
|
|
|
64
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Aggregate the ledger per model. peersOnly nulls excluded; lowN flags < 3 runs.
|
|
111
|
+
* v4.7 GOA-7 D10: groups by `row.resolvedModel || row.model` — v2 rows segment
|
|
112
|
+
* by the executable id that actually served; rows without a resolvedModel
|
|
113
|
+
* (pre-v2 history, leg-less rows, hand-assembled tally input) stay alias-keyed
|
|
114
|
+
* with `legacy: true`. `aliases` lists every row-level `model` (alias) observed
|
|
115
|
+
* for the group, most recently observed FIRST — ledger append order is the only
|
|
116
|
+
* recency signal (`date` is day-granular, free-form on the MCP path), so
|
|
117
|
+
* aliases[0] is the launch-preferred name (pickFallbackChair, D11).
|
|
118
|
+
* Version-blind by design: schemaVersion is never read (legacy-read, R2).
|
|
119
|
+
*/
|
|
65
120
|
function deriveReliability(opts = {}) {
|
|
66
121
|
const dir = opts.dir || getConfigDir();
|
|
67
|
-
const
|
|
122
|
+
const byKey = new Map();
|
|
68
123
|
for (const row of readRows(dir)) {
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
const key = row.resolvedModel || row.model;
|
|
125
|
+
if (!byKey.has(key)) { byKey.set(key, []); }
|
|
126
|
+
byKey.get(key).push(row);
|
|
71
127
|
}
|
|
72
|
-
return [...
|
|
128
|
+
return [...byKey.entries()].map(([model, rows]) => {
|
|
73
129
|
const peers = rows.map(r => r.streetCredPeersOnly).filter(v => typeof v === 'number');
|
|
74
130
|
const confirms = rows.map(r => r.confirmRate).filter(v => typeof v === 'number');
|
|
75
131
|
const facts = rows.map(r => r.factErrorRate).filter(v => typeof v === 'number');
|
|
76
132
|
const conformance = rows.reduce((acc, r) => { acc[r.conformance] = (acc[r.conformance] || 0) + 1; return acc; }, {});
|
|
133
|
+
const lastSeen = new Map();
|
|
134
|
+
rows.forEach((r, i) => { lastSeen.set(r.model, i); });
|
|
135
|
+
const aliases = [...lastSeen.entries()].sort((a, b) => b[1] - a[1]).map(([m]) => m);
|
|
77
136
|
return {
|
|
78
137
|
model, runs: rows.length, lowN: rows.length < 3,
|
|
79
138
|
avgStreetCredPeersOnly: avg(peers),
|
|
80
139
|
lifetimeConfirmRate: avg(confirms),
|
|
81
140
|
lifetimeFactErrorRate: avg(facts),
|
|
82
141
|
conformance,
|
|
142
|
+
aliases,
|
|
143
|
+
...(rows.every(r => !r.resolvedModel) ? { legacy: true } : {}),
|
|
83
144
|
};
|
|
84
145
|
});
|
|
85
146
|
}
|
package/src/council/report.js
CHANGED
|
@@ -66,13 +66,24 @@ function toModel(verdict, wave) {
|
|
|
66
66
|
.map(f => ({ id: f.id, previousTier: f.debate.previousTier, tier: f.tier })),
|
|
67
67
|
};
|
|
68
68
|
const runStats = verdict.runStats || [];
|
|
69
|
-
// Cost-row role tag (Plan 2 final review F1): #83 gave
|
|
70
|
-
// runStats row, so a bench model can now appear twice
|
|
71
|
-
// indistinguishable by `model` alone.
|
|
72
|
-
//
|
|
73
|
-
// their
|
|
69
|
+
// Cost-row role tag (Plan 2 final review F1, extended v4.7 D6): #83 gave
|
|
70
|
+
// judges their own runStats row, so a bench model can now appear twice
|
|
71
|
+
// (seat + judge), indistinguishable by `model` alone. v4.7's row-per-launch
|
|
72
|
+
// producers (chair-attempt/repair/superseded) create the exact same
|
|
73
|
+
// collision for their model. Tag ONLY these four roles — old verdicts have
|
|
74
|
+
// none of them, so chair/critic/lens/seat rows stay byte-identical to their
|
|
75
|
+
// historical rendering (report.test.js:189-199 pins the judge case exactly).
|
|
76
|
+
// Object.create(null): a plain `{...}` literal inherits Object.prototype, so a role
|
|
77
|
+
// literally named 'constructor'/'toString'/etc would resolve to an inherited (truthy)
|
|
78
|
+
// function via bracket lookup instead of `undefined` — silently corrupting that row's
|
|
79
|
+
// rendered model label. A null-prototype object has no inherited keys to collide with.
|
|
80
|
+
const ROLE_SUFFIX = Object.create(null);
|
|
81
|
+
ROLE_SUFFIX.judge = 'judge';
|
|
82
|
+
ROLE_SUFFIX['chair-attempt'] = 'chair-attempt';
|
|
83
|
+
ROLE_SUFFIX.repair = 'repair';
|
|
84
|
+
ROLE_SUFFIX.superseded = 'superseded';
|
|
74
85
|
const costRows = runStats.map(r => ({
|
|
75
|
-
model: r.role
|
|
86
|
+
model: ROLE_SUFFIX[r.role] ? `${r.model} (${ROLE_SUFFIX[r.role]})` : r.model,
|
|
76
87
|
status: r.status, durationMs: r.durationMs,
|
|
77
88
|
cost: r.usage && r.usage.cost ? r.usage.cost : null,
|
|
78
89
|
}));
|
|
@@ -40,6 +40,9 @@ function worseConformance(a, b) {
|
|
|
40
40
|
* leg doc yields durationMs/usage null (never invent a value). `model` (the
|
|
41
41
|
* council alias) overrides leg.model (the resolved executable id) so ledger
|
|
42
42
|
* rows join meta.models by exact string (ledger.js:20-24).
|
|
43
|
+
* `resolvedModel` (v4.7 GOA-7) preserves leg.model — the executable id that
|
|
44
|
+
* actually served, post-fallback-substitution — emit-only-when-set and never
|
|
45
|
+
* sourced from modelInput (an alias must never masquerade as a resolved id).
|
|
43
46
|
*
|
|
44
47
|
* ⚠️ LC-11 / review F1: `findingsUnverified` and `repairRefused` are the same
|
|
45
48
|
* class of fact as `conformance` and ride the same row. They are the two halves
|
|
@@ -60,6 +63,8 @@ function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsU
|
|
|
60
63
|
conformance: conformance || 'clean',
|
|
61
64
|
...(findingsUnverified ? { findingsUnverified: true } : {}),
|
|
62
65
|
...(repairRefused ? { repairRefused } : {}),
|
|
66
|
+
...(leg && leg.waveId ? { waveId: leg.waveId } : {}),
|
|
67
|
+
...(leg && leg.model ? { resolvedModel: leg.model } : {}),
|
|
63
68
|
status: leg ? leg.status : 'error',
|
|
64
69
|
durationMs: leg && typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
65
70
|
usage: (leg && leg.usage) || null,
|
|
@@ -131,14 +136,17 @@ function claudeRunStatsRow() {
|
|
|
131
136
|
* @param {{runId: string, date: string, bench: string[], chair: string,
|
|
132
137
|
* reviews: Array<{model, role, conformance, leg, globalFindings}>,
|
|
133
138
|
* judgeResults: Array<{judge, ok, order, adjudications}>,
|
|
134
|
-
* chairStats: object|null, claudeReview?: object|null}} args
|
|
139
|
+
* chairStats: object|null, claudeReview?: object|null, extraRows?: Array<object>}} args
|
|
135
140
|
* `claudeReview` (v4.1 §4.4) amends the v4.0 meta pin: present ⇒ claudeInCouncil
|
|
136
141
|
* true, 'claude' joins meta.models (the street-cred universe), its findings join
|
|
137
142
|
* the pool and it gets the synthesized null-usage runStats row. Absent ⇒ v4.0
|
|
138
|
-
* output byte-for-byte.
|
|
143
|
+
* output byte-for-byte. `extraRows` (v4.7 D2/E4) are pre-built runStats rows
|
|
144
|
+
* (repair/superseded/dead-seat-error, from runStage1 today) appended right
|
|
145
|
+
* after the primary review rows, before judge/chair accounting — absent or
|
|
146
|
+
* empty ⇒ byte-for-byte unchanged, so the pre-v4.7 length-7 pins stay green.
|
|
139
147
|
*/
|
|
140
148
|
function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, chairStats,
|
|
141
|
-
claudeReview }) {
|
|
149
|
+
claudeReview, extraRows }) {
|
|
142
150
|
const meta = {
|
|
143
151
|
runId, date, runType: 'headless',
|
|
144
152
|
models: bench.slice(), // bench seats exactly: critic included, chair excluded
|
|
@@ -154,6 +162,10 @@ function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, cha
|
|
|
154
162
|
leg: r.leg, model: r.model, role: r.role, wasChair: false, conformance: r.conformance,
|
|
155
163
|
findingsUnverified: r.findingsUnverified, repairRefused: r.repairRefused,
|
|
156
164
|
}));
|
|
165
|
+
// v4.7 D2/E4: pre-built rows (repair/superseded/dead-seat-error) ride right
|
|
166
|
+
// after the primary review rows — same "primary-adjacent" shape, just not
|
|
167
|
+
// sourced from a surviving review. Absent/empty ⇒ no-op (pre-v4.7 byte parity).
|
|
168
|
+
runStats.push(...(extraRows || []));
|
|
157
169
|
if (claudeReview) {
|
|
158
170
|
meta.models.push(CLAUDE_SEAT); // last, mirroring its review-N+1 label
|
|
159
171
|
meta.claudeInCouncil = true;
|
|
@@ -153,9 +153,9 @@ function createBudget({ allLegs, maxCost, runDir, degrade, write }) {
|
|
|
153
153
|
channel: 'budget-refusal',
|
|
154
154
|
what: `wave ${rec.waveId} (${rec.models.join(', ') || 'no models'}) — those seats DID NOT `
|
|
155
155
|
+ 'LAUNCH and are missing from this council',
|
|
156
|
-
why: `the $${maxCost}
|
|
156
|
+
why: `the $${maxCost} cost ceiling for this run refused it${message ? `: ${message}` : ''}`,
|
|
157
157
|
effect: 'The run continues with the bench that did launch and will exit degraded (2)',
|
|
158
|
-
remedy:
|
|
158
|
+
remedy: "Raise this run's cost ceiling, or turn the cost gate off, to seat them",
|
|
159
159
|
});
|
|
160
160
|
if (runDir) {
|
|
161
161
|
// Never let bookkeeping sink a run that is otherwise fine.
|
package/src/council/run-chair.js
CHANGED
|
@@ -20,6 +20,7 @@ const { parseChairVerdict } = require('./parse-stage2');
|
|
|
20
20
|
const runState = require('./run-state');
|
|
21
21
|
const { isAbortExit } = require('./run-stages');
|
|
22
22
|
const { emitStageStarted, emitStageTerminal } = require('../observe/events');
|
|
23
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Chair fallback promotion (spec §4): the highest peers-only street-cred
|
|
@@ -31,15 +32,33 @@ const { emitStageStarted, emitStageTerminal } = require('../observe/events');
|
|
|
31
32
|
* a --claude-review run puts a real 'claude' row in the ledger, so without this
|
|
32
33
|
* filter a LATER run could promote it and walk straight past the pre-flight
|
|
33
34
|
* --chair claude guard — with no Claude leg to launch.
|
|
35
|
+
*
|
|
36
|
+
* v4.7 GOA-7 D11: exclusions test the group key AND aliases[]; the promoted
|
|
37
|
+
* name is aliases[0] (most-recent alias) so the launch string stays routable
|
|
38
|
+
* through the same alias policy both call sites (run.js mid-walk, run-server.js
|
|
39
|
+
* pre-seed) already resolve.
|
|
34
40
|
* @returns {string|null}
|
|
35
41
|
*/
|
|
36
42
|
function pickFallbackChair(statsRows, bench, failedChair) {
|
|
37
43
|
const benchSet = new Set(bench);
|
|
44
|
+
// v4.7 GOA-7 D11: an aggregate's identity is its key PLUS every alias it was
|
|
45
|
+
// observed under — post-D10 keys may be executable ids while bench/o.chair
|
|
46
|
+
// stay alias-space, so every exclusion tests the whole name set (a bench
|
|
47
|
+
// seat's resolved-keyed group must never be promoted as its own chair).
|
|
48
|
+
// The LAUNCHED name is aliases[0] (most-recent alias): alias-space names
|
|
49
|
+
// re-enter the router's alias bridge and current key/gateway policy; a raw
|
|
50
|
+
// executable id would dodge them (divergent-vendor forms, openrouter-
|
|
51
|
+
// literals under --gateway direct, dropped aliases). aliases[] is non-empty
|
|
52
|
+
// for every ledger-derived group; the bare-model fallback covers pre-D10
|
|
53
|
+
// aggregate shapes only.
|
|
54
|
+
const names = (r) => [r.model, ...(Array.isArray(r.aliases) ? r.aliases : [])];
|
|
55
|
+
const excluded = (r) => names(r).some(n => n === 'claude' || benchSet.has(n) || n === failedChair);
|
|
38
56
|
const candidates = (statsRows || [])
|
|
39
|
-
.filter(r =>
|
|
40
|
-
&& typeof r.avgStreetCredPeersOnly === 'number')
|
|
57
|
+
.filter(r => !excluded(r) && typeof r.avgStreetCredPeersOnly === 'number')
|
|
41
58
|
.sort((a, b) => a.avgStreetCredPeersOnly - b.avgStreetCredPeersOnly);
|
|
42
|
-
|
|
59
|
+
if (!candidates.length) { return null; }
|
|
60
|
+
const top = candidates[0];
|
|
61
|
+
return (Array.isArray(top.aliases) && top.aliases.length) ? top.aliases[0] : top.model;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
@@ -98,6 +117,7 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
98
117
|
// v4.3 Task 3 (spec §7.2 named defect): without this, chair spend is
|
|
99
118
|
// ledgered with councilRunId:null and is unattributable.
|
|
100
119
|
councilRunId: o.runId, councilName: o.councilName,
|
|
120
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
101
121
|
});
|
|
102
122
|
addWave(solo.wave);
|
|
103
123
|
const ok = solo.leg && solo.leg.status === 'complete'
|
|
@@ -117,12 +137,24 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
117
137
|
// cost-skipped chair (the `if` branch) simply never calls recordAttempt —
|
|
118
138
|
// chairAttempts is never checkpointed and the key stays absent on run.json.
|
|
119
139
|
const chairAttempts = [];
|
|
140
|
+
// v4.7 D2 (spec "the count is the count"): a non-primary row per launch so
|
|
141
|
+
// spend is fully attributable — one 'chair-attempt' row per FAILED attempt
|
|
142
|
+
// that produced a rawLeg (null rawLeg = no wave = no money = no row), plus
|
|
143
|
+
// one 'repair' row for a launched ch4 (pushed below, after the ch4 block).
|
|
144
|
+
// The eventual SUCCESSFUL attempt's leg is never pushed here — it becomes
|
|
145
|
+
// the primary 'chair' row (wasChair:true) via run.js's own chairStats.
|
|
146
|
+
const chairRows = [];
|
|
120
147
|
const recordAttempt = (attempt, waveId, model) => {
|
|
121
148
|
const cls = classifyChairAttempt(attempt.rawLeg, attempt.errorDoc);
|
|
122
149
|
chairAttempts.push({ waveId, model, outcome: cls.outcome, reason: cls.reason });
|
|
123
150
|
// Checkpointed HERE, before the caller's own isAbortExit bail — a mid-walk
|
|
124
151
|
// kill must not lose the attempts already resolved (spec §8 kill-mid-walk).
|
|
125
152
|
runState.checkpoint(o.runDir, { chairAttempts });
|
|
153
|
+
if (!attempt.leg && attempt.rawLeg) {
|
|
154
|
+
chairRows.push(buildRunStatsEntry({
|
|
155
|
+
leg: attempt.rawLeg, model, role: 'chair-attempt', wasChair: false,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
126
158
|
};
|
|
127
159
|
if (overBudget()) {
|
|
128
160
|
// Ceiling hit after the tally is computable: skip the chair, write the
|
|
@@ -183,19 +215,37 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
183
215
|
// ---- Chair VERDICT line (one repair re-prompt, spec §5) ----
|
|
184
216
|
let overallVerdict = chairText ? parseChairVerdict(chairText) : null;
|
|
185
217
|
if (chairText && !overallVerdict && !overBudget()) {
|
|
186
|
-
|
|
218
|
+
const waveId4 = `${o.runId}-ch4`;
|
|
219
|
+
runState.appendStageWave(o.runDir, 'chair', waveId4);
|
|
187
220
|
const repair = await launchers.launchSolo({
|
|
188
221
|
// ⚠️ LC-12: the synthesis rides along. The chair leg SUCCEEDED — only the
|
|
189
222
|
// VERDICT line is missing — so a fresh repair session that cannot see the
|
|
190
223
|
// synthesis is picking a verdict on an artifact it has never read.
|
|
191
224
|
model: actualChair, prompt: stage2.buildChairRepairPrompt({ synthesis: chairText }),
|
|
192
|
-
project: o.runDir, waveId:
|
|
225
|
+
project: o.runDir, waveId: waveId4,
|
|
193
226
|
timeout: o.timeout, gateway: o.gateway, noValidateModel: o.noValidateModel,
|
|
194
227
|
noCostGate: o.noCostGate,
|
|
195
228
|
councilRunId: o.runId, councilName: o.councilName,
|
|
229
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
196
230
|
});
|
|
197
231
|
addWave(repair.wave);
|
|
198
232
|
if (isAbortExit(repair.exitCode) || isSignalled()) { return bail(repair.exitCode || isSignalled()); }
|
|
233
|
+
// repair.leg is the raw leg document — launchSolo DOES null it, but only
|
|
234
|
+
// on a wave-less failure (a pre-flight refusal with no wave launched at
|
|
235
|
+
// all: run-launch.js's launchSolo derives `leg` from `wave.legs[0]`, so
|
|
236
|
+
// no wave means no leg, no waveId, no money spent — the errata E3 "no
|
|
237
|
+
// leg = no wave = no money = no row" case). A wave that DID launch
|
|
238
|
+
// always yields a leg document, whatever its status. The `if
|
|
239
|
+
// (repair.leg)` guard below is therefore load-bearing on that exact
|
|
240
|
+
// distinction: a launched ch4 (a leg document exists, whatever its
|
|
241
|
+
// status) gets its own row so the repair's spend is attributed even when
|
|
242
|
+
// it never supplies a VERDICT; a ch4 that never even launched gets no
|
|
243
|
+
// row at all, because there is nothing billed to attribute.
|
|
244
|
+
if (repair.leg) {
|
|
245
|
+
chairRows.push(buildRunStatsEntry({
|
|
246
|
+
leg: repair.leg, model: actualChair, role: 'repair', wasChair: false,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
199
249
|
overallVerdict = parseChairVerdict((repair.leg && repair.leg.summary) || '');
|
|
200
250
|
chairConformance = overallVerdict ? 'repaired' : 'unstructured';
|
|
201
251
|
}
|
|
@@ -216,6 +266,12 @@ async function runChair(ctx, { packet, degrade, statsFn, isSignalled }) {
|
|
|
216
266
|
|
|
217
267
|
return {
|
|
218
268
|
aborted: null, chairLeg, actualChair, chairText, chairConformance, overallVerdict,
|
|
269
|
+
// Additive (v4.7 D2): chairRows holds the non-primary rows (attempts +
|
|
270
|
+
// repair); chairAttempts is handed back too so run.js can key the
|
|
271
|
+
// give-up row on "the walk actually happened" without re-reading disk —
|
|
272
|
+
// NOT on chairRows, since attempts that die pre-wave record an outcome
|
|
273
|
+
// but produce no row (errata E3: no wave = no money = no row).
|
|
274
|
+
chairRows, chairAttempts,
|
|
219
275
|
};
|
|
220
276
|
}
|
|
221
277
|
|
|
@@ -13,62 +13,38 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const dbrief = require('./briefings-debate');
|
|
15
15
|
const { parseDebateDefense, parseRevote } = require('./parse-stage2');
|
|
16
|
-
const { applyDebate, debateRunStatsRows, PAST_TENSE
|
|
16
|
+
const { applyDebate, debateRunStatsRows, PAST_TENSE,
|
|
17
|
+
allNoResponse, nothingToDebate, disputingJudges, debateTargets, bundleFor } = require('./debate');
|
|
17
18
|
const { materializeDebate } = require('./run-launch');
|
|
18
19
|
const { tally } = require('./tally');
|
|
19
20
|
const { isAbortExit } = require('./run-stages');
|
|
20
21
|
const runState = require('./run-state');
|
|
21
22
|
const { emitStageStarted } = require('../observe/events');
|
|
22
23
|
|
|
23
|
-
/** Spec §5.7 fallback: a dead/unparseable defense means every bundled id's original stands. */
|
|
24
|
-
function allNoResponse(ids) {
|
|
25
|
-
const byId = {};
|
|
26
|
-
for (const id of ids) { byId[id] = { action: 'no-response' }; }
|
|
27
|
-
return byId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** True when there is nothing to challenge (spec §5.1). */
|
|
31
|
-
function nothingToDebate(provisionalRecord) {
|
|
32
|
-
if (!provisionalRecord || provisionalRecord.judged === false) { return true; }
|
|
33
|
-
const n = provisionalRecord.findings.filter(f => f.tier === 'Contested' || f.tier === 'Disputed').length;
|
|
34
|
-
return n === 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Judges whose provisional adjudications dispute at least one bundled id. */
|
|
38
|
-
function disputingJudges(provisionalRecord, bundledIds) {
|
|
39
|
-
const ids = new Set(bundledIds);
|
|
40
|
-
const judges = new Set();
|
|
41
|
-
for (const f of provisionalRecord.findings) {
|
|
42
|
-
if (!ids.has(f.id)) { continue; }
|
|
43
|
-
for (const adj of f.adjudications || []) {
|
|
44
|
-
if (adj.verdict === 'dispute') { judges.add(adj.judge); }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return [...judges];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Group Contested+Disputed findings by raiser (defense targets). */
|
|
51
|
-
function debateTargets(provisionalRecord, tallyInput) {
|
|
52
|
-
const claimById = new Map(tallyInput.findings.map(f => [f.id, f]));
|
|
53
|
-
const byRaiser = {};
|
|
54
|
-
const previousTier = {};
|
|
55
|
-
for (const f of provisionalRecord.findings) {
|
|
56
|
-
if (f.tier !== 'Contested' && f.tier !== 'Disputed') { continue; }
|
|
57
|
-
previousTier[f.id] = f.tier;
|
|
58
|
-
const src = claimById.get(f.id) || {};
|
|
59
|
-
const peerVerdicts = (f.adjudications || []).filter(a => a.judge !== f.raiser).map(a => a.verdict);
|
|
60
|
-
(byRaiser[f.raiser] = byRaiser[f.raiser] || []).push({ id: f.id, claim: src.claim,
|
|
61
|
-
severity: f.severity, location: src.location, peerVerdicts, disputeReasons: [] });
|
|
62
|
-
}
|
|
63
|
-
return { byRaiser, previousTier };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
24
|
/** Common launch options for every debate leg (judge-isolated `_scratch` cwd). */
|
|
67
25
|
function legOpts(ctx, waveId) {
|
|
68
26
|
return { project: ctx.scratchDir, waveId, timeout: ctx.o.timeout, gateway: ctx.o.gateway,
|
|
69
27
|
noValidateModel: ctx.o.noValidateModel, noCostGate: ctx.o.noCostGate,
|
|
70
28
|
// v4.3 Task 3 (spec §7.2): attribution ids for every defense/re-vote leg.
|
|
71
|
-
councilRunId: ctx.o.runId, councilName: ctx.o.councilName
|
|
29
|
+
councilRunId: ctx.o.runId, councilName: ctx.o.councilName,
|
|
30
|
+
tag: ctx.o.tag }; // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* v4.7 D2/E4: normalize a raw (possibly leg-absent) leg into the shape
|
|
35
|
+
* debateRunStatsRows' superseded/repair lists expect. Same never-invent-a-waveId
|
|
36
|
+
* discipline as buildRunStatsEntry (run-assemble.js) — only spread `waveId` when
|
|
37
|
+
* the leg genuinely carries one — but keyed on an explicit `model` (the raiser or
|
|
38
|
+
* judge identity), since a leg-absent attempt has no `.model` of its own to read.
|
|
39
|
+
* Threads resolvedModel (the raw leg's .model, the executable id) emit-only-when-set — v4.7 GOA-7 D8.
|
|
40
|
+
*/
|
|
41
|
+
function legRow(model, leg, conformance) {
|
|
42
|
+
return leg
|
|
43
|
+
? { model, status: leg.status, durationMs: typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
44
|
+
usage: leg.usage || null, conformance, summary: leg.summary || '',
|
|
45
|
+
...(leg.waveId ? { waveId: leg.waveId } : {}),
|
|
46
|
+
...(leg.model ? { resolvedModel: leg.model } : {}) }
|
|
47
|
+
: { model, status: 'error', durationMs: null, usage: null, conformance, summary: '' };
|
|
72
48
|
}
|
|
73
49
|
|
|
74
50
|
async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
@@ -90,6 +66,12 @@ async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
|
90
66
|
let parsed = leg ? parseDebateDefense(leg.summary, expectedIds)
|
|
91
67
|
: { ok: false, byId: allNoResponse(expectedIds), errors: [{ code: 'DEAD_LEG', detail: 'no summary' }] };
|
|
92
68
|
let conformance = leg ? 'clean' : 'unstructured';
|
|
69
|
+
// v4.7 D2/E4: the repair's loser leg — the ORIGINAL when the repair produced a
|
|
70
|
+
// usable (complete) leg (today's leg-swap below is unchanged), or the failed
|
|
71
|
+
// repair attempt itself when it did not — retained so runDebate can turn it
|
|
72
|
+
// into an extra debate-defense runStats row. Both stay null when no repair is
|
|
73
|
+
// attempted at all (today's single-row shape, byte-identical).
|
|
74
|
+
let supersededLeg = null, repairLeg = null;
|
|
93
75
|
if (leg && !parsed.ok) {
|
|
94
76
|
const repairId = `${waveId}r`;
|
|
95
77
|
runState.appendStageWave(ctx.o.runDir, 'debate-defense', repairId);
|
|
@@ -103,13 +85,17 @@ async function runDefenseSolo(ctx, raiser, findings, idx) {
|
|
|
103
85
|
const leg2 = res2.leg && res2.leg.status === 'complete' ? res2.leg : null;
|
|
104
86
|
parsed = leg2 ? parseDebateDefense(leg2.summary, expectedIds) : parsed;
|
|
105
87
|
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
106
|
-
if (leg2) { leg = leg2; }
|
|
88
|
+
if (leg2) { supersededLeg = legRow(raiser, leg, 'unstructured'); leg = leg2; }
|
|
89
|
+
else { repairLeg = legRow(raiser, res2.leg, 'unstructured'); }
|
|
107
90
|
}
|
|
108
91
|
// A dead leg (no complete summary) OR an 'unstructured' conformance after the one
|
|
109
92
|
// repair is a debate degradation (spec §5.7) — surfaced via the returned leg.
|
|
110
93
|
const stub = { model: raiser, status: 'error', durationMs: null, usage: null, conformance: 'unstructured', summary: '' };
|
|
111
94
|
return { raiser, byId: parsed.byId,
|
|
112
|
-
leg: leg ? { model: raiser, status: leg.status, durationMs: leg.durationMs, usage: leg.usage,
|
|
95
|
+
leg: leg ? { model: raiser, status: leg.status, durationMs: leg.durationMs, usage: leg.usage,
|
|
96
|
+
conformance, summary: leg.summary, waveId: leg.waveId,
|
|
97
|
+
...(leg.model ? { resolvedModel: leg.model } : {}) } : stub,
|
|
98
|
+
supersededLeg, repairLeg };
|
|
113
99
|
}
|
|
114
100
|
|
|
115
101
|
async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
@@ -131,6 +117,9 @@ async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
|
131
117
|
ctx.addWave(res.wave);
|
|
132
118
|
if (isAbortExit(res.exitCode)) { return { aborted: res.exitCode }; }
|
|
133
119
|
const byJudge = {}, legs = [];
|
|
120
|
+
// v4.7 D2/E4: mirrors runDefenseSolo's supersededLeg/repairLeg — one list each,
|
|
121
|
+
// accumulated across every judge in this wave (most judges contribute neither).
|
|
122
|
+
const supersededLegs = [], repairLegs = [];
|
|
134
123
|
for (const leg of ((res.wave && res.wave.legs) || [])) {
|
|
135
124
|
// The council ALIAS, not the resolved executable id — runStats rows join
|
|
136
125
|
// meta.models by exact string (run-assemble.js's buildRunStatsEntry).
|
|
@@ -154,27 +143,15 @@ async function runRevoteWave(ctx, judges, bundleFindings) {
|
|
|
154
143
|
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
155
144
|
// Symmetric with runDefenseSolo's `if (leg2) { leg = leg2; }` — otherwise
|
|
156
145
|
// revote-<model>.md and the runStats row keep the PRE-repair output.
|
|
157
|
-
if (leg2) { outLeg = leg2; }
|
|
146
|
+
if (leg2) { supersededLegs.push(legRow(judge, leg, 'unstructured')); outLeg = leg2; }
|
|
147
|
+
else { repairLegs.push(legRow(judge, r2.leg, 'unstructured')); }
|
|
158
148
|
}
|
|
159
149
|
byJudge[judge] = parsed.byId;
|
|
160
|
-
legs.push({ model: judge, status: outLeg.status, durationMs: outLeg.durationMs, usage: outLeg.usage,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** The re-vote bundle: defended-or-amended findings ONLY (spec §5.1 — withdrawn never appear). */
|
|
166
|
-
function bundleFor(defenseResults, tallyInput) {
|
|
167
|
-
const out = [];
|
|
168
|
-
for (const dr of defenseResults) {
|
|
169
|
-
for (const [id, resp] of Object.entries(dr.byId)) {
|
|
170
|
-
if (resp.action !== 'defend' && resp.action !== 'amend') { continue; }
|
|
171
|
-
const src = tallyInput.findings.find(f => f.id === id) || {};
|
|
172
|
-
out.push({ id, severity: src.severity, amended: resp.action === 'amend',
|
|
173
|
-
claim: resp.action === 'amend' ? resp.claim : src.claim,
|
|
174
|
-
argument: resp.argument || 'defended without extra argument' });
|
|
175
|
-
}
|
|
150
|
+
legs.push({ model: judge, status: outLeg.status, durationMs: outLeg.durationMs, usage: outLeg.usage,
|
|
151
|
+
conformance, summary: outLeg.summary || '', waveId: outLeg.waveId,
|
|
152
|
+
...(outLeg.model ? { resolvedModel: outLeg.model } : {}) });
|
|
176
153
|
}
|
|
177
|
-
return
|
|
154
|
+
return { byJudge, legs, supersededLegs, repairLegs };
|
|
178
155
|
}
|
|
179
156
|
|
|
180
157
|
/**
|
|
@@ -221,7 +198,7 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
221
198
|
const stampedInput = { ...tallyInput, findings: tallyInput.findings.map(f => ({ ...f, previousTier: previousTier[f.id] })) };
|
|
222
199
|
|
|
223
200
|
// ---- Re-vote mini-wave (disputing judges only) ----
|
|
224
|
-
let revoteByJudge = {}, revoteLegs = [];
|
|
201
|
+
let revoteByJudge = {}, revoteLegs = [], revoteSuperseded = [], revoteRepairs = [];
|
|
225
202
|
const defendedOrAmended = bundleFor(defenseResults, tallyInput);
|
|
226
203
|
const judges = disputingJudges(provisionalRecord, defendedOrAmended.map(f => f.id));
|
|
227
204
|
// A re-vote is warranted only when something was defended/amended AND ≥1 judge disputed it.
|
|
@@ -238,6 +215,8 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
238
215
|
if (rv.aborted) { return { aborted: rv.aborted, contested, disputed }; }
|
|
239
216
|
revoteByJudge = rv.byJudge;
|
|
240
217
|
revoteLegs = rv.legs;
|
|
218
|
+
revoteSuperseded = rv.supersededLegs;
|
|
219
|
+
revoteRepairs = rv.repairLegs;
|
|
241
220
|
// revote-<model>.md per surviving judge leg, mirroring rebuttal-<model>.md
|
|
242
221
|
// (spec §5.1 'raw outputs revote-<model>.md').
|
|
243
222
|
materializeDebate(ctx.o.runDir, revoteLegs, 'revote');
|
|
@@ -247,7 +226,12 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
247
226
|
const { input: debatedInput, debateFindings } = applyDebate({
|
|
248
227
|
tallyInput: stampedInput, provisionalRecord, defenseByRaiser, revoteByJudge });
|
|
249
228
|
debatedInput.runStats = [...(debatedInput.runStats || []),
|
|
250
|
-
...debateRunStatsRows({ defenseLegs: defenseResults.map(d => d.leg), revoteLegs
|
|
229
|
+
...debateRunStatsRows({ defenseLegs: defenseResults.map(d => d.leg), revoteLegs,
|
|
230
|
+
// v4.7 D2/E4: the retained loser legs from every raiser's defense repair
|
|
231
|
+
// plus every judge's re-vote repair — same append, no new channel into
|
|
232
|
+
// buildTallyInput.
|
|
233
|
+
supersededLegs: [...defenseResults.map(d => d.supersededLeg).filter(Boolean), ...revoteSuperseded],
|
|
234
|
+
repairLegs: [...defenseResults.map(d => d.repairLeg).filter(Boolean), ...revoteRepairs] })];
|
|
251
235
|
|
|
252
236
|
// verdictChanges: findings whose tier moved from provisional to debated.
|
|
253
237
|
const provTierById = new Map(provisionalRecord.findings.map(f => [f.id, f.tier]));
|
|
@@ -63,12 +63,18 @@ function createLaunchers(deps = {}) {
|
|
|
63
63
|
/**
|
|
64
64
|
* @param {{models: string[], prompt: string, project: string, waveId: string,
|
|
65
65
|
* timeout?: number, gateway?: string, noValidateModel?: boolean, agent?: string,
|
|
66
|
-
* councilRunId?: string, councilName?: string,
|
|
66
|
+
* councilRunId?: string, councilName?: string, tag?: string, fallback?: object,
|
|
67
|
+
* catalog?: Array, noOutputBackstopMs?: number}} opts
|
|
67
68
|
* councilRunId/councilName (v4.3 Task 3, spec §7.2) are additive attribution
|
|
68
69
|
* ids forwarded verbatim into the runFanout call so it can stamp them onto
|
|
69
|
-
* every leg.
|
|
70
|
+
* every leg. tag (v4.7 F8 D16) rides the same forward — every call site
|
|
71
|
+
* below that sets councilRunId/councilName sets `tag: o.tag` alongside it.
|
|
72
|
+
* fallback/catalog (v4.3 Task 18, spec §6.2) are likewise
|
|
70
73
|
* additive/opt-in — omitted by callers that must never substitute (the
|
|
71
74
|
* chair, debate legs); run-stages.js's Stage-1/Stage-2 launches pass them.
|
|
75
|
+
* noOutputBackstopMs (Task 5, #129) is opt-in and spread-guarded on
|
|
76
|
+
* Number.isFinite (0 is a valid disable value); only run-retry.js sets it,
|
|
77
|
+
* to escalate the window on a Stage-1 retry.
|
|
72
78
|
* @returns {Promise<{wave: object|null, exitCode: number}>}
|
|
73
79
|
*/
|
|
74
80
|
async function launchWave(opts) {
|
|
@@ -111,6 +117,18 @@ function createLaunchers(deps = {}) {
|
|
|
111
117
|
noValidateModel: opts.noValidateModel,
|
|
112
118
|
councilRunId: opts.councilRunId,
|
|
113
119
|
councilName: opts.councilName,
|
|
120
|
+
// v4.7 F8 D16: rides the SAME forward as councilRunId/councilName above —
|
|
121
|
+
// undefined when no --tag, so stampLegAttribution's `if (options.tag)`
|
|
122
|
+
// guard (fanout-wave-io.js) simply no-ops, byte-identical to today.
|
|
123
|
+
tag: opts.tag,
|
|
124
|
+
// Task 5 (#129): spread-guarded on Number.isFinite, NOT on truthiness —
|
|
125
|
+
// an explicit 0 is this knob's documented disable hatch
|
|
126
|
+
// (no-output-backstop.js:13-15) and a truthiness guard would silently
|
|
127
|
+
// drop it. Guarding at all — rather than a plain
|
|
128
|
+
// `noOutputBackstopMs: opts.noOutputBackstopMs` — keeps the transport
|
|
129
|
+
// call key-identical for run-stage1-launch / run-stage2 / run-chair /
|
|
130
|
+
// run-debate, none of which set it.
|
|
131
|
+
...(Number.isFinite(opts.noOutputBackstopMs) ? { noOutputBackstopMs: opts.noOutputBackstopMs } : {}),
|
|
114
132
|
// v4.3 Task 18 (spec §6.2): additive/opt-in. Callers that must never
|
|
115
133
|
// substitute (run-chair.js, run-debate.js) simply omit these — runLeg's
|
|
116
134
|
// fallback path only activates when `fallback.enabled` is true.
|
package/src/council/run-retry.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const briefings = require('./briefings');
|
|
18
18
|
const { materializeReviews, isAbortExit } = require('./run-launch');
|
|
19
19
|
const runState = require('./run-state');
|
|
20
|
+
const { resolveNoOutputBackstopMs } = require('../utils/no-output-backstop');
|
|
20
21
|
const { waveStillDeadNote, srcLegStillDeadNote, retryLegStillDeadNote, missingLegStillDeadNote }
|
|
21
22
|
= require('./run-retry-notes');
|
|
22
23
|
|
|
@@ -141,7 +142,18 @@ function briefingFor(o, unit) {
|
|
|
141
142
|
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts = { reviewed: 0, total: 0 } } = {}) {
|
|
142
143
|
const { o, launchers } = ctx;
|
|
143
144
|
const out = { aborted: null, recoveredLegs: [], stillDeadNotes: [],
|
|
144
|
-
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: []
|
|
145
|
+
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: [],
|
|
146
|
+
stillDeadRetryLegs: [] };
|
|
147
|
+
// Task 5 (#129): SL-2 retries the SAME model under the SAME conditions, so a
|
|
148
|
+
// latency failure is structurally unhealable. Double the window, clamped to
|
|
149
|
+
// the leg timeout so the failure CLASS stays NO_OUTPUT_BACKSTOP rather than
|
|
150
|
+
// silently becoming an ordinary timeout at a low --timeout. 2*0 === 0 keeps
|
|
151
|
+
// the disable hatch. (o.timeout || 15) * 60 * 1000 mirrors fanout.js:254.
|
|
152
|
+
const legTimeoutMs = (o.timeout || 15) * 60 * 1000;
|
|
153
|
+
const escalatedBackstopMs = Math.min(
|
|
154
|
+
2 * (Number.isFinite(o.noOutputBackstopMs) ? o.noOutputBackstopMs : resolveNoOutputBackstopMs()),
|
|
155
|
+
legTimeoutMs,
|
|
156
|
+
);
|
|
145
157
|
|
|
146
158
|
for (const unit of groupStage1Losses(o, deadWaves, deadLegs)) {
|
|
147
159
|
// Task-4 review hardening: a unit this pass cannot even ATTEMPT — an
|
|
@@ -168,8 +180,10 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
168
180
|
const common = { project: o.runDir, timeout: o.timeout, gateway: o.gateway,
|
|
169
181
|
noValidateModel: o.noValidateModel, noCostGate: o.noCostGate,
|
|
170
182
|
councilRunId: o.runId, councilName: o.councilName,
|
|
183
|
+
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
171
184
|
fallback: o.fallback, catalog: o.catalog,
|
|
172
|
-
waveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, prompt: briefingFor(o, unit)
|
|
185
|
+
waveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, prompt: briefingFor(o, unit),
|
|
186
|
+
noOutputBackstopMs: escalatedBackstopMs };
|
|
173
187
|
// Dispatch by UNIT TYPE, not model count (spec §4: bench is always a wave —
|
|
174
188
|
// even down to its last surviving seat — critic/lens are always solos).
|
|
175
189
|
// A model-count proxy (`models.length === 1`) is wrong for a bench unit
|
|
@@ -234,6 +248,7 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
234
248
|
data: { seat, retryWaveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, firstFailure: ff } });
|
|
235
249
|
} else {
|
|
236
250
|
out.stillDeadNotes.push(retryLegStillDeadNote(seat, ff, leg, unit, counts));
|
|
251
|
+
out.stillDeadRetryLegs.push(leg);
|
|
237
252
|
if (ff && ff.class === 'wave') {
|
|
238
253
|
if (!lostWaveSeats.has(ff.waveId)) { lostWaveSeats.set(ff.waveId, []); }
|
|
239
254
|
lostWaveSeats.get(ff.waveId).push(seat);
|