@kernlang/agon 0.1.7 → 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-AONHRJRW.js → chunk-24EWX243.js} +1394 -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-RKXVKX25.js → chunk-6SOOHJZQ.js} +75 -13
  9. package/dist/chunk-6SOOHJZQ.js.map +1 -0
  10. package/dist/{chunk-6WWOJXG4.js → chunk-FORBHCTM.js} +41 -7
  11. package/dist/chunk-FORBHCTM.js.map +1 -0
  12. package/dist/{chunk-WDT5NJOA.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-O2SJ5JIQ.js → forge-ZFCSXC3Z.js} +6 -6
  22. package/dist/index.js +16659 -14480
  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-ODAAXWOD.js → update-H3JQXPGO.js} +6 -6
  33. package/package.json +3 -3
  34. package/dist/chunk-6WWOJXG4.js.map +0 -1
  35. package/dist/chunk-AONHRJRW.js.map +0 -1
  36. package/dist/chunk-BPKY4OF2.js.map +0 -1
  37. package/dist/chunk-RKXVKX25.js.map +0 -1
  38. package/dist/chunk-WDT5NJOA.js.map +0 -1
  39. package/dist/plan-mode-PFLUPGSY.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-O2SJ5JIQ.js.map → forge-ZFCSXC3Z.js.map} +0 -0
  43. /package/dist/{plan-mode-PFLUPGSY.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-ODAAXWOD.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) {
@@ -8869,6 +9520,7 @@ RULE 2b \u2014 INTAKE FLOW: Every normal turn includes ROUTING CONTEXT with INTA
8869
9520
  forge-slice / forge-full: define the target clearly first; forge only the hard slice or full broad task that benefits from competing implementations.
8870
9521
  brainstorm / tribunal / campfire / review: call the matching orchestration tool directly when that is the recommended flow.
8871
9522
  answer: answer directly.
9523
+ RULE 2c \u2014 MODE IS A CHOICE, NOT A REFLEX (decisions, not retrieval): Before firing ANY orchestration mode, ask what the task actually needs, then pick the mode that fits \u2014 never the one your reflex reaches for. Orchestration (Brainstorm/Tribunal/Campfire/Council) exists to resolve TRADEOFFS and DECISIONS: competing approaches, contested correctness, hard-to-reverse calls. It is NOT a way to FIND an answer you can produce yourself by reading code. If the ask is a lookup, an analysis, or already in hand \u2192 answer directly; convening a panel to discover what one investigation would reveal burns the user's tokens and time. "Find me X / are there issues in Y / how does Z work" = investigate and answer, NOT brainstorm. Escalate by stakes and reversibility: QuickNero = one adversary \xB7 Tribunal = two-side debate \xB7 Council = whole-panel + chair \xB7 Forge = competitive build. When the user explicitly names a mode ("brainstorm it", "tribunal this"), honor it \u2014 this rule governs the turns where YOU own the routing choice.
8872
9524
 
8873
9525
  CRITICAL: You have orchestration modes as DIRECT TOOL CALLS. NEVER use Bash to run CLI commands for orchestration. Call the tools directly:
8874
9526
 
@@ -8956,8 +9608,15 @@ RULE 4b \u2014 MODE PURPOSE (don't mix them up \u2014 and don't always default t
8956
9608
  Your code will be auto-reviewed after implementation \u2014 write carefully the first time.
8957
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.
8958
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.
8959
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.
8960
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.
8961
9620
 
8962
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.
8963
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.
@@ -8975,7 +9634,12 @@ RULE 10 \u2014 TURN CLOSURE: End every turn with one clear closing line so the u
8975
9634
 
8976
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.
8977
9636
 
8978
- 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).`;
8979
9643
  function buildCesarSystemPrompt(ctx) {
8980
9644
  const config = ctx.config;
8981
9645
  const cesarCwd = resolveWorkingDir();
@@ -9007,6 +9671,18 @@ MODES vs ENGINES: the names above are ENGINES \u2014 runnable backends you deleg
9007
9671
  systemParts.push(`## OPERATING MODE
9008
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.`);
9009
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
+ }
9010
9686
  if (ctx.cesarMemory) {
9011
9687
  const memoryCtx = ctx.cesarMemory.toPromptContext();
9012
9688
  if (memoryCtx) systemParts.push(memoryCtx);
@@ -9033,14 +9709,14 @@ ${fragments.join("\n")}`);
9033
9709
  } else {
9034
9710
  const stats = tracker.getStats();
9035
9711
  let budgetWarning = "";
9036
- if (stats.totalCostUsd > 0.5) {
9712
+ if (stats.meteredCostUsd > 0.5) {
9037
9713
  budgetWarning = `
9038
9714
 
9039
- URGENT: Planning has spent $${stats.totalCostUsd.toFixed(2)}. Stop investigating and decide NOW \u2014 call ProposePlan or ExitPlanMode.`;
9040
- } 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) {
9041
9717
  budgetWarning = `
9042
9718
 
9043
- 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.`;
9044
9720
  }
9045
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.
9046
9722
 
@@ -9146,6 +9822,16 @@ function capSnapshotMessageContent(content) {
9146
9822
  return `${content.slice(0, CESAR_SNAPSHOT_MSG_CHAR_CAP)}
9147
9823
  \u2026 [${content.length - CESAR_SNAPSHOT_MSG_CHAR_CAP} chars truncated for Cesar context]`;
9148
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
+ }
9149
9835
  function buildCesarConversationSnapshot(session, chatSession) {
9150
9836
  const directHistory = session?.getMessageHistory?.() ?? [];
9151
9837
  if (directHistory.length > 0) {
@@ -9196,12 +9882,20 @@ function buildOnToolCall(ctx, toolRegistry, config) {
9196
9882
  allowedCommands: config.allowedCommands ?? [],
9197
9883
  toolPermissions: config.toolPermissions ?? {},
9198
9884
  sessionAllowList: getSessionAllowList(),
9199
- 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)
9200
9894
  };
9201
9895
  return async (name, args, callId) => {
9202
9896
  const activePlan = ctx.activePlan;
9203
9897
  if (activePlan && ["planning", "awaiting_approval"].includes(activePlan.state)) {
9204
- const BLOCKED_IN_PLAN = ["Forge", "Pipeline", "Agent", "Goal", "Edit", "Write"];
9898
+ const BLOCKED_IN_PLAN = ["Forge", "Pipeline", "Agent", "Goal", "Edit", "Write", "MultiEdit"];
9205
9899
  if (BLOCKED_IN_PLAN.includes(name)) {
9206
9900
  return `[BLOCKED] Tool "${name}" is not available in plan mode. Use ProposePlan to propose your execution strategy.`;
9207
9901
  }
@@ -9268,7 +9962,7 @@ ${cleaned}`;
9268
9962
  }
9269
9963
  }
9270
9964
  if (name === "ExitPlanMode") {
9271
- const { handleExitPlanMode } = await import("./plan-mode-PFLUPGSY.js");
9965
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
9272
9966
  return "[DELEGATION_BREAK] " + handleExitPlanMode(String(args.reason ?? ""), ctx.cesar?.planDispatch ?? null, ctx);
9273
9967
  }
9274
9968
  if (name === "ProposePlan") {
@@ -9291,7 +9985,7 @@ ${cleaned}`;
9291
9985
  }
9292
9986
  }
9293
9987
  }
9294
- const { handleProposePlan } = await import("./plan-mode-PFLUPGSY.js");
9988
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
9295
9989
  const dispatch = ctx.cesar.planDispatch;
9296
9990
  if (!dispatch) {
9297
9991
  return "[PLAN_ERROR] Internal plan display dispatch unavailable. Retry the plan request so Agon can render the approval panel.";
@@ -9357,8 +10051,15 @@ ${cleaned}`;
9357
10051
  return new Promise((resolve4) => {
9358
10052
  const d = ctx.cesar.lastDispatch;
9359
10053
  if (d) {
9360
- const cmd = args.command ?? args.file_path ?? JSON.stringify(args);
9361
- 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 });
9362
10063
  } else {
9363
10064
  resolve4(true);
9364
10065
  }
@@ -9366,11 +10067,15 @@ ${cleaned}`;
9366
10067
  }
9367
10068
  );
