amicus 4.8.1 → 4.9.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 +249 -0
- package/README.md +8 -3
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +54 -4
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +36 -15
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui.js +18 -8
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +1 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +1 -1
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +63 -15
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models.js +8 -1
- package/src/sidecar/read.js +34 -10
- package/src/template/render.js +16 -7
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +11 -3
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +2 -5
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +10 -2
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/result-schema.js +10 -0
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +33 -7
- package/src/workspace/seat-space.js +10 -6
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
* stays intact inside the fence. It must NOT be applied to CLI --json stdout
|
|
18
18
|
* (the byte-parseable programmatic channel) or amicus_read mode=metadata —
|
|
19
19
|
* structured data a caller parses, where wrapping would break the contract.
|
|
20
|
+
*
|
|
21
|
+
* This module ALSO hosts the OUTBOUND family's tag neutralizer
|
|
22
|
+
* (`defangOutboundFenceTags`, PR #200 tails B2/C2; close tags only until PR
|
|
23
|
+
* #206 round 3 B3b took it to open tags too). It lives here rather than
|
|
24
|
+
* beside either outbound builder because there are two of them — the council
|
|
25
|
+
* briefing tail (src/council/briefings-stage2-task.js :: fenceBriefing) and the
|
|
26
|
+
* parent-conversation section (src/prompt-builder.js :: buildContextSection) —
|
|
27
|
+
* in two directories that do not import each other, and this is the one module
|
|
28
|
+
* in the tree whose subject is fences and which both can reach without either
|
|
29
|
+
* depending on the other.
|
|
20
30
|
*/
|
|
21
31
|
'use strict';
|
|
22
32
|
|
|
@@ -36,4 +46,104 @@ ${body}
|
|
|
36
46
|
</untrusted_sidecar_output>`;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Every OUTBOUND house fence's tag name. ONE list, deliberately: both surfaces
|
|
51
|
+
* run the neutralizer over their whole vocabulary, not just their own tag, so a
|
|
52
|
+
* fence added to the family is covered everywhere by editing this array — and a
|
|
53
|
+
* body that carries the SIBLING surface's tags (either end, since round 3)
|
|
54
|
+
* cannot smuggle them through the surface that does not happen to emit them.
|
|
55
|
+
* @see src/council/briefings-stage2-task.js :: BRIEFING_FENCE_CLOSE
|
|
56
|
+
* @see src/prompt-builder.js :: buildContextSection
|
|
57
|
+
*/
|
|
58
|
+
const OUTBOUND_FENCE_TAGS = ['council_briefing', 'previous_conversation'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `</council_briefing>` / `</previous_conversation>`, case-insensitively and
|
|
62
|
+
* tolerating whitespace ANYWHERE inside the angle brackets — including between
|
|
63
|
+
* the `<` and the `/` (round 3, C1: the round-2 pattern required those two to
|
|
64
|
+
* be adjacent, so `< /council_briefing>` rode through untouched, and a reading
|
|
65
|
+
* model honours that spelling exactly as readily as the tight one). The slash
|
|
66
|
+
* and its surrounding space are INSIDE the captured group, so the author's own
|
|
67
|
+
* spelling survives into the defanged form and nothing is hidden.
|
|
68
|
+
* ⚠️ Module-level and `g`-flagged: safe ONLY because its sole use is
|
|
69
|
+
* `String#replace`, which resets `lastIndex` around the call. A `.test()` on
|
|
70
|
+
* this object would carry `lastIndex` between calls and skip matches — build a
|
|
71
|
+
* fresh regex if one is ever needed. Same warning for the OPEN pattern below.
|
|
72
|
+
*/
|
|
73
|
+
const OUTBOUND_FENCE_CLOSE_RE =
|
|
74
|
+
new RegExp(`<(\\s*\\/\\s*(?:${OUTBOUND_FENCE_TAGS.join('|')})\\s*)>`, 'gi');
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The same families' OPEN tags, attributes and all (round 3, B3b).
|
|
78
|
+
*
|
|
79
|
+
* `\\b` after the name is what keeps this off tags that merely START with a
|
|
80
|
+
* house name — `<council_briefingx>` is somebody else's markup — and
|
|
81
|
+
* `[^<>]*` carries whatever attributes the author wrote through into the
|
|
82
|
+
* escaped form. It cannot match a CLOSE tag: `\\s*` does not consume the `/`.
|
|
83
|
+
*/
|
|
84
|
+
const OUTBOUND_FENCE_OPEN_RE =
|
|
85
|
+
new RegExp(`<(\\s*(?:${OUTBOUND_FENCE_TAGS.join('|')})\\b[^<>]*)>`, 'gi');
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Neutralize any outbound house fence tag inside a body about to be embedded in
|
|
89
|
+
* one (PR #200 tails B2/C2; round 3 B3b/C1 widened it from closes to both ends).
|
|
90
|
+
*
|
|
91
|
+
* Without this, untrusted text that contains the close tag ends the fence early
|
|
92
|
+
* in the reading model's eyes and everything after it reads as the engine
|
|
93
|
+
* speaking — the fence's entire purpose, undone by a string the author types.
|
|
94
|
+
* The replacement is an entity escape (`</council_briefing>`), so the
|
|
95
|
+
* text stays legible as what the author wrote while no longer being a tag:
|
|
96
|
+
* nothing is deleted and nothing is silently swallowed.
|
|
97
|
+
*
|
|
98
|
+
* OPEN TAGS TOO, and round 2's reason for skipping them was wrong (B3b). That
|
|
99
|
+
* reason — "an open tag inside a fence cannot escape it" — holds for a STRICT
|
|
100
|
+
* parser and fails for the reader this fence is actually addressed to. A model
|
|
101
|
+
* that balances tags reads the attacker's `<council_briefing …>` and the
|
|
102
|
+
* engine's REAL `</council_briefing>` as one pair: the attacker's tag gets the
|
|
103
|
+
* close, and the engine's fence is left unterminated. Everything after the
|
|
104
|
+
* attacker's open reads as fenced material, and everything the ENGINE writes
|
|
105
|
+
* after the real close reads as still inside a fence. That is the same escape
|
|
106
|
+
* one tag along. MEASURED before widening (W12, re-verified round 3): nothing
|
|
107
|
+
* in the tree PARSES these tags — the only occurrences outside test assertions
|
|
108
|
+
* are the two producers' own literals — so escaping opens breaks no consumer.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ THE BOUNDARY IS SOFT, and this is a disclosure, not a caveat. An entity
|
|
111
|
+
* escape is a convention about how a READING MODEL should interpret bytes, not
|
|
112
|
+
* a parser guarantee: some models decode `</council_briefing>` back to
|
|
113
|
+
* the tag while reading, and a model that does is not fenced by this. What this
|
|
114
|
+
* buys is defense in depth — it removes the LITERAL tag, so the escape stops
|
|
115
|
+
* being free and starts depending on a decoding step the attacker does not
|
|
116
|
+
* control. The load-bearing protection is still the preamble both fences carry
|
|
117
|
+
* (the enclosed text is reference material, not instructions); this hardens it,
|
|
118
|
+
* and does not replace it. Deleting the tag outright would be a stronger
|
|
119
|
+
* boundary and a worse product — the reader would silently lose text the author
|
|
120
|
+
* wrote, which is the failure the whole fence exists to avoid.
|
|
121
|
+
*
|
|
122
|
+
* Total over non-strings (returns its argument unchanged): callers hand it
|
|
123
|
+
* whatever they were given, and a missing body must never be a throw inside a
|
|
124
|
+
* prompt builder.
|
|
125
|
+
* @param {string} text the untrusted body about to be fenced
|
|
126
|
+
* @returns {string} the same bytes when it carries no house tag
|
|
127
|
+
*/
|
|
128
|
+
function defangOutboundFenceTags(text) {
|
|
129
|
+
if (typeof text !== 'string') { return text; }
|
|
130
|
+
// Closes first: once a close is escaped its `<` is gone, so the OPEN pattern
|
|
131
|
+
// cannot see it. (It could not match one anyway — see the pattern's note —
|
|
132
|
+
// but the order makes that independent of the pattern staying that way.)
|
|
133
|
+
return text
|
|
134
|
+
.replace(OUTBOUND_FENCE_CLOSE_RE, '<$1>')
|
|
135
|
+
.replace(OUTBOUND_FENCE_OPEN_RE, '<$1>');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// FUNCTIONS FIRST, constant last — the PR 201 round-2 workaround for
|
|
139
|
+
// `scripts/generate-docs-helpers.js :: extractExports`, which renders EVERY
|
|
140
|
+
// export as `name()` and keeps only the first five. Ordering conceals a
|
|
141
|
+
// constant behind that cap; MEASURED here, it cannot, because this module
|
|
142
|
+
// exports three names and the cap never fires. So CLAUDE.md's Key Exports cell
|
|
143
|
+
// reads `OUTBOUND_FENCE_TAGS()` for what is an array — a known-defect instance
|
|
144
|
+
// of the generator bug, not a claim this module makes, and the second one below
|
|
145
|
+
// the workaround's floor after `utils/text-sanitize.js`. It goes away with the
|
|
146
|
+
// generator ruling filed in BACKLOG.md; padding the export list to five to hide
|
|
147
|
+
// it would not be a fix, and the list itself must stay exported —
|
|
148
|
+
// tests/utils/outbound-fence-defang.test.js reads it as the LIVE vocabulary.
|
|
149
|
+
module.exports = { fenceSidecarOutput, defangOutboundFenceTags, OUTBOUND_FENCE_TAGS };
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* (src/headless.js `formatFoldOutput`, exported from src/headless.js and
|
|
6
6
|
* re-exported from src/index.js) — byte-for-byte the same **7-line** head:
|
|
7
7
|
* marker, Model, Session, Client, CWD, Mode, `---`. ⚠️ v4.4.1 DOC-5: this said
|
|
8
|
-
* "8-line" for two releases. Line 8 (`VERDICT:`
|
|
8
|
+
* "8-line" for two releases. Line 8 (the terminal line — `VERDICT:` on a review
|
|
9
|
+
* run, `ANSWER:` on a task one since v4.9 W8) is council's OWN addition and
|
|
9
10
|
* has no counterpart in formatFoldOutput, whose 8th element is the summary
|
|
10
11
|
* body. Only the first 7 lines are the shared contract; anyone changing the
|
|
11
|
-
* shared format must sync those and leave
|
|
12
|
+
* shared format must sync those and leave the terminal line alone.
|
|
12
13
|
* src/headless.js is the SOURCE OF TRUTH; keep the head in sync with it.
|
|
13
14
|
* (Line numbers deliberately omitted — the previous `:775-789`/`:797` citation
|
|
14
15
|
* had drifted by ~500 lines.) The duplication is deliberate:
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
* The chair body is UNTRUSTED model text: it passes through stripFoldMarkers
|
|
25
26
|
* before embedding, so chair prose containing a marker can never truncate or
|
|
26
27
|
* spoof the fold (the exact hazard the nonce closure exists for).
|
|
27
|
-
* Degradation mirrors the engine's ladder: no chair →
|
|
28
|
+
* Degradation mirrors the engine's ladder: no chair → `<terminal>: none` + tally
|
|
28
29
|
* summary; pre-tally → stage/status summary. Never blocked, always labeled.
|
|
29
30
|
*/
|
|
30
31
|
'use strict';
|
|
@@ -63,14 +64,34 @@ function buildFoldText(o) {
|
|
|
63
64
|
// amicus_verdict MCP path types overallVerdict as a bare z.string().nullable()
|
|
64
65
|
// — mcp-tools.js :: amicus_verdict), so a multi-line or marker-bearing value
|
|
65
66
|
// must never reach the head verbatim: an embedded '\n' would shift every line below
|
|
66
|
-
//
|
|
67
|
+
// the terminal line (a raw string containing '\n' becomes several elements once the
|
|
67
68
|
// head array is '\n'-joined), and an embedded marker could spoof the fold.
|
|
68
|
-
// Safe on the shipped engine path (
|
|
69
|
-
//
|
|
69
|
+
// Safe on the shipped engine path (`run-chair.js :: runChair` parses the chair's
|
|
70
|
+
// terminal line with `parse-stage2.js :: parseChairTerminal`, which returns a
|
|
71
|
+
// canonical phrase of the run's OWN scale — CHAIR_VERDICTS on a review run,
|
|
72
|
+
// CHAIR_ANSWERS on a task one) — this is defense-in-depth, not a fix for a real
|
|
70
73
|
// producer.
|
|
74
|
+
// ⚠️ DE-ROT (v4.9 W8): this said "parseChairVerdict returns a canonical
|
|
75
|
+
// CHAIR_VERDICTS phrase", true only until W7 added the second scale — after
|
|
76
|
+
// which it named the wrong parser for every task run.
|
|
71
77
|
const overall = verdict && verdict.overallVerdict
|
|
72
78
|
? stripFoldMarkers(String(verdict.overallVerdict)).replace(/[\r\n]+/g, ' ').trim()
|
|
73
79
|
: null;
|
|
80
|
+
// v4.9 W8: the terminal line names the SCALE its phrase belongs to. The two
|
|
81
|
+
// scales are disjoint by pinned construction (tests/council/chair-scale-drift.test.js),
|
|
82
|
+
// so labelling a CHAIR_ANSWERS phrase `VERDICT:` asserts a scale it is not on.
|
|
83
|
+
// `intent` is emit-when-'task' (verdict.js :: buildVerdict, the W5 ruling), so
|
|
84
|
+
// absence — and an explicit 'review' — keep the review fold byte-identical.
|
|
85
|
+
// ⚠️ v4.9 fix round 2 (council B2): `run.intent` is the SECOND carrier, and it
|
|
86
|
+
// is what covers the DEGRADED fold — the one a user reaches for precisely when
|
|
87
|
+
// a task run went wrong. With no verdict.json (or a parse-failed one) there is
|
|
88
|
+
// no verdict.intent, and the fold headed `VERDICT: none` on a run that was
|
|
89
|
+
// never on that scale. `o.run` is the parsed run.json, which checkpoints
|
|
90
|
+
// `intent: 'task'` at start (src/council/run.js :: runCouncil):
|
|
91
|
+
// electron/ipc-workspace.js passes `run: detail.run` and run-detail.js ::
|
|
92
|
+
// getRunDetail reads it straight from `<runDir>/run.json`.
|
|
93
|
+
// PR #200 round-5 B3: parens on the whole disjunction — behaviour-identical.
|
|
94
|
+
const terminalLabel = ((verdict && verdict.intent === 'task') || run.intent === 'task') ? 'ANSWER' : 'VERDICT';
|
|
74
95
|
const tierCounts = (verdict && verdict.tierCounts) || (tally && tally.tierCounts) || null;
|
|
75
96
|
const cost = run.usage && run.usage.cost ? run.usage.cost : null;
|
|
76
97
|
|
|
@@ -82,7 +103,7 @@ function buildFoldText(o) {
|
|
|
82
103
|
`CWD: ${o.project}`,
|
|
83
104
|
'Mode: council',
|
|
84
105
|
'---',
|
|
85
|
-
|
|
106
|
+
`${terminalLabel}: ${overall || 'none'}`,
|
|
86
107
|
];
|
|
87
108
|
if (tierCounts) {
|
|
88
109
|
head.push(tierLine(tierCounts));
|
|
@@ -50,7 +50,8 @@ function seatOf(leg) {
|
|
|
50
50
|
model: leg.model || null,
|
|
51
51
|
modelInput: leg.modelInput || null,
|
|
52
52
|
role: leg.role || null,
|
|
53
|
-
// ⚠️ v4.8 R5: the leg's seat id, and the reason live-dead-seats.js
|
|
53
|
+
// ⚠️ v4.8 R5: the leg's seat id, and the reason live-dead-seats.js ::
|
|
54
|
+
// deadSeats' `if (s.seat)` suppression arm's
|
|
54
55
|
// `if (s.seat)` arm is reachable at all. Null on a unique-alias bench, so the
|
|
55
56
|
// guard there stays load-bearing — see the comment at that line.
|
|
56
57
|
seat: leg.seat || null,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* unconditionally (src/council/tally.js :: tally) — it is never a real source for
|
|
16
16
|
* either the override badge or the post-override tier. Only `buildVerdict`
|
|
17
17
|
* materializes `{from,to,reason}` and rewrites `tier` to `tierOverride.to`
|
|
18
|
-
* (src/council/verdict.js
|
|
18
|
+
* (src/council/verdict.js :: buildVerdict's `findings` map — re-anchored BY SYMBOL at v4.9 W9,
|
|
19
|
+
* which shifted the old `:128-132`). So both fields are joined in from
|
|
19
20
|
* verdict.findings[] by `id`; when verdict is absent/unparseable (caller
|
|
20
21
|
* passes null/undefined, or a finding has no verdict-side counterpart) the
|
|
21
22
|
* row falls back to tally's own (pre-override) tier and renders no badge.
|
|
@@ -183,7 +184,10 @@ function buildMatrixModel(tally, labelMap, verdict) {
|
|
|
183
184
|
// ⚠️ DE-ROT (F29): v4.1 decorates tally.json findings in place with
|
|
184
185
|
// `debate: {action, previousTier}` (src/council/debate.js :: decorateRecord; action ∈
|
|
185
186
|
// defended|amended|withdrawn|no-response) and verdict.json carries it through
|
|
186
|
-
// (src/council/verdict.js
|
|
187
|
+
// (src/council/verdict.js :: buildVerdict's `if (f.debate)` carry-through — the old
|
|
188
|
+
// `:43` was ALREADY rotted onto summarizeSeatLoss, which has since left verdict.js
|
|
189
|
+
// altogether; re-anchored by symbol, not re-numbered). Consumed by
|
|
190
|
+
// electron/workspace-ui/workspace-matrix.js's
|
|
187
191
|
// renderMatrix, which renders a `.debate-badge` in the tier cell (alongside the
|
|
188
192
|
// thin/tierOverride badges) so a withdrawn/amended/defended/no-response finding never
|
|
189
193
|
// renders as an ordinary live row. Absent on non-debate runs, hence `|| null`.
|
|
@@ -124,7 +124,7 @@ function costPanel(run, tally) {
|
|
|
124
124
|
|
|
125
125
|
// ⚠️ PRE-FLIGHT (P3): F04's correction is implemented here rather than left as prose.
|
|
126
126
|
// VERIFIED on shipped main (Task 0): `finalize(exitCode, error)` writes `error: error || null`
|
|
127
|
-
// (run-finalize.js :: writeRunTerminal), and `return finalize(degraded.value ? 2 : 0)` (run.js:
|
|
127
|
+
// (run-finalize.js :: writeRunTerminal), and `return finalize(degraded.value ? 2 : 0)` (run.js:294) is the ONLY
|
|
128
128
|
// exit-2 path — it passes NO error. Every error-bearing call is `finalize(1, …)`. So on a
|
|
129
129
|
// `status:'partial'` run — precisely the run this panel exists to explain — `run.error` is
|
|
130
130
|
// GUARANTEED null, and the old one-line formula rendered "undefined: undefined".
|
|
@@ -142,13 +142,32 @@ function degradedReason(run) {
|
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// v4.9 W8 T-B: `intent` rides this payload so the renderer can label a task run's
|
|
146
|
+
// chip `ANSWER:` (electron/workspace-ui/workspace-matrix.js :: renderVerdict is a
|
|
147
|
+
// plain browser script and cannot require() src/, so a fork key it cannot read is
|
|
148
|
+
// a fork it cannot make). Unlike verdict.json — emit-when-'task' per the W5 ruling,
|
|
149
|
+
// because that artifact has a byte-identity contract — this is an in-memory IPC
|
|
150
|
+
// model whose literal is CLOSED and always materializes every key with a default,
|
|
151
|
+
// so `intent` is materialized both ways and defaults to 'review'. The renderer
|
|
152
|
+
// still treats an ABSENT key as review: pre-v4.9 payloads carry none.
|
|
153
|
+
// ⚠️ v4.9 fix round 2 (council B2): sourced from `run` as well as `verdict`.
|
|
154
|
+
// Reading `verdict.intent` ALONE meant the two panels that exist to explain a
|
|
155
|
+
// broken run could not label it: on the narrow leg where a task run exits before
|
|
156
|
+
// the verdict write — or leaves a truncated one — there is no verdict.intent to
|
|
157
|
+
// read, and the panel defaulted to review, so the Workspace chip said "no chair
|
|
158
|
+
// verdict" about a run that was never on that scale. `run.json` checkpoints
|
|
159
|
+
// `intent: 'task'` at start (run.js :: runCouncil) and `run` is already this
|
|
160
|
+
// function's first parameter, so the honest source costs nothing to reach.
|
|
145
161
|
function verdictPanel(run, verdict) {
|
|
146
162
|
const reason = degradedReason(run);
|
|
163
|
+
// PR #200 round-5 B3: parens on the whole disjunction — behaviour-identical.
|
|
164
|
+
const intent = ((verdict && verdict.intent === 'task') || (run && run.intent === 'task')) ? 'task' : 'review';
|
|
147
165
|
if (!verdict || verdict.parseError) {
|
|
148
|
-
return { present: false, overallVerdict: null, tierCounts: null, streetCred: [], decisions: [], reason };
|
|
166
|
+
return { present: false, overallVerdict: null, tierCounts: null, streetCred: [], decisions: [], reason, intent };
|
|
149
167
|
}
|
|
150
168
|
return {
|
|
151
169
|
present: true,
|
|
170
|
+
intent,
|
|
152
171
|
overallVerdict: verdict.overallVerdict === undefined ? null : verdict.overallVerdict,
|
|
153
172
|
tierCounts: verdict.tierCounts || null,
|
|
154
173
|
streetCred: Array.isArray(verdict.streetCred) ? verdict.streetCred : [],
|
|
@@ -233,11 +252,18 @@ function getRunDetail(project, runId) {
|
|
|
233
252
|
// of a copy of the question makes that divergence unrepresentable.
|
|
234
253
|
seatSpace: isSeatTable(run.seats),
|
|
235
254
|
// ⚠️ Fix-wave 3 (council-3 C2): seats[] is PRESENT but unusable, so the run silently
|
|
236
|
-
// lost per-seat
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
255
|
+
// lost per-seat ARTIFACT NAMING — two seats on one model become indistinguishable in
|
|
256
|
+
// the reviews/judges/re-vote panels, whose roster this flag sends back to `bench`
|
|
257
|
+
// (workspace-lazy.js :: roster). isSeatTable fails WHOLE (one malformed entry drops the
|
|
258
|
+
// entire table), which is the fail-safe direction but is exactly the correct-but-silent
|
|
259
|
+
// degrade the product principle rejects. Emitted here, beside the predicate that decides
|
|
260
|
+
// it, rather than re-derived renderer-side; workspace-banners.js is its only consumer.
|
|
261
|
+
// ⚠️ "in every panel" — what this comment said until v4.9 W9 — was already FALSE when
|
|
262
|
+
// written, and W9's PR5b-1 measured it: `costPanel` above maps `tally.runStats`, whose
|
|
263
|
+
// `seat` is stamped from the in-memory seat at assembly (run-stats-entry.js ::
|
|
264
|
+
// buildRunStatsEntry), so the seats panel keeps one row per seat no matter what
|
|
265
|
+
// run.seats looks like. Ruling V15 discloses that split in the banner rather than
|
|
266
|
+
// forcing the seats panel down to `seatSpace` for uniformity.
|
|
241
267
|
// NOT the same as `!seatSpace`: a run with no seats[] at all is a legacy run, not a
|
|
242
268
|
// broken one, and must not be bannered.
|
|
243
269
|
seatTableRejected: Array.isArray(run.seats) && run.seats.length > 0 && !isSeatTable(run.seats),
|
|
@@ -75,18 +75,22 @@ function isSeatTable(seats) {
|
|
|
75
75
|
* bound seat a `rebuttal-` an orphan may have written, re-arming RN-1 in the debate
|
|
76
76
|
* namespace (MEASURED; the one regression this fix wave introduced). The question is:
|
|
77
77
|
* does the note POSITIVELY PROVE the orphan did NOT write this kind? Exactly one such
|
|
78
|
-
* proof exists. `orphanLegNote` has three call sites — run-stages.js
|
|
79
|
-
* and its retry
|
|
80
|
-
*
|
|
78
|
+
* proof exists. `orphanLegNote` has three call sites — two in run-stages.js :: runStage1
|
|
79
|
+
* (Stage-1's own bind and its retry pass; cited `:71 and :140` until the v4.9 W9 fix round
|
|
80
|
+
* moved a builder out of that function and both numbers rotted — anchored by SYMBOL now, per
|
|
81
|
+
* the anti-rot rule) and run-stage2.js :: bindStage2Seats (the -s2 judge wave) — and
|
|
82
|
+
* `data.waveId` separates them EXACTLY, not heuristically: run-stage2.js :: bindStage2Seats
|
|
83
|
+
* and the launch site in run-stage2.js :: runStage2 both build it as `${runId}-s2` from
|
|
81
84
|
* the runId this run.json carries. A -s2 note says the leg BOUND in Stage 1, so its review
|
|
82
85
|
* landed under a SEAT name: `review-<alias>.md` is provably not its. Nothing else is
|
|
83
86
|
* provable, and the near misses are why:
|
|
84
87
|
* - a Stage-1 note exonerates NOTHING, not even judge-: that orphan is re-admitted to
|
|
85
|
-
* Stage 2 under a PLACEHOLDER seat
|
|
86
|
-
*
|
|
88
|
+
* Stage 2 under a PLACEHOLDER seat that stage1-bind.js :: bindPaddedWave pads in and
|
|
89
|
+
* drops from `judgeSeatOf` (called from run-stage2.js :: bindStage2Seats since the
|
|
90
|
+
* v4.9 W2 split), so its judge leg takes the alias branch too — and emits no -s2 note,
|
|
87
91
|
* because it BOUND. Measured: run-stages.test.js :: "M2: the placeholder never becomes…".
|
|
88
92
|
* - rebuttal-/revote- are never exonerated: a debate leg whose raiser/judge key names no
|
|
89
|
-
* seat takes materializeDebate's alias branch (run-debate.js ::
|
|
93
|
+
* seat takes materializeDebate's alias branch (run-debate.js :: runDefenseWave;
|
|
90
94
|
* run-debate-revote.js :: runRevoteWave, `seat = seatOf.get(leg) || null`). A rebuttal
|
|
91
95
|
* leg's unbound raiser still has no note recording it. A revote leg's unbound judge
|
|
92
96
|
* SOMETIMES gets one, since v4.8 T5.1 (channel `seat-unbound`, run-debate-revote.js's
|