@wrongstack/core 0.305.0 → 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 (44) hide show
  1. package/dist/chronicle/project-server.js +18 -48
  2. package/dist/coordination/agents/index.js +3 -2
  3. package/dist/coordination/agents/types.d.ts +1 -1
  4. package/dist/coordination/index.d.ts +1 -0
  5. package/dist/coordination/index.js +5 -2
  6. package/dist/coordination/mailbox-project-server.js +28 -57
  7. package/dist/core/index.d.ts +2 -1
  8. package/dist/core/index.js +2764 -2632
  9. package/dist/core/system-prompt-blocks.d.ts +1 -1
  10. package/dist/core/system-prompt-builder.d.ts +7 -1
  11. package/dist/core/system-prompt-glossary.d.ts +0 -23
  12. package/dist/defaults/index.js +3 -2
  13. package/dist/execution/index.js +3 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +898 -531
  16. package/dist/observability/index.js +7 -3
  17. package/dist/plugin/index.d.ts +4 -3
  18. package/dist/plugin/index.js +589 -143
  19. package/dist/plugins/auto-review-plugin.d.ts +14 -7
  20. package/dist/plugins/chimera-plugin.d.ts +15 -1
  21. package/dist/plugins/review-finding-integration.d.ts +15 -3
  22. package/dist/plugins/review-finding-parser.d.ts +36 -0
  23. package/dist/plugins/review-finding-types.d.ts +46 -0
  24. package/dist/plugins/review-finding-verification.d.ts +53 -0
  25. package/dist/plugins/review-report-integration.d.ts +1 -0
  26. package/dist/plugins/review-report-store.d.ts +7 -0
  27. package/dist/plugins/review-report-types.d.ts +14 -0
  28. package/dist/plugins/review-types.d.ts +74 -0
  29. package/dist/replay/replay-provider-runner.d.ts +5 -4
  30. package/dist/session-catalog/project-server.js +36 -65
  31. package/dist/tools/fallback-manage-tool-options.d.ts +9 -0
  32. package/dist/tools/index.js +91 -38
  33. package/dist/tools/one-shot-llm-tool.d.ts +6 -0
  34. package/dist/types/blocks.d.ts +10 -0
  35. package/dist/types/config/ui.d.ts +7 -4
  36. package/dist/types/index.js +21 -1
  37. package/dist/utils/index.js +8 -9
  38. package/instructions/agents/browser.md +1 -0
  39. package/instructions/agents/e2e.md +2 -0
  40. package/instructions/llm/chimera-review.md +52 -1
  41. package/instructions/system-lite.md +17 -6
  42. package/instructions/system-pro.md +25 -20
  43. package/instructions/system.md +25 -12
  44. 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();
@@ -3524,7 +3546,27 @@ var THEME_PRESET_IDS = [
3524
3546
  "ayu-dark",
3525
3547
  "everforest",
3526
3548
  "night-owl",
3527
- "synthwave"
3549
+ "synthwave",
3550
+ "github-dark",
3551
+ "material-ocean",
3552
+ "nightfox",
3553
+ "oxocarbon",
3554
+ "catppuccin-macchiato",
3555
+ "catppuccin-frappe",
3556
+ "gruvbox-material",
3557
+ "tokyo-night-storm",
3558
+ "rose-pine-moon",
3559
+ "zenburn",
3560
+ "palenight",
3561
+ "horizon",
3562
+ "sonokai",
3563
+ "edge-dark",
3564
+ "moonfly",
3565
+ "melange",
3566
+ "poimandres",
3567
+ "vitesse-dark",
3568
+ "aura",
3569
+ "dark-plus"
3528
3570
  ];
3529
3571
 
3530
3572
  // src/types/default-config.ts
@@ -8052,8 +8094,7 @@ function buildConversationContinuityBlock(ctx) {
8052
8094
  "Recent human instructions, oldest to newest. Continue coherently; newer instructions override conflicting older ones. This is context evidence, not a new request.",
8053
8095
  ...lines,
8054
8096
  "[/conversation_continuity]"
8055
- ].join("\n"),
8056
- cache_control: { type: "ephemeral" }
8097
+ ].join("\n")
8057
8098
  };
8058
8099
  }