9368
10069
  let output = result.result.ok ? result.result.content : result.result.error ?? "Tool execution failed";
9369
- 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) {
9370
10072
  const diag = buildToolErrorDiagnostic(name, args, result.result.error);
9371
10073
  const retryKey = `${name}:${JSON.stringify(args)}`;
9372
10074
  const used = nativeToolErrorRetries.get(retryKey) ?? 0;
9373
- 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) {
9374
10079
  nativeToolErrorRetries.set(retryKey, 1);
9375
10080
  output = `[RETRYABLE_TOOL_ERROR] ${diag}
9376
10081
  Retry this ${name} call ONCE with corrected input that matches the tool's schema. Do not narrate before retrying.`;
@@ -9383,7 +10088,7 @@ Repair retry already used for this exact ${name} input in this turn. Stop retryi
9383
10088
  }
9384
10089
  if (CACHEABLE_TOOLS.has(name)) {
9385
10090
  toolResultCache.set(cacheKey, output);
9386
- } else if (["Edit", "Write", "Bash"].includes(name)) {
10091
+ } else if (["Edit", "Write", "MultiEdit", "Bash"].includes(name)) {
9387
10092
  toolResultCache.clear();
9388
10093
  ctx.cesar.blockedOnConfidence = null;
9389
10094
  }
@@ -9407,9 +10112,12 @@ function buildOnApproval(ctx, engineId) {
9407
10112
  const perms = cfg.toolPermissions ?? {};
9408
10113
  const allowed = cfg.allowedCommands ?? [];
9409
10114
  const mode = cfg.permissionMode ?? "ask";
9410
- 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" };
9411
10116
  const agonTool = toolMap[tool.toLowerCase()] ?? tool;
9412
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);
9413
10121
  const turnId = ctx.cesar?.turnId;
9414
10122
  const cwd = resolveWorkingDir();
9415
10123
  const logApproval = (decision, source, reason, args) => {
@@ -9444,7 +10152,7 @@ function buildOnApproval(ctx, engineId) {
9444
10152
  }
9445
10153
  };
