@wrongstack/core 0.305.1 → 0.306.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.
Files changed (38) hide show
  1. package/dist/coordination/agents/index.js +3 -2
  2. package/dist/coordination/agents/types.d.ts +1 -1
  3. package/dist/coordination/index.js +3 -2
  4. package/dist/core/index.d.ts +2 -1
  5. package/dist/core/index.js +2764 -2632
  6. package/dist/core/system-prompt-blocks.d.ts +1 -1
  7. package/dist/core/system-prompt-builder.d.ts +7 -1
  8. package/dist/core/system-prompt-glossary.d.ts +0 -23
  9. package/dist/defaults/index.js +3 -2
  10. package/dist/execution/index.js +3 -2
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +875 -530
  13. package/dist/observability/index.js +7 -3
  14. package/dist/plugin/index.d.ts +4 -3
  15. package/dist/plugin/index.js +589 -143
  16. package/dist/plugins/auto-review-plugin.d.ts +14 -7
  17. package/dist/plugins/chimera-plugin.d.ts +15 -1
  18. package/dist/plugins/review-finding-integration.d.ts +15 -3
  19. package/dist/plugins/review-finding-parser.d.ts +36 -0
  20. package/dist/plugins/review-finding-types.d.ts +46 -0
  21. package/dist/plugins/review-finding-verification.d.ts +53 -0
  22. package/dist/plugins/review-report-integration.d.ts +1 -0
  23. package/dist/plugins/review-report-store.d.ts +7 -0
  24. package/dist/plugins/review-report-types.d.ts +14 -0
  25. package/dist/plugins/review-types.d.ts +74 -0
  26. package/dist/replay/replay-provider-runner.d.ts +5 -4
  27. package/dist/tools/fallback-manage-tool-options.d.ts +9 -0
  28. package/dist/tools/index.js +91 -38
  29. package/dist/tools/one-shot-llm-tool.d.ts +6 -0
  30. package/dist/types/blocks.d.ts +10 -0
  31. package/dist/utils/index.js +8 -9
  32. package/instructions/agents/browser.md +1 -0
  33. package/instructions/agents/e2e.md +2 -0
  34. package/instructions/llm/chimera-review.md +52 -1
  35. package/instructions/system-lite.md +17 -6
  36. package/instructions/system-pro.md +25 -20
  37. package/instructions/system.md +25 -12
  38. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1242,7 +1242,9 @@ var init_review_report_store = __esm({
1242
1242
  unparseableCount: input.unparseableCount,
1243
1243
  durationSeconds: input.durationSeconds ?? existing.durationSeconds,
1244
1244
  rawText: input.rawText || existing.rawText,
1245
- files: input.files.length > 0 ? input.files : existing.files
1245
+ files: input.files.length > 0 ? input.files : existing.files,
1246
+ ...input.evidenceStatus !== void 0 ? { evidenceStatus: input.evidenceStatus } : {},
1247
+ ...input.evidenceChecks !== void 0 ? { evidenceChecks: input.evidenceChecks } : {}
1246
1248
  };
1247
1249
  await fsp29.appendFile(this.filePath, JSON.stringify({ __report: 1, data: updated }) + NL, {
1248
1250
  encoding: "utf8",
@@ -1265,7 +1267,9 @@ var init_review_report_store = __esm({
1265
1267
  unparseableCount: input.unparseableCount,
1266
1268
  durationSeconds: input.durationSeconds,
1267
1269
  rawText: input.rawText,
1268
- ...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {}
1270
+ ...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {},
1271
+ ...input.evidenceStatus !== void 0 ? { evidenceStatus: input.evidenceStatus } : {},
1272
+ ...input.evidenceChecks !== void 0 ? { evidenceChecks: input.evidenceChecks } : {}
1269
1273
  };
1270
1274
  const createdEvent = {
1271
1275
  id: randomUUID30(),
@@ -1309,6 +1313,24 @@ var init_review_report_store = __esm({
1309
1313
  return { ...entry.report };
1310
1314
  });
1311
1315
  }
1316
+ async updateEvidence(reportId, status, checks) {
1317
+ return withFileLock(this.filePath, async () => {
1318
+ const all = await this._readAll();
1319
+ const entry = all.find((candidate) => candidate.report.id === reportId);
1320
+ if (!entry) throw new Error(`Review report not found: ${reportId}`);
1321
+ const updated = {
1322
+ ...this._materialize(entry),
1323
+ evidenceStatus: status,
1324
+ evidenceChecks: checks
1325
+ };
1326
+ await fsp29.appendFile(
1327
+ this.filePath,
1328
+ `${JSON.stringify({ __report: 1, data: updated })}${NL}`,
1329
+ { encoding: "utf8", mode: SECRET_FILE_MODE }
1330
+ );
1331
+ return updated;
1332
+ });
1333
+ }
1312
1334
  async addNote(reportId, actor, note) {
1313
1335
  return withFileLock(this.filePath, async () => {
1314
1336
  const all = await this._readAll();
@@ -8072,8 +8094,7 @@ function buildConversationContinuityBlock(ctx) {
8072
8094
  "Recent human instructions, oldest to newest. Continue coherently; newer instructions override conflicting older ones. This is context evidence, not a new request.",
8073
8095
  ...lines,
8074
8096
  "[/conversation_continuity]"
8075
- ].join("\n"),
8076
- cache_control: { type: "ephemeral" }
8097
+ ].join("\n")
8077
8098
  };
8078
8099
  }
8079
8100
  function recordToolOutputEvidence(ctx, input) {
@@ -8291,8 +8312,7 @@ function buildCompletedWorkLedgerBlock(ctx) {
8291
8312
  if (items.length === 0) return void 0;
8292
8313
  return {
8293
8314
  type: "text",
8294
- text: formatCompletedWorkLedger(items),
8295
- cache_control: { type: "ephemeral" }
8315
+ text: formatCompletedWorkLedger(items)
8296
8316
  };
8297
8317
  }
8298
8318
  function syncCompletedWorkLedgerBlock(_ctx) {
@@ -10049,218 +10069,6 @@ function providerBoundToRequest(request) {
10049
10069
  return requestProviders.get(request);
10050
10070
  }
10051
10071
 
10052
- // src/core/agent-response.ts
10053
- var MAX_TODO_SNAPSHOT_ITEMS = 10;
10054
- var MAX_TODO_SNAPSHOT_CONTENT = 180;
10055
- function buildLiveNextStepsGateBlock(ctx) {
10056
- if (ctx.agentId !== "leader") return void 0;
10057
- const openTodos = ctx.todos.filter(
10058
- (todo) => todo.status === "pending" || todo.status === "in_progress"
10059
- );
10060
- if (openTodos.length === 0) {
10061
- const toolRoute = ctx.tools?.some((t2) => t2.name === "nextsteps") ? [
10062
- "Calling the `nextsteps` tool with the same items satisfies branch 1 as well; if you both call it and write the block, the block wins."
10063
- ] : [];
10064
- return {
10065
- type: "text",
10066
- text: [
10067
- "[nextsteps_gate]",
10068
- "Authoritative live state for this request: open todos = 0.",
10069
- "On the final response, you MUST take exactly one branch:",
10070
- "1. If at least one genuinely useful follow-on action exists, include a balanced <nextsteps> block containing 1-4 exact prompt messages that can be submitted back to you through the current TUI or WebUI input.",
10071
- "Every item must ask the agent to perform work. Never put a human-only chore or an instruction addressed to the user inside <nextsteps>; natural-language agent-directed imperatives are valid and need not be shell commands.",
10072
- ...toolRoute,
10073
- "2. If no useful follow-on action truly exists, omit <nextsteps> and explicitly tell the user in normal prose that no further steps are needed for this task.",
10074
- "Silently omitting both is invalid. Do not decide by chance, tone, or response length, and do not invent filler suggestions.",
10075
- "[/nextsteps_gate]"
10076
- ].join("\n"),
10077
- cache_control: { type: "ephemeral" }
10078
- };
10079
- }
10080
- const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
10081
- const normalized = todo.content.replace(/\s+/g, " ").trim();
10082
- const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
10083
- return formatTodoForModel({ ...todo, content });
10084
- });
10085
- const omitted = openTodos.length - todoSnapshot.length;
10086
- if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
10087
- const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
10088
- "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state.",
10089
- ...hasKanbanBoundTodos(openTodos) ? [
10090
- "Rows below carry a <kanban board/task> binding. Pass those exact ids back as `kanbanBoardId`/`kanbanTaskId` on every row you resend; a row that arrives without its binding is not applied to its card."
10091
- ] : []
10092
- ] : [];
10093
- return {
10094
- type: "text",
10095
- text: [
10096
- "[nextsteps_gate]",
10097
- `Authoritative live state for this request: open todos = ${openTodos.length}.`,
10098
- "You MUST omit <nextsteps> entirely while these todos remain open. Continue or finish the tracked work; do not propose unrelated follow-on work.",
10099
- ...todoReconciliation,
10100
- "Open todo snapshot:",
10101
- ...todoSnapshot,
10102
- "[/nextsteps_gate]"
10103
- ].join("\n"),
10104
- cache_control: { type: "ephemeral" }
10105
- };
10106
- }
10107
- var MAX_MEMORY_EVIDENCE_CHARS = 12e3;
10108
- function buildMemoryEvidenceBlocks(ctx) {
10109
- const blocks = [];
10110
- let remaining = MAX_MEMORY_EVIDENCE_CHARS;
10111
- for (const entry of ctx.memoryEvidence) {
10112
- if (remaining <= 0) break;
10113
- const text2 = entry.text.trim();
10114
- if (!text2) continue;
10115
- const source = entry.source.replace(/[^a-z0-9_.-]+/gi, "-").slice(0, 80) || "memory";
10116
- const bounded = text2.slice(0, remaining);
10117
- remaining -= bounded.length;
10118
- blocks.push({
10119
- type: "text",
10120
- text: `[memory_evidence source="${source}"]
10121
- ${bounded}
10122
- [/memory_evidence]`,
10123
- cache_control: { type: "ephemeral" }
10124
- });
10125
- }
10126
- return blocks;
10127
- }
10128
- function createAgentResponseHandler(a) {
10129
- const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
10130
- function stabilizePromptEpoch() {
10131
- const prompt = a.ctx.systemPrompt;
10132
- if (stabilizedPromptEpochs.has(prompt)) return;
10133
- for (const block of prompt) {
10134
- if (block.cache_control) Object.freeze(block.cache_control);
10135
- Object.freeze(block);
10136
- }
10137
- Object.freeze(prompt);
10138
- stabilizedPromptEpochs.add(prompt);
10139
- }
10140
- async function buildAndRunRequestPipeline(opts) {
10141
- if (a.ctx.toolAdjacencyDirty) {
10142
- const repaired = repairToolUseAdjacency(a.ctx.messages);
10143
- a.ctx.toolAdjacencyDirty = false;
10144
- if (repaired.report.changed) {
10145
- a.ctx.state.replaceMessages(repaired.messages);
10146
- a.events.emit("context.repaired", {
10147
- sessionId: resolveEventSessionId(a.ctx),
10148
- ctx: a.ctx,
10149
- ...repaired.report
10150
- });
10151
- a.logger.warn(
10152
- `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
10153
- );
10154
- }
10155
- }
10156
- stabilizePromptEpoch();
10157
- const volatileLedger = buildCompletedWorkLedgerBlock(a.ctx);
10158
- const continuity = buildConversationContinuityBlock(a.ctx);
10159
- const liveNextStepsGate = buildLiveNextStepsGateBlock(a.ctx);
10160
- const memoryEvidence = buildMemoryEvidenceBlocks(a.ctx);
10161
- const volatileBlocks = [
10162
- volatileLedger,
10163
- continuity,
10164
- liveNextStepsGate,
10165
- ...memoryEvidence
10166
- ].filter((block) => block !== void 0);
10167
- const system = volatileBlocks.length > 0 ? [...a.ctx.systemPrompt, ...volatileBlocks] : a.ctx.systemPrompt;
10168
- await a.ctx.waitForModelTransition();
10169
- const provider = a.ctx.provider;
10170
- const baseReq = {
10171
- model: opts.model ?? a.ctx.model,
10172
- system,
10173
- messages: a.ctx.messages,
10174
- tools: a.tools.listForProvider(),
10175
- // `maxTokens` is deliberately NOT set here. The provider adapter
10176
- // resolves the ceiling from the catalog entry for the model in
10177
- // `req.model`, which is the only source that stays correct across a
10178
- // `/model` switch, a fallback hop, or a subagent on a model-matrix
10179
- // entry — `provider.capabilities` is resolved once, for the model the
10180
- // session booted on, and pinning it here would override the accurate
10181
- // per-request value with a stale one. Callers that genuinely want a
10182
- // smaller response (one-shot LLM helpers, compaction, the brain) still
10183
- // set `maxTokens` on their own Request and keep priority over the
10184
- // catalog.
10185
- // Provider-agnostic cache-partition key from the stable prompt epoch.
10186
- // Wires that support prompt caching (OpenAI `prompt_cache_key`) read it;
10187
- // the config `ttl` is merged over this by the ModelRuntime middleware.
10188
- cache: { key: deriveCachePrefixKey(a.ctx.systemPrompt) }
10189
- };
10190
- const request = await a.pipelines.request.run(baseReq);
10191
- bindRequestProvider(request, provider);
10192
- return { request, provider };
10193
- }
10194
- async function processResponse(raw, req, requestProvider = a.ctx.provider) {
10195
- let res = raw;
10196
- res = await a.pipelines.response.run(res);
10197
- res = maybeAppendPendingNextSteps(a.ctx, res);
10198
- a.events.emit("provider.response", {
10199
- sessionId: resolveEventSessionId(a.ctx),
10200
- ctx: a.ctx,
10201
- model: req.model,
10202
- content: res.content,
10203
- usage: res.usage,
10204
- stopReason: res.stopReason
10205
- });
10206
- a.ctx.tokenCounter.account(res.usage, req.model, requestProvider.id);
10207
- if (hasMeaningfulContent(res.content)) {
10208
- await a.ctx.session.append({
10209
- type: "llm_response",
10210
- ts: (/* @__PURE__ */ new Date()).toISOString(),
10211
- content: res.content,
10212
- stopReason: res.stopReason,
10213
- usage: res.usage
10214
- });
10215
- a.ctx.state.appendMessage({ role: "assistant", content: res.content });
10216
- if (!a.ctx.toolAdjacencyDirty) {
10217
- for (const block of res.content) {
10218
- if (block.type === "tool_use") {
10219
- a.ctx.toolAdjacencyDirty = true;
10220
- break;
10221
- }
10222
- }
10223
- }
10224
- try {
10225
- await a.ctx.flushConversationJournal();
10226
- await a.ctx.session.flush();
10227
- } catch (err) {
10228
- (a.logger.debug ?? a.logger.warn)?.(`LLM response flush failed: ${toErrorMessage(err)}`);
10229
- }
10230
- } else {
10231
- a.logger.warn("Empty assistant response \u2014 not appended to context or session", {
10232
- model: req.model,
10233
- stopReason: res.stopReason,
10234
- aborted: a.ctx.signal.aborted
10235
- });
10236
- }
10237
- if (a.ctx.signal.aborted) {
10238
- const parts2 = [];
10239
- for (const block of res.content) {
10240
- if (isTextBlock(block)) parts2.push(block.text);
10241
- }
10242
- return { finalText: parts2.join(""), aborted: true, done: false };
10243
- }
10244
- const parts = [];
10245
- const streamed = requestProvider.capabilities.streaming;
10246
- for (const block of res.content) {
10247
- if (isTextBlock(block)) {
10248
- const rendered = await a.pipelines.assistantOutput.run(block);
10249
- parts.push(rendered.text);
10250
- if (!streamed) a.renderer?.write(rendered);
10251
- }
10252
- }
10253
- const finalText = parts.join("");
10254
- markAssistantReferencedEvidence(a.ctx, finalText);
10255
- let directive = "none";
10256
- if (finalText) {
10257
- directive = parseContinueDirective(finalText);
10258
- }
10259
- return { finalText, aborted: false, done: false, directive };
10260
- }
10261
- return { buildAndRunRequestPipeline, processResponse };
10262
- }
10263
-
10264
10072
  // src/types/runtime-capability-manifest.ts
10265
10073
  var PLAYWRIGHT_ALIASES = {
10266
10074
  playwright_navigate: "browser_navigate",
@@ -10543,6 +10351,493 @@ function runtimeToolReferencesFromText(text2) {
10543
10351
  return [...references];
10544
10352
  }
10545
10353
 
10354
+ // src/core/instruction-template.ts
10355
+ var CANONICAL_TOOL_NAMES = new Set(
10356
+ RUNTIME_CAPABILITY_MANIFEST.flatMap((entry) => [...entry.tools])
10357
+ );
10358
+ var DIRECTIVE_RE = /[ \t]*<!--\s*ws:(if|else|end)\b([^>]*?)-->[ \t]*(?:\r?\n)?/g;
10359
+ var PLACEHOLDER_RE = /\{\{\s*(tools:)?\s*([a-zA-Z0-9_.,\s-]+?)\s*\}\}/g;
10360
+ function renderInstructionLayer(text2, ctx) {
10361
+ if (!text2) return text2;
10362
+ const hasDirectives = text2.includes("<!--ws:") || text2.includes("<!-- ws:");
10363
+ const hasPlaceholders = text2.includes("{{");
10364
+ if (!hasDirectives && !hasPlaceholders) return text2;
10365
+ const rendered = hasDirectives ? emit(parse2(text2), ctx) : text2;
10366
+ const substituted = hasPlaceholders ? substitute(rendered, ctx) : rendered;
10367
+ const guarded = ctx?.strictToolReferences ? dropLinesWithUnavailableToolReferences(
10368
+ substituted,
10369
+ ctx,
10370
+ /* @__PURE__ */ new Set([...CANONICAL_TOOL_NAMES, ...declaredToolNames(text2)])
10371
+ ) : substituted;
10372
+ return tidy(guarded);
10373
+ }
10374
+ function declaredToolNames(text2) {
10375
+ const names = /* @__PURE__ */ new Set();
10376
+ for (const marker of text2.matchAll(/<!--\s*ws:if\b([^>]*?)-->/g)) {
10377
+ for (const attr of (marker[1] ?? "").matchAll(/!?tool=([A-Za-z0-9_.,-]+)/g)) {
10378
+ for (const name of (attr[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10379
+ }
10380
+ }
10381
+ for (const placeholder of text2.matchAll(/\{\{\s*tools:\s*([^}]+)}}/g)) {
10382
+ for (const name of (placeholder[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10383
+ }
10384
+ return names;
10385
+ }
10386
+ function dropLinesWithUnavailableToolReferences(text2, ctx, declared) {
10387
+ const unavailable = [...declared].filter((name) => !ctx.toolNames.has(name));
10388
+ if (unavailable.length === 0) return text2;
10389
+ return text2.split(/(?<=\n)/).filter((line) => !unavailable.some((name) => formattedToolMention(line, name))).join("");
10390
+ }
10391
+ function formattedToolMention(line, name) {
10392
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10393
+ const token = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`);
10394
+ if (line.split("`").some((segment, index) => {
10395
+ if (index % 2 !== 1) return false;
10396
+ if (segment.includes(`<${name}`) || segment.includes(`</${name}`)) return false;
10397
+ return token.test(segment);
10398
+ })) {
10399
+ return true;
10400
+ }
10401
+ return line.split("**").some((segment, index) => index % 2 === 1 && segment.trim() === name);
10402
+ }
10403
+ function parse2(text2) {
10404
+ const root = [];
10405
+ const stack = [];
10406
+ const current = () => {
10407
+ const frame = stack[stack.length - 1];
10408
+ if (!frame) return root;
10409
+ return frame.branches[frame.branches.length - 1];
10410
+ };
10411
+ const pushText = (value) => {
10412
+ if (value) current().push({ kind: "text", value });
10413
+ };
10414
+ DIRECTIVE_RE.lastIndex = 0;
10415
+ let cursor = 0;
10416
+ for (let m = DIRECTIVE_RE.exec(text2); m !== null; m = DIRECTIVE_RE.exec(text2)) {
10417
+ pushText(text2.slice(cursor, m.index));
10418
+ cursor = m.index + m[0].length;
10419
+ const keyword = m[1];
10420
+ if (keyword === "if") {
10421
+ stack.push({ test: parseCondition(m[2] ?? ""), branches: [[]] });
10422
+ } else if (keyword === "else") {
10423
+ const frame = stack[stack.length - 1];
10424
+ if (frame && frame.branches.length === 1) frame.branches.push([]);
10425
+ } else {
10426
+ const frame = stack.pop();
10427
+ if (frame) current().push({ kind: "if", test: frame.test, body: frame.branches });
10428
+ }
10429
+ }
10430
+ pushText(text2.slice(cursor));
10431
+ while (stack.length > 0) {
10432
+ const frame = stack.pop();
10433
+ current().push(...frame.branches.flat());
10434
+ }
10435
+ return root;
10436
+ }
10437
+ function parseCondition(raw) {
10438
+ const tokens = raw.trim().split(/\s+/).filter(Boolean);
10439
+ if (tokens.length === 0) return null;
10440
+ const attrs = [];
10441
+ for (const token of tokens) {
10442
+ const m = /^(!?)([a-zA-Z]+)=(.+)$/.exec(token);
10443
+ if (!m) return null;
10444
+ const key = (m[2] ?? "").toLowerCase();
10445
+ if (key !== "tool" && key !== "tier" && key !== "role") return null;
10446
+ const values = (m[3] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
10447
+ if (values.length === 0) return null;
10448
+ attrs.push({ key, negated: m[1] === "!", values });
10449
+ }
10450
+ return attrs;
10451
+ }
10452
+ function evaluate(test, ctx) {
10453
+ if (test === null || !ctx) return true;
10454
+ return test.every((attr) => {
10455
+ const matched = attr.key === "tool" ? attr.values.some((v) => ctx.toolNames.has(v)) : attr.key === "tier" ? attr.values.includes(ctx.tier) : attr.values.includes(ctx.subagent ? "subagent" : "leader");
10456
+ return attr.negated ? !matched : matched;
10457
+ });
10458
+ }
10459
+ function emit(nodes, ctx) {
10460
+ let out = "";
10461
+ for (const node of nodes) {
10462
+ if (node.kind === "text") {
10463
+ out += node.value;
10464
+ continue;
10465
+ }
10466
+ const branch = evaluate(node.test, ctx) ? node.body[0] : node.body[1];
10467
+ if (branch) out += emit(branch, ctx);
10468
+ }
10469
+ return out;
10470
+ }
10471
+ function substitute(text2, ctx) {
10472
+ PLACEHOLDER_RE.lastIndex = 0;
10473
+ return text2.replace(PLACEHOLDER_RE, (match, toolsPrefix, body) => {
10474
+ if (toolsPrefix) {
10475
+ const names = body.split(",").map((n) => n.trim()).filter(Boolean).filter((n) => !ctx || ctx.toolNames.has(n));
10476
+ return names.map((n) => `\`${n}\``).join(", ");
10477
+ }
10478
+ const value = ctx?.vars?.[body.trim()];
10479
+ return value === void 0 ? match : String(value);
10480
+ });
10481
+ }
10482
+ function tidy(text2) {
10483
+ return text2.replace(/(\r?\n){3,}/g, "$1$1");
10484
+ }
10485
+
10486
+ // src/core/system-prompt-blocks.ts
10487
+ var SYSTEM_BLOCK_SOURCE = /* @__PURE__ */ new WeakMap();
10488
+ function tagBlock(block, source) {
10489
+ SYSTEM_BLOCK_SOURCE.set(block, source);
10490
+ return block;
10491
+ }
10492
+ function shortSessionId(sessionId) {
10493
+ const leaf = sessionId.split("/").pop() ?? sessionId;
10494
+ return leaf.length > 12 ? `${leaf.slice(0, 12)}\u2026` : leaf;
10495
+ }
10496
+ function instructionSection(bundle, key, vars = {}, tplCtx) {
10497
+ const template = bundle.sections?.[key];
10498
+ if (!template) return "";
10499
+ return renderInstructionLayer(
10500
+ template,
10501
+ tplCtx ? { ...tplCtx, vars: { ...tplCtx.vars, ...vars } } : void 0
10502
+ ).replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, name) => {
10503
+ const value = vars[name];
10504
+ return value === void 0 ? match : String(value);
10505
+ });
10506
+ }
10507
+ function renderToolSelectionBoundary(tool) {
10508
+ const selection = tool.selection;
10509
+ if (!selection?.doNotUseWhen.trim()) return "";
10510
+ const alternatives = selection.useInstead?.filter(Boolean) ?? [];
10511
+ const instead = alternatives.length > 0 ? ` Use ${alternatives.map((name) => `\`${name}\``).join(" or ")} instead.` : "";
10512
+ return `Do not use when ${selection.doNotUseWhen.trim()}${instead}`;
10513
+ }
10514
+ function agentsFingerprint(agents) {
10515
+ if (!agents || agents.length === 0) return "0";
10516
+ let h = 2166136261;
10517
+ for (const a of agents) {
10518
+ const fields = [
10519
+ a.agentId,
10520
+ a.name,
10521
+ a.source,
10522
+ a.sessionId,
10523
+ a.status,
10524
+ a.currentTask,
10525
+ a.currentTool,
10526
+ a.online ? "1" : "0"
10527
+ ];
10528
+ for (const field of fields) {
10529
+ const value = field ?? "";
10530
+ for (let i = 0; i < value.length; i++) {
10531
+ h ^= value.charCodeAt(i);
10532
+ h = Math.imul(h, 16777619) >>> 0;
10533
+ }
10534
+ h ^= 255;
10535
+ h = Math.imul(h, 16777619) >>> 0;
10536
+ }
10537
+ }
10538
+ return `${agents.length}:${h.toString(36)}`;
10539
+ }
10540
+
10541
+ // src/core/agent-response.ts
10542
+ var MAX_TODO_SNAPSHOT_ITEMS = 10;
10543
+ var MAX_TODO_SNAPSHOT_CONTENT = 180;
10544
+ function buildLiveNextStepsGateBlock(ctx) {
10545
+ if (ctx.agentId !== "leader") return void 0;
10546
+ const openTodos = ctx.todos.filter(
10547
+ (todo) => todo.status === "pending" || todo.status === "in_progress"
10548
+ );
10549
+ if (openTodos.length === 0) {
10550
+ const toolRoute = ctx.tools?.some((t2) => t2.name === "nextsteps") ? [
10551
+ "Calling the `nextsteps` tool with the same items satisfies branch 1 as well; if you both call it and write the block, the block wins."
10552
+ ] : [];
10553
+ return {
10554
+ type: "text",
10555
+ text: [
10556
+ "[nextsteps_gate]",
10557
+ "Authoritative live state for this request: open todos = 0.",
10558
+ "On the final response, you MUST take exactly one branch:",
10559
+ "1. If at least one genuinely useful follow-on action exists, include a balanced <nextsteps> block containing 1-4 exact prompt messages that can be submitted back to you through the current TUI or WebUI input.",
10560
+ "Every item must ask the agent to perform work. Never put a human-only chore or an instruction addressed to the user inside <nextsteps>; natural-language agent-directed imperatives are valid and need not be shell commands.",
10561
+ ...toolRoute,
10562
+ "2. If no useful follow-on action truly exists, omit <nextsteps> and explicitly tell the user in normal prose that no further steps are needed for this task.",
10563
+ "Silently omitting both is invalid. Do not decide by chance, tone, or response length, and do not invent filler suggestions.",
10564
+ "[/nextsteps_gate]"
10565
+ ].join("\n")
10566
+ };
10567
+ }
10568
+ const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
10569
+ const normalized = todo.content.replace(/\s+/g, " ").trim();
10570
+ const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
10571
+ return formatTodoForModel({ ...todo, content });
10572
+ });
10573
+ const omitted = openTodos.length - todoSnapshot.length;
10574
+ if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
10575
+ const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
10576
+ "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state.",
10577
+ ...hasKanbanBoundTodos(openTodos) ? [
10578
+ "Rows below carry a <kanban board/task> binding. Pass those exact ids back as `kanbanBoardId`/`kanbanTaskId` on every row you resend; a row that arrives without its binding is not applied to its card."
10579
+ ] : []
10580
+ ] : [];
10581
+ return {
10582
+ type: "text",
10583
+ text: [
10584
+ "[nextsteps_gate]",
10585
+ `Authoritative live state for this request: open todos = ${openTodos.length}.`,
10586
+ "You MUST omit <nextsteps> entirely while these todos remain open. Continue or finish the tracked work; do not propose unrelated follow-on work.",
10587
+ ...todoReconciliation,
10588
+ "Open todo snapshot:",
10589
+ ...todoSnapshot,
10590
+ "[/nextsteps_gate]"
10591
+ ].join("\n")
10592
+ };
10593
+ }
10594
+ var MAX_MEMORY_EVIDENCE_CHARS = 12e3;
10595
+ function buildMemoryEvidenceBlocks(ctx) {
10596
+ const blocks = [];
10597
+ let remaining = MAX_MEMORY_EVIDENCE_CHARS;
10598
+ for (const entry of ctx.memoryEvidence) {
10599
+ if (remaining <= 0) break;
10600
+ const text2 = entry.text.trim();
10601
+ if (!text2) continue;
10602
+ const source = entry.source.replace(/[^a-z0-9_.-]+/gi, "-").slice(0, 80) || "memory";
10603
+ const bounded = text2.slice(0, remaining);
10604
+ remaining -= bounded.length;
10605
+ blocks.push({
10606
+ type: "text",
10607
+ text: `[memory_evidence source="${source}"]
10608
+ ${bounded}
10609
+ [/memory_evidence]`
10610
+ });
10611
+ }
10612
+ return blocks;
10613
+ }
10614
+ var EPOCH_VOLATILE_SOURCES = /* @__PURE__ */ new Set([
10615
+ "plan",
10616
+ "contributor",
10617
+ "glossary",
10618
+ "peers"
10619
+ ]);
10620
+ var promptEpochPartitions = /* @__PURE__ */ new WeakMap();
10621
+ function partitionPromptEpoch(prompt) {
10622
+ const cached2 = promptEpochPartitions.get(prompt);
10623
+ if (cached2) return cached2;
10624
+ const stable2 = [];
10625
+ const tail = [];
10626
+ for (const block of prompt) {
10627
+ const source = SYSTEM_BLOCK_SOURCE.get(block);
10628
+ if (source && EPOCH_VOLATILE_SOURCES.has(source)) {
10629
+ tail.push({ type: "text", text: block.text });
10630
+ } else {
10631
+ stable2.push(block);
10632
+ }
10633
+ }
10634
+ const partition = { stable: stable2, tail };
10635
+ promptEpochPartitions.set(prompt, partition);
10636
+ return partition;
10637
+ }
10638
+ var LIVE_CONTEXT_HEADER = {
10639
+ type: "text",
10640
+ text: "[live_context]\nThe blocks below are live session state (active plan, glossary, completed-work ledger, conversation continuity, response gates, memory evidence) re-sent with every request. They are steering context, not a new user message. Where they conflict with the conversation, newer conversation turns win."
10641
+ };
10642
+ var NEXT_STEPS_BLOCK_RE = /<nextsteps\b[^>]*>[\s\S]*?<\/nextsteps>[ \t]*\n?/gi;
10643
+ var NEXT_STEPS_STRIPPED_PLACEHOLDER = "[nextsteps suggestions were delivered to the user]";
10644
+ var strippedNextStepsCache = /* @__PURE__ */ new WeakMap();
10645
+ function stripDeliveredNextSteps(history) {
10646
+ let out = null;
10647
+ for (let i = 0; i < history.length; i++) {
10648
+ const msg = history[i];
10649
+ const replaced = stripNextStepsFromMessage(msg);
10650
+ if (out === null && replaced !== msg) out = history.slice(0, i);
10651
+ if (out !== null) out.push(replaced);
10652
+ }
10653
+ return out ?? history;
10654
+ }
10655
+ function stripNextStepsFromMessage(msg) {
10656
+ if (msg.role !== "assistant") return msg;
10657
+ const cached2 = strippedNextStepsCache.get(msg);
10658
+ if (cached2) return cached2;
10659
+ const hasTag = typeof msg.content === "string" ? msg.content.includes("<nextsteps") : msg.content.some((b) => b.type === "text" && b.text.includes("<nextsteps"));
10660
+ if (!hasTag) {
10661
+ strippedNextStepsCache.set(msg, msg);
10662
+ return msg;
10663
+ }
10664
+ let clone;
10665
+ if (typeof msg.content === "string") {
10666
+ const text2 = msg.content.replace(NEXT_STEPS_BLOCK_RE, "").trimEnd();
10667
+ clone = { ...msg, content: text2.length > 0 ? text2 : NEXT_STEPS_STRIPPED_PLACEHOLDER };
10668
+ } else {
10669
+ const blocks = msg.content.map(
10670
+ (b) => b.type === "text" && b.text.includes("<nextsteps") ? { ...b, text: b.text.replace(NEXT_STEPS_BLOCK_RE, "").trimEnd() } : b
10671
+ ).filter((b) => b.type !== "text" || b.text.trim().length > 0);
10672
+ clone = {
10673
+ ...msg,
10674
+ content: blocks.length > 0 ? blocks : [{ type: "text", text: NEXT_STEPS_STRIPPED_PLACEHOLDER }]
10675
+ };
10676
+ }
10677
+ strippedNextStepsCache.set(msg, clone);
10678
+ return clone;
10679
+ }
10680
+ function composeRequestMessages(history, tail) {
10681
+ if (history.length === 0) return null;
10682
+ const out = history.slice();
10683
+ const lastIdx = out.length - 1;
10684
+ const last = out[lastIdx];
10685
+ const blocks = typeof last.content === "string" ? [{ type: "text", text: last.content }] : last.content.slice();
10686
+ const boundary = blocks[blocks.length - 1];
10687
+ if (boundary && (boundary.type === "text" || boundary.type === "tool_result")) {
10688
+ blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
10689
+ }
10690
+ if (tail.length === 0 || last.role !== "user") {
10691
+ out[lastIdx] = { ...last, content: blocks };
10692
+ if (tail.length > 0) out.push({ role: "user", content: [LIVE_CONTEXT_HEADER, ...tail] });
10693
+ return out;
10694
+ }
10695
+ out[lastIdx] = { ...last, content: [...blocks, LIVE_CONTEXT_HEADER, ...tail] };
10696
+ return out;
10697
+ }
10698
+ function createAgentResponseHandler(a) {
10699
+ const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
10700
+ function stabilizePromptEpoch() {
10701
+ const prompt = a.ctx.systemPrompt;
10702
+ if (stabilizedPromptEpochs.has(prompt)) return;
10703
+ for (const block of prompt) {
10704
+ if (block.cache_control) Object.freeze(block.cache_control);
10705
+ Object.freeze(block);
10706
+ }
10707
+ Object.freeze(prompt);
10708
+ stabilizedPromptEpochs.add(prompt);
10709
+ }
10710
+ async function buildAndRunRequestPipeline(opts) {
10711
+ if (a.ctx.toolAdjacencyDirty) {
10712
+ const repaired = repairToolUseAdjacency(a.ctx.messages);
10713
+ a.ctx.toolAdjacencyDirty = false;
10714
+ if (repaired.report.changed) {
10715
+ a.ctx.state.replaceMessages(repaired.messages);
10716
+ a.events.emit("context.repaired", {
10717
+ sessionId: resolveEventSessionId(a.ctx),
10718
+ ctx: a.ctx,
10719
+ ...repaired.report
10720
+ });
10721
+ a.logger.warn(
10722
+ `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
10723
+ );
10724
+ }
10725
+ }
10726
+ stabilizePromptEpoch();
10727
+ const { stable: stableSystem, tail: epochTail } = partitionPromptEpoch(a.ctx.systemPrompt);
10728
+ const volatileLedger = buildCompletedWorkLedgerBlock(a.ctx);
10729
+ const continuity = buildConversationContinuityBlock(a.ctx);
10730
+ const liveNextStepsGate = buildLiveNextStepsGateBlock(a.ctx);
10731
+ const memoryEvidence = buildMemoryEvidenceBlocks(a.ctx);
10732
+ const liveContextTail = [
10733
+ ...epochTail,
10734
+ volatileLedger,
10735
+ continuity,
10736
+ liveNextStepsGate,
10737
+ ...memoryEvidence
10738
+ ].filter((block) => block !== void 0);
10739
+ const requestHistory = stripDeliveredNextSteps(a.ctx.messages);
10740
+ const composedMessages = composeRequestMessages(requestHistory, liveContextTail);
10741
+ const system = composedMessages ? stableSystem : liveContextTail.length > 0 ? [...stableSystem, ...liveContextTail] : stableSystem;
10742
+ await a.ctx.waitForModelTransition();
10743
+ const provider = a.ctx.provider;
10744
+ const baseReq = {
10745
+ model: opts.model ?? a.ctx.model,
10746
+ system,
10747
+ messages: composedMessages ?? requestHistory,
10748
+ tools: a.tools.listForProvider(),
10749
+ // `maxTokens` is deliberately NOT set here. The provider adapter
10750
+ // resolves the ceiling from the catalog entry for the model in
10751
+ // `req.model`, which is the only source that stays correct across a
10752
+ // `/model` switch, a fallback hop, or a subagent on a model-matrix
10753
+ // entry — `provider.capabilities` is resolved once, for the model the
10754
+ // session booted on, and pinning it here would override the accurate
10755
+ // per-request value with a stale one. Callers that genuinely want a
10756
+ // smaller response (one-shot LLM helpers, compaction, the brain) still
10757
+ // set `maxTokens` on their own Request and keep priority over the
10758
+ // catalog.
10759
+ // Provider-agnostic cache-partition key from the STABLE part of the
10760
+ // prompt epoch. Wires that support prompt caching (OpenAI
10761
+ // `prompt_cache_key`) read it; the config `ttl` is merged over this by
10762
+ // the ModelRuntime middleware. Keyed off the stable partition, not the
10763
+ // full epoch — a glossary/plan refresh must not re-route the cache
10764
+ // partition when the actual prefix bytes did not change.
10765
+ cache: { key: deriveCachePrefixKey(stableSystem) }
10766
+ };
10767
+ const request = await a.pipelines.request.run(baseReq);
10768
+ bindRequestProvider(request, provider);
10769
+ return { request, provider };
10770
+ }
10771
+ async function processResponse(raw, req, requestProvider = a.ctx.provider) {
10772
+ let res = raw;
10773
+ res = await a.pipelines.response.run(res);
10774
+ res = maybeAppendPendingNextSteps(a.ctx, res);
10775
+ a.events.emit("provider.response", {
10776
+ sessionId: resolveEventSessionId(a.ctx),
10777
+ ctx: a.ctx,
10778
+ model: req.model,
10779
+ content: res.content,
10780
+ usage: res.usage,
10781
+ stopReason: res.stopReason
10782
+ });
10783
+ a.ctx.tokenCounter.account(res.usage, req.model, requestProvider.id);
10784
+ if (hasMeaningfulContent(res.content)) {
10785
+ await a.ctx.session.append({
10786
+ type: "llm_response",
10787
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10788
+ content: res.content,
10789
+ stopReason: res.stopReason,
10790
+ usage: res.usage
10791
+ });
10792
+ a.ctx.state.appendMessage({ role: "assistant", content: res.content });
10793
+ if (!a.ctx.toolAdjacencyDirty) {
10794
+ for (const block of res.content) {
10795
+ if (block.type === "tool_use") {
10796
+ a.ctx.toolAdjacencyDirty = true;
10797
+ break;
10798
+ }
10799
+ }
10800
+ }
10801
+ try {
10802
+ await a.ctx.flushConversationJournal();
10803
+ await a.ctx.session.flush();
10804
+ } catch (err) {
10805
+ (a.logger.debug ?? a.logger.warn)?.(`LLM response flush failed: ${toErrorMessage(err)}`);
10806
+ }
10807
+ } else {
10808
+ a.logger.warn("Empty assistant response \u2014 not appended to context or session", {
10809
+ model: req.model,
10810
+ stopReason: res.stopReason,
10811
+ aborted: a.ctx.signal.aborted
10812
+ });
10813
+ }
10814
+ if (a.ctx.signal.aborted) {
10815
+ const parts2 = [];
10816
+ for (const block of res.content) {
10817
+ if (isTextBlock(block)) parts2.push(block.text);
10818
+ }
10819
+ return { finalText: parts2.join(""), aborted: true, done: false };
10820
+ }
10821
+ const parts = [];
10822
+ const streamed = requestProvider.capabilities.streaming;
10823
+ for (const block of res.content) {
10824
+ if (isTextBlock(block)) {
10825
+ const rendered = await a.pipelines.assistantOutput.run(block);
10826
+ parts.push(rendered.text);
10827
+ if (!streamed) a.renderer?.write(rendered);
10828
+ }
10829
+ }
10830
+ const finalText = parts.join("");
10831
+ markAssistantReferencedEvidence(a.ctx, finalText);
10832
+ let directive = "none";
10833
+ if (finalText) {
10834
+ directive = parseContinueDirective(finalText);
10835
+ }
10836
+ return { finalText, aborted: false, done: false, directive };
10837
+ }
10838
+ return { buildAndRunRequestPipeline, processResponse };
10839
+ }
10840
+
10546
10841
  // src/types/system-prompt.ts
