amicus 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +4 -3
- package/electron/ipc-workspace.js +283 -0
- package/electron/main.js +27 -0
- package/electron/preload-workspace.js +40 -0
- package/electron/workspace-shell.js +85 -0
- package/electron/workspace-ui/index.html +111 -0
- package/electron/workspace-ui/live-model.js +101 -0
- package/electron/workspace-ui/md-lite.js +119 -0
- package/electron/workspace-ui/workspace-app.js +240 -0
- package/electron/workspace-ui/workspace-matrix.js +212 -0
- package/electron/workspace-ui/workspace-panels.js +226 -0
- package/electron/workspace-ui/workspace-render.js +271 -0
- package/electron/workspace-ui/workspace-verbs.js +247 -0
- package/electron/workspace-ui/workspace.css +172 -0
- package/package.json +1 -1
- package/schemas/council-run-live.schema.json +25 -1
- package/schemas/council-run.schema.json +14 -0
- package/schemas/progress.schema.json +14 -1
- package/skills/second-opinion/MODEL-NOTES.md +53 -5
- package/src/cli-handlers-council-run.js +25 -3
- package/src/cli-handlers-spend.js +32 -5
- package/src/cli-handlers-watch.js +37 -10
- package/src/council/briefings.js +35 -2
- package/src/council/run-budget.js +224 -0
- package/src/council/run-launch.js +44 -6
- package/src/council/run-stages.js +17 -3
- package/src/council/run.js +12 -11
- package/src/headless.js +347 -14
- package/src/mcp-council-awareness.js +53 -3
- package/src/observe/council-legs.js +183 -0
- package/src/observe/live-doc.js +21 -3
- package/src/observe/watch-render.js +19 -0
- package/src/opencode-client.js +15 -3
- package/src/sidecar/child-sessions.js +198 -0
- package/src/sidecar/conversation-mirror.js +111 -37
- package/src/sidecar/fanout-budget.js +71 -0
- package/src/sidecar/fanout-leg.js +23 -1
- package/src/sidecar/fanout.js +4 -11
- package/src/sidecar/tool-part.js +196 -0
- package/src/sidecar/workspace-window.js +62 -0
- package/src/spend-query.js +21 -6
- package/src/utils/env-num.js +42 -0
- package/src/utils/path-fence.js +82 -0
- package/src/utils/pricing.js +98 -9
- package/src/workspace/artifact-guard.js +187 -0
- package/src/workspace/blind-mode.js +32 -0
- package/src/workspace/fold-format.js +95 -0
- package/src/workspace/live-normalize.js +156 -0
- package/src/workspace/matrix-model.js +94 -0
- package/src/workspace/run-detail.js +223 -0
- package/src/workspace/run-scan.js +148 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="Content-Security-Policy"
|
|
6
|
+
content="default-src 'none'; script-src file:; style-src file: 'unsafe-inline'; font-src file:; img-src file: data:">
|
|
7
|
+
<title>Amicus Council Workspace</title>
|
|
8
|
+
<link rel="stylesheet" href="../../src/design/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="./workspace.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app">
|
|
13
|
+
<aside id="rail" aria-label="Council runs">
|
|
14
|
+
<header class="rail-head">
|
|
15
|
+
<span class="rail-title">Council runs</span>
|
|
16
|
+
<span id="rail-project" class="rail-project mono"></span>
|
|
17
|
+
</header>
|
|
18
|
+
<ul id="run-list" tabindex="0" aria-label="Run list"></ul>
|
|
19
|
+
</aside>
|
|
20
|
+
|
|
21
|
+
<main id="detail">
|
|
22
|
+
<div id="empty-state" class="empty-state">Select a council run</div>
|
|
23
|
+
|
|
24
|
+
<div id="run-view" hidden>
|
|
25
|
+
<header id="run-header">
|
|
26
|
+
<div class="run-head-row">
|
|
27
|
+
<h1 id="run-title" class="mono"></h1>
|
|
28
|
+
<div id="run-chips" class="chips"></div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="run-head-row">
|
|
31
|
+
<div id="cost-gauge" class="gauge" role="meter" aria-label="Run cost vs ceiling">
|
|
32
|
+
<div id="cost-gauge-fill" class="gauge-fill"></div>
|
|
33
|
+
<span id="cost-gauge-text" class="gauge-text mono"></span>
|
|
34
|
+
</div>
|
|
35
|
+
<label class="blind-label">
|
|
36
|
+
<input type="checkbox" id="blind-toggle"> Blind mode
|
|
37
|
+
</label>
|
|
38
|
+
<button id="abort-btn" class="btn danger" hidden>Abort run…</button>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
<div id="banner" class="banner" hidden></div>
|
|
43
|
+
|
|
44
|
+
<nav id="stage-rail" class="stage-rail" aria-label="Stages"></nav>
|
|
45
|
+
|
|
46
|
+
<section id="seats-panel" class="panel" aria-label="Seats">
|
|
47
|
+
<h2 class="panel-title">Seats</h2>
|
|
48
|
+
<table class="table">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr><th>Seat</th><th>Role</th><th>Status</th><th>Stage</th><th>Msgs</th>
|
|
51
|
+
<th>Tokens in/out</th><th>Cost</th><th>Last activity</th><th></th></tr>
|
|
52
|
+
</thead>
|
|
53
|
+
<tbody id="seats-body"></tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<details id="reviews-panel" class="panel">
|
|
58
|
+
<summary class="panel-title">Stage-1 reviews</summary>
|
|
59
|
+
<div id="reviews-body" class="prose-host"></div>
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
<details id="bundle-panel" class="panel">
|
|
63
|
+
<summary class="panel-title">Peer-review packet (verbatim)</summary>
|
|
64
|
+
<div id="bundle-body" class="prose-host"></div>
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details id="judges-panel" class="panel">
|
|
68
|
+
<summary class="panel-title">Judge outputs</summary>
|
|
69
|
+
<div id="judges-body" class="prose-host"></div>
|
|
70
|
+
</details>
|
|
71
|
+
|
|
72
|
+
<section id="matrix-panel" class="panel" aria-label="Adjudication matrix">
|
|
73
|
+
<h2 class="panel-title">Adjudication matrix</h2>
|
|
74
|
+
<div id="matrix-body"></div>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<section id="verdict-panel" class="panel" aria-label="Verdict">
|
|
78
|
+
<h2 class="panel-title">Verdict</h2>
|
|
79
|
+
<div id="verdict-body"></div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<section id="cost-panel" class="panel" aria-label="Cost">
|
|
83
|
+
<h2 class="panel-title">Cost by seat</h2>
|
|
84
|
+
<div id="cost-body"></div>
|
|
85
|
+
</section>
|
|
86
|
+
</div>
|
|
87
|
+
</main>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div id="dialog-abort" class="dialog-backdrop" hidden>
|
|
91
|
+
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-abort-title">
|
|
92
|
+
<h2 id="dialog-abort-title">Abort this council run?</h2>
|
|
93
|
+
<p>The active wave and solo legs will be aborted via the engine's abort path. Artifacts already on disk stay browsable.</p>
|
|
94
|
+
<div class="dialog-actions">
|
|
95
|
+
<button id="dialog-abort-cancel" class="btn">Cancel</button>
|
|
96
|
+
<button id="dialog-abort-confirm" class="btn danger">Abort run</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<script src="./md-lite.js"></script>
|
|
102
|
+
<script src="./live-model.js"></script>
|
|
103
|
+
<script src="./workspace-render.js"></script>
|
|
104
|
+
<script src="./workspace-matrix.js"></script>
|
|
105
|
+
<!-- ⚠️ DE-ROT (F05): panels + verbs are the Task 13 split of workspace-app.js.
|
|
106
|
+
Load order matters: workspace-app.js boots last and owns `state`. -->
|
|
107
|
+
<script src="./workspace-panels.js"></script>
|
|
108
|
+
<script src="./workspace-verbs.js"></script>
|
|
109
|
+
<script src="./workspace-app.js"></script>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
var TERMINAL_STATUSES = ['complete', 'partial', 'error', 'crashed', 'aborted', 'timeout', 'idle-timeout'];
|
|
15
|
+
|
|
16
|
+
// ⚠️ DE-ROT (F41): STAGE_LABELS must be mirrored here too. The live loop labels stages that
|
|
17
|
+
// START AFTER the run was opened, and those names are absent from the frozen derived.stageRail,
|
|
18
|
+
// so without this table every post-open stage renders its raw name ("tally-final", "debate-revote").
|
|
19
|
+
var STAGE_LABELS = {
|
|
20
|
+
stage1: 'Stage 1 — independent review',
|
|
21
|
+
stage2: 'Stage 2 — peer cross-review',
|
|
22
|
+
'debate-defense': 'Debate — defense',
|
|
23
|
+
'debate-revote': 'Debate — re-vote',
|
|
24
|
+
'tally-provisional': 'Tally (provisional)',
|
|
25
|
+
tally: 'Tally',
|
|
26
|
+
'tally-final': 'Tally (final)',
|
|
27
|
+
chair: 'Chair synthesis',
|
|
28
|
+
verdict: 'Verdict',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Poll cadence per spec §4.3: 1.5s visible+focused, 5s otherwise, stop at terminal. */
|
|
32
|
+
function pollDelay(state) {
|
|
33
|
+
if (state.terminal) { return null; }
|
|
34
|
+
return (state.visible && state.focused) ? 1500 : 5000;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Blind default per spec resolved Q2: ON live, OFF terminal. */
|
|
38
|
+
function defaultBlind(status) {
|
|
39
|
+
return TERMINAL_STATUSES.indexOf(status) === -1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dash(v) {
|
|
43
|
+
return (v === null || v === undefined || v === '') ? '—' : String(v);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Display cells for one seat row, in the seats-table column order:
|
|
48
|
+
* [name, role, status, stage, msgs, tokens in/out, cost, last activity, stalled]
|
|
49
|
+
*/
|
|
50
|
+
function seatCells(seat, blindOn, labelOf) {
|
|
51
|
+
// ⚠️ DE-ROT (F36): key the lookup on the council ALIAS, never on `seat.model`. labelMap's
|
|
52
|
+
// values are aliases; a LIVE leg's `model` is the resolved executable id, so labelOf(model)
|
|
53
|
+
// misses and Blind ON renders the real model. `modelInput` is the alias Task 0.5 stamps per
|
|
54
|
+
// leg (terminal rows from runStats are already alias-only, so the fallback is exact there).
|
|
55
|
+
var alias = seat.modelInput || seat.model;
|
|
56
|
+
var label = labelOf ? labelOf(alias) : null;
|
|
57
|
+
var name = blindOn && label ? label : alias;
|
|
58
|
+
var tokens = (seat.tokensIn === null || seat.tokensIn === undefined) &&
|
|
59
|
+
(seat.tokensOut === null || seat.tokensOut === undefined)
|
|
60
|
+
? '—'
|
|
61
|
+
: dash(seat.tokensIn) + '/' + dash(seat.tokensOut);
|
|
62
|
+
return [
|
|
63
|
+
dash(name),
|
|
64
|
+
dash(seat.role),
|
|
65
|
+
dash(seat.status),
|
|
66
|
+
dash(seat.stage),
|
|
67
|
+
dash(seat.messages),
|
|
68
|
+
tokens,
|
|
69
|
+
dash(seat.costDisplay),
|
|
70
|
+
// ⚠️ DE-ROT (F35): pass-through of an ISO timestamp, NOT a relative string. renderSeats
|
|
71
|
+
// formats it with relTime() before calling in — live-model.js is node-tested and loads
|
|
72
|
+
// before workspace-render.js, so it cannot reach relTime itself.
|
|
73
|
+
dash(seat.lastActivity),
|
|
74
|
+
seat.stalled ? '⏳ stalled' : '',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
79
|
+
function seatsFromRunStats(costRows) {
|
|
80
|
+
return (costRows || []).map(function (r) {
|
|
81
|
+
return {
|
|
82
|
+
// ⚠️ DE-ROT (F37): composite id — a v4.1 `--debate` run emits extra runStats rows for
|
|
83
|
+
// the SAME bench alias (role 'rebuttal'/'revote', src/council/debate.js:88-96). With no
|
|
84
|
+
// id, renderSeats keys on model and the re-vote row silently overwrites the seat row.
|
|
85
|
+
// (If the panel is meant to be bench-only, filter to seat/critic/lens:* instead and
|
|
86
|
+
// leave rebuttal/revote spend to the cost table — but say which; do not leave it implied.)
|
|
87
|
+
id: r.model + ':' + (r.role || 'seat'),
|
|
88
|
+
model: r.model, role: r.role || null, status: r.status || null,
|
|
89
|
+
stage: null, messages: null, tokensIn: null, tokensOut: null,
|
|
90
|
+
costDisplay: r.costDisplay || null, lastActivity: null, latestPreview: null,
|
|
91
|
+
stalled: false,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ⚠️ DE-ROT (F41): STAGE_LABELS is exported so applyLive() can label post-open stages.
|
|
97
|
+
var api = { pollDelay: pollDelay, seatCells: seatCells, seatsFromRunStats: seatsFromRunStats,
|
|
98
|
+
defaultBlind: defaultBlind, dash: dash, TERMINAL_STATUSES: TERMINAL_STATUSES, STAGE_LABELS: STAGE_LABELS };
|
|
99
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
100
|
+
if (typeof window !== 'undefined') { window.AmicusLive = api; }
|
|
101
|
+
})();
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
function parseInline(text) {
|
|
19
|
+
var out = [];
|
|
20
|
+
var rest = String(text);
|
|
21
|
+
while (rest.length) {
|
|
22
|
+
var m = /`([^`\n]+)`/.exec(rest);
|
|
23
|
+
if (!m) { out.push({ code: false, text: rest }); break; }
|
|
24
|
+
if (m.index > 0) { out.push({ code: false, text: rest.slice(0, m.index) }); }
|
|
25
|
+
out.push({ code: true, text: m[1] });
|
|
26
|
+
rest = rest.slice(m.index + m[0].length);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseMdLite(text) {
|
|
32
|
+
var lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
33
|
+
var blocks = [];
|
|
34
|
+
var i = 0;
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
var line = lines[i];
|
|
37
|
+
if (/^```/.test(line)) {
|
|
38
|
+
var buf = [];
|
|
39
|
+
i += 1;
|
|
40
|
+
while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i += 1; }
|
|
41
|
+
i += 1; // closing fence (or EOF)
|
|
42
|
+
blocks.push({ t: 'code', text: buf.join('\n') });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
var h = H_RE.exec(line);
|
|
46
|
+
if (h) { blocks.push({ t: 'h', level: h[1].length, text: h[2] }); i += 1; continue; }
|
|
47
|
+
if (UL_RE.test(line)) {
|
|
48
|
+
var ul = [];
|
|
49
|
+
while (i < lines.length && UL_RE.test(lines[i])) { ul.push(lines[i].replace(UL_RE, '')); i += 1; }
|
|
50
|
+
blocks.push({ t: 'ul', items: ul });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (OL_RE.test(line)) {
|
|
54
|
+
var ol = [];
|
|
55
|
+
while (i < lines.length && OL_RE.test(lines[i])) { ol.push(lines[i].replace(OL_RE, '')); i += 1; }
|
|
56
|
+
blocks.push({ t: 'ol', items: ol });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!line.trim()) { i += 1; continue; }
|
|
60
|
+
var p = [line];
|
|
61
|
+
i += 1;
|
|
62
|
+
while (i < lines.length && lines[i].trim() &&
|
|
63
|
+
!/^```/.test(lines[i]) && !H_RE.test(lines[i]) &&
|
|
64
|
+
!UL_RE.test(lines[i]) && !OL_RE.test(lines[i])) {
|
|
65
|
+
p.push(lines[i]);
|
|
66
|
+
i += 1;
|
|
67
|
+
}
|
|
68
|
+
blocks.push({ t: 'p', text: p.join(' ') });
|
|
69
|
+
}
|
|
70
|
+
return blocks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyInline(el, segs, doc) {
|
|
74
|
+
for (var i = 0; i < segs.length; i++) {
|
|
75
|
+
if (segs[i].code) {
|
|
76
|
+
var c = doc.createElement('code');
|
|
77
|
+
c.textContent = segs[i].text;
|
|
78
|
+
el.appendChild(c);
|
|
79
|
+
} else {
|
|
80
|
+
el.appendChild(doc.createTextNode(segs[i].text));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Render prose into container. Panels use h3–h6 so page structure wins. */
|
|
86
|
+
function renderMdLite(container, text, doc) {
|
|
87
|
+
var d = doc || document;
|
|
88
|
+
container.textContent = '';
|
|
89
|
+
var blocks = parseMdLite(text);
|
|
90
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
91
|
+
var b = blocks[i];
|
|
92
|
+
var el;
|
|
93
|
+
if (b.t === 'h') {
|
|
94
|
+
el = d.createElement('h' + Math.min(6, b.level + 2));
|
|
95
|
+
applyInline(el, parseInline(b.text), d);
|
|
96
|
+
} else if (b.t === 'code') {
|
|
97
|
+
el = d.createElement('pre');
|
|
98
|
+
var code = d.createElement('code');
|
|
99
|
+
code.textContent = b.text;
|
|
100
|
+
el.appendChild(code);
|
|
101
|
+
} else if (b.t === 'ul' || b.t === 'ol') {
|
|
102
|
+
el = d.createElement(b.t);
|
|
103
|
+
for (var j = 0; j < b.items.length; j++) {
|
|
104
|
+
var li = d.createElement('li');
|
|
105
|
+
applyInline(li, parseInline(b.items[j]), d);
|
|
106
|
+
el.appendChild(li);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
el = d.createElement('p');
|
|
110
|
+
applyInline(el, parseInline(b.text), d);
|
|
111
|
+
}
|
|
112
|
+
container.appendChild(el);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var api = { parseMdLite: parseMdLite, parseInline: parseInline, renderMdLite: renderMdLite };
|
|
117
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
118
|
+
if (typeof window !== 'undefined') { window.AmicusMd = api; }
|
|
119
|
+
})();
|
|
@@ -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
|
+
})();
|