9446
10154
  if (ctx.explorationMode) {
9447
- const WRITE_TOOLS = ["Edit", "Write", "Bash"];
10155
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit", "Bash"];
9448
10156
  if (WRITE_TOOLS.includes(agonTool)) {
9449
10157
  logApproval("blocked", "policy.exploration", "exploration mode is read-only");
9450
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.";
@@ -9460,7 +10168,7 @@ function buildOnApproval(ctx, engineId) {
9460
10168
  logApproval("blocked", "policy.plan-mode", "plan mode blocks mutating Bash");
9461
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.";
9462
10170
  }
9463
- const WRITE_TOOLS = ["Edit", "Write"];
10171
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit"];
9464
10172
  if (WRITE_TOOLS.includes(agonTool)) {
9465
10173
  logApproval("blocked", "policy.plan-mode", "plan mode blocks file writes");
9466
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.";
@@ -9486,7 +10194,7 @@ function buildOnApproval(ctx, engineId) {
9486
10194
  }
9487
10195
  }
9488
10196
  if (!ctx.cesar.confidenceSatisfied) {
9489
- const WRITE_TOOLS = ["Edit", "Write"];
10197
+ const WRITE_TOOLS = ["Edit", "Write", "MultiEdit"];
9490
10198
  if (WRITE_TOOLS.includes(agonTool)) {
9491
10199
  const blocks = (ctx.cesar.confidenceBlockCount ?? 0) + 1;
9492
10200
  ctx.cesar.confidenceBlockCount = blocks;
@@ -9499,11 +10207,19 @@ function buildOnApproval(ctx, engineId) {
9499
10207
  ctx.cesar.blockedOnConfidence = null;
9500
10208
  }
9501
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
+ }
9502
10214
  if (perm === "deny" || mode === "deny-all") {
9503
10215
  logApproval("denied", perm === "deny" ? "settings.toolPermissions" : "settings.permissionMode", perm === "deny" ? `${agonTool} denied in settings` : "permissionMode=deny-all");
9504
10216
  return false;
9505
10217
  }
9506
- 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") {
9507
10223
  const approvalCwd = resolveWorkingDir();
9508
10224
  const approvalCache = getProjectFileStateCache(approvalCwd);
9509
10225
  const approvalArgs = approvalArgsFromCommand(agonTool, command);
@@ -9551,7 +10267,14 @@ function buildOnApproval(ctx, engineId) {
9551
10267
  const dispatch = ctx.cesar.lastDispatch;
9552
10268
  if (dispatch) {
9553
10269
  logApproval("prompted", "user-prompt", `Cesar (${engineId}) wants to execute`);
9554
- 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) => {
9555
10278
  const wasApproved = typeof approved === "string" ? approved === "y" || approved === "a" : !!approved;
9556
10279
  logApproval(wasApproved ? "approved" : "denied", "user-prompt", wasApproved ? "user approved" : "user denied");
9557
10280
  resolve4(wasApproved);
@@ -9595,7 +10318,7 @@ function loadCesarMcpServers(config, cwd) {
9595
10318
  const resolvedPath = isAbsolute2(rawPath) ? rawPath : resolve3(cwd, rawPath);
9596
10319
  let parsed;
9597
10320
  try {
9598
- parsed = JSON.parse(readFileSync12(resolvedPath, "utf-8"));
10321
+ parsed = JSON.parse(readFileSync13(resolvedPath, "utf-8"));
9599
10322
  } catch (err) {
9600
10323
  throw new Error(`Failed to load Cesar MCP config at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`);
9601
10324
  }
@@ -9619,7 +10342,7 @@ function mcpConfigFingerprint(config) {
9619
10342
  if (enabled && configPath) {
9620
10343
  try {
9621
10344
  const resolvedPath = isAbsolute2(configPath) ? configPath : resolve3(resolveWorkingDir(), configPath);
9622
- mtime = String(statSync4(resolvedPath).mtimeMs);
10345
+ mtime = String(statSync5(resolvedPath).mtimeMs);
9623
10346
  } catch {
9624
10347
  }
9625
10348
  }
@@ -9630,17 +10353,17 @@ function resolveAgonMcpServerPath(fromUrl) {
9630
10353
  const raw = fromUrl ?? import.meta.url;
9631
10354
  const url = raw.startsWith("file:") ? raw : pathToFileURL(raw).href;
9632
10355
  const bundledSibling = join12(dirname5(fileURLToPath(url)), "mcp", "index.js");
9633
- if (existsSync12(bundledSibling)) return bundledSibling;
10356
+ if (existsSync13(bundledSibling)) return bundledSibling;
9634
10357
  try {
9635
10358
  const req = createRequire(url);
9636
10359
  const resolved = req.resolve("@kernlang/agon-mcp");
9637
- if (existsSync12(resolved)) return resolved;
10360
+ if (existsSync13(resolved)) return resolved;
9638
10361
  } catch {
9639
10362
  }
9640
10363
  let dir = dirname5(fileURLToPath(url));
9641
10364
  for (let i = 0; i < 12; i++) {
9642
10365
  const cand = join12(dir, "packages", "mcp", "dist", "index.js");
9643
- if (existsSync12(cand)) return cand;
10366
+ if (existsSync13(cand)) return cand;
9644
10367
  const parent = dirname5(dir);
9645
10368
  if (parent === dir) break;
9646
10369
  dir = parent;
@@ -9759,7 +10482,7 @@ async function ensureCesarSession(ctx) {
9759
10482
  mkdirSync11(signalDir, { recursive: true });
9760
10483
  const sessionSignalId = `cesar-${Date.now()}`;
9761
10484
  const mcpServerPath = resolveAgonMcpServerPath();
9762
- if (!existsSync12(mcpServerPath)) {
10485
+ if (!existsSync13(mcpServerPath)) {
9763
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.`);
9764
10487
  }
9765
10488
  const mcpEnv = { AGON_SIGNAL_DIR: signalDir, AGON_SESSION_ID: sessionSignalId };
@@ -9810,9 +10533,177 @@ async function ensureCesarSession(ctx) {
9810
10533
  await session.start();
9811
10534
  ctx.setCesarSession(session);
9812
10535
  ctx.cesar.mcpFingerprint = currentMcpFp;
10536
+ ctx.cesar.budgetWarned = false;
9813
10537
  return session;
9814
10538
  }
9815
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
+
9816
10707
  // src/generated/cesar/escalation.ts
9817
10708
  import { join as join13 } from "path";
9818
10709
  import { mkdirSync as mkdirSync12 } from "fs";
@@ -9882,18 +10773,94 @@ async function promptDelegation(action, dispatch, hardened, tribunalMode, team)
9882
10773
  return { approved: answer === "y" };
9883
10774
  }
9884
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
+
9885
10851
  // src/generated/cesar/brain.ts
9886
10852
  async function commitTurnAndDelegate(pendingDel, input, response, cesarEngineId, streaming, dispatch, ctx, telemetry) {
9887
10853
  if (streaming) {
9888
10854
  dispatch({ type: "streaming-end", engineId: cesarEngineId });
9889
10855
  }
9890
10856
  if (!streaming) {
10857
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
9891
10858
  dispatch({ type: "spinner-stop" });
9892
10859
  }
9893
10860
  await yieldToInk();
9894
10861
  appendMessage(ctx.chatSession, { role: "user", content: input, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9895
10862
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: response, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9896
- tracker.record(cesarEngineId, { prompt: input, response });
10863
+ recordCesarTurn(ctx, cesarEngineId, input, response);
9897
10864
  const delResult = await promptDelegation(pendingDel.action, dispatch, pendingDel.hardened, pendingDel.tribunalMode, pendingDel.team);
9898
10865
  const happened = buildWhatHappenedSummary(telemetry ?? {});
9899
10866
  if (happened) {
@@ -9915,12 +10882,13 @@ async function commitTurnAndSuggest(suggestion, input, response, cesarEngineId,
9915
10882
  dispatch({ type: "streaming-end", engineId: cesarEngineId });
9916
10883
  }
9917
10884
  if (!streaming) {
10885
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
9918
10886
  dispatch({ type: "spinner-stop" });
9919
10887
  }
9920
10888
  await yieldToInk();
9921
10889
  appendMessage(ctx.chatSession, { role: "user", content: input, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9922
10890
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: response, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9923
- tracker.record(cesarEngineId, { prompt: input, response });
10891
+ recordCesarTurn(ctx, cesarEngineId, input, response);
9924
10892
  if (suggestion.rest) {
9925
10893
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: suggestion.rest });
9926
10894
  }
@@ -9957,6 +10925,22 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9957
10925
  }
9958
10926
  const _toolsUsed = [];
9959
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
+ };
9960
10944
  let _toolEventCount = 0;
9961
10945
  let _readToolEventCount = 0;
9962
10946
  let _toolCallTurns = 0;
@@ -9966,6 +10950,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9966
10950
  let _narratedToolStalls = 0;
9967
10951
  let _autoToolExecutions = 0;
9968
10952
  let _confidenceToolUsed = false;
10953
+ let _liveTodosEmitted = false;
9969
10954
  let _actualCesarEngineId = "";
9970
10955
  let _actualCesarBackend = "unknown";
9971
10956
  let _actualHasNativeTools = false;
@@ -9978,6 +10963,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
9978
10963
  };
9979
10964
  const recordToolUse = (name, source, input2, status) => {
9980
10965
  const toolName = String(name || "tool");
10966
+ _noteBashForGate(toolName, input2);
9981
10967
  const normalizedSource = source === "eager" ? "xml" : source === "auto" ? "native" : source;
9982
10968
  const key = `${normalizedSource}:${toolName}:${String(input2 ?? "").slice(0, 500)}`;
9983
10969
  _toolEventCount++;
@@ -10002,6 +10988,45 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10002
10988
  input: input2
10003
10989
  });
10004
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
+ };
10005
11030
  const normalizeConfidenceReasoning = (value) => {
10006
11031
  return String(value ?? "").replace(/\s+/g, " ").trim();
10007
11032
  };
@@ -10028,7 +11053,8 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10028
11053
  xmlToolCalls: _xmlToolCalls,
10029
11054
  narratedToolStalls: _narratedToolStalls,
10030
11055
  autoToolExecutions: _autoToolExecutions,
10031
- confidenceToolUsed: _confidenceToolUsed
11056
+ confidenceToolUsed: _confidenceToolUsed,
11057
+ liveTodosEmitted: _liveTodosEmitted
10032
11058
  });
10033
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;
10034
11060
  const _isFollowUp = FOLLOWUP_RE.test(input.trim());
@@ -10064,11 +11090,6 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10064
11090
  ctx.cesar.queue = null;
10065
11091
  ctx.cesar.abortSignal = null;
10066
11092
  } else {
10067
- if (_isFollowUp) {
10068
- const elapsed = Math.round((Date.now() - busySince) / 1e3);
10069
- dispatch({ type: "info", message: `Cesar still working\u2026 ${elapsed}s` });
10070
- return { delegated: false, responded: true };
10071
- }
10072
11093
  const existing = ctx.cesar.queue;
10073
11094
  if (existing) {
10074
11095
  existing.input = existing.input + "\n\n" + input;
@@ -10093,6 +11114,7 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10093
11114
  ctx.cesar.searchNudged = false;
10094
11115
  ctx.cesar.turnId = _turnId;
10095
11116
  ctx.cesar.planDispatch = dispatch;
11117
+ markSteeringTurn(_turnId);
10096
11118
  const _brainStartMs = Date.now();
10097
11119
  if (ctx.eventBus) await ctx.eventBus.emit("pre:cesar-brain", { input });
10098
11120
  try {
@@ -10105,6 +11127,33 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10105
11127
  }
10106
11128
  const cesarEngineId = config.cesarEngine ?? config.forgeFixedStarter ?? "claude";
10107
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
+ };
10108
11157
  recordTimeline({
10109
11158
  event: "turn_start",
10110
11159
  engineId: cesarEngineId,
@@ -10125,9 +11174,26 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10125
11174
  ctx.setActiveAbort(abort);
10126
11175
  ctx.cesar.abortSignal = abort.signal;
10127
11176
  ctx.cesar.lastDispatch = dispatch;
11177
+ dispatch({ type: "todos-clear", scope: "live" });
10128
11178
  dispatch({ type: "confidence-update", value: null });
10129
11179
  dispatch({ type: "spinner-start", message: "Cesar thinking\u2026", color });
10130
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
+ }
10131
11197
  let session;
10132
11198
  try {
10133
11199
  session = await ensureCesarSession(ctx);
@@ -10160,14 +11226,18 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10160
11226
  if (freshResult.stdout.trim()) {
10161
11227
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: freshResult.stdout.trim() });
10162
11228
  appendMessage(ctx.chatSession, { role: "engine", engineId: cesarEngineId, content: freshResult.stdout.trim(), timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10163
- 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
+ }
10164
11234
  return { delegated: false, responded: true };
10165
11235
  }
10166
11236
  appendUserTurnIfAbsent(ctx.chatSession, input);
10167
11237
  const brainHint = (freshResult.stderr || "").split("\n")[0].slice(0, 200).trim();
10168
11238
  if (brainHint) dispatch({ type: "warning", message: `Cesar (${cesarEngineId}) returned no response: ${brainHint}` });
10169
11239
  const health = engineHealth.get(cesarEngineId);
10170
- if (health && (health.status === "auth-failed" || health.status === "unreachable")) {
11240
+ if (health && (health.status === "auth-failed" || health.status === "unreachable" || health.status === "binary-missing")) {
10171
11241
  dispatch({ type: "warning", message: `Engine ${cesarEngineId} marked ${health.status} \u2014 run /cesar to switch to a healthy engine, or /engines to fix credentials.` });
10172
11242
  }
10173
11243
  return { delegated: false, responded: false };
@@ -10185,17 +11255,21 @@ async function handleCesarBrain(input, dispatch, ctx, images) {
10185
11255
  let response = "";
10186
11256
  let streaming = false;
10187
11257
  let wasStreamed = false;
11258
+ let previewShown = false;
10188
11259
  let parsedConfidence = null;
10189
11260
  let confidenceParsed = false;
10190
11261
  let insideThinkBlock = false;
10191
11262
  let suppressXmlToolDisplay = false;
10192
11263
  let sawStreamingXmlToolCall = false;
10193
11264
  let xmlDisplayHold = "";
11265
+ const stripTodosForDisplay = createTodosDisplayStripper();
11266
+ const stripPreambleForDisplay = createPreambleStripper();
10194
11267
  let hadToolActivity = false;
10195
11268
  let _engineErrored = false;
10196
11269
  let _engineErrorMsg = "";
10197
11270
  let secondOpinionPromise = null;
10198
11271
  let usedQuickNero = false;
11272
+ let _escalationSuggested = false;
10199
11273
  const eagerPromises = [];
10200
11274
  let eagerToolCtx = null;
10201
11275
  const shouldInterruptForXmlTool = () => {
@@ -10341,12 +11415,12 @@ ${enrichedInput}`;
10341
11415
  const signalDir = ctx.cesar.mcpSignalPath ? join14(ctx.cesar.mcpSignalPath, "..") : null;
10342
11416
  const processMcpSideChannel = () => {
10343
11417
  try {
10344
- if (!signalDir || !existsSync13(signalDir)) return;
11418
+ if (!signalDir || !existsSync14(signalDir)) return;
10345
11419
  const completions = readdirSync(signalDir).filter((f) => f.includes("-tool-") && f.endsWith(".json"));
10346
11420
  for (const f of completions) {
10347
11421
  const donePath = join14(signalDir, f);
10348
11422
  try {
10349
- const done = JSON.parse(readFileSync13(donePath, "utf-8"));
11423
+ const done = JSON.parse(readFileSync14(donePath, "utf-8"));
10350
11424
  if (done.type !== "tool-completion") continue;
10351
11425
  try {
10352
11426
  unlinkSync(donePath);
@@ -10356,21 +11430,21 @@ ${enrichedInput}`;
10356
11430
  const status = done.status === "error" ? "error" : "done";
10357
11431
  const toolInput = typeof done.args === "string" ? done.args : JSON.stringify(done.args ?? {});
10358
11432
  recordToolUse(String(done.tool ?? "tool"), "mcp", toolInput, status);
10359
- dispatch({
11433
+ dispatchToolCall({
10360
11434
  type: "tool-call",
10361
11435
  engineId: cesarEngineId,
10362
11436
  tool: String(done.tool ?? "tool"),
10363
11437
  input: toolInput,
10364
11438
  status,
10365
11439
  output: typeof done.output === "string" ? done.output : void 0
10366
- });
11440
+ }, { standalone: true });
10367
11441
  } catch {
10368
11442
  }
10369
11443
  }
10370
11444
  const files = readdirSync(signalDir).filter((f) => f.includes("-perm-") && !f.includes("-response"));
10371
11445
  for (const f of files) {
10372
11446
  const reqPath = join14(signalDir, f);
10373
- const req = JSON.parse(readFileSync13(reqPath, "utf-8"));
11447
+ const req = JSON.parse(readFileSync14(reqPath, "utf-8"));
10374
11448
  if (req.type !== "permission-request") continue;
10375
11449
  if (Date.now() - req.timestamp > 65e3) {
10376
11450
  try {
@@ -10380,7 +11454,7 @@ ${enrichedInput}`;
10380
11454
  continue;
10381
11455
  }
10382
11456
  const respPath = reqPath.replace(".json", "-response.json");
10383
- if (existsSync13(respPath)) continue;
11457
+ if (existsSync14(respPath)) continue;
10384
11458
  const cfg = loadConfig();
10385
11459
  const allowed = cfg.allowedCommands ?? [];
10386
11460
  const cmdBase = (req.args?.command ?? "").toString().trim().split(/\s+/)[0];
@@ -10413,12 +11487,30 @@ ${enrichedInput}`;
10413
11487
  input: reqArgs
10414
11488
  });
10415
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
+ }
10416
11508
  if (cmdBase && allowed.some((a) => cmdBase.toLowerCase().startsWith(a.toLowerCase()))) {
10417
11509
  logMcpApproval("approved", "mcp.allowedCommands", "command matched allowedCommands");
10418
11510
  writeFileSync11(respPath, JSON.stringify({ type: "permission-response", id: req.id, approved: true }));
10419
11511
  continue;
10420
11512
  }
10421
- if (reqTool === "Edit" || reqTool === "Write") {
11513
+ if (reqTool === "Edit" || reqTool === "Write" || reqTool === "MultiEdit") {
10422
11514
  const approvalCwd = resolveWorkingDir();
10423
11515
  const approvalCache = getProjectFileStateCache(approvalCwd);
10424
11516
  const activePlan = ctx.activePlan;
@@ -10440,7 +11532,16 @@ ${enrichedInput}`;
10440
11532
  }
10441
11533
  }
10442
11534
  logMcpApproval("prompted", "mcp.user-prompt", "Cesar wants to execute");
10443
- 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) => {
10444
11545
  const wasApproved = typeof approved === "string" ? approved === "y" || approved === "a" : approved;
10445
11546
  logMcpApproval(wasApproved ? "approved" : "denied", "mcp.user-prompt", wasApproved ? "user approved" : "user denied");
10446
11547
  if (typeof approved === "string" && approved === "a" || approved === true) {
@@ -10469,6 +11570,13 @@ ${enrichedInput}`;
10469
11570
  const gen = session.send(sendOptions);
10470
11571
  for await (const chunk of gen) {
10471
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
+ }
10472
11580
  if (chunk.type === "status") {
10473
11581
  const statusText = String(chunk.content ?? "");
10474
11582
  const _ctxMeta = chunk.metadata ?? {};
@@ -10479,7 +11587,8 @@ ${enrichedInput}`;
10479
11587
  used: Number(_ctxMeta.used ?? 0),
10480
11588
  limit: Number(_ctxMeta.limit ?? 0),
10481
11589
  compacted: Number(_ctxMeta.compacted ?? 0),
10482
- cached: Number(_ctxMeta.cached ?? 0)
11590
+ cached: Number(_ctxMeta.cached ?? 0),
11591
+ source: typeof _ctxMeta.source === "string" ? _ctxMeta.source : void 0
10483
11592
  });
10484
11593
  continue;
10485
11594
  }
@@ -10506,7 +11615,7 @@ ${enrichedInput}`;
10506
11615
  dispatch({ type: "spinner-update", message: `Cesar: ${toolName}\u2026` });
10507
11616
  if (meta.input && STREAM_ORCH.has(toolName)) {
10508
11617
  if (cesarFastPath) {
10509
- 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.` });
10510
11619
  continue;
10511
11620
  }
10512
11621
  if (!ctx.cesar.pendingDelegation) {
@@ -10514,35 +11623,35 @@ ${enrichedInput}`;
10514
11623
  ctx.eventBus?.emit("cesar:delegation", { action: toolName.toLowerCase(), source: `stream-${toolStatus}` }).catch(() => {
10515
11624
  });
10516
11625
  }
10517
- 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 });
10518
11627
  continue;
10519
11628
  }
10520
11629
  if (toolStatus === "done") {
10521
- 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 });
10522
11631
  } else if (toolStatus === "native") {
10523
- 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 });
10524
11633
  } else if (toolStatus === "running" && meta.input && toolRegistry && !ctx.cesar.hasNativeTools) {
10525
11634
  if (ctx.cesar.pendingDelegation) {
10526
- 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 });
10527
11636
  continue;
10528
11637
  }
10529
11638
  const EAGER_ORCH = STREAM_ORCH;
10530
11639
  if (EAGER_ORCH.has(toolName)) {
10531
11640
  if (cesarFastPath) {
10532
- 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.` });
10533
11642
  continue;
10534
11643
  }
10535
11644
  ctx.cesar.pendingDelegation = extractDelegation(toolName, meta.input ?? {});
10536
11645
  ctx.eventBus?.emit("cesar:delegation", { action: toolName.toLowerCase(), source: "stream" }).catch(() => {
10537
11646
  });
10538
- 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" });
10539
11648
  continue;
10540
11649
  }
10541
- 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 });
10542
11651
  if (!eagerToolCtx) eagerToolCtx = createEagerToolContext(ctx, config, abort.signal, dispatch);
10543
11652
  eagerPromises.push(executeEagerTool(toolName, meta, toolRegistry, eagerToolCtx, dispatch, cesarEngineId));
10544
11653
  } else {
10545
- 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 });
10546
11655
  }
10547
11656
  continue;
10548
11657
  }