10547
10842
  function flattenSystemPromptRegions(regions) {
10548
10843
  return [...regions.core, ...regions.session, ...regions.volatile];
@@ -10723,138 +11018,6 @@ function firstExistingDirSync(candidates) {
10723
11018
  return candidates[0] ?? "";
10724
11019
  }
10725
11020
 
10726
- // src/core/instruction-template.ts
10727
- var CANONICAL_TOOL_NAMES = new Set(
10728
- RUNTIME_CAPABILITY_MANIFEST.flatMap((entry) => [...entry.tools])
10729
- );
10730
- var DIRECTIVE_RE = /[ \t]*<!--\s*ws:(if|else|end)\b([^>]*?)-->[ \t]*(?:\r?\n)?/g;
10731
- var PLACEHOLDER_RE = /\{\{\s*(tools:)?\s*([a-zA-Z0-9_.,\s-]+?)\s*\}\}/g;
10732
- function renderInstructionLayer(text2, ctx) {
10733
- if (!text2) return text2;
10734
- const hasDirectives = text2.includes("<!--ws:") || text2.includes("<!-- ws:");
10735
- const hasPlaceholders = text2.includes("{{");
10736
- if (!hasDirectives && !hasPlaceholders) return text2;
10737
- const rendered = hasDirectives ? emit(parse2(text2), ctx) : text2;
10738
- const substituted = hasPlaceholders ? substitute(rendered, ctx) : rendered;
10739
- const guarded = ctx?.strictToolReferences ? dropLinesWithUnavailableToolReferences(
10740
- substituted,
10741
- ctx,
10742
- /* @__PURE__ */ new Set([...CANONICAL_TOOL_NAMES, ...declaredToolNames(text2)])
10743
- ) : substituted;
10744
- return tidy(guarded);
10745
- }
10746
- function declaredToolNames(text2) {
10747
- const names = /* @__PURE__ */ new Set();
10748
- for (const marker of text2.matchAll(/<!--\s*ws:if\b([^>]*?)-->/g)) {
10749
- for (const attr of (marker[1] ?? "").matchAll(/!?tool=([A-Za-z0-9_.,-]+)/g)) {
10750
- for (const name of (attr[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10751
- }
10752
- }
10753
- for (const placeholder of text2.matchAll(/\{\{\s*tools:\s*([^}]+)}}/g)) {
10754
- for (const name of (placeholder[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10755
- }
10756
- return names;
10757
- }
10758
- function dropLinesWithUnavailableToolReferences(text2, ctx, declared) {
10759
- const unavailable = [...declared].filter((name) => !ctx.toolNames.has(name));
10760
- if (unavailable.length === 0) return text2;
10761
- return text2.split(/(?<=\n)/).filter((line) => !unavailable.some((name) => formattedToolMention(line, name))).join("");
10762
- }
10763
- function formattedToolMention(line, name) {
10764
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10765
- const token = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`);
10766
- if (line.split("`").some((segment, index) => {
10767
- if (index % 2 !== 1) return false;
10768
- if (segment.includes(`<${name}`) || segment.includes(`</${name}`)) return false;
10769
- return token.test(segment);
10770
- })) {
10771
- return true;
10772
- }
10773
- return line.split("**").some((segment, index) => index % 2 === 1 && segment.trim() === name);
10774
- }
10775
- function parse2(text2) {
10776
- const root = [];
10777
- const stack = [];
10778
- const current = () => {
10779
- const frame = stack[stack.length - 1];
10780
- if (!frame) return root;
10781
- return frame.branches[frame.branches.length - 1];
10782
- };
10783
- const pushText = (value) => {
10784
- if (value) current().push({ kind: "text", value });
10785
- };
10786
- DIRECTIVE_RE.lastIndex = 0;
10787
- let cursor = 0;
10788
- for (let m = DIRECTIVE_RE.exec(text2); m !== null; m = DIRECTIVE_RE.exec(text2)) {
10789
- pushText(text2.slice(cursor, m.index));
10790
- cursor = m.index + m[0].length;
10791
- const keyword = m[1];
10792
- if (keyword === "if") {
10793
- stack.push({ test: parseCondition(m[2] ?? ""), branches: [[]] });
10794
- } else if (keyword === "else") {
10795
- const frame = stack[stack.length - 1];
10796
- if (frame && frame.branches.length === 1) frame.branches.push([]);
10797
- } else {
10798
- const frame = stack.pop();
10799
- if (frame) current().push({ kind: "if", test: frame.test, body: frame.branches });
10800
- }
10801
- }
10802
- pushText(text2.slice(cursor));
10803
- while (stack.length > 0) {
10804
- const frame = stack.pop();
10805
- current().push(...frame.branches.flat());
10806
- }
10807
- return root;
10808
- }
10809
- function parseCondition(raw) {
10810
- const tokens = raw.trim().split(/\s+/).filter(Boolean);
10811
- if (tokens.length === 0) return null;
10812
- const attrs = [];
10813
- for (const token of tokens) {
10814
- const m = /^(!?)([a-zA-Z]+)=(.+)$/.exec(token);
10815
- if (!m) return null;
10816
- const key = (m[2] ?? "").toLowerCase();
10817
- if (key !== "tool" && key !== "tier" && key !== "role") return null;
10818
- const values = (m[3] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
10819
- if (values.length === 0) return null;
10820
- attrs.push({ key, negated: m[1] === "!", values });
10821
- }
10822
- return attrs;
10823
- }
10824
- function evaluate(test, ctx) {
10825
- if (test === null || !ctx) return true;
10826
- return test.every((attr) => {
10827
- const matched = attr.key === "tool" ? attr.values.some((v) => ctx.toolNames.has(v)) : attr.key === "tier" ? attr.values.includes(ctx.tier) : attr.values.includes(ctx.subagent ? "subagent" : "leader");
10828
- return attr.negated ? !matched : matched;
10829
- });
10830
- }
10831
- function emit(nodes, ctx) {
10832
- let out = "";
10833
- for (const node of nodes) {
10834
- if (node.kind === "text") {
10835
- out += node.value;
10836
- continue;
10837
- }
10838
- const branch = evaluate(node.test, ctx) ? node.body[0] : node.body[1];
10839
- if (branch) out += emit(branch, ctx);
10840
- }
10841
- return out;
10842
- }
10843
- function substitute(text2, ctx) {
10844
- PLACEHOLDER_RE.lastIndex = 0;
10845
- return text2.replace(PLACEHOLDER_RE, (match, toolsPrefix, body) => {
10846
- if (toolsPrefix) {
10847
- const names = body.split(",").map((n) => n.trim()).filter(Boolean).filter((n) => !ctx || ctx.toolNames.has(n));
10848
- return names.map((n) => `\`${n}\``).join(", ");
10849
- }
10850
- const value = ctx?.vars?.[body.trim()];
10851
- return value === void 0 ? match : String(value);
10852
- });
10853
- }
10854
- function tidy(text2) {
10855
- return text2.replace(/(\r?\n){3,}/g, "$1$1");
10856
- }
10857
-
10858
11021
  // src/core/modes/default.ts
10859
11022
  import { readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
10860
11023
  import * as path23 from "node:path";
@@ -10889,61 +11052,6 @@ function isDirectory(candidate) {
10889
11052
  }
10890
11053
  }
10891
11054
 
10892
- // src/core/system-prompt-blocks.ts
10893
- var SYSTEM_BLOCK_SOURCE = /* @__PURE__ */ new WeakMap();
10894
- function tagBlock(block, source) {
10895
- SYSTEM_BLOCK_SOURCE.set(block, source);
10896
- return block;
10897
- }
10898
- function shortSessionId(sessionId) {
10899
- const leaf = sessionId.split("/").pop() ?? sessionId;
10900
- return leaf.length > 12 ? `${leaf.slice(0, 12)}\u2026` : leaf;
10901
- }
10902
- function instructionSection(bundle, key, vars = {}, tplCtx) {
10903
- const template = bundle.sections?.[key];
10904
- if (!template) return "";
10905
- return renderInstructionLayer(
10906
- template,
10907
- tplCtx ? { ...tplCtx, vars: { ...tplCtx.vars, ...vars } } : void 0
10908
- ).replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, name) => {
10909
- const value = vars[name];
10910
- return value === void 0 ? match : String(value);
10911
- });
10912
- }
10913
- function renderToolSelectionBoundary(tool) {
10914
- const selection = tool.selection;
10915
- if (!selection?.doNotUseWhen.trim()) return "";
10916
- const alternatives = selection.useInstead?.filter(Boolean) ?? [];
10917
- const instead = alternatives.length > 0 ? ` Use ${alternatives.map((name) => `\`${name}\``).join(" or ")} instead.` : "";
10918
- return `Do not use when ${selection.doNotUseWhen.trim()}${instead}`;
10919
- }
10920
- function agentsFingerprint(agents) {
10921
- if (!agents || agents.length === 0) return "0";
10922
- let h = 2166136261;
10923
- for (const a of agents) {
10924
- const fields = [
10925
- a.agentId,
10926
- a.name,
10927
- a.source,
10928
- a.sessionId,
10929
- a.status,
10930
- a.currentTask,
10931
- a.currentTool,
10932
- a.online ? "1" : "0"
10933
- ];
10934
- for (const field of fields) {
10935
- const value = field ?? "";
10936
- for (let i = 0; i < value.length; i++) {
10937
- h ^= value.charCodeAt(i);
10938
- h = Math.imul(h, 16777619) >>> 0;
10939
- }
10940
- h ^= 255;
10941
- h = Math.imul(h, 16777619) >>> 0;
10942
- }
10943
- }
10944
- return `${agents.length}:${h.toString(36)}`;
10945
- }
10946
-
10947
11055
  // src/core/system-prompt-environment.ts
10948
11056
  import * as os6 from "node:os";
10949
11057
  import * as path25 from "node:path";
@@ -11231,24 +11339,14 @@ async function renderDomainGlossary(ctx, memory, options = {}) {
11231
11339
  );
11232
11340
  return lines.join("\n");
11233
11341
  }
11234
- function makeDomainGlossaryContributor(glossary) {
11235
- return async (ctx) => {
11236
- const text2 = await renderDomainGlossary(ctx, glossary.memory);
11237
- if (!text2) return [];
11238
- return [{ type: "text", text: text2 }];
11239
- };
11240
- }
11241
11342
  function parseTermEntry(text2) {
11242
11343
  const trimmed = text2.trim();
11243
- const separators = [" \u2014 ", " \u2013 ", " - ", " -- "];
11244
- for (const sep10 of separators) {
11245
- const idx = trimmed.indexOf(sep10);
11246
- if (idx > 0) {
11247
- return {
11248
- term: trimmed.slice(0, idx).trim(),
11249
- definition: trimmed.slice(idx + sep10.length).trim()
11250
- };
11251
- }
11344
+ const idx = trimmed.indexOf(" \u2014 ");
11345
+ if (idx > 0) {
11346
+ return {
11347
+ term: trimmed.slice(0, idx).trim(),
11348
+ definition: trimmed.slice(idx + 3).trim()
11349
+ };
11252
11350
  }
11253
11351
  return { term: trimmed, definition: "" };
11254
11352
  }
@@ -11792,7 +11890,13 @@ var DefaultSystemPromptBuilder = class {
11792
11890
  _lastCatalogTools;
11793
11891
  /** Cached rendered online agents string, keyed by content fingerprint. */
11794
11892
  _lastOnlineAgents;
11795
- /** Cached full buildToolUsage output — keyed by tools array ref + agents fingerprint + tier. */
11893
+ /**
11894
+ * Cached full buildToolUsage output — keyed by tools array ref + tier.
11895
+ * Deliberately NOT keyed by the online-agents fingerprint: the live peer
11896
+ * snapshot moved out of this layer into the `peers` volatile block, so
11897
+ * layer2 stays byte-stable (and provider-cache-friendly) while agents
11898
+ * join, leave, or change status.
11899
+ */
11796
11900
  _toolsUsageCache;
11797
11901
  _instructionBundle;
11798
11902
  /**
@@ -11957,6 +12061,26 @@ var DefaultSystemPromptBuilder = class {
11957
12061
  volatile.push(tagBlock({ type: "text", text: glossary }, "glossary"));
11958
12062
  }
11959
12063
  }
12064
+ const hasMailboxTools = ctx.tools.some(
12065
+ (t2) => t2.name === "mailbox" || t2.name === "mail_send" || t2.name === "mail_inbox"
12066
+ );
12067
+ if (hasMailboxTools) {
12068
+ const peers = this.renderOnlineAgents(ctx.onlineAgents).trim();
12069
+ if (peers) {
12070
+ volatile.push(
12071
+ tagBlock(
12072
+ {
12073
+ type: "text",
12074
+ text: `[online_agents]
12075
+ Live fleet peer snapshot for this request (see the Inter-agent mailbox guidance for how to coordinate):
12076
+ ${peers}
12077
+ [/online_agents]`
12078
+ },
12079
+ "peers"
12080
+ )
12081
+ );
12082
+ }
12083
+ }
11960
12084
  if (!ctx.subagent) {
11961
12085
  session.push(
11962
12086
  tagBlock(
@@ -12051,9 +12175,8 @@ var DefaultSystemPromptBuilder = class {
12051
12175
  const instructions = await this.instructions();
12052
12176
  const tpl = tplCtx ?? this.templateContext(ctx);
12053
12177
  const section = (key, vars = {}) => instructionSection(instructions, key, vars, tpl);
12054
- const agentsHash = agentsFingerprint(ctx.onlineAgents);
12055
12178
  const tier = this.tier;
12056
- if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.agentsHash === agentsHash && this._toolsUsageCache?.tier === tier) {
12179
+ if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier) {
12057
12180
  return this._toolsUsageCache.text;
12058
12181
  }
12059
12182
  const byCat = /* @__PURE__ */ new Map();
@@ -12131,7 +12254,7 @@ ${hint.trim()}`);
12131
12254
  (t2) => t2.name === "mailbox" || t2.name === "mail_send" || t2.name === "mail_inbox"