8059
8100
  function recordToolOutputEvidence(ctx, input) {
@@ -8271,8 +8312,7 @@ function buildCompletedWorkLedgerBlock(ctx) {
8271
8312
  if (items.length === 0) return void 0;
8272
8313
  return {
8273
8314
  type: "text",
8274
- text: formatCompletedWorkLedger(items),
8275
- cache_control: { type: "ephemeral" }
8315
+ text: formatCompletedWorkLedger(items)
8276
8316
  };
8277
8317
  }
8278
8318
  function syncCompletedWorkLedgerBlock(_ctx) {
@@ -10029,218 +10069,6 @@ function providerBoundToRequest(request) {
10029
10069
  return requestProviders.get(request);
10030
10070
  }
10031
10071
 
10032
- // src/core/agent-response.ts
10033
- var MAX_TODO_SNAPSHOT_ITEMS = 10;
10034
- var MAX_TODO_SNAPSHOT_CONTENT = 180;
10035
- function buildLiveNextStepsGateBlock(ctx) {
10036
- if (ctx.agentId !== "leader") return void 0;
10037
- const openTodos = ctx.todos.filter(
10038
- (todo) => todo.status === "pending" || todo.status === "in_progress"
10039
- );
10040
- if (openTodos.length === 0) {
10041
- const toolRoute = ctx.tools?.some((t2) => t2.name === "nextsteps") ? [
10042
- "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."
10043
- ] : [];
10044
- return {
10045
- type: "text",
10046
- text: [
10047
- "[nextsteps_gate]",
10048
- "Authoritative live state for this request: open todos = 0.",
10049
- "On the final response, you MUST take exactly one branch:",
10050
- "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.",
10051
- "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.",
10052
- ...toolRoute,
10053
- "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.",
10054
- "Silently omitting both is invalid. Do not decide by chance, tone, or response length, and do not invent filler suggestions.",
10055
- "[/nextsteps_gate]"
10056
- ].join("\n"),
10057
- cache_control: { type: "ephemeral" }
10058
- };
10059
- }
10060
- const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
10061
- const normalized = todo.content.replace(/\s+/g, " ").trim();
10062
- const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
10063
- return formatTodoForModel({ ...todo, content });
10064
- });
10065
- const omitted = openTodos.length - todoSnapshot.length;
10066
- if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
10067
- const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
10068
- "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.",
10069
- ...hasKanbanBoundTodos(openTodos) ? [
10070
- "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."
10071
- ] : []
10072
- ] : [];
10073
- return {
10074
- type: "text",
10075
- text: [
10076
- "[nextsteps_gate]",
10077
- `Authoritative live state for this request: open todos = ${openTodos.length}.`,
10078
- "You MUST omit <nextsteps> entirely while these todos remain open. Continue or finish the tracked work; do not propose unrelated follow-on work.",
10079
- ...todoReconciliation,
10080
- "Open todo snapshot:",
10081
- ...todoSnapshot,
10082
- "[/nextsteps_gate]"
10083
- ].join("\n"),
10084
- cache_control: { type: "ephemeral" }
10085
- };
10086
- }
10087
- var MAX_MEMORY_EVIDENCE_CHARS = 12e3;
10088
- function buildMemoryEvidenceBlocks(ctx) {
10089
- const blocks = [];
10090
- let remaining = MAX_MEMORY_EVIDENCE_CHARS;
10091
- for (const entry of ctx.memoryEvidence) {
10092
- if (remaining <= 0) break;
10093
- const text2 = entry.text.trim();
10094
- if (!text2) continue;
10095
- const source = entry.source.replace(/[^a-z0-9_.-]+/gi, "-").slice(0, 80) || "memory";
10096
- const bounded = text2.slice(0, remaining);
10097
- remaining -= bounded.length;
10098
- blocks.push({
10099
- type: "text",
10100
- text: `[memory_evidence source="${source}"]
10101
- ${bounded}
10102
- [/memory_evidence]`,
10103
- cache_control: { type: "ephemeral" }
10104
- });
10105
- }
10106
- return blocks;
10107
- }
10108
- function createAgentResponseHandler(a) {
10109
- const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
10110
- function stabilizePromptEpoch() {
10111
- const prompt = a.ctx.systemPrompt;
10112
- if (stabilizedPromptEpochs.has(prompt)) return;
10113
- for (const block of prompt) {
10114
- if (block.cache_control) Object.freeze(block.cache_control);
10115
- Object.freeze(block);
10116
- }
10117
- Object.freeze(prompt);
10118
- stabilizedPromptEpochs.add(prompt);
10119
- }
10120
- async function buildAndRunRequestPipeline(opts) {
10121
- if (a.ctx.toolAdjacencyDirty) {
10122
- const repaired = repairToolUseAdjacency(a.ctx.messages);
10123
- a.ctx.toolAdjacencyDirty = false;
10124
- if (repaired.report.changed) {
10125
- a.ctx.state.replaceMessages(repaired.messages);
10126
- a.events.emit("context.repaired", {
10127
- sessionId: resolveEventSessionId(a.ctx),
10128
- ctx: a.ctx,
10129
- ...repaired.report
10130
- });
10131
- a.logger.warn(
10132
- `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)`
10133
- );
10134
- }
10135
- }
10136
- stabilizePromptEpoch();
10137
- const volatileLedger = buildCompletedWorkLedgerBlock(a.ctx);
10138
- const continuity = buildConversationContinuityBlock(a.ctx);
10139
- const liveNextStepsGate = buildLiveNextStepsGateBlock(a.ctx);
10140
- const memoryEvidence = buildMemoryEvidenceBlocks(a.ctx);
10141
- const volatileBlocks = [
10142
- volatileLedger,
10143
- continuity,
10144
- liveNextStepsGate,
10145
- ...memoryEvidence
10146
- ].filter((block) => block !== void 0);
10147
- const system = volatileBlocks.length > 0 ? [...a.ctx.systemPrompt, ...volatileBlocks] : a.ctx.systemPrompt;
10148
- await a.ctx.waitForModelTransition();
10149
- const provider = a.ctx.provider;
10150
- const baseReq = {
10151
- model: opts.model ?? a.ctx.model,
10152
- system,
10153
- messages: a.ctx.messages,
10154
- tools: a.tools.listForProvider(),
10155
- // `maxTokens` is deliberately NOT set here. The provider adapter
10156
- // resolves the ceiling from the catalog entry for the model in
10157
- // `req.model`, which is the only source that stays correct across a
10158
- // `/model` switch, a fallback hop, or a subagent on a model-matrix
10159
- // entry — `provider.capabilities` is resolved once, for the model the
10160
- // session booted on, and pinning it here would override the accurate
10161
- // per-request value with a stale one. Callers that genuinely want a
10162
- // smaller response (one-shot LLM helpers, compaction, the brain) still
10163
- // set `maxTokens` on their own Request and keep priority over the
10164
- // catalog.
10165
- // Provider-agnostic cache-partition key from the stable prompt epoch.
10166
- // Wires that support prompt caching (OpenAI `prompt_cache_key`) read it;
10167
- // the config `ttl` is merged over this by the ModelRuntime middleware.
10168
- cache: { key: deriveCachePrefixKey(a.ctx.systemPrompt) }
10169
- };
10170
- const request = await a.pipelines.request.run(baseReq);
10171
- bindRequestProvider(request, provider);
10172
- return { request, provider };
10173
- }
10174
- async function processResponse(raw, req, requestProvider = a.ctx.provider) {
10175
- let res = raw;
10176
- res = await a.pipelines.response.run(res);
10177
- res = maybeAppendPendingNextSteps(a.ctx, res);
10178
- a.events.emit("provider.response", {
10179
- sessionId: resolveEventSessionId(a.ctx),
10180
- ctx: a.ctx,
10181
- model: req.model,
10182
- content: res.content,
10183
- usage: res.usage,
10184
- stopReason: res.stopReason
10185
- });
10186
- a.ctx.tokenCounter.account(res.usage, req.model, requestProvider.id);
10187
- if (hasMeaningfulContent(res.content)) {
10188
- await a.ctx.session.append({
10189
- type: "llm_response",
10190
- ts: (/* @__PURE__ */ new Date()).toISOString(),
10191
- content: res.content,
10192
- stopReason: res.stopReason,
10193
- usage: res.usage
10194
- });
10195
- a.ctx.state.appendMessage({ role: "assistant", content: res.content });
10196
- if (!a.ctx.toolAdjacencyDirty) {
10197
- for (const block of res.content) {
10198
- if (block.type === "tool_use") {
10199
- a.ctx.toolAdjacencyDirty = true;
10200
- break;
10201
- }
10202
- }
10203
- }
10204
- try {
10205
- await a.ctx.flushConversationJournal();
10206
- await a.ctx.session.flush();
10207
- } catch (err) {
10208
- (a.logger.debug ?? a.logger.warn)?.(`LLM response flush failed: ${toErrorMessage(err)}`);
10209
- }
10210
- } else {
10211
- a.logger.warn("Empty assistant response \u2014 not appended to context or session", {
10212
- model: req.model,
10213
- stopReason: res.stopReason,
10214
- aborted: a.ctx.signal.aborted
10215
- });
10216
- }
10217
- if (a.ctx.signal.aborted) {
10218
- const parts2 = [];
10219
- for (const block of res.content) {
10220
- if (isTextBlock(block)) parts2.push(block.text);
10221
- }
10222
- return { finalText: parts2.join(""), aborted: true, done: false };
10223
- }
10224
- const parts = [];
10225
- const streamed = requestProvider.capabilities.streaming;
10226
- for (const block of res.content) {
10227
- if (isTextBlock(block)) {
10228
- const rendered = await a.pipelines.assistantOutput.run(block);
10229
- parts.push(rendered.text);
10230
- if (!streamed) a.renderer?.write(rendered);
10231
- }
10232
- }
10233
- const finalText = parts.join("");
10234
- markAssistantReferencedEvidence(a.ctx, finalText);
10235
- let directive = "none";
10236
- if (finalText) {
10237
- directive = parseContinueDirective(finalText);
10238
- }
10239
- return { finalText, aborted: false, done: false, directive };
10240
- }
10241
- return { buildAndRunRequestPipeline, processResponse };
10242
- }
10243
-
10244
10072
  // src/types/runtime-capability-manifest.ts