@@ -10556,6 +11665,7 @@ ${enrichedInput}`;
10556
11665
  const errBody = _errFull.slice(0, 200) || "unknown stream error";
10557
11666
  const _deterministic = /\b(?:400|401|403|404)\b|not found|unauthorized|invalid api key|authentication|no such (?:route|endpoint)/i.test(_errFull);
10558
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 });
10559
11669
  clearInterval(heartbeat);
10560
11670
  processMcpSideChannel();
10561
11671
  if (mcpWatcherInterval) clearInterval(mcpWatcherInterval);
@@ -10623,19 +11733,29 @@ ${enrichedInput}`;
10623
11733
  cleanFirst = response.replace(/<think>[\s\S]*/gi, "");
10624
11734
  }
10625
11735
  }
11736
+ emitPreamble(response);
11737
+ cleanFirst = stripPreambleForDisplay(cleanFirst);
10626
11738
  if (!ctx.cesar.hasNativeTools) {
10627
11739
  const split = splitBeforeToolMarkup(cleanFirst);
10628
11740
  cleanFirst = split.visible;
10629
11741
  if (split.hasToolMarkup) suppressXmlToolDisplay = true;
11742
+ } else {
11743
+ cleanFirst = stripTodosForDisplay(cleanFirst);
10630
11744
  }
