amicus 3.2.2 → 4.0.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 +78 -0
- package/README.md +15 -3
- package/electron/main.js +4 -1
- package/package.json +3 -1
- package/schemas/abort.schema.json +17 -0
- package/schemas/alias-audit.schema.json +17 -0
- package/schemas/council-run.schema.json +37 -0
- package/schemas/council-stats.schema.json +28 -0
- package/schemas/council-tally.schema.json +70 -0
- package/schemas/council-validate.schema.json +22 -0
- package/schemas/council-verdict.schema.json +47 -0
- package/schemas/doctor.schema.json +29 -0
- package/schemas/error.schema.json +23 -0
- package/schemas/model-catalog.schema.json +19 -0
- package/schemas/run.schema.json +26 -0
- package/schemas/spend.schema.json +16 -0
- package/schemas/wave.schema.json +33 -0
- package/skills/second-opinion/SEAT-BRIEFS.md +5 -3
- package/skills/second-opinion/SKILL.md +8 -0
- package/src/cli-handlers-abort.js +29 -0
- package/src/cli-handlers-council-run.js +168 -0
- package/src/cli-handlers-council.js +8 -5
- package/src/cli-handlers-status.js +35 -4
- package/src/cli.js +9 -0
- package/src/council/anonymize.js +76 -0
- package/src/council/briefings-stage2.js +150 -0
- package/src/council/briefings.js +141 -0
- package/src/council/findings.js +13 -1
- package/src/council/ledger.js +13 -1
- package/src/council/parse-stage2.js +103 -0
- package/src/council/run-assemble.js +100 -0
- package/src/council/run-launch.js +99 -0
- package/src/council/run-stages.js +187 -0
- package/src/council/run-state.js +122 -0
- package/src/council/run.js +269 -0
- package/src/council/tally.js +3 -1
- package/src/council/verdict.js +9 -2
- package/src/headless.js +24 -25
- package/src/mcp-council-run.js +267 -0
- package/src/mcp-server.js +87 -28
- package/src/mcp-tools.js +50 -0
- package/src/prompt-builder.js +36 -19
- package/src/sidecar/electron-lock.js +4 -1
- package/src/sidecar/fanout-leg.js +2 -2
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/resume.js +7 -2
- package/src/utils/abort-result.js +1 -1
- package/src/utils/error-doc.js +2 -0
- package/src/utils/fold-marker.js +21 -0
- package/src/utils/route-error.js +26 -0
- package/src/utils/start-helpers.js +19 -10
- package/src/utils/untrusted-fence.js +8 -7
package/src/prompt-builder.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Spec Reference: §6 Fold Mechanism, §9 Implementation
|
|
5
5
|
* Constructs system prompts for sidecar sessions in both interactive and headless modes.
|
|
6
6
|
*/
|
|
7
|
-
const { buildFoldMarker } = require('./utils/fold-marker');
|
|
7
|
+
const { buildFoldMarker, stripFoldMarkers } = require('./utils/fold-marker');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Summary template for fold output per spec §6.1
|
|
@@ -78,17 +78,19 @@ function buildSystemPrompt(briefing, context, project, headless, mode, client) {
|
|
|
78
78
|
* @param {string} [mode='code'] - Agent mode ('code', 'ask', or 'plan')
|
|
79
79
|
* @param {string} [summaryLength='normal'] - Desired summary length for headless mode
|
|
80
80
|
* @param {string} [client='code-local'] - Client type for branding
|
|
81
|
-
* @param {string} [nonce] - Per-run fold nonce (15b.3, #BL-7 residual).
|
|
82
|
-
* headless mode
|
|
83
|
-
* the legacy bare `[SIDECAR_FOLD]`, so
|
|
84
|
-
* this SAME nonce) can't be forced into
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* calling buildPrompts, pass it here, and pass the SAME value to runHeadless's
|
|
88
|
-
* options.nonce.
|
|
89
|
-
* marker
|
|
90
|
-
*
|
|
81
|
+
* @param {string} [nonce] - Per-run fold nonce (15b.3, #BL-7 residual). REQUIRED in
|
|
82
|
+
* headless mode; ignored in interactive mode. In headless mode the model is instructed
|
|
83
|
+
* to emit `[SIDECAR_FOLD:<nonce>]` instead of the legacy bare `[SIDECAR_FOLD]`, so
|
|
84
|
+
* runHeadless's detector (which must be given this SAME nonce) can't be forced into
|
|
85
|
+
* completing by a model that merely echoes the public, guessable bare marker. Callers
|
|
86
|
+
* that build a headless prompt generate one nonce (utils/fold-marker.generateFoldNonce())
|
|
87
|
+
* BEFORE calling buildPrompts, pass it here, and pass the SAME value to runHeadless's
|
|
88
|
+
* options.nonce. Omitting it in headless mode THROWS (see @throws) — there is no
|
|
89
|
+
* bare-marker fallback on this live path: a real prompt must never advertise the
|
|
90
|
+
* guessable marker. Interactive mode ignores the nonce entirely (GUI fold is
|
|
91
|
+
* exit-code driven, not marker-detected).
|
|
91
92
|
* @returns {{system: string, userMessage: string}} Separated prompts
|
|
93
|
+
* @throws {TypeError} If `headless` is true and no `nonce` is supplied.
|
|
92
94
|
*
|
|
93
95
|
* @example
|
|
94
96
|
* const { system, userMessage } = buildPrompts(
|
|
@@ -101,6 +103,19 @@ function buildSystemPrompt(briefing, context, project, headless, mode, client) {
|
|
|
101
103
|
* // Use: POST /session/:id/message { system, parts: [{ type: 'text', text: userMessage }] }
|
|
102
104
|
*/
|
|
103
105
|
function buildPrompts(briefing, context, project, headless, mode, summaryLength = 'normal', client, nonce) {
|
|
106
|
+
// 15b.3 (#BL-7 residual): a headless run MUST carry a per-run nonce so the
|
|
107
|
+
// model is instructed to emit the unguessable `[SIDECAR_FOLD:<nonce>]` — never
|
|
108
|
+
// the public, guessable bare `[SIDECAR_FOLD]`. buildPrompts is the live
|
|
109
|
+
// orchestration boundary (start / continue / fanout / mcp-server all route
|
|
110
|
+
// through it, and all four already generate and pass a nonce), so a forgotten
|
|
111
|
+
// nonce fails loud HERE rather than silently baking the bare marker into an
|
|
112
|
+
// executed prompt. Mirrors headless.js's producer precedent — extractSummary /
|
|
113
|
+
// formatFoldOutput throw a TypeError when the nonce is missing. Interactive
|
|
114
|
+
// mode is exempt: its fold is exit-code driven, not marker-detected.
|
|
115
|
+
if (headless && !nonce) {
|
|
116
|
+
throw new TypeError('buildPrompts requires a per-run nonce for headless mode (15b.3/v4.0 §9)');
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
const systemSections = [
|
|
105
120
|
buildHeader(client),
|
|
106
121
|
buildEnvironmentSection(project, mode),
|
|
@@ -108,11 +123,11 @@ function buildPrompts(briefing, context, project, headless, mode, summaryLength
|
|
|
108
123
|
];
|
|
109
124
|
|
|
110
125
|
// Strip fold markers from context so the model doesn't mimic them from
|
|
111
|
-
// previous sidecar outputs in the conversation history.
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
const cleanContext = context
|
|
126
|
+
// previous sidecar outputs in the conversation history. stripFoldMarkers
|
|
127
|
+
// (src/utils/fold-marker.js) removes BOTH the legacy bare `[SIDECAR_FOLD]`
|
|
128
|
+
// and any nonced `[SIDECAR_FOLD:<nonce>]` — marker-only lines vanish
|
|
129
|
+
// entirely; inline occurrences are removed in place (v4.0 §9).
|
|
130
|
+
const cleanContext = stripFoldMarkers(context);
|
|
116
131
|
|
|
117
132
|
let userMessage;
|
|
118
133
|
if (headless) {
|
|
@@ -254,9 +269,11 @@ Keep track of key findings as you work.`;
|
|
|
254
269
|
* @param {string} [nonce] - Per-run fold nonce (15b.3, #BL-7 residual). When provided,
|
|
255
270
|
* the model is instructed to emit `[SIDECAR_FOLD:<nonce>]` instead of the legacy bare
|
|
256
271
|
* `[SIDECAR_FOLD]` — see buildPrompts' @param doc for the full rationale. Falls back to
|
|
257
|
-
* the legacy bare marker when omitted
|
|
258
|
-
*
|
|
259
|
-
*
|
|
272
|
+
* the legacy bare marker when omitted. NOTE: buildPrompts — the live orchestration path —
|
|
273
|
+
* now THROWS rather than reach this helper without a nonce in headless mode, so the
|
|
274
|
+
* fallback branch is reachable ONLY via the deprecated buildSystemPrompt(), which has no
|
|
275
|
+
* orchestration-layer caller to source a nonce from and launches no real runs. Its bare
|
|
276
|
+
* marker is therefore inert: nothing on a live run path ever advertises the guessable form.
|
|
260
277
|
* @returns {string}
|
|
261
278
|
*/
|
|
262
279
|
function buildHeadlessModeSection(summaryLength, nonce) {
|
|
@@ -22,7 +22,10 @@ const STALE_MS = 15 * 60 * 1000;
|
|
|
22
22
|
|
|
23
23
|
/** Temp-dir lockfile path, keyed by the electron install dir. */
|
|
24
24
|
function lockPathFor(electronDir) {
|
|
25
|
-
|
|
25
|
+
// Hash the FULL path: a truncated hex prefix collapsed distinct installs that
|
|
26
|
+
// share a leading path segment (C:\Users…, /home/us…) onto one lockfile,
|
|
27
|
+
// defeating per-install isolation. Mirrors src/utils/engine-lock.js.
|
|
28
|
+
const key = require('crypto').createHash('sha1').update(electronDir).digest('hex').slice(0, 16);
|
|
26
29
|
return path.join(os.tmpdir(), `amicus-electron-repair-${key}.lock`);
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -64,7 +64,7 @@ function buildRoutingFailureLeg({ leg, legId, waveId, quiet }) {
|
|
|
64
64
|
* Run one leg end-to-end: session record → runHeadless (shared server) →
|
|
65
65
|
* leg finalize. Never throws — always resolves to a run document.
|
|
66
66
|
*/
|
|
67
|
-
async function runLeg({ leg, legId, waveId, project, systemPrompt, userMessage, timeoutMs, agent, client, server, summaryLength, reasoning, quiet, foldNonce }) {
|
|
67
|
+
async function runLeg({ leg, legId, waveId, project, systemPrompt, userMessage, timeoutMs, agent, client, server, summaryLength, reasoning, quiet, foldNonce, directory }) {
|
|
68
68
|
const { IdleWatchdog } = require('../utils/idle-watchdog');
|
|
69
69
|
const { markAborted } = require('../utils/session-abort');
|
|
70
70
|
const { runHeadless } = require('../headless');
|
|
@@ -107,7 +107,7 @@ async function runLeg({ leg, legId, waveId, project, systemPrompt, userMessage,
|
|
|
107
107
|
result = await runHeadless(
|
|
108
108
|
leg.model, systemPrompt, userMessage, legId, project,
|
|
109
109
|
timeoutMs, agent || 'build',
|
|
110
|
-
{ client, server, watchdog, summaryLength, reasoning, nonce: foldNonce }
|
|
110
|
+
{ client, server, watchdog, summaryLength, reasoning, nonce: foldNonce, directory }
|
|
111
111
|
);
|
|
112
112
|
} catch (err) {
|
|
113
113
|
result = { summary: '', completed: false, timedOut: false, aborted: false, error: err.message, taskId: legId };
|
package/src/sidecar/fanout.js
CHANGED
|
@@ -245,7 +245,7 @@ async function runFanout(options) {
|
|
|
245
245
|
leg, legId: legIds[i], waveId, project, systemPrompt, userMessage,
|
|
246
246
|
timeoutMs, agent: options.agent, client, server,
|
|
247
247
|
summaryLength: options.summaryLength, reasoning, quiet: options.quiet,
|
|
248
|
-
foldNonce,
|
|
248
|
+
foldNonce, directory: options.directory,
|
|
249
249
|
})
|
|
250
250
|
: Promise.resolve(buildRoutingFailureLeg({ leg, legId: legIds[i], waveId, quiet: options.quiet }))
|
|
251
251
|
)));
|
package/src/sidecar/resume.js
CHANGED
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
} = require('./session-utils');
|
|
18
18
|
const { acquireLock, releaseLock } = require('../utils/session-lock');
|
|
19
19
|
const { runHeadless } = require('../headless');
|
|
20
|
-
const { extractNonceFromText, generateFoldNonce } = require('../utils/fold-marker');
|
|
20
|
+
const { extractNonceFromText, generateFoldNonce, stripFoldMarkers } = require('../utils/fold-marker');
|
|
21
21
|
const { logger } = require('../utils/logger');
|
|
22
22
|
|
|
23
23
|
/** Load session metadata from session directory */
|
|
@@ -83,7 +83,12 @@ function buildResumeUserMessage(briefing, conversation) {
|
|
|
83
83
|
|
|
84
84
|
if (conversation) {
|
|
85
85
|
parts.push('## PREVIOUS CONVERSATION\n');
|
|
86
|
-
|
|
86
|
+
// v4.0 §9: the replay used to carry the previous turn's valid nonced
|
|
87
|
+
// [SIDECAR_FOLD:<nonce>] marker verbatim (BACKLOG "Resume nonce-echo
|
|
88
|
+
// hazard"). Each run mints a fresh nonce so the stale marker could never
|
|
89
|
+
// fold the new run, but a wire-format token has no business riding into
|
|
90
|
+
// the new prompt — strip every marker line before embedding.
|
|
91
|
+
parts.push(stripFoldMarkers(conversation));
|
|
87
92
|
parts.push('\n---\n');
|
|
88
93
|
parts.push('## RESUME\n');
|
|
89
94
|
parts.push('You are resuming a previous session. Continue from where you left off.');
|
|
@@ -16,7 +16,7 @@ const { SCHEMA_VERSION } = require('./result-schema-version');
|
|
|
16
16
|
* a specific taskId that exists yet was not running (already terminal) reports
|
|
17
17
|
* ok:false so a scripted caller can tell "nothing happened" from "you aborted N".
|
|
18
18
|
* @param {object} opts
|
|
19
|
-
* @param {'session'|'wave'|'all'} opts.scope
|
|
19
|
+
* @param {'session'|'wave'|'all'|'council-run'} opts.scope
|
|
20
20
|
* @param {string|null} opts.taskId - null for scope:'all'
|
|
21
21
|
* @param {string[]} opts.aborted - ids actually marked aborted (session/wave id + any legs)
|
|
22
22
|
* @returns {object} abort document
|
package/src/utils/error-doc.js
CHANGED
|
@@ -20,6 +20,8 @@ const ERROR_CODES = Object.freeze({
|
|
|
20
20
|
BAD_SESSION: 'BAD_SESSION', // task id missing / invalid / not found
|
|
21
21
|
BUDGET_EXCEEDED: 'BUDGET_EXCEEDED', // the WS-2 #10 spend gate
|
|
22
22
|
INTERNAL: 'INTERNAL', // unexpected pre-flight throw
|
|
23
|
+
COUNCIL_QUORUM: 'COUNCIL_QUORUM', // council run: <2 surviving Stage-1 reviews (v4.0 §4)
|
|
24
|
+
COST_EXCEEDED: 'COST_EXCEEDED', // council run: whole-run --max-cost ceiling hit pre-tally (v4.0 §4)
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
/**
|
package/src/utils/fold-marker.js
CHANGED
|
@@ -70,10 +70,31 @@ function extractNonceFromText(text) {
|
|
|
70
70
|
return m ? m[1] : null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* v4.0 §9 (BL-7 done-done): remove every fold-marker occurrence — bare
|
|
75
|
+
* `[SIDECAR_FOLD]` and nonced `[SIDECAR_FOLD:<nonce>]` — from a text.
|
|
76
|
+
* A line consisting of ONLY a marker (plus horizontal whitespace) is removed
|
|
77
|
+
* together with its line terminator; a marker embedded mid-line is removed in
|
|
78
|
+
* place, keeping the rest of the line. Superset of prompt-builder.js's old
|
|
79
|
+
* inline regex (which left empty lines behind) — used by the resume replay
|
|
80
|
+
* (src/sidecar/resume.js) and buildPrompts (src/prompt-builder.js) so a
|
|
81
|
+
* replayed conversation never carries a stale wire-format token.
|
|
82
|
+
* @param {string|null|undefined} text
|
|
83
|
+
* @returns {string|null|undefined} falsy input is returned unchanged
|
|
84
|
+
*/
|
|
85
|
+
function stripFoldMarkers(text) {
|
|
86
|
+
if (!text) { return text; }
|
|
87
|
+
const marker = `\\[${FOLD_MARKER_PREFIX}(:[^\\]]*)?\\]`;
|
|
88
|
+
const markerOnlyLine = new RegExp(`^[^\\S\\r\\n]*${marker}[^\\S\\r\\n]*(?:\\r?\\n|$)`, 'gm');
|
|
89
|
+
const inlineMarker = new RegExp(marker, 'g');
|
|
90
|
+
return text.replace(markerOnlyLine, '').replace(inlineMarker, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
module.exports = {
|
|
74
94
|
FOLD_MARKER_PREFIX,
|
|
75
95
|
generateFoldNonce,
|
|
76
96
|
buildFoldMarker,
|
|
77
97
|
trailingFoldMarkerRegex,
|
|
78
98
|
extractNonceFromText,
|
|
99
|
+
stripFoldMarkers,
|
|
79
100
|
};
|
package/src/utils/route-error.js
CHANGED
|
@@ -145,9 +145,35 @@ function toCliMessage(result) {
|
|
|
145
145
|
return lines.join('\n');
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* v4.0 §7: map a RouteResult onto error-doc fields ({code, message, hint})
|
|
150
|
+
* for buildErrorDoc/failJson — the CLI --json and MCP failure surfaces.
|
|
151
|
+
* Key-shaped reasons map to MISSING_KEY; everything else is BAD_MODEL.
|
|
152
|
+
* Suggestions are inlined into the message text (the error doc has no
|
|
153
|
+
* structured suggestions slot; the hint carries the fix line).
|
|
154
|
+
* @param {object} result a RouteResult with kind 'error' or 'selection_required'
|
|
155
|
+
* @returns {{code: string, message: string, hint: (string|null)}}
|
|
156
|
+
*/
|
|
157
|
+
function toErrorDocFields(result) {
|
|
158
|
+
const { ERROR_CODES } = require('./error-doc');
|
|
159
|
+
const KEY_REASONS = ['no_openrouter_key', 'no_direct_key', 'no_key_for_vendor'];
|
|
160
|
+
const err = toStructuredError(result);
|
|
161
|
+
const sentence = REASON_TEXT[err.reason] || `Model routing error (${err.reason}).`;
|
|
162
|
+
let message = err.requested ? `${sentence} (requested "${err.requested}")` : sentence;
|
|
163
|
+
if (err.suggestions.length > 0) {
|
|
164
|
+
message += ` Did you mean: ${err.suggestions.map((s) => s && s.model).filter(Boolean).join(', ')}?`;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
code: KEY_REASONS.includes(err.reason) ? ERROR_CODES.MISSING_KEY : ERROR_CODES.BAD_MODEL,
|
|
168
|
+
message,
|
|
169
|
+
hint: FIX_HINTS[err.reason] || null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
module.exports = {
|
|
149
174
|
toStructuredError,
|
|
150
175
|
toCliMessage,
|
|
176
|
+
toErrorDocFields,
|
|
151
177
|
REASON_TEXT,
|
|
152
178
|
ROUTE_ERROR_REASONS,
|
|
153
179
|
SELECTION_REQUIRED_REASON,
|
|
@@ -46,7 +46,8 @@ function deriveAlias(args) {
|
|
|
46
46
|
async function resolveLaunchModel(args) {
|
|
47
47
|
const { resolveGatewayMode } = require('./config');
|
|
48
48
|
const { resolveRouteForLaunch } = require('./route-launch');
|
|
49
|
-
const { toCliMessage,
|
|
49
|
+
const { toCliMessage, toErrorDocFields } = require('./route-error');
|
|
50
|
+
const { failJson, ERROR_CODES } = require('./error-doc');
|
|
50
51
|
const { resolveModelInputOrDefault } = require('./model-input-default');
|
|
51
52
|
|
|
52
53
|
const gatewayMode = resolveGatewayMode(args.gateway);
|
|
@@ -66,9 +67,13 @@ async function resolveLaunchModel(args) {
|
|
|
66
67
|
// via model-input-default.js, so this lookup lives in exactly one place.
|
|
67
68
|
const modelInput = resolveModelInputOrDefault(args.model);
|
|
68
69
|
if (modelInput === undefined) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const message = 'No model specified and no default configured. Run \'amicus setup\' to set a default model.';
|
|
71
|
+
if (args.json) {
|
|
72
|
+
// v4.0 §7: --json pre-flight failures land on STDOUT as the error doc.
|
|
73
|
+
failJson(true, { code: ERROR_CODES.BAD_MODEL, message, hint: 'amicus setup' });
|
|
74
|
+
} else {
|
|
75
|
+
process.stderr.write(`${message}\n`);
|
|
76
|
+
}
|
|
72
77
|
process.exit(1);
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -106,17 +111,21 @@ async function resolveLaunchModel(args) {
|
|
|
106
111
|
provenance: result.provenance || {},
|
|
107
112
|
};
|
|
108
113
|
} catch (err) {
|
|
109
|
-
|
|
114
|
+
const message = err.message || 'Model selection cancelled.';
|
|
115
|
+
if (args.json) {
|
|
116
|
+
failJson(true, { code: ERROR_CODES.BAD_MODEL, message,
|
|
117
|
+
hint: 'Pass --model <vendor/model> explicitly to skip the picker.' });
|
|
118
|
+
} else {
|
|
119
|
+
process.stderr.write(`${message}\n`);
|
|
120
|
+
}
|
|
110
121
|
process.exit(1);
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
// 'error': render and exit.
|
|
115
|
-
//
|
|
116
|
-
// resolves a catalog miss straight to kind:'error' with reason
|
|
117
|
-
// 'model_not_found' instead; see gateway-router.js's catalogGate.)
|
|
125
|
+
// 'error': render and exit. Under --json the error doc goes to STDOUT
|
|
126
|
+
// (v4.0 §7 — was toStructuredError on stderr); human stderr is unchanged.
|
|
118
127
|
if (args.json) {
|
|
119
|
-
|
|
128
|
+
failJson(true, toErrorDocFields(result));
|
|
120
129
|
} else {
|
|
121
130
|
process.stderr.write(`${toCliMessage(result)}\n`);
|
|
122
131
|
}
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* marked as data, not instructions.
|
|
10
10
|
*
|
|
11
11
|
* Applies to every prose channel a sidecar model's output reaches an agent
|
|
12
|
-
* through: MCP amicus_read (summary, wave summary, conversation)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* JSON
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* through: MCP amicus_read (summary, wave summary, conversation), the CLI's
|
|
13
|
+
* non-JSON stdout (read summary/conversation/wave-human, and the foreground
|
|
14
|
+
* start/continue/resume summary echo), and — since v4.0 (H9) — the council
|
|
15
|
+
* MCP tools' JSON returns (amicus_council_tally / amicus_council_stats /
|
|
16
|
+
* amicus_verdict), whose docs embed untrusted model-raised findings; the JSON
|
|
17
|
+
* stays intact inside the fence. It must NOT be applied to CLI --json stdout
|
|
18
|
+
* (the byte-parseable programmatic channel) or amicus_read mode=metadata —
|
|
19
|
+
* structured data a caller parses, where wrapping would break the contract.
|
|
19
20
|
*/
|
|
20
21
|
'use strict';
|
|
21
22
|
|