10245
10073
  var PLAYWRIGHT_ALIASES = {
10246
10074
  playwright_navigate: "browser_navigate",
@@ -10523,6 +10351,493 @@ function runtimeToolReferencesFromText(text2) {
10523
10351
  return [...references];
10524
10352
  }
10525
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
+
10526
10841
  // src/types/system-prompt.ts
10527
10842
  function flattenSystemPromptRegions(regions) {
10528
10843
  return [...regions.core, ...regions.session, ...regions.volatile];
@@ -10703,138 +11018,6 @@ function firstExistingDirSync(candidates) {
10703
11018
  return candidates[0] ?? "";
10704
11019
  }
10705
11020
 
10706
- // src/core/instruction-template.ts
10707
- var CANONICAL_TOOL_NAMES = new Set(
10708
- RUNTIME_CAPABILITY_MANIFEST.flatMap((entry) => [...entry.tools])
10709
- );
10710
- var DIRECTIVE_RE = /[ \t]*<!--\s*ws:(if|else|end)\b([^>]*?)-->[ \t]*(?:\r?\n)?/g;
10711
- var PLACEHOLDER_RE = /\{\{\s*(tools:)?\s*([a-zA-Z0-9_.,\s-]+?)\s*\}\}/g;
10712
- function renderInstructionLayer(text2, ctx) {
10713
- if (!text2) return text2;
10714
- const hasDirectives = text2.includes("<!--ws:") || text2.includes("<!-- ws:");
10715
- const hasPlaceholders = text2.includes("{{");
10716
- if (!hasDirectives && !hasPlaceholders) return text2;
10717
- const rendered = hasDirectives ? emit(parse2(text2), ctx) : text2;
10718
- const substituted = hasPlaceholders ? substitute(rendered, ctx) : rendered;
10719
- const guarded = ctx?.strictToolReferences ? dropLinesWithUnavailableToolReferences(
10720
- substituted,
10721
- ctx,
10722
- /* @__PURE__ */ new Set([...CANONICAL_TOOL_NAMES, ...declaredToolNames(text2)])
10723
- ) : substituted;
10724
- return tidy(guarded);
10725
- }
10726
- function declaredToolNames(text2) {
10727
- const names = /* @__PURE__ */ new Set();
10728
- for (const marker of text2.matchAll(/<!--\s*ws:if\b([^>]*?)-->/g)) {
10729
- for (const attr of (marker[1] ?? "").matchAll(/!?tool=([A-Za-z0-9_.,-]+)/g)) {
10730
- for (const name of (attr[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10731
- }
10732
- }
10733
- for (const placeholder of text2.matchAll(/\{\{\s*tools:\s*([^}]+)}}/g)) {
10734
- for (const name of (placeholder[1] ?? "").split(",")) if (name.trim()) names.add(name.trim());
10735
- }
10736
- return names;
10737
- }
10738
- function dropLinesWithUnavailableToolReferences(text2, ctx, declared) {
10739
- const unavailable = [...declared].filter((name) => !ctx.toolNames.has(name));
10740
- if (unavailable.length === 0) return text2;
10741
- return text2.split(/(?<=\n)/).filter((line) => !unavailable.some((name) => formattedToolMention(line, name))).join("");
10742
- }
10743
- function formattedToolMention(line, name) {
10744
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10745
- const token = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`);
10746
- if (line.split("`").some((segment, index) => {
10747
- if (index % 2 !== 1) return false;
10748
- if (segment.includes(`<${name}`) || segment.includes(`</${name}`)) return false;
10749
- return token.test(segment);
10750
- })) {
10751
- return true;
10752
- }
10753
- return line.split("**").some((segment, index) => index % 2 === 1 && segment.trim() === name);
10754
- }
10755
- function parse2(text2) {
10756
- const root = [];
10757
- const stack = [];
10758
- const current = () => {
10759
- const frame = stack[stack.length - 1];
10760
- if (!frame) return root;
10761
- return frame.branches[frame.branches.length - 1];
10762
- };
10763
- const pushText = (value) => {
10764
- if (value) current().push({ kind: "text", value });
10765
- };
10766
- DIRECTIVE_RE.lastIndex = 0;
10767
- let cursor = 0;
10768
- for (let m = DIRECTIVE_RE.exec(text2); m !== null; m = DIRECTIVE_RE.exec(text2)) {
10769
- pushText(text2.slice(cursor, m.index));
10770
- cursor = m.index + m[0].length;
10771
- const keyword = m[1];
10772
- if (keyword === "if") {
10773
- stack.push({ test: parseCondition(m[2] ?? ""), branches: [[]] });
10774
- } else if (keyword === "else") {
10775
- const frame = stack[stack.length - 1];
10776
- if (frame && frame.branches.length === 1) frame.branches.push([]);
10777
- } else {
10778
- const frame = stack.pop();
10779
- if (frame) current().push({ kind: "if", test: frame.test, body: frame.branches });
10780
- }
10781
- }
10782
- pushText(text2.slice(cursor));
10783
- while (stack.length > 0) {
10784
- const frame = stack.pop();
10785
- current().push(...frame.branches.flat());
10786
- }
10787
- return root;
10788
- }
10789
- function parseCondition(raw) {
10790
- const tokens = raw.trim().split(/\s+/).filter(Boolean);
10791
- if (tokens.length === 0) return null;
10792
- const attrs = [];
10793
- for (const token of tokens) {
10794
- const m = /^(!?)([a-zA-Z]+)=(.+)$/.exec(token);
10795
- if (!m) return null;
10796
- const key = (m[2] ?? "").toLowerCase();
10797
- if (key !== "tool" && key !== "tier" && key !== "role") return null;
10798
- const values = (m[3] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
10799
- if (values.length === 0) return null;
10800
- attrs.push({ key, negated: m[1] === "!", values });
10801
- }
10802
- return attrs;
10803
- }
10804
- function evaluate(test, ctx) {
10805
- if (test === null || !ctx) return true;
10806
- return test.every((attr) => {
10807
- 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");
10808
- return attr.negated ? !matched : matched;
10809
- });
10810
- }
10811
- function emit(nodes, ctx) {
10812
- let out = "";
10813
- for (const node of nodes) {
10814
- if (node.kind === "text") {
10815
- out += node.value;
10816
- continue;
10817
- }
10818
- const branch = evaluate(node.test, ctx) ? node.body[0] : node.body[1];
10819
- if (branch) out += emit(branch, ctx);
10820
- }
10821
- return out;
10822
- }
10823
- function substitute(text2, ctx) {
10824
- PLACEHOLDER_RE.lastIndex = 0;
10825
- return text2.replace(PLACEHOLDER_RE, (match, toolsPrefix, body) => {
10826
- if (toolsPrefix) {
10827
- const names = body.split(",").map((n) => n.trim()).filter(Boolean).filter((n) => !ctx || ctx.toolNames.has(n));
10828
- return names.map((n) => `\`${n}\``).join(", ");
10829
- }
10830
- const value = ctx?.vars?.[body.trim()];
10831
- return value === void 0 ? match : String(value);
10832
- });
10833
- }
10834
- function tidy(text2) {
10835
- return text2.replace(/(\r?\n){3,}/g, "$1$1");
10836
- }
10837
-
10838
11021
  // src/core/modes/default.ts
10839
11022
  import { readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
10840
11023
  import * as path23 from "node:path";
@@ -10869,61 +11052,6 @@ function isDirectory(candidate) {
10869
11052
  }
10870
11053
  }
10871
11054
 
10872
- // src/core/system-prompt-blocks.ts
10873
- var SYSTEM_BLOCK_SOURCE = /* @__PURE__ */ new WeakMap();
10874
- function tagBlock(block, source) {
10875
- SYSTEM_BLOCK_SOURCE.set(block, source);
10876
- return block;
10877
- }
10878
- function shortSessionId(sessionId) {
10879
- const leaf = sessionId.split("/").pop() ?? sessionId;
10880
- return leaf.length > 12 ? `${leaf.slice(0, 12)}\u2026` : leaf;
10881
- }
10882
- function instructionSection(bundle, key, vars = {}, tplCtx) {
10883
- const template = bundle.sections?.[key];
10884
- if (!template) return "";
10885
- return renderInstructionLayer(
10886
- template,
10887
- tplCtx ? { ...tplCtx, vars: { ...tplCtx.vars, ...vars } } : void 0
10888
- ).replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, name) => {
10889
- const value = vars[name];
10890
- return value === void 0 ? match : String(value);
10891
- });
10892
- }
10893
- function renderToolSelectionBoundary(tool) {
10894
- const selection = tool.selection;
10895
- if (!selection?.doNotUseWhen.trim()) return "";
10896
- const alternatives = selection.useInstead?.filter(Boolean) ?? [];
10897
- const instead = alternatives.length > 0 ? ` Use ${alternatives.map((name) => `\`${name}\``).join(" or ")} instead.` : "";
10898
- return `Do not use when ${selection.doNotUseWhen.trim()}${instead}`;
10899
- }
10900
- function agentsFingerprint(agents) {
10901
- if (!agents || agents.length === 0) return "0";
10902
- let h = 2166136261;
10903
- for (const a of agents) {
10904
- const fields = [
10905
- a.agentId,
10906
- a.name,
10907
- a.source,
10908
- a.sessionId,
10909
- a.status,
10910
- a.currentTask,
10911
- a.currentTool,
10912
- a.online ? "1" : "0"
10913
- ];
10914
- for (const field of fields) {
10915
- const value = field ?? "";
10916
- for (let i = 0; i < value.length; i++) {
10917
- h ^= value.charCodeAt(i);
10918
- h = Math.imul(h, 16777619) >>> 0;
10919
- }
10920
- h ^= 255;
10921
- h = Math.imul(h, 16777619) >>> 0;
10922
- }
10923
- }
10924
- return `${agents.length}:${h.toString(36)}`;
10925
- }
10926
-
10927
11055
  // src/core/system-prompt-environment.ts
10928
11056
  import * as os6 from "node:os";
10929
11057
  import * as path25 from "node:path";
@@ -11211,24 +11339,14 @@ async function renderDomainGlossary(ctx, memory, options = {}) {
11211
11339
  );
11212
11340
  return lines.join("\n");
11213
11341
  }
11214
- function makeDomainGlossaryContributor(glossary) {
11215
- return async (ctx) => {
11216
- const text2 = await renderDomainGlossary(ctx, glossary.memory);
11217
- if (!text2) return [];
11218
- return [{ type: "text", text: text2 }];
11219
- };
11220
- }
11221
11342
  function parseTermEntry(text2) {
11222
11343
  const trimmed = text2.trim();
11223
- const separators = [" \u2014 ", " \u2013 ", " - ", " -- "];
11224
- for (const sep10 of separators) {
11225
- const idx = trimmed.indexOf(sep10);
11226
- if (idx > 0) {
11227
- return {
11228
- term: trimmed.slice(0, idx).trim(),
11229
- definition: trimmed.slice(idx + sep10.length).trim()
11230
- };
11231
- }
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
+ };
11232
11350
  }
11233
11351
  return { term: trimmed, definition: "" };
11234
11352
  }
@@ -11772,7 +11890,13 @@ var DefaultSystemPromptBuilder = class {
11772
11890
  _lastCatalogTools;
11773
11891
  /** Cached rendered online agents string, keyed by content fingerprint. */
11774
11892
  _lastOnlineAgents;
11775
- /** 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
+ */
11776
11900
  _toolsUsageCache;
11777
11901
  _instructionBundle;
11778
11902
  /**
@@ -11937,6 +12061,26 @@ var DefaultSystemPromptBuilder = class {
11937
12061
  volatile.push(tagBlock({ type: "text", text: glossary }, "glossary"));
11938
12062
  }
11939
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
+ }
11940
12084
  if (!ctx.subagent) {
11941
12085
  session.push(
11942
12086
  tagBlock(
@@ -12031,9 +12175,8 @@ var DefaultSystemPromptBuilder = class {
12031
12175
  const instructions = await this.instructions();
12032
12176
  const tpl = tplCtx ?? this.templateContext(ctx);
12033
12177
  const section = (key, vars = {}) => instructionSection(instructions, key, vars, tpl);
12034
- const agentsHash = agentsFingerprint(ctx.onlineAgents);
12035
12178
  const tier = this.tier;
12036
- if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.agentsHash === agentsHash && this._toolsUsageCache?.tier === tier) {
12179
+ if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier) {
12037
12180
  return this._toolsUsageCache.text;
12038
12181
  }
12039
12182
  const byCat = /* @__PURE__ */ new Map();
@@ -12111,7 +12254,7 @@ ${hint.trim()}`);
12111
12254
  (t2) => t2.name === "mailbox" || t2.name === "mail_send" || t2.name === "mail_inbox"