10631
11745
  if (cleanFirst.trim()) dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: cleanFirst });
10632
11746
  } else {
10633
11747
  response += chunk.content;
10634
11748
  noteXmlToolDetected(true);
10635
11749
  let displayChunk = chunk.content;
11750
+ emitPreamble(response);
11751
+ displayChunk = stripPreambleForDisplay(displayChunk);
11752
+ if (!displayChunk) continue;
10636
11753
  if (!ctx.cesar.hasNativeTools) {
10637
11754
  displayChunk = takeXmlSafeDisplayChunk(displayChunk);
10638
11755
  if (!displayChunk) continue;
11756
+ } else {
11757
+ displayChunk = stripTodosForDisplay(displayChunk);
11758
+ if (!displayChunk && !insideThinkBlock) continue;
10639
11759
  }
10640
11760
  if (insideThinkBlock) {
10641
11761
  if (displayChunk.includes("</think>")) {
@@ -10669,10 +11789,21 @@ ${enrichedInput}`;
10669
11789
  }
10670
11790
  }
10671
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
+ }
10672
11797
  const trailingXmlSafe = takeXmlSafeDisplayChunk("", true);
10673
11798
  if (streaming && trailingXmlSafe.trim()) {
10674
11799
  dispatch({ type: "streaming-chunk", engineId: cesarEngineId, chunk: trailingXmlSafe });
10675
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
+ }
10676
11807
  } catch (err) {
10677
11808
  clearInterval(heartbeat);
10678
11809
  processMcpSideChannel();
@@ -10683,6 +11814,7 @@ ${enrichedInput}`;
10683
11814
  dispatch({ type: "warning", message: `Cesar stream error (partial response preserved): ${(err.message ?? "").slice(0, 80)}` });
10684
11815
  } else {
10685
11816
  dispatch({ type: "warning", message: "Cesar session error \u2014 will restart on next message" });
11817
+ if (previewShown) dispatch({ type: "streaming-end", engineId: cesarEngineId });
10686
11818
  return { delegated: false, responded: false, decisionReason: "stream-error" };
10687
11819
  }
10688
11820
  }
@@ -10691,6 +11823,7 @@ ${enrichedInput}`;
10691
11823
  if (mcpWatcherInterval) clearInterval(mcpWatcherInterval);
10692
11824
  if (abort.signal.aborted) {
10693
11825
  dispatch({ type: "spinner-stop" });
11826
+ if (previewShown) dispatch({ type: "streaming-end", engineId: cesarEngineId });
10694
11827
  const elapsed = Math.round((Date.now() - _turnStart) / 1e3);
10695
11828
  if (elapsed >= cesarTimeout) {
10696
11829
  dispatch({ type: "warning", message: `Cesar timed out after ${elapsed}s. Try a simpler question, or use /forge for complex tasks.` });
@@ -10703,6 +11836,8 @@ ${enrichedInput}`;
10703
11836
  if (ctx.cesar.hasNativeTools) {
10704
11837
  response = response.replace(/<tool\s+name="[^"]+">[\s\S]*?<\/tool>/g, "").trim();
10705
11838
  }
11839
+ response = emitPreamble(response);
11840
+ response = emitLiveTodos(response);
10706
11841
  if (eagerPromises.length > 0 && !ctx.cesar.hasNativeTools && session.alive && !abort.signal.aborted) {
10707
11842
  dispatch({ type: "spinner-start", message: `Cesar: awaiting ${eagerPromises.length} tool result${eagerPromises.length > 1 ? "s" : ""}\u2026`, color });
10708
11843
  const eagerResults = await Promise.all(eagerPromises);
@@ -10715,7 +11850,8 @@ ${enrichedInput}`;
10715
11850
  const failedTools = eagerFailedToolNames(eagerResults);
10716
11851
  const repairUsed = [];
10717
11852
  const repairResults = [];
10718
- 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 } : {} });
10719
11855
  for await (const chunk of contGen) {
10720
11856
  if (chunk.type === "text") continuation += chunk.content;
10721
11857
  if (chunk.type === "tool_call") {
@@ -10732,9 +11868,9 @@ ${enrichedInput}`;
10732
11868
  continue;
10733
11869
  }
10734
11870
  if (toolStatus !== "running" && toolStatus !== "native") {
10735
- 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 });
10736
11872
  } else if (failedTools.includes(toolName)) {
10737
- 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." });
10738
11874
  }