12132
12255
  );
12133
12256
  if (hasMailbox) {
12134
- const onlineAgentsInfo = this.renderOnlineAgents(ctx.onlineAgents);
12257
+ const onlineAgentsInfo = "";
12135
12258
  const hasMailboxPowerTool = tools.some((t2) => t2.name === "mailbox");
12136
12259
  const mailStatusCommand = tools.some((t2) => t2.name === "fleet_status") ? "`fleet_status`" : hasMailboxPowerTool ? "`mailbox action=status` or `mailbox action=online`" : "the online-agent list above";
12137
12260
  const mailInboxCommand = tools.some((t2) => t2.name === "mail_inbox") ? "`mail_inbox`" : "`mailbox action=check`";
@@ -12180,7 +12303,7 @@ ${hint.trim()}`);
12180
12303
  }
12181
12304
  }
12182
12305
  const text2 = lines.join("\n");
12183
- this._toolsUsageCache = { toolsRef: tools, agentsHash, tier, text: text2 };
12306
+ this._toolsUsageCache = { toolsRef: tools, tier, text: text2 };
12184
12307
  return text2;
12185
12308
  }
12186
12309
  renderOnlineAgents(agents) {
@@ -12240,6 +12363,8 @@ var SYSTEM_BLOCK_SOURCES = [
12240
12363
  "leader-after-task",
12241
12364
  "contributor",
12242
12365
  "ledger",
12366
+ "glossary",
12367
+ "peers",
12243
12368
  "nextsteps",
12244
12369
  "other"
12245
12370
  ];
@@ -23658,6 +23783,7 @@ var TOOLS = {
23658
23783
  "glob",
23659
23784
  "search",
23660
23785
  "tree",
23786
+ "diff",
23661
23787
  "write",
23662
23788
  "edit",
23663
23789
  "replace",
@@ -24689,7 +24815,7 @@ var VERIFY_AGENTS = [
24689
24815
  id: "e2e",
24690
24816
  name: "E2E",
24691
24817
  role: "e2e",
24692
- tools: [...TOOLS.build, "fetch", ...SPECIALIST_TOOLS.browser],
24818
+ tools: [...TOOLS.build, "fetch", "e2e_plan", ...SPECIALIST_TOOLS.browser],
24693
24819
  prompt: agentPrompt("e2e")
24694
24820
  },
24695
24821
  budget: HEAVY_BUDGET,
@@ -25202,7 +25328,7 @@ var DOMAIN_AGENTS = [
25202
25328
  id: "designer",
25203
25329
  name: "Designer",
25204
25330
  role: "designer",
25205
- tools: [...TOOLS.docs],
25331
+ tools: [...TOOLS.docs, "design"],
25206
25332
  prompt: agentPrompt("designer")
25207
25333
  },
25208
25334
  budget: MEDIUM_BUDGET,
@@ -62878,7 +63004,10 @@ var SENSITIVE_FLAG_PATTERNS = [
62878
63004
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
62879
63005
  // The separator group is optional so the attached form (the common one) matches.
62880
63006
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
62881
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
63007
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
63008
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
63009
+ // redacted, not just the first. Synced with packages/tools _redact-command.ts.
63010
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
62882
63011
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
62883
63012
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
62884
63013
  // redaction function (false positive = cosmetic noise; false negative = leak).
@@ -62886,8 +63015,9 @@ var SENSITIVE_FLAG_PATTERNS = [
62886
63015
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
62887
63016
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
62888
63017
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
62889
- // when preceded by a flag name (e.g. --github-token=EyJ...).
62890
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
63018
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
63019
+ // every such flag in the command line is redacted, not just the first.
63020
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
62891
63021
  ];
62892
63022
  function redactCommand(cmd) {
62893
63023
  let result = cmd;
@@ -80134,7 +80264,10 @@ var DefaultPluginAPI = class {
80134
80264
  }
80135
80265
  };
80136
80266
  this.tools = {
80137
- register: (t2) => tr.register(t2, owner),
80267
+ register: (t2) => {
80268
+ tr.register(t2, owner);
80269
+ tr.exposeToProvider(t2.name);
80270
+ },
80138
80271
  unregister: (name) => {
80139
80272
  assertCanMutateTool(name, "unregister");
80140
80273
  return tr.unregister(name);
@@ -81212,6 +81345,15 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
81212
81345
  maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH
81213
81346
  };
81214
81347
  }
81348
+ function severitiesFromFindings(findings) {
81349
+ const severities = { critical: 0, high: 0, medium: 0 };
81350
+ for (const finding of findings) {
81351
+ if (finding.severity === "critical") severities.critical++;
81352
+ else if (finding.severity === "high") severities.high++;
81353
+ else if (finding.severity === "medium") severities.medium++;
81354
+ }
81355
+ return severities;
81356
+ }
81215
81357
  function parseReviewSeverity(text2) {
81216
81358
  const result = { critical: 0, high: 0, medium: 0 };
81217
81359
  if (!text2) return result;
@@ -81228,9 +81370,20 @@ function parseReviewSeverity(text2) {
81228
81370
  }
81229
81371
  return result;
81230
81372
  }
81231
- function decideCascadeAgents(text2, severities) {
81373
+ function decideCascadeAgents(text2, severities, findings) {
81232
81374
  const agents = /* @__PURE__ */ new Set();
81233
81375
  if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
81376
+ if (findings && findings.length > 0) {
81377
+ const highPlus = findings.filter(
81378
+ (finding) => finding.severity === "critical" || finding.severity === "high"
81379
+ );
81380
+ if (highPlus.some((finding) => finding.category === "security")) {
81381
+ agents.add("security-scanner");
81382
+ }
81383
+ if (highPlus.every((finding) => finding.category !== void 0)) {
81384
+ return [...agents];
81385
+ }
81386
+ }
81234
81387
  const securityKeywords = [
81235
81388
  "injection",
81236
81389
  "xss",
@@ -81603,7 +81756,9 @@ function createAutoReviewPlugin() {
81603
81756
  maxFiles: cfg.maxFilesPerBatch,
81604
81757
  autoFix: "off",
81605
81758
  cascadeOn: "off",
81606
- maxCascadeDepth: 0
81759
+ maxCascadeDepth: 0,
81760
+ fallbackModels: [...cfg.fallbackModels],
81761
+ fallbackProfile: void 0
81607
81762
  },
81608
81763
  files: filesWithContent,
81609
81764
  activeTodos: ctxTodos,
@@ -81708,7 +81863,9 @@ function createAutoReviewPlugin() {
81708
81863
  maxFiles: cfg.maxFilesPerBatch,
81709
81864
  autoFix: "off",
81710
81865
  cascadeOn: "off",
81711
- maxCascadeDepth: 0
81866
+ maxCascadeDepth: 0,
81867
+ fallbackModels: [...cfg.fallbackModels],
81868
+ fallbackProfile: void 0
81712
81869
  },
81713
81870
  files: filesWithContent,
81714
81871
  cascadeOn: cfg.cascadeOn,
@@ -81750,23 +81907,31 @@ function createAutoReviewPlugin() {
81750
81907
  if (!p.reviewText) return;
81751
81908
  const cascadeOn = p.bundle.cascadeOn ?? "off";
81752
81909
  if (cascadeOn === "off") return;
81753
- const severities = parseReviewSeverity(p.reviewText);
81910
+ const parsed = p.parsedReport;
81911
+ const verifiedFindings = parsed?.findings.filter((f) => f.verification?.status === "verified") ?? [];
81912
+ const severities = parsed ? severitiesFromFindings(verifiedFindings) : parseReviewSeverity(p.reviewText);
81754
81913
  const threshold = shouldCascade(cascadeOn, severities);
81755
81914
  if (!threshold) return;
81756
- const agents = decideCascadeAgents(p.reviewText, severities);
81915
+ const agents = decideCascadeAgents(
81916
+ p.reviewText,
81917
+ severities,
81918
+ parsed ? verifiedFindings : void 0
81919
+ );
81757
81920
  if (agents.length === 0) {
81758
81921
  return;
81759
81922
  }
81760
81923
  const cascadePayload = {
81761
81924
  bundle: p.bundle,
81925
+ ...p.reportId ? { reportId: p.reportId } : {},
81762
81926
  reviewText: p.reviewText,
81763
81927
  severities,
81764
81928
  threshold,
81765
- agents
81929
+ agents,
81930
+ ...parsed ? { verifiedFindings } : {}
81766
81931
  };
81767
81932
  api.emitCustom("chimera.cascade_needed", cascadePayload);
81768
81933
  api.log.info(
81769
- `[auto-review] cascade_needed emitted \u2014 ${severities.critical} critical, ${severities.high} high, ${severities.medium} medium; agents: ${agents.join(", ")}`
81934
+ `[auto-review] cascade_needed emitted \u2014 ${severities.critical} critical, ${severities.high} high, ${severities.medium} medium; agents: ${agents.join(", ")}${parsed ? ` (gated on ${verifiedFindings.length} verified finding(s))` : ""}`
81770
81935
  );
81771
81936
  } catch (err) {
81772
81937
  api.log.warn(
@@ -81798,12 +81963,88 @@ init_review_finding_store();
81798
81963
  // src/plugins/review-finding-parser.ts
81799
81964
  init_review_finding_types();
81800
81965
  import { randomUUID as randomUUID40 } from "node:crypto";
81966
+ var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
81967
+ var CATEGORIES = /* @__PURE__ */ new Set([
81968
+ "bug",
81969
+ "security",
81970
+ "performance",
81971
+ "type",
81972
+ "contract",
81973
+ "test",
81974
+ "other"
81975
+ ]);
81976
+ var CONFIDENCES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
81977
+ var FENCED_BLOCK = /```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi;
81978
+ function extractStructuredFindingsBlock(reportText) {
81979
+ if (!reportText) return null;
81980
+ let best = null;
81981
+ for (const match of reportText.matchAll(FENCED_BLOCK)) {
81982
+ const body = match[1];
81983
+ if (!body?.trim()) continue;
81984
+ let parsed;
81985
+ try {
81986
+ parsed = JSON.parse(body);
81987
+ } catch {
81988
+ continue;
81989
+ }
81990
+ if (typeof parsed !== "object" || parsed === null) continue;
81991
+ const findings = parsed.findings;
81992
+ if (!Array.isArray(findings)) continue;
81993
+ const items = [];
81994
+ for (const raw of findings) {
81995
+ const item = normalizeStructuredItem(raw);
81996
+ if (item) items.push(item);
81997
+ }
81998
+ if (findings.length > 0 && items.length === 0) continue;
81999
+ const durationRaw = parsed.durationSeconds;
82000
+ const durationSeconds = typeof durationRaw === "number" && Number.isFinite(durationRaw) && durationRaw > 0 ? Math.floor(durationRaw) : void 0;
82001
+ best = { findings: items, ...durationSeconds !== void 0 ? { durationSeconds } : {} };
82002
+ }
82003
+ return best;
82004
+ }
82005
+ function normalizeStructuredItem(raw) {
82006
+ if (typeof raw !== "object" || raw === null) return null;
82007
+ const item = raw;
82008
+ const severity = typeof item.severity === "string" ? item.severity.toLowerCase() : "";
82009
+ if (!SEVERITIES.has(severity)) return null;
82010
+ const title = typeof item.title === "string" ? item.title.trim() : "";
82011
+ if (title.length === 0) return null;
82012
+ const file = typeof item.file === "string" && item.file.trim().length > 0 ? item.file.trim() : void 0;
82013
+ const line = typeof item.line === "number" && Number.isInteger(item.line) && item.line >= 1 ? item.line : void 0;
82014
+ const categoryRaw = typeof item.category === "string" ? item.category.toLowerCase() : "";
82015
+ const category = CATEGORIES.has(categoryRaw) ? categoryRaw : void 0;
82016
+ const confidenceRaw = typeof item.confidence === "string" ? item.confidence.toLowerCase() : "";
82017
+ const confidence = CONFIDENCES.has(confidenceRaw) ? confidenceRaw : void 0;
82018
+ return {
82019
+ severity,
82020
+ ...file ? { file } : {},
82021
+ ...line !== void 0 ? { line } : {},
82022
+ ...category ? { category } : {},
82023
+ ...confidence ? { confidence } : {},
82024
+ title,
82025
+ ...typeof item.description === "string" && item.description.trim().length > 0 ? { description: item.description.trim() } : {},
82026
+ ...typeof item.suggestedFix === "string" && item.suggestedFix.trim().length > 0 ? { suggestedFix: item.suggestedFix.trim() } : {}
82027
+ };
82028
+ }
81801
82029
  var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
81802
82030
  var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
81803
82031
  function parseChimeraReviewReport(reportText, context = {}) {
81804
82032
  if (!reportText || reportText.trim().length === 0) {
81805
82033
  return { findings: [], unparseableCount: 0 };
81806
82034
  }
82035
+ const structured = extractStructuredFindingsBlock(reportText);
82036
+ if (structured) {
82037
+ const reportId2 = context.reportId ?? randomUUID40();
82038
+ const findings2 = structured.findings.map(
82039
+ (item) => buildFindingFromStructuredItem(item, { ...context, reportId: reportId2 })
82040
+ );
82041
+ return {
82042
+ findings: findings2,
82043
+ unparseableCount: 0,
82044
+ ...structured.durationSeconds !== void 0 ? { durationSeconds: structured.durationSeconds } : {},
82045
+ structured: true
82046
+ };
82047
+ }
81807
82048
  const findings = [];
81808
82049
  const reportId = context.reportId ?? randomUUID40();
81809
82050
  let unparseableCount = 0;
@@ -81935,14 +82176,92 @@ function normalizeFindingSource(reviewType) {
81935
82176
  return "chimera";
81936
82177
  }
81937
82178
  }
82179
+ function buildFindingFromStructuredItem(item, context) {
82180
+ const file = item.file;
82181
+ const line = item.line;
82182
+ const title = item.title;
82183
+ const description = item.description ?? title;
82184
+ return {
82185
+ id: randomUUID40(),
82186
+ fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
82187
+ severity: item.severity,
82188
+ source: normalizeFindingSource(context.reviewType),
82189
+ ...file ? { location: { file, ...line !== void 0 ? { line } : {} } } : {},
82190
+ ...item.category ? { category: item.category } : {},
82191
+ ...item.confidence ? { confidence: item.confidence } : {},
82192
+ title,
82193
+ description,
82194
+ ...item.suggestedFix ? { suggestedFix: item.suggestedFix } : {},
82195
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
82196
+ status: "active",
82197
+ originReport: {
82198
+ reportId: context.reportId ?? randomUUID40(),
82199
+ sessionId: context.sessionId ?? "",
82200
+ agentId: context.agentId ?? "",
82201
+ reviewerModel: context.reviewerModel ?? ""
82202
+ }
82203
+ };
82204
+ }
81938
82205
 
81939
82206
  // src/plugins/review-report-integration.ts
81940
82207
  init_review_finding_store();
82208
+
82209
+ // src/plugins/review-finding-integration.ts
82210
+ init_review_finding_store();
82211
+ function classifyChimeraReviewSource(bundle) {
82212
+ const cascadeDepth = bundle.cascadeDepth ?? 0;
82213
+ if (cascadeDepth > 0) return "cascade";
82214
+ const cascadeOn = bundle.cascadeOn;
82215
+ if (cascadeOn !== void 0 && cascadeOn !== "off") return "auto";
82216
+ return "chimera";
82217
+ }
82218
+ async function integrateFindings(payload, projectDir, reportId) {
82219
+ if ((!payload.reviewText || payload.reviewText.trim().length === 0) && !payload.parsedReport) {
82220
+ return { created: 0, relinked: 0, reopened: 0, totalFindings: 0, unparseableCount: 0 };
82221
+ }
82222
+ const store = new JsonlFindingStore(projectDir);
82223
+ const source = classifyChimeraReviewSource(payload.bundle);
82224
+ const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
82225
+ const sessionId = payload.sessionId ?? payload.cwd;
82226
+ const model = payload.bundle.config.model;
82227
+ const parsed = payload.parsedReport ?? parseChimeraReviewReport(payload.reviewText, {
82228
+ sessionId,
82229
+ agentId,
82230
+ reviewerModel: model,
82231
+ reviewType: source,
82232
+ reportId
82233
+ });
82234
+ if (parsed.findings.length === 0) {
82235
+ return {
82236
+ created: 0,
82237
+ relinked: 0,
82238
+ reopened: 0,
82239
+ totalFindings: 0,
82240
+ unparseableCount: parsed.unparseableCount
82241
+ };
82242
+ }
82243
+ const result = await store.upsert(parsed.findings, {
82244
+ sessionId,
82245
+ reportId,
82246
+ agentId,
82247
+ model
82248
+ });
82249
+ return {
82250
+ created: result.created,
82251
+ relinked: result.relinked,
82252
+ reopened: result.reopened,
82253
+ reportId,
82254
+ totalFindings: parsed.findings.length,
82255
+ unparseableCount: parsed.unparseableCount
82256
+ };
82257
+ }
82258
+
82259
+ // src/plugins/review-report-integration.ts
81941
82260
  init_review_report_store();
81942
82261
  async function persistReviewReport(payload, reportId, projectDir) {
81943
82262
  const store = new JsonlReportStore(projectDir);
81944
82263
  const existed = await store.get(reportId);
81945
- const source = classifySource(payload);
82264
+ const source = classifyChimeraReviewSource(payload.bundle);
81946
82265
  const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
81947
82266
  const sessionId = payload.sessionId ?? payload.cwd;
81948
82267
  const model = payload.bundle.config.model;
@@ -81952,7 +82271,13 @@ async function persistReviewReport(payload, reportId, projectDir) {
81952
82271
  status: f.status
81953
82272
  }));