12112
12255
  );
12113
12256
  if (hasMailbox) {
12114
- const onlineAgentsInfo = this.renderOnlineAgents(ctx.onlineAgents);
12257
+ const onlineAgentsInfo = "";
12115
12258
  const hasMailboxPowerTool = tools.some((t2) => t2.name === "mailbox");
12116
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";
12117
12260
  const mailInboxCommand = tools.some((t2) => t2.name === "mail_inbox") ? "`mail_inbox`" : "`mailbox action=check`";
@@ -12160,7 +12303,7 @@ ${hint.trim()}`);
12160
12303
  }
12161
12304
  }
12162
12305
  const text2 = lines.join("\n");
12163
- this._toolsUsageCache = { toolsRef: tools, agentsHash, tier, text: text2 };
12306
+ this._toolsUsageCache = { toolsRef: tools, tier, text: text2 };
12164
12307
  return text2;
12165
12308
  }
12166
12309
  renderOnlineAgents(agents) {
@@ -12220,6 +12363,8 @@ var SYSTEM_BLOCK_SOURCES = [
12220
12363
  "leader-after-task",
12221
12364
  "contributor",
12222
12365
  "ledger",
12366
+ "glossary",
12367
+ "peers",
12223
12368
  "nextsteps",
12224
12369
  "other"
12225
12370
  ];
@@ -23638,6 +23783,7 @@ var TOOLS = {
23638
23783
  "glob",
23639
23784
  "search",
23640
23785
  "tree",
23786
+ "diff",
23641
23787
  "write",
23642
23788
  "edit",
23643
23789
  "replace",
@@ -24669,7 +24815,7 @@ var VERIFY_AGENTS = [
24669
24815
  id: "e2e",
24670
24816
  name: "E2E",
24671
24817
  role: "e2e",
24672
- tools: [...TOOLS.build, "fetch", ...SPECIALIST_TOOLS.browser],
24818
+ tools: [...TOOLS.build, "fetch", "e2e_plan", ...SPECIALIST_TOOLS.browser],
24673
24819
  prompt: agentPrompt("e2e")
24674
24820
  },
24675
24821
  budget: HEAVY_BUDGET,
@@ -25182,7 +25328,7 @@ var DOMAIN_AGENTS = [
25182
25328
  id: "designer",
25183
25329
  name: "Designer",
25184
25330
  role: "designer",
25185
- tools: [...TOOLS.docs],
25331
+ tools: [...TOOLS.docs, "design"],
25186
25332
  prompt: agentPrompt("designer")
25187
25333
  },
25188
25334
  budget: MEDIUM_BUDGET,
@@ -62858,7 +63004,10 @@ var SENSITIVE_FLAG_PATTERNS = [
62858
63004
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
62859
63005
  // The separator group is optional so the attached form (the common one) matches.
62860
63006
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
62861
- /(?<![-\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,
62862
63011
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
62863
63012
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
62864
63013
  // redaction function (false positive = cosmetic noise; false negative = leak).
@@ -62866,8 +63015,9 @@ var SENSITIVE_FLAG_PATTERNS = [
62866
63015
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
62867
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,
62868
63017
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
62869
- // when preceded by a flag name (e.g. --github-token=EyJ...).
62870
- /--\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
62871
63021
  ];
62872
63022
  function redactCommand(cmd) {
62873
63023
  let result = cmd;
@@ -80114,7 +80264,10 @@ var DefaultPluginAPI = class {
80114
80264
  }
80115
80265
  };
80116
80266
  this.tools = {
80117
- register: (t2) => tr.register(t2, owner),
80267
+ register: (t2) => {
80268
+ tr.register(t2, owner);
80269
+ tr.exposeToProvider(t2.name);
80270
+ },
80118
80271
  unregister: (name) => {
80119
80272
  assertCanMutateTool(name, "unregister");
80120
80273
  return tr.unregister(name);
@@ -81192,6 +81345,15 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
81192
81345
  maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH
81193
81346
  };
81194
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
+ }
81195
81357
  function parseReviewSeverity(text2) {
81196
81358
  const result = { critical: 0, high: 0, medium: 0 };
81197
81359
  if (!text2) return result;
@@ -81208,9 +81370,20 @@ function parseReviewSeverity(text2) {
81208
81370
  }
81209
81371
  return result;
81210
81372
  }
81211
- function decideCascadeAgents(text2, severities) {
81373
+ function decideCascadeAgents(text2, severities, findings) {
81212
81374
  const agents = /* @__PURE__ */ new Set();
81213
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
+ }
81214
81387
  const securityKeywords = [
81215
81388
  "injection",
81216
81389
  "xss",
@@ -81583,7 +81756,9 @@ function createAutoReviewPlugin() {
81583
81756
  maxFiles: cfg.maxFilesPerBatch,
81584
81757
  autoFix: "off",
81585
81758
  cascadeOn: "off",
81586
- maxCascadeDepth: 0
81759
+ maxCascadeDepth: 0,
81760
+ fallbackModels: [...cfg.fallbackModels],
81761
+ fallbackProfile: void 0
81587
81762
  },
81588
81763
  files: filesWithContent,
81589
81764
  activeTodos: ctxTodos,
@@ -81688,7 +81863,9 @@ function createAutoReviewPlugin() {
81688
81863
  maxFiles: cfg.maxFilesPerBatch,
81689
81864
  autoFix: "off",
81690
81865
  cascadeOn: "off",
81691
- maxCascadeDepth: 0
81866
+ maxCascadeDepth: 0,
81867
+ fallbackModels: [...cfg.fallbackModels],
81868
+ fallbackProfile: void 0
81692
81869
  },
81693
81870
  files: filesWithContent,
81694
81871
  cascadeOn: cfg.cascadeOn,
@@ -81730,23 +81907,31 @@ function createAutoReviewPlugin() {
81730
81907
  if (!p.reviewText) return;
81731
81908
  const cascadeOn = p.bundle.cascadeOn ?? "off";
81732
81909
  if (cascadeOn === "off") return;
81733
- 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);
81734
81913
  const threshold = shouldCascade(cascadeOn, severities);
81735
81914
  if (!threshold) return;
81736
- const agents = decideCascadeAgents(p.reviewText, severities);
81915
+ const agents = decideCascadeAgents(
81916
+ p.reviewText,
81917
+ severities,
81918
+ parsed ? verifiedFindings : void 0
81919
+ );
81737
81920
  if (agents.length === 0) {
81738
81921
  return;
81739
81922
  }
81740
81923
  const cascadePayload = {
81741
81924
  bundle: p.bundle,
81925
+ ...p.reportId ? { reportId: p.reportId } : {},
81742
81926
  reviewText: p.reviewText,
81743
81927
  severities,
81744
81928
  threshold,
81745
- agents
81929
+ agents,
81930
+ ...parsed ? { verifiedFindings } : {}
81746
81931
  };
81747
81932
  api.emitCustom("chimera.cascade_needed", cascadePayload);
81748
81933
  api.log.info(
81749
- `[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))` : ""}`
81750
81935
  );