10739
11875
  }
10740
11876
  if (chunk.type === "done" || chunk.type === "error") break;
@@ -10753,7 +11889,7 @@ ${enrichedInput}`;
10753
11889
  const meta = chunk.metadata ?? {};
10754
11890
  const toolName = chunk.content || "tool";
10755
11891
  const toolInput = typeof meta.input === "string" ? meta.input : meta.input ? JSON.stringify(meta.input) : "";
10756
- 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." });
10757
11893
  }
10758
11894
  if (chunk.type === "done" || chunk.type === "error") break;
10759
11895
  }
@@ -10761,7 +11897,7 @@ ${enrichedInput}`;
10761
11897
  }
10762
11898
  }
10763
11899
  dispatch({ type: "spinner-stop" });
10764
- if (continuation.trim()) response = continuation.trim();
11900
+ if (continuation.trim()) response = emitLiveTodos(emitPreamble(continuation.trim()));
10765
11901
  }
10766
11902
  }
10767
11903
  if (!confidenceParsed && response) {
@@ -10797,22 +11933,22 @@ ${enrichedInput}`;
10797
11933
  if (!ctx.cesar.pendingDelegation && ctx.cesar.mcpSignalPath) {
10798
11934
  try {
10799
11935
  const signalPath = ctx.cesar.mcpSignalPath;
10800
- if (existsSync13(signalPath)) {
10801
- const signals = JSON.parse(readFileSync13(signalPath, "utf-8"));
11936
+ if (existsSync14(signalPath)) {
11937
+ const signals = JSON.parse(readFileSync14(signalPath, "utf-8"));
10802
11938
  unlinkSync(signalPath);
10803
11939
  for (const signal of Array.isArray(signals) ? signals : [signals]) {
10804
11940
  if (!signal.timestamp || Date.now() - signal.timestamp >= 6e4) continue;
10805
11941
  if (cesarFastPath && FAST_PATH_BLOCKED_TOOLS.includes(signal.tool)) {
10806
11942
  const toolInput = JSON.stringify(signal.args ?? {});
10807
11943
  recordToolUse(signal.tool, "mcp", toolInput, "error");
10808
- dispatch({
11944
+ dispatchToolCall({
10809
11945
  type: "tool-call",
10810
11946
  engineId: cesarEngineId,
10811
11947
  tool: signal.tool,
10812
11948
  input: toolInput,
10813
11949
  status: "error",
10814
11950
  output: `Blocked by fast-${fastPathMode}: do the direct work without orchestration.`
10815
- });
11951
+ }, { standalone: true });
10816
11952
  continue;
10817
11953
  }
10818
11954
  if (signal.tool === "ReportConfidence") {
@@ -10842,17 +11978,17 @@ ${enrichedInput}`;
10842
11978
  recordToolUse("ProposePlan", "mcp", JSON.stringify(signal.args ?? {}), "done");
10843
11979
  const activePlan = ctx.activePlan;
10844
11980
  if (activePlan && ["planning", "running", "paused"].includes(activePlan.state)) {
10845
- dispatch({
11981
+ dispatchToolCall({
10846
11982
  type: "tool-call",
10847
11983
  engineId: cesarEngineId,
10848
11984
  tool: "ProposePlan",
10849
11985
  input: JSON.stringify(signal.args ?? {}),
10850
11986
  status: "error",
10851
11987
  output: "A Cesar plan is already active; nested plans are blocked. Resume or cancel the current plan before proposing another."
10852
- });
11988
+ }, { standalone: true });
10853
11989
  continue;
10854
11990
  }
10855
- const { handleProposePlan } = await import("./plan-mode-PFLUPGSY.js");
11991
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
10856
11992
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
10857
11993
  if (planDispatch) {
10858
11994
  try {
@@ -10865,11 +12001,11 @@ ${enrichedInput}`;
10865
12001
  }
10866
12002
  } else if (signal.tool === "ExitPlanMode") {
10867
12003
  recordToolUse("ExitPlanMode", "mcp", JSON.stringify(signal.args ?? {}), "done");
10868
- const { handleExitPlanMode } = await import("./plan-mode-PFLUPGSY.js");
12004
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
10869
12005
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
10870
12006
  try {
10871
12007
  const exitResult = handleExitPlanMode(String(signal.args?.reason ?? ""), planDispatch, ctx);
10872
- 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 });
10873
12009
  } catch (err) {
10874
12010
  console.warn(`[agon] ExitPlanMode via MCP signal failed: ${err instanceof Error ? err.message : String(err)}`);
10875
12011
  }
@@ -10922,7 +12058,14 @@ ${enrichedInput}`;
10922
12058
  // Phase 1: investigate only — mutating tools blocked until after escalation check
10923
12059
  blockedTools: cesarFastPath ? FAST_PATH_BLOCKED_TOOLS : void 0,
10924
12060
  blockedToolMessage: cesarFastPath ? `Blocked by fast-${fastPathMode}: do the direct work without orchestration.` : void 0,
10925
- 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)
10926
12069
  };
10927
12070
  const _lastToolInputs = {};
10928
12071
  const xmlToolBridge = createStreamBridge(dispatch, { initialEngineId: cesarEngineId });
@@ -10968,12 +12111,13 @@ ${enrichedInput}`;
10968
12111
  async (message) => {
10969
12112
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
10970
12113
  if (!session.alive || abort.signal.aborted) return "";
12114
+ const sendMessage = drainSteeringIntoSend(message, "xml");
10971
12115
  dispatch({ type: "spinner-start", message: "Cesar processing results\u2026", color });
10972
12116
  _engineErrored = false;
10973
12117
  _engineErrorMsg = "";
10974
12118
  let nextResponse = "";
10975
12119
  const gen = session.send({
10976
- message,
12120
+ message: sendMessage,
10977
12121
  signal: abort.signal,
10978
12122
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
10979
12123
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11045,7 +12189,7 @@ ${enrichedInput}`;
11045
12189
  const lastInput = _lastToolInputs[tool] ?? "{}";
11046
12190
  let command = "";
11047
12191
  try {
11048
- command = JSON.parse(lastInput).command ?? JSON.parse(lastInput).file_path ?? lastInput;
12192
+ command = renderToolPermissionCommand(tool, JSON.parse(lastInput));
11049
12193
  } catch {
11050
12194
  command = lastInput;
11051
12195
  }
@@ -11072,7 +12216,7 @@ ${enrichedInput}`;
11072
12216
  try {
11073
12217
  const activePlan = ctx.activePlan;
11074
12218
  if (activePlan && ["planning", "running", "paused"].includes(activePlan.state)) {
11075
- dispatch({
12219
+ dispatchToolCall({
11076
12220
  type: "tool-call",
11077
12221
  engineId: cesarEngineId,
11078
12222
  tool: "ProposePlan",
@@ -11081,7 +12225,7 @@ ${enrichedInput}`;
11081
12225
  output: "A Cesar plan is already active; nested plans are blocked. Resume or cancel the current plan before proposing another."
11082
12226
  });