81954
82273
  const reviewStatus = payload.status === "success" ? "success" : "failed";
81955
- const parsed = reviewStatus === "success" ? parseChimeraReviewReport(payload.reviewText, { reportId }) : { findings: [], unparseableCount: 0, durationSeconds: void 0 };
82274
+ const parsed = reviewStatus === "success" ? payload.parsedReport ?? parseChimeraReviewReport(payload.reviewText, {
82275
+ sessionId,
82276
+ agentId,
82277
+ reviewerModel: model,
82278
+ reviewType: source,
82279
+ reportId
82280
+ }) : { findings: [], unparseableCount: 0, durationSeconds: void 0 };
81956
82281
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
81957
82282
  for (const finding of parsed.findings) {
81958
82283
  counts[finding.severity]++;
@@ -81970,7 +82295,12 @@ async function persistReviewReport(payload, reportId, projectDir) {
81970
82295
  unparseableCount: parsed.unparseableCount,
81971
82296
  durationSeconds: parsed.durationSeconds,
81972
82297
  rawText: payload.reviewText,
81973
- ...cascadeDepth !== void 0 ? { cascadeDepth } : {}
82298
+ ...cascadeDepth !== void 0 ? { cascadeDepth } : {},
82299
+ // P0-3: carry the cascade evidence verification (status + per-check
82300
+ // comparisons) so the persisted report is auditable. Absent on initial
82301
+ // reviews — no cascade step produced evidence yet.
82302
+ ...payload.bundle.evidenceStatus !== void 0 ? { evidenceStatus: payload.bundle.evidenceStatus } : {},
82303
+ ...payload.bundle.evidenceChecks !== void 0 ? { evidenceChecks: payload.bundle.evidenceChecks } : {}
81974
82304
  };
81975
82305
  await store.persist(input);
81976
82306
  if (reviewStatus === "success" && parsed.findings.length === 0 && parsed.unparseableCount === 0 && isExplicitAllClearReview(payload.reviewText)) {
@@ -82033,13 +82363,6 @@ async function syncReportReopen(reportId, projectDir, actor, reason) {
82033
82363
  });
82034
82364
  return { reportId, reopened: true, previousLifecycle: report.lifecycle };
82035
82365
  }
