amicus 4.4.0 → 4.4.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 +32 -0
- package/README.md +3 -1
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +200 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +371 -0
- package/docs/council.md +911 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +55 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +699 -0
- package/electron/fold.js +1 -1
- package/electron/main.js +4 -1
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-ui/live-model.js +12 -1
- package/electron/workspace-ui/md-lite.js +52 -8
- package/electron/workspace-ui/workspace-matrix.js +46 -9
- package/electron/workspace-ui/workspace-panels.js +14 -3
- package/electron/workspace-ui/workspace-render.js +7 -1
- package/electron/workspace-ui/workspace-verbs.js +48 -2
- package/package.json +8 -3
- package/schemas/council-run.schema.json +20 -0
- package/schemas/progress.schema.json +12 -0
- package/schemas/spend.schema.json +52 -4
- package/src/cli-handlers-spend.js +20 -2
- package/src/cli-handlers-watch.js +11 -0
- package/src/cli.js +4 -2
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +24 -1
- package/src/council/findings.js +236 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +64 -11
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +29 -1
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +132 -111
- package/src/council/run-state.js +23 -1
- package/src/council/run.js +44 -46
- package/src/council/tally.js +10 -0
- package/src/headless.js +175 -6
- package/src/observe/council-legs.js +60 -3
- package/src/observe/live-doc.js +18 -1
- package/src/observe/watch-render.js +4 -1
- package/src/sidecar/child-sessions.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +6 -0
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +61 -70
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +8 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/spend-query.js +17 -5
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +39 -1
- package/src/utils/pricing.js +26 -10
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +22 -1
- package/src/workspace/fold-format.js +33 -4
- package/src/workspace/live-normalize.js +28 -15
- package/src/workspace/run-detail.js +7 -1
|
@@ -75,10 +75,88 @@ function ensurePortAvailable(port = DEFAULT_PORT) {
|
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* A start failure that is a LOCK RACE, not a deterministic error.
|
|
80
|
+
*
|
|
81
|
+
* OpenCode opens one shared SQLite database (~/.local/share/opencode/opencode.db)
|
|
82
|
+
* at startup, so two servers starting in the same instant can collide on it and
|
|
83
|
+
* the loser exits 1 with `database is locked`. Measured: council run v441plan01
|
|
84
|
+
* lost four of five seats in 736ms to exactly this.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NARROW. A missing binary, a bad key, a busy port and a config
|
|
87
|
+
* error are all deterministic — retrying them only triples the latency before
|
|
88
|
+
* the same failure, so they must fall straight through untouched.
|
|
89
|
+
*/
|
|
90
|
+
const LOCK_CLASS_START_FAILURE = /database is locked|database table is locked|SQLITE_BUSY/i;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Backoff between start attempts; 5 attempts total, ≤3.75s of added latency.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ WIDENED from 3 attempts at 250/500ms (≤750ms) by v4.4.1 Step 10.5. 750ms is
|
|
96
|
+
* thin against a multi-megabyte WAL and any concurrent process touching the same
|
|
97
|
+
* OpenCode database: run v441plan02 exhausted it, the council degraded to one
|
|
98
|
+
* server per wave, and then lost four of five seats to the very race the retry
|
|
99
|
+
* exists to survive. The wait is bounded and paid at most once per acquisition;
|
|
100
|
+
* the cost of not waiting is a dead bench.
|
|
101
|
+
*
|
|
102
|
+
* Still lock-class ONLY — a deterministic failure (missing binary, bad key, busy
|
|
103
|
+
* port, config error) never sleeps a single millisecond here.
|
|
104
|
+
*/
|
|
105
|
+
const LOCK_RETRY_DELAYS_MS = [250, 500, 1000, 2000];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {Error|null} error
|
|
109
|
+
* @returns {boolean} true only for a lock-class (retryable) start failure
|
|
110
|
+
*/
|
|
111
|
+
function isLockClassStartFailure(error) {
|
|
112
|
+
if (!error) { return false; }
|
|
113
|
+
// The real failure arrives as a message with the server's own stdout inlined
|
|
114
|
+
// ("Server exited with code 1 / Server output: … database is locked"), but
|
|
115
|
+
// check the usual carriers too so a wrapped/spawn-shaped error still matches.
|
|
116
|
+
const carriers = [error.message, error.stderr, error.stdout, error.cause && error.cause.message];
|
|
117
|
+
return carriers.some(c => typeof c === 'string' && LOCK_CLASS_START_FAILURE.test(c));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run `attempt` with a BOUNDED retry on a lock-class failure only.
|
|
122
|
+
*
|
|
123
|
+
* Never fails closed: the final failure is rethrown unchanged, so every caller
|
|
124
|
+
* that already degrades on a start failure (runFanout writes an error wave; a
|
|
125
|
+
* council run falls back to per-wave servers) degrades exactly as before —
|
|
126
|
+
* just later, and far less often. This is the half of the fix that covers what
|
|
127
|
+
* a per-run shared server cannot: two separate `amicus` processes, or a CLI run
|
|
128
|
+
* beside a live MCP server, still contend for the same database.
|
|
129
|
+
*
|
|
130
|
+
* @param {(attempt: number) => Promise<T>} attempt
|
|
131
|
+
* @param {{retryDelayMs?: number}} [opts] retryDelayMs: test seam — collapses
|
|
132
|
+
* every backoff to this value so a retry test does not sleep for real.
|
|
133
|
+
* @returns {Promise<T>}
|
|
134
|
+
* @template T
|
|
135
|
+
*/
|
|
136
|
+
async function retryOnLockRace(attempt, opts = {}) {
|
|
137
|
+
const delays = opts.retryDelayMs === undefined
|
|
138
|
+
? LOCK_RETRY_DELAYS_MS
|
|
139
|
+
: LOCK_RETRY_DELAYS_MS.map(() => opts.retryDelayMs);
|
|
140
|
+
for (let i = 0; ; i += 1) {
|
|
141
|
+
try {
|
|
142
|
+
return await attempt(i);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (i >= delays.length || !isLockClassStartFailure(error)) { throw error; }
|
|
145
|
+
logger.warn('OpenCode server start lost a lock race — retrying', {
|
|
146
|
+
attempt: i + 1, of: delays.length + 1, delayMs: delays[i], error: error.message,
|
|
147
|
+
});
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, delays[i]));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
78
153
|
module.exports = {
|
|
79
154
|
DEFAULT_PORT,
|
|
155
|
+
LOCK_RETRY_DELAYS_MS,
|
|
80
156
|
isPortInUse,
|
|
81
157
|
getPortPid,
|
|
82
158
|
killPortProcess,
|
|
83
|
-
ensurePortAvailable
|
|
159
|
+
ensurePortAvailable,
|
|
160
|
+
isLockClassStartFailure,
|
|
161
|
+
retryOnLockRace
|
|
84
162
|
};
|
|
@@ -45,7 +45,10 @@ const SPEND_LEDGER_FILE = 'spend-ledger.jsonl';
|
|
|
45
45
|
* @param {string} [opts.waveId] present for a fanout leg
|
|
46
46
|
* @param {string} opts.model resolved model id (or alias, if that's all the caller has)
|
|
47
47
|
* @param {'headless'|'interactive'|'leg'} opts.mode
|
|
48
|
-
* @param {{tokens:object, cost:{amount:number|null,currency:string,source:string}
|
|
48
|
+
* @param {{tokens:object, cost:{amount:number|null,currency:string,source:string},
|
|
49
|
+
* subtreeUnknown?:boolean}|null} opts.usage `subtreeUnknown` (v4.4.1 CA-2) is
|
|
50
|
+
* copied onto the row when truthy — this leg's own cost resolved, but a child
|
|
51
|
+
* session it spawned could not be priced, so the row's `cost` is a FLOOR
|
|
49
52
|
* @param {string} [opts.op] 'leg' | 'start' | 'continue' | 'resume'
|
|
50
53
|
* @param {string} [opts.status] terminal status
|
|
51
54
|
* @param {string} [opts.councilRunId] council run id (additive attribution)
|
|
@@ -86,18 +89,36 @@ function appendSpend({ taskId, waveId, model, mode, usage,
|
|
|
86
89
|
if (attempt !== undefined) { row.attempt = attempt; }
|
|
87
90
|
if (substitutedFor !== undefined) { row.substitutedFor = substitutedFor; }
|
|
88
91
|
if (retryOfWaveId !== undefined) { row.retryOfWaveId = retryOfWaveId; }
|
|
92
|
+
// v4.4.1 CA-2: a leg whose OWN cost is known but which spawned a child
|
|
93
|
+
// session the walk could not price writes a PRICED row — so `unpricedRows`
|
|
94
|
+
// never catches it and `amicus spend` reads as a complete measurement while
|
|
95
|
+
// `council run` says `costExact:false` about the same dollars. Omitted (not
|
|
96
|
+
// `|| false`) so a pre-4.4.1 row and an ordinary row stay identical, matching
|
|
97
|
+
// the linkage-field convention above.
|
|
98
|
+
if (usage.subtreeUnknown) { row.subtreeUnknown = true; }
|
|
89
99
|
fs.appendFileSync(path.join(dir, SPEND_LEDGER_FILE), JSON.stringify(row) + '\n');
|
|
90
100
|
} catch (e) {
|
|
91
101
|
logger.debug('spend-ledger append failed (best-effort, run unaffected)', { taskId, error: e.message });
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Read the ledger. Corrupt lines are skipped — and v4.4.1 A2 widens "corrupt"
|
|
107
|
+
* from "does not parse" to "does not parse AS A ROW". A line that is valid JSON
|
|
108
|
+
* but not a plain object (`"foo"`, `42`, `[1,2]`) used to survive `filter(Boolean)`
|
|
109
|
+
* and be treated as a row by every consumer: `aggregateSpend` counted it in
|
|
110
|
+
* `runs`, scored it into `unpricedRows`/`sourceMix.unknown` off its absent cost
|
|
111
|
+
* block, and `--rows` echoed it into a published document that says rows are
|
|
112
|
+
* objects. A scalar in a JSONL ledger of row objects is a corrupt line, so it is
|
|
113
|
+
* now dropped like any other — one fewer way the totals can be inflated by damage.
|
|
114
|
+
* @param {string} [dir] @returns {Array<object>} parsed rows; corrupt lines skipped
|
|
115
|
+
*/
|
|
96
116
|
function readSpendRows(dir) {
|
|
97
117
|
const file = path.join(dir || getConfigDir(), SPEND_LEDGER_FILE);
|
|
98
118
|
if (!fs.existsSync(file)) { return []; }
|
|
99
119
|
return fs.readFileSync(file, 'utf-8').split('\n').map(l => l.trim()).filter(Boolean)
|
|
100
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
120
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
121
|
+
.filter(r => r !== null && typeof r === 'object' && !Array.isArray(r));
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
module.exports = { appendSpend, readSpendRows, SPEND_LEDGER_FILE, SPEND_LEDGER_SCHEMA_VERSION };
|
|
@@ -24,6 +24,9 @@ const FIXED_ARTIFACTS = Object.freeze(['briefing-stage1.md', 'bundle-stage2.md',
|
|
|
24
24
|
// `artifact not allowed: <name>`. Writers: tally-provisional.json = src/council/run.js:199;
|
|
25
25
|
// revote-bundle.md = run-debate.js:119; debate.json = run-debate.js:261; the per-seat
|
|
26
26
|
// rebuttal-/revote- pair = materializeDebate (run-launch.js:127-136).
|
|
27
|
+
// ⚠️ FIVE KINDS, THREE ENTRIES — that is not a miscount (v4.4.1 DOC-7, re-verified). This const
|
|
28
|
+
// holds only the three RUN-LEVEL names; the last two of the five, the rebuttal-/revote- pair, are
|
|
29
|
+
// per BENCH MODEL and are appended inside artifactAllowlist below, next to review-/judge-.
|
|
27
30
|
const DEBATE_ARTIFACTS = Object.freeze(['tally-provisional.json', 'revote-bundle.md', 'debate.json']);
|
|
28
31
|
const MAX_ARTIFACT_BYTES = 200 * 1024;
|
|
29
32
|
|
|
@@ -167,7 +170,25 @@ function readRunArtifact(project, runId, name, deps = {}) {
|
|
|
167
170
|
|
|
168
171
|
let realTarget;
|
|
169
172
|
try { realTarget = realpathSync(path.join(ptr.runDir, name)); }
|
|
170
|
-
catch
|
|
173
|
+
catch (err) {
|
|
174
|
+
// ⚠️ v4.4.1 RN-10: this catch used to answer `not written yet: <name>` for ANY realpath
|
|
175
|
+
// failure — ENOENT, EACCES, EPERM, EIO, ELOOP, a dangling symlink — so a permission problem
|
|
176
|
+
// was indistinguishable from a file the council simply has not produced yet. That is not a
|
|
177
|
+
// cosmetic conflation: electron/ipc-workspace.js's workspace:fold reads chair-output.md
|
|
178
|
+
// through this function, and on a permission error it produced a silent CHAIRLESS fold that
|
|
179
|
+
// still reported {ok: true}. The logger.warn it now emits was the mitigation — but it logged
|
|
180
|
+
// this string, so the log said "not written yet" about a file that was right there.
|
|
181
|
+
//
|
|
182
|
+
// ⚠️ Keep the sanitization. Do NOT re-interpolate `err.message`: a realpath failure's message
|
|
183
|
+
// embeds the full resolved path it tried to open, which round 4 deliberately stopped handing
|
|
184
|
+
// back over IPC (see the run.json catch above). `err.code` is a bare symbolic errno with no
|
|
185
|
+
// path in it, and it is the one piece an operator reading the fold warning actually needs —
|
|
186
|
+
// whitelisted to the errno character class so nothing else can ever ride out through here.
|
|
187
|
+
const code = err && typeof err.code === 'string' && /^[A-Z][A-Z0-9_]{1,15}$/.test(err.code)
|
|
188
|
+
? err.code : 'unknown';
|
|
189
|
+
if (code === 'ENOENT') { return { error: `not written yet: ${name}` }; }
|
|
190
|
+
return { error: `artifact unreadable (${code}): ${name}` };
|
|
191
|
+
}
|
|
171
192
|
if (!isRealpathContained(realDir, realTarget)) {
|
|
172
193
|
return { error: 'artifact escapes run directory' };
|
|
173
194
|
}
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* Council Workspace — fold payload builder (v4.4 §7).
|
|
3
3
|
*
|
|
4
4
|
* MIRRORS the shipped fold-header builder `formatFoldOutput`
|
|
5
|
-
* (src/headless.js
|
|
6
|
-
* — byte-for-byte the same
|
|
7
|
-
*
|
|
5
|
+
* (src/headless.js `formatFoldOutput`, exported from src/headless.js and
|
|
6
|
+
* re-exported from src/index.js) — byte-for-byte the same **7-line** head:
|
|
7
|
+
* marker, Model, Session, Client, CWD, Mode, `---`. ⚠️ v4.4.1 DOC-5: this said
|
|
8
|
+
* "8-line" for two releases. Line 8 (`VERDICT:`) is council's OWN addition and
|
|
9
|
+
* has no counterpart in formatFoldOutput, whose 8th element is the summary
|
|
10
|
+
* body. Only the first 7 lines are the shared contract; anyone changing the
|
|
11
|
+
* shared format must sync those and leave `VERDICT:` alone.
|
|
12
|
+
* src/headless.js is the SOURCE OF TRUTH; keep the head in sync with it.
|
|
13
|
+
* (Line numbers deliberately omitted — the previous `:775-789`/`:797` citation
|
|
14
|
+
* had drifted by ~500 lines.) The duplication is deliberate:
|
|
8
15
|
* requiring headless.js transitively pulls opencode-client / progress /
|
|
9
16
|
* conversation-mirror at require time (src/headless.js:8-17), which
|
|
10
17
|
* src/workspace/ must stay free of.
|
|
@@ -79,7 +86,29 @@ function buildFoldText(o) {
|
|
|
79
86
|
} else {
|
|
80
87
|
head.push(`Run: ${run.status || 'unknown'} — ${stageSummary(run)}`);
|
|
81
88
|
}
|
|
82
|
-
|
|
89
|
+
// ⚠️ v4.4.1 DOC-5: this used to append ` (${cost.source})` for EVERY source,
|
|
90
|
+
// on top of a glyph formatCost had already spent on the same fact —
|
|
91
|
+
// `~$0.0100 (estimated)`, `? (unknown)`. formatCost (src/utils/pricing.js)
|
|
92
|
+
// prefixes `~` for both 'estimated' and 'mixed', and returns a bare `?` when
|
|
93
|
+
// the source is 'unknown'. So for 'estimated' and 'unknown' the word was pure
|
|
94
|
+
// repetition and is gone.
|
|
95
|
+
//
|
|
96
|
+
// Two sources still spell themselves out, because the glyph vocabulary cannot
|
|
97
|
+
// express them:
|
|
98
|
+
// - `reported` — a plain `$0.4321` is also what an unrecognised/absent
|
|
99
|
+
// source renders as, so the absence of a glyph cannot mean "exact".
|
|
100
|
+
// - `mixed` — `~` encodes *inexact*, not *which kind of inexact*. Collapsing
|
|
101
|
+
// 'mixed' into a bare `~$…` makes it indistinguishable from 'estimated',
|
|
102
|
+
// and the two are not the same claim: 'mixed' means some legs reported
|
|
103
|
+
// real usage and some were estimated, i.e. part of this number is
|
|
104
|
+
// measured. That is strictly more information than 'estimated', and this
|
|
105
|
+
// is the fold line of a release whose theme is cost truthfulness.
|
|
106
|
+
// Each thing is still said exactly once — the `~` says "inexact", the word
|
|
107
|
+
// says "which kind", and neither restates the other.
|
|
108
|
+
const costSourceLabel = cost && (cost.source === 'reported' || cost.source === 'mixed')
|
|
109
|
+
? ` (${cost.source})`
|
|
110
|
+
: '';
|
|
111
|
+
head.push(`Cost: ${formatCost(cost)}${costSourceLabel}`);
|
|
83
112
|
|
|
84
113
|
// Review follow-up #1: strip FIRST, then test emptiness on the STRIPPED
|
|
85
114
|
// result — not the raw one. A chair body consisting solely of a marker
|
|
@@ -20,10 +20,14 @@
|
|
|
20
20
|
const { formatCost } = require('../utils/pricing');
|
|
21
21
|
const { TERMINAL_STATUSES } = require('./run-detail');
|
|
22
22
|
|
|
23
|
+
// ⚠️ v4.4.1 RN-7: the `doc.wave.legs` fallback that used to sit here is GONE, deliberately.
|
|
24
|
+
// No producer nests legs under `wave` — the WAVE composed doc (src/mcp-server.js:592-662)
|
|
25
|
+
// carries a TOP-LEVEL `legs`, exactly like the council one, and this file's header already
|
|
26
|
+
// says not to model the wave doc at all. The arm asserted a shape that does not exist, and
|
|
27
|
+
// wsgate01's reviewer believed it. If a nested shape ever appears, add it back WITH a
|
|
28
|
+
// producer to point at.
|
|
23
29
|
function legRowsOf(doc) {
|
|
24
|
-
|
|
25
|
-
if (doc.wave && Array.isArray(doc.wave.legs)) { return doc.wave.legs; }
|
|
26
|
-
return [];
|
|
30
|
+
return Array.isArray(doc.legs) ? doc.legs : [];
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
function numOrNull(v) { return typeof v === 'number' ? v : null; }
|
|
@@ -32,7 +36,9 @@ function seatOf(leg) {
|
|
|
32
36
|
const usage = leg.usage || null;
|
|
33
37
|
const tokens = usage && usage.tokens ? usage.tokens : null;
|
|
34
38
|
return {
|
|
35
|
-
|
|
39
|
+
// ⚠️ v4.4.1 RN-7: `leg.legId` was a second dead fallback here — no leg row, council or
|
|
40
|
+
// wave, has ever carried it (src/observe/council-legs.js stamps `taskId`). Deleted.
|
|
41
|
+
id: leg.taskId || null,
|
|
36
42
|
// ⚠️ DE-ROT (F34/F36): `model` and `modelInput` are TWO SEPARATE fields, never collapsed.
|
|
37
43
|
// A live leg's `model` is the resolved executable id (e.g. `google/gemini-2.5`); `modelInput`
|
|
38
44
|
// is the council ALIAS (e.g. `gemini`) that run.json's labelMap and blind mode's labelFor()
|
|
@@ -108,19 +114,26 @@ function normalizeLive(doc) {
|
|
|
108
114
|
view: doc.view || null,
|
|
109
115
|
runId: doc.runId || doc.taskId || null,
|
|
110
116
|
status,
|
|
111
|
-
// ⚠️ PRE-FLIGHT
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
117
|
+
// ⚠️ v4.4.1 RN-7 (was PRE-FLIGHT P6): the `|| doc.currentStage` fallback is GONE. P6 had
|
|
118
|
+
// already corrected it once — from the phantom `doc.stage` to the real `currentStage` — and
|
|
119
|
+
// its own comment conceded the arm was "harmless today only by coincidence". It is in fact
|
|
120
|
+
// strictly unreachable: buildCouncilStatusPayload writes `stages` as `(run.stages || []).map(…)`
|
|
121
|
+
// (always an array) and derives `currentStage` from the SAME
|
|
122
|
+
// `stages.find(s => s.status === 'running')` predicate `active` uses one line up
|
|
123
|
+
// (src/mcp-council-awareness.js:155-157), so `active` is null in exactly the cases
|
|
124
|
+
// `currentStage` is too. Keeping it asserted a divergence between the two fields that no
|
|
125
|
+
// producer can create.
|
|
126
|
+
stageName: active ? active.name : null,
|
|
118
127
|
stages,
|
|
119
128
|
seats: legRowsOf(doc).map(seatOf),
|
|
120
|
-
//
|
|
121
|
-
// fallback readout if a seat row
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
// ⚠️ v4.4.1 RN-8 (D1 ruling: delete the promise). `legsTotal`/`legsComplete` used to be
|
|
130
|
+
// mapped here under a comment promising them as "the honest fallback readout if a seat row
|
|
131
|
+
// is ever unavailable" — a UI fallback nobody ever wired. Nothing in electron/workspace-ui/
|
|
132
|
+
// read either field. Documenting a feature that does not exist is worse than not having it,
|
|
133
|
+
// so the fields and the promise are both gone. ⚠️ The SAME names on the composed doc
|
|
134
|
+
// (src/mcp-council-awareness.js) and in src/cli-handlers-status.js / src/mcp-wait.js ARE
|
|
135
|
+
// consumed — this deletion is scoped to the workspace's LiveModel only. If a future task
|
|
136
|
+
// wants the counters in the GUI, re-add them WITH the renderer that paints them.
|
|
124
137
|
// ⚠️ DE-ROT (F39): these two are ACTIVE-STAGE spend, not the run total.
|
|
125
138
|
// buildCouncilStatusPayload rolls up only the legs of the currently-RUNNING stage's sub-waves
|
|
126
139
|
// (mcp-council-awareness.js:136-146) and omits `usage` entirely until one of those legs flushes
|
|
@@ -23,7 +23,13 @@ const { isRealpathContained } = require('../utils/path-fence');
|
|
|
23
23
|
* ("zero v4.3") and live-doc.js is v4.3. Not to be confused with the shipped
|
|
24
24
|
* src/utils/result-schema.js:13 TERMINAL_STATUSES, which is the LEG set (no 'partial').
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
// ⚠️ v4.4.1 A1: 'timed-out' added alongside 'timeout' — see the long note at
|
|
27
|
+
// src/observe/live-doc.js:18 for why both spellings are real and which producer writes each.
|
|
28
|
+
// Council run.json only ever carries aborted|complete|error|partial (run-finalize.js:38
|
|
29
|
+
// statusForExit), so the new name is inert for THIS module's own reads; it is carried anyway
|
|
30
|
+
// because the drift pin demands byte-identity with the source list, and that pin is the only
|
|
31
|
+
// thing keeping the three copies honest.
|
|
32
|
+
const TERMINAL_STATUSES = ['complete', 'partial', 'error', 'crashed', 'aborted', 'timeout', 'timed-out', 'idle-timeout'];
|
|
27
33
|
|
|
28
34
|
/** Friendly labels for known v4.0 stage names; unknown names pass through raw
|
|
29
35
|
* — the graceful-when-present rule (spec §5.2). */
|