11083
12227
  } else {
11084
- const { handleProposePlan } = await import("./plan-mode-PFLUPGSY.js");
12228
+ const { handleProposePlan } = await import("./plan-mode-I3BZOBFB.js");
11085
12229
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
11086
12230
  const plan = await handleProposePlan(ppArgs, planDispatch, ctx);
11087
12231
  if (ctx.setActivePlan) ctx.setActivePlan(plan);
@@ -11105,10 +12249,10 @@ ${enrichedInput}`;
11105
12249
  const epArgs = ctx.cesar._exitPlanModeArgs;
11106
12250
  delete ctx.cesar._exitPlanModeArgs;
11107
12251
  try {
11108
- const { handleExitPlanMode } = await import("./plan-mode-PFLUPGSY.js");
12252
+ const { handleExitPlanMode } = await import("./plan-mode-I3BZOBFB.js");
11109
12253
  const planDispatch = ctx.cesar.planDispatch ?? dispatch;
11110
12254
  const exitResult = handleExitPlanMode(String(epArgs?.reason ?? ""), planDispatch, ctx);
11111
- 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 });
11112
12256
  } catch (err) {
11113
12257
  console.warn(`[agon] ExitPlanMode via tool loop failed: ${err instanceof Error ? err.message : String(err)}`);
11114
12258
  }
@@ -11134,7 +12278,7 @@ ${enrichedInput}`;
11134
12278
  confidenceParsed = true;
11135
12279
  }
11136
12280
  }
11137
- 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;
11138
12282
  if (ctx.cesar.quickNeroRequested) ctx.cesar.quickNeroRequested = false;
11139
12283
  if (shouldQuickNero) {
11140
12284
  const quickNeroConfidence = parsedConfidence;
@@ -11174,7 +12318,7 @@ ${qnResult.challengeText}` : ""
11174
12318
  appendMessage(ctx.chatSession, { role: "engine", engineId: ch.engineId, content: ch.content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11175
12319
  }
11176
12320
  }
11177
- tracker.record(cesarEngineId, { prompt: input, response });
12321
+ recordCesarTurn(ctx, cesarEngineId, input, response);
11178
12322
  return {
11179
12323
  mode: escalationAction,
11180
12324
  delegated: true,
@@ -11188,12 +12332,24 @@ ${qnResult.challengeText}` : ""
11188
12332
  };
11189
12333
  }
11190
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
+ }
11191
12342
  const investigationResponse = response;
11192
12343
  let mutationStallForced = false;
11193
12344
  if (!mutationDeferred && !inPlanMode && !ctx.cesar.pendingDelegation && (hadToolActivity || ranToolLoop) && session.alive && !abort.signal.aborted && detectMutationIntentStall(response)) {
11194
- mutationDeferred = true;
11195
- mutationStallForced = true;
11196
- 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
+ }
11197
12353
  }
11198
12354
  if (mutationDeferred && toolRegistry && session.alive && !abort.signal.aborted) {
11199
12355
  toolCtx.readOnlyMode = false;
@@ -11213,10 +12369,11 @@ ${qnResult.challengeText}` : ""
11213
12369
  async (message) => {
11214
12370
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
11215
12371
  if (!session.alive || abort.signal.aborted) return "";
12372
+ const sendMessage = drainSteeringIntoSend(message, "exec");
11216
12373
  dispatch({ type: "spinner-start", message: "Cesar executing\u2026", color });
11217
12374
  let nextResponse = "";
11218
12375
  const gen = session.send({
11219
- message,
12376
+ message: sendMessage,
11220
12377
  signal: abort.signal,
11221
12378
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
11222
12379
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11292,7 +12449,14 @@ ${qnResult.challengeText}` : ""
11292
12449
  dispatch({ type: "spinner-stop" });
11293
12450
  }
11294
12451
  }
11295
- 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) {
11296
12460
  dispatch({ type: "warning", message: "Cesar claimed a job was running but never dispatched one \u2014 grounding..." });
11297
12461
  dispatch({ type: "spinner-start", message: "Cesar grounding\u2026", color });
11298
12462
  try {
@@ -11404,12 +12568,11 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11404
12568
  dispatch({ type: "spinner-stop" });
11405
12569
  }
11406
12570
  }
