amicus 4.3.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 +64 -0
- package/README.md +6 -3
- 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/ipc-workspace.js +283 -0
- package/electron/main.js +31 -1
- package/electron/preload-workspace.js +40 -0
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-shell.js +85 -0
- package/electron/workspace-ui/index.html +111 -0
- package/electron/workspace-ui/live-model.js +112 -0
- package/electron/workspace-ui/md-lite.js +163 -0
- package/electron/workspace-ui/workspace-app.js +240 -0
- package/electron/workspace-ui/workspace-matrix.js +249 -0
- package/electron/workspace-ui/workspace-panels.js +237 -0
- package/electron/workspace-ui/workspace-render.js +277 -0
- package/electron/workspace-ui/workspace-verbs.js +293 -0
- package/electron/workspace-ui/workspace.css +172 -0
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +25 -1
- package/schemas/council-run.schema.json +34 -0
- package/schemas/progress.schema.json +26 -1
- package/schemas/spend.schema.json +52 -4
- package/skills/second-opinion/MODEL-NOTES.md +53 -5
- package/src/cli-handlers-council-run.js +25 -3
- package/src/cli-handlers-spend.js +50 -5
- package/src/cli-handlers-watch.js +48 -10
- 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 +59 -3
- 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 +277 -0
- 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 +73 -7
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +148 -113
- package/src/council/run-state.js +23 -1
- package/src/council/run.js +52 -53
- package/src/council/tally.js +10 -0
- package/src/headless.js +519 -17
- package/src/mcp-council-awareness.js +53 -3
- package/src/observe/council-legs.js +240 -0
- package/src/observe/live-doc.js +39 -4
- package/src/observe/watch-render.js +23 -1
- package/src/opencode-client.js +15 -3
- package/src/sidecar/child-sessions.js +197 -0
- package/src/sidecar/conversation-mirror.js +111 -37
- package/src/sidecar/fanout-budget.js +71 -0
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +29 -1
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +65 -81
- 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/sidecar/tool-part.js +196 -0
- package/src/sidecar/workspace-window.js +62 -0
- package/src/spend-query.js +33 -6
- package/src/utils/env-num.js +42 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +120 -0
- package/src/utils/pricing.js +114 -9
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +208 -0
- package/src/workspace/blind-mode.js +32 -0
- package/src/workspace/fold-format.js +124 -0
- package/src/workspace/live-normalize.js +169 -0
- package/src/workspace/matrix-model.js +94 -0
- package/src/workspace/run-detail.js +229 -0
- package/src/workspace/run-scan.js +148 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council Workspace — pure renderer-side view logic (poll cadence, seat row
|
|
3
|
+
* cells, blind default). No DOM here: node-tested via the module guard.
|
|
4
|
+
* TERMINAL_STATUSES mirrors src/workspace/run-detail.js (the renderer cannot
|
|
5
|
+
* require across the sandbox; keep the two lists in sync).
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ⚠️ DE-ROT (F26): was a 5-name list — missing 'crashed' and 'idle-timeout', so a crashed
|
|
11
|
+
// or idle-timed-out run would poll forever and never flip to its terminal rendering.
|
|
12
|
+
// Must stay byte-identical to src/workspace/run-detail.js TERMINAL_STATUSES, which itself
|
|
13
|
+
// mirrors the shipped src/observe/live-doc.js TERMINAL set. A drift pin asserts this.
|
|
14
|
+
// ⚠️ v4.4.1 A1: 'timed-out' added alongside 'timeout'. Both spellings are real and are written
|
|
15
|
+
// by different producers — see src/observe/live-doc.js:18. Inert for the workspace (a council
|
|
16
|
+
// run.json's status vocabulary is aborted|complete|error|partial), carried for byte-identity.
|
|
17
|
+
var TERMINAL_STATUSES = ['complete', 'partial', 'error', 'crashed', 'aborted', 'timeout', 'timed-out', 'idle-timeout'];
|
|
18
|
+
|
|
19
|
+
// ⚠️ DE-ROT (F41): STAGE_LABELS must be mirrored here too. The live loop labels stages that
|
|
20
|
+
// START AFTER the run was opened, and those names are absent from the frozen derived.stageRail,
|
|
21
|
+
// so without this table every post-open stage renders its raw name ("tally-final", "debate-revote").
|
|
22
|
+
var STAGE_LABELS = {
|
|
23
|
+
stage1: 'Stage 1 — independent review',
|
|
24
|
+
stage2: 'Stage 2 — peer cross-review',
|
|
25
|
+
'debate-defense': 'Debate — defense',
|
|
26
|
+
'debate-revote': 'Debate — re-vote',
|
|
27
|
+
'tally-provisional': 'Tally (provisional)',
|
|
28
|
+
tally: 'Tally',
|
|
29
|
+
'tally-final': 'Tally (final)',
|
|
30
|
+
chair: 'Chair synthesis',
|
|
31
|
+
verdict: 'Verdict',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Poll cadence per spec §4.3: 1.5s visible+focused, 5s otherwise, stop at terminal. */
|
|
35
|
+
function pollDelay(state) {
|
|
36
|
+
if (state.terminal) { return null; }
|
|
37
|
+
return (state.visible && state.focused) ? 1500 : 5000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Blind default per spec resolved Q2: ON live, OFF terminal. */
|
|
41
|
+
function defaultBlind(status) {
|
|
42
|
+
return TERMINAL_STATUSES.indexOf(status) === -1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dash(v) {
|
|
46
|
+
return (v === null || v === undefined || v === '') ? '—' : String(v);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Display cells for one seat row, in the seats-table column order:
|
|
51
|
+
* [name, role, status, stage, msgs, tokens in/out, cost, last activity, stalled]
|
|
52
|
+
*/
|
|
53
|
+
function seatCells(seat, blindOn, labelOf) {
|
|
54
|
+
// ⚠️ DE-ROT (F36): key the lookup on the council ALIAS, never on `seat.model`. labelMap's
|
|
55
|
+
// values are aliases; a LIVE leg's `model` is the resolved executable id, so labelOf(model)
|
|
56
|
+
// misses and Blind ON renders the real model. `modelInput` is the alias Task 0.5 stamps per
|
|
57
|
+
// leg (terminal rows from runStats are already alias-only, so the fallback is exact there).
|
|
58
|
+
var alias = seat.modelInput || seat.model;
|
|
59
|
+
var label = labelOf ? labelOf(alias) : null;
|
|
60
|
+
// ⚠️ v4.4.1 RN-9: this hand-rolled flip is LOAD-BEARING — do NOT "fix" it into
|
|
61
|
+
// AmicusRender.display({model: seat.model, label: label}, blindOn). The other two copies
|
|
62
|
+
// (workspace-panels.js's review/judge titles) were routed through display() because their
|
|
63
|
+
// pair's `model` IS the identity they must print. Here it is not: the seat's printable
|
|
64
|
+
// identity is the council ALIAS resolved on the line above (F36 — a live leg's `seat.model`
|
|
65
|
+
// is the RESOLVED executable id, e.g. `google/gemini-2.5-pro`, which labelMap never keys on),
|
|
66
|
+
// so display()'s blind-OFF arm would print the resolved id and undo F36. This module is also
|
|
67
|
+
// node-tested with NO DOM and no window.AmicusRender to call. Deliberate third copy.
|
|
68
|
+
var name = blindOn && label ? label : alias;
|
|
69
|
+
var tokens = (seat.tokensIn === null || seat.tokensIn === undefined) &&
|
|
70
|
+
(seat.tokensOut === null || seat.tokensOut === undefined)
|
|
71
|
+
? '—'
|
|
72
|
+
: dash(seat.tokensIn) + '/' + dash(seat.tokensOut);
|
|
73
|
+
return [
|
|
74
|
+
dash(name),
|
|
75
|
+
dash(seat.role),
|
|
76
|
+
dash(seat.status),
|
|
77
|
+
dash(seat.stage),
|
|
78
|
+
dash(seat.messages),
|
|
79
|
+
tokens,
|
|
80
|
+
dash(seat.costDisplay),
|
|
81
|
+
// ⚠️ DE-ROT (F35): pass-through of an ISO timestamp, NOT a relative string. renderSeats
|
|
82
|
+
// formats it with relTime() before calling in — live-model.js is node-tested and loads
|
|
83
|
+
// before workspace-render.js, so it cannot reach relTime itself.
|
|
84
|
+
dash(seat.lastActivity),
|
|
85
|
+
seat.stalled ? '⏳ stalled' : '',
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
90
|
+
function seatsFromRunStats(costRows) {
|
|
91
|
+
return (costRows || []).map(function (r) {
|
|
92
|
+
return {
|
|
93
|
+
// ⚠️ DE-ROT (F37): composite id — a v4.1 `--debate` run emits extra runStats rows for
|
|
94
|
+
// the SAME bench alias (role 'rebuttal'/'revote', src/council/debate.js:88-96). With no
|
|
95
|
+
// id, renderSeats keys on model and the re-vote row silently overwrites the seat row.
|
|
96
|
+
// (If the panel is meant to be bench-only, filter to seat/critic/lens:* instead and
|
|
97
|
+
// leave rebuttal/revote spend to the cost table — but say which; do not leave it implied.)
|
|
98
|
+
id: r.model + ':' + (r.role || 'seat'),
|
|
99
|
+
model: r.model, role: r.role || null, status: r.status || null,
|
|
100
|
+
stage: null, messages: null, tokensIn: null, tokensOut: null,
|
|
101
|
+
costDisplay: r.costDisplay || null, lastActivity: null, latestPreview: null,
|
|
102
|
+
stalled: false,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ⚠️ DE-ROT (F41): STAGE_LABELS is exported so applyLive() can label post-open stages.
|
|
108
|
+
var api = { pollDelay: pollDelay, seatCells: seatCells, seatsFromRunStats: seatsFromRunStats,
|
|
109
|
+
defaultBlind: defaultBlind, dash: dash, TERMINAL_STATUSES: TERMINAL_STATUSES, STAGE_LABELS: STAGE_LABELS };
|
|
110
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
111
|
+
if (typeof window !== 'undefined') { window.AmicusLive = api; }
|
|
112
|
+
})();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-lite — dependency-free renderer for untrusted model prose
|
|
3
|
+
* (v4.4 §4.2, resolved Q7). Supported: #–#### headings, - / * bullets,
|
|
4
|
+
* numbered items, ``` fences, `inline code`, paragraphs. EVERY string lands
|
|
5
|
+
* in the DOM via textContent / createTextNode — no HTML-string injection, no
|
|
6
|
+
* HTML passthrough, no links. (⚠️ DE-ROT F32: wording avoids the banned token —
|
|
7
|
+
* see the note above the block.) The parser is pure (node-tested); the applier takes
|
|
8
|
+
* an explicit document. Loaded as a plain script (window.AmicusMd) and as a
|
|
9
|
+
* CommonJS module (jest).
|
|
10
|
+
*/
|
|
11
|
+
(function () {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
var H_RE = /^(#{1,4})\s+(.*)$/;
|
|
15
|
+
var UL_RE = /^\s*[-*]\s+/;
|
|
16
|
+
var OL_RE = /^\s*\d+[.)]\s+/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split one line into literal / inline-code segments in a SINGLE linear pass.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ v4.4.1 A1/D1 (Confirmed 4/4), with its SEVERITY CLAIM CORRECTED. The
|
|
22
|
+
* previous form re-`exec`ed a freshly sliced `rest` each iteration
|
|
23
|
+
* (`rest = rest.slice(...)`) — quadratic as WRITTEN, but not as it ran. V8's
|
|
24
|
+
* `String.prototype.slice` returns a SlicedString (parent pointer + offset),
|
|
25
|
+
* not a copy, so the old code measured ~8 ms at artifact-guard.js's 200 KB cap
|
|
26
|
+
* and scaled LINEARLY (~2x per doubling out to 2.4 MB). A deliberately-copying
|
|
27
|
+
* control took 78,232 ms at that same 200 KB — that is the freeze the finding
|
|
28
|
+
* described, and it is not what shipped. Numbers:
|
|
29
|
+
* `.superpowers/sdd/task-10-report.md:127-148`.
|
|
30
|
+
*
|
|
31
|
+
* What the rewrite removes is therefore a silent dependence on an undocumented
|
|
32
|
+
* engine optimisation that nothing here states or tests — a latent PORTABILITY
|
|
33
|
+
* hazard on any engine without sliced strings — and not a live performance
|
|
34
|
+
* defect or an exploitable-DoS-that-was. The `lastIndex` cursor walks the
|
|
35
|
+
* ORIGINAL string once and never copies a tail, so linearity is now a property
|
|
36
|
+
* of the algorithm rather than of the engine.
|
|
37
|
+
*
|
|
38
|
+
* Output is identical to the old function for every input: the pattern is
|
|
39
|
+
* context-free — no `^`, `\b`, lookaround or backreference — so a /g scan
|
|
40
|
+
* resuming at `lastIndex` lands on exactly the same match positions that
|
|
41
|
+
* re-`exec`ing the remainder did. The three boundary cases match too: no
|
|
42
|
+
* match at all yields one literal segment, a trailing match yields no empty
|
|
43
|
+
* tail segment, and empty input yields [].
|
|
44
|
+
*
|
|
45
|
+
* The regex is constructed per call and deliberately NOT hoisted to module
|
|
46
|
+
* scope: a /g regex carries mutable `lastIndex`, so one shared instance would
|
|
47
|
+
* leak cursor state between calls and silently drop spans.
|
|
48
|
+
*/
|
|
49
|
+
function parseInline(text) {
|
|
50
|
+
var out = [];
|
|
51
|
+
var s = String(text);
|
|
52
|
+
var re = /`([^`\n]+)`/g;
|
|
53
|
+
var pos = 0;
|
|
54
|
+
var m;
|
|
55
|
+
while ((m = re.exec(s)) !== null) {
|
|
56
|
+
if (m.index > pos) { out.push({ code: false, text: s.slice(pos, m.index) }); }
|
|
57
|
+
out.push({ code: true, text: m[1] });
|
|
58
|
+
pos = re.lastIndex;
|
|
59
|
+
}
|
|
60
|
+
if (pos < s.length) { out.push({ code: false, text: s.slice(pos) }); }
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseMdLite(text) {
|
|
65
|
+
var lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
66
|
+
var blocks = [];
|
|
67
|
+
var i = 0;
|
|
68
|
+
while (i < lines.length) {
|
|
69
|
+
var line = lines[i];
|
|
70
|
+
if (/^```/.test(line)) {
|
|
71
|
+
var buf = [];
|
|
72
|
+
i += 1;
|
|
73
|
+
while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i += 1; }
|
|
74
|
+
i += 1; // closing fence (or EOF)
|
|
75
|
+
blocks.push({ t: 'code', text: buf.join('\n') });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
var h = H_RE.exec(line);
|
|
79
|
+
// ⚠️ v4.4.1 D3: trim the heading text. H_RE's `\s+` eats the run of
|
|
80
|
+
// whitespace after the hashes, but `(.*)$` keeps everything to end of
|
|
81
|
+
// line — so `# Title ` rendered a heading with trailing blanks baked
|
|
82
|
+
// into its text node.
|
|
83
|
+
if (h) { blocks.push({ t: 'h', level: h[1].length, text: h[2].trim() }); i += 1; continue; }
|
|
84
|
+
if (UL_RE.test(line)) {
|
|
85
|
+
var ul = [];
|
|
86
|
+
while (i < lines.length && UL_RE.test(lines[i])) { ul.push(lines[i].replace(UL_RE, '')); i += 1; }
|
|
87
|
+
blocks.push({ t: 'ul', items: ul });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (OL_RE.test(line)) {
|
|
91
|
+
var ol = [];
|
|
92
|
+
while (i < lines.length && OL_RE.test(lines[i])) { ol.push(lines[i].replace(OL_RE, '')); i += 1; }
|
|
93
|
+
blocks.push({ t: 'ol', items: ol });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!line.trim()) { i += 1; continue; }
|
|
97
|
+
var p = [line];
|
|
98
|
+
i += 1;
|
|
99
|
+
while (i < lines.length && lines[i].trim() &&
|
|
100
|
+
!/^```/.test(lines[i]) && !H_RE.test(lines[i]) &&
|
|
101
|
+
!UL_RE.test(lines[i]) && !OL_RE.test(lines[i])) {
|
|
102
|
+
p.push(lines[i]);
|
|
103
|
+
i += 1;
|
|
104
|
+
}
|
|
105
|
+
blocks.push({ t: 'p', text: p.join(' ') });
|
|
106
|
+
}
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyInline(el, segs, doc) {
|
|
111
|
+
for (var i = 0; i < segs.length; i++) {
|
|
112
|
+
if (segs[i].code) {
|
|
113
|
+
var c = doc.createElement('code');
|
|
114
|
+
c.textContent = segs[i].text;
|
|
115
|
+
el.appendChild(c);
|
|
116
|
+
} else {
|
|
117
|
+
el.appendChild(doc.createTextNode(segs[i].text));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Render prose into container. Panels use h3–h6 so page structure wins. */
|
|
123
|
+
function renderMdLite(container, text, doc) {
|
|
124
|
+
var d = doc || document;
|
|
125
|
+
container.textContent = '';
|
|
126
|
+
var blocks = parseMdLite(text);
|
|
127
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
128
|
+
var b = blocks[i];
|
|
129
|
+
var el;
|
|
130
|
+
if (b.t === 'h') {
|
|
131
|
+
// ⚠️ v4.4.1 D4: the `Math.min(6, …)` that used to wrap this was
|
|
132
|
+
// unreachable — H_RE's `#{1,4}` bounds level to 1–4, so the tag is
|
|
133
|
+
// always h3–h6 and the clamp could never fire. Dead defensive code is
|
|
134
|
+
// worse than none here: it made the h6 ceiling look enforced when the
|
|
135
|
+
// real guarantee lives in H_RE. If that `#{1,4}` is ever widened, THIS
|
|
136
|
+
// line must widen with it — h7 is not an element. The
|
|
137
|
+
// `'#'.repeat(4) + ' X'` → h6 test pins the true boundary.
|
|
138
|
+
el = d.createElement('h' + (b.level + 2));
|
|
139
|
+
applyInline(el, parseInline(b.text), d);
|
|
140
|
+
} else if (b.t === 'code') {
|
|
141
|
+
el = d.createElement('pre');
|
|
142
|
+
var code = d.createElement('code');
|
|
143
|
+
code.textContent = b.text;
|
|
144
|
+
el.appendChild(code);
|
|
145
|
+
} else if (b.t === 'ul' || b.t === 'ol') {
|
|
146
|
+
el = d.createElement(b.t);
|
|
147
|
+
for (var j = 0; j < b.items.length; j++) {
|
|
148
|
+
var li = d.createElement('li');
|
|
149
|
+
applyInline(li, parseInline(b.items[j]), d);
|
|
150
|
+
el.appendChild(li);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
el = d.createElement('p');
|
|
154
|
+
applyInline(el, parseInline(b.text), d);
|
|
155
|
+
}
|
|
156
|
+
container.appendChild(el);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var api = { parseMdLite: parseMdLite, parseInline: parseInline, renderMdLite: renderMdLite };
|
|
161
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
162
|
+
if (typeof window !== 'undefined') { window.AmicusMd = api; }
|
|
163
|
+
})();
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council Workspace — application state + wiring (v4.4 §5).
|
|
3
|
+
* Historical rendering in this task; Task 15 adds the live poll loop into
|
|
4
|
+
* the seams marked LIVE; Task 16/17 flip the Abort button + confirm dialog.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ DE-ROT (F05) / PRE-FLIGHT (P2): this file is the last of the three-way
|
|
7
|
+
* split (workspace-app.js / workspace-panels.js / workspace-verbs.js). It
|
|
8
|
+
* loads LAST (md-lite → live-model → workspace-render → workspace-matrix →
|
|
9
|
+
* workspace-panels → workspace-verbs → workspace-app), so panels/verbs
|
|
10
|
+
* already exist and may be captured once, here, at load time.
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
var P = window.AmicusPanels;
|
|
16
|
+
var V = window.AmicusVerbs;
|
|
17
|
+
|
|
18
|
+
var invoke = function (ch) {
|
|
19
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
20
|
+
return window.amicusWorkspace.invoke.apply(null, [ch].concat(args));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
var state = {
|
|
24
|
+
runId: null,
|
|
25
|
+
detail: null,
|
|
26
|
+
debate: null, // ⚠️ DE-ROT (F38): parsed debate.json, fetched once per run-open; stays
|
|
27
|
+
// null on a non-debate run, an aborted/skipped debate, or a parse failure —
|
|
28
|
+
// drillIntoJudge's judge-*.md fallback covers all three.
|
|
29
|
+
blind: false,
|
|
30
|
+
labelByModel: {},
|
|
31
|
+
listTimer: null,
|
|
32
|
+
liveTimer: null, // LIVE (Task 15)
|
|
33
|
+
liveEpoch: 0, // LIVE (Task 15): bumped by stopLiveLoop() to invalidate in-flight ticks
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function labelOf(model) { return state.labelByModel[model] || null; }
|
|
37
|
+
function isBlind() { return state.blind; }
|
|
38
|
+
|
|
39
|
+
function $(id) { return document.getElementById(id); }
|
|
40
|
+
|
|
41
|
+
// ---- run list ----------------------------------------------------------
|
|
42
|
+
function refreshList() {
|
|
43
|
+
return invoke('workspace:list-runs').then(function (rows) {
|
|
44
|
+
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): thread the
|
|
45
|
+
// CURRENTLY-open run's blind state + label lookup through. labelOf only resolves a
|
|
46
|
+
// label for models that belong to the currently open run, so other rows' chair chips
|
|
47
|
+
// degrade gracefully to the raw id — see renderRunList's own note in workspace-render.js.
|
|
48
|
+
window.AmicusRender.renderRunList($('run-list'), rows, state.runId, openRun, state.blind, labelOf);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function startListLoop() {
|
|
53
|
+
if (state.listTimer) { clearInterval(state.listTimer); }
|
|
54
|
+
state.listTimer = setInterval(refreshList, 5000); // spec §4.3
|
|
55
|
+
window.addEventListener('focus', refreshList);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- run detail --------------------------------------------------------
|
|
59
|
+
function openRun(runId) {
|
|
60
|
+
state.runId = runId;
|
|
61
|
+
state.debate = null;
|
|
62
|
+
return invoke('workspace:get-run', runId).then(function (detail) {
|
|
63
|
+
state.detail = detail;
|
|
64
|
+
// ⚠️ DE-ROT (F38): debate.json is the re-vote index the matrix drill-in needs — fetched
|
|
65
|
+
// once per run-open (never per render), fire-and-forget. An aborted or cost-ceiling
|
|
66
|
+
// -skipped debate never writes it; a parse failure leaves state.debate null either way,
|
|
67
|
+
// which drillIntoJudge treats as "no re-vote for this (judge, id)" and falls back to
|
|
68
|
+
// today's judge-*.md path.
|
|
69
|
+
// ⚠️ CODE REVIEW (round 2, finding 1): this fire-and-forget fetch is exactly the F09
|
|
70
|
+
// class of bug if left unguarded — a stale response from a run the user has since
|
|
71
|
+
// navigated away from must never overwrite the run now open. Capture `runId` and check
|
|
72
|
+
// it's still `state.runId` before writing; a rejection (dead channel, closed window) is
|
|
73
|
+
// caught too, so it never surfaces as an unhandled rejection in the renderer.
|
|
74
|
+
if (detail && detail.run && detail.run.debate) {
|
|
75
|
+
invoke('workspace:read-artifact', runId, 'debate.json').then(function (res) {
|
|
76
|
+
if (state.runId !== runId) { return; }
|
|
77
|
+
try { state.debate = JSON.parse(res.text); } catch (err) { state.debate = null; }
|
|
78
|
+
}).catch(function () {
|
|
79
|
+
if (state.runId !== runId) { return; }
|
|
80
|
+
state.debate = null;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
renderDetail();
|
|
84
|
+
refreshList();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderDetail() {
|
|
89
|
+
var d = state.detail;
|
|
90
|
+
$('empty-state').hidden = true;
|
|
91
|
+
$('run-view').hidden = false;
|
|
92
|
+
var R = window.AmicusRender;
|
|
93
|
+
|
|
94
|
+
if (!d || d.error || !d.derived) {
|
|
95
|
+
$('run-title').textContent = (d && d.runId) || 'run';
|
|
96
|
+
// ⚠️ Fix-wave item 1: an unreadable run has no derived model behind it, so the Abort
|
|
97
|
+
// button — otherwise left however the PREVIOUS (possibly live, non-terminal) run set it
|
|
98
|
+
// — must not stay enabled here pointed at a run whose detail can't back an abort.
|
|
99
|
+
$('abort-btn').hidden = true;
|
|
100
|
+
R.renderBanner($('banner'),
|
|
101
|
+
'Run unreadable: ' + ((d && d.error) || (d && d.run && d.run.parseError) || 'unknown') +
|
|
102
|
+
(d && d.runDir ? ' — ' + d.runDir : ''), '');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// blind default: computed ONCE per run-open from status (resolution 9)
|
|
107
|
+
state.blind = window.AmicusLive.defaultBlind(d.run.status);
|
|
108
|
+
$('blind-toggle').checked = state.blind;
|
|
109
|
+
state.labelByModel = {};
|
|
110
|
+
d.derived.names.forEach(function (p) { state.labelByModel[p.model] = p.label; });
|
|
111
|
+
|
|
112
|
+
$('run-title').textContent = d.runId;
|
|
113
|
+
R.renderHeaderChips($('run-chips'), d.run, state.blind, labelOf);
|
|
114
|
+
// v4.4 §8: 6th arg = costExact. A run with an unpriced seat draws an
|
|
115
|
+
// indeterminate gauge and a `≥` readout instead of a confident percentage.
|
|
116
|
+
R.renderGauge($('cost-gauge-fill'), $('cost-gauge-text'),
|
|
117
|
+
d.derived.cost.costAmount, d.derived.cost.maxCost, d.derived.cost.totalDisplay,
|
|
118
|
+
d.derived.cost.costExact !== false);
|
|
119
|
+
R.renderStageRail($('stage-rail'), d.derived.stageRail);
|
|
120
|
+
|
|
121
|
+
renderBanners();
|
|
122
|
+
P.renderSeatsPanel();
|
|
123
|
+
P.renderMatrixPanel();
|
|
124
|
+
P.renderVerdictPanel();
|
|
125
|
+
R.renderCost($('cost-body'), d.derived.cost, state.blind, labelOf);
|
|
126
|
+
P.wireLazyPanels();
|
|
127
|
+
var isTerminal = window.AmicusLive.TERMINAL_STATUSES.indexOf(d.run.status) !== -1;
|
|
128
|
+
$('abort-btn').hidden = isTerminal;
|
|
129
|
+
V.startLiveLoop();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderBanners() {
|
|
133
|
+
var d = state.detail;
|
|
134
|
+
var R = window.AmicusRender;
|
|
135
|
+
if (!d.derived.schemaSupported) {
|
|
136
|
+
R.renderBanner($('banner'), 'This run was written by a different amicus version (schemaVersion ' +
|
|
137
|
+
d.run.schemaVersion + ') — artifacts: ' + d.runDir, 'warn');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): two distinct bench
|
|
141
|
+
// entries that sanitize to the same artifact name (src/workspace/artifact-guard.js's
|
|
142
|
+
// artifactAllowlist) mean this run directory cannot hold both models' review/judge files
|
|
143
|
+
// under distinct names — drillIntoJudge's artifact lookup would otherwise silently
|
|
144
|
+
// misattribute one model's prose to the other. This is a run-integrity defect the user
|
|
145
|
+
// must see, not a display quirk to smooth over — surfaced ahead of the run.error/reason
|
|
146
|
+
// banner below since it calls into question every judges-panel section's attribution.
|
|
147
|
+
if (d.derived.artifactCollisions && d.derived.artifactCollisions.length) {
|
|
148
|
+
var c = d.derived.artifactCollisions[0];
|
|
149
|
+
R.renderBanner($('banner'),
|
|
150
|
+
'Run integrity error: bench entries ' + c.models.join(' and ') + ' both sanitize to "' + c.sanitized +
|
|
151
|
+
'" — this run directory cannot distinguish their artifacts, so prose below may be misattributed.', '');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// ⚠️ PRE-FLIGHT (P3), caught live by the CDP e2e (Task 18): gating on `d.run.error` ALONE
|
|
155
|
+
// never shows this banner for a `status:'partial'` run — `finalize()` only ever sets
|
|
156
|
+
// `run.error` on the exit-1 path (src/council/run.js:98-100); the exit-2 "degraded" path
|
|
157
|
+
// leaves it null (run-detail.js:83-91). `verdictPanel.reason` (degradedReason()) already
|
|
158
|
+
// covers BOTH cases — exit-1's {code, message} and the partial-run stage-failure/skip
|
|
159
|
+
// sentence — so check for either signal, not run.error alone.
|
|
160
|
+
if (d.run.error || d.derived.verdictPanel.reason) {
|
|
161
|
+
// ⚠️ Code review round 2, finding 2: `d.run.error` is a structured {code, message} object
|
|
162
|
+
// (never a string) — passing it straight to renderBanner set `textContent = <object>`,
|
|
163
|
+
// which coerces to the literal string "[object Object]". `d.derived.verdictPanel.reason`
|
|
164
|
+
// is already the correctly-formatted string (run-detail.js's degradedReason(): "CODE:
|
|
165
|
+
// message" on the exit-1 path, or a stage-failure/skip sentence otherwise) — reuse it
|
|
166
|
+
// rather than re-deriving the same formatting a second time here.
|
|
167
|
+
R.renderBanner($('banner'), d.derived.verdictPanel.reason || 'Run reported an error',
|
|
168
|
+
d.run.status === 'error' ? '' : 'warn');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
R.renderBanner($('banner'), null);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- blind toggle + keyboard ------------------------------------------
|
|
175
|
+
$('blind-toggle').addEventListener('change', function (e) {
|
|
176
|
+
state.blind = !!e.target.checked;
|
|
177
|
+
renderDetail_preserveBlind();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
function renderDetail_preserveBlind() {
|
|
181
|
+
var keep = state.blind;
|
|
182
|
+
// ⚠️ Fix-wave item 1: renderDetail() itself early-returns safely for an unreadable run
|
|
183
|
+
// (!d || d.error || !d.derived) — but this wrapper used to run past that guard
|
|
184
|
+
// unconditionally, dereferencing the (nonexistent) derived model via renderSeatsPanel()
|
|
185
|
+
// and again on the last line below. Reachable from a typo on the primary documented
|
|
186
|
+
// entry point: `amicus watch <badId> --ui` -> ?runId= boot -> getRunDetail -> error ->
|
|
187
|
+
// the error branch unhides #run-view before the derived-model guard, so the Blind
|
|
188
|
+
// checkbox is live with nothing behind it.
|
|
189
|
+
if (!state.detail || state.detail.error || !state.detail.derived) {
|
|
190
|
+
$('blind-toggle').checked = keep;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
renderDetail();
|
|
194
|
+
state.blind = keep;
|
|
195
|
+
$('blind-toggle').checked = keep;
|
|
196
|
+
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): renderDetail() (just
|
|
197
|
+
// called above) resets state.blind to the run's DEFAULT before this function restores the
|
|
198
|
+
// user's chosen `keep` value — renderHeaderChips was painted during that inner call with
|
|
199
|
+
// the (temporarily wrong) default blind state and, unlike seats/matrix/verdict/cost below,
|
|
200
|
+
// was never repainted afterward. Re-render it here too, now that state.blind is correct.
|
|
201
|
+
window.AmicusRender.renderHeaderChips($('run-chips'), state.detail.run, state.blind, labelOf);
|
|
202
|
+
P.renderSeatsPanel();
|
|
203
|
+
P.renderMatrixPanel();
|
|
204
|
+
P.renderVerdictPanel();
|
|
205
|
+
window.AmicusRender.renderCost($('cost-body'), state.detail.derived.cost, state.blind, labelOf);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
$('run-list').addEventListener('keydown', function (e) {
|
|
209
|
+
var items = Array.prototype.slice.call($('run-list').querySelectorAll('li[data-run-id]'));
|
|
210
|
+
if (!items.length) { return; }
|
|
211
|
+
var idx = items.findIndex(function (li) { return li.dataset.runId === state.runId; });
|
|
212
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); openRun(items[Math.min(items.length - 1, idx + 1)].dataset.runId); }
|
|
213
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); openRun(items[Math.max(0, idx - 1)].dataset.runId); }
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
document.addEventListener('keydown', function (e) {
|
|
217
|
+
if (e.key === 'Escape') { $('dialog-abort').hidden = true; }
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---- boot --------------------------------------------------------------
|
|
221
|
+
// ⚠️ PRE-FLIGHT (P2): publish the namespace BEFORE anything can call into panels/verbs — they
|
|
222
|
+
// read `window.AmicusApp.*` at call time and this file loads last.
|
|
223
|
+
window.AmicusApp = {
|
|
224
|
+
state: state, invoke: invoke, $: $, labelOf: labelOf,
|
|
225
|
+
isBlind: isBlind, openRun: openRun, renderDetail: renderDetail, renderBanners: renderBanners,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// ⚠️ PRE-FLIGHT (P4) + DE-ROT (F09): register the three prose `toggle` listeners exactly ONCE,
|
|
229
|
+
// here at boot. wireLazyPanels() (called from renderDetail, on every run-open and blind-toggle)
|
|
230
|
+
// only rewrites the per-run `loaders` spec map from now on — it never adds a listener.
|
|
231
|
+
P.proseLoader('reviews-panel');
|
|
232
|
+
P.proseLoader('bundle-panel');
|
|
233
|
+
P.proseLoader('judges-panel');
|
|
234
|
+
|
|
235
|
+
var boot = new URLSearchParams(window.location.search).get('runId');
|
|
236
|
+
refreshList().then(function () {
|
|
237
|
+
if (boot) { openRun(boot); }
|
|
238
|
+
});
|
|
239
|
+
startListLoop();
|
|
240
|
+
})();
|