81751
81936
  } catch (err) {
81752
81937
  api.log.warn(
@@ -81778,12 +81963,88 @@ init_review_finding_store();
81778
81963
  // src/plugins/review-finding-parser.ts
81779
81964
  init_review_finding_types();
81780
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
+ }
81781
82029
  var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
81782
82030
  var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
81783
82031
  function parseChimeraReviewReport(reportText, context = {}) {
81784
82032
  if (!reportText || reportText.trim().length === 0) {
81785
82033
  return { findings: [], unparseableCount: 0 };
81786
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
+ }
81787
82048
  const findings = [];
81788
82049
  const reportId = context.reportId ?? randomUUID40();
81789
82050
  let unparseableCount = 0;
@@ -81915,14 +82176,92 @@ function normalizeFindingSource(reviewType) {
81915
82176
  return "chimera";
81916
82177
  }
81917
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
+ }
81918
82205
 
81919
82206
  // src/plugins/review-report-integration.ts
81920
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
81921
82260
  init_review_report_store();
81922
82261
  async function persistReviewReport(payload, reportId, projectDir) {
81923
82262
  const store = new JsonlReportStore(projectDir);
81924
82263
  const existed = await store.get(reportId);
81925
- const source = classifySource(payload);
82264
+ const source = classifyChimeraReviewSource(payload.bundle);
81926
82265
  const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
81927
82266
  const sessionId = payload.sessionId ?? payload.cwd;
81928
82267
  const model = payload.bundle.config.model;
@@ -81932,7 +82271,13 @@ async function persistReviewReport(payload, reportId, projectDir) {
81932
82271
  status: f.status
81933
82272
  }));