11407
- const _AUTO_CONT_WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
11408
12571
  const _AUTO_CONT_LOOP_ORCH = /* @__PURE__ */ new Set(["Forge", "Brainstorm", "Tribunal", "Campfire", "Pipeline", "Review", "Agent", "Goal", "Conquer"]);
11409
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;
11410
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;
11411
12574
  const _detectTurnState = (resp, baselineToolCount) => {
11412
- const wroteSinceBaseline = _toolsUsed.slice(baselineToolCount).some((t) => _AUTO_CONT_WRITE_TOOLS.has(t));
12575
+ const wroteSinceBaseline = _toolsUsed.slice(baselineToolCount).some((t) => isWriteToolName(t));
11413
12576
  if (findTrailingUserQuestion(resp)) return "asks-user";
11414
12577
  if (wroteSinceBaseline && !_AUTO_CONT_CONTINUE_RE.test(resp)) return "done";
11415
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);
@@ -11418,6 +12581,18 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11418
12581
  if (_AUTO_CONT_READONLY_DONE_RE.test(resp)) return "done";
11419
12582
  return "stuck";
11420
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
+ };
11421
12596
  const _buildContToolLoopOpts = () => ({
11422
12597
  onToolCall: (name, inp) => {
11423
12598
  _lastToolInputs[name] = JSON.stringify(inp);
@@ -11452,7 +12627,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11452
12627
  const lastInput = _lastToolInputs[tool] ?? "{}";
11453
12628
  let command = "";
11454
12629
  try {
11455
- command = JSON.parse(lastInput).command ?? JSON.parse(lastInput).file_path ?? lastInput;
12630
+ command = renderToolPermissionCommand(tool, JSON.parse(lastInput));
11456
12631
  } catch {
11457
12632
  command = lastInput;
11458
12633
  }
@@ -11476,8 +12651,85 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11476
12651
  let _consecutiveNoProgress = 0;
11477
12652
  while (_continuations < MAX_CONTINUATIONS && session.alive && !abort.signal.aborted && !ctx.cesar.pendingDelegation) {
11478
12653
  const state = _detectTurnState(response, _loopStartToolCount);
11479
- if (state === "asks-user" || state === "done") break;
11480
- 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));
11481
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;
11482
12734
  const filesMentioned = Array.from(new Set(Array.from(response.matchAll(filePathRe), (m) => m[1]))).slice(0, 3);
11483
12735
  let nudge;
@@ -11513,7 +12765,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11513
12765
  break;
11514
12766
  }
11515
12767
  dispatch({ type: "spinner-stop" });
11516
- 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()));
11517
12769
  if (_engineErrored) {
11518
12770
  const _reason = _engineErrorMsg || "no response";
11519
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.` });
@@ -11534,12 +12786,13 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11534
12786
  async (message) => {
11535
12787
  if (ctx.cesar.pendingDelegation) return "[Delegation pending]";
11536
12788
  if (!session.alive || abort.signal.aborted) return "";
12789
+ const sendMessage = drainSteeringIntoSend(message, "auto");
11537
12790
  dispatch({ type: "spinner-start", message: "Cesar executing\u2026", color });
11538
12791
  _engineErrored = false;
11539
12792
  _engineErrorMsg = "";
11540
12793
  let nextResp = "";
11541
12794
  const gen = session.send({
11542
- message,
12795
+ message: sendMessage,
11543
12796
  signal: abort.signal,
11544
12797
  toolLoopBaseBudget: cesarFastPath ? fastPathBaseBudget : void 0,
11545
12798
  toolLoopMaxBudget: cesarFastPath ? fastPathMaxBudget : void 0
@@ -11636,6 +12889,9 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11636
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.`
11637
12890
  });
11638
12891
  }
12892
+ if (previewShown && !streaming) {
12893
+ dispatch({ type: "streaming-end", engineId: cesarEngineId });
12894
+ }
11639
12895
  if (!streaming && response && !ranToolLoop && !wasStreamed) {
11640
12896
  dispatch({ type: "spinner-stop" });
11641
12897
  dispatch({ type: "engine-block", engineId: cesarEngineId, color, content: response });
@@ -11654,7 +12910,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11654
12910
  appendMessage(ctx.chatSession, { role: "engine", engineId: ch.engineId, content: ch.content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11655
12911
  }
11656
12912
  }
11657
- const tokenUsage = tracker.record(cesarEngineId, { prompt: input, response });
12913
+ const tokenUsage = recordCesarTurn(ctx, cesarEngineId, input, response);
11658
12914
  try {
11659
12915
  const tracePath = join14(RUNS_DIR, "cesar-trace.jsonl");
11660
12916
  const taskClass = classifyTask(input);
@@ -11686,6 +12942,11 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11686
12942
  confidenceToolUsed: toolTelemetry.confidenceToolUsed,
11687
12943
  hasNativeTools: toolTelemetry.hasNativeTools,
11688
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,
11689
12950
  confidence: parsedConfidence,
11690
12951
  tokens: tokenUsage ? { prompt: tokenUsage.promptTokens, response: tokenUsage.responseTokens, cost: tokenUsage.costUsd } : void 0
11691
12952
  }) + "\n");
@@ -11801,6 +13062,7 @@ ${cleanFinalAnswer}` : cleanFinalAnswer;
11801
13062
  ctx.cesar.abortSignal = null;
11802
13063
  ctx.cesar.turnId = void 0;
11803
13064
  ctx.setActiveAbort(null);
13065
+ releaseSteeringTurn(_turnId);
11804
13066
  const queued = ctx.cesar.queue;
11805
13067
  if (queued) {
11806
13068
  ctx.cesar.queue = null;
@@ -11832,6 +13094,7 @@ export {
11832
13094
  joinProblemInput,
11833
13095
  runThinkChain,
11834
13096
  runDelegate,
13097
+ runPrText,
11835
13098
  goalDir,
11836
13099
  loadJournal,
11837
13100
  isTestFile,
@@ -11862,15 +13125,20 @@ export {
11862
13125
  clearPermissionQueue,
11863
13126
  clearThinkingBuffer,
11864
13127
  handleOutputEvent,
13128
+ yieldToInk,
13129
+ replayCesarHarnessLogs,
11865
13130
  parseConfidence,
11866
13131
  confidenceBadge,
11867
13132
  parseSuggestion,
11868
- yieldToInk,
11869
- replayCesarHarnessLogs,
13133
+ onSteeringChange,
13134
+ pushSteering,
13135
+ peekSteeringCount,
13136
+ drainLeftoverSteering,
13137
+ clearSteering,
13138
+ handleCesarBrain,
11870
13139
  buildCesarSystemPrompt,
11871
13140
  saveCesarConversationSnapshot,
11872
13141
  resolveCesarBackend,
11873
- ensureCesarSession,
11874
- handleCesarBrain
13142
+ ensureCesarSession
11875
13143
  };
11876
- //# sourceMappingURL=chunk-AONHRJRW.js.map
13144
+ //# sourceMappingURL=chunk-24EWX243.js.map