@kernlang/agon 0.1.8 → 0.2.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 (45) hide show
  1. package/dist/{chunk-GMVFKWQA.js → chunk-24EWX243.js} +1393 -126
  2. package/dist/chunk-24EWX243.js.map +1 -0
  3. package/dist/{chunk-SUT2HDOY.js → chunk-47QQZGXP.js} +11 -1
  4. package/dist/{chunk-SUT2HDOY.js.map → chunk-47QQZGXP.js.map} +1 -1
  5. package/dist/{chunk-45YTXJWJ.js → chunk-4ZDVR5XR.js} +3 -1
  6. package/dist/{chunk-45YTXJWJ.js.map → chunk-4ZDVR5XR.js.map} +1 -1
  7. package/dist/{chunk-I2PMSXJ3.js → chunk-6GENPQFW.js} +2 -2
  8. package/dist/{chunk-KPU23NS2.js → chunk-6SOOHJZQ.js} +75 -13
  9. package/dist/chunk-6SOOHJZQ.js.map +1 -0
  10. package/dist/{chunk-GHAMYNRC.js → chunk-FORBHCTM.js} +41 -7
  11. package/dist/chunk-FORBHCTM.js.map +1 -0
  12. package/dist/{chunk-CQBQPSE4.js → chunk-MMPLPYSK.js} +219 -29
  13. package/dist/chunk-MMPLPYSK.js.map +1 -0
  14. package/dist/{chunk-BPKY4OF2.js → chunk-MWF4RHRU.js} +3072 -1011
  15. package/dist/chunk-MWF4RHRU.js.map +1 -0
  16. package/dist/{dispatch-J4RSWLXM.js → dispatch-FQQWL2YW.js} +2 -2
  17. package/dist/engines/agy.json +6 -0
  18. package/dist/engines/claude.json +4 -0
  19. package/dist/engines/codex.json +4 -0
  20. package/dist/engines/minimax-coding-plan-minimax-m3.json +4 -0
  21. package/dist/{forge-5QSRUNW6.js → forge-ZFCSXC3Z.js} +6 -6
  22. package/dist/index.js +16712 -14634
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/engines/agy.json +6 -0
  25. package/dist/mcp/engines/claude.json +4 -0
  26. package/dist/mcp/engines/codex.json +4 -0
  27. package/dist/mcp/engines/minimax-coding-plan-minimax-m3.json +4 -0
  28. package/dist/mcp/index.js +34 -6
  29. package/dist/mcp/index.js.map +1 -1
  30. package/dist/plan-mode-I3BZOBFB.js +17 -0
  31. package/dist/{src-253BUXEF.js → src-WMV62WO7.js} +171 -9
  32. package/dist/{update-WLRTYR77.js → update-H3JQXPGO.js} +6 -6
  33. package/package.json +2 -2
  34. package/dist/chunk-BPKY4OF2.js.map +0 -1
  35. package/dist/chunk-CQBQPSE4.js.map +0 -1
  36. package/dist/chunk-GHAMYNRC.js.map +0 -1
  37. package/dist/chunk-GMVFKWQA.js.map +0 -1
  38. package/dist/chunk-KPU23NS2.js.map +0 -1
  39. package/dist/plan-mode-5IQ2SKIS.js +0 -17
  40. /package/dist/{chunk-I2PMSXJ3.js.map → chunk-6GENPQFW.js.map} +0 -0
  41. /package/dist/{dispatch-J4RSWLXM.js.map → dispatch-FQQWL2YW.js.map} +0 -0
  42. /package/dist/{forge-5QSRUNW6.js.map → forge-ZFCSXC3Z.js.map} +0 -0
  43. /package/dist/{plan-mode-5IQ2SKIS.js.map → plan-mode-I3BZOBFB.js.map} +0 -0
  44. /package/dist/{src-253BUXEF.js.map → src-WMV62WO7.js.map} +0 -0
  45. /package/dist/{update-WLRTYR77.js.map → update-H3JQXPGO.js.map} +0 -0