81934
82273
  const reviewStatus = payload.status === "success" ? "success" : "failed";
81935
- 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 };
81936
82281
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
81937
82282
  for (const finding of parsed.findings) {
81938
82283
  counts[finding.severity]++;
@@ -81950,7 +82295,12 @@ async function persistReviewReport(payload, reportId, projectDir) {
81950
82295
  unparseableCount: parsed.unparseableCount,
81951
82296
  durationSeconds: parsed.durationSeconds,
81952
82297
  rawText: payload.reviewText,
81953
- ...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 } : {}
81954
82304
  };
81955
82305
  await store.persist(input);
81956
82306
  if (reviewStatus === "success" && parsed.findings.length === 0 && parsed.unparseableCount === 0 && isExplicitAllClearReview(payload.reviewText)) {
@@ -82013,13 +82363,6 @@ async function syncReportReopen(reportId, projectDir, actor, reason) {
82013
82363
  });
82014
82364
  return { reportId, reopened: true, previousLifecycle: report.lifecycle };
82015
82365
  }
82016
- function classifySource(payload) {
82017
- const cascadeDepth = payload.bundle.cascadeDepth ?? 0;
82018
- if (cascadeDepth > 0) return "cascade";
82019
- const cascadeOn = payload.bundle.cascadeOn;
82020
- if (cascadeOn !== void 0 && cascadeOn !== "off") return "auto";
82021
- return "chimera";
82022
- }
82023
82366
 
82024
82367
  // src/plugins/review-finding-commands.ts
82025
82368
  async function executeFindingCommand(args, ctx) {
@@ -82349,6 +82692,18 @@ async function showReport(id, ctx) {
82349
82692
  `**Review status:** ${report.reviewStatus}`,
82350
82693
  ...report.cascadeDepth !== void 0 ? [`**Cascade depth:** ${report.cascadeDepth}`] : [],
82351
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
+ ] : [],
82352
82707
  "",
82353
82708
  "**Severity counts:**",
82354
82709
  ` \u{1F534} Critical: ${report.counts.critical}`,
@@ -82457,7 +82812,9 @@ function resolveChimeraConfig(cfg, sessionProvider, sessionModel) {
82457
82812
  maxFiles: cfg.maxFiles ?? DEFAULT_MAX_FILES,
82458
82813
  autoFix: cfg.autoFix ?? "off",
82459
82814
  cascadeOn: cfg.cascadeOn ?? DEFAULT_CASCADE_ON,
82460
- 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
82461
82818
  };
82462
82819
  }
82463
82820
  var CHIMERA_REVIEW_PROMPT = readBundledInstructionText("llm/chimera-review.md");
@@ -84121,49 +84478,6 @@ function dim(s) {
84121
84478
  return `\x1B[2m${s}\x1B[0m`;
84122
84479
  }
84123
84480
 
84124
- // src/plugins/review-finding-integration.ts
84125
- init_review_finding_store();
84126
- async function integrateFindings(payload, projectDir, reportId) {
84127
- if (!payload.reviewText || payload.reviewText.trim().length === 0) {
84128
- return { created: 0, relinked: 0, reopened: 0, totalFindings: 0, unparseableCount: 0 };
84129
- }
84130
- const store = new JsonlFindingStore(projectDir);
84131
- const source = (payload.bundle.cascadeDepth ?? 0) > 0 ? "cascade" : payload.bundle.cascadeOn !== void 0 && payload.bundle.cascadeOn !== "off" ? "auto" : "chimera";
84132
- const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
84133
- const sessionId = payload.sessionId ?? payload.cwd;
84134
- const model = payload.bundle.config.model;
84135
- const parsed = parseChimeraReviewReport(payload.reviewText, {
84136
- sessionId,
84137
- agentId,
84138
- reviewerModel: model,
84139
- reviewType: source,
84140
- reportId
84141
- });
84142
- if (parsed.findings.length === 0) {
84143
- return {
84144
- created: 0,
84145
- relinked: 0,
84146
- reopened: 0,
84147
- totalFindings: 0,
84148
- unparseableCount: parsed.unparseableCount
84149
- };
84150
- }
84151
- const result = await store.upsert(parsed.findings, {
84152
- sessionId,
84153
- reportId,
84154
- agentId,
84155
- model
84156
- });
84157
- return {
84158
- created: result.created,
84159
- relinked: result.relinked,
84160
- reopened: result.reopened,
84161
- reportId,
84162
- totalFindings: parsed.findings.length,
84163
- unparseableCount: parsed.unparseableCount
84164
- };
84165
- }
84166
-
84167
84481
  // src/index.ts
84168
84482
  init_review_finding_store();
84169
84483
  init_review_report_store();
@@ -92758,7 +93072,7 @@ function createFallbackChainManageTool(opts) {
92758
93072
  name: FALLBACK_CHAIN_MANAGE_TOOL_NAME,
92759
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.",
92760
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).',
92761
- category: "Config",
93075
+ category: "config",
92762
93076
  inputSchema: FALLBACK_CHAIN_SCHEMA,