82036
- function classifySource(payload) {
82037
- const cascadeDepth = payload.bundle.cascadeDepth ?? 0;
82038
- if (cascadeDepth > 0) return "cascade";
82039
- const cascadeOn = payload.bundle.cascadeOn;
82040
- if (cascadeOn !== void 0 && cascadeOn !== "off") return "auto";
82041
- return "chimera";
82042
- }
82043
82366
 
82044
82367
  // src/plugins/review-finding-commands.ts
82045
82368
  async function executeFindingCommand(args, ctx) {
@@ -82369,6 +82692,18 @@ async function showReport(id, ctx) {
82369
82692
  `**Review status:** ${report.reviewStatus}`,
82370
82693
  ...report.cascadeDepth !== void 0 ? [`**Cascade depth:** ${report.cascadeDepth}`] : [],
82371
82694
  ...report.durationSeconds !== void 0 ? [`**Duration:** ${report.durationSeconds}s`] : [],
82695
+ ...report.evidenceStatus !== void 0 ? [
82696
+ `**Evidence:** ${report.evidenceStatus === "verified" ? "\u2705 verified" : report.evidenceStatus === "failed" ? "\u274C failed" : "\u26A0\uFE0F missing"}`,
82697
+ ...report.evidenceChecks && report.evidenceChecks.length > 0 ? [
82698
+ "",
82699
+ ...report.evidenceChecks.map((check) => {
82700
+ const mark = check.ok ? "\u2713" : "\u2717";
82701
+ const claimed = check.claimedExitCode ?? "\u2014";
82702
+ const actual = check.actualExitCode ?? "\u2014";
82703
+ return ` ${mark} \`${check.name}\` \u2014 \`${check.command}\` (claimed ${claimed}, observed ${actual})`;
82704
+ })
82705
+ ] : []
82706
+ ] : [],
82372
82707
  "",
82373
82708
  "**Severity counts:**",
82374
82709
  ` \u{1F534} Critical: ${report.counts.critical}`,
@@ -82477,7 +82812,9 @@ function resolveChimeraConfig(cfg, sessionProvider, sessionModel) {
82477
82812
  maxFiles: cfg.maxFiles ?? DEFAULT_MAX_FILES,
82478
82813
  autoFix: cfg.autoFix ?? "off",
82479
82814
  cascadeOn: cfg.cascadeOn ?? DEFAULT_CASCADE_ON,
82480
- maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH2
82815
+ maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH2,
82816
+ fallbackModels: cfg.fallbackModels ? [...cfg.fallbackModels] : [],
82817
+ fallbackProfile: cfg.fallbackProfile
82481
82818
  };
82482
82819
  }
82483
82820
  var CHIMERA_REVIEW_PROMPT = readBundledInstructionText("llm/chimera-review.md");
@@ -84141,49 +84478,6 @@ function dim(s) {
84141
84478
  return `\x1B[2m${s}\x1B[0m`;
84142
84479
  }
84143
84480
 
84144
- // src/plugins/review-finding-integration.ts
84145
- init_review_finding_store();
84146
- async function integrateFindings(payload, projectDir, reportId) {
84147
- if (!payload.reviewText || payload.reviewText.trim().length === 0) {
84148
- return { created: 0, relinked: 0, reopened: 0, totalFindings: 0, unparseableCount: 0 };
84149
- }
84150
- const store = new JsonlFindingStore(projectDir);
84151
- const source = (payload.bundle.cascadeDepth ?? 0) > 0 ? "cascade" : payload.bundle.cascadeOn !== void 0 && payload.bundle.cascadeOn !== "off" ? "auto" : "chimera";
84152
- const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
84153
- const sessionId = payload.sessionId ?? payload.cwd;
84154
- const model = payload.bundle.config.model;
84155
- const parsed = parseChimeraReviewReport(payload.reviewText, {
84156
- sessionId,
84157
- agentId,
84158
- reviewerModel: model,
84159
- reviewType: source,
84160
- reportId
84161
- });
84162
- if (parsed.findings.length === 0) {
84163
- return {
84164
- created: 0,
84165
- relinked: 0,
84166
- reopened: 0,
84167
- totalFindings: 0,
84168
- unparseableCount: parsed.unparseableCount
84169
- };
84170
- }
84171
- const result = await store.upsert(parsed.findings, {
84172
- sessionId,
84173
- reportId,
84174
- agentId,
84175
- model
84176
- });
84177
- return {
84178
- created: result.created,
84179
- relinked: result.relinked,
84180
- reopened: result.reopened,
84181
- reportId,
84182
- totalFindings: parsed.findings.length,
84183
- unparseableCount: parsed.unparseableCount
84184
- };
84185
- }
84186
-
84187
84481
  // src/index.ts
84188
84482
  init_review_finding_store();
84189
84483
  init_review_report_store();
@@ -92778,7 +93072,7 @@ function createFallbackChainManageTool(opts) {
92778
93072
  name: FALLBACK_CHAIN_MANAGE_TOOL_NAME,
92779
93073
  description: "View or change the active rate-limit fallback chain. When the primary model is overloaded (429/5xx), the agent rotates through this chain in order. Every new entry must be a FAVORITE model \u2014 add it via favorite_manage first. Use insert to place a fallback at a specific position; use remove to delete an entry.",
92780
93074
  usageHint: '"list" to see the current chain. "add" with a favorite model to append. "insert" with an index (1-based) to place before that position. "remove" with index or model ref. "clear" to empty the chain (auto fallback takes over).',
92781
- category: "Config",
93075
+ category: "config",
92782
93076
  inputSchema: FALLBACK_CHAIN_SCHEMA,
92783
93077
  permission: "auto",
92784
93078
  mutating: true,
@@ -92928,7 +93222,7 @@ function createFavoriteManageTool(opts) {
92928
93222
  name: FAVORITE_MANAGE_TOOL_NAME,
92929
93223
  description: "Manage your favorite provider/model list. Favorites are the only models that can be added to fallback chains and profiles. The LLM uses this tool to curate which models are available for fallback and role assignment.",
92930
93224
  usageHint: 'Start with "list" to see current favorites. Use "add <provider/model>" to add. Use "remove <index|ref>" to remove.',
92931
- category: "Config",
93225
+ category: "config",
92932
93226
  inputSchema: FAVORITE_MANAGE_SCHEMA,
92933
93227
  permission: "auto",
92934
93228
  mutating: true,
@@ -93008,7 +93302,7 @@ async function storeProviderKey(providers, input, keyValue, opts) {
93008
93302
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
93009
93303
  });
93010
93304
  entry.apiKeys = existingKeys;
93011
- entry.apiKey = void 0;
93305
+ delete entry.apiKey;
93012
93306
  if (input.setActive !== false) {
93013
93307
  entry.activeKey = label;
93014
93308
  }
@@ -93051,7 +93345,7 @@ function createSystemConfigViewTool(opts) {
93051
93345
  name: SYSTEM_CONFIG_VIEW_TOOL_NAME,
93052
93346
  description: "Get a comprehensive view of all provider, model, fallback, and matrix configuration. Shows the complete state across all configurable areas so you can see what is available and make informed decisions when assigning models, creating fallback profiles, or managing providers. Use the section parameter to focus on specific areas.",
93053
93347
  usageHint: '"section: all" for everything. "section: providers" for configured providers and key status. "section: models" for leader model and favorites. "section: fallbacks" for chains, profiles, and toggles. "section: matrix" for per-role assignments. "section: refiner" for goal refinement config.',
93054
- category: "Config",
93348
+ category: "config",
93055
93349
  inputSchema: SYSTEM_CONFIG_VIEW_SCHEMA,
93056
93350
  permission: "auto",
93057
93351
  mutating: false,
@@ -93355,7 +93649,7 @@ function createFallbackProfileManageTool(opts) {
93355
93649
  name: FALLBACK_PROFILE_MANAGE_TOOL_NAME,
93356
93650
  description: "Manage named fallback profiles. A profile is a reusable, ordered list of model references that can be assigned to agent roles. Every entry in a profile must be a FAVORITE model \u2014 add it via favorite_manage first. Use /setmodel or agent_model_assign to assign a profile to a role.",
93357
93651
  usageHint: '"list" to see all profiles. "set" with name and chain (array of model refs) to create or replace a profile. "delete" with name to remove a profile.',
93358
- category: "Config",
93652
+ category: "config",
93359
93653
  inputSchema: FALLBACK_PROFILE_SCHEMA,
93360
93654
  permission: "auto",
93361
93655
  mutating: true,
@@ -93466,7 +93760,7 @@ function createAgentModelAssignTool(opts) {
93466
93760
  name: AGENT_MODEL_ASSIGN_TOOL_NAME,
93467
93761
  description: "Assign a provider/model or a fallback profile to a specific agent role, phase, or the fleet-wide default. This is the LLM-accessible equivalent of /setmodel set. The provider+model combination must be in your favorites list (unless only clearing). Resolution precedence: exact role \u2192 phase \u2192 * \u2192 leader model.",
93468
93762
  usageHint: 'Use "list" as role to see current assignments. Set with role + model, or role + provider + model, or role + profile. Set role + clear=true to remove a matrix entry. The provider/model must be a favorite.',
93469
- category: "Config",
93763
+ category: "config",
93470
93764
  inputSchema: AGENT_MODEL_ASSIGN_SCHEMA,
93471
93765
  permission: "auto",
93472
93766
  mutating: true,
@@ -93614,7 +93908,7 @@ function createProviderManageTool(opts) {
93614
93908
  name: PROVIDER_MANAGE_TOOL_NAME,
93615
93909
  description: "View or configure provider entries. List all configured providers with their type, model lists, base URL, and key status. Add new providers, update their settings, or remove unused ones. API keys should be set via provider_key_set instead of passing them here \u2014 they are visible in the LLM output.",
93616
93910
  usageHint: '"list" to see all providers. "add" with provider id and type to create. "configure" to update models, baseUrl, family, or envVars. "remove" to delete a provider. Use provider_key_set for API key management.',
93617
- category: "Config",
93911
+ category: "config",
93618
93912
  inputSchema: PROVIDER_MANAGE_SCHEMA,
93619
93913
  permission: "auto",
93620
93914
  mutating: true,
@@ -93763,9 +94057,11 @@ function createProviderKeySetTool(opts) {
93763
94057
  name: PROVIDER_KEY_SET_TOOL_NAME,
93764
94058
  description: "Set the API key for a provider. For security, prefer using envVar (reads from environment variable, value never visible to the LLM) over passing the key directly. When neither key nor envVar is provided, the tool returns a prompt for interactive key entry \u2014 the UI will present an input field and the key is stored without LLM visibility.\n\nAfter setting a key, the provider becomes usable for model assignments and fallback chains. Add its models to favorites with favorite_manage to unlock them for fallback/profile use.",
93765
94059
  usageHint: 'Preferred: provider_key_set({ provider: "openai", envVar: "OPENAI_API_KEY" }). For interactive input: provider_key_set({ provider: "openai" }) \u2014 the UI will prompt. Direct key: provider_key_set({ provider: "openai", key: "sk-..." }) \u2014 visible to LLM.',
93766
- category: "Config",
94060
+ category: "config",
93767
94061
  inputSchema: PROVIDER_KEY_SET_SCHEMA,
93768
- permission: "auto",
94062
+ // 'confirm', not 'auto' — this tool writes credentials to disk (and can
94063
+ // read arbitrary env vars into the config file), so the user must see it.
94064
+ permission: "confirm",
93769
94065
  mutating: true,
93770
94066
  riskTier: "standard",
93771
94067
  icon: "settings",
@@ -93861,7 +94157,7 @@ function createLeaderModelSetTool(opts) {
93861
94157
  name: LEADER_MODEL_SET_TOOL_NAME,
93862
94158
  description: 'View or change the leader provider/model and system toggles. The leader is the primary model used for the main agent interactions. "set" changes it directly. "profile" derives it from a named fallback profile (first entry becomes leader, rest become the fallback chain). "toggle" controls fallbackAuto (smart default fallback) and favoriteModelsOnly (restrict auto-fallback to favorites only).',
93863
94159
  usageHint: '"show" to see current state. "set" with provider+model to change. "profile" with name to derive from a profile. "toggle" with toggle name and value to change a boolean setting.',
93864
- category: "Config",
94160
+ category: "config",
93865
94161
  inputSchema: LEADER_MODEL_SET_SCHEMA,
93866
94162
  permission: "auto",
93867
94163
  mutating: true,
@@ -93885,11 +94181,21 @@ function createLeaderModelSetTool(opts) {
93885
94181
  if (!input.provider || !input.model) {
93886
94182
  return { status: "error", message: 'Provide "provider" and "model" for the leader.' };
93887
94183
  }
94184
+ if (opts.switchProviderAndModel) {
94185
+ const switchError = await opts.switchProviderAndModel(input.provider, input.model);
94186
+ if (switchError) {
94187
+ return {
94188
+ status: "error",
94189
+ message: `Could not switch to ${input.provider}/${input.model}: ${switchError}. Config was not changed.`
94190
+ };
94191
+ }
94192
+ }
93888
94193
  await opts.updateConfig((cfg) => {
93889
94194
  cfg.provider = input.provider;
93890
94195
  cfg.model = input.model;
93891
94196
  });
93892
- return { status: "ok", message: `\u2713 Leader \u2192 ${input.provider}/${input.model}` };
94197
+ const liveNote = opts.switchProviderAndModel ? "" : " (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
94198
+ return { status: "ok", message: `\u2713 Leader \u2192 ${input.provider}/${input.model}${liveNote}` };
93893
94199
  }
93894
94200
  if (input.action === "profile") {
93895
94201
  if (!input.profile) {
@@ -93908,15 +94214,25 @@ function createLeaderModelSetTool(opts) {
93908
94214
  return { status: "error", message: `Cannot parse "${first}" as a valid model reference.` };
93909
94215
  }
93910
94216
  const rest = chain.slice(1);
94217
+ if (opts.switchProviderAndModel) {
94218
+ const switchError = await opts.switchProviderAndModel(provider, model);
94219
+ if (switchError) {
94220
+ return {
94221
+ status: "error",
94222
+ message: `Could not switch to ${provider}/${model}: ${switchError}. Config was not changed.`
94223
+ };
94224
+ }
94225
+ }
93911
94226
  await opts.updateConfig((cfg) => {
93912
94227
  cfg.provider = provider;
93913
94228
  cfg.model = model;
93914
94229
  cfg.fallbackModels = rest;
93915
94230
  });
94231
+ const profileLiveNote = opts.switchProviderAndModel ? "" : "\n (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
93916
94232
  return {
93917
94233
  status: "ok",
93918
94234
  message: `\u2713 Leader \u2192 ${provider}/${model} (profile: ${input.profile})` + (rest.length > 0 ? `
93919
- Fallback chain: ${rest.join(" \u2192 ")}` : "")
94235
+ Fallback chain: ${rest.join(" \u2192 ")}` : "") + profileLiveNote
93920
94236
  };
93921
94237
  }
93922
94238
  if (input.action === "toggle") {
@@ -94087,20 +94403,22 @@ async function runEnable(name, deps) {
94087
94403
  const known = Object.keys(all).join(", ");
94088
94404
  return `Unknown server "${name}". Available presets: ${known}`;
94089
94405
  }
94090
- await updateJsonObjectFile(deps.configPath, (full) => {
94406
+ const persistEnabled = () => updateJsonObjectFile(deps.configPath, (full) => {
94091
94407
  const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
94092
94408
  setJsonPath(full, ["mcpServers", name], { ...current[name], ...cfg, enabled: true });
94093
94409
  });
94094
94410
  try {
94095
94411
  const live = deps.registry.describe().find((s) => s.name === name);
94096
94412
  if (live && live.state === "connected") {
94097
- return `${green("\u25CF")} Server "${name}" is already running (${live.toolCount} tools registered).`;
94413
+ await persistEnabled();
94414
+ return `Server "${name}" is already running (${live.toolCount} tools registered).`;
94098
94415
  }
94099
94416
  await deps.registry.start({ ...cfg, enabled: true });
94417
+ await persistEnabled();
94100
94418
  const updated = deps.registry.describe().find((s) => s.name === name);
94101
- return `${green("\u2713 Enabled and started")} "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
94419
+ return `Enabled and started "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
94102
94420
  } catch (err) {
94103
- return `${red("\u2717 Failed to start")} "${name}": ${toErrorMessage(err)}`;
94421
+ return `Failed to start "${name}": ${toErrorMessage(err)}. Config was left unchanged (server stays disabled).`;
94104
94422
  }
94105
94423
  }
94106
94424
  async function runDisable(name, deps) {
@@ -94174,34 +94492,34 @@ function isMcpServerRecord(value) {
94174
94492
  return !!value && typeof value === "object" && !Array.isArray(value);
94175
94493
  }
94176
94494
  function bold(s) {
94177
- return `\x1B[1m${s}\x1B[0m`;
94495
+ return s;
94178
94496
  }
94179
94497
  function dim2(s) {
94180
- return `\x1B[2m${s}\x1B[0m`;
94498
+ return s;
94181
94499
  }
94182
94500
  function green(s) {
94183
- return `\x1B[32m${s}\x1B[0m`;
94501
+ return s;
94184
94502
  }
94185
94503
  function yellow(s) {
94186
- return `\x1B[33m${s}\x1B[0m`;
94504
+ return s;
94187
94505
  }
94188
94506
  function red(s) {
94189
- return `\x1B[31m${s}\x1B[0m`;
94507
+ return s;
94190
94508
  }
94191
94509
  function badge(state) {
94192
94510
  switch (state) {
94193
94511
  case "connected":
94194
- return green("\u25CF connected");
94512
+ return "\u25CF connected";
94195
94513
  case "connecting":
94196
- return `\x1B[36m\u25D0 connecting\x1B[0m`;
94514
+ return "\u25D0 connecting";
94197
94515
  case "reconnecting":
94198
- return `\x1B[36m\u25D1 reconnecting\x1B[0m`;
94516
+ return "\u25D1 reconnecting";
94199
94517
  case "disconnected":
94200
- return dim2("\u25CB disconnected");
94518
+ return "\u25CB disconnected";
94201
94519
  case "failed":
94202
- return red("\u2717 failed");
94520
+ return "\u2717 failed";
94203
94521
  default:
94204
- return dim2(state);
94522
+ return state;
94205
94523
  }
94206
94524
  }
94207
94525
 
@@ -94243,13 +94561,19 @@ function createMcpUseTool(opts) {
94243
94561
  const servers = registry2.describe();
94244
94562
  const serverInfo = servers.find((s) => s.name === serverName);
94245
94563
  if (!serverInfo) {
94246
- return `Server "${serverName}" not found. Available: ${servers.map((s) => s.name).join(", ") || "none"}.`;
94564
+ throw new Error(
94565
+ `Server "${serverName}" not found. Available: ${servers.map((s) => s.name).join(", ") || "none"}.`
94566
+ );
94247
94567
  }
94248
94568
  if (serverInfo.state !== "connected") {
94249
- return `Server "${serverName}" is not connected (state: ${serverInfo.state}). Use \`mcp_control({ action: "enable", server: "${serverName}" })\` first.`;
94569
+ throw new Error(
94570
+ `Server "${serverName}" is not connected (state: ${serverInfo.state}). Use \`mcp_control({ action: "enable", server: "${serverName}" })\` first.`
94571
+ );
94250
94572
  }
94251
- if (registry2.activateServer) {
94252
- registry2.activateServer(serverName);
94573
+ const alreadyActive = registry2.isActivated?.(serverName) === true;
94574
+ const didActivate = !alreadyActive && Boolean(registry2.activateServer);
94575
+ if (didActivate) {
94576
+ registry2.activateServer?.(serverName);
94253
94577
  }
94254
94578
  try {
94255
94579
  const qualifiedName = mcpQualifiedToolName(serverName, toolName);
@@ -94257,7 +94581,7 @@ function createMcpUseTool(opts) {
94257
94581
  if (!mcpTool) {
94258
94582
  const allTools = toolRegistry.list().filter((t2) => t2.name.startsWith(mcpServerToolPrefix(serverName))).map((t2) => t2.name.replace(mcpServerToolPrefix(serverName), ""));
94259
94583
  const hint = allTools.length > 0 ? `Available tools on "${serverName}": ${allTools.join(", ")}.` : `No tools found on "${serverName}". The server may not have published any tools.`;
94260
- return `Tool "${toolName}" not found on server "${serverName}". ${hint}`;
94584
+ throw new Error(`Tool "${toolName}" not found on server "${serverName}". ${hint}`);
94261
94585
  }
94262
94586
  const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
94263
94587
  if (typeof governedExecute !== "function") {
@@ -94267,7 +94591,7 @@ function createMcpUseTool(opts) {
94267
94591
  if (!result.success) throw new Error(result.error ?? "MCP tool execution failed");
94268
94592
  return result.result;
94269
94593
  } finally {
94270
- if (registry2.deactivateServer) {
94594
+ if (didActivate && registry2.deactivateServer) {
94271
94595
  registry2.deactivateServer(serverName);
94272
94596
  }
94273
94597
  }
@@ -94277,6 +94601,7 @@ function createMcpUseTool(opts) {
94277
94601
 
94278
94602
  // src/tools/one-shot-llm-tool.ts
94279
94603
  var ONE_SHOT_LLM_TOOL_NAME = "llm";
94604
+ var MAX_TIMEOUT_MS2 = 12e4;
94280
94605
  var INPUT_SCHEMA2 = {
94281
94606
  type: "object",
94282
94607
  properties: {
@@ -94353,9 +94678,10 @@ var INPUT_SCHEMA2 = {
94353
94678
  },
94354
94679
  timeoutMs: {
94355
94680
  type: "number",
94356
- description: "Hard timeout in ms (default 30s)."
94681
+ description: `Hard timeout in ms (default 30s, clamped to a maximum of ${MAX_TIMEOUT_MS2}).`
94357
94682
  }
94358
- }
94683
+ },
94684
+ additionalProperties: false
94359
94685
  };
94360
94686
  function createOneShotLLMTool(opts) {
94361
94687
  const orchestrator = new OneShotOrchestrator({
@@ -94363,6 +94689,7 @@ function createOneShotLLMTool(opts) {
94363
94689
  getConfig: opts.getConfig,
94364
94690
  fallbackProfileManager: opts.fallbackProfileManager,
94365
94691
  modelRouter: opts.modelRouter,
94692
+ statusTracker: opts.statusTracker,
94366
94693
  logger: opts.logger,
94367
94694
  wrapProviderCall: opts.wrapProviderCall
94368
94695
  });
@@ -94371,8 +94698,23 @@ function createOneShotLLMTool(opts) {
94371
94698
  description: "Make a one-shot LLM call with a system prompt and user input. Supports provider selection, model routing by role, fallback chains, and timeout. Returns the response text, model info, token usage, and whether a fallback was used. Use this for summarization, classification, extraction, and any single-turn LLM task.",
94372
94699
  usageHint: "Provide `system` for the instruction and `userPrompt` for the input. Either set `model`+`providerId`, or have defaults configured on the tool. Set `fallbackModels` for resilience. Check `error` on the result for failure details.",
94373
94700
  inputSchema: INPUT_SCHEMA2,
94701
+ // Metadata mirrors council-tool.ts — both are read-only meta tools that
94702
+ // spend tokens but never touch the workspace.
94703
+ category: "meta",
94374
94704
  permission: "auto",
94375
94705
  mutating: false,
94706
+ riskTier: "safe",
94707
+ maxOutputBytes: 262144,
94708
+ validate(input) {
94709
+ const hasPrompt = typeof input.userPrompt === "string" && input.userPrompt.trim().length > 0;
94710
+ const hasMessages = Array.isArray(input.messages) && input.messages.length > 0;
94711
+ if (!hasPrompt && !hasMessages) {
94712
+ return [
94713
+ "Provide `userPrompt` (a single user turn) or `messages` (a conversation array) \u2014 without either the llm tool has nothing to send to the model."
94714
+ ];
94715
+ }
94716
+ return [];
94717
+ },
94376
94718
  async execute(input, _ctx, { signal }) {
94377
94719
  if (!input.model && !input.providerId && !opts.defaultModel && !opts.defaultProvider) {
94378
94720
  return {
@@ -94389,7 +94731,11 @@ function createOneShotLLMTool(opts) {
94389
94731
  ...input,
94390
94732
  signal: input.signal ? AbortSignal.any([input.signal, signal]) : signal,
94391
94733
  model: input.model ?? opts.defaultModel,
94392
- providerId: input.providerId ?? opts.defaultProvider
94734
+ providerId: input.providerId ?? opts.defaultProvider,
94735
+ // Clamp runaway timeouts (documented on the schema). Non-positive
94736
+ // values fall back to the orchestrator default rather than making the
94737
+ // call instantly un-completable.
94738
+ ...typeof input.timeoutMs === "number" && input.timeoutMs > 0 ? { timeoutMs: Math.min(input.timeoutMs, MAX_TIMEOUT_MS2) } : { timeoutMs: void 0 }
94393
94739
  };
94394
94740
  return orchestrator.call(effectiveInput);
94395
94741
  }
@@ -96383,7 +96729,6 @@ export {
96383
96729
  makeDesignStudioRequestMiddleware,
96384
96730
  makeDesignVerifyToolCallMiddleware,
96385
96731
  makeDirectorSessionFactory,
96386
- makeDomainGlossaryContributor,
96387
96732
  makeFleetEmitTool,
96388
96733
  makeFleetStatusTool,
96389
96734
  makeFleetTool,