@@ -4,28 +4,34 @@ import {
4
4
  cleanEngineOutput,
5
5
  icons,
6
6
  parseMarkdownBlocks
7
- } from "./chunk-I2PMSXJ3.js";
7
+ } from "./chunk-6GENPQFW.js";
8
8
  import {
9
9
  AGON_MODE_NAMES,
10
10
  CORPUS_PATH,
11
11
  FitnessError,
12
+ PERMISSION_DENIED_MESSAGE,
12
13
  RUNS_DIR,
13
14
  Semaphore,
14
15
  ToolRegistry,
15
16
  agonPath,
17
+ appendAttribution,
16
18
  appendMessage,
17
19
  appendUserTurnIfAbsent,
18
20
  applyPatch,
19
21
  assignForgeRoles,
22
+ bashRanGate,
23
+ budgetRatioPct,
20
24
  buildCodebaseMap,
21
25
  buildCritiquePrompt,
22
26
  buildForgePrompt,
23
27
  buildHistoryPrimedPrompt,
24
28
  buildKernContextSpine,
29
+ buildProjectMemoryBlock,
25
30
  buildSpecializedPrompt,
26
31
  buildStageContext,
27
32
  buildSynthesisPrompt,
28
33
  buildToolSystemPrompt,
34
+ checkSessionBudget,
29
35
  classifyDispatchFailure,
30
36
  classifyTask,
31
37
  composeTeams,
@@ -44,6 +50,7 @@ import {
44
50
  createGoalTool,
45
51
  createGrepTool,
46
52
  createListPlansTool,
53
+ createMultiEditTool,
47
54
  createPersistentSession,
48
55
  createPipelineTool,
49
56
  createProposePlanTool,
@@ -52,6 +59,7 @@ import {
52
59
  createReportConfidenceTool,
53
60
  createRetrieveResultTool,
54
61
  createReviewTool,
62
+ createSaveMemoryTool,
55
63
  createSidechainLogger,
56
64
  createStreamBridge,
57
65
  createTodoWriteTool,
@@ -62,11 +70,14 @@ import {
62
70
  determineWinner,
63
71
  diffFileCount,
64
72
  diffLineCount,
73
+ discoverGate,
65
74
  discoverMcpServers,
66
75
  engineHealth,
67
76
  ensureAgonHome,
68
77
  estimateCost,
78
+ estimateSessionTokens,
69
79
  estimateTokens,
80
+ evaluateToolRules,
70
81
  executeToolCall,
71
82
  extractPatchFilePatterns,
72
83
  formatChatContextForPrompt,
@@ -77,6 +88,7 @@ import {
77
88
  getRatings,
78
89
  gitChangedFiles,
79
90
  hasProjectBrief,
91
+ isGateSkipSignal,
80
92
  isReadOnlyCommand,
81
93
  listCesarPlans,
82
94
  loadConfig,
@@ -84,7 +96,9 @@ import {
84
96
  makeFormat,
85
97
  mcpDiscoveryFingerprint,
86
98
  mcpServersToWireFormat,
99
+ parsePermissionRuleSet,
87
100
  parseToolCalls,
101
+ parseToolHooks,
88
102
  pickTopRatedEngine,
89
103
  planCostEstimator,
90
104
  rankByTaskClass,
@@ -102,6 +116,7 @@ import {
102
116
  stashSnapshot,
103
117
  toolsToOpenAIFormat,
104
118
  tracker,
119
+ updateChatSummary,
105
120
  updateGlickoRanked,
106
121
  updateTeamElo,
107
122
  validateSyntax,
@@ -110,7 +125,7 @@ import {
110
125
  worktreeDiff,
111
126
  worktreePruneOrphaned,
112
127
  worktreeRemoveBestEffort
113
- } from "./chunk-BPKY4OF2.js";
128
+ } from "./chunk-MWF4RHRU.js";
114
129
 
115
130
  // ../forge/src/generated/forge.ts
116
131
  import { randomUUID as randomUUID2 } from "crypto";
@@ -166,7 +181,7 @@ async function healthCheckEngine(engineId, registry, adapter, _cwd, timeoutSec,
166
181
  }
167
182
  const message = err instanceof Error ? err.message : String(err);
168
183
  const status = classifyDispatchFailure({ errorMessage: message });
169
- if (status === "auth-failed" || status === "unreachable") {
184
+ if (status === "auth-failed" || status === "unreachable" || status === "binary-missing") {
170
185
  engineHealth.mark(engineId, status, message);
171
186
  }
172
187
  return {
@@ -190,7 +205,7 @@ async function healthCheckEngine(engineId, registry, adapter, _cwd, timeoutSec,
190
205
  }
191
206
  if (exitCode !== 0) {
192
207
  const status = classifyDispatchFailure({ stderr, exitCode, errorMessage: stderr });
193
- if (status === "auth-failed" || status === "unreachable") {
208
+ if (status === "auth-failed" || status === "unreachable" || status === "binary-missing") {
194
209
  engineHealth.mark(engineId, status, stderr || `exit-${exitCode}`);
195
210
  }
196
211
  return {
@@ -260,7 +275,7 @@ async function preflightHealthFilter(opts) {
260
275
  const afterQuarantine = [];
261
276
  for (const id of engineIds) {
262
277
  const health = engineHealth.get(id);
263
- if (health && (health.status === "auth-failed" || health.status === "unreachable")) {
278
+ if (health && (health.status === "auth-failed" || health.status === "unreachable" || health.status === "binary-missing")) {
264
279
  skipped.push({ engineId: id, status: health.status, reason: health.reason || "quarantined this session" });
265
280
  } else {
266
281
  afterQuarantine.push(id);
@@ -1720,7 +1735,7 @@ async function runForge(options, registry, adapter, onEvent) {
1720
1735
  return false;
1721
1736
  }
1722
1737
  const health = engineHealth.get(id);
1723
- if (health && (health.status === "auth-failed" || health.status === "unreachable")) {
1738
+ if (health && (health.status === "auth-failed" || health.status === "unreachable" || health.status === "binary-missing")) {
1724
1739
  skippedQuarantine.push({ id, reason: health.reason, status: health.status });
1725
1740
  return false;
1726
1741
  }
@@ -3435,7 +3450,7 @@ async function runCouncil(opts) {
3435
3450
  }
3436
3451
  if (cesarId && !engines.includes(cesarId)) {
3437
3452
  const __ch = engineHealth.get(cesarId);
3438
- if (__ch && (__ch.status === "auth-failed" || __ch.status === "unreachable")) {
3453
+ if (__ch && (__ch.status === "auth-failed" || __ch.status === "unreachable" || __ch.status === "binary-missing")) {
3439
3454
  warnings.push(`Configured Cesar chair '${cesarId}' is quarantined this session (${__ch.status}); falling back to ${engines[0]} as chair.`);
3440
3455
  cesarId = "";
3441
3456
  }
@@ -5225,6 +5240,75 @@ async function runThinkChain(opts) {
5225
5240
  };
5226
5241
  }
5227
5242
 
5243
+ // ../forge/src/generated/pr-text.ts
5244
+ function buildPrTextPrompt(opts) {
5245
+ const lines = [];
5246
+ lines.push("Write the pull-request text for a change that was just pushed. You are writing for the human reviewer who will merge it.");
5247
+ lines.push("");
5248
+ lines.push("TASK / INTENT:");
5249
+ lines.push(opts.intent.trim());
5250
+ if (opts.context && opts.context.trim()) {
5251
+ lines.push("");
5252
+ lines.push("RUN FACTS (verified \u2014 you may state these):");
5253
+ lines.push(opts.context.trim());
5254
+ }
5255
+ if (opts.commits.trim()) {
5256
+ lines.push("");
5257
+ lines.push("COMMITS ON THIS BRANCH:");
5258
+ lines.push(opts.commits.trim());
5259
+ }
5260
+ lines.push("");
5261
+ lines.push("DIFF (may be truncated):");
5262
+ lines.push(opts.diff.trim() || "(diff unavailable \u2014 describe from the commits and run facts only)");
5263
+ lines.push("");
5264
+ lines.push("Requirements:");
5265
+ lines.push("- Title: ONE imperative line, max 72 chars, conventional-commit style when natural (feat:/fix:/refactor:/\u2026). No trailing period.");
5266
+ lines.push("- Body: GitHub markdown with exactly these sections:");
5267
+ lines.push(" ## Summary \u2014 2-4 sentences: what changed and why.");
5268
+ lines.push(" ## Changes \u2014 bullets of the concrete changes, grouped by area, naming key files.");
5269
+ lines.push(" ## Verification \u2014 how it was verified. ONLY state what the run facts/diff show (gate command, tests in the diff); never invent test runs or results.");
5270
+ lines.push("- No preamble, no sign-off, no emojis, no flattery, nothing the diff does not support.");
5271
+ lines.push("");
5272
+ lines.push('Reply EXACTLY in this format (first line literally starts with "TITLE: "):');
5273
+ lines.push("TITLE: <the title>");
5274
+ lines.push("");
5275
+ lines.push("<the body markdown>");
5276
+ return lines.join("\n");
5277
+ }
5278
+ function parsePrText(raw) {
5279
+ let cleaned = raw.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim();
5280
+ const fence = cleaned.match(/^```[a-z]*\s*\n([\s\S]*?)\n```\s*$/i);
5281
+ if (fence) cleaned = fence[1].trim();
5282
+ if (!cleaned) return null;
5283
+ const lines = cleaned.split("\n");
5284
+ const i = lines.findIndex((l) => /^\s*TITLE:\s*\S/.test(l));
5285
+ if (i < 0) return null;
5286
+ const title = lines[i].replace(/^\s*TITLE:\s*/, "").trim();
5287
+ let body = lines.slice(i + 1).join("\n").trim();
5288
+ body = body.replace(/^BODY:\s*/i, "").trim();
5289
+ if (!title || !body) return null;
5290
+ return { title, body };
5291
+ }
5292
+ async function runPrText(opts) {
5293
+ try {
5294
+ const r = await runDelegate({
5295
+ engineId: opts.engineId,
5296
+ task: buildPrTextPrompt(opts),
5297
+ cwd: opts.cwd,
5298
+ registry: opts.registry,
5299
+ adapter: opts.adapter,
5300
+ timeout: opts.timeout,
5301
+ outputDir: opts.outputDir,
5302
+ signal: opts.signal
5303
+ });
5304
+ const parsed = parsePrText(r.response ?? "");
5305
+ if (!parsed) return { ok: false, title: "", body: "", engineId: opts.engineId };
5306
+ return { ok: true, title: parsed.title, body: parsed.body, engineId: opts.engineId };
5307
+ } catch {
5308
+ return { ok: false, title: "", body: "", engineId: opts.engineId };
5309
+ }
5310
+ }
5311
+
5228
5312
  // ../forge/src/generated/goal/journal.ts
5229
5313
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync as renameSync4 } from "fs";
5230
5314
  import { dirname as dirname2 } from "path";
@@ -6078,7 +6162,7 @@ ${gate2.stdout || ""}`), detail: `gate failed after fix pass${logPath ? ` \u2014
6078
6162
  if (rev.blocking) throw { kind: "review", detail: rev.summary };
6079
6163
  }
6080
6164
  await git(["add", "-A"], wt);
6081
- const commit = await git(["commit", "-m", `goal(${spec.goalId}): ${task.source}`, "--no-verify"], wt);
6165
+ const commit = await git(["commit", "-m", appendAttribution(`goal(${spec.goalId}): ${task.source}`, loadConfig(repoRoot2)), "--no-verify"], wt);
6082
6166
  if (commit.exitCode !== 0) throw { kind: "error", detail: `commit failed: ${commit.stderr.trim()}` };
6083
6167
  const headRes = await git(["rev-parse", "HEAD"], wt);
6084
6168
  const newSha = headRes.stdout.trim();
@@ -6695,10 +6779,11 @@ function filterDefaultOrchestrationEngines(engineIds) {
6695
6779
 
6696
6780
  // src/generated/cesar/brain.ts
6697
6781
  import { join as join14 } from "path";
6698
- import { mkdirSync as mkdirSync13, appendFileSync as appendFileSync2, existsSync as existsSync13, readFileSync as readFileSync13, unlinkSync, readdirSync, writeFileSync as writeFileSync11 } from "fs";
6782
+ import { mkdirSync as mkdirSync13, appendFileSync as appendFileSync2, existsSync as existsSync14, readFileSync as readFileSync14, unlinkSync, readdirSync, writeFileSync as writeFileSync11 } from "fs";
6699
6783
 
6700
6784
  // src/generated/cesar/confidence.ts
6701
6785
  var CONFIDENCE_TIERS = { direct: 96, quickNero: 93, nero: 88, brainstorm: 72, advisor: 72 };
6786
+ var ESCALATION_SUGGESTION_THRESHOLD = 85;
6702
6787
  function parseConfidence(response) {
6703
6788
  const tildeMatch = response.match(/^~(\d{1,3})%\s*/);
6704
6789
  if (tildeMatch) {
@@ -6716,6 +6801,26 @@ function parseConfidence(response) {
6716
6801
  }
6717
6802
  return { value: null, rest: response };
6718
6803
  }
6804
+ function extractStrictConfidence(text) {
6805
+ if (!text) {
6806
+ return null;
6807
+ }
6808
+ const all = Array.from(text.matchAll(/\bCONFIDENCE:\s*~?(\d{1,3})\s*%/gi));
6809
+ const m = all.length > 0 ? all[all.length - 1] : null;
6810
+ if (!m) {
6811
+ return null;
6812
+ }
6813
+ const n = parseInt(m[1], 10);
6814
+ if (!Number.isFinite(n) || n < 0 || n > 100) {
6815
+ return null;
6816
+ }
6817
+ return n;
6818
+ }
6819
+ function buildEscalationSuggestionLine(value) {
6820
+ const dim = "\x1B[2m";
6821
+ const reset = "\x1B[0m";
6822
+ return `${dim}${value}% \u2014 want a nero/tribunal on this?${reset}`;
6823
+ }
6719
6824
  function confidenceColor(value) {
6720
6825
  if (value >= 96) return "\x1B[32m";
6721
6826
  if (value >= 93) return "\x1B[33m";
@@ -6762,8 +6867,61 @@ function parseSuggestion(response) {
6762
6867
  return { action, rest, hardened, tribunalMode, team };
6763
6868
  }
6764
6869
 
6870
+ // src/generated/cesar/todos-marker.ts
6871
+ function parseLiveTodos(response) {
6872
+ const blockRe = /\[TODOS\]([\s\S]*?)\[\/TODOS\]/gi;
6873
+ let found = false;
6874
+ let lastBody = null;
6875
+ let rest = response;
6876
+ const matches = response.match(blockRe);
6877
+ if (!matches || matches.length === 0) {
6878
+ return { todos: [], found: false, rest: response };
6879
+ }
6880
+ found = true;
6881
+ rest = response.replace(blockRe, "").trim();
6882
+ let m;
6883
+ const re2 = /\[TODOS\]([\s\S]*?)\[\/TODOS\]/gi;
6884
+ while ((m = re2.exec(response)) !== null) {
6885
+ lastBody = m[1];
6886
+ }
6887
+ if (lastBody === null) return { todos: [], found, rest };
6888
+ const allowed = /* @__PURE__ */ new Set(["pending", "running", "done", "failed", "cancelled"]);
6889
+ let parsed;
6890
+ try {
6891
+ parsed = JSON.parse(lastBody.trim());
6892
+ } catch {
6893
+ return { todos: [], found, rest };
6894
+ }
6895
+ if (!Array.isArray(parsed)) return { todos: [], found, rest };
6896
+ const MAX_LIVE_TODOS = 50;
6897
+ const order = [];
6898
+ const byId = /* @__PURE__ */ new Map();
6899
+ for (const raw of parsed.slice(0, MAX_LIVE_TODOS)) {
6900
+ if (!raw || typeof raw !== "object") continue;
6901
+ const r = raw;
6902
+ const id = r.id !== void 0 && r.id !== null ? String(r.id) : "";
6903
+ const text = r.text !== void 0 && r.text !== null ? String(r.text) : "";
6904
+ if (!id || !text) continue;
6905
+ const state = allowed.has(String(r.state)) ? String(r.state) : "pending";
6906
+ const note = r.note !== void 0 && r.note !== null ? String(r.note) : void 0;
6907
+ if (!byId.has(id)) order.push(id);
6908
+ byId.set(id, { id, text, state, note, source: "live" });
6909
+ }
6910
+ const todos = order.map((id) => byId.get(id)).filter(Boolean);
6911
+ return { todos, found, rest };
6912
+ }
6913
+ function parsePreamble(response) {
6914
+ const text = String(response ?? "");
6915
+ const m = text.match(/^(\s*)\[INTENT\][ \t]*([^\r\n]*)(\r?\n)?/i);
6916
+ if (!m) return { intent: null, found: false, rest: text };
6917
+ const intent = String(m[2] ?? "").trim();
6918
+ if (!intent) return { intent: null, found: false, rest: text };
6919
+ const rest = text.slice(m[0].length).replace(/^\r?\n/, "");
6920
+ return { intent, found: true, rest };
6921
+ }
6922
+
6765
6923
  // src/generated/cesar/session.ts
6766
- import { readFileSync as readFileSync12, statSync as statSync4, existsSync as existsSync12 } from "fs";
6924
+ import { readFileSync as readFileSync13, statSync as statSync5, existsSync as existsSync13 } from "fs";
6767
6925
  import { isAbsolute as isAbsolute2, resolve as resolve3, dirname as dirname5 } from "path";
6768
6926
  import { join as join12 } from "path";
6769
6927
  import { mkdirSync as mkdirSync11 } from "fs";
@@ -6775,6 +6933,7 @@ function createCesarToolRegistry(engineId) {
6775
6933
  const toolRegistry = new ToolRegistry();
6776
6934
  toolRegistry.register(createReadTool());
6777
6935
  toolRegistry.register(createEditTool());
6936
+ toolRegistry.register(createMultiEditTool());
6778
6937
  toolRegistry.register(createWriteTool());
6779
6938
  toolRegistry.register(createBashTool());
6780
6939
  toolRegistry.register(createGrepTool());
@@ -6792,6 +6951,7 @@ function createCesarToolRegistry(engineId) {
6792
6951
  toolRegistry.register(createReportConfidenceTool());
6793
6952
  toolRegistry.register(createQuickNeroTool());
6794
6953
  toolRegistry.register(createTodoWriteTool());
6954
+ toolRegistry.register(createSaveMemoryTool());
6795
6955
  toolRegistry.register(createProposePlanTool());
6796
6956
  toolRegistry.register(createExitPlanModeTool());
6797
6957
  toolRegistry.register(createListPlansTool());
@@ -6802,7 +6962,7 @@ function createEagerToolContext(ctx, config, signal, dispatch) {
6802
6962
  const cwd = resolveWorkingDir();
6803
6963
  const fsc = getProjectFileStateCache(cwd);
6804
6964
  const explorationMode = ctx.explorationMode ?? false;
6805
- return { cwd, readFileState: fsc.cache, abortSignal: signal, permissionMode: config.permissionMode ?? "ask", explorationMode, allowedCommands: config.allowedCommands ?? [], toolPermissions: config.toolPermissions ?? {}, source: "orchestrator", onProgress: (msg) => dispatch({ type: "spinner-update", message: `Cesar: ${msg}` }) };
6965
+ return { cwd, readFileState: fsc.cache, abortSignal: signal, permissionMode: config.permissionMode ?? "ask", explorationMode, allowedCommands: config.allowedCommands ?? [], toolPermissions: config.toolPermissions ?? {}, source: "orchestrator", permissionRules: parsePermissionRuleSet(config.permissions), toolHooks: parseToolHooks(config.hooks), onProgress: (msg) => dispatch({ type: "spinner-update", message: `Cesar: ${msg}` }) };
6806
6966
  }
6807
6967
  function parseEagerToolInput(toolName, input) {
6808
6968
  const raw = typeof input === "string" ? input : input === void 0 ? "" : (() => {
@@ -6860,7 +7020,7 @@ async function executeEagerTool(toolName, meta, toolRegistry, toolCtx, dispatch,
6860
7020
  result: { ok: false, content: "", error },
6861
7021
  durationMs: 0
6862
7022
  };
6863
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: error });
7023
+ dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: error, durationMs: result2.durationMs });
6864
7024
  return result2;
6865
7025
  }
6866
7026
  const parsedInput = parsed.input ?? {};
@@ -6891,9 +7051,9 @@ async function executeEagerTool(toolName, meta, toolRegistry, toolCtx, dispatch,
6891
7051
  const out = result.result.ok ? result.result.content : result.result.error;
6892
7052
  const status = result.result.ok ? "done" : "error";
6893
7053
  if (streamStarted) {
6894
- dispatch({ type: "tool-stream-end", streamId, engineId: cesarEngineId, tool: toolName, input: toolInput, status, output: out });
7054
+ dispatch({ type: "tool-stream-end", streamId, engineId: cesarEngineId, tool: toolName, input: toolInput, status, output: out, durationMs: result.durationMs });
6895
7055
  } else {
6896
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status, output: out });
7056
+ dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status, output: out, durationMs: result.durationMs });
6897
7057
  }
6898
7058
  return result;
6899
7059
  }
@@ -7042,6 +7202,9 @@ function updateTodoState(todos, id, state, note) {
7042
7202
  function clearTodos() {
7043
7203
  return [];
7044
7204
  }
7205
+ function clearLiveTodos(todos) {
7206
+ return todos.filter((t) => t.source !== "live");
7207
+ }
7045
7208
  function todosFromPlanSteps(steps) {
7046
7209
  const allowed = /* @__PURE__ */ new Set(["pending", "running", "done", "failed", "cancelled"]);
7047
7210
  return steps.map((s) => ({
@@ -7117,7 +7280,7 @@ function _showNextPermission(actions) {
7117
7280
  _drainAutoApproved(actions);
7118
7281
  if (_permissionQueue.length === 0) return;
7119
7282
  const next = _permissionQueue[0];
7120
- actions.addBlock({ type: "permission-ask", tool: next.tool, command: next.command, description: next.description, reason: next.reason, resolve: next.resolve });
7283
+ actions.addBlock({ type: "permission-ask", tool: next.tool, command: next.command, description: next.description, reason: next.reason, diffPreview: next.diffPreview, fallbackNote: next.fallbackNote, resolve: next.resolve });
7121
7284
  const permResolve = next.resolve;
7122
7285
  const permCommand = next.command;
7123
7286
  actions.setQuestionState({
@@ -7127,6 +7290,8 @@ function _showNextPermission(actions) {
7127
7290
  command: permCommand,
7128
7291
  description: next.description,
7129
7292
  reason: next.reason,
7293
+ diffPreview: next.diffPreview,
7294
+ fallbackNote: next.fallbackNote,
7130
7295
  choices: [
7131
7296
  { key: "y", label: "Yes", color: "#4ade80" },
7132
7297
  { key: "n", label: "No", color: "#ef4444" },
@@ -7192,6 +7357,7 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7192
7357
  "context-usage",
7193
7358
  "streaming-chunk",
7194
7359
  "streaming-start",
7360
+ "streaming-preview",
7195
7361
  "thinking-chunk",
7196
7362
  "thinking-start",
7197
7363
  "thinking-stop",
@@ -7238,16 +7404,35 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7238
7404
  }
7239
7405
  return;
7240
7406
  }
7407
+ case "streaming-preview": {
7408
+ const eid = event.engineId;
7409
+ const draftText = event.content;
7410
+ actions.setStreamingText((prev) => {
7411
+ const existing = prev[eid];
7412
+ if (existing && !existing.draft) return prev;
7413
+ return {
7414
+ ...prev,
7415
+ [eid]: {
7416
+ engineId: eid,
7417
+ content: draftText,
7418
+ startedAt: existing ? existing.startedAt : Date.now(),
7419
+ draft: true
7420
+ }
7421
+ };
7422
+ });
7423
+ return;
7424
+ }
7241
7425
  case "streaming-chunk": {
7242
7426
  const eid = event.engineId;
7243
7427
  const chunk = event.chunk;
7244
7428
  actions.setStreamingText((prev) => {
7245
7429
  const existing = prev[eid];
7430
+ const base = existing && !existing.draft ? existing.content : "";
7246
7431
  return {
7247
7432
  ...prev,
7248
7433
  [eid]: {
7249
7434
  engineId: eid,
7250
- content: existing ? existing.content + chunk : chunk,
7435
+ content: base + chunk,
7251
7436
  startedAt: existing ? existing.startedAt : Date.now()
7252
7437
  }
7253
7438
  };
@@ -7258,6 +7443,14 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7258
7443
  const eid = event.engineId;
7259
7444
  const st = state.streamingText[eid];
7260
7445
  if (st) {
7446
+ if (st.draft) {
7447
+ actions.setStreamingText((prev) => {
7448
+ const next = { ...prev };
7449
+ delete next[eid];
7450
+ return next;
7451
+ });
7452
+ return;
7453
+ }
7261
7454
  const color = actions.getEngineColor(st.engineId);
7262
7455
  const cleaned = cleanEngineOutput(st.content);
7263
7456
  actions.setStreamingText((prev) => {
@@ -7291,7 +7484,12 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7291
7484
  return;
7292
7485
  }
7293
7486
  case "todos-clear": {
7294
- actions.setTodos(clearTodos());
7487
+ const scope = event.scope;
7488
+ if (scope === "live") {
7489
+ actions.setTodos((prev) => clearLiveTodos(prev));
7490
+ } else {
7491
+ actions.setTodos(clearTodos());
7492
+ }
7295
7493
  return;
7296
7494
  }
7297
7495
  case "clear":
@@ -7345,6 +7543,8 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7345
7543
  command: event.command,
7346
7544
  description: event.description,
7347
7545
  reason: event.reason,
7546
+ diffPreview: event.diffPreview,
7547
+ fallbackNote: event.fallbackNote,
7348
7548
  resolve: event.resolve
7349
7549
  };
7350
7550
  _permissionQueue.push(entry);
@@ -7389,7 +7589,7 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7389
7589
  }
7390
7590
  case "context-usage": {
7391
7591
  const e = event;
7392
- actions.setCesarContext({ pct: e.pct, used: e.used, limit: e.limit, compacted: e.compacted ?? 0, cached: e.cached ?? 0 });
7592
+ actions.setCesarContext({ pct: e.pct, used: e.used, limit: e.limit, compacted: e.compacted ?? 0, cached: e.cached ?? 0, source: e.source ?? "estimate" });
7393
7593
  return;
7394
7594
  }
7395
7595
  case "agent-step-start": {
@@ -7568,7 +7768,8 @@ function handleOutputEvent(event, state, actions, mode, chatStartTime) {
7568
7768
  tool: e.tool ?? existing.tool ?? "",
7569
7769
  input: e.input ?? existing.input ?? "",
7570
7770
  status: e.status,
7571
- output
7771
+ output,
7772
+ ...e.durationMs !== void 0 ? { durationMs: e.durationMs } : {}
7572
7773
  };
7573
7774
  const key = toolCallKey(toolCallEvent);
7574
7775
  if (e.status === "done" || e.status === "error") {
@@ -8226,8 +8427,22 @@ function shouldSpeculate(hints, config) {
8226
8427
 
8227
8428
  // src/generated/cesar/brain-helpers.ts
8228
8429
  var yieldToInk = () => new Promise((resolve4) => setImmediate(resolve4));
8430
+ function recordCesarTurn(ctx, cesarEngineId, input, response) {
8431
+ try {
8432
+ const s = ctx.cesarSession;
8433
+ const u = s && typeof s.getTurnUsage === "function" ? s.getTurnUsage() : null;
8434
+ if (u && u.totalTokens > 0) {
8435
+ return tracker.record(cesarEngineId, {
8436
+ usage: { promptTokens: u.promptTokens, completionTokens: u.completionTokens, totalTokens: u.totalTokens, source: u.source ?? "sdk" },
8437
+ model: u.model
8438
+ });
8439
+ }
8440
+ } catch {
8441
+ }
8442
+ return tracker.record(cesarEngineId, { prompt: input, response });
8443
+ }
8229
8444
  var splitBeforeToolMarkup = (text) => {
8230
- const markers = ["<tool ", "<invoke ", "<tool_call", "<toolcall", "<tool_call_tool>", "<function=", "[TOOL_CALLS]"];
8445
+ const markers = ["<tool ", "<invoke ", "<tool_call", "<toolcall", "<tool_call_tool>", "<function=", "[TOOL_CALLS]", "[TODOS]"];
8231
8446
  const lower = text.toLowerCase();
8232
8447
  let idx = -1;
8233
8448
  for (const marker of markers) {
@@ -8238,6 +8453,109 @@ var splitBeforeToolMarkup = (text) => {
8238
8453
  return { visible: text.slice(0, idx), hasToolMarkup: true };
8239
8454
  };
8240
8455
  var XML_TOOL_MARKUP_HOLD_CHARS = 24;
8456
+ var createTodosDisplayStripper = () => {
8457
+ let insideBlock = false;
8458
+ let hold = "";
8459
+ const OPEN = "[todos]";
8460
+ const CLOSE = "[/todos]";
8461
+ const trailingPrefixLen = (combined, marker) => {
8462
+ const lower = combined.toLowerCase();
8463
+ for (let n = Math.min(marker.length - 1, combined.length); n > 0; n--) {
8464
+ if (lower.slice(combined.length - n) === marker.slice(0, n)) return n;
8465
+ }
8466
+ return 0;
8467
+ };
8468
+ return (chunk, force = false) => {
8469
+ let combined = hold + chunk;
8470
+ hold = "";
8471
+ if (force) {
8472
+ insideBlock = false;
8473
+ return combined;
8474
+ }
8475
+ let out = "";
8476
+ while (combined.length > 0) {
8477
+ if (insideBlock) {
8478
+ const end = combined.toLowerCase().indexOf(CLOSE);
8479
+ if (end < 0) {
8480
+ const p = trailingPrefixLen(combined, CLOSE);
8481
+ if (p > 0) hold = combined.slice(combined.length - p);
8482
+ combined = "";
8483
+ break;
8484
+ }
8485
+ insideBlock = false;
8486
+ combined = combined.slice(end + CLOSE.length);
8487
+ continue;
8488
+ }
8489
+ const start = combined.toLowerCase().indexOf(OPEN);
8490
+ if (start < 0) break;
8491
+ out += combined.slice(0, start);
8492
+ insideBlock = true;
8493
+ combined = combined.slice(start + OPEN.length);
8494
+ }
8495
+ if (!insideBlock) {
8496
+ const p = trailingPrefixLen(combined, OPEN);
8497
+ if (p > 0) {
8498
+ hold = combined.slice(combined.length - p);
8499
+ combined = combined.slice(0, combined.length - p);
8500
+ }
8501
+ out += combined;
8502
+ }
8503
+ return out;
8504
+ };
8505
+ };
8506
+ var createPreambleStripper = () => {
8507
+ const MARKER = "[intent]";
8508
+ let decided = false;
8509
+ let hold = "";
8510
+ const couldBeMarker = (lowerTrimmed) => {
8511
+ const n = Math.min(lowerTrimmed.length, MARKER.length);
8512
+ return lowerTrimmed.slice(0, n) === MARKER.slice(0, n);
8513
+ };
8514
+ const stripLeadingPreamble = (text) => {
8515
+ const m = text.match(/^(\s*)\[intent\][ \t]*([^\r\n]*)(\r?\n)?/i);
8516
+ if (!m) return text;
8517
+ if (!String(m[2] ?? "").trim()) return text;
8518
+ return text.slice(m[0].length).replace(/^\r?\n/, "");
8519
+ };
8520
+ return (chunk, force = false) => {
8521
+ if (decided) return chunk;
8522
+ hold += chunk;
8523
+ if (force) {
8524
+ decided = true;
8525
+ const afterWs2 = hold.replace(/^\s*/, "");
8526
+ const out = couldBeMarker(afterWs2.toLowerCase()) && afterWs2.toLowerCase().startsWith(MARKER) ? stripLeadingPreamble(hold) : hold;
8527
+ hold = "";
8528
+ return out;
8529
+ }
8530
+ const leadingWsMatch = hold.match(/^\s*/);
8531
+ const leadingWs = leadingWsMatch ? leadingWsMatch[0] : "";
8532
+ const afterWs = hold.slice(leadingWs.length);
8533
+ const lower = afterWs.toLowerCase();
8534
+ if (!couldBeMarker(lower)) {
8535
+ decided = true;
8536
+ const out = hold;
8537
+ hold = "";
8538
+ return out;
8539
+ }
8540
+ if (lower.length < MARKER.length) return "";
8541
+ const nlIdx = afterWs.search(/\r?\n/);
8542
+ if (nlIdx < 0) {
8543
+ return "";
8544
+ }
8545
+ if (!afterWs.slice(MARKER.length, nlIdx).trim()) {
8546
+ decided = true;
8547
+ const out = hold;
8548
+ hold = "";
8549
+ return out;
8550
+ }
8551
+ const nlMatch = afterWs.slice(nlIdx).match(/^\r?\n/);
8552
+ const nlLen = nlMatch ? nlMatch[0].length : 0;
8553
+ const rest = afterWs.slice(nlIdx + nlLen);
8554
+ decided = true;
8555
+ hold = "";
8556
+ return rest;
8557
+ };
8558
+ };
8241
8559
  function isUserDirectedQuestion(lastLine) {
8242
8560
  const last = String(lastLine ?? "").trim();
8243
8561
  if (!/\?\s*$/.test(last)) return false;
@@ -8281,20 +8599,50 @@ function detectNarratedToolStall(text) {
8281
8599
  const STALL_RE = /\b(?:let me (?:check|look|examine|read|search|find|see|review|explore|investigate|understand|get|grab|continue)|i (?:need|want|should|will) (?:to )?(?:check|look|examine|read|search|find|see|review|explore|investigate|understand|get|grab|continue)|now (?:let me|i'll)|continu(?:e|ing)|keep (?:reading|investigating|looking)|next[,.]?\s*(?:i|let)|before (?:i can|proceeding|implementing|deciding))\b/i;
8282
8600
  return FAKE_GATE_RE.test(tail) || READ_INTENT_RE.test(tail) || SEARCH_INTENT_RE.test(tail) || DIR_INTENT_RE.test(tail) || STALL_RE.test(tail);
8283
8601
  }
8602
+ function stripNonAssertionSpans(text) {
8603
+ let body = String(text ?? "");
8604
+ const KEEP_CLAIM_RE = /^(?:i(?:'ll|'m|\s+will|\s+have|\s+need\s+to|\s+want\s+to|\s+already)\b|let\s+me\b|please\s+\w+)/i;
8605
+ body = body.replace(/```[\s\S]*?(?:```|$)/g, " ");
8606
+ body = body.replace(/`([^`\n]{1,300})`/g, (_m, inner) => KEEP_CLAIM_RE.test(inner.trim()) ? ` ${inner} ` : " ");
8607
+ body = body.replace(/["“”]([^"“”\n]{1,300})["“”]/g, (_m, inner) => KEEP_CLAIM_RE.test(inner.trim()) ? ` ${inner} ` : " ");
8608
+ body = body.replace(/\([A-Z][A-Za-z]*(?:\s*[,/&]\s*[A-Z][A-Za-z]*)+\)/g, " ");
8609
+ return body;
8610
+ }
8284
8611
  function detectMutationIntentStall(text) {
8285
- const body = String(text ?? "").trim();
8612
+ const body = stripNonAssertionSpans(String(text ?? "")).trim();
8286
8613
  if (!body) return false;
8287
- const MUTATION_INTENT_RE = /\b(?:edit|write|apply|patch|implement|insert|replace|land it|commit it|the (?:change|fix|diff|patch|edit)|make the (?:edit|change)s?|ready to (?:paste|apply))\b/i;
8288
- const HANDBACK_RE = /\b(?:read-?only|can'?t (?:write|edit|apply|mutate|touch)|no (?:write|edit|bash) tool|(?:edit|write|bash)(?: tool)?(?: is)? (?:not enabled|disabled|not wired|not available|unavailable)|not (?:enabled|wired|reachable) in this (?:context|session|turn)|spawn (?:an? )?agent|dispatch (?:an? )?agent|paste (?:it|this|the\b)|apply (?:it|this)\b|git apply|you (?:can )?(?:run|apply|paste)|in your terminal|hand you the (?:patch|diff|commands?)|copy[- ]?paste)\b/i;
8289
- return MUTATION_INTENT_RE.test(body) && HANDBACK_RE.test(body);
8614
+ const CANNOT_RE = /\b(?:i\s+(?:cannot|can'?t|am\s+unable\s+to)\s+(?:write|edit|apply|mutate|touch)|i'?m\s+read-?only|(?:this|the)\s+(?:session|environment|context|sandbox)\s+is\s+read-?only|i\s+have\s+no\s+(?:write|edit|bash)\s+tools?|no\s+(?:write|edit)\s+access|(?:edit|write|bash)(?:\s+tool)?\s+is\s+(?:not\s+(?:enabled|available|wired)|disabled|unavailable)|not\s+(?:enabled|wired|reachable)\s+in\s+this\s+(?:context|session|turn))\b/i;
8615
+ const USER_APPLY_RE = /\b(?:paste\s+(?:it|this|the)\b|git\s+apply\b|apply\s+(?:it|this)\b|in\s+your\s+terminal|you\s+(?:can\s+)?(?:run|apply|paste)\b|copy[- ]?paste|hand\s+you\s+the\s+(?:patch|diff|commands?))/i;
8616
+ const AGENT_ESCAPE_RE = /\b(?:spawn|dispatch)\s+(?:an?\s+)?agent\b/i;
8617
+ const INTENT_RE = /\b(?:i(?:'ll|\s+will|\s+need\s+to|\s+want\s+to|'?m\s+going\s+to)|let\s+me)\s+(?:\w+\s+){0,3}?(?:edit|write|apply|commit|patch|fix|update|implement|modify|create|make)\b/i;
8618
+ const RESULT_FIRST_RE = /\b(?:patch|fix|change|diff|edit|update)e?s?\s+(?:is|are)?\s*(?:ready|done|complete|prepared|below|attached)\b|\bready\s+to\s+(?:paste|apply)\b/i;
8619
+ const PAST_DONE_RE = /\bi(?:'ve)?\s+(?:already\s+)?(?:made|updated|edited|wrote|written|applied|changed|fixed|patched|implemented|created|prepared)\b/i;
8620
+ const PRESENTATION_RE = /\bhere(?:'s|\s+is|\s+are)\s+(?:the\s+|a\s+)?(?:full\s+)?(?:diff|patch|change|fix|edit|code)\b|\b(?:diff|patch)\s+(?:is\s+)?(?:below|above)\b/i;
8621
+ const intentish = INTENT_RE.test(body) || RESULT_FIRST_RE.test(body) || PAST_DONE_RE.test(body) || PRESENTATION_RE.test(body);
8622
+ if (CANNOT_RE.test(body) && (intentish || USER_APPLY_RE.test(body) || AGENT_ESCAPE_RE.test(body))) return true;
8623
+ if (USER_APPLY_RE.test(body) && intentish) return true;
8624
+ if (AGENT_ESCAPE_RE.test(body) && (intentish || CANNOT_RE.test(body))) return true;
8625
+ return false;
8290
8626
  }
8291
8627
  function detectFabricatedDelegation(text) {
8292
- const body = String(text ?? "").trim();
8628
+ const body = stripNonAssertionSpans(String(text ?? "")).trim();
8293
8629
  if (!body) return false;
8294
8630
  const TARGET_RE = /\b(?:review(?:er)?s?|forg(?:e|ing)|tribunal|brainstorm|campfire|agents?|engines?|jobs?)\b/i;
8295
8631
  if (!TARGET_RE.test(body)) return false;
8296
- const DISPATCH_RE = /\b(?:kick(?:ed|ing)?\s*(?:it|them|that|the\s+\w+)?\s*off|fired?\s*(?:it|them|off)|dispatch(?:ed|ing)|delegat(?:ed|ing)|(?:is|are|now)\s+running|running\s+(?:in|now)|in\s+parallel|reading\s+the\s+(?:diff|changes|code)|working\s+(?:on\s+it|in\s+parallel)|in\s+progress|under\s*way|i'?ll\s+(?:get\s+back|report|let\s+you\s+know|surface|update)|report(?:s|ing)?\s+back|when\s+they\s+(?:report|land|return|finish|come\s+back)|still\s+(?:running|going|working|in\s+progress)|spun?\s+up|started\s+(?:the|a)\s+(?:review|forge|job|tribunal|brainstorm))\b/i;
8297
- return DISPATCH_RE.test(body);
8632
+ const FP_DISPATCH_RE = /\b(?:i(?:'ve)?\s+(?:already\s+)?(?:kick(?:ed)?\s*(?:it|them|that|the\s+\w+)?\s*off|fired\s+(?:it|them|off)|dispatch(?:ed)?|delegat(?:ed)?|started|launched|spun\s+up|queued)|i'?m\s+(?:running|dispatching|launching|starting))\b/i;
8633
+ const DELEGATED_TO_RE = /\b(?:review|forge|tribunal|brainstorm|campfire|jobs?|agents?)\s+(?:was|were|has\s+been|have\s+been)?\s*delegated\s+to\b/i;
8634
+ const RUNNING_STATE_RE = /\b(?:review(?:er)?s?|forge|tribunal|brainstorm|campfire|agents?|engines?|jobs?)\b[^.!?\n]{0,60}?(?:\b(?:is|are)\s+(?:still\s+|currently\s+|each\s+|now\s+)?(?:running|working|reading|evaluating|analyzing|reviewing|on\s+it)\b|\bin\s+progress\b|\bunder\s*way\b)/i;
8635
+ const PASSIVE_DISPATCH_RE = /\b(?:review(?:er)?s?|forge|tribunal|brainstorm|campfire|agents?|engines?|jobs?)\b[^.!?\n]{0,60}?\b(?:was|were|has\s+been|have\s+been|is|are)\s+(?:queued|dispatched|launched|kicked\s+off|spun\s+up|fired\s+off)\b/i;
8636
+ const REPORT_BACK_RE = /\bi'?ll\s+(?:get\s+back|report\s+back|let\s+you\s+know|surface|update)\b|\bwhen\s+they\s+(?:report|land|return|finish|come\s+back)\b/i;
8637
+ return FP_DISPATCH_RE.test(body) || DELEGATED_TO_RE.test(body) || RUNNING_STATE_RE.test(body) || PASSIVE_DISPATCH_RE.test(body) || REPORT_BACK_RE.test(body);
8638
+ }
8639
+ function shouldDeescalateGuard(opts) {
8640
+ const kind = String(opts.intakeKind ?? "");
8641
+ const flow = String(opts.recommendedFlow ?? "");
8642
+ const conversational = kind === "chat" && flow === "answer" || kind === "exploration" && (flow === "campfire" || flow === "brainstorm" || flow === "answer");
8643
+ if (!conversational) return false;
8644
+ if (opts.usedMutatingTool !== false) return false;
8645
+ return true;
8298
8646
  }
8299
8647
  function eagerFailedToolNames(results) {
8300
8648
  const names = [];
@@ -8323,6 +8671,17 @@ function shouldStopAfterXmlToolCall(toolName) {
8323
8671
  const HANDOFF_TOOLS = /* @__PURE__ */ new Set(["Forge", "Brainstorm", "Tribunal", "Campfire", "Pipeline", "Review", "Agent", "Goal", "ProposePlan", "ExitPlanMode"]);
8324
8672
  return HANDOFF_TOOLS.has(String(toolName ?? ""));
8325
8673
  }
8674
+ function stripAgonToolPrefix(name) {
8675
+ const n = String(name ?? "");
8676
+ return n.toLowerCase().startsWith("agon") ? n.slice(4) : n;
8677
+ }
8678
+ function isBashToolName(name) {
8679
+ return stripAgonToolPrefix(name).toLowerCase() === "bash";
8680
+ }
8681
+ var WRITE_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit"]);
8682
+ function isWriteToolName(name) {
8683
+ return WRITE_TOOL_NAMES.has(stripAgonToolPrefix(name).toLowerCase());
8684
+ }
8326
8685
  function buildReviewFollowupPrompt(input, ctx) {
8327
8686
  const trimmed = input.trim();
8328
8687
  const match = trimmed.match(/^fix it(?:\s+with\s+([a-z0-9._-]+))?[\s?!.,;:]*$/i);
@@ -8383,6 +8742,9 @@ function canonicalToolName(tool) {
8383
8742
  if (lower === "agonedit" || lower === "edit" || lower === "fileedit" || lower === "filechange") {
8384
8743
  return "Edit";
8385
8744
  }
8745
+ if (lower === "agonmultiedit" || lower === "multiedit" || lower === "multi_edit") {
8746
+ return "MultiEdit";
8747
+ }
8386
8748
  if (lower === "agonwrite" || lower === "write") {
8387
8749
  return "Write";
8388
8750
  }
@@ -8391,6 +8753,32 @@ function canonicalToolName(tool) {
8391
8753
  }
8392
8754
  return tool;
8393
8755
  }
8756
+ function estimateMultiEditApproval(cachedContent, edits) {
8757
+ if (!Array.isArray(edits) || edits.length === 0) {
8758
+ return { ok: false, reason: "missing edits array", nextContent: "", tokens: 0 };
8759
+ }
8760
+ let working = cachedContent;
8761
+ let tokens = 0;
8762
+ for (let i = 0; i < edits.length; i++) {
8763
+ const e = edits[i] ?? {};
8764
+ const oldS = e.old_string;
8765
+ const newS = e.new_string;
8766
+ if (typeof oldS !== "string" || typeof newS !== "string" || oldS === "") {
8767
+ return { ok: false, reason: `edits[${i}] missing/empty old_string or new_string`, nextContent: "", tokens: 0 };
8768
+ }
8769
+ const occ = countOccurrences(working, oldS);
8770
+ if (occ <= 0) {
8771
+ return { ok: false, reason: `edits[${i}] old_string not found in cached content`, nextContent: "", tokens: 0 };
8772
+ }
8773
+ const replaceAll = e.replace_all === true;
8774
+ if (!replaceAll && occ > 1) {
8775
+ return { ok: false, reason: `edits[${i}] old_string not unique and replace_all not set`, nextContent: "", tokens: 0 };
8776
+ }
8777
+ tokens += estimateChangedTokens(oldS, newS) * (replaceAll ? occ : 1);
8778
+ working = replaceAll ? working.split(oldS).join(newS) : working.replace(oldS, newS);
8779
+ }
8780
+ return { ok: true, reason: "ok", nextContent: working, tokens };
8781
+ }
8394
8782
  function approvalArgsFromCommand(tool, command) {
8395
8783
  const raw = String(command ?? "").trim();
8396
8784
  if (!raw) return {};
@@ -8512,7 +8900,7 @@ function applyCesarSelfTurnApproval(tool, args, toolCtx, config) {
8512
8900
  return { approve: false, reason: "read-only mode active" };
8513
8901
  }
8514
8902
  const t = canonicalToolName(tool);
8515
- if (t !== "Edit" && t !== "Write") {
8903
+ if (t !== "Edit" && t !== "Write" && t !== "MultiEdit") {
8516
8904
  return { approve: false, reason: "not a file edit/write" };
8517
8905
  }
8518
8906
  const configured = toolCtx.toolPermissions?.[t];
@@ -8560,6 +8948,13 @@ function applyCesarSelfTurnApproval(tool, args, toolCtx, config) {
8560
8948
  const editMultiplier = normalizedArgs.replace_all === true ? occurrences : 1;
8561
8949
  diffTokens = perEditTokens * editMultiplier;
8562
8950
  nextContent = normalizedArgs.replace_all === true ? cached.content.split(oldString).join(newString) : cached.content.replace(oldString, newString);
8951
+ } else if (t === "MultiEdit") {
8952
+ const meRes = estimateMultiEditApproval(cached.content, normalizedArgs.edits);
8953
+ if (!meRes.ok) {
8954
+ return { approve: false, reason: `MultiEdit ${meRes.reason}`, tool: t, path: filePath };
8955
+ }
8956
+ diffTokens = meRes.tokens;
8957
+ nextContent = meRes.nextContent;
8563
8958
  } else {
8564
8959
  const content = normalizedArgs.content;
8565
8960
  if (typeof content !== "string") {
@@ -8577,8 +8972,264 @@ function applyCesarSelfTurnApproval(tool, args, toolCtx, config) {
8577
8972
  return { approve: true, reason: `bounded ${t} on previously read file (${diffTokens} tokens)`, tool: t, path: filePath, diffTokens };
8578
8973
  }
8579
8974
 
8975
+ // src/generated/cesar/approval-diff.ts
8976
+ import { existsSync as existsSync11, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
8977
+ var APPROVAL_DIFF_MAX_LINES_PER_FILE = 8;
8978
+ var APPROVAL_DIFF_MAX_TOTAL_LINES = 24;
8979
+ var APPROVAL_DIFF_MAX_FILE_BYTES = 262144;
8980
+ var APPROVAL_DIFF_MAX_CONTENT_CHARS = 262144;
8981
+ function approvalToolIsFileMutating(tool) {
8982
+ const key = String(tool ?? "").toLowerCase();
8983
+ return key === "edit" || key === "write" || key === "agonedit" || key === "agonwrite" || key === "multiedit" || key === "agonmultiedit";
8984
+ }
8985
+ function isProbablyBinary(text) {
8986
+ const sample = text.slice(0, 8192);
8987
+ return sample.indexOf("\0") !== -1;
8988
+ }
8989
+ function normalizeCurlyQuotes(text) {
8990
+ return text.replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');
8991
+ }
8992
+ function countOccurrences2(haystack, needle) {
8993
+ if (!needle) return 0;
8994
+ let count = 0;
8995
+ let pos = 0;
8996
+ while (true) {
8997
+ pos = haystack.indexOf(needle, pos);
8998
+ if (pos === -1) break;
8999
+ count++;
9000
+ pos += needle.length;
9001
+ }
9002
+ return count;
9003
+ }
9004
+ function buildApprovalDiffPreview(tool, args) {
9005
+ const key = String(tool ?? "").toLowerCase();
9006
+ const filePath = typeof args?.file_path === "string" ? String(args.file_path) : "";
9007
+ if (!filePath) return null;
9008
+ const cwd = process.cwd();
9009
+ const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length).replace(/^[/\\]/, "") : filePath;
9010
+ let oldContent = "";
9011
+ let newContent = "";
9012
+ let status = "edited";
9013
+ if (key === "write" || key === "agonwrite") {
9014
+ if (typeof args?.content !== "string") return null;
9015
+ newContent = String(args.content);
9016
+ const exists = existsSync11(filePath);
9017
+ if (!exists) {
9018
+ status = "created";
9019
+ oldContent = "";
9020
+ } else {
9021
+ status = "edited";
9022
+ try {
9023
+ const st = statSync4(filePath);
9024
+ if (st.size > APPROVAL_DIFF_MAX_FILE_BYTES) {
9025
+ return { fallback: `file is ${Math.round(st.size / 1024)}KB \u2014 diff hidden` };
9026
+ }
9027
+ oldContent = readFileSync11(filePath, "utf-8");
9028
+ } catch {
9029
+ oldContent = "";
9030
+ }
9031
+ }
9032
+ if (newContent.length > APPROVAL_DIFF_MAX_CONTENT_CHARS) {
9033
+ return { fallback: `new content is ${Math.round(newContent.length / 1024)}KB \u2014 diff hidden` };
9034
+ }
9035
+ } else if (key === "edit" || key === "agonedit") {
9036
+ const oldStr = typeof args?.old_string === "string" ? String(args.old_string) : "";
9037
+ const newStr = typeof args?.new_string === "string" ? String(args.new_string) : "";
9038
+ const replaceAll = args?.replace_all === true;
9039
+ if (oldStr === "") return { fallback: "empty old_string \u2014 edit will fail" };
9040
+ if (!existsSync11(filePath)) return null;
9041
+ try {
9042
+ const st = statSync4(filePath);
9043
+ if (st.size > APPROVAL_DIFF_MAX_FILE_BYTES) {
9044
+ return { fallback: `file is ${Math.round(st.size / 1024)}KB \u2014 diff hidden` };
9045
+ }
9046
+ oldContent = readFileSync11(filePath, "utf-8");
9047
+ } catch {
9048
+ return null;
9049
+ }
9050
+ let searchStr = oldStr;
9051
+ let baseContent = oldContent;
9052
+ if (oldStr && !baseContent.includes(searchStr)) {
9053
+ const normContent = normalizeCurlyQuotes(baseContent);
9054
+ const normSearch = normalizeCurlyQuotes(oldStr);
9055
+ if (normContent.includes(normSearch)) {
9056
+ baseContent = normContent;
9057
+ searchStr = normSearch;
9058
+ } else {
9059
+ return null;
9060
+ }
9061
+ }
9062
+ if (searchStr && !replaceAll) {
9063
+ const occurrences = countOccurrences2(baseContent, searchStr);
9064
+ if (occurrences > 1) {
9065
+ return { fallback: `old_string matches ${occurrences} locations \u2014 edit will fail` };
9066
+ }
9067
+ }
9068
+ oldContent = baseContent;
9069
+ newContent = searchStr ? replaceAll ? baseContent.split(searchStr).join(newStr) : baseContent.replace(searchStr, newStr) : baseContent;
9070
+ } else if (key === "multiedit" || key === "agonmultiedit") {
9071
+ const edits = Array.isArray(args?.edits) ? args.edits : null;
9072
+ if (!edits || edits.length === 0) return null;
9073
+ if (!existsSync11(filePath)) return null;
9074
+ try {
9075
+ const st = statSync4(filePath);
9076
+ if (st.size > APPROVAL_DIFF_MAX_FILE_BYTES) {
9077
+ return { fallback: `file is ${Math.round(st.size / 1024)}KB \u2014 diff hidden` };
9078
+ }
9079
+ oldContent = readFileSync11(filePath, "utf-8");
9080
+ } catch {
9081
+ return null;
9082
+ }
9083
+ let working = oldContent;
9084
+ let failed = false;
9085
+ for (let i = 0; i < edits.length; i++) {
9086
+ const e = edits[i] ?? {};
9087
+ const oldStr2 = typeof e.old_string === "string" ? e.old_string : "";
9088
+ const newStr2 = typeof e.new_string === "string" ? e.new_string : "";
9089
+ const replaceAll2 = e.replace_all === true;
9090
+ if (oldStr2 === "") {
9091
+ failed = true;
9092
+ break;
9093
+ }
9094
+ let search2 = oldStr2;
9095
+ let base2 = working;
9096
+ if (!base2.includes(search2)) {
9097
+ const nb = normalizeCurlyQuotes(base2);
9098
+ const ns = normalizeCurlyQuotes(oldStr2);
9099
+ if (nb.includes(ns)) {
9100
+ base2 = nb;
9101
+ search2 = ns;
9102
+ } else {
9103
+ failed = true;
9104
+ break;
9105
+ }
9106
+ }
9107
+ if (!replaceAll2 && countOccurrences2(base2, search2) > 1) {
9108
+ failed = true;
9109
+ break;
9110
+ }
9111
+ working = replaceAll2 ? base2.split(search2).join(newStr2) : base2.replace(search2, newStr2);
9112
+ }
9113
+ if (failed) return { fallback: "one or more edits will not apply \u2014 see prompt" };
9114
+ status = "edited";
9115
+ newContent = working;
9116
+ } else {
9117
+ return null;
9118
+ }
9119
+ if (isProbablyBinary(oldContent) || isProbablyBinary(newContent)) {
9120
+ return { fallback: "binary file \u2014 diff hidden" };
9121
+ }
9122
+ if (oldContent === newContent) {
9123
+ return null;
9124
+ }
9125
+ const file = computeFileDiffPreview(
9126
+ filePath,
9127
+ relPath,
9128
+ status,
9129
+ oldContent,
9130
+ newContent,
9131
+ APPROVAL_DIFF_MAX_LINES_PER_FILE,
9132
+ APPROVAL_DIFF_MAX_TOTAL_LINES
9133
+ );
9134
+ if (!file) return null;
9135
+ return { files: [file], totalFiles: 1 };
9136
+ }
9137
+ function computeFileDiffPreview(path, relPath, status, oldContent, newContent, maxLines, maxTotal) {
9138
+ const oldAll = oldContent.length === 0 ? [] : oldContent.replace(/\n$/, "").split("\n");
9139
+ const newAll = newContent.length === 0 ? [] : newContent.replace(/\n$/, "").split("\n");
9140
+ const APPROVAL_DIFF_MAX_LCS_LINES = 1200;
9141
+ let pre = 0;
9142
+ const oldLenAll = oldAll.length;
9143
+ const newLenAll = newAll.length;
9144
+ while (pre < oldLenAll && pre < newLenAll && oldAll[pre] === newAll[pre]) pre++;
9145
+ let suf = 0;
9146
+ while (suf < oldLenAll - pre && suf < newLenAll - pre && oldAll[oldLenAll - 1 - suf] === newAll[newLenAll - 1 - suf]) suf++;
9147
+ const oldLines = oldAll.slice(pre, oldLenAll - suf);
9148
+ const newLines = newAll.slice(pre, newLenAll - suf);
9149
+ const n = oldLines.length;
9150
+ const m = newLines.length;
9151
+ if (n + m > APPROVAL_DIFF_MAX_LCS_LINES) {
9152
+ const addCount = m;
9153
+ const delCount = n;
9154
+ return {
9155
+ path,
9156
+ relPath,
9157
+ status,
9158
+ additions: addCount,
9159
+ deletions: delCount,
9160
+ lines: [`@@ +${addCount}/-${delCount} lines \u2014 diff too large to preview @@`],
9161
+ omitted: addCount + delCount
9162
+ };
9163
+ }
9164
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
9165
+ for (let i2 = n - 1; i2 >= 0; i2--) {
9166
+ for (let j2 = m - 1; j2 >= 0; j2--) {
9167
+ lcs[i2][j2] = oldLines[i2] === newLines[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
9168
+ }
9169
+ }
9170
+ const ops = [];
9171
+ let i = 0;
9172
+ let j = 0;
9173
+ while (i < n && j < m) {
9174
+ if (oldLines[i] === newLines[j]) {
9175
+ ops.push({ kind: " ", text: oldLines[i] });
9176
+ i++;
9177
+ j++;
9178
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
9179
+ ops.push({ kind: "-", text: oldLines[i] });
9180
+ i++;
9181
+ } else {
9182
+ ops.push({ kind: "+", text: newLines[j] });
9183
+ j++;
9184
+ }
9185
+ }
9186
+ while (i < n) {
9187
+ ops.push({ kind: "-", text: oldLines[i] });
9188
+ i++;
9189
+ }
9190
+ while (j < m) {
9191
+ ops.push({ kind: "+", text: newLines[j] });
9192
+ j++;
9193
+ }
9194
+ let additions = 0;
9195
+ let deletions = 0;
9196
+ for (const op of ops) {
9197
+ if (op.kind === "+") additions++;
9198
+ else if (op.kind === "-") deletions++;
9199
+ }
9200
+ if (additions === 0 && deletions === 0) return null;
9201
+ const perFileCap = Math.max(1, Math.min(maxLines, maxTotal));
9202
+ const visibleLines = [];
9203
+ let interesting = 0;
9204
+ for (const op of ops) {
9205
+ if (op.kind === " ") continue;
9206
+ interesting++;
9207
+ if (visibleLines.length < perFileCap) {
9208
+ visibleLines.push(`${op.kind}${truncateDiffLine(op.text, 120)}`);
9209
+ }
9210
+ }
9211
+ const omitted = Math.max(0, interesting - visibleLines.length);
9212
+ const headerLine = `@@ ${status === "created" ? "new file" : `-${deletions} +${additions}`} @@`;
9213
+ return {
9214
+ path,
9215
+ relPath,
9216
+ status,
9217
+ additions,
9218
+ deletions,
9219
+ lines: [headerLine, ...visibleLines],
9220
+ omitted
9221
+ };
9222
+ }
9223
+ function truncateDiffLine(line, max) {
9224
+ const text = String(line ?? "").replace(/\t/g, " ");
9225
+ if (text.length <= max) {
9226
+ return text;
9227
+ }
9228
+ return text.slice(0, Math.max(0, max - 1)) + "\u2026";
9229
+ }
9230
+
8580
9231
  // src/generated/cesar/tool-observability.ts
8581
- import { appendFileSync, mkdirSync as mkdirSync10, existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
9232
+ import { appendFileSync, mkdirSync as mkdirSync10, existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
8582
9233
  import { join as join11 } from "path";
8583
9234
  function createCesarTurnId() {
8584
9235
  return `cesar-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -8726,11 +9377,11 @@ function recordCesarConfidence(record) {
8726
9377
  }
8727
9378
  }
8728
9379
  function readJsonlRecords(filePath) {
8729
- if (!existsSync11(filePath)) {
9380
+ if (!existsSync12(filePath)) {
8730
9381
  return [];
8731
9382
  }
8732
9383
  const out = [];
8733
- const text = readFileSync11(filePath, "utf-8");
9384
+ const text = readFileSync12(filePath, "utf-8");
8734
9385
  for (const line of text.split("\n")) {
8735
9386
  const trimmed = line.trim();
8736
9387
  if (!trimmed) {
@@ -8957,8 +9608,15 @@ RULE 4b \u2014 MODE PURPOSE (don't mix them up \u2014 and don't always default t
8957
9608
  Your code will be auto-reviewed after implementation \u2014 write carefully the first time.
8958
9609
  RULE 4c \u2014 REVIEW: Use Review(target?, engine?, engines?) to delegate read-only code review. Default target is "uncommitted". Targets: "uncommitted", "branch:NAME", "commit:SHA". Specify engine ONLY when the user explicitly names one via "with <engine>"; if they name multiple reviewers, specify engines:["codex","claude"] or the named set and Agon will run them in parallel. Otherwise choose two diverse engines for high-stakes reviews or omit engine/engines and let Agon auto-select. If the user asks to review AND fix, do NOT stop at Review; call Pipeline(task, fitnessCmd?, engines?) so the findings are fixed immediately through Agon's agent harness.
8959
9610
  RULE 5 \u2014 WORKSPACE: Use Read for files. Use Grep for search. NEVER use cat/head/tail/grep via Bash. For shell commands use AgonBash (MCP tool), for edits use AgonEdit, for new files use AgonWrite. The user will see a Y/N/Always prompt for write operations. If they choose "Always", that command is auto-approved going forward. When the user says "commit", call AgonBash with the git commands. Don't say "I can't" or "I need permission" \u2014 call the tool and the permission system handles it.
9611
+ RULE 5b \u2014 DURABLE MEMORY: Call SaveMemory(memory, section) to record a fact that should survive into FUTURE sessions \u2014 and ONLY for that. section is one of Decisions / Constraints / Conventions / Session Notes. Use it when a real, durable fact is established: an architectural decision ("we chose session tokens over JWT"), a hard constraint ("Node 20 floor", "no top-level await"), or a project convention ("commits use conventional format"). Do NOT save transient/session state, the current TODO, things obvious from the code, or speculation \u2014 that is noise next session. One fact per call, no date (it is added automatically); the user confirms each memory. Saved memory comes back as a [PROJECT MEMORY] block at the top of your context in later sessions, so don't re-save what is already there.
8960
9612
  RULE 6 \u2014 AFTER DELEGATION: After calling Forge/Brainstorm/Tribunal/Campfire/Pipeline/Review/Agent, STOP. Do not continue responding. The orchestrator handles the rest. After calling Delegate, WAIT for the result \u2014 do NOT stop. Incorporate the delegated result into your response.
8961
9613
  RULE 7 \u2014 NO NARRATION: NEVER narrate your research process. Do not write "Reading the file...", "I'm checking...", "Let me look at...", "I've confirmed...". The user sees your text output \u2014 if you narrate exploration it looks like you have no clue. Instead: call tools SILENTLY, then speak ONLY when you have the answer or decision. Your visible output should be conclusions, answers, and actions \u2014 never a play-by-play of your investigation. If you need to read files or search code, call Read/Grep/Glob directly without announcing it.
9614
+ RULE 7b \u2014 LIVE TODO CHECKLIST (the sanctioned exception to RULE 7): For multi-step work \u2014 more than 2 distinct steps \u2014 pin a live checklist so the user can watch progress without you narrating it. Emit a [TODOS] block: one JSON array, each item {"id","text","state"} where state is pending|running|done|failed. It is INVISIBLE to the user (the runtime strips it and renders a pinned checklist instead) \u2014 never describe it in prose. Re-emit the WHOLE block (latest snapshot) whenever a step's state changes \u2014 typically right after a tool finishes \u2014 so items tick from running to done as you work. OPTIONAL: skip it for single-step or chat turns. Example for "refactor the parser and add a test":
9615
+ [TODOS]
9616
+ [{"id":"1","text":"Read the parser module","state":"done"},{"id":"2","text":"Refactor the parse loop","state":"running"},{"id":"3","text":"Add a unit test","state":"pending"}]
9617
+ [/TODOS]
9618
+ Keep texts short (\u22646 words). Do NOT use [TODOS] in plan mode \u2014 there ProposePlan owns the checklist.
9619
+ RULE 7c \u2014 INTENT PREAMBLE (the second sanctioned exception to RULE 7): Before multi-step work, you MAY open your response with ONE short intent line as the VERY FIRST line: [INTENT] <one line>. It is INVISIBLE prose to the user (the runtime strips it and renders a distinct dim line BEFORE your tool work) \u2014 never describe it. Only at the start of the response counts; keep it to one line, e.g. [INTENT] Tracing the dispatch path before changing timeouts. OPTIONAL: skip it for single-step or chat turns.
8962
9620
 
8963
9621
  RULE 8 \u2014 AUTONOMOUS PLANS: Plan mode is optional, not the default. Stay live unless staged execution is genuinely useful. Switch to planning when the task needs multiple dependent steps, expensive orchestration, resumability, explicit approval, or cost visibility. When you call ProposePlan, decide whether to set autoApprove=true. Set it ONLY when (a) the user clearly described a multi-stage workflow ("plan it, build it, review it"; "investigate then forge it"; "do the whole thing autonomously") AND (b) you have HIGH confidence in the steps after investigation (not before). The runtime applies a layered policy and may still ask the user \u2014 your autoApprove=true is permission, not a guarantee. Default to autoApprove=false (or omit it) whenever you are uncertain or when the plan touches mutating steps and the user did not explicitly invite autonomous execution. selfReview defaults to true for mutating plans \u2014 only set selfReview=false for purely advisory plans (brainstorm/tribunal/research only) where a code-review gate would have nothing to review.
8964
9622
  RULE 8b \u2014 AUTONOMOUS BUILD TOOLS (Goal / Conquer): These run a build to completion in the BACKGROUND. Goal(intent, queue?, gate?) drives a finite, machine-verifiable task QUEUE (forge \u2192 witness \u2192 review \u2192 commit per task on a goal/* branch). Conquer(task, gate, builder?, engines?) is for an OPEN-ENDED build you'd otherwise babysit: it drives an external builder CLI as the user (builder defaults to codex \u2014 pass builder:"codex" / "claude" / "agy") unattended until the gate passes, convening nero/tribunal/council on forks, then STOPS at a human merge gate (NEVER auto-merges). Fire EITHER ONLY when the user explicitly asks to build it unattended ("conquer this with codex", "build it autonomously") \u2014 NEVER on a vague request; both are long, real-spend, multi-hour runs. Conquer REQUIRES a discriminating gate (the done-spec, e.g. gate:"pnpm test"); if the user did not give one, ASK for the command that proves the build is done before calling. After calling either, STOP and wait \u2014 the background job and the merge gate handle the rest.
@@ -8976,7 +9634,12 @@ RULE 10 \u2014 TURN CLOSURE: End every turn with one clear closing line so the u
8976
9634
 
8977
9635
  FORBIDDEN closure shapes: "Standing by \u2014 awaiting your call on: 1. Commit? 2. Tests? 3. Both?" \u2014 that reads as still-thinking, not a closure. "Or move on to something else." trailing line \u2014 that's filler. A menu of next-steps you could simply do yourself \u2014 that is not a genuine fork. Stopping after only a recap with no done/asking/fork line \u2014 forbidden, always.
8978
9636
 
8979
- CONFIDENCE REFRESH: If your confidence changed materially during the turn (e.g. started at 40% "need to verify" and verified successfully \u2192 real confidence is now 95%), call ReportConfidence again with the new number BEFORE the closing line. Stale initial confidence in the recap misleads the user about what state you're in.`;
9637
+ LEAD WITH FINDINGS: Your FIRST sentence answers what happened or what you found \u2014 the conclusion, not the preamble. Supporting detail comes after. Use prose for a simple answer; reach for bullets only when the content is genuinely a list. No filler openers ("Great question", "Let me explain", "Sure!"). Don't bury the lede under a recap of what you did.
9638
+ GOOD: "The leak is in brain.kern \u2014 the drain timer never clears on early return. Added the cleanup at line 793; typecheck green."
9639
+ BAD: "Great question! So I went and looked into this. There are a few things going on here. First, let me walk through the architecture. The session has a drain timer..."
9640
+
9641
+ CONFIDENCE REFRESH: If your confidence changed materially during the turn (e.g. started at 40% "need to verify" and verified successfully \u2192 real confidence is now 95%), call ReportConfidence again with the new number BEFORE the closing line. Stale initial confidence in the recap misleads the user about what state you're in.
9642
+ If you END the turn still below ~85%, write CONFIDENCE: NN% once near your closing line \u2014 the UI uses that exact shape to offer the user an escalation (nero/tribunal).`;
8980
9643
  function buildCesarSystemPrompt(ctx) {
8981
9644
  const config = ctx.config;
8982
9645
  const cesarCwd = resolveWorkingDir();
@@ -9008,6 +9671,18 @@ MODES vs ENGINES: the names above are ENGINES \u2014 runnable backends you deleg
9008
9671
  systemParts.push(`## OPERATING MODE
9009
9672
  Exploration mode is ON. Stay read-only: inspect files, search, and use read-only shell commands only. Do not call Edit or Write. Do not run non-read-only Bash commands.`);
9010
9673
  }
9674
+ const commitCoAuthor = (config.commitCoAuthor || "").replace(/[`\r\n]+/g, " ").replace(/\s+/g, " ").trim();
9675
+ if (commitCoAuthor) {
9676
+ systemParts.push(`COMMIT ATTRIBUTION: When you create a git commit yourself via Bash, end the commit message with a trailer line \`Co-Authored-By: ${commitCoAuthor}\`. This attribution is configurable (config.commitCoAuthor) and is ON by default.
9677
+ - If the user asks to REMOVE/DISABLE the commit logo/attribution, first ask whether to disable it for THIS PROJECT or MACHINE-WIDE, then disable it: machine-wide, run \`agon config set commitCoAuthor ""\` (writes ~/.agon/config.json); per-project, safely edit ./.agon.json with your file tools (read the JSON if it exists, set/merge the key \`"commitCoAuthor": ""\`, write it back \u2014 never clobber other keys). Confirm once done.
9678
+ - If the user asks to CHANGE the identity instead, set the new value the same way (\`agon config set commitCoAuthor "<value>"\` machine-wide, or merge it into ./.agon.json per-project).`);
9679
+ }
9680
+ try {
9681
+ const projectMemory = buildProjectMemoryBlock(cesarCwd);
9682
+ if (projectMemory) systemParts.push(projectMemory);
9683
+ } catch (err) {
9684
+ console.warn(`[agon] project memory block skipped: ${err instanceof Error ? err.message : String(err)}`);
9685
+ }
9011
9686
  if (ctx.cesarMemory) {
9012
9687
  const memoryCtx = ctx.cesarMemory.toPromptContext();
9013
9688
  if (memoryCtx) systemParts.push(memoryCtx);
@@ -9034,14 +9709,14 @@ ${fragments.join("\n")}`);
9034
9709
  } else {
9035
9710
  const stats = tracker.getStats();
9036
9711
  let budgetWarning = "";
9037
- if (stats.totalCostUsd > 0.5) {
9712
+ if (stats.meteredCostUsd > 0.5) {
9038
9713
  budgetWarning = `
9039
9714
 
9040
- URGENT: Planning has spent $${stats.totalCostUsd.toFixed(2)}. Stop investigating and decide NOW \u2014 call ProposePlan or ExitPlanMode.`;
9041
- } else if (stats.totalCostUsd > 0.25) {
9715
+ URGENT: Planning has spent $${stats.meteredCostUsd.toFixed(2)}. Stop investigating and decide NOW \u2014 call ProposePlan or ExitPlanMode.`;
9716
+ } else if (stats.meteredCostUsd > 0.25) {
9042
9717
  budgetWarning = `
9043
9718
 
9044
- WARNING: Planning has spent $${stats.totalCostUsd.toFixed(2)}. Wrap up and decide \u2014 call ProposePlan or ExitPlanMode.`;
9719
+ WARNING: Planning has spent $${stats.meteredCostUsd.toFixed(2)}. Wrap up and decide \u2014 call ProposePlan or ExitPlanMode.`;
9045
9720
  }
9046
9721
  systemParts.push(`RULE 9 \u2014 PLAN MODE: You are in PLAN MODE because the user asked for planning, so proposing a plan is usually the right call here. But you are NEVER trapped and never forced: if you decide a plan is not the right approach \u2014 the task is simple enough to do live, or planning is blocking progress \u2014 call ExitPlanMode with a one-line reason and work live instead. You decide.
9047
9722
 
@@ -9147,6 +9822,16 @@ function capSnapshotMessageContent(content) {
9147
9822
  return `${content.slice(0, CESAR_SNAPSHOT_MSG_CHAR_CAP)}
9148
9823
  \u2026 [${content.length - CESAR_SNAPSHOT_MSG_CHAR_CAP} chars truncated for Cesar context]`;
9149
9824
  }
9825
+ function renderToolPermissionCommand(tool, args) {
9826
+ const a = args && typeof args === "object" ? args : {};
9827
+ if (tool === "SaveMemory") {
9828
+ return `[${String(a.section ?? "")}] ${String(a.memory ?? "")}`.trim();
9829
+ }
9830
+ if (tool === "MultiEdit") {
9831
+ return JSON.stringify(args ?? {});
9832
+ }
9833
+ return String(a.command ?? a.file_path ?? JSON.stringify(args ?? {}));
9834
+ }
9150
9835
  function buildCesarConversationSnapshot(session, chatSession) {
9151
9836
  const directHistory = session?.getMessageHistory?.() ?? [];
9152
9837
  if (directHistory.length > 0) {
@@ -9197,12 +9882,20 @@ function buildOnToolCall(ctx, toolRegistry, config) {
9197
9882
  allowedCommands: config.allowedCommands ?? [],
9198
9883
  toolPermissions: config.toolPermissions ?? {},
9199
9884
  sessionAllowList: getSessionAllowList(),
9200
- source: "orchestrator"
9885
+ source: "orchestrator",
9886
+ // CC-parity allow/deny rules reach the API tool-execution path here:
9887
+ // executeToolCall → handler.checkPermission consults ctx.permissionRules
9888
+ // (deny-first, before any mode-based auto-allow). Without this, a
9889
+ // Bash(rm:*) deny rule never fires for API engines under 'smart' mode.
9890
+ permissionRules: parsePermissionRuleSet(config.permissions),
9891
+ // CC-parity PreToolUse/PostToolUse hooks reach the API tool-execution
9892
+ // path here: executeToolCall fires them around handler.execute.
9893
+ toolHooks: parseToolHooks(config.hooks)
9201
9894
  };
9202
9895
  return async (name, args, callId) => {
9203
9896
  const activePlan = ctx.activePlan;
9204
9897
  if (activePlan && ["planning", "awaiting_approval"].includes(activePlan.state)) {
9205
- const BLOCKED_IN_PLAN = ["Forge", "Pipeline", "Agent", "Goal", "Edit", "Write"];
9898
+ const BLOCKED_IN_PLAN = ["Forge", "Pipeline", "Agent", "Goal", "Edit", "Write", "MultiEdit"];
9206
9899
  if (BLOCKED_IN_PLAN.includes(name)) {
9207
9900
  return `[BLOCKED] Tool "${name}" is not available in plan mode. Use ProposePlan to propose your execution strategy.`;
9208
9901
  }
@@ -9269,7 +9962,7 @@ ${cleaned}`;
9269
9962
  }
9270
9963
  }
9271
9964
  if (name === "ExitPlanMode") {
9272
- const { handleExitPlanMode } = await import("./plan-mode-5IQ2SKIS.js");
9965
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
9273
9966
  return "[DELEGATION_BREAK] " + handleExitPlanMode(String(args.reason ?? ""), ctx.cesar?.planDispatch ?? null, ctx);
9274
9967
  }
9275
9968
  if (name === "ProposePlan") {
@@ -9292,7 +9985,7 @@ ${cleaned}`;
9292
9985
  }
9293
9986
  }
9294
9987
  }
9295
- const { handleProposePlan } = await import("./plan-mode-5IQ2SKIS.js");
9988
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
9296
9989
  const dispatch = ctx.cesar.planDispatch;
9297
9990
  if (!dispatch) {
9298
9991
  return "[PLAN_ERROR] Internal plan display dispatch unavailable. Retry the plan request so Agon can render the approval panel.";
@@ -9358,8 +10051,15 @@ ${cleaned}`;
9358
10051
  return new Promise((resolve4) => {
9359
10052
  const d = ctx.cesar.lastDispatch;
9360
10053
  if (d) {
9361
- const cmd = args.command ?? args.file_path ?? JSON.stringify(args);
9362
- d({ type: "permission-ask", tool, command: cmd, reason: message, resolve: resolve4 });
10054
+ const cmd = renderToolPermissionCommand(tool, args);
10055
+ let permDiffPreview = void 0;
10056
+ if (approvalToolIsFileMutating(tool)) {
10057
+ try {
10058
+ permDiffPreview = buildApprovalDiffPreview(String(tool), args);
10059
+ } catch {
10060
+ }
10061
+ }
10062
+ d({ type: "permission-ask", tool, command: cmd, reason: message, diffPreview: permDiffPreview && Array.isArray(permDiffPreview.files) ? permDiffPreview : void 0, fallbackNote: permDiffPreview && typeof permDiffPreview.fallback === "string" ? permDiffPreview.fallback : void 0, resolve: resolve4 });
9363
10063
  } else {
9364
10064
  resolve4(true);
9365
10065
  }
@@ -9367,11 +10067,15 @@ ${cleaned}`;
9367
10067
  }
9368
10068
  );
9369
10069
  let output = result.result.ok ? result.result.content : result.result.error ?? "Tool execution failed";
9370
- if (!result.result.ok) {
10070
+ const isPermissionDenial = !result.result.ok && typeof result.result.error === "string" && (result.result.error.includes(PERMISSION_DENIED_MESSAGE) || result.result.error.includes("User denied permission") || result.result.error.startsWith("DENIED:"));
10071
+ if (!result.result.ok && !isPermissionDenial) {
9371
10072
  const diag = buildToolErrorDiagnostic(name, args, result.result.error);
9372
10073
  const retryKey = `${name}:${JSON.stringify(args)}`;
9373
10074
  const used = nativeToolErrorRetries.get(retryKey) ?? 0;
9374
- if (used <= 0) {
10075
+ const isPermissionDenial2 = typeof result.result.error === "string" && result.result.error.includes(PERMISSION_DENIED_MESSAGE);
10076
+ if (isPermissionDenial2) {
10077
+ output = result.result.error;
10078
+ } else if (used <= 0) {
9375
10079
  nativeToolErrorRetries.set(retryKey, 1);
9376
10080
  output = `[RETRYABLE_TOOL_ERROR] ${diag}
9377
10081
  Retry this ${name} call ONCE with corrected input that matches the tool's schema. Do not narrate before retrying.`;
@@ -9384,7 +10088,7 @@ Repair retry already used for this exact ${name} input in this turn. Stop retryi
9384
10088
  }
9385
10089
  if (CACHEABLE_TOOLS.has(name)) {
9386
10090
  toolResultCache.set(cacheKey, output);
9387
- } else if (["Edit", "Write", "Bash"].includes(name)) {
10091
+ } else if (["Edit", "Write", "MultiEdit", "Bash"].includes(name)) {
9388
10092
  toolResultCache.clear();
9389
10093
  ctx.cesar.blockedOnConfidence = null;
9390
10094
  }
@@ -9408,9 +10112,12 @@ function buildOnApproval(ctx, engineId) {
9408
10112
  const perms = cfg.toolPermissions ?? {};
9409
10113
  const allowed = cfg.allowedCommands ?? [];
9410
10114
  const mode = cfg.permissionMode ?? "ask";
9411
- const toolMap = { shell: "Bash", bash: "Bash", edit: "Edit", write: "Write", read: "Read", grep: "Grep", glob: "Glob" };
10115
+ const toolMap = { shell: "Bash", bash: "Bash", edit: "Edit", write: "Write", multiedit: "MultiEdit", read: "Read", grep: "Grep", glob: "Glob" };
9412
10116
  const agonTool = toolMap[tool.toLowerCase()] ?? tool;
9413
10117
  const perm = perms[agonTool];
10118
+ const ruleSet = parsePermissionRuleSet(cfg.permissions);
10119
+ const ruleArg = agonTool === "Bash" ? command : String(approvalArgsFromCommand(agonTool, command)?.file_path ?? "");
10120
+ const ruleDecision = evaluateToolRules(agonTool, ruleArg, resolveWorkingDir(), ruleSet);
9414
10121
  const turnId = ctx.cesar?.turnId;
9415
10122
  const cwd = resolveWorkingDir();
9416
10123
  const logApproval = (decision, source, reason, args) => {
@@ -9445,7 +10152,7 @@ function buildOnApproval(ctx, engineId) {
9445
10152
  }
9446
10153
  };
9447
10154
  if (ctx.explorationMode) {
9448
- const WRITE_TOOLS = ["Edit", "Write", "Bash"];
10155
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit", "Bash"];
9449
10156
  if (WRITE_TOOLS.includes(agonTool)) {
9450
10157
  logApproval("blocked", "policy.exploration", "exploration mode is read-only");
9451
10158
  return "BLOCKED: Exploration mode is read-only. Use Read, Grep, Glob tools only. Do not narrate around this. Either keep investigating, or wait for the user to disable exploration mode before retrying the same tool.";
@@ -9461,7 +10168,7 @@ function buildOnApproval(ctx, engineId) {
9461
10168
  logApproval("blocked", "policy.plan-mode", "plan mode blocks mutating Bash");
9462
10169
  return "BLOCKED: Plan mode \u2014 mutating Bash is not allowed before approval. Use Read/Grep/Glob or read-only Bash for investigation, call ProposePlan, then wait for the user to type go/yes before retrying this command.";
9463
10170
  }
9464
- const WRITE_TOOLS = ["Edit", "Write"];
10171
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit"];
9465
10172
  if (WRITE_TOOLS.includes(agonTool)) {
9466
10173
  logApproval("blocked", "policy.plan-mode", "plan mode blocks file writes");
9467
10174
  return "BLOCKED: Plan mode \u2014 no code changes allowed. Call ProposePlan with the execution plan now, then wait for approval before retrying the same tool. Do not narrate instead of acting.";
@@ -9487,7 +10194,7 @@ function buildOnApproval(ctx, engineId) {
9487
10194
  }
9488
10195
  }
9489
10196
  if (!ctx.cesar.confidenceSatisfied) {
9490
- const WRITE_TOOLS = ["Edit", "Write"];
10197
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit"];
9491
10198
  if (WRITE_TOOLS.includes(agonTool)) {
9492
10199
  const blocks = (ctx.cesar.confidenceBlockCount ?? 0) + 1;
9493
10200
  ctx.cesar.confidenceBlockCount = blocks;
@@ -9500,11 +10207,19 @@ function buildOnApproval(ctx, engineId) {
9500
10207
  ctx.cesar.blockedOnConfidence = null;
9501
10208
  }
9502
10209
  }
10210
+ if (ruleDecision === "deny") {
10211
+ logApproval("denied", "settings.permissions", `${agonTool} denied by permissions rule`);
10212
+ return `DENIED: ${agonTool}${agonTool === "Bash" ? ` (${command})` : ""} is blocked by a deny rule in .agon.json permissions. Do not retry this \u2014 choose a different approach or ask the user to amend the rule.`;
10213
+ }
9503
10214
  if (perm === "deny" || mode === "deny-all") {
9504
10215
  logApproval("denied", perm === "deny" ? "settings.toolPermissions" : "settings.permissionMode", perm === "deny" ? `${agonTool} denied in settings` : "permissionMode=deny-all");
9505
10216
  return false;
9506
10217
  }
9507
- if (agonTool === "Edit" || agonTool === "Write") {
10218
+ if (ruleDecision === "allow") {
10219
+ logApproval("approved", "settings.permissions", `${agonTool} allowed by permissions rule`);
10220
+ return true;
10221
+ }
10222
+ if (agonTool === "Edit" || agonTool === "Write" || agonTool === "MultiEdit") {
9508
10223
  const approvalCwd = resolveWorkingDir();
9509
10224
  const approvalCache = getProjectFileStateCache(approvalCwd);
9510
10225
  const approvalArgs = approvalArgsFromCommand(agonTool, command);
@@ -9552,7 +10267,14 @@ function buildOnApproval(ctx, engineId) {
9552
10267
  const dispatch = ctx.cesar.lastDispatch;
9553
10268
  if (dispatch) {
9554
10269
  logApproval("prompted", "user-prompt", `Cesar (${engineId}) wants to execute`);
9555
- dispatch({ type: "permission-ask", tool: agonTool, command, reason: `Cesar (${engineId}) wants to execute`, resolve: (approved) => {
10270
+ let permDiffPreview = void 0;
10271
+ if (approvalToolIsFileMutating(agonTool)) {
10272
+ try {
10273
+ permDiffPreview = buildApprovalDiffPreview(agonTool, approvalArgsFromCommand(agonTool, command));
10274
+ } catch {
10275
+ }
10276
+ }
10277
+ dispatch({ type: "permission-ask", tool: agonTool, command, reason: `Cesar (${engineId}) wants to execute`, diffPreview: permDiffPreview && Array.isArray(permDiffPreview.files) ? permDiffPreview : void 0, fallbackNote: permDiffPreview && typeof permDiffPreview.fallback === "string" ? permDiffPreview.fallback : void 0, resolve: (approved) => {
9556
10278
  const wasApproved = typeof approved === "string" ? approved === "y" || approved === "a" : !!approved;
9557
10279
  logApproval(wasApproved ? "approved" : "denied", "user-prompt", wasApproved ? "user approved" : "user denied");
9558
10280
  resolve4(wasApproved);
@@ -9596,7 +10318,7 @@ function loadCesarMcpServers(config, cwd) {
9596
10318
  const resolvedPath = isAbsolute2(rawPath) ? rawPath : resolve3(cwd, rawPath);
9597
10319
  let parsed;
9598
10320
  try {
9599
- parsed = JSON.parse(readFileSync12(resolvedPath, "utf-8"));
10321
+ parsed = JSON.parse(readFileSync13(resolvedPath, "utf-8"));
9600
10322
  } catch (err) {
9601
10323
  throw new Error(`Failed to load Cesar MCP config at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`);
9602
10324
  }
@@ -9620,7 +10342,7 @@ function mcpConfigFingerprint(config) {
9620
10342
  if (enabled && configPath) {
9621
10343
  try {
9622
10344
  const resolvedPath = isAbsolute2(configPath) ? configPath : resolve3(resolveWorkingDir(), configPath);
9623
- mtime = String(statSync4(resolvedPath).mtimeMs);
10345
+ mtime = String(statSync5(resolvedPath).mtimeMs);
9624
10346
  } catch {
9625
10347
  }
9626
10348
  }
@@ -9631,17 +10353,17 @@ function resolveAgonMcpServerPath(fromUrl) {
9631
10353
  const raw = fromUrl ?? import.meta.url;
9632
10354
  const url = raw.startsWith("file:") ? raw : pathToFileURL(raw).href;
9633
10355
  const bundledSibling = join12(dirname5(fileURLToPath(url)), "mcp", "index.js");
9634
- if (existsSync12(bundledSibling)) return bundledSibling;
10356
+ if (existsSync13(bundledSibling)) return bundledSibling;
9635
10357
  try {
9636
10358
  const req = createRequire(url);
9637
10359
  const resolved = req.resolve("@kernlang/agon-mcp");
9638
- if (existsSync12(resolved)) return resolved;
10360
+ if (existsSync13(resolved)) return resolved;
9639
10361
  } catch {
9640
10362
  }
9641
10363
  let dir = dirname5(fileURLToPath(url));
9642
10364
  for (let i = 0; i < 12; i++) {
9643
10365
  const cand = join12(dir, "packages", "mcp", "dist", "index.js");
9644
- if (existsSync12(cand)) return cand;
10366
+ if (existsSync13(cand)) return cand;
9645
10367
  const parent = dirname5(dir);
9646
10368
  if (parent === dir) break;
9647
10369
  dir = parent;
@@ -9760,7 +10482,7 @@ async function ensureCesarSession(ctx) {
9760
10482
  mkdirSync11(signalDir, { recursive: true });
9761
10483
  const sessionSignalId = `cesar-${Date.now()}`;
9762
10484
  const mcpServerPath = resolveAgonMcpServerPath();
9763
- if (!existsSync12(mcpServerPath)) {
10485
+ if (!existsSync13(mcpServerPath)) {
9764
10486
  console.error(`[agon] cesar: agon-orchestration MCP server not found at ${mcpServerPath} \u2014 orchestration tools (Forge/Tribunal/AgonBash/DeliverAnswer) will be UNAVAILABLE and Cesar will fall back to slow scraping.`);
9765
10487
  }
9766
10488
  const mcpEnv = { AGON_SIGNAL_DIR: signalDir, AGON_SESSION_ID: sessionSignalId };
@@ -9811,9 +10533,177 @@ async function ensureCesarSession(ctx) {
9811
10533
  await session.start();
9812
10534
  ctx.setCesarSession(session);
9813
10535
  ctx.cesar.mcpFingerprint = currentMcpFp;
10536
+ ctx.cesar.budgetWarned = false;
9814
10537
  return session;
9815
10538
  }
9816
10539
 
10540
+ // src/generated/cesar/context-budget.ts
10541
+ var TOOL_PROMPT_OVERHEAD_TOKENS = 2500;
10542
+ function estimateBrainTokens(ctx, session, backend, budget, pendingInput) {
10543
+ if (backend === "api" && session && typeof session.getContextUsage === "function") {
10544
+ try {
10545
+ const live = session.getContextUsage();
10546
+ if (live && Number.isFinite(live.tokens) && live.tokens > 0 && live.source !== "estimate") {
10547
+ const pendingTokens = pendingInput ? estimateSessionTokens({ messageHistory: [{ role: "user", content: pendingInput }] }, budget) : 0;
10548
+ return live.tokens + pendingTokens;
10549
+ }
10550
+ } catch {
10551
+ }
10552
+ }
10553
+ if (backend === "api" && session && typeof session.getMessageHistory === "function") {
10554
+ try {
10555
+ const history = session.getMessageHistory() ?? [];
10556
+ if (Array.isArray(history) && history.length > 0) {
10557
+ const withPending = pendingInput ? [...history, { role: "user", content: pendingInput }] : history;
10558
+ return estimateSessionTokens({ messageHistory: withPending }, budget);
10559
+ }
10560
+ } catch {
10561
+ }
10562
+ }
10563
+ let systemPromptChars = 0;
10564
+ try {
10565
+ systemPromptChars = (buildCesarSystemPrompt(ctx) ?? "").length;
10566
+ } catch {
10567
+ }
10568
+ const chat = ctx.chatSession;
10569
+ let continuityChars = 0;
10570
+ let userTurnsChars = 0;
10571
+ let toolResultsChars = 0;
10572
+ if (chat) {
10573
+ continuityChars += (chat.summary ?? "").length;
10574
+ const msgs = Array.isArray(chat.messages) ? chat.messages : [];
10575
+ for (const m of msgs) {
10576
+ const len = typeof m?.content === "string" ? m.content.length : 0;
10577
+ if (m?.role === "user") userTurnsChars += len;
10578
+ else toolResultsChars += len;
10579
+ }
10580
+ }
10581
+ try {
10582
+ const digest = ctx.cesarMemory?.toPromptContext?.();
10583
+ if (typeof digest === "string") continuityChars += digest.length;
10584
+ } catch {
10585
+ }
10586
+ const base = estimateSessionTokens({
10587
+ pty: {
10588
+ systemPromptChars,
10589
+ userTurnsChars,
10590
+ toolResultsChars,
10591
+ continuityChars,
10592
+ pendingInputChars: (pendingInput ?? "").length
10593
+ }
10594
+ }, budget);
10595
+ return base + TOOL_PROMPT_OVERHEAD_TOKENS;
10596
+ }
10597
+ async function enforceContextBudget(ctx, session, engine, backend, dispatch, pendingInput) {
10598
+ const budget = engine?.sessionBudget;
10599
+ if (!budget || typeof budget.contextWindow !== "number" || budget.contextWindow <= 0) {
10600
+ return { proceed: true, compacted: false };
10601
+ }
10602
+ let estimated = 0;
10603
+ try {
10604
+ estimated = estimateBrainTokens(ctx, session, backend, budget, pendingInput);
10605
+ } catch {
10606
+ return { proceed: true, compacted: false };
10607
+ }
10608
+ const engineId = engine && typeof engine.id === "string" ? engine.id : "this engine";
10609
+ const check = checkSessionBudget(estimated, budget);
10610
+ const pct = budgetRatioPct(check.ratio);
10611
+ const autoMode = ctx.autoModeQueued === true || ctx.cesar?.autoModeQueued === true;
10612
+ const doCompactReboot = () => {
10613
+ let folded = false;
10614
+ try {
10615
+ if (ctx.chatSession) folded = updateChatSummary(ctx.chatSession);
10616
+ } catch {
10617
+ }
10618
+ try {
10619
+ session?.close?.();
10620
+ } catch {
10621
+ }
10622
+ try {
10623
+ ctx.setCesarSession(null);
10624
+ } catch {
10625
+ }
10626
+ try {
10627
+ ctx.cesarMemory?.clearSession?.();
10628
+ } catch {
10629
+ }
10630
+ return folded;
10631
+ };
10632
+ const live = session && typeof session.getContextUsage === "function" ? session.getContextUsage() : null;
10633
+ const canCompactInPlace = !!(backend === "api" && session && typeof session.compact === "function" && live && Number(live.limit) > 0 && live.source !== "estimate");
10634
+ if (canCompactInPlace) {
10635
+ const limit = Number(live.limit);
10636
+ const warnLimit = Math.floor(limit * 0.7);
10637
+ const softLimit = Math.max(warnLimit + 1, Number.isFinite(Number(live.softLimit)) && Number(live.softLimit) > 0 ? Number(live.softLimit) : Math.floor(limit * 0.85));
10638
+ const hardLimit = Math.floor(limit * 0.95);
10639
+ const pendingTokens = pendingInput ? estimateSessionTokens({ messageHistory: [{ role: "user", content: pendingInput }] }, budget) : 0;
10640
+ const projected = Number(live.tokens) + pendingTokens;
10641
+ const pctOf = (n) => Math.max(0, Math.round(n / limit * 100));
10642
+ if (projected < warnLimit) return { proceed: true, compacted: false };
10643
+ if (projected < softLimit) {
10644
+ if (!autoMode && ctx.cesar && !ctx.cesar.budgetWarned) {
10645
+ ctx.cesar.budgetWarned = true;
10646
+ dispatch({ type: "info", message: `Context at ${pctOf(projected)}% of ${engineId}'s window \u2014 Cesar will auto-compact in place around ${pctOf(softLimit)}%. Run /compact to fold older turns now.` });
10647
+ }
10648
+ return { proceed: true, compacted: false };
10649
+ }
10650
+ let res = null;
10651
+ try {
10652
+ res = await session.compact();
10653
+ } catch {
10654
+ res = null;
10655
+ }
10656
+ if (res && res.ok) {
10657
+ dispatch({ type: "info", message: `Compacted context in place: ${pctOf(res.beforeTokens)}% \u2192 ${pctOf(res.afterTokens)}% of ${engineId}'s window (${res.method === "llm" ? "older turns summarized by the engine" : "older turns folded into a structured summary"}). The session continues.` });
10658
+ dispatch({ type: "context-usage", pct: pctOf(res.afterTokens), used: res.afterTokens, limit, compacted: 1, cached: 0, source: "projected" });
10659
+ if (res.afterTokens + pendingTokens < hardLimit) {
10660
+ return { proceed: true, compacted: true };
10661
+ }
10662
+ }
10663
+ const foldedIp = doCompactReboot();
10664
+ let postEstimateIp = projected;
10665
+ try {
10666
+ postEstimateIp = estimateBrainTokens(ctx, null, backend, budget, pendingInput);
10667
+ } catch {
10668
+ }
10669
+ const postCheckIp = checkSessionBudget(postEstimateIp, budget);
10670
+ if (postCheckIp.level !== "hard-stop") {
10671
+ dispatch({ type: "info", message: `In-place compaction couldn't free enough at ${pctOf(projected)}% \u2014 auto-compacted Cesar context${foldedIp ? " (folded older turns into a summary)" : ""} and rebooted the brain with fresh context. The transcript is preserved.` });
10672
+ return { proceed: true, compacted: true };
10673
+ }
10674
+ dispatch({ type: "error", message: `Cesar context is still at ${budgetRatioPct(postCheckIp.ratio)}% of ${engineId}'s window after compaction \u2014 too full to safely send this turn. Trim the message, or run /clear to reset the session, then resend.` });
10675
+ return { proceed: false, compacted: true };
10676
+ }
10677
+ if (check.level === "ok") {
10678
+ return { proceed: true, compacted: false };
10679
+ }
10680
+ if (check.level === "warn") {
10681
+ if (!autoMode && ctx.cesar && !ctx.cesar.budgetWarned) {
10682
+ ctx.cesar.budgetWarned = true;
10683
+ dispatch({ type: "info", message: `Context at ${pct}% of ${engineId}'s window \u2014 Cesar will auto-compact soon. Run /compact now to fold older turns, or /clear to reset.` });
10684
+ }
10685
+ return { proceed: true, compacted: false };
10686
+ }
10687
+ if (check.level === "compact") {
10688
+ const folded = doCompactReboot();
10689
+ dispatch({ type: "info", message: `Context reached ${pct}% \u2014 auto-compacted Cesar context${folded ? " (folded older turns into a summary)" : ""} and rebooted the brain with fresh context. The transcript is preserved.` });
10690
+ return { proceed: true, compacted: true };
10691
+ }
10692
+ const foldedHs = doCompactReboot();
10693
+ let postEstimate = estimated;
10694
+ try {
10695
+ postEstimate = estimateBrainTokens(ctx, null, backend, budget, pendingInput);
10696
+ } catch {
10697
+ }
10698
+ const postCheck = checkSessionBudget(postEstimate, budget);
10699
+ if (postCheck.level !== "hard-stop") {
10700
+ dispatch({ type: "info", message: `Context hit ${pct}% \u2014 auto-compacted Cesar context${foldedHs ? " (folded older turns into a summary)" : ""} and rebooted the brain (now ~${budgetRatioPct(postCheck.ratio)}%). The transcript is preserved.` });
10701
+ return { proceed: true, compacted: true };
10702
+ }
10703
+ dispatch({ type: "error", message: `Cesar context is still at ${budgetRatioPct(postCheck.ratio)}% of ${engineId}'s window after auto-compaction \u2014 too full to safely send this turn. Trim the message, or run /clear to reset the session, then resend.` });
10704
+ return { proceed: false, compacted: true };
10705
+ }
10706
+
9817
10707
  // src/generated/cesar/escalation.ts
9818
10708
  import { join as join13 } from "path";
9819
10709
  import { mkdirSync as mkdirSync12 } from "fs";
@@ -9883,18 +10773,94 @@ async function promptDelegation(action, dispatch, hardened, tribunalMode, team)
9883
10773
  return { approved: answer === "y" };
9884
10774
  }
9885
10775
 
10776
+ // src/generated/cesar/steering.ts
10777
+ var _queue = [];
10778
+ var _activeTurnId = { value: null };
10779
+ var _listeners = [];
10780
+ function _notify() {
10781
+ const active = _activeTurnId.value;
10782
+ let n = 0;
10783
+ if (active) {
10784
+ for (const entry of _queue) if (entry.turnId === active) n++;
10785
+ }
10786
+ for (const cb of _listeners) {
10787
+ try {
10788
+ cb(n);
10789
+ } catch {
10790
+ }
10791
+ }
10792
+ }
10793
+ function onSteeringChange(cb) {
10794
+ _listeners.push(cb);
10795
+ return () => {
10796
+ const i = _listeners.indexOf(cb);
10797
+ if (i >= 0) _listeners.splice(i, 1);
10798
+ };
10799
+ }
10800
+ function markSteeringTurn(turnId) {
10801
+ _activeTurnId.value = turnId;
10802
+ _queue.length = 0;
10803
+ _notify();
10804
+ }
10805
+ function pushSteering(input, images) {
10806
+ const active = _activeTurnId.value;
10807
+ if (!active) {
10808
+ return false;
10809
+ }
10810
+ _queue.push({ turnId: active, input, images });
10811
+ _notify();
10812
+ return true;
10813
+ }
10814
+ function drainSteering(turnId) {
10815
+ const mine = [];
10816
+ const rest = [];
10817
+ for (const entry of _queue) {
10818
+ if (entry.turnId === turnId) mine.push({ input: entry.input, images: entry.images });
10819
+ else rest.push(entry);
10820
+ }
10821
+ _queue.length = 0;
10822
+ for (const entry of rest) _queue.push(entry);
10823
+ _notify();
10824
+ return mine;
10825
+ }
10826
+ function peekSteeringCount() {
10827
+ const active = _activeTurnId.value;
10828
+ if (!active) return 0;
10829
+ let n = 0;
10830
+ for (const entry of _queue) if (entry.turnId === active) n++;
10831
+ return n;
10832
+ }
10833
+ function releaseSteeringTurn(turnId) {
10834
+ if (_activeTurnId.value === turnId) {
10835
+ _activeTurnId.value = null;
10836
+ _notify();
10837
+ }
10838
+ }
10839
+ function drainLeftoverSteering() {
10840
+ const all = _queue.map((e) => ({ input: e.input, images: e.images }));
10841
+ _queue.length = 0;
10842
+ _notify();
10843
+ return all;
10844
+ }
10845
+ function clearSteering() {
10846
+ _queue.length = 0;
10847
+ _activeTurnId.value = null;
10848
+ _notify();
10849
+ }
10850
+
9886
10851
  // src/generated/cesar/brain.ts
9887
10852
  async function commitTurnAndDelegate(pendingDel, input, response, cesarEngineId, streaming, dispatch, ctx, telemetry) {
9888
10853
  if (streaming) {
9889
10854
  dispatch({ type: "streaming-end", engineId: cesarEngineId });
9890
10855
  }
9891
10856
  if (!streaming) {
10857
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
9892
10858
  dispatch({ type: "spinner-stop" });
9893
10859
  }
9894
10860
  await yieldToInk();
9895
10861
  appendMessage(ctx.chatSession, { role: "user", content: input, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9896
10862
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: response, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9897
- tracker.record(cesarEngineId, { prompt: input, response });
10863
+ recordCesarTurn(ctx, cesarEngineId, input, response);
9898
10864
  const delResult = await promptDelegation(pendingDel.action, dispatch, pendingDel.hardened, pendingDel.tribunalMode, pendingDel.team);
9899
10865
  const happened = buildWhatHappenedSummary(telemetry ?? {});
9900
10866
  if (happened) {
@@ -9916,12 +10882,13 @@ async function commitTurnAndSuggest(suggestion, input, response, cesarEngineId,
9916
10882
  dispatch({ type: "streaming-end", engineId: cesarEngineId });
9917
10883
  }
9918
10884
  if (!streaming) {
10885
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
9919
10886
  dispatch({ type: "spinner-stop" });
9920
10887
  }
9921
10888
  await yieldToInk();
9922
10889
  appendMessage(ctx.chatSession, { role: "user", content: input, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9923
10890
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: response, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9924
- tracker.record(cesarEngineId, { prompt: input, response });
10891
+ recordCesarTurn(ctx, cesarEngineId, input, response);
9925
10892
  if (suggestion.rest) {
9926
10893
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: suggestion.rest });
9927
10894
  }
@@ -9958,6 +10925,22 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9958
10925
  }
9959
10926
  const _toolsUsed = [];
9960
10927
  const _toolUseKeys = /* @__PURE__ */ new Set();
10928
+ const _gate = discoverGate(_turnCwd);
10929
+ if (ctx.cesar) ctx.cesar.discoveredGate = _gate;
10930
+ if (ctx.cesar?.gateNudgedClaim && isGateSkipSignal(input)) ctx.cesar.gateWaived = true;
10931
+ if (ctx.cesar) ctx.cesar.gateNudgedClaim = void 0;
10932
+ let _ranGate = false;
10933
+ const _noteBashForGate = (toolName, rawInput) => {
10934
+ if (_ranGate || !_gate.matchers.length) return;
10935
+ if (!isBashToolName(toolName)) return;
10936
+ let cmd = String(rawInput ?? "");
10937
+ try {
10938
+ const parsed = JSON.parse(cmd);
10939
+ if (parsed && typeof parsed.command === "string") cmd = parsed.command;
10940
+ } catch {
10941
+ }
10942
+ if (bashRanGate(cmd, _gate.matchers)) _ranGate = true;
10943
+ };
9961
10944
  let _toolEventCount = 0;
9962
10945
  let _readToolEventCount = 0;
9963
10946
  let _toolCallTurns = 0;
@@ -9967,6 +10950,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9967
10950
  let _narratedToolStalls = 0;
9968
10951
  let _autoToolExecutions = 0;
9969
10952
  let _confidenceToolUsed = false;
10953
+ let _liveTodosEmitted = false;
9970
10954
  let _actualCesarEngineId = "";
9971
10955
  let _actualCesarBackend = "unknown";
9972
10956
  let _actualHasNativeTools = false;
@@ -9979,6 +10963,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9979
10963
  };
9980
10964
  const recordToolUse = (name, source, input2, status) => {
9981
10965
  const toolName = String(name || "tool");
10966
+ _noteBashForGate(toolName, input2);
9982
10967
  const normalizedSource = source === "eager" ? "xml" : source === "auto" ? "native" : source;
9983
10968
  const key = `${normalizedSource}:${toolName}:${String(input2 ?? "").slice(0, 500)}`;
9984
10969
  _toolEventCount++;
@@ -10003,6 +10988,45 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10003
10988
  input: input2
10004
10989
  });
10005
10990
  };
10991
+ const _toolStartMs = /* @__PURE__ */ new Map();
10992
+ const dispatchToolCall = (evt, opts) => {
10993
+ const key = String(opts?.toolCallId || evt.tool || "tool");
10994
+ let durationMs = evt.durationMs;
10995
+ if (evt.status === "running") {
10996
+ if (!opts?.eager && !opts?.standalone) _toolStartMs.set(key, Date.now());
10997
+ } else if (opts?.standalone) {
10998
+ } else {
10999
+ const startedAt = _toolStartMs.get(key);
11000
+ if (startedAt !== void 0) {
11001
+ if (durationMs === void 0) durationMs = Date.now() - startedAt;
11002
+ _toolStartMs.delete(key);
11003
+ }
11004
+ }
11005
+ dispatch({ ...evt, ...durationMs !== void 0 ? { durationMs } : {} });
11006
+ };
11007
+ let _preambleEmitted = false;
11008
+ const emitPreamble = (text) => {
11009
+ const parsed = parsePreamble(text);
11010
+ if (!parsed.found || !parsed.intent) return text;
11011
+ if (!_preambleEmitted) {
11012
+ _preambleEmitted = true;
11013
+ dispatch({ type: "cesar-preamble", engineId: _actualCesarEngineId || void 0, intent: parsed.intent });
11014
+ }
11015
+ return parsed.rest;
11016
+ };
11017
+ const emitLiveTodos = (text) => {
11018
+ const planActive = !!(ctx.activePlan && ["planning", "awaiting_approval", "running", "paused"].includes(ctx.activePlan.state));
11019
+ if (planActive) return text;
11020
+ const parsed = parseLiveTodos(text);
11021
+ if (!parsed.found) return text;
11022
+ if (parsed.todos.length > 0) {
11023
+ _liveTodosEmitted = true;
11024
+ dispatch({ type: "todos-set", todos: parsed.todos });
11025
+ } else {
11026
+ dispatch({ type: "todos-clear", scope: "live" });
11027
+ }
11028
+ return parsed.rest;
11029
+ };
10006
11030
  const normalizeConfidenceReasoning = (value) => {
10007
11031
  return String(value ?? "").replace(/\s+/g, " ").trim();
10008
11032
  };
@@ -10029,7 +11053,8 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10029
11053
  xmlToolCalls: _xmlToolCalls,
10030
11054
  narratedToolStalls: _narratedToolStalls,
10031
11055
  autoToolExecutions: _autoToolExecutions,
10032
- confidenceToolUsed: _confidenceToolUsed
11056
+ confidenceToolUsed: _confidenceToolUsed,
11057
+ liveTodosEmitted: _liveTodosEmitted
10033
11058
  });
10034
11059
  const FOLLOWUP_RE = /^(still\??|and\??|go on|continue|yes|no|ok|why\??|how\??|what\??|really\??|more|details|explain|show me|huh\??|so\??|\?\??|y|n)$/i;
10035
11060
  const _isFollowUp = FOLLOWUP_RE.test(input.trim());
@@ -10065,11 +11090,6 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10065
11090
  ctx.cesar.queue = null;
10066
11091
  ctx.cesar.abortSignal = null;
10067
11092
  } else {
10068
- if (_isFollowUp) {
10069
- const elapsed = Math.round((Date.now() - busySince) / 1e3);
10070
- dispatch({ type: "info", message: `Cesar still working\u2026 ${elapsed}s` });
10071
- return { delegated: false, responded: true };
10072
- }
10073
11093
  const existing = ctx.cesar.queue;
10074
11094
  if (existing) {
10075
11095
  existing.input = existing.input + "\n\n" + input;
@@ -10094,6 +11114,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10094
11114
  ctx.cesar.searchNudged = false;
10095
11115
  ctx.cesar.turnId = _turnId;
10096
11116
  ctx.cesar.planDispatch = dispatch;
11117
+ markSteeringTurn(_turnId);
10097
11118
  const _brainStartMs = Date.now();
10098
11119
  if (ctx.eventBus) await ctx.eventBus.emit("pre:cesar-brain", { input });
10099
11120
  try {
@@ -10106,6 +11127,33 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10106
11127
  }
10107
11128
  const cesarEngineId = config.cesarEngine ?? config.forgeFixedStarter ?? "claude";
10108
11129
  _actualCesarEngineId = cesarEngineId;
11130
+ let _lastDrainedSteerImages = null;
11131
+ const drainSteeringIntoSend = (carrier, source) => {
11132
+ _lastDrainedSteerImages = null;
11133
+ const pending = drainSteering(_turnId);
11134
+ if (pending.length === 0) return carrier;
11135
+ const blocks = [];
11136
+ const drainedImages = [];
11137
+ for (const msg of pending) {
11138
+ const text = (msg.input ?? "").trim();
11139
+ for (const img of msg.images ?? []) {
11140
+ const p = img?.path;
11141
+ if (typeof p === "string" && p) drainedImages.push(p);
11142
+ }
11143
+ if (!text) continue;
11144
+ dispatch({ type: "user-message", content: text });
11145
+ appendMessage(ctx.chatSession, { role: "user", content: text, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11146
+ blocks.push(text);
11147
+ recordTimeline({ event: "steering_injected", engineId: cesarEngineId, cwd: _turnCwd, source, input: { text, images: drainedImages.length || void 0 } });
11148
+ }
11149
+ if (drainedImages.length) _lastDrainedSteerImages = drainedImages;
11150
+ if (blocks.length === 0) return carrier;
11151
+ const steer = blocks.map((b) => `[User steering \u2014 injected mid-turn]
11152
+ ${b}`).join("\n\n");
11153
+ return carrier ? `${carrier}
11154
+
11155
+ ${steer}` : steer;
11156
+ };
10109
11157
  recordTimeline({
10110
11158
  event: "turn_start",
10111
11159
  engineId: cesarEngineId,
@@ -10126,9 +11174,26 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10126
11174
  ctx.setActiveAbort(abort);
10127
11175
  ctx.cesar.abortSignal = abort.signal;
10128
11176
  ctx.cesar.lastDispatch = dispatch;
11177
+ dispatch({ type: "todos-clear", scope: "live" });
10129
11178
  dispatch({ type: "confidence-update", value: null });
10130
11179
  dispatch({ type: "spinner-start", message: "Cesar thinking\u2026", color });
10131
11180
  await yieldToInk();
11181
+ try {
11182
+ const _budgetBackend = resolveCesarBackend(ctx, cesarEngineId);
11183
+ const _budgetGate = await enforceContextBudget(
11184
+ ctx,
11185
+ ctx.cesarSession ?? null,
11186
+ _budgetBackend.engine,
11187
+ _budgetBackend.backend,
11188
+ dispatch,
11189
+ input
11190
+ );
11191
+ if (!_budgetGate.proceed) {
11192
+ dispatch({ type: "spinner-stop" });
11193
+ return { delegated: false, responded: false };
11194
+ }
11195
+ } catch {
11196
+ }
10132
11197
  let session;
10133
11198
  try {
10134
11199
  session = await ensureCesarSession(ctx);
@@ -10161,14 +11226,18 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10161
11226
  if (freshResult.stdout.trim()) {
10162
11227
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: freshResult.stdout.trim() });
10163
11228
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: freshResult.stdout.trim(), timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10164
- tracker.record(cesarEngineId, { prompt: input, response: freshResult.stdout.trim() });
11229
+ if (freshResult.usage && freshResult.usage.totalTokens > 0) {
11230
+ tracker.record(cesarEngineId, { usage: freshResult.usage });
11231
+ } else {
11232
+ tracker.record(cesarEngineId, { prompt: input, response: freshResult.stdout.trim() });
11233
+ }
10165
11234
  return { delegated: false, responded: true };
10166
11235
  }
10167
11236
  appendUserTurnIfAbsent(ctx.chatSession, input);
10168
11237
  const brainHint = (freshResult.stderr || "").split("\n")[0].slice(0, 200).trim();
10169
11238
  if (brainHint) dispatch({ type: "warning", message: `Cesar (${cesarEngineId}) returned no response: ${brainHint}` });
10170
11239
  const health = engineHealth.get(cesarEngineId);
10171
- if (health && (health.status === "auth-failed" || health.status === "unreachable")) {
11240
+ if (health && (health.status === "auth-failed" || health.status === "unreachable" || health.status === "binary-missing")) {
10172
11241
  dispatch({ type: "warning", message: `Engine ${cesarEngineId} marked ${health.status} \u2014 run /cesar to switch to a healthy engine, or /engines to fix credentials.` });
10173
11242
  }
10174
11243
  return { delegated: false, responded: false };
@@ -10186,17 +11255,21 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10186
11255
  let response = "";
10187
11256
  let streaming = false;
10188
11257
  let wasStreamed = false;
11258
+ let previewShown = false;
10189
11259
  let parsedConfidence = null;
10190
11260
  let confidenceParsed = false;
10191
11261
  let insideThinkBlock = false;
10192
11262
  let suppressXmlToolDisplay = false;
10193
11263
  let sawStreamingXmlToolCall = false;
10194
11264
  let xmlDisplayHold = "";
11265
+ const stripTodosForDisplay = createTodosDisplayStripper();
11266
+ const stripPreambleForDisplay = createPreambleStripper();
10195
11267
  let hadToolActivity = false;
10196
11268
  let _engineErrored = false;
10197
11269
  let _engineErrorMsg = "";
10198
11270
  let secondOpinionPromise = null;
10199
11271
  let usedQuickNero = false;
11272
+ let _escalationSuggested = false;
10200
11273
  const eagerPromises = [];
10201
11274
  let eagerToolCtx = null;
10202
11275
  const shouldInterruptForXmlTool = () => {
@@ -10342,12 +11415,12 @@ ${enrichedInput}`;
10342
11415
  const signalDir = ctx.cesar.mcpSignalPath ? join14(ctx.cesar.mcpSignalPath, "..") : null;
10343
11416
  const processMcpSideChannel = () => {
10344
11417
  try {
10345
- if (!signalDir || !existsSync13(signalDir)) return;
11418
+ if (!signalDir || !existsSync14(signalDir)) return;
10346
11419
  const completions = readdirSync(signalDir).filter((f) => f.includes("-tool-") && f.endsWith(".json"));
10347
11420
  for (const f of completions) {
10348
11421
  const donePath = join14(signalDir, f);
10349
11422
  try {
10350
- const done = JSON.parse(readFileSync13(donePath, "utf-8"));
11423
+ const done = JSON.parse(readFileSync14(donePath, "utf-8"));
10351
11424
  if (done.type !== "tool-completion") continue;
10352
11425
  try {
10353
11426
  unlinkSync(donePath);
@@ -10357,21 +11430,21 @@ ${enrichedInput}`;
10357
11430
  const status = done.status === "error" ? "error" : "done";
10358
11431
  const toolInput = typeof done.args === "string" ? done.args : JSON.stringify(done.args ?? {});
10359
11432
  recordToolUse(String(done.tool ?? "tool"), "mcp", toolInput, status);
10360
- dispatch({
11433
+ dispatchToolCall({
10361
11434
  type: "tool-call",
10362
11435
  engineId: cesarEngineId,
10363
11436
  tool: String(done.tool ?? "tool"),
10364
11437
  input: toolInput,
10365
11438
  status,
10366
11439
  output: typeof done.output === "string" ? done.output : void 0
10367
- });
11440
+ }, { standalone: true });
10368
11441
  } catch {
10369
11442
  }
10370
11443
  }
10371
11444
  const files = readdirSync(signalDir).filter((f) => f.includes("-perm-") && !f.includes("-response"));
10372
11445
  for (const f of files) {
10373
11446
  const reqPath = join14(signalDir, f);
10374
- const req = JSON.parse(readFileSync13(reqPath, "utf-8"));
11447
+ const req = JSON.parse(readFileSync14(reqPath, "utf-8"));
10375
11448
  if (req.type !== "permission-request") continue;
10376
11449
  if (Date.now() - req.timestamp > 65e3) {
10377
11450
  try {
@@ -10381,7 +11454,7 @@ ${enrichedInput}`;
10381
11454
  continue;
10382
11455
  }
10383
11456
  const respPath = reqPath.replace(".json", "-response.json");
10384
- if (existsSync13(respPath)) continue;
11457
+ if (existsSync14(respPath)) continue;
10385
11458
  const cfg = loadConfig();
10386
11459
  const allowed = cfg.allowedCommands ?? [];
10387
11460
  const cmdBase = (req.args?.command ?? "").toString().trim().split(/\s+/)[0];
@@ -10414,12 +11487,30 @@ ${enrichedInput}`;
10414
11487
  input: reqArgs
10415
11488
  });
10416
11489
  };
11490
+ if (String(cfg.permissionMode ?? "ask") === "deny-all") {
11491
+ logMcpApproval("denied", "settings.permissionMode", "permissionMode=deny-all");
11492
+ writeFileSync11(respPath, JSON.stringify({ type: "permission-response", id: req.id, approved: false, reason: "All tool execution is denied (permissionMode=deny-all)" }));
11493
+ continue;
11494
+ }
11495
+ const mcpRuleSet = parsePermissionRuleSet(cfg.permissions);
11496
+ const mcpRuleArg = reqTool === "Bash" ? String(req.args?.command ?? "") : String(req.args?.file_path ?? "");
11497
+ const mcpRuleDecision = evaluateToolRules(reqTool, mcpRuleArg, resolveWorkingDir(), mcpRuleSet);
11498
+ if (mcpRuleDecision === "deny") {
11499
+ logMcpApproval("denied", "settings.permissions", `${reqTool} denied by permissions rule`);
11500
+ writeFileSync11(respPath, JSON.stringify({ type: "permission-response", id: req.id, approved: false, reason: `${reqTool} blocked by deny rule in .agon.json permissions` }));
11501
+ continue;
11502
+ }
11503
+ if (mcpRuleDecision === "allow") {
11504
+ logMcpApproval("approved", "settings.permissions", `${reqTool} allowed by permissions rule`);
11505
+ writeFileSync11(respPath, JSON.stringify({ type: "permission-response", id: req.id, approved: true }));
11506
+ continue;
11507
+ }
10417
11508
  if (cmdBase && allowed.some((a) => cmdBase.toLowerCase().startsWith(a.toLowerCase()))) {
10418
11509
  logMcpApproval("approved", "mcp.allowedCommands", "command matched allowedCommands");
10419
11510
  writeFileSync11(respPath, JSON.stringify({ type: "permission-response", id: req.id, approved: true }));
10420
11511
  continue;
10421
11512
  }
10422
- if (reqTool === "Edit" || reqTool === "Write") {
11513
+ if (reqTool === "Edit" || reqTool === "Write" || reqTool === "MultiEdit") {
10423
11514
  const approvalCwd = resolveWorkingDir();
10424
11515
  const approvalCache = getProjectFileStateCache(approvalCwd);
10425
11516
  const activePlan = ctx.activePlan;
@@ -10441,7 +11532,16 @@ ${enrichedInput}`;
10441
11532
  }
10442
11533
  }
10443
11534
  logMcpApproval("prompted", "mcp.user-prompt", "Cesar wants to execute");
10444
- dispatch({ type: "permission-ask", tool: req.tool, command: String(req.args?.command ?? req.args?.file_path ?? JSON.stringify(req.args)), reason: `Cesar wants to execute`, resolve: (approved) => {
11535
+ const askCommand = renderToolPermissionCommand(reqTool, reqArgs);
11536
+ const askReason = reqTool === "SaveMemory" ? "Cesar wants to save a durable project memory" : "Cesar wants to execute";
11537
+ let permDiffPreview = void 0;
11538
+ if (approvalToolIsFileMutating(req.tool)) {
11539
+ try {
11540
+ permDiffPreview = buildApprovalDiffPreview(String(req.tool), reqArgs);
11541
+ } catch {
11542
+ }
11543
+ }
11544
+ dispatch({ type: "permission-ask", tool: req.tool, command: askCommand, reason: askReason, diffPreview: permDiffPreview && Array.isArray(permDiffPreview.files) ? permDiffPreview : void 0, fallbackNote: permDiffPreview && typeof permDiffPreview.fallback === "string" ? permDiffPreview.fallback : void 0, resolve: (approved) => {
10445
11545
  const wasApproved = typeof approved === "string" ? approved === "y" || approved === "a" : approved;
10446
11546
  logMcpApproval(wasApproved ? "approved" : "denied", "mcp.user-prompt", wasApproved ? "user approved" : "user denied");
10447
11547
  if (typeof approved === "string" && approved === "a" || approved === true) {
@@ -10470,6 +11570,13 @@ ${enrichedInput}`;
10470
11570
  const gen = session.send(sendOptions);
10471
11571
  for await (const chunk of gen) {
10472
11572
  if (abort.signal.aborted) break;
11573
+ if (chunk.type === "preview") {
11574
+ if (!abort.signal.aborted && !streaming && !wasStreamed) {
11575
+ previewShown = true;
11576
+ dispatch({ type: "streaming-preview", engineId: cesarEngineId, content: String(chunk.content ?? "") });
11577
+ }
11578
+ continue;
11579
+ }
10473
11580
  if (chunk.type === "status") {
10474
11581
  const statusText = String(chunk.content ?? "");
10475
11582
  const _ctxMeta = chunk.metadata ?? {};
@@ -10480,7 +11587,8 @@ ${enrichedInput}`;
10480
11587
  used: Number(_ctxMeta.used ?? 0),
10481
11588
  limit: Number(_ctxMeta.limit ?? 0),
10482
11589
  compacted: Number(_ctxMeta.compacted ?? 0),
10483
- cached: Number(_ctxMeta.cached ?? 0)
11590
+ cached: Number(_ctxMeta.cached ?? 0),
11591
+ source: typeof _ctxMeta.source === "string" ? _ctxMeta.source : void 0
10484
11592
  });
10485
11593
  continue;
10486
11594
  }
@@ -10507,7 +11615,7 @@ ${enrichedInput}`;
10507
11615
  dispatch({ type: "spinner-update", message: `Cesar: ${toolName}\u2026` });
10508
11616
  if (meta.input && STREAM_ORCH.has(toolName)) {
10509
11617
  if (cesarFastPath) {
10510
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` });
11618
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` });
10511
11619
  continue;
10512
11620
  }
10513
11621
  if (!ctx.cesar.pendingDelegation) {
@@ -10515,35 +11623,35 @@ ${enrichedInput}`;
10515
11623
  ctx.eventBus?.emit("cesar:delegation", { action: toolName.toLowerCase(), source: `stream-${toolStatus}` }).catch(() => {
10516
11624
  });
10517
11625
  }
10518
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done", output: typeof meta.output === "string" ? meta.output : void 0 });
11626
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done", output: typeof meta.output === "string" ? meta.output : void 0 });
10519
11627
  continue;
10520
11628
  }
10521
11629
  if (toolStatus === "done") {
10522
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done", output: typeof meta.output === "string" ? meta.output : void 0 });
11630
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done", output: typeof meta.output === "string" ? meta.output : void 0 }, { toolCallId: meta.toolCallId });
10523
11631
  } else if (toolStatus === "native") {
10524
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "running" });
11632
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "running" }, { toolCallId: meta.toolCallId });
10525
11633
  } else if (toolStatus === "running" && meta.input && toolRegistry && !ctx.cesar.hasNativeTools) {
10526
11634
  if (ctx.cesar.pendingDelegation) {
10527
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done" });
11635
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done" }, { toolCallId: meta.toolCallId });
10528
11636
  continue;
10529
11637
  }
10530
11638
  const EAGER_ORCH = STREAM_ORCH;
10531
11639
  if (EAGER_ORCH.has(toolName)) {
10532
11640
  if (cesarFastPath) {
10533
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` });
11641
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` });
10534
11642
  continue;
10535
11643
  }
10536
11644
  ctx.cesar.pendingDelegation = extractDelegation(toolName, meta.input ?? {});
10537
11645
  ctx.eventBus?.emit("cesar:delegation", { action: toolName.toLowerCase(), source: "stream" }).catch(() => {
10538
11646
  });
10539
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done" });
11647
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "done" });
10540
11648
  continue;
10541
11649
  }
10542
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "running" });
11650
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "running" }, { toolCallId: meta.toolCallId, eager: true });
10543
11651
  if (!eagerToolCtx) eagerToolCtx = createEagerToolContext(ctx, config, abort.signal, dispatch);
10544
11652
  eagerPromises.push(executeEagerTool(toolName, meta, toolRegistry, eagerToolCtx, dispatch, cesarEngineId));
10545
11653
  } else {
10546
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: toolStatus, output: typeof meta.output === "string" ? meta.output : void 0 });
11654
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: toolStatus, output: typeof meta.output === "string" ? meta.output : void 0 }, { toolCallId: meta.toolCallId });
10547
11655
  }
10548
11656
  continue;
10549
11657
  }
@@ -10557,6 +11665,7 @@ ${enrichedInput}`;
10557
11665
  const errBody = _errFull.slice(0, 200) || "unknown stream error";
10558
11666
  const _deterministic = /\b(?:400|401|403|404)\b|not found|unauthorized|invalid api key|authentication|no such (?:route|endpoint)/i.test(_errFull);
10559
11667
  dispatch({ type: "warning", message: `Cesar (${cesarEngineId}) stream error before any output: ${errBody}.${_deterministic ? " Looks like an engine config/auth issue \u2014 check the engine with /engines." : " Try again or switch engine with /engine."}` });
11668
+ if (previewShown) dispatch({ type: "streaming-end", engineId: cesarEngineId });
10560
11669
  clearInterval(heartbeat);
10561
11670
  processMcpSideChannel();
10562
11671
  if (mcpWatcherInterval) clearInterval(mcpWatcherInterval);
@@ -10624,19 +11733,29 @@ ${enrichedInput}`;
10624
11733
  cleanFirst = response.replace(/<think>[\s\S]*/gi, "");
10625
11734
  }
10626
11735
  }
11736
+ emitPreamble(response);
11737
+ cleanFirst = stripPreambleForDisplay(cleanFirst);
10627
11738
  if (!ctx.cesar.hasNativeTools) {
10628
11739
  const split = splitBeforeToolMarkup(cleanFirst);
10629
11740
  cleanFirst = split.visible;
10630
11741
  if (split.hasToolMarkup) suppressXmlToolDisplay = true;
11742
+ } else {
11743
+ cleanFirst = stripTodosForDisplay(cleanFirst);
10631
11744
  }
10632
11745
  if (cleanFirst.trim()) dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: cleanFirst });
10633
11746
  } else {
10634
11747
  response += chunk.content;
10635
11748
  noteXmlToolDetected(true);
10636
11749
  let displayChunk = chunk.content;
11750
+ emitPreamble(response);
11751
+ displayChunk = stripPreambleForDisplay(displayChunk);
11752
+ if (!displayChunk) continue;
10637
11753
  if (!ctx.cesar.hasNativeTools) {
10638
11754
  displayChunk = takeXmlSafeDisplayChunk(displayChunk);
10639
11755
  if (!displayChunk) continue;
11756
+ } else {
11757
+ displayChunk = stripTodosForDisplay(displayChunk);
11758
+ if (!displayChunk && !insideThinkBlock) continue;
10640
11759
  }
10641
11760
  if (insideThinkBlock) {
10642
11761
  if (displayChunk.includes("</think>")) {
@@ -10670,10 +11789,21 @@ ${enrichedInput}`;
10670
11789
  }
10671
11790
  }
10672
11791
  }
11792
+ const trailingPreambleSafe = stripPreambleForDisplay("", true);
11793
+ if (streaming && trailingPreambleSafe) {
11794
+ const routed = ctx.cesar.hasNativeTools ? stripTodosForDisplay(trailingPreambleSafe) : takeXmlSafeDisplayChunk(trailingPreambleSafe);
11795
+ if (routed.trim()) dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: routed });
11796
+ }
10673
11797
  const trailingXmlSafe = takeXmlSafeDisplayChunk("", true);
10674
11798
  if (streaming && trailingXmlSafe.trim()) {
10675
11799
  dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: trailingXmlSafe });
10676
11800
  }
11801
+ if (ctx.cesar.hasNativeTools) {
11802
+ const trailingTodosSafe = stripTodosForDisplay("", true);
11803
+ if (streaming && trailingTodosSafe.trim()) {
11804
+ dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: trailingTodosSafe });
11805
+ }
11806
+ }
10677
11807
  } catch (err) {
10678
11808
  clearInterval(heartbeat);
10679
11809
  processMcpSideChannel();
@@ -10684,6 +11814,7 @@ ${enrichedInput}`;
10684
11814
  dispatch({ type: "warning", message: `Cesar stream error (partial response preserved): ${(err.message ?? "").slice(0, 80)}` });
10685
11815
  } else {
10686
11816
  dispatch({ type: "warning", message: "Cesar session error \u2014 will restart on next message" });
11817
+ if (previewShown) dispatch({ type: "streaming-end", engineId: cesarEngineId });
10687
11818
  return { delegated: false, responded: false, decisionReason: "stream-error" };
10688
11819
  }
10689
11820
  }
@@ -10692,6 +11823,7 @@ ${enrichedInput}`;
10692
11823
  if (mcpWatcherInterval) clearInterval(mcpWatcherInterval);
10693
11824
  if (abort.signal.aborted) {
10694
11825
  dispatch({ type: "spinner-stop" });
11826
+ if (previewShown) dispatch({ type: "streaming-end", engineId: cesarEngineId });
10695
11827
  const elapsed = Math.round((Date.now() - _turnStart) / 1e3);
10696
11828
  if (elapsed >= cesarTimeout) {
10697
11829
  dispatch({ type: "warning", message: `Cesar timed out after ${elapsed}s. Try a simpler question, or use /forge for complex tasks.` });
@@ -10704,6 +11836,8 @@ ${enrichedInput}`;
10704
11836
  if (ctx.cesar.hasNativeTools) {
10705
11837
  response = response.replace(/<tool\s+name="[^"]+">[\s\S]*?<\/tool>/g, "").trim();
10706
11838
  }
11839
+ response = emitPreamble(response);
11840
+ response = emitLiveTodos(response);
10707
11841
  if (eagerPromises.length > 0 && !ctx.cesar.hasNativeTools && session.alive && !abort.signal.aborted) {
10708
11842
  dispatch({ type: "spinner-start", message: `Cesar: awaiting ${eagerPromises.length} tool result${eagerPromises.length > 1 ? "s" : ""}\u2026`, color });
10709
11843
  const eagerResults = await Promise.all(eagerPromises);
@@ -10716,7 +11850,8 @@ ${enrichedInput}`;
10716
11850
  const failedTools = eagerFailedToolNames(eagerResults);
10717
11851
  const repairUsed = [];
10718
11852
  const repairResults = [];
10719
- const contGen = session.send({ message: formatted, signal: abort.signal });
11853
+ const _steerMsg = drainSteeringIntoSend(formatted, "xml");
11854
+ const contGen = session.send({ message: _steerMsg, signal: abort.signal, ..._lastDrainedSteerImages ? { images: _lastDrainedSteerImages } : {} });
10720
11855
  for await (const chunk of contGen) {
10721
11856
  if (chunk.type === "text") continuation += chunk.content;
10722
11857
  if (chunk.type === "tool_call") {
@@ -10733,9 +11868,9 @@ ${enrichedInput}`;
10733
11868
  continue;
10734
11869
  }
10735
11870
  if (toolStatus !== "running" && toolStatus !== "native") {
10736
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: toolStatus, output: typeof meta.output === "string" ? meta.output : void 0 });
11871
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: toolStatus, output: typeof meta.output === "string" ? meta.output : void 0 });
10737
11872
  } else if (failedTools.includes(toolName)) {
10738
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: "Repair retry already used for this tool in this turn." });
11873
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: "Repair retry already used for this tool in this turn." });
10739
11874
  }
10740
11875
  }
10741
11876
  if (chunk.type === "done" || chunk.type === "error") break;
@@ -10754,7 +11889,7 @@ ${enrichedInput}`;
10754
11889
  const meta = chunk.metadata ?? {};
10755
11890
  const toolName = chunk.content || "tool";
10756
11891
  const toolInput = typeof meta.input === "string" ? meta.input : meta.input ? JSON.stringify(meta.input) : "";
10757
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: "Tool repair loop is one retry per failed tool; further tool calls were not executed automatically." });
11892
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: toolName, input: toolInput, status: "error", output: "Tool repair loop is one retry per failed tool; further tool calls were not executed automatically." });
10758
11893
  }
10759
11894
  if (chunk.type === "done" || chunk.type === "error") break;
10760
11895
  }
@@ -10762,7 +11897,7 @@ ${enrichedInput}`;
10762
11897
  }
10763
11898
  }
10764
11899
  dispatch({ type: "spinner-stop" });
10765
- if (continuation.trim()) response = continuation.trim();
11900
+ if (continuation.trim()) response = emitLiveTodos(emitPreamble(continuation.trim()));
10766
11901
  }
10767
11902
  }
10768
11903
  if (!confidenceParsed && response) {
@@ -10798,22 +11933,22 @@ ${enrichedInput}`;
10798
11933
  if (!ctx.cesar.pendingDelegation && ctx.cesar.mcpSignalPath) {
10799
11934
  try {
10800
11935
  const signalPath = ctx.cesar.mcpSignalPath;
10801
- if (existsSync13(signalPath)) {
10802
- const signals = JSON.parse(readFileSync13(signalPath, "utf-8"));
11936
+ if (existsSync14(signalPath)) {
11937
+ const signals = JSON.parse(readFileSync14(signalPath, "utf-8"));
10803
11938
  unlinkSync(signalPath);
10804
11939
  for (const signal of Array.isArray(signals) ? signals : [signals]) {
10805
11940
  if (!signal.timestamp || Date.now() - signal.timestamp >= 6e4) continue;
10806
11941
  if (cesarFastPath && FAST_PATH_BLOCKED_TOOLS.includes(signal.tool)) {
10807
11942
  const toolInput = JSON.stringify(signal.args ?? {});
10808
11943
  recordToolUse(signal.tool, "mcp", toolInput, "error");
10809
- dispatch({
11944
+ dispatchToolCall({
10810
11945
  type: "tool-call",
10811
11946
  engineId: cesarEngineId,
10812
11947
  tool: signal.tool,
10813
11948
  input: toolInput,
10814
11949
  status: "error",
10815
11950
  output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.`
10816
- });
11951
+ }, { standalone: true });
10817
11952
  continue;
10818
11953
  }
10819
11954
  if (signal.tool === "ReportConfidence") {
@@ -10843,17 +11978,17 @@ ${enrichedInput}`;
10843
11978
  recordToolUse("ProposePlan", "mcp", JSON.stringify(signal.args ?? {}), "done");
10844
11979
  const activePlan = ctx.activePlan;
10845
11980
  if (activePlan && ["planning", "running", "paused"].includes(activePlan.state)) {
10846
- dispatch({
11981
+ dispatchToolCall({
10847
11982
  type: "tool-call",
10848
11983
  engineId: cesarEngineId,
10849
11984
  tool: "ProposePlan",
10850
11985
  input: JSON.stringify(signal.args ?? {}),
10851
11986
  status: "error",
10852
11987
  output: "A Cesar plan is already active; nested plans are blocked. Resume or cancel the current plan before proposing another."
10853
- });
11988
+ }, { standalone: true });
10854
11989
  continue;
10855
11990
  }
10856
- const { handleProposePlan } = await import("./plan-mode-5IQ2SKIS.js");
11991
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
10857
11992
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
10858
11993
  if (planDispatch) {
10859
11994
  try {
@@ -10866,11 +12001,11 @@ ${enrichedInput}`;
10866
12001
  }
10867
12002
  } else if (signal.tool === "ExitPlanMode") {
10868
12003
  recordToolUse("ExitPlanMode", "mcp", JSON.stringify(signal.args ?? {}), "done");
10869
- const { handleExitPlanMode } = await import("./plan-mode-5IQ2SKIS.js");
12004
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
10870
12005
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
10871
12006
  try {
10872
12007
  const exitResult = handleExitPlanMode(String(signal.args?.reason ?? ""), planDispatch, ctx);
10873
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: "ExitPlanMode", input: JSON.stringify(signal.args ?? {}), status: "done", output: exitResult });
12008
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: "ExitPlanMode", input: JSON.stringify(signal.args ?? {}), status: "done", output: exitResult }, { standalone: true });
10874
12009
  } catch (err) {
10875
12010
  console.warn(`[agon] ExitPlanMode via MCP signal failed: ${err instanceof Error ? err.message : String(err)}`);
10876
12011
  }
@@ -10923,7 +12058,14 @@ ${enrichedInput}`;
10923
12058
  // Phase 1: investigate only — mutating tools blocked until after escalation check
10924
12059
  blockedTools: cesarFastPath ? FAST_PATH_BLOCKED_TOOLS : void 0,
10925
12060
  blockedToolMessage: cesarFastPath ? `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` : void 0,
10926
- source: "orchestrator"
12061
+ source: "orchestrator",
12062
+ // CC-parity allow/deny rules reach all three XML runToolLoop paths via
12063
+ // this shared toolCtx (runToolLoop → executeToolCalls → executeToolCall
12064
+ // → handler.checkPermission). deny-first, before mode-based auto-allow.
12065
+ permissionRules: parsePermissionRuleSet(config.permissions),
12066
+ // CC-parity PreToolUse/PostToolUse hooks reach all three XML
12067
+ // runToolLoop paths via this shared toolCtx (→ executeToolCall).
12068
+ toolHooks: parseToolHooks(config.hooks)
10927
12069
  };
10928
12070
  const _lastToolInputs = {};
10929
12071
  const xmlToolBridge = createStreamBridge(dispatch, { initialEngineId: cesarEngineId });
@@ -10969,12 +12111,13 @@ ${enrichedInput}`;
10969
12111
  async (message) => {
10970
12112
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
10971
12113
  if (!session.alive || abort.signal.aborted) return "";
12114
+ const sendMessage = drainSteeringIntoSend(message, "xml");
10972
12115
  dispatch({ type: "spinner-start", message: "Cesar processing results\u2026", color });
10973
12116
  _engineErrored = false;
10974
12117
  _engineErrorMsg = "";
10975
12118
  let nextResponse = "";
10976
12119
  const gen = session.send({
10977
- message,
12120
+ message: sendMessage,
10978
12121
  signal: abort.signal,
10979
12122
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
10980
12123
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11046,7 +12189,7 @@ ${enrichedInput}`;
11046
12189
  const lastInput = _lastToolInputs[tool] ?? "{}";
11047
12190
  let command = "";
11048
12191
  try {
11049
- command = JSON.parse(lastInput).command ?? JSON.parse(lastInput).file_path ?? lastInput;
12192
+ command = renderToolPermissionCommand(tool, JSON.parse(lastInput));
11050
12193
  } catch {
11051
12194
  command = lastInput;
11052
12195
  }
@@ -11073,7 +12216,7 @@ ${enrichedInput}`;
11073
12216
  try {
11074
12217
  const activePlan = ctx.activePlan;
11075
12218
  if (activePlan && ["planning", "running", "paused"].includes(activePlan.state)) {
11076
- dispatch({
12219
+ dispatchToolCall({
11077
12220
  type: "tool-call",
11078
12221
  engineId: cesarEngineId,
11079
12222
  tool: "ProposePlan",
@@ -11082,7 +12225,7 @@ ${enrichedInput}`;
11082
12225
  output: "A Cesar plan is already active; nested plans are blocked. Resume or cancel the current plan before proposing another."
11083
12226
  });
11084
12227
  } else {
11085
- const { handleProposePlan } = await import("./plan-mode-5IQ2SKIS.js");
12228
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
11086
12229
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
11087
12230
  const plan = await handleProposePlan(ppArgs, planDispatch, ctx);
11088
12231
  if (ctx.setActivePlan) ctx.setActivePlan(plan);
@@ -11106,10 +12249,10 @@ ${enrichedInput}`;
11106
12249
  const epArgs = ctx.cesar._exitPlanModeArgs;
11107
12250
  delete ctx.cesar._exitPlanModeArgs;
11108
12251
  try {
11109
- const { handleExitPlanMode } = await import("./plan-mode-5IQ2SKIS.js");
12252
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
11110
12253
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
11111
12254
  const exitResult = handleExitPlanMode(String(epArgs?.reason ?? ""), planDispatch, ctx);
11112
- dispatch({ type: "tool-call", engineId: cesarEngineId, tool: "ExitPlanMode", input: JSON.stringify(epArgs ?? {}), status: "done", output: exitResult });
12255
+ dispatchToolCall({ type: "tool-call", engineId: cesarEngineId, tool: "ExitPlanMode", input: JSON.stringify(epArgs ?? {}), status: "done", output: exitResult });
11113
12256
  } catch (err) {
11114
12257
  console.warn(`[agon] ExitPlanMode via tool loop failed: ${err instanceof Error ? err.message : String(err)}`);
11115
12258
  }
@@ -11135,7 +12278,7 @@ ${enrichedInput}`;
11135
12278
  confidenceParsed = true;
11136
12279
  }
11137
12280
  }
11138
- const shouldQuickNero = parsedConfidence !== null && !cesarFastPath && !secondOpinionPromise && !ctx.cesar.advisorPending && !_isFollowUp && !abort.signal.aborted && !ctx.cesar.pendingDelegation && (ctx.cesar.quickNeroRequested === true || routingHints.uncertaintyFamily === "challenge" || routingHints.uncertaintyFamily === "tradeoff" || routingHints.uncertaintyFamily === "implementation" && parsedConfidence < 86 || mutationDeferred && parsedConfidence < 90);
12281
+ const shouldQuickNero = parsedConfidence !== null && !cesarFastPath && !secondOpinionPromise && !ctx.cesar.advisorPending && !_isFollowUp && !abort.signal.aborted && !ctx.cesar.pendingDelegation && ctx.cesar.quickNeroRequested === true;
11139
12282
  if (ctx.cesar.quickNeroRequested) ctx.cesar.quickNeroRequested = false;
11140
12283
  if (shouldQuickNero) {
11141
12284
  const quickNeroConfidence = parsedConfidence;
@@ -11175,7 +12318,7 @@ ${qnResult.challengeText}` : ""
11175
12318
  appendMessage(ctx.chatSession, { role: "engine", engineId: ch.engineId, content: ch.content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11176
12319
  }
11177
12320
  }
11178
- tracker.record(cesarEngineId, { prompt: input, response });
12321
+ recordCesarTurn(ctx, cesarEngineId, input, response);
11179
12322
  return {
11180
12323
  mode: escalationAction,
11181
12324
  delegated: true,
@@ -11189,12 +12332,24 @@ ${qnResult.challengeText}` : ""
11189
12332
  };
11190
12333
  }
11191
12334
  }
12335
+ if (!_escalationSuggested && !usedQuickNero && !cesarFastPath && !_isFollowUp && !abort.signal.aborted && !ctx.cesar.pendingDelegation) {
12336
+ const _strictConf = extractStrictConfidence(response);
12337
+ if (_strictConf !== null && _strictConf < ESCALATION_SUGGESTION_THRESHOLD) {
12338
+ _escalationSuggested = true;
12339
+ dispatch({ type: "info", message: buildEscalationSuggestionLine(_strictConf) });
12340
+ }
12341
+ }
11192
12342
  const investigationResponse = response;
11193
12343
  let mutationStallForced = false;
11194
12344
  if (!mutationDeferred && !inPlanMode && !ctx.cesar.pendingDelegation && (hadToolActivity || ranToolLoop) && session.alive && !abort.signal.aborted && detectMutationIntentStall(response)) {
11195
- mutationDeferred = true;
11196
- mutationStallForced = true;
11197
- dispatch({ type: "warning", message: "Cesar described a write but called no tool \u2014 unlocking execution and pushing it to apply directly." });
12345
+ const _usedMutatingTool = _toolsUsed.some((t) => isWriteToolName(t) || isBashToolName(t));
12346
+ if (shouldDeescalateGuard({ intakeKind: routingHints.intakeKind, recommendedFlow: routingHints.recommendedFlow, usedMutatingTool: _usedMutatingTool })) {
12347
+ dispatch({ type: "warning", message: "Cesar guard: write-narration matched on a conversational turn \u2014 warning only, no auto-unlock (de-escalated)." });
12348
+ } else {
12349
+ mutationDeferred = true;
12350
+ mutationStallForced = true;
12351
+ dispatch({ type: "warning", message: "Cesar described a write but called no tool \u2014 unlocking execution and pushing it to apply directly." });
12352
+ }
11198
12353
  }
11199
12354
  if (mutationDeferred && toolRegistry && session.alive && !abort.signal.aborted) {
11200
12355
  toolCtx.readOnlyMode = false;
@@ -11214,10 +12369,11 @@ ${qnResult.challengeText}` : ""
11214
12369
  async (message) => {
11215
12370
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
11216
12371
  if (!session.alive || abort.signal.aborted) return "";
12372
+ const sendMessage = drainSteeringIntoSend(message, "exec");
11217
12373
  dispatch({ type: "spinner-start", message: "Cesar executing\u2026", color });
11218
12374
  let nextResponse = "";
11219
12375
  const gen = session.send({
11220
- message,
12376
+ message: sendMessage,
11221
12377
  signal: abort.signal,
11222
12378
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
11223
12379
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11293,7 +12449,14 @@ ${qnResult.challengeText}` : ""
11293
12449
  dispatch({ type: "spinner-stop" });
11294
12450
  }
11295
12451
  }
11296
- if (!ctx.cesar.pendingDelegation && session.alive && !abort.signal.aborted && detectFabricatedDelegation(response.trim())) {
12452
+ const _fabDelegationDetected = !ctx.cesar.pendingDelegation && session.alive && !abort.signal.aborted && detectFabricatedDelegation(response.trim());
12453
+ if (_fabDelegationDetected && shouldDeescalateGuard({
12454
+ intakeKind: routingHints.intakeKind,
12455
+ recommendedFlow: routingHints.recommendedFlow,
12456
+ usedMutatingTool: _toolsUsed.some((t) => isWriteToolName(t) || isBashToolName(t))
12457
+ })) {
12458
+ dispatch({ type: "warning", message: "Cesar guard: dispatch-claim matched on a conversational turn \u2014 warning only, no grounding turn (de-escalated)." });
12459
+ } else if (_fabDelegationDetected) {
11297
12460
  dispatch({ type: "warning", message: "Cesar claimed a job was running but never dispatched one \u2014 grounding..." });
11298
12461
  dispatch({ type: "spinner-start", message: "Cesar grounding\u2026", color });
11299
12462
  try {
@@ -11405,12 +12568,11 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11405
12568
  dispatch({ type: "spinner-stop" });
11406
12569
  }
11407
12570
  }
11408
- const _AUTO_CONT_WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
11409
12571
  const _AUTO_CONT_LOOP_ORCH = /* @__PURE__ */ new Set(["Forge", "Brainstorm", "Tribunal", "Campfire", "Pipeline", "Review", "Agent", "Goal", "Conquer"]);
11410
12572
  const _AUTO_CONT_CONTINUE_RE = /\b(?:now i'?ll|next i'?ll|still need|let me also|i'?ll also|then i'?ll|next step|next up)\b/i;
11411
12573
  const _AUTO_CONT_READONLY_DONE_RE = /\b(?:tests? passed|all (?:tests|checks) pass|no matches found|no issues found|no errors|all clean|nothing to (?:do|fix|change)|already (?:correct|fixed|in place|done))\b/i;
11412
12574
  const _detectTurnState = (resp, baselineToolCount) => {
11413
- const wroteSinceBaseline = _toolsUsed.slice(baselineToolCount).some((t) => _AUTO_CONT_WRITE_TOOLS.has(t));
12575
+ const wroteSinceBaseline = _toolsUsed.slice(baselineToolCount).some((t) => isWriteToolName(t));
11414
12576
  if (findTrailingUserQuestion(resp)) return "asks-user";
11415
12577
  if (wroteSinceBaseline && !_AUTO_CONT_CONTINUE_RE.test(resp)) return "done";
11416
12578
  const effectSummary = /(?:created|modified|deleted|updated|added|removed|fixed|implemented|renamed)\b[^.]{0,80}(?:\b(?:and|,)\b[^.]{0,80}\b(?:created|modified|deleted|updated|added|removed|fixed|implemented|renamed)\b)/i.test(resp);
@@ -11419,6 +12581,18 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11419
12581
  if (_AUTO_CONT_READONLY_DONE_RE.test(resp)) return "done";
11420
12582
  return "stuck";
11421
12583
  };
12584
+ const _doneClaimSignature = (resp) => {
12585
+ return `${_toolsUsed.length}:${resp.trim().slice(-200)}`;
12586
+ };
12587
+ const _shouldGateNudge = (resp) => {
12588
+ const g = ctx.cesar?.discoveredGate;
12589
+ if (!g || !g.command || !g.matchers.length) return false;
12590
+ if (ctx.cesar?.gateWaived) return false;
12591
+ if (_ranGate) return false;
12592
+ if (!_toolsUsed.some((t) => isWriteToolName(t))) return false;
12593
+ if (ctx.cesar?.gateNudgedClaim === _doneClaimSignature(resp)) return false;
12594
+ return true;
12595
+ };
11422
12596
  const _buildContToolLoopOpts = () => ({
11423
12597
  onToolCall: (name, inp) => {
11424
12598
  _lastToolInputs[name] = JSON.stringify(inp);
@@ -11453,7 +12627,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11453
12627
  const lastInput = _lastToolInputs[tool] ?? "{}";
11454
12628
  let command = "";
11455
12629
  try {
11456
- command = JSON.parse(lastInput).command ?? JSON.parse(lastInput).file_path ?? lastInput;
12630
+ command = renderToolPermissionCommand(tool, JSON.parse(lastInput));
11457
12631
  } catch {
11458
12632
  command = lastInput;
11459
12633
  }
@@ -11477,8 +12651,85 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11477
12651
  let _consecutiveNoProgress = 0;
11478
12652
  while (_continuations < MAX_CONTINUATIONS && session.alive && !abort.signal.aborted && !ctx.cesar.pendingDelegation) {
11479
12653
  const state = _detectTurnState(response, _loopStartToolCount);
11480
- if (state === "asks-user" || state === "done") break;
11481
- const wroteFiles = _toolsUsed.some((t) => _AUTO_CONT_WRITE_TOOLS.has(t));
12654
+ if (state === "asks-user") break;
12655
+ if (state === "done") {
12656
+ if (_shouldGateNudge(response)) {
12657
+ if (ctx.cesar) ctx.cesar.gateNudgedClaim = _doneClaimSignature(response);
12658
+ const _g = ctx.cesar.discoveredGate;
12659
+ const _gateNudge = `[SYSTEM] You claimed the task is done but never ran the project's verification gate (${_g.command}) this turn. Run it now to confirm the change is green, or tell me in one sentence why it should be skipped.`;
12660
+ _continuations++;
12661
+ dispatch({ type: "warning", message: `Cesar claimed done without running the gate (${_g.command}) \u2014 nudging to verify (${_continuations}/${MAX_CONTINUATIONS}).` });
12662
+ dispatch({ type: "spinner-start", message: `${cesarEngineId} verifying\u2026`, color });
12663
+ let _gateResp = "";
12664
+ try {
12665
+ const _gateGen = session.send({
12666
+ message: _gateNudge,
12667
+ signal: abort.signal,
12668
+ toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
12669
+ toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
12670
+ });
12671
+ for await (const _c of _gateGen) {
12672
+ if (abort.signal.aborted) break;
12673
+ if (_c.type === "text") _gateResp += _c.content;
12674
+ if (_c.type === "error" && !_gateResp.trim()) {
12675
+ _engineErrored = true;
12676
+ _engineErrorMsg = String(_c.content ?? "").slice(0, 300);
12677
+ }
12678
+ if (_c.type === "done" || _c.type === "error") break;
12679
+ }
12680
+ } catch (gateErr) {
12681
+ dispatch({ type: "spinner-stop" });
12682
+ _engineErrored = true;
12683
+ _engineErrorMsg = gateErr instanceof Error ? gateErr.message : String(gateErr);
12684
+ break;
12685
+ }
12686
+ dispatch({ type: "spinner-stop" });
12687
+ if (_engineErrored) break;
12688
+ const _cleanGate = _gateResp.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim();
12689
+ try {
12690
+ const _parsedGate = parseToolCalls(_cleanGate);
12691
+ if (_parsedGate.hasToolCalls && toolRegistry) {
12692
+ const _gateResult = await runToolLoop(
12693
+ async (message) => {
12694
+ if (!session.alive || abort.signal.aborted) return "";
12695
+ _engineErrored = false;
12696
+ _engineErrorMsg = "";
12697
+ let _nr = "";
12698
+ const _g2 = session.send({ message, signal: abort.signal, toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0, toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0 });
12699
+ for await (const _ch of _g2) {
12700
+ if (_ch.type === "text") _nr += _ch.content;
12701
+ if (_ch.type === "error" && !_nr.trim()) {
12702
+ _engineErrored = true;
12703
+ _engineErrorMsg = String(_ch.content ?? "").slice(0, 300);
12704
+ }
12705
+ if (_ch.type === "done" || _ch.type === "error") break;
12706
+ }
12707
+ return _nr.trim() || (_engineErrorMsg ? `[Engine error: ${_engineErrorMsg}]` : "[No response from engine]");
12708
+ },
12709
+ _cleanGate,
12710
+ toolCtx,
12711
+ toolRegistry,
12712
+ _buildContToolLoopOpts()
12713
+ );
12714
+ response = response + "\n\n" + (_gateResult.finalText?.trim() ?? "");
12715
+ _toolCallTurns += _gateResult.turns ?? 0;
12716
+ } else if (_cleanGate) {
12717
+ dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: _cleanGate });
12718
+ response = response + "\n\n" + _cleanGate;
12719
+ }
12720
+ } catch {
12721
+ if (_cleanGate) {
12722
+ dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: _cleanGate });
12723
+ response = response + "\n\n" + _cleanGate;
12724
+ }
12725
+ }
12726
+ _prevToolCount = _toolsUsed.length;
12727
+ if (ctx.cesar) ctx.cesar.gateNudgedClaim = _doneClaimSignature(response);
12728
+ continue;
12729
+ }
12730
+ break;
12731
+ }
12732
+ const wroteFiles = _toolsUsed.some((t) => isWriteToolName(t));
11482
12733
  const filePathRe = /(?:^|\s|`)((?:\.{1,2}\/|~\/|\/|(?:packages|src|tests|scripts|kern)\/)[\w./-]+\.(?:ts|tsx|kern|js|jsx|py|md|json|yaml|yml|toml|sh|css|scss|html))\b/g;
11483
12734
  const filesMentioned = Array.from(new Set(Array.from(response.matchAll(filePathRe), (m) => m[1]))).slice(0, 3);
11484
12735
  let nudge;
@@ -11514,7 +12765,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11514
12765
  break;
11515
12766
  }
11516
12767
  dispatch({ type: "spinner-stop" });
11517
- const cleanCont = contResponse.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim();
12768
+ const cleanCont = emitLiveTodos(emitPreamble(contResponse.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim()));
11518
12769
  if (_engineErrored) {
11519
12770
  const _reason = _engineErrorMsg || "no response";
11520
12771
  dispatch({ type: "warning", message: `Cesar (${cesarEngineId}) stopped \u2014 engine did not respond: ${_reason.slice(0, 160)}. Try again, /compact, or switch engine with /engine.` });
@@ -11535,12 +12786,13 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11535
12786
  async (message) => {
11536
12787
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
11537
12788
  if (!session.alive || abort.signal.aborted) return "";
12789
+ const sendMessage = drainSteeringIntoSend(message, "auto");
11538
12790
  dispatch({ type: "spinner-start", message: "Cesar executing\u2026", color });
11539
12791
  _engineErrored = false;
11540
12792
  _engineErrorMsg = "";
11541
12793
  let nextResp = "";
11542
12794
  const gen = session.send({
11543
- message,
12795
+ message: sendMessage,
11544
12796
  signal: abort.signal,
11545
12797
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
11546
12798
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11637,6 +12889,9 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11637
12889
  message: _readHeavy ? `Cesar made ${_turnToolEvents} tool calls this turn (${_readToolEventCount} reads) \u2014 read-heavy, may be searching in circles. Consider /compact or a more specific instruction.` : `Cesar made ${_turnToolEvents} tool calls this turn \u2014 unusually high. If it felt stuck, /compact to shrink context or re-prompt more specifically.`
11638
12890
  });
11639
12891
  }
12892
+ if (previewShown && !streaming) {
12893
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
12894
+ }
11640
12895
  if (!streaming && response && !ranToolLoop && !wasStreamed) {
11641
12896
  dispatch({ type: "spinner-stop" });
11642
12897
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: response });
@@ -11655,7 +12910,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11655
12910
  appendMessage(ctx.chatSession, { role: "engine", engineId: ch.engineId, content: ch.content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11656
12911
  }
11657
12912
  }
11658
- const tokenUsage = tracker.record(cesarEngineId, { prompt: input, response });
12913
+ const tokenUsage = recordCesarTurn(ctx, cesarEngineId, input, response);
11659
12914
  try {
11660
12915
  const tracePath = join14(RUNS_DIR, "cesar-trace.jsonl");
11661
12916
  const taskClass = classifyTask(input);
@@ -11687,6 +12942,11 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11687
12942
  confidenceToolUsed: toolTelemetry.confidenceToolUsed,
11688
12943
  hasNativeTools: toolTelemetry.hasNativeTools,
11689
12944
  delegated: false,
12945
+ // Live-todo telemetry: emitted? + whether this was a multi-tool turn
12946
+ // (toolCallTurns>1) — the later-gated metric is multi-tool turns that
12947
+ // emitted ZERO todos, derivable from these two fields.
12948
+ liveTodosEmitted: toolTelemetry.liveTodosEmitted,
12949
+ multiToolTurn: toolTelemetry.toolCallTurns > 1,
11690
12950
  confidence: parsedConfidence,
11691
12951
  tokens: tokenUsage ? { prompt: tokenUsage.promptTokens, response: tokenUsage.responseTokens, cost: tokenUsage.costUsd } : void 0
11692
12952
  }) + "\n");
@@ -11802,6 +13062,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11802
13062
  ctx.cesar.abortSignal = null;
11803
13063
  ctx.cesar.turnId = void 0;
11804
13064
  ctx.setActiveAbort(null);
13065
+ releaseSteeringTurn(_turnId);
11805
13066
  const queued = ctx.cesar.queue;
11806
13067
  if (queued) {
11807
13068
  ctx.cesar.queue = null;
@@ -11833,6 +13094,7 @@ export {
11833
13094
  joinProblemInput,
11834
13095
  runThinkChain,
11835
13096
  runDelegate,
13097
+ runPrText,
11836
13098
  goalDir,
11837
13099
  loadJournal,
11838
13100
  isTestFile,
@@ -11863,15 +13125,20 @@ export {
11863
13125
  clearPermissionQueue,
11864
13126
  clearThinkingBuffer,
11865
13127
  handleOutputEvent,
13128
+ yieldToInk,
13129
+ replayCesarHarnessLogs,
11866
13130
  parseConfidence,
11867
13131
  confidenceBadge,
11868
13132
  parseSuggestion,
11869
- yieldToInk,
11870
- replayCesarHarnessLogs,
13133
+ onSteeringChange,
13134
+ pushSteering,
13135
+ peekSteeringCount,
13136
+ drainLeftoverSteering,
13137
+ clearSteering,
13138
+ handleCesarBrain,
11871
13139
  buildCesarSystemPrompt,
11872
13140
  saveCesarConversationSnapshot,
11873
13141
  resolveCesarBackend,
11874
- ensureCesarSession,
11875
- handleCesarBrain
13142
+ ensureCesarSession
11876
13143
  };
11877
- //# sourceMappingURL=chunk-GMVFKWQA.js.map
13144
+ //# sourceMappingURL=chunk-24EWX243.js.map