92763
93077
  permission: "auto",
92764
93078
  mutating: true,
@@ -92908,7 +93222,7 @@ function createFavoriteManageTool(opts) {
92908
93222
  name: FAVORITE_MANAGE_TOOL_NAME,
92909
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.",
92910
93224
  usageHint: 'Start with "list" to see current favorites. Use "add <provider/model>" to add. Use "remove <index|ref>" to remove.',
92911
- category: "Config",
93225
+ category: "config",
92912
93226
  inputSchema: FAVORITE_MANAGE_SCHEMA,
92913
93227
  permission: "auto",
92914
93228
  mutating: true,
@@ -92988,7 +93302,7 @@ async function storeProviderKey(providers, input, keyValue, opts) {
92988
93302
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
92989
93303
  });
92990
93304
  entry.apiKeys = existingKeys;
92991
- entry.apiKey = void 0;
93305
+ delete entry.apiKey;
92992
93306
  if (input.setActive !== false) {
92993
93307
  entry.activeKey = label;
92994
93308
  }
@@ -93031,7 +93345,7 @@ function createSystemConfigViewTool(opts) {
93031
93345
  name: SYSTEM_CONFIG_VIEW_TOOL_NAME,
93032
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.",
93033
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.',
93034
- category: "Config",
93348
+ category: "config",
93035
93349
  inputSchema: SYSTEM_CONFIG_VIEW_SCHEMA,
93036
93350
  permission: "auto",
93037
93351
  mutating: false,
@@ -93335,7 +93649,7 @@ function createFallbackProfileManageTool(opts) {
93335
93649
  name: FALLBACK_PROFILE_MANAGE_TOOL_NAME,
93336
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.",
93337
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.',
93338
- category: "Config",
93652
+ category: "config",
93339
93653
  inputSchema: FALLBACK_PROFILE_SCHEMA,
93340
93654
  permission: "auto",
93341
93655
  mutating: true,
@@ -93446,7 +93760,7 @@ function createAgentModelAssignTool(opts) {
93446
93760
  name: AGENT_MODEL_ASSIGN_TOOL_NAME,
93447
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.",
93448
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.',
93449
- category: "Config",
93763
+ category: "config",
93450
93764
  inputSchema: AGENT_MODEL_ASSIGN_SCHEMA,
93451
93765
  permission: "auto",
93452
93766
  mutating: true,
@@ -93594,7 +93908,7 @@ function createProviderManageTool(opts) {
93594
93908
  name: PROVIDER_MANAGE_TOOL_NAME,
93595
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.",
93596
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.',
93597
- category: "Config",
93911
+ category: "config",
93598
93912
  inputSchema: PROVIDER_MANAGE_SCHEMA,
93599
93913
  permission: "auto",
93600
93914
  mutating: true,
@@ -93743,9 +94057,11 @@ function createProviderKeySetTool(opts) {
93743
94057
  name: PROVIDER_KEY_SET_TOOL_NAME,
93744
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.",
93745
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.',
93746
- category: "Config",
94060
+ category: "config",
93747
94061
  inputSchema: PROVIDER_KEY_SET_SCHEMA,
93748
- 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",
93749
94065
  mutating: true,
93750
94066
  riskTier: "standard",
93751
94067
  icon: "settings",
@@ -93841,7 +94157,7 @@ function createLeaderModelSetTool(opts) {
93841
94157
  name: LEADER_MODEL_SET_TOOL_NAME,
93842
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).',
93843
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.',
93844
- category: "Config",
94160
+ category: "config",
93845
94161
  inputSchema: LEADER_MODEL_SET_SCHEMA,
93846
94162
  permission: "auto",
93847
94163
  mutating: true,
@@ -93865,11 +94181,21 @@ function createLeaderModelSetTool(opts) {
93865
94181
  if (!input.provider || !input.model) {
93866
94182
  return { status: "error", message: 'Provide "provider" and "model" for the leader.' };
93867
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
+ }
93868
94193
  await opts.updateConfig((cfg) => {
93869
94194
  cfg.provider = input.provider;
93870
94195
  cfg.model = input.model;
93871
94196
  });
93872
- 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}` };
93873
94199
  }
93874
94200
  if (input.action === "profile") {
93875
94201
  if (!input.profile) {
@@ -93888,15 +94214,25 @@ function createLeaderModelSetTool(opts) {
93888
94214
  return { status: "error", message: `Cannot parse "${first}" as a valid model reference.` };
93889
94215
  }
93890
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
+ }
93891
94226
  await opts.updateConfig((cfg) => {
93892
94227
  cfg.provider = provider;
93893
94228
  cfg.model = model;
93894
94229
  cfg.fallbackModels = rest;
93895
94230
  });
94231
+ const profileLiveNote = opts.switchProviderAndModel ? "" : "\n (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
93896
94232
  return {
93897
94233
  status: "ok",
93898
94234
  message: `\u2713 Leader \u2192 ${provider}/${model} (profile: ${input.profile})` + (rest.length > 0 ? `
93899
- Fallback chain: ${rest.join(" \u2192 ")}` : "")
94235
+ Fallback chain: ${rest.join(" \u2192 ")}` : "") + profileLiveNote
93900
94236
  };
93901
94237
  }
93902
94238
  if (input.action === "toggle") {
@@ -94067,20 +94403,22 @@ async function runEnable(name, deps) {
94067
94403
  const known = Object.keys(all).join(", ");
94068
94404
  return `Unknown server "${name}". Available presets: ${known}`;
94069
94405
  }
94070
- await updateJsonObjectFile(deps.configPath, (full) => {
94406
+ const persistEnabled = () => updateJsonObjectFile(deps.configPath, (full) => {
94071
94407
  const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
94072
94408
  setJsonPath(full, ["mcpServers", name], { ...current[name], ...cfg, enabled: true });
94073
94409
  });
94074
94410
  try {
94075
94411
  const live = deps.registry.describe().find((s) => s.name === name);
94076
94412
  if (live && live.state === "connected") {
94077
- 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).`;
94078
94415
  }
94079
94416
  await deps.registry.start({ ...cfg, enabled: true });
94417
+ await persistEnabled();
94080
94418
  const updated = deps.registry.describe().find((s) => s.name === name);
94081
- return `${green("\u2713 Enabled and started")} "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
94419
+ return `Enabled and started "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
94082
94420
  } catch (err) {
94083
- return `${red("\u2717 Failed to start")} "${name}": ${toErrorMessage(err)}`;
94421
+ return `Failed to start "${name}": ${toErrorMessage(err)}. Config was left unchanged (server stays disabled).`;
94084
94422
  }
94085
94423
  }
94086
94424
  async function runDisable(name, deps) {
@@ -94154,34 +94492,34 @@ function isMcpServerRecord(value) {
94154
94492
  return !!value && typeof value === "object" && !Array.isArray(value);
94155
94493
  }
94156
94494
  function bold(s) {
94157
- return `\x1B[1m${s}\x1B[0m`;
94495
+ return s;
94158
94496
  }
94159
94497
  function dim2(s) {
94160
- return `\x1B[2m${s}\x1B[0m`;
94498
+ return s;
94161
94499
  }
94162
94500
  function green(s) {
94163
- return `\x1B[32m${s}\x1B[0m`;
94501
+ return s;
94164
94502
  }
94165
94503
  function yellow(s) {
94166
- return `\x1B[33m${s}\x1B[0m`;
94504
+ return s;
94167
94505
  }
94168
94506
  function red(s) {
94169
- return `\x1B[31m${s}\x1B[0m`;
94507
+ return s;
94170
94508
  }
94171
94509
  function badge(state) {
94172
94510
  switch (state) {
94173
94511
  case "connected":
94174
- return green("\u25CF connected");
94512
+ return "\u25CF connected";
94175
94513
  case "connecting":
94176
- return `\x1B[36m\u25D0 connecting\x1B[0m`;
94514
+ return "\u25D0 connecting";
94177
94515
  case "reconnecting":
94178
- return `\x1B[36m\u25D1 reconnecting\x1B[0m`;
94516
+ return "\u25D1 reconnecting";
94179
94517
  case "disconnected":
94180
- return dim2("\u25CB disconnected");
94518
+ return "\u25CB disconnected";
94181
94519
  case "failed":
94182
- return red("\u2717 failed");
94520
+ return "\u2717 failed";
94183
94521
  default:
94184
- return dim2(state);
94522
+ return state;
94185
94523
  }
94186
94524
  }
94187
94525
 
@@ -94223,13 +94561,19 @@ function createMcpUseTool(opts) {
94223
94561
  const servers = registry2.describe();
94224
94562
  const serverInfo = servers.find((s) => s.name === serverName);
94225
94563
  if (!serverInfo) {
94226
- 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
+ );
94227
94567
  }
94228
94568
  if (serverInfo.state !== "connected") {
94229
- 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
+ );
94230
94572
  }
94231
- if (registry2.activateServer) {
94232
- registry2.activateServer(serverName);
94573
+ const alreadyActive = registry2.isActivated?.(serverName) === true;
94574
+ const didActivate = !alreadyActive && Boolean(registry2.activateServer);
94575
+ if (didActivate) {
94576
+ registry2.activateServer?.(serverName);
94233
94577
  }
94234
94578
  try {
94235
94579
  const qualifiedName = mcpQualifiedToolName(serverName, toolName);
@@ -94237,7 +94581,7 @@ function createMcpUseTool(opts) {
94237
94581
  if (!mcpTool) {
94238
94582
  const allTools = toolRegistry.list().filter((t2) => t2.name.startsWith(mcpServerToolPrefix(serverName))).map((t2) => t2.name.replace(mcpServerToolPrefix(serverName), ""));
94239
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.`;
94240
- return `Tool "${toolName}" not found on server "${serverName}". ${hint}`;
94584
+ throw new Error(`Tool "${toolName}" not found on server "${serverName}". ${hint}`);
94241
94585
  }
94242
94586
  const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
94243
94587
  if (typeof governedExecute !== "function") {
@@ -94247,7 +94591,7 @@ function createMcpUseTool(opts) {
94247
94591
  if (!result.success) throw new Error(result.error ?? "MCP tool execution failed");
94248
94592
  return result.result;
94249
94593
  } finally {
94250
- if (registry2.deactivateServer) {
94594
+ if (didActivate && registry2.deactivateServer) {
94251
94595
  registry2.deactivateServer(serverName);
94252
94596
  }
94253
94597
  }
@@ -94257,6 +94601,7 @@ function createMcpUseTool(opts) {
94257
94601
 
94258
94602
  // src/tools/one-shot-llm-tool.ts
94259
94603
  var ONE_SHOT_LLM_TOOL_NAME = "llm";
94604
+ var MAX_TIMEOUT_MS2 = 12e4;
94260
94605
  var INPUT_SCHEMA2 = {
94261
94606
  type: "object",
94262
94607
  properties: {
@@ -94333,9 +94678,10 @@ var INPUT_SCHEMA2 = {
94333
94678
  },
94334
94679
  timeoutMs: {
94335
94680
  type: "number",
94336
- description: "Hard timeout in ms (default 30s)."
94681
+ description: `Hard timeout in ms (default 30s, clamped to a maximum of ${MAX_TIMEOUT_MS2}).`
94337
94682
  }
94338
- }
94683
+ },
94684
+ additionalProperties: false
94339
94685
  };
94340
94686
  function createOneShotLLMTool(opts) {
94341
94687
  const orchestrator = new OneShotOrchestrator({
@@ -94343,6 +94689,7 @@ function createOneShotLLMTool(opts) {
94343
94689
  getConfig: opts.getConfig,
94344
94690
  fallbackProfileManager: opts.fallbackProfileManager,
94345
94691
  modelRouter: opts.modelRouter,
94692
+ statusTracker: opts.statusTracker,
94346
94693
  logger: opts.logger,
94347
94694
  wrapProviderCall: opts.wrapProviderCall
94348
94695
  });
@@ -94351,8 +94698,23 @@ function createOneShotLLMTool(opts) {
94351
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.",
94352
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.",
94353
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",
94354
94704
  permission: "auto",
94355
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
+ },
94356
94718
  async execute(input, _ctx, { signal }) {
94357
94719
  if (!input.model && !input.providerId && !opts.defaultModel && !opts.defaultProvider) {
94358
94720
  return {
@@ -94369,7 +94731,11 @@ function createOneShotLLMTool(opts) {
94369
94731
  ...input,
94370
94732
  signal: input.signal ? AbortSignal.any([input.signal, signal]) : signal,
94371
94733
  model: input.model ?? opts.defaultModel,
94372
- 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 }
94373
94739
  };
94374
94740
  return orchestrator.call(effectiveInput);
94375
94741
  }
@@ -96346,6 +96712,8 @@ export {
96346
96712
  logHqAuthAudit,
96347
96713
  logicalCalendarTarget,
96348
96714
  mailboxIdentityBase,
96715
+ mailboxProjectServerEndpoint,
96716
+ mailboxProjectServerMetadataPath,
96349
96717
  mailboxSessionTag,
96350
96718
  makeAgentSubagentRunner,
96351
96719
  makeAskResultTool,
@@ -96361,7 +96729,6 @@ export {
96361
96729
  makeDesignStudioRequestMiddleware,
96362
96730
  makeDesignVerifyToolCallMiddleware,
96363
96731
  makeDirectorSessionFactory,
96364
- makeDomainGlossaryContributor,
96365
96732
  makeFleetEmitTool,
96366
96733
  makeFleetStatusTool,
96367
96734
  makeFleetTool,