@wrongstack/cli 1.0.24 → 1.0.26

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 (124) hide show
  1. package/data/providers.json +44 -0
  2. package/dist/{acp-AJWSKWE5.js → acp-LI665LO2.js} +5 -5
  3. package/dist/ascii-mode-SG4PRNLP.js +7 -0
  4. package/dist/{auth-GRBZ7Y3D.js → auth-TE55JF4Z.js} +14 -14
  5. package/dist/{bench-N5K7Z5O3.js → bench-GTLPM22B.js} +2 -2
  6. package/dist/boot/append-system-prompt.d.ts +10 -0
  7. package/dist/boot/ascii-mode.d.ts +12 -0
  8. package/dist/boot/container-wiring.d.ts +4 -2
  9. package/dist/boot/dispatch-singleshot.d.ts +4 -1
  10. package/dist/boot/mcp-config-flag.d.ts +22 -0
  11. package/dist/boot/piped-stdin.d.ts +40 -0
  12. package/dist/boot/restricted-mode.d.ts +15 -0
  13. package/dist/boot/safe-mode.d.ts +30 -0
  14. package/dist/boot/single-shot-budget.d.ts +30 -0
  15. package/dist/boot/stream-json.d.ts +36 -0
  16. package/dist/boot/structured-output.d.ts +16 -0
  17. package/dist/boot/system-prompt-builder.d.ts +2 -0
  18. package/dist/boot/tool-restriction-flags.d.ts +36 -0
  19. package/dist/boot/worktree-sessions.d.ts +21 -0
  20. package/dist/boot-provider-selection.d.ts +67 -0
  21. package/dist/boot.d.ts +1 -38
  22. package/dist/{chunk-LFMVHT3R.js → chunk-2ERX3RPN.js} +11 -13
  23. package/dist/chunk-4TVYGG6G.js +62 -0
  24. package/dist/chunk-5JYDKG5B.js +154 -0
  25. package/dist/{chunk-3VXYY5JC.js → chunk-5UZBUV2H.js} +3 -19
  26. package/dist/chunk-BEHGRNOQ.js +58 -0
  27. package/dist/chunk-CZTXF6HY.js +140 -0
  28. package/dist/{chunk-SKPDE62W.js → chunk-EPR42SWM.js} +36 -6
  29. package/dist/{chunk-YBUCXXYZ.js → chunk-F5DCFAKI.js} +1 -1
  30. package/dist/chunk-HLSHFXTO.js +97 -0
  31. package/dist/{chunk-BLD5DSVA.js → chunk-KJS5543T.js} +2 -2
  32. package/dist/{chunk-QFPQXKCG.js → chunk-KOEO4CJG.js} +21 -6
  33. package/dist/chunk-LBPLTTZ6.js +105 -0
  34. package/dist/{chunk-3OCUS3VN.js → chunk-LDCGZJTC.js} +4 -4
  35. package/dist/chunk-LKPEBZBE.js +143 -0
  36. package/dist/{chunk-DRM7PSW7.js → chunk-OAVBM55Q.js} +3 -3
  37. package/dist/chunk-PZQBGUVZ.js +18 -0
  38. package/dist/{chunk-MHEBWVXL.js → chunk-Q4INH2DG.js} +46 -2
  39. package/dist/{chunk-ZZRSG7VK.js → chunk-RZ4ZDRYA.js} +3 -3
  40. package/dist/{chunk-U46QR4WR.js → chunk-TI7KLHYX.js} +132 -3
  41. package/dist/{chunk-PGKJ2OQP.js → chunk-WQV2NP57.js} +5 -5
  42. package/dist/chunk-X756ZS72.js +329 -0
  43. package/dist/{chunk-JF2MULWF.js → chunk-YHYINL47.js} +47 -15
  44. package/dist/{cli-context-34Q5IJRA.js → cli-context-UVGAN3NO.js} +407 -327
  45. package/dist/{cli-main-AELJDBGU.js → cli-main-TN3KZ2P6.js} +2652 -1978
  46. package/dist/cli-update-notice.d.ts +2 -1
  47. package/dist/config-doctor.d.ts +1 -1
  48. package/dist/{diag-doctor-WIFJH3XH.js → diag-doctor-BIZAFIAT.js} +2 -2
  49. package/dist/{execution-PYC5FCS4.js → execution-7QIP6GRY.js} +1948 -1666
  50. package/dist/fleet/host-types.d.ts +3 -1
  51. package/dist/goal-commands.d.ts +47 -0
  52. package/dist/goal-host.d.ts +1 -14
  53. package/dist/{hq-JTUTFI4L.js → hq-GLZ7QESM.js} +6 -6
  54. package/dist/{hq-server-PC7GZMPT.js → hq-server-YTKDVQPY.js} +2 -2
  55. package/dist/{hq-service-NW56MUP2.js → hq-service-BMMJAHRT.js} +4 -4
  56. package/dist/import-claude-code-32PCSIQI.js +236 -0
  57. package/dist/index.js +12 -8
  58. package/dist/{mailbox-serve-WDOCH6KR.js → mailbox-serve-NW5VZG7R.js} +3 -3
  59. package/dist/{modeldiag-36OGAL5C.js → modeldiag-36VODVIH.js} +8 -8
  60. package/dist/{per-subcommand-help-XE77MTGJ.js → per-subcommand-help-QDAQASPZ.js} +2 -2
  61. package/dist/permission-prompt.d.ts +14 -0
  62. package/dist/permission-rules-view.d.ts +18 -0
  63. package/dist/{permissions-CFDLXETM.js → permissions-PL27UHYD.js} +67 -45
  64. package/dist/{plugin-usage-S5LVGMBW.js → plugin-usage-CIGOOTX3.js} +2 -2
  65. package/dist/{plugins-RHBIVLI7.js → plugins-HQTXUD6A.js} +4 -2
  66. package/dist/preflight.d.ts +1 -1
  67. package/dist/{projects-LMNAGGEU.js → projects-T4CZCA25.js} +3 -6
  68. package/dist/provider-helpers.d.ts +7 -11
  69. package/dist/{providers-models-2CFG46RD.js → providers-models-5Z2PJTEW.js} +5 -5
  70. package/dist/release-asset-download.d.ts +10 -0
  71. package/dist/release-attestation.d.ts +28 -0
  72. package/dist/remote/remote-session.d.ts +64 -0
  73. package/dist/remote/remote-target.d.ts +69 -0
  74. package/dist/remote-O5WXHLWV.js +561 -0
  75. package/dist/{replay-Z3FTJEMM.js → replay-DEI566CW.js} +2 -2
  76. package/dist/services/commit-message.d.ts +2 -1
  77. package/dist/session-move-P2EC2COA.js +44 -0
  78. package/dist/session-move.d.ts +20 -0
  79. package/dist/session-stats.d.ts +10 -0
  80. package/dist/{sessions-config-IQUH2GXT.js → sessions-config-SXTTLNXA.js} +35 -5
  81. package/dist/short-circuit-flags-74Y5N6CF.js +10 -0
  82. package/dist/slash-commands/command-context.d.ts +15 -5
  83. package/dist/slash-commands/goal-refiner.d.ts +4 -49
  84. package/dist/slash-commands/mcp.d.ts +2 -1
  85. package/dist/slash-commands/permissions.d.ts +4 -0
  86. package/dist/slash-commands/settings-context-mutations.d.ts +5 -0
  87. package/dist/slash-commands/settings-limits-mutations.d.ts +12 -0
  88. package/dist/standalone-auto-update-TOTRGCSA.js +249 -0
  89. package/dist/standalone-auto-update.d.ts +65 -0
  90. package/dist/standalone-update-LZO3MNLH.js +24 -0
  91. package/dist/standalone-update.d.ts +37 -1
  92. package/dist/subcommands/handlers/import-claude-code.d.ts +42 -0
  93. package/dist/subcommands/handlers/permissions.d.ts +9 -3
  94. package/dist/subcommands/handlers/remote.d.ts +6 -0
  95. package/dist/subcommands/handlers/version-help.d.ts +5 -0
  96. package/dist/{subcommands-ZTSIN7LG.js → subcommands-Q6ORSB2E.js} +2 -2
  97. package/dist/tui-resume-callback.d.ts +18 -0
  98. package/dist/{typesafe-HVEVPA6R.js → typesafe-U7YB2RRV.js} +5 -5
  99. package/dist/{typesafe-topic-replay-4W73BOLV.js → typesafe-topic-replay-ZILIU3WS.js} +18 -2
  100. package/dist/{update-7XIOEXTD.js → update-TDZX4IL2.js} +4 -4
  101. package/dist/{update-check-2POMBHFB.js → update-check-J5ZVKS73.js} +3 -3
  102. package/dist/version-JB2UAV2A.js +11 -0
  103. package/dist/version-help-XJJQ7YSH.js +10 -0
  104. package/dist/webui-server/route-contexts.d.ts +1 -1
  105. package/dist/webui-server/session-start-payload.d.ts +5 -1
  106. package/dist/{webui-server-L2SICPTB.js → webui-server-YTHLRFD2.js} +509 -366
  107. package/dist/webui-server-lifecycle.d.ts +57 -0
  108. package/dist/webui-server.d.ts +0 -14
  109. package/dist/wiring/brain-and-orchestration.d.ts +3 -1
  110. package/dist/wiring/cli-prompt-and-tools-setup.d.ts +9 -0
  111. package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
  112. package/dist/wiring/lifecycle-plugins.d.ts +4 -2
  113. package/dist/wiring/metrics.d.ts +9 -1
  114. package/dist/wiring/pipeline.d.ts +2 -7
  115. package/dist/wiring/plugins.d.ts +3 -0
  116. package/dist/wiring/provider-status.d.ts +1 -0
  117. package/dist/wiring/provider-utility-tools.d.ts +3 -1
  118. package/package.json +28 -28
  119. package/dist/chunk-2BKFWIER.js +0 -98
  120. package/dist/chunk-424ORSE7.js +0 -56
  121. package/dist/chunk-QBBF7GIK.js +0 -65
  122. package/dist/short-circuit-flags-SDT53OML.js +0 -10
  123. package/dist/standalone-update-MW47J3AK.js +0 -164
  124. package/dist/version-help-WZ623NWM.js +0 -10
@@ -9,6 +9,25 @@ import {
9
9
  deriveFsAccessPair,
10
10
  persistConfigSetting
11
11
  } from "./chunk-SFBFYIOW.js";
12
+ import {
13
+ checkStructuredAnswer,
14
+ parseOutputFormat,
15
+ repairPrompt,
16
+ startStreamJson,
17
+ streamJsonInit,
18
+ withSchemaInstruction
19
+ } from "./chunk-LKPEBZBE.js";
20
+ import {
21
+ theme
22
+ } from "./chunk-QD544J2B.js";
23
+ import {
24
+ WEBUI_SESSION_CHILD_CAPABILITIES,
25
+ writeWebuiSessionChildError,
26
+ writeWebuiSessionChildReady
27
+ } from "./chunk-ZMX5HRLI.js";
28
+ import {
29
+ fmtTok
30
+ } from "./chunk-EWKESL4L.js";
12
31
  import "./chunk-WHOIR577.js";
13
32
  import {
14
33
  advanceToNextTask,
@@ -23,49 +42,38 @@ import {
23
42
  trySaveSpecFromAIOutput,
24
43
  trySaveTasksFromAIOutput
25
44
  } from "./chunk-JHUJUIOH.js";
26
- import {
27
- theme
28
- } from "./chunk-QD544J2B.js";
29
- import {
30
- fmtTok
31
- } from "./chunk-EWKESL4L.js";
32
- import {
33
- WEBUI_SESSION_CHILD_CAPABILITIES,
34
- writeWebuiSessionChildError,
35
- writeWebuiSessionChildReady
36
- } from "./chunk-ZMX5HRLI.js";
37
45
  import {
38
46
  terminalLink,
39
47
  terminalText
40
48
  } from "./chunk-HXJYHAR4.js";
41
- import {
42
- activeProfileConfigPath
43
- } from "./chunk-YMXXOOFN.js";
44
- import {
45
- backupCurrent
46
- } from "./chunk-WSTVGC2C.js";
47
49
  import {
48
50
  startCliHqConnection
49
- } from "./chunk-BLD5DSVA.js";
51
+ } from "./chunk-KJS5543T.js";
52
+ import {
53
+ CLI_VERSION
54
+ } from "./chunk-F5DCFAKI.js";
50
55
  import {
51
56
  resolveActiveApiKey
52
57
  } from "./chunk-LUMETJFO.js";
53
58
  import {
54
- CLI_VERSION
55
- } from "./chunk-YBUCXXYZ.js";
59
+ activeProfileConfigPath
60
+ } from "./chunk-YMXXOOFN.js";
61
+ import {
62
+ backupCurrent
63
+ } from "./chunk-WSTVGC2C.js";
56
64
 
57
65
  // src/execution.ts
58
- import * as path13 from "node:path";
66
+ import * as path14 from "node:path";
59
67
  import { effectiveFallbackChain as effectiveFallbackChain2, setQueuedMessagesSnapshot as setQueuedMessagesSnapshot2 } from "@wrongstack/core/agent";
60
68
  import { LeaderAutoWakeController } from "@wrongstack/core/coordination";
61
69
  import { updateReviewReportEvidence } from "@wrongstack/core/plugin";
62
70
  import { noOpVault } from "@wrongstack/core/security";
63
- import { attachTodosCheckpoint as attachTodosCheckpoint2 } from "@wrongstack/core/storage";
71
+ import { attachTodosCheckpoint as attachTodosCheckpoint2, QueueStore } from "@wrongstack/core/storage";
64
72
  import { normalizeTokenSavingTier as normalizeTokenSavingTier2 } from "@wrongstack/core/types";
65
- import { hasOpenTodos as hasOpenTodos3, mergeCustomModelDefs } from "@wrongstack/core/utils";
66
- import { capabilitiesFor } from "@wrongstack/providers";
73
+ import { mergeCustomModelDefs, sessionScopedPath as sessionScopedPath3 } from "@wrongstack/core/utils";
74
+ import { listGitWorktrees } from "@wrongstack/core/worktree";
75
+ import { capabilitiesFor, catalogProviderIdFor } from "@wrongstack/providers";
67
76
  import { createToolVisionAdapters } from "@wrongstack/runtime/vision";
68
- import { parseNextSteps as parseNextSteps2 } from "@wrongstack/tools/next-steps";
69
77
 
70
78
  // src/boot/dispatch-singleshot.ts
71
79
  import { color, writeOut } from "@wrongstack/core/utils";
@@ -87,19 +95,117 @@ ${err.describe()}`);
87
95
  ].join("\n");
88
96
  }
89
97
 
98
+ // src/boot/single-shot-budget.ts
99
+ function parseMaxBudgetUsd(value) {
100
+ if (value === void 0) return void 0;
101
+ const amount = typeof value === "string" ? Number(value.trim()) : Number.NaN;
102
+ if (!Number.isFinite(amount) || amount <= 0) {
103
+ throw new Error("--max-budget-usd needs a positive dollar amount (e.g. --max-budget-usd 0.50)");
104
+ }
105
+ return amount;
106
+ }
107
+ function watchSingleShotBudget(opts) {
108
+ const leaderStart = opts.tokenCounter.estimateCost().total;
109
+ const subagentCost = /* @__PURE__ */ new Map();
110
+ let exceeded = false;
111
+ const spent = () => {
112
+ let total = opts.tokenCounter.estimateCost().total - leaderStart;
113
+ for (const cost of subagentCost.values()) total += cost;
114
+ return total;
115
+ };
116
+ const check = () => {
117
+ if (exceeded || spent() <= opts.limitUsd) return;
118
+ exceeded = true;
119
+ opts.onExceeded();
120
+ };
121
+ const offLeader = opts.events.on("token.accounted", check);
122
+ const offSubagent = opts.events.on("subagent.token_accounted", (payload) => {
123
+ subagentCost.set(payload.subagentId, payload.cost.total);
124
+ check();
125
+ });
126
+ return {
127
+ spent,
128
+ get exceeded() {
129
+ return exceeded;
130
+ },
131
+ dispose() {
132
+ offLeader();
133
+ offSubagent();
134
+ }
135
+ };
136
+ }
137
+
90
138
  // src/boot/dispatch-singleshot.ts
139
+ function exitCodeForStatus(status) {
140
+ if (status === "failed" || status === "max_iterations") return 1;
141
+ if (status === "aborted") return 130;
142
+ return 0;
143
+ }
91
144
  async function runSingleShotDispatch(ctx) {
92
145
  const { agent, query, flags, tokenCounter, renderer } = ctx;
146
+ let limitUsd;
147
+ let format;
148
+ try {
149
+ limitUsd = parseMaxBudgetUsd(flags["max-budget-usd"]);
150
+ format = parseOutputFormat(flags["output-format"]) ?? (flags["output-json"] ? "json" : "text");
151
+ } catch (err) {
152
+ renderer.writeError(err instanceof Error ? err.message : String(err));
153
+ return 2;
154
+ }
93
155
  const ctrl = new AbortController();
94
156
  const onSigint = () => ctrl.abort();
95
157
  process.on("SIGINT", onSigint);
158
+ let budget;
159
+ if (limitUsd !== void 0 && ctx.events) {
160
+ budget = watchSingleShotBudget({
161
+ limitUsd,
162
+ tokenCounter,
163
+ events: ctx.events,
164
+ onExceeded: () => ctrl.abort()
165
+ });
166
+ }
167
+ let stopStream;
168
+ if (format === "stream-json") {
169
+ const tools = agent.tools?.listForProvider?.() ?? agent.tools?.list?.() ?? [];
170
+ writeOut(
171
+ streamJsonInit({
172
+ sessionId: agent.ctx?.session?.id ?? null,
173
+ provider: agent.ctx?.provider?.id ?? null,
174
+ model: agent.ctx?.model ?? null,
175
+ cwd: agent.ctx?.cwd ?? process.cwd(),
176
+ tools: tools.map((tool) => tool.name)
177
+ })
178
+ );
179
+ if (ctx.events) {
180
+ stopStream = startStreamJson({
181
+ events: ctx.events,
182
+ write: (line) => writeOut(line),
183
+ includePartialMessages: flags["include-partial-messages"] === true
184
+ });
185
+ }
186
+ }
96
187
  const startedAt = Date.now();
97
188
  const before = tokenCounter.total();
98
189
  const costBefore = tokenCounter.estimateCost().total;
190
+ const schema = typeof flags["json-schema"] === "string" ? JSON.parse(flags["json-schema"]) : void 0;
99
191
  let result;
192
+ let structured;
100
193
  try {
101
- result = await agent.run(query, { signal: ctrl.signal });
194
+ result = await agent.run(schema ? withSchemaInstruction(query, schema) : query, {
195
+ signal: ctrl.signal
196
+ });
197
+ if (schema && result.status === "done") {
198
+ structured = checkStructuredAnswer(result.finalText, schema);
199
+ if (!structured.ok && !ctrl.signal.aborted) {
200
+ const first = result;
201
+ result = await agent.run(repairPrompt(structured.errors), { signal: ctrl.signal });
202
+ result = { ...result, iterations: first.iterations + result.iterations };
203
+ if (result.status === "done") structured = checkStructuredAnswer(result.finalText, schema);
204
+ }
205
+ }
102
206
  } finally {
207
+ stopStream?.();
208
+ budget?.dispose();
103
209
  process.off("SIGINT", onSigint);
104
210
  const { getProcessRegistry: getProcessRegistry2 } = await import("@wrongstack/tools");
105
211
  getProcessRegistry2().killAll({ preserveBackground: true });
@@ -113,9 +219,17 @@ async function runSingleShotDispatch(ctx) {
113
219
  cost: costAfter - costBefore,
114
220
  elapsedMs: Date.now() - startedAt
115
221
  };
116
- if (flags["output-json"]) {
222
+ const schemaFailed = structured !== void 0 && !structured.ok;
223
+ const exitCode = budget?.exceeded || schemaFailed ? 1 : exitCodeForStatus(result.status);
224
+ if (format !== "text") {
117
225
  const json = JSON.stringify({
226
+ // In a stream the result is one event among many; name it.
227
+ ...format === "stream-json" ? { type: "result" } : {},
118
228
  status: result.status,
229
+ // Lets a script continue the conversation: `wstack --resume <sessionId>`.
230
+ // Read defensively — the session is swapped in by /resume and absent in
231
+ // some embedders.
232
+ sessionId: agent.ctx?.session?.id ?? null,
119
233
  finalText: result.finalText ?? null,
120
234
  error: result.error ? {
121
235
  code: result.error.code,
@@ -125,14 +239,22 @@ async function runSingleShotDispatch(ctx) {
125
239
  message: result.error.message,
126
240
  context: result.error.context ?? null
127
241
  } : null,
128
- usage
242
+ usage,
243
+ ...limitUsd !== void 0 ? { budget: { limitUsd, exceeded: budget?.exceeded ?? false } } : {},
244
+ ...schema ? {
245
+ structuredOutput: structured?.ok ? structured.value : null,
246
+ schemaErrors: structured && !structured.ok ? structured.errors : null
247
+ } : {}
129
248
  });
130
249
  writeOut(json + "\n");
131
- return 0;
250
+ return exitCode;
132
251
  }
133
- let code = 0;
134
- if (result.status === "failed") {
135
- code = 1;
252
+ const code = exitCode;
253
+ if (budget?.exceeded) {
254
+ renderer.writeError(
255
+ `Stopped: spend $${budget.spent().toFixed(4)} exceeded --max-budget-usd $${limitUsd}.`
256
+ );
257
+ } else if (result.status === "failed") {
136
258
  const err = result.error;
137
259
  if (err) {
138
260
  const tag = err.recoverable ? " (recoverable)" : "";
@@ -143,11 +265,11 @@ async function runSingleShotDispatch(ctx) {
143
265
  renderer.writeError("Failed.");
144
266
  }
145
267
  } else if (result.status === "aborted") {
146
- code = 130;
147
268
  renderer.writeWarning("Aborted.");
148
269
  } else if (result.status === "max_iterations") {
149
- code = 1;
150
270
  renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
271
+ } else if (structured && !structured.ok) {
272
+ renderer.writeError(`Answer did not match --json-schema: ${structured.errors.join("; ")}`);
151
273
  }
152
274
  if (result.finalText) renderer.write("\n" + result.finalText + "\n");
153
275
  const r = result;
@@ -216,7 +338,7 @@ async function runWebUIDispatch(ctx) {
216
338
  const isSimpleUi = !isSessionChild && flags["simpleui"] === true;
217
339
  agent.disableInteractiveConfirmation();
218
340
  renderer.setSilent(true);
219
- const { runWebUI } = await import("./webui-server-L2SICPTB.js");
341
+ const { runWebUI } = await import("./webui-server-YTHLRFD2.js");
220
342
  const flagValue = (names) => {
221
343
  for (const name of names) {
222
344
  if (!Object.hasOwn(flags, name)) continue;
@@ -426,14 +548,14 @@ async function runWebUIDispatch(ctx) {
426
548
  );
427
549
  }
428
550
  });
429
- const webuiExit = new Promise((resolve4) => {
551
+ const webuiExit = new Promise((resolve5) => {
430
552
  webuiPromise.then(() => {
431
553
  renderer.setSilent(false);
432
554
  renderer.write("\n");
433
555
  renderer.writeInfo(
434
556
  color2.yellow(` Shutting down ${isSimpleUi ? "SimpleUI" : "WebUI"} server\u2026`)
435
557
  );
436
- resolve4(0);
558
+ resolve5(0);
437
559
  }).catch((err) => {
438
560
  if (webuiSessionChild) {
439
561
  void writeWebuiSessionChildError(webuiSessionChild.readyFile, {
@@ -451,7 +573,7 @@ async function runWebUIDispatch(ctx) {
451
573
  ` \u2717 ${isSimpleUi ? "SimpleUI" : "WebUI"} failed to start: ${err instanceof Error ? err.message : String(err)}`
452
574
  )
453
575
  );
454
- resolve4(1);
576
+ resolve5(1);
455
577
  });
456
578
  });
457
579
  return webuiExit;
@@ -467,6 +589,68 @@ function resolveExecutionMode(positional, flags) {
467
589
  return "repl";
468
590
  }
469
591
 
592
+ // src/boot/piped-stdin.ts
593
+ var PIPED_STDIN_FIRST_BYTE_TIMEOUT_MS = 3e3;
594
+ var PIPED_STDIN_MAX_BYTES = 2 * 1024 * 1024;
595
+ var EMPTY = { text: "", truncated: false, timedOut: false };
596
+ function readPipedStdin(source, opts = {}) {
597
+ if (source.isTTY) return Promise.resolve(EMPTY);
598
+ const firstByteTimeoutMs = opts.firstByteTimeoutMs ?? PIPED_STDIN_FIRST_BYTE_TIMEOUT_MS;
599
+ const maxBytes = opts.maxBytes ?? PIPED_STDIN_MAX_BYTES;
600
+ return new Promise((resolve5) => {
601
+ const chunks = [];
602
+ let size = 0;
603
+ let truncated = false;
604
+ let settled = false;
605
+ const finish = (timedOut) => {
606
+ if (settled) return;
607
+ settled = true;
608
+ clearTimeout(timer);
609
+ source.removeListener("data", onData);
610
+ source.removeListener("end", onEnd);
611
+ source.removeListener("close", onEnd);
612
+ source.removeListener("error", onError);
613
+ source.pause();
614
+ if (timedOut || truncated) source.unref?.();
615
+ const text = timedOut ? "" : Buffer.concat(chunks).toString("utf8").trimEnd();
616
+ resolve5({ text, truncated, timedOut });
617
+ };
618
+ const onData = (chunk) => {
619
+ clearTimeout(timer);
620
+ const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
621
+ const room = maxBytes - size;
622
+ if (buf.length >= room) {
623
+ chunks.push(buf.subarray(0, Math.max(0, room)));
624
+ size = maxBytes;
625
+ truncated = true;
626
+ finish(false);
627
+ return;
628
+ }
629
+ chunks.push(buf);
630
+ size += buf.length;
631
+ };
632
+ const onEnd = () => finish(false);
633
+ const onError = () => finish(false);
634
+ const timer = setTimeout(() => {
635
+ if (size === 0) finish(true);
636
+ }, firstByteTimeoutMs);
637
+ source.on("data", onData);
638
+ source.on("end", onEnd);
639
+ source.on("close", onEnd);
640
+ source.on("error", onError);
641
+ source.resume();
642
+ });
643
+ }
644
+ function appendPipedStdin(query, piped) {
645
+ if (!piped) return query;
646
+ if (!query.trim()) return piped;
647
+ return `${query}
648
+
649
+ <stdin>
650
+ ${piped}
651
+ </stdin>`;
652
+ }
653
+
470
654
  // src/boot/tui-coordinator-callbacks.ts
471
655
  function createTuiCoordinatorCallbacks({
472
656
  state,
@@ -656,7 +840,6 @@ Reply with ONLY the JSON object.`
656
840
  content: [userPrompt]
657
841
  }
658
842
  ],
659
- maxTokens: 1024,
660
843
  temperature: 0
661
844
  },
662
845
  { signal: context.signal }
@@ -1022,6 +1205,7 @@ import {
1022
1205
  } from "@wrongstack/core/agent";
1023
1206
  import { resetSessionSubagentPolicy } from "@wrongstack/core/coordination";
1024
1207
  import { TOKENS } from "@wrongstack/core/kernel";
1208
+ import { restoreSessionPermissionOverrides } from "@wrongstack/core/security";
1025
1209
  import { DefaultSessionStore } from "@wrongstack/core/storage";
1026
1210
  import {
1027
1211
  activateProjectStateGuard,
@@ -1056,7 +1240,8 @@ async function switchProjectInPlace(ctx, targetRoot, displayName) {
1056
1240
  await fs2.mkdir(nextWpaths.projectSessions, { recursive: true });
1057
1241
  const nextSessionStore = new DefaultSessionStore({
1058
1242
  dir: nextWpaths.projectSessions,
1059
- projectRoot: resolved
1243
+ projectRoot: resolved,
1244
+ events
1060
1245
  });
1061
1246
  const nextWriter = await nextSessionStore.create({
1062
1247
  id: "",
@@ -1132,6 +1317,7 @@ async function switchProjectInPlace(ctx, targetRoot, displayName) {
1132
1317
  context.session = nextWriter;
1133
1318
  context.state.replaceMessages([]);
1134
1319
  resetSessionSubagentPolicy(context);
1320
+ restoreSessionPermissionOverrides(context.meta, {});
1135
1321
  context.state.replaceTodos([]);
1136
1322
  context.clearFileTracking();
1137
1323
  context.clearMemoryEvidence?.();
@@ -1266,574 +1452,132 @@ async function onSDDOutput(output) {
1266
1452
  return messages;
1267
1453
  }
1268
1454
 
1269
- // src/boot/tui-session-resume.ts
1455
+ // src/boot/tui-session-stub-enrich.ts
1456
+ import * as fs3 from "node:fs/promises";
1270
1457
  import * as path7 from "node:path";
1271
- import {
1272
- restoreSessionSubagentModelPlan,
1273
- restoreSessionSubagentPolicy
1274
- } from "@wrongstack/core/coordination";
1275
- import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
1276
- import { attachTodosCheckpoint, loadTodosCheckpoint } from "@wrongstack/core/storage";
1277
- import { projectLastRequestTokens } from "@wrongstack/core/types/session-timeline";
1278
- import { sessionScopedPath as sessionScopedPath2, toErrorMessage } from "@wrongstack/core/utils";
1279
- var RESERVATION_WINDOW_MS = 6e4;
1280
- var RESERVATION_RENEW_MS = 2e4;
1281
- function messageText(content) {
1282
- if (typeof content === "string") return content;
1283
- if (!Array.isArray(content)) return "";
1284
- return content.map(
1285
- (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
1286
- ).filter(Boolean).join("\n");
1458
+ var SLICE_BYTES = 256 * 1024;
1459
+ var MAX_TITLE_CHARS = 80;
1460
+ var PICKER_POOL_CEILING = 1500;
1461
+ var ENRICH_CONCURRENCY = 16;
1462
+ function isStubSummary(s) {
1463
+ return !s.title && !s.endedAt;
1287
1464
  }
1288
- function lastAssistantTextOf(messages) {
1289
- for (let index = messages.length - 1; index >= 0; index -= 1) {
1290
- const message = messages[index];
1291
- if (message?.role !== "assistant") continue;
1292
- const text = messageText(message.content).trim();
1293
- if (text) return text;
1465
+ function containedJsonlPath(sessionsDir, id) {
1466
+ const root = path7.resolve(sessionsDir);
1467
+ const file = path7.resolve(root, `${id}.jsonl`);
1468
+ if (file !== root && !file.startsWith(root + path7.sep)) return void 0;
1469
+ return file;
1470
+ }
1471
+ function stripControlChars(text) {
1472
+ return text.replaceAll(
1473
+ /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|[\u0000-\u001f\u007f-\u009f]/g,
1474
+ ""
1475
+ );
1476
+ }
1477
+ function truncate(text) {
1478
+ const oneLine = stripControlChars(text).replaceAll(/\s+/g, " ").trim();
1479
+ return oneLine.length > MAX_TITLE_CHARS ? `${oneLine.slice(0, MAX_TITLE_CHARS - 1)}\u2026` : oneLine;
1480
+ }
1481
+ function contentToText(content) {
1482
+ if (typeof content === "string") return content;
1483
+ if (Array.isArray(content)) {
1484
+ return content.map(
1485
+ (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
1486
+ ).filter(Boolean).join(" ");
1294
1487
  }
1295
- return void 0;
1488
+ return "";
1296
1489
  }
1297
- async function resumeSession(ctx, sessionId) {
1298
- const { state, agent, tokenCounter, switchProviderAndModel, events, onLoadProgress } = ctx;
1299
- const fail = (stage2, message) => {
1300
- try {
1301
- ctx.onFailure?.({ stage: stage2, message: message || "unknown error" });
1302
- } catch {
1303
- }
1304
- return null;
1305
- };
1306
- const warnings = [];
1307
- const warn = (label, err) => {
1308
- warnings.push(`${label}: ${toErrorMessage(err)}`);
1309
- };
1310
- let stage = "start";
1311
- const setStage = (next) => {
1312
- stage = next;
1490
+ function scanUserInputs(chunk) {
1491
+ let first;
1492
+ let last;
1493
+ for (const line of chunk.split("\n")) {
1494
+ const trimmed = line.trim();
1495
+ if (!trimmed.includes('"user_input"')) continue;
1313
1496
  try {
1314
- ctx.onStage?.(next);
1497
+ const event = JSON.parse(trimmed);
1498
+ if (event.type !== "user_input" || typeof event.ts !== "string") continue;
1499
+ const text = contentToText(event.content).trim();
1500
+ if (!text) continue;
1501
+ const hit = { ts: event.ts, text };
1502
+ if (!first) first = hit;
1503
+ last = hit;
1315
1504
  } catch {
1316
1505
  }
1317
- };
1318
- if (!state.activeSessionStore) {
1319
- return fail("no_session_store", "No session store is bound to this project.");
1320
1506
  }
1321
- setStage("load_replay_module");
1322
- let replaySessionMessages;
1507
+ return { first, last };
1508
+ }
1509
+ async function firstUserInput(file) {
1510
+ let handle;
1323
1511
  try {
1324
- ({ replaySessionMessages } = await import("@wrongstack/tui"));
1325
- if (typeof replaySessionMessages !== "function") {
1326
- throw new TypeError("@wrongstack/tui does not export replaySessionMessages");
1327
- }
1328
- } catch (err) {
1329
- return fail(stage, toErrorMessage(err));
1512
+ handle = await fs3.open(file, "r");
1513
+ const { size } = await handle.stat();
1514
+ const length = Math.min(SLICE_BYTES, size);
1515
+ const buffer = Buffer.alloc(length);
1516
+ await handle.read(buffer, 0, length, 0);
1517
+ const text = buffer.toString("utf8");
1518
+ const cutoff = text.lastIndexOf("\n");
1519
+ return scanUserInputs(cutoff > 0 ? text.slice(0, cutoff) : text).first;
1520
+ } catch {
1521
+ return void 0;
1522
+ } finally {
1523
+ await handle?.close().catch(() => void 0);
1330
1524
  }
1331
- setStage("resolve_id");
1332
- let canonicalSessionId = sessionId;
1525
+ }
1526
+ async function lastUserInput(file) {
1527
+ let handle;
1333
1528
  try {
1334
- if (state.activeSessionStore.resolveId) {
1335
- canonicalSessionId = await state.activeSessionStore.resolveId(sessionId);
1336
- }
1337
- } catch (err) {
1338
- console.error(
1339
- JSON.stringify({
1340
- level: "error",
1341
- event: "execution.resume_id_resolve_failed",
1342
- sessionId,
1343
- message: toErrorMessage(err),
1344
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1345
- })
1346
- );
1347
- return fail(stage, toErrorMessage(err));
1529
+ handle = await fs3.open(file, "r");
1530
+ const { size } = await handle.stat();
1531
+ const start = Math.max(0, size - SLICE_BYTES);
1532
+ const length = size - start;
1533
+ const buffer = Buffer.alloc(length);
1534
+ await handle.read(buffer, 0, length, start);
1535
+ const text = buffer.toString("utf8");
1536
+ const from = text.indexOf("\n");
1537
+ return scanUserInputs(from >= 0 ? text.slice(from + 1) : text).last;
1538
+ } catch {
1539
+ return void 0;
1540
+ } finally {
1541
+ await handle?.close().catch(() => void 0);
1348
1542
  }
1349
- const previousSessionId = agent.ctx.session?.id;
1350
- let hydrated;
1351
- let identityClaimed = false;
1352
- let resumeClaim;
1353
- let writerSwapped = false;
1354
- let openedWriter;
1355
- let oldWriter;
1356
- let oldMessages;
1357
- let oldSessionRefCurrent;
1358
- let previousDetachFn;
1359
- let oldTodos = [];
1360
- let oldPlanPath;
1361
- let oldTaskPath;
1362
- try {
1363
- setStage("reserve_ownership");
1364
- if (state.wpaths.projectSlug && state.wpaths.globalRoot) {
1365
- const { getSessionRegistry } = await import("@wrongstack/core/storage");
1366
- const registry = getSessionRegistry(state.wpaths.globalRoot);
1367
- resumeClaim = await registry.reserveResume({
1368
- sessionId: canonicalSessionId,
1369
- projectSlug: state.wpaths.projectSlug,
1370
- projectRoot: state.projectRoot,
1371
- reservationMs: RESERVATION_WINDOW_MS
1372
- });
1373
- } else if (state.activateSessionIdentity) {
1374
- await state.activateSessionIdentity(canonicalSessionId);
1375
- identityClaimed = true;
1376
- }
1377
- setStage("open_journal");
1378
- const claimToRenew = resumeClaim;
1379
- let renewFailures = 0;
1380
- const renew = () => {
1381
- if (!claimToRenew) return;
1382
- void claimToRenew.renew(RESERVATION_WINDOW_MS).catch((err) => {
1383
- if (renewFailures++ > 0) return;
1384
- console.error(
1385
- JSON.stringify({
1386
- level: "warn",
1387
- event: "execution.resume_reservation_renew_failed",
1388
- sessionId: canonicalSessionId,
1389
- message: toErrorMessage(err),
1390
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1391
- })
1392
- );
1393
- });
1394
- };
1395
- const renewTimer = setInterval(renew, RESERVATION_RENEW_MS);
1396
- renewTimer.unref?.();
1397
- let resumed;
1398
- try {
1399
- resumed = await state.activeSessionStore.resume(canonicalSessionId, onLoadProgress);
1400
- } finally {
1401
- clearInterval(renewTimer);
1402
- }
1403
- openedWriter = resumed.writer;
1404
- hydrated = resumed.data;
1405
- setStage("activate_ownership");
1406
- await Promise.resolve(resumeClaim?.renew(RESERVATION_WINDOW_MS)).catch(() => void 0);
1407
- if (resumeClaim) {
1408
- await resumeClaim.activate({
1409
- sessionId: canonicalSessionId,
1410
- projectSlug: state.wpaths.projectSlug,
1411
- projectRoot: state.projectRoot,
1412
- projectName: path7.basename(state.projectRoot),
1413
- workingDir: agent.ctx.workingDir,
1414
- clientType: "tui",
1415
- pid: process.pid,
1416
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1417
- });
1418
- identityClaimed = true;
1419
- await state.activateSessionIdentity?.(canonicalSessionId);
1420
- }
1421
- setStage("replay_history");
1422
- const meta = resumed.data.metadata;
1423
- const entries = replaySessionMessages(
1424
- resumed.data.messages,
1425
- resumed.data.events,
1426
- /* startId */
1427
- 1
1428
- );
1429
- try {
1430
- agent.container?.safeResolve?.(TOKENS2.DelegationTracker)?.rehydrate(canonicalSessionId, resumed.data.events ?? []);
1431
- } catch (err) {
1432
- warn("delegation rehydrate", err);
1433
- }
1434
- setStage("read_sidecars");
1435
- const sessionsDir = state.wpaths.projectSessions;
1436
- const resumedTodosPath = sessionScopedPath2(sessionsDir, resumed.writer.id, ".todos.json");
1437
- const restoredTodos = await loadTodosCheckpoint(
1438
- resumedTodosPath,
1439
- events,
1440
- agent.ctx.traceId,
1441
- resumed.writer.id
1442
- ).catch(() => null);
1443
- setStage("swap_writer");
1444
- oldWriter = agent.ctx.session;
1445
- oldMessages = [...agent.ctx.messages];
1446
- agent.ctx.session = resumed.writer;
1447
- try {
1448
- agent.ctx.state.replaceMessages(resumed.data.messages);
1449
- restoreSessionSubagentPolicy(agent.ctx, resumed.data.events, resumed.data.subagentsAllowed);
1450
- restoreSessionSubagentModelPlan(agent.ctx, resumed.data.events);
1451
- } catch (err) {
1452
- agent.ctx.session = oldWriter;
1453
- agent.ctx.state.replaceMessages(oldMessages);
1454
- throw err;
1455
- }
1456
- writerSwapped = true;
1457
- if (state.sessionRef) {
1458
- oldSessionRefCurrent = state.sessionRef.current;
1459
- state.sessionRef.current = resumed.writer;
1460
- }
1461
- setStage("flush_journal");
1462
- await agent.ctx.flushConversationJournal().catch((err) => {
1463
- console.error(
1464
- JSON.stringify({
1465
- level: "error",
1466
- event: "execution.resume_journal_flush_failed",
1467
- message: toErrorMessage(err),
1468
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1469
- })
1470
- );
1471
- warn("conversation journal did not flush", err);
1472
- });
1473
- setStage("repoint_sidecars");
1474
- previousDetachFn = state.detachActiveTodosCheckpoint;
1475
- oldTodos = agent.ctx.state.todos ? [...agent.ctx.state.todos] : [];
1476
- oldPlanPath = agent.ctx.state.meta?.["plan.path"];
1477
- oldTaskPath = agent.ctx.state.meta?.["task.path"];
1478
- try {
1479
- await Promise.resolve(previousDetachFn?.()).catch(() => void 0);
1480
- agent.ctx.state.setMeta(
1481
- "plan.path",
1482
- sessionScopedPath2(sessionsDir, resumed.writer.id, ".plan.json")
1483
- );
1484
- agent.ctx.state.setMeta(
1485
- "task.path",
1486
- sessionScopedPath2(sessionsDir, resumed.writer.id, ".tasks.json")
1487
- );
1488
- agent.ctx.state.replaceTodos(restoredTodos ?? []);
1489
- try {
1490
- state.detachActiveTodosCheckpoint = attachTodosCheckpoint(
1491
- agent.ctx.state,
1492
- resumedTodosPath,
1493
- resumed.writer.id,
1494
- events,
1495
- agent.ctx.traceId
1496
- );
1497
- } catch {
1498
- state.detachActiveTodosCheckpoint = void 0;
1499
- }
1500
- } catch (err) {
1501
- warn("todo/plan sidecars were not re-pointed", err);
1543
+ }
1544
+ function effectiveLastActivity(s) {
1545
+ return s.lastActivityAt ?? s.endedAt ?? s.startedAt ?? "";
1546
+ }
1547
+ function hasActivity(s) {
1548
+ return (s.messageCount ?? 0) > 0 || (s.iterationCount ?? 0) > 0 || (s.toolCallCount ?? 0) > 0 || (s.tokenTotal ?? 0) > 0 || Boolean(s.title?.trim()) || Boolean(s.lastUserMessage?.trim());
1549
+ }
1550
+ async function selectPickerSessions(storeList, sessionsDir, limit) {
1551
+ const poolSize = Math.max(limit, Math.min(PICKER_POOL_CEILING, limit * 3));
1552
+ const enriched = await enrichStubSummaries(await storeList(poolSize), sessionsDir);
1553
+ return enriched.filter(hasActivity).sort((a, b) => effectiveLastActivity(b).localeCompare(effectiveLastActivity(a))).slice(0, limit);
1554
+ }
1555
+ async function enrichStubSummaries(summaries, sessionsDir) {
1556
+ const out = [...summaries];
1557
+ const pending = summaries.reduce((indexes, summary, index) => {
1558
+ if (isStubSummary(summary)) indexes.push(index);
1559
+ return indexes;
1560
+ }, []);
1561
+ if (pending.length === 0) return out;
1562
+ let cursor = 0;
1563
+ const worker = async () => {
1564
+ for (; ; ) {
1565
+ const slot = cursor++;
1566
+ if (slot >= pending.length) return;
1567
+ const index = pending[slot];
1568
+ const summary = out[index];
1569
+ const file = containedJsonlPath(sessionsDir, summary.id);
1570
+ if (!file) continue;
1502
1571
  try {
1503
- state.detachActiveTodosCheckpoint = previousDetachFn;
1504
- agent.ctx.state.replaceTodos([...oldTodos]);
1505
- if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
1506
- if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
1507
- } catch {
1508
- }
1509
- }
1510
- setStage("restore_model");
1511
- const currentProviderId = agent.ctx.provider.id;
1512
- const savedProvider = typeof meta.provider === "string" ? meta.provider.trim() : "";
1513
- const savedModel = typeof meta.model === "string" ? meta.model.trim() : "";
1514
- const hasSavedRoute = savedProvider.length > 0 && savedProvider.toLowerCase() !== "unknown" && savedModel.length > 0 && savedModel.toLowerCase() !== "unknown";
1515
- const targetProviderId = hasSavedRoute ? savedProvider : currentProviderId;
1516
- const targetModel = hasSavedRoute ? savedModel : agent.ctx.model;
1517
- if (switchProviderAndModel && targetProviderId && (targetProviderId !== currentProviderId || targetModel !== agent.ctx.model)) {
1518
- try {
1519
- const error = await Promise.resolve(switchProviderAndModel(targetProviderId, targetModel));
1520
- if (typeof error === "string" && error)
1521
- warn(`could not restore ${targetProviderId}/${targetModel}`, error);
1522
- } catch (err) {
1523
- console.error(
1524
- JSON.stringify({
1525
- level: "error",
1526
- event: "execution.resume_model_restore_failed",
1527
- provider: targetProviderId,
1528
- model: targetModel,
1529
- message: toErrorMessage(err),
1530
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1531
- })
1532
- );
1533
- warn(`could not restore ${targetProviderId}/${targetModel}`, err);
1534
- }
1535
- } else if (targetModel !== agent.ctx.model) {
1536
- agent.ctx.model = targetModel;
1537
- }
1538
- setStage("finalize_previous_session");
1539
- let finalizeOldWriter;
1540
- if (oldWriter && oldWriter !== resumed.writer) {
1541
- const staleWriter = oldWriter;
1542
- const endedUsage = tokenCounter.total();
1543
- finalizeOldWriter = async () => {
1544
- let appendOk = false;
1545
- try {
1546
- await staleWriter.append({
1547
- type: "session_end",
1548
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1549
- usage: endedUsage
1550
- });
1551
- appendOk = true;
1552
- } catch (err) {
1553
- console.error(
1554
- JSON.stringify({
1555
- level: "error",
1556
- event: "execution.session_end_append_failed",
1557
- message: toErrorMessage(err),
1558
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1559
- })
1560
- );
1561
- }
1562
- if (appendOk) {
1563
- try {
1564
- await staleWriter.close();
1565
- } catch (err) {
1566
- console.error(
1567
- JSON.stringify({
1568
- level: "error",
1569
- event: "execution.session_close_failed",
1570
- message: toErrorMessage(err),
1571
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1572
- })
1573
- );
1574
- }
1575
- }
1576
- };
1577
- }
1578
- setStage("token_accounting");
1579
- try {
1580
- tokenCounter.reset();
1581
- tokenCounter.account(resumed.data.usage, targetModel, targetProviderId);
1582
- } catch (err) {
1583
- warn("token accounting for the resumed session failed", err);
1584
- }
1585
- const resumedRequestTokens = projectLastRequestTokens(resumed.data.events);
1586
- agent.ctx.lastRequestTokens = resumedRequestTokens;
1587
- tokenCounter.setCurrentRequestTokens?.(resumedRequestTokens ?? 0);
1588
- const tokens = resumedRequestTokens ?? 0;
1589
- const maxContext = agent.ctx.provider?.capabilities?.maxContext ?? 0;
1590
- const finalText = lastAssistantTextOf(resumed.data.messages);
1591
- const result = {
1592
- entries,
1593
- nextId: entries.length + 1,
1594
- sessionId: resumed.writer.id,
1595
- contextSnapshot: { tokens, maxContext },
1596
- attached: true,
1597
- warnings,
1598
- ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
1599
- };
1600
- if (finalizeOldWriter) void finalizeOldWriter();
1601
- return result;
1602
- } catch (err) {
1603
- if (!identityClaimed) await resumeClaim?.cancel().catch(() => void 0);
1604
- if (identityClaimed && previousSessionId) {
1605
- await state.activateSessionIdentity?.(previousSessionId).catch((rollbackErr) => {
1606
- console.error(
1607
- JSON.stringify({
1608
- level: "error",
1609
- event: "execution.resume_identity_rollback_failed",
1610
- sessionId: previousSessionId,
1611
- message: toErrorMessage(rollbackErr),
1612
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1613
- })
1614
- );
1615
- });
1616
- }
1617
- if (!writerSwapped) {
1618
- await openedWriter?.close().catch(() => void 0);
1619
- } else {
1620
- try {
1621
- if (oldWriter !== void 0) agent.ctx.session = oldWriter;
1622
- if (state.sessionRef) state.sessionRef.current = oldSessionRefCurrent;
1623
- if (oldMessages !== void 0) agent.ctx.state.replaceMessages(oldMessages);
1624
- if (previousDetachFn !== void 0) state.detachActiveTodosCheckpoint = previousDetachFn;
1625
- agent.ctx.state.replaceTodos([...oldTodos]);
1626
- if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
1627
- if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
1628
- } catch (rollbackErr) {
1629
- console.error(
1630
- JSON.stringify({
1631
- level: "error",
1632
- event: "execution.resume_post_swap_rollback_failed",
1633
- message: toErrorMessage(rollbackErr),
1634
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1635
- })
1636
- );
1637
- }
1638
- if (openedWriter && openedWriter !== oldWriter) {
1639
- await openedWriter.close().catch((closeErr) => {
1640
- console.error(
1641
- JSON.stringify({
1642
- level: "error",
1643
- event: "execution.resume_opened_writer_close_failed",
1644
- message: toErrorMessage(closeErr),
1645
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1646
- })
1647
- );
1648
- });
1649
- }
1650
- }
1651
- const reason = err instanceof Error ? err.stack ?? err.message : String(err);
1652
- console.error(
1653
- JSON.stringify({
1654
- level: "error",
1655
- event: "execution.resume_session_failed",
1656
- sessionId: canonicalSessionId,
1657
- stage,
1658
- message: reason,
1659
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1660
- })
1661
- );
1662
- const message = toErrorMessage(err);
1663
- if (err instanceof Error && err.name === "SessionOwnershipConflictError") {
1664
- return fail(stage, message);
1665
- }
1666
- let data = hydrated;
1667
- if (!data) {
1668
- try {
1669
- data = await state.activeSessionStore.load(canonicalSessionId, onLoadProgress);
1670
- } catch {
1671
- data = void 0;
1672
- }
1673
- }
1674
- if (data) {
1675
- try {
1676
- const entries = replaySessionMessages(
1677
- data.messages,
1678
- data.events,
1679
- /* startId */
1680
- 1
1681
- );
1682
- const finalText = lastAssistantTextOf(data.messages);
1683
- fail(stage, message);
1684
- return {
1685
- entries,
1686
- nextId: entries.length + 1,
1687
- sessionId: canonicalSessionId,
1688
- // Deliberately no contextSnapshot: the chip reports the LIVE
1689
- // window, and the live session is still the one we rolled back to.
1690
- // Painting the viewed session's fill over it would be a lie.
1691
- attached: false,
1692
- warnings: [...warnings, `not attached (at ${stage}): ${message}`],
1693
- ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
1694
- };
1695
- } catch (renderErr) {
1696
- console.error(
1697
- JSON.stringify({
1698
- level: "error",
1699
- event: "execution.resume_readonly_render_failed",
1700
- sessionId: canonicalSessionId,
1701
- message: toErrorMessage(renderErr),
1702
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1703
- })
1704
- );
1705
- }
1706
- }
1707
- return fail(stage, message);
1708
- }
1709
- }
1710
-
1711
- // src/boot/tui-session-stub-enrich.ts
1712
- import * as fs3 from "node:fs/promises";
1713
- import * as path8 from "node:path";
1714
- var SLICE_BYTES = 256 * 1024;
1715
- var MAX_TITLE_CHARS = 80;
1716
- var PICKER_POOL_CEILING = 1500;
1717
- var ENRICH_CONCURRENCY = 16;
1718
- function isStubSummary(s) {
1719
- return !s.title && !s.endedAt;
1720
- }
1721
- function containedJsonlPath(sessionsDir, id) {
1722
- const root = path8.resolve(sessionsDir);
1723
- const file = path8.resolve(root, `${id}.jsonl`);
1724
- if (file !== root && !file.startsWith(root + path8.sep)) return void 0;
1725
- return file;
1726
- }
1727
- function stripControlChars(text) {
1728
- return text.replaceAll(
1729
- /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|[\u0000-\u001f\u007f-\u009f]/g,
1730
- ""
1731
- );
1732
- }
1733
- function truncate(text) {
1734
- const oneLine = stripControlChars(text).replaceAll(/\s+/g, " ").trim();
1735
- return oneLine.length > MAX_TITLE_CHARS ? `${oneLine.slice(0, MAX_TITLE_CHARS - 1)}\u2026` : oneLine;
1736
- }
1737
- function contentToText(content) {
1738
- if (typeof content === "string") return content;
1739
- if (Array.isArray(content)) {
1740
- return content.map(
1741
- (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
1742
- ).filter(Boolean).join(" ");
1743
- }
1744
- return "";
1745
- }
1746
- function scanUserInputs(chunk) {
1747
- let first;
1748
- let last;
1749
- for (const line of chunk.split("\n")) {
1750
- const trimmed = line.trim();
1751
- if (!trimmed.includes('"user_input"')) continue;
1752
- try {
1753
- const event = JSON.parse(trimmed);
1754
- if (event.type !== "user_input" || typeof event.ts !== "string") continue;
1755
- const text = contentToText(event.content).trim();
1756
- if (!text) continue;
1757
- const hit = { ts: event.ts, text };
1758
- if (!first) first = hit;
1759
- last = hit;
1760
- } catch {
1761
- }
1762
- }
1763
- return { first, last };
1764
- }
1765
- async function firstUserInput(file) {
1766
- let handle;
1767
- try {
1768
- handle = await fs3.open(file, "r");
1769
- const { size } = await handle.stat();
1770
- const length = Math.min(SLICE_BYTES, size);
1771
- const buffer = Buffer.alloc(length);
1772
- await handle.read(buffer, 0, length, 0);
1773
- const text = buffer.toString("utf8");
1774
- const cutoff = text.lastIndexOf("\n");
1775
- return scanUserInputs(cutoff > 0 ? text.slice(0, cutoff) : text).first;
1776
- } catch {
1777
- return void 0;
1778
- } finally {
1779
- await handle?.close().catch(() => void 0);
1780
- }
1781
- }
1782
- async function lastUserInput(file) {
1783
- let handle;
1784
- try {
1785
- handle = await fs3.open(file, "r");
1786
- const { size } = await handle.stat();
1787
- const start = Math.max(0, size - SLICE_BYTES);
1788
- const length = size - start;
1789
- const buffer = Buffer.alloc(length);
1790
- await handle.read(buffer, 0, length, start);
1791
- const text = buffer.toString("utf8");
1792
- const from = text.indexOf("\n");
1793
- return scanUserInputs(from >= 0 ? text.slice(from + 1) : text).last;
1794
- } catch {
1795
- return void 0;
1796
- } finally {
1797
- await handle?.close().catch(() => void 0);
1798
- }
1799
- }
1800
- function effectiveLastActivity(s) {
1801
- return s.lastActivityAt ?? s.endedAt ?? s.startedAt ?? "";
1802
- }
1803
- function hasActivity(s) {
1804
- return (s.messageCount ?? 0) > 0 || (s.iterationCount ?? 0) > 0 || (s.toolCallCount ?? 0) > 0 || (s.tokenTotal ?? 0) > 0 || Boolean(s.title?.trim()) || Boolean(s.lastUserMessage?.trim());
1805
- }
1806
- async function selectPickerSessions(storeList, sessionsDir, limit) {
1807
- const poolSize = Math.max(limit, Math.min(PICKER_POOL_CEILING, limit * 3));
1808
- const enriched = await enrichStubSummaries(await storeList(poolSize), sessionsDir);
1809
- return enriched.filter(hasActivity).sort((a, b) => effectiveLastActivity(b).localeCompare(effectiveLastActivity(a))).slice(0, limit);
1810
- }
1811
- async function enrichStubSummaries(summaries, sessionsDir) {
1812
- const out = [...summaries];
1813
- const pending = summaries.reduce((indexes, summary, index) => {
1814
- if (isStubSummary(summary)) indexes.push(index);
1815
- return indexes;
1816
- }, []);
1817
- if (pending.length === 0) return out;
1818
- let cursor = 0;
1819
- const worker = async () => {
1820
- for (; ; ) {
1821
- const slot = cursor++;
1822
- if (slot >= pending.length) return;
1823
- const index = pending[slot];
1824
- const summary = out[index];
1825
- const file = containedJsonlPath(sessionsDir, summary.id);
1826
- if (!file) continue;
1827
- try {
1828
- const stat3 = await fs3.stat(file);
1829
- const [first, last] = await Promise.all([firstUserInput(file), lastUserInput(file)]);
1830
- const title = truncate(first?.text ?? "");
1831
- out[index] = {
1832
- ...summary,
1833
- ...title ? { title } : {},
1834
- ...last ? { lastUserMessage: truncate(last.text) } : {},
1835
- lastActivityAt: stat3.mtime.toISOString()
1836
- };
1572
+ const stat3 = await fs3.stat(file);
1573
+ const [first, last] = await Promise.all([firstUserInput(file), lastUserInput(file)]);
1574
+ const title = truncate(first?.text ?? "");
1575
+ out[index] = {
1576
+ ...summary,
1577
+ ...title ? { title } : {},
1578
+ ...last ? { lastUserMessage: truncate(last.text) } : {},
1579
+ lastActivityAt: stat3.mtime.toISOString()
1580
+ };
1837
1581
  } catch {
1838
1582
  }
1839
1583
  }
@@ -2275,1037 +2019,1550 @@ function createSettingsAdapter(ctx) {
2275
2019
  if (s.fleetChatVerbosity !== void 0) {
2276
2020
  if (fleetStreamController?.setMode) fleetStreamController.setMode(s.fleetChatVerbosity);
2277
2021
  }
2278
- applyLiveSettings?.(s);
2279
- return null;
2280
- } catch (err) {
2281
- const message = err instanceof Error ? err.message : String(err);
2282
- console.debug(
2022
+ applyLiveSettings?.(s);
2023
+ return null;
2024
+ } catch (err) {
2025
+ const message = err instanceof Error ? err.message : String(err);
2026
+ console.debug(
2027
+ JSON.stringify({
2028
+ level: "error",
2029
+ event: "execution.settings_persist_failed",
2030
+ message,
2031
+ errorName: err instanceof Error ? err.name : void 0,
2032
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2033
+ })
2034
+ );
2035
+ return message;
2036
+ }
2037
+ }
2038
+ return { getSettings, saveSettings };
2039
+ }
2040
+
2041
+ // src/boot/tui-theme-adapter.ts
2042
+ import * as fs4 from "node:fs/promises";
2043
+ import * as path8 from "node:path";
2044
+ import { THEME_PRESET_IDS } from "@wrongstack/core/types";
2045
+ import { updateJsonObjectFile } from "@wrongstack/core/utils";
2046
+ var VALID_PRESETS = new Set(THEME_PRESET_IDS);
2047
+ function createThemeAdapter({ configStore, wpaths }) {
2048
+ return {
2049
+ getThemePreset: () => {
2050
+ const live = configStore.get();
2051
+ const candidate = live.themePreset;
2052
+ return candidate && VALID_PRESETS.has(candidate) ? candidate : void 0;
2053
+ },
2054
+ saveThemePreset: async (preset) => {
2055
+ if (!VALID_PRESETS.has(preset)) {
2056
+ throw new Error(`Unknown theme preset: ${preset}`);
2057
+ }
2058
+ const configPath = activeProfileConfigPath(wpaths, configStore.get());
2059
+ await fs4.mkdir(path8.dirname(configPath), { recursive: true });
2060
+ await updateJsonObjectFile(configPath, async (config) => {
2061
+ await backupCurrent(void 0, configPath);
2062
+ config.themePreset = preset;
2063
+ });
2064
+ configStore.update({ themePreset: preset });
2065
+ }
2066
+ };
2067
+ }
2068
+
2069
+ // src/boot/worktree-sessions.ts
2070
+ import * as path9 from "node:path";
2071
+ function samePath(a, b) {
2072
+ const ra = path9.resolve(a);
2073
+ const rb = path9.resolve(b);
2074
+ return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
2075
+ }
2076
+ function worktreeOfSession(summary, worktrees) {
2077
+ const checkout = summary.checkout;
2078
+ if (!checkout) return void 0;
2079
+ const wt = worktrees.find((w) => samePath(w.root, checkout));
2080
+ if (!wt || wt.current) return void 0;
2081
+ return {
2082
+ root: wt.root,
2083
+ name: path9.basename(wt.root) || wt.root,
2084
+ ...wt.branch ? { branch: wt.branch } : {}
2085
+ };
2086
+ }
2087
+ function withWorktreeSwitch(resume, lookup, switchProject) {
2088
+ return async (sessionId, ...rest) => {
2089
+ const target = lookup(sessionId);
2090
+ if (target) {
2091
+ const error = await switchProject(target.root, target.name);
2092
+ if (error) throw new Error(`Could not switch to worktree ${target.name}: ${error}`);
2093
+ }
2094
+ return resume(sessionId, ...rest);
2095
+ };
2096
+ }
2097
+
2098
+ // src/brain-menu/panel-service.ts
2099
+ import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
2100
+ var PERSONA_CATALOG = BUILTIN_COUNCIL_PERSONAS.map((persona) => ({
2101
+ id: persona.id,
2102
+ name: persona.name,
2103
+ description: persona.description,
2104
+ defaultVeto: persona.defaultVeto
2105
+ }));
2106
+ var PERSONA_CYCLE = PERSONA_CATALOG.map((persona) => persona.id);
2107
+ function compactEntry(entry) {
2108
+ return entry.provider ? `${entry.provider}/${entry.model}` : entry.model;
2109
+ }
2110
+ function createBrainPanelHost(deps) {
2111
+ const { brainRuntime } = deps;
2112
+ const apply = async (patch) => {
2113
+ try {
2114
+ const { persisted } = brainRuntime.apply(patch);
2115
+ const result = await persisted;
2116
+ return result.ok ? null : `Applied live but NOT saved: ${result.error ?? "unknown error"}`;
2117
+ } catch (err) {
2118
+ return err instanceof Error ? err.message : String(err);
2119
+ }
2120
+ };
2121
+ const currentPool = () => brainRuntime.getSnapshot().models.map(compactEntry);
2122
+ const currentVoters = () => brainRuntime.getSnapshot().council.voters.map((v) => ({ ...v }));
2123
+ const applyVoters = (voters) => apply({ council: { voters: voters.length > 0 ? voters : null } });
2124
+ return {
2125
+ getSettings() {
2126
+ const snap = brainRuntime.getSnapshot();
2127
+ return {
2128
+ mode: snap.mode,
2129
+ riskLevel: snap.maxAutoRisk,
2130
+ strategy: snap.strategy,
2131
+ decisionTimeoutMs: snap.decisionTimeoutMs,
2132
+ humanTimeoutMs: snap.humanTimeoutMs,
2133
+ pool: snap.models.map(compactEntry),
2134
+ poolResolved: snap.poolLabels,
2135
+ usingSessionModel: snap.usingSessionModel,
2136
+ councilEnabled: snap.council.enabled,
2137
+ councilMinRisk: snap.council.minRisk,
2138
+ voters: snap.council.voters.map((v) => ({
2139
+ label: compactEntry(v),
2140
+ persona: v.persona,
2141
+ veto: v.veto,
2142
+ weight: v.weight
2143
+ })),
2144
+ councilSeats: snap.councilLabels,
2145
+ personaCatalog: PERSONA_CATALOG.map((persona) => ({ ...persona })),
2146
+ councilQuorum: snap.council.quorum,
2147
+ councilApproval: snap.council.approval,
2148
+ councilDistinctness: snap.council.distinctness,
2149
+ councilPerCallTimeoutMs: snap.council.perCallTimeoutMs,
2150
+ councilMaxConcurrency: snap.council.maxConcurrency,
2151
+ councilVoterMaxTokens: snap.council.voterMaxTokens,
2152
+ councilDeliberationRounds: snap.council.deliberationRounds,
2153
+ councilJudgeMaxTokens: snap.council.judgeMaxTokens,
2154
+ // The EFFECTIVE judge, not `council.judge`. The configured one is
2155
+ // usually absent, so the panel used to read "auto" and say nothing
2156
+ // about who actually breaks the panel's ties — including when that is
2157
+ // one of the seats that produced the tie.
2158
+ judgeLabel: snap.judgeLabel,
2159
+ judgeConfigured: snap.council.judge !== void 0,
2160
+ judgeIsVoter: snap.judgeIsVoter,
2161
+ ledgerEnabled: snap.ledger.enabled,
2162
+ autoDenyAfterFailures: snap.ledger.autoDenyAfterFailures,
2163
+ terminalPolicy: snap.terminalPolicy,
2164
+ heuristics: {
2165
+ lowRiskAutoAnswer: snap.heuristics.lowRiskAutoAnswer,
2166
+ blockedResolved: snap.heuristics.blockedResolved,
2167
+ deadlockSkip: snap.heuristics.deadlockSkip,
2168
+ retryExhausted: snap.heuristics.retryExhausted,
2169
+ continuePing: snap.heuristics.continuePing,
2170
+ blockedResolvedMarkers: snap.heuristics.blockedResolvedMarkers
2171
+ },
2172
+ llmMaxTokens: snap.llm.maxTokens,
2173
+ llmRejectUncertain: snap.llm.rejectUncertain,
2174
+ llmMinConfidence: snap.llm.minConfidence,
2175
+ llmDenyIsTerminal: snap.llm.denyIsTerminal,
2176
+ cacheEnabled: snap.cache.enabled,
2177
+ cacheTtlMs: snap.cache.ttlMs,
2178
+ cacheMaxEntries: snap.cache.maxEntries,
2179
+ cacheHits: snap.cache.hits,
2180
+ cacheMisses: snap.cache.misses,
2181
+ cacheSize: snap.cache.size,
2182
+ traceEnabled: snap.trace.enabled,
2183
+ traceContent: snap.trace.content,
2184
+ tracePath: snap.trace.path,
2185
+ circuitState: snap.circuit?.state,
2186
+ circuitFailures: snap.circuit?.consecutiveFailures,
2187
+ ruleCount: snap.rules.length,
2188
+ ruleErrors: [...snap.ruleErrors]
2189
+ };
2190
+ },
2191
+ setMode: (mode) => apply({ mode }),
2192
+ setRisk: (level) => apply({ maxAutoRisk: level }),
2193
+ setStrategy: (strategy) => apply({ strategy }),
2194
+ setDecisionTimeout: (ms) => apply({ decisionTimeoutMs: ms ?? null }),
2195
+ setHumanTimeout: (ms) => apply({ humanTimeoutMs: ms ?? null }),
2196
+ addPoolModel: (providerId, model) => apply({ models: [...currentPool(), `${providerId}/${model}`] }),
2197
+ removePoolModel: (index) => {
2198
+ const next = currentPool().filter((_, i) => i !== index);
2199
+ return apply({ models: next.length > 0 ? next : null });
2200
+ },
2201
+ clearPool: () => apply({ models: null }),
2202
+ setCouncilEnabled: (on) => apply({ council: { enabled: on } }),
2203
+ setCouncilMinRisk: (risk) => apply({ council: { minRisk: risk } }),
2204
+ addVoter: (providerId, model) => {
2205
+ const voters = currentVoters();
2206
+ const persona = PERSONA_CYCLE[voters.length % PERSONA_CYCLE.length];
2207
+ return applyVoters([...voters, { provider: providerId, model, persona }]);
2208
+ },
2209
+ removeVoter: (index) => applyVoters(currentVoters().filter((_, i) => i !== index)),
2210
+ cycleVoterPersona: (index) => {
2211
+ const voters = currentVoters();
2212
+ const voter = voters[index];
2213
+ if (!voter) return Promise.resolve("No such voter.");
2214
+ const at = PERSONA_CYCLE.indexOf(voter.persona ?? "");
2215
+ voter.persona = PERSONA_CYCLE[(at + 1) % PERSONA_CYCLE.length];
2216
+ return applyVoters(voters);
2217
+ },
2218
+ toggleVoterVeto: (index) => {
2219
+ const voters = currentVoters();
2220
+ const voter = voters[index];
2221
+ if (!voter) return Promise.resolve("No such voter.");
2222
+ voter.veto = !voter.veto;
2223
+ return applyVoters(voters);
2224
+ },
2225
+ setJudge: (providerId, model) => apply({ council: { judge: `${providerId}/${model}` } }),
2226
+ clearJudge: () => apply({ council: { judge: null } }),
2227
+ // Resolution + budget knobs. `BrainCouncilPatch` accepts null for the
2228
+ // three integer fields, so `undefined` here really does clear to default.
2229
+ setCouncilQuorum: (fraction) => apply({ council: { quorum: fraction } }),
2230
+ setCouncilApproval: (fraction) => apply({ council: { approval: fraction } }),
2231
+ setCouncilDistinctness: (mode) => apply({ council: { distinctness: mode } }),
2232
+ setCouncilPerCallTimeout: (ms) => apply({ council: { perCallTimeoutMs: ms ?? null } }),
2233
+ setCouncilMaxConcurrency: (count) => apply({ council: { maxConcurrency: count ?? null } }),
2234
+ setCouncilVoterMaxTokens: (tokens) => apply({ council: { voterMaxTokens: tokens ?? null } }),
2235
+ setCouncilDeliberationRounds: (rounds) => apply({ council: { deliberationRounds: rounds ?? null } }),
2236
+ setCouncilJudgeMaxTokens: (tokens) => apply({ council: { judgeMaxTokens: tokens ?? null } }),
2237
+ setLedgerEnabled: (on) => apply({ ledger: { enabled: on } }),
2238
+ setAutoDeny: (count) => apply({ ledger: { autoDenyAfterFailures: count ?? null } }),
2239
+ setTerminalPolicy: (policy) => apply({ terminalPolicy: policy }),
2240
+ // Every sub-block below is MERGED field-by-field by `mergePatch`, so a
2241
+ // single-key patch leaves the operator's other knobs alone.
2242
+ setHeuristic: (key, on) => apply({ heuristics: { [key]: on } }),
2243
+ setLlmMaxTokens: (tokens) => apply({ llm: { maxTokens: tokens ?? null } }),
2244
+ setLlmRejectUncertain: (on) => apply({ llm: { rejectUncertain: on } }),
2245
+ setLlmMinConfidence: (value) => apply({ llm: { minConfidence: value } }),
2246
+ setLlmDenyIsTerminal: (mode) => apply({ llm: { denyIsTerminal: mode } }),
2247
+ setCacheEnabled: (on) => apply({ cache: { enabled: on } }),
2248
+ setCacheTtl: (ms) => apply({ cache: { ttlMs: ms } }),
2249
+ setCacheMaxEntries: (count) => apply({ cache: { maxEntries: count } }),
2250
+ setTraceEnabled: (on) => apply({ trace: { enabled: on } }),
2251
+ setTraceContent: (content) => apply({ trace: { content } })
2252
+ };
2253
+ }
2254
+
2255
+ // src/chimera-reviewer-policy.ts
2256
+ import { parseModelRef } from "@wrongstack/core/agent";
2257
+ import {
2258
+ buildReviewerModelPool,
2259
+ selectRoundRobinReviewerAssignment
2260
+ } from "@wrongstack/core/plugin";
2261
+ function resolveReviewerFallbackModels(reviewFallbackModels, sessionRef) {
2262
+ const base = reviewFallbackModels && reviewFallbackModels.length > 0 ? [...reviewFallbackModels] : [];
2263
+ if (sessionRef && !base.includes(sessionRef)) {
2264
+ base.push(sessionRef);
2265
+ }
2266
+ return base;
2267
+ }
2268
+ var reviewerRoundRobinCursor = 0;
2269
+ function __resetReviewerRoundRobinCursor(value = 0) {
2270
+ reviewerRoundRobinCursor = value;
2271
+ }
2272
+ function assignReviewerModels(provider, model, fallbackModels, selection = "round-robin", statusTracker, random = Math.random) {
2273
+ const pool = buildReviewerModelPool(provider, model, fallbackModels, statusTracker);
2274
+ if (pool.length <= 1) {
2275
+ return {
2276
+ provider,
2277
+ model,
2278
+ fallbackModels: [...fallbackModels]
2279
+ };
2280
+ }
2281
+ const cursor = selection === "random" ? Math.min(pool.length - 1, Math.floor(Math.max(0, random()) * pool.length)) : reviewerRoundRobinCursor;
2282
+ const assignment = selectRoundRobinReviewerAssignment(
2283
+ pool,
2284
+ cursor,
2285
+ provider,
2286
+ model,
2287
+ statusTracker
2288
+ );
2289
+ if (selection === "round-robin") reviewerRoundRobinCursor = assignment.nextCursor;
2290
+ return {
2291
+ provider: assignment.provider,
2292
+ model: assignment.model,
2293
+ fallbackModels: assignment.fallbackModels
2294
+ };
2295
+ }
2296
+ function assignReviewerModelsRoundRobin(provider, model, fallbackModels, statusTracker) {
2297
+ return assignReviewerModels(provider, model, fallbackModels, "round-robin", statusTracker);
2298
+ }
2299
+ var MAX_REVIEWER_ATTEMPTS = 4;
2300
+ var REVIEWER_ATTEMPT_TIMEOUT_MS = 9e5;
2301
+ var REVIEWER_LADDER_BUDGET_MS = 12e5;
2302
+ var REVIEWER_MIN_ATTEMPT_MS = 12e4;
2303
+ function buildReviewerAttemptLadder(input) {
2304
+ const assignedChain = [...input.assigned.fallbackModels ?? []];
2305
+ const profileChain = [...input.profileChain ?? []];
2306
+ const rungs = [];
2307
+ const seen = /* @__PURE__ */ new Set();
2308
+ const key = (provider, model) => `${provider.trim()}/${model.trim()}`;
2309
+ const push = (provider, model, fallbackModels, tier) => {
2310
+ const p = provider.trim();
2311
+ const m = model.trim();
2312
+ if (!p || !m) return;
2313
+ if (seen.has(key(p, m))) return;
2314
+ seen.add(key(p, m));
2315
+ rungs.push({ provider: p, model: m, fallbackModels, tier });
2316
+ };
2317
+ push(input.assigned.provider, input.assigned.model, [...assignedChain], "primary");
2318
+ for (const [chain, tier] of [
2319
+ [assignedChain, "fallback"],
2320
+ [profileChain, "fallback"]
2321
+ ]) {
2322
+ for (let i = 0; i < chain.length; i++) {
2323
+ const parsed = parseModelRef(chain[i]);
2324
+ push(parsed.provider ?? "", parsed.model, chain.slice(i + 1), tier);
2325
+ }
2326
+ }
2327
+ const sessionKey = key(input.session.provider, input.session.model);
2328
+ push(input.session.provider, input.session.model, [], "session");
2329
+ const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_REVIEWER_ATTEMPTS));
2330
+ if (rungs.length <= cap) return rungs;
2331
+ const trimmed = rungs.slice(0, cap);
2332
+ if (!trimmed.some((r) => key(r.provider, r.model) === sessionKey)) {
2333
+ const sessionRung = rungs.find((r) => key(r.provider, r.model) === sessionKey);
2334
+ if (sessionRung) trimmed[trimmed.length - 1] = sessionRung;
2335
+ }
2336
+ return trimmed;
2337
+ }
2338
+ var MAX_MUTATING_ATTEMPTS = 2;
2339
+ function buildMutatingAgentLadder(input) {
2340
+ const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_MUTATING_ATTEMPTS));
2341
+ const inherit = {
2342
+ provider: "",
2343
+ model: "",
2344
+ fallbackModels: [],
2345
+ tier: "inherit"
2346
+ };
2347
+ if (cap === 1) return [inherit];
2348
+ return [
2349
+ inherit,
2350
+ ...buildReviewerAttemptLadder({
2351
+ assigned: { provider: "", model: "" },
2352
+ profileChain: input.profileChain,
2353
+ session: input.session,
2354
+ maxAttempts: cap - 1
2355
+ })
2356
+ ];
2357
+ }
2358
+ function shouldAdvanceReviewerLadder(status, error) {
2359
+ if (status === "success") return false;
2360
+ if (status === "stopped") return false;
2361
+ if (error?.kind === "aborted_by_parent") return false;
2362
+ return true;
2363
+ }
2364
+ function reviewerAttemptTimeoutMs(remainingMs, attemptTimeoutMs = REVIEWER_ATTEMPT_TIMEOUT_MS, minAttemptMs = REVIEWER_MIN_ATTEMPT_MS) {
2365
+ if (remainingMs < minAttemptMs) return 0;
2366
+ return Math.min(attemptTimeoutMs, Math.floor(remainingMs));
2367
+ }
2368
+ var CHIMERA_REVIEW_READ_ONLY_TOOLS = Object.freeze([
2369
+ "read",
2370
+ "grep",
2371
+ "glob",
2372
+ "tree",
2373
+ "codebase-stats",
2374
+ "codebase-search",
2375
+ "codebase-skeleton",
2376
+ "codebase-context",
2377
+ "codebase-repo-map",
2378
+ "codebase-incoming-calls",
2379
+ "codebase-outgoing-calls"
2380
+ ]);
2381
+ function applyChimeraReviewerReadOnlyPolicy(config) {
2382
+ return {
2383
+ ...config,
2384
+ // A security boundary, not an additive default: discard any inherited
2385
+ // mutation tools/capabilities so Chimera reviewers cannot repair files.
2386
+ tools: [...CHIMERA_REVIEW_READ_ONLY_TOOLS],
2387
+ allowedCapabilities: ["fs.read"],
2388
+ worktree: "off",
2389
+ // Mandatory model-driven completion: a reviewer must never be killed by
2390
+ // the wall-clock watchdog. At its deadline (or when the leader's session
2391
+ // ends and `Director.requestFinish()` fires) it receives an in-band
2392
+ // `subagent.finish_requested` notification between tool batches and
2393
+ // finishes its report in its own turn within the granted grace window.
2394
+ // Only if that window also elapses does the terminal stop apply — the
2395
+ // reviewer's bounded maximum lifetime.
2396
+ gracefulFinish: true
2397
+ };
2398
+ }
2399
+
2400
+ // src/execution-cleanup.ts
2401
+ async function finalizeExecutionCleanup(input) {
2402
+ const {
2403
+ offStorageObservability,
2404
+ fleetStatusLine,
2405
+ onCoordinatorStop,
2406
+ stats,
2407
+ renderer,
2408
+ detachTodosCheckpoint,
2409
+ mcpRegistry,
2410
+ agent,
2411
+ session,
2412
+ tokenCounter,
2413
+ events,
2414
+ chimeraWork,
2415
+ director,
2416
+ reader
2417
+ } = input;
2418
+ offStorageObservability();
2419
+ fleetStatusLine?.stop();
2420
+ onCoordinatorStop?.();
2421
+ try {
2422
+ stats.render(renderer);
2423
+ } catch (_err) {
2424
+ }
2425
+ await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
2426
+ try {
2427
+ const { getProcessRegistry: getProcessRegistry2 } = await import("@wrongstack/tools");
2428
+ getProcessRegistry2().killAll({ force: true, includeProtected: true });
2429
+ } catch (err) {
2430
+ console.warn(
2431
+ JSON.stringify({
2432
+ level: "warn",
2433
+ event: "shutdown.process_kill_all_failed",
2434
+ message: `Process registry killAll failed: ${err instanceof Error ? err.message : String(err)}`,
2435
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2436
+ })
2437
+ );
2438
+ }
2439
+ await mcpRegistry.stopAll().catch((err) => {
2440
+ console.warn(
2441
+ JSON.stringify({
2442
+ level: "warn",
2443
+ event: "shutdown.mcp_stop_failed",
2444
+ message: `MCP registry stopAll failed: ${err instanceof Error ? err.message : String(err)}`,
2445
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2446
+ })
2447
+ );
2448
+ });
2449
+ const activeSession = agent.ctx.session ?? session;
2450
+ const pending = activeSession.pendingToolUses;
2451
+ await activeSession.append({
2452
+ type: "session_end",
2453
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2454
+ usage: tokenCounter.total(),
2455
+ pendingToolUses: pending.length > 0 ? pending : void 0
2456
+ }).catch((err) => {
2457
+ console.warn(
2458
+ JSON.stringify({
2459
+ level: "warn",
2460
+ event: "shutdown.session_end_append_failed",
2461
+ message: `session_end append failed: ${err instanceof Error ? err.message : String(err)}`,
2462
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2463
+ })
2464
+ );
2465
+ });
2466
+ const sessionEndProducers = /* @__PURE__ */ new Set();
2467
+ events.emit("session.ended", {
2468
+ id: activeSession.id,
2469
+ sessionId: activeSession.id,
2470
+ usage: tokenCounter.total(),
2471
+ waitUntil: (work) => {
2472
+ const tracked = Promise.resolve(work).finally(() => sessionEndProducers.delete(tracked));
2473
+ void tracked.catch(() => void 0);
2474
+ sessionEndProducers.add(tracked);
2475
+ }
2476
+ });
2477
+ try {
2478
+ const model = agent.ctx.model;
2479
+ if (!model) {
2480
+ console.warn(
2481
+ JSON.stringify({
2482
+ level: "warn",
2483
+ event: "shutdown.wrongtrace_telemetry_skipped",
2484
+ message: "Skipping WrongTrace telemetry: agent model unknown at session end",
2485
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2486
+ })
2487
+ );
2488
+ } else {
2489
+ const { reportWrongTraceSessionTelemetry } = await import("./wrongtrace-telemetry-3ASFKOZO.js");
2490
+ const usage = tokenCounter.total();
2491
+ const report = reportWrongTraceSessionTelemetry({
2492
+ sessionId: activeSession.id,
2493
+ agentName: "wrongstack-cli",
2494
+ model,
2495
+ provider: agent.ctx.provider?.id ?? "",
2496
+ usage: {
2497
+ input: usage.input,
2498
+ output: usage.output,
2499
+ cacheRead: usage.cacheRead,
2500
+ cacheWrite: usage.cacheWrite
2501
+ },
2502
+ costUsd: tokenCounter.estimateCost().total
2503
+ });
2504
+ const deadline = new Promise((resolve5) => {
2505
+ const t = setTimeout(resolve5, 5e3);
2506
+ t.unref?.();
2507
+ });
2508
+ const tracked = Promise.race([report, deadline]).finally(
2509
+ () => sessionEndProducers.delete(tracked)
2510
+ );
2511
+ void tracked.catch(() => void 0);
2512
+ sessionEndProducers.add(tracked);
2513
+ }
2514
+ } catch {
2515
+ }
2516
+ try {
2517
+ const { snapshotGateDecisions, persistWrongTraceGateCounters } = await import("./wrongtrace-gate-counters-7C2EAAZL.js");
2518
+ const projectRoot = agent.ctx.projectRoot;
2519
+ if (projectRoot) {
2520
+ await persistWrongTraceGateCounters(projectRoot, snapshotGateDecisions());
2521
+ }
2522
+ } catch {
2523
+ }
2524
+ if (director) {
2525
+ try {
2526
+ director.requestFinish("leader session ended \u2014 finish your review now");
2527
+ } catch (finishErr) {
2528
+ console.warn(
2529
+ JSON.stringify({
2530
+ level: "warn",
2531
+ event: "shutdown.subagent_finish_notify_failed",
2532
+ message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2534
+ })
2535
+ );
2536
+ }
2537
+ }
2538
+ try {
2539
+ while (sessionEndProducers.size > 0) {
2540
+ await Promise.allSettled([...sessionEndProducers]);
2541
+ }
2542
+ if (director) {
2543
+ try {
2544
+ director.requestFinish("leader session ended \u2014 finish your review now");
2545
+ } catch {
2546
+ }
2547
+ }
2548
+ await chimeraWork?.drainAndClose();
2549
+ } catch (err) {
2550
+ console.warn(
2551
+ JSON.stringify({
2552
+ level: "warn",
2553
+ event: "shutdown.chimera_work_failed",
2554
+ message: `Pending chimera work failed: ${err instanceof Error ? err.message : String(err)}`,
2555
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2556
+ })
2557
+ );
2558
+ }
2559
+ if (director) {
2560
+ try {
2561
+ director.requestFinish("leader session ended \u2014 finish your review now");
2562
+ } catch (finishErr) {
2563
+ console.warn(
2283
2564
  JSON.stringify({
2284
- level: "error",
2285
- event: "execution.settings_persist_failed",
2286
- message,
2287
- errorName: err instanceof Error ? err.name : void 0,
2565
+ level: "warn",
2566
+ event: "shutdown.subagent_finish_notify_failed",
2567
+ message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2568
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2569
+ })
2570
+ );
2571
+ }
2572
+ try {
2573
+ await director.terminateAll();
2574
+ } catch (termErr) {
2575
+ console.warn(
2576
+ JSON.stringify({
2577
+ level: "warn",
2578
+ event: "shutdown.director_terminate_all_failed",
2579
+ message: termErr instanceof Error ? termErr.message : String(termErr),
2288
2580
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2289
2581
  })
2290
2582
  );
2291
- return message;
2292
2583
  }
2293
2584
  }
2294
- return { getSettings, saveSettings };
2585
+ await activeSession.close().catch((err) => {
2586
+ console.warn(
2587
+ JSON.stringify({
2588
+ level: "warn",
2589
+ event: "shutdown.session_close_failed",
2590
+ message: `Session close failed: ${err instanceof Error ? err.message : String(err)}`,
2591
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2592
+ })
2593
+ );
2594
+ });
2595
+ await reader.close().catch((err) => {
2596
+ console.warn(
2597
+ JSON.stringify({
2598
+ level: "warn",
2599
+ event: "shutdown.reader_close_failed",
2600
+ message: `Input reader close failed: ${err instanceof Error ? err.message : String(err)}`,
2601
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2602
+ })
2603
+ );
2604
+ });
2295
2605
  }
2296
2606
 
2297
- // src/boot/tui-theme-adapter.ts
2298
- import * as fs4 from "node:fs/promises";
2299
- import * as path9 from "node:path";
2300
- import { THEME_PRESET_IDS } from "@wrongstack/core/types";
2301
- import { updateJsonObjectFile } from "@wrongstack/core/utils";
2302
- var VALID_PRESETS = new Set(THEME_PRESET_IDS);
2303
- function createThemeAdapter({ configStore, wpaths }) {
2304
- return {
2305
- getThemePreset: () => {
2306
- const live = configStore.get();
2307
- const candidate = live.themePreset;
2308
- return candidate && VALID_PRESETS.has(candidate) ? candidate : void 0;
2309
- },
2310
- saveThemePreset: async (preset) => {
2311
- if (!VALID_PRESETS.has(preset)) {
2312
- throw new Error(`Unknown theme preset: ${preset}`);
2313
- }
2314
- const configPath = activeProfileConfigPath(wpaths, configStore.get());
2315
- await fs4.mkdir(path9.dirname(configPath), { recursive: true });
2316
- await updateJsonObjectFile(configPath, async (config) => {
2317
- await backupCurrent(void 0, configPath);
2318
- config.themePreset = preset;
2319
- });
2320
- configStore.update({ themePreset: preset });
2607
+ // src/execution-kanban-dispatch.ts
2608
+ import { randomUUID } from "node:crypto";
2609
+ import { missingRequiredRuntimeTools } from "@wrongstack/core/agent-catalog";
2610
+ import { WIDE_SUBAGENT_CAPABILITIES } from "@wrongstack/core/security";
2611
+ import { stripFrontmatter } from "@wrongstack/core/skills";
2612
+
2613
+ // src/kanban-dispatch-route.ts
2614
+ import { fallbackProfileChain, parseModelRef as parseModelRef2 } from "@wrongstack/core/agent";
2615
+ import { resolveTier } from "@wrongstack/core/coordination";
2616
+ function resolveKanbanDispatchRoute(config, spawnOpts) {
2617
+ let provider = spawnOpts?.provider;
2618
+ let model = spawnOpts?.model;
2619
+ let fallbackModels = spawnOpts?.fallbackModels;
2620
+ let tier;
2621
+ if (spawnOpts?.fallbackProfile) {
2622
+ const chain = fallbackProfileChain(config, spawnOpts.fallbackProfile);
2623
+ const primary = chain[0] ? parseModelRef2(chain[0]) : void 0;
2624
+ if (primary?.model) {
2625
+ provider = primary.provider ?? config.provider;
2626
+ model = primary.model;
2627
+ fallbackModels = spawnOpts.fallbackModels ?? chain.slice(1);
2628
+ }
2629
+ } else if (spawnOpts?.tier || spawnOpts?.role) {
2630
+ const resolved = resolveTier(config, {
2631
+ ...spawnOpts.tier ? { tier: spawnOpts.tier } : {},
2632
+ ...spawnOpts.role ? { role: spawnOpts.role } : {}
2633
+ });
2634
+ if (resolved?.model) {
2635
+ tier = resolved.tier;
2636
+ provider = provider ?? resolved.provider ?? config.provider;
2637
+ model = model ?? resolved.model;
2638
+ fallbackModels = fallbackModels ?? resolved.fallbackModels?.slice() ?? void 0;
2321
2639
  }
2322
- };
2640
+ }
2641
+ return { provider, model, fallbackModels, ...tier ? { tier } : {} };
2323
2642
  }
2324
2643
 
2325
- // src/brain-menu/panel-service.ts
2326
- import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
2327
- var PERSONA_CATALOG = BUILTIN_COUNCIL_PERSONAS.map((persona) => ({
2328
- id: persona.id,
2329
- name: persona.name,
2330
- description: persona.description,
2331
- defaultVeto: persona.defaultVeto
2332
- }));
2333
- var PERSONA_CYCLE = PERSONA_CATALOG.map((persona) => persona.id);
2334
- function compactEntry(entry) {
2335
- return entry.provider ? `${entry.provider}/${entry.model}` : entry.model;
2336
- }
2337
- function createBrainPanelHost(deps) {
2338
- const { brainRuntime } = deps;
2339
- const apply = async (patch) => {
2340
- try {
2341
- const { persisted } = brainRuntime.apply(patch);
2342
- const result = await persisted;
2343
- return result.ok ? null : `Applied live but NOT saved: ${result.error ?? "unknown error"}`;
2344
- } catch (err) {
2345
- return err instanceof Error ? err.message : String(err);
2644
+ // src/execution-kanban-dispatch.ts
2645
+ function createKanbanDispatchHandler({
2646
+ config,
2647
+ events,
2648
+ skillLoader,
2649
+ sddSubagentFactory
2650
+ }) {
2651
+ if (!sddSubagentFactory) return {};
2652
+ const onKanbanDispatch = async (description, spawnOpts) => {
2653
+ const subagentId = `kanban-${randomUUID().slice(0, 8)}`;
2654
+ const taskId = randomUUID();
2655
+ const name = spawnOpts?.name ?? "kanban-agent";
2656
+ const {
2657
+ provider: resolvedProvider,
2658
+ model: resolvedModel,
2659
+ fallbackModels: resolvedFallbackModels
2660
+ } = resolveKanbanDispatchRoute(config, spawnOpts);
2661
+ let agentDescription = description;
2662
+ if (spawnOpts?.skills?.length && skillLoader) {
2663
+ const availableToolNames = spawnOpts.tools;
2664
+ const loaded = [];
2665
+ for (const skillName of spawnOpts.skills) {
2666
+ try {
2667
+ const manifest = await skillLoader.find(skillName);
2668
+ if (!manifest) {
2669
+ process.emitWarning(`Kanban dispatch skill not found, skipped: ${skillName}`, {
2670
+ code: "WRONGSTACK_KANBAN_SKILL_SKIPPED"
2671
+ });
2672
+ continue;
2673
+ }
2674
+ if (availableToolNames !== void 0 && missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames).length > 0) {
2675
+ process.emitWarning(
2676
+ `Kanban dispatch skill "${skillName}" needs tools this worker does not have, skipped.`,
2677
+ { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2678
+ );
2679
+ continue;
2680
+ }
2681
+ const body = stripFrontmatter(await skillLoader.readBody(skillName)).trim();
2682
+ if (!body) continue;
2683
+ loaded.push(`## Required skill: ${skillName}
2684
+
2685
+ ${body}`);
2686
+ } catch (err) {
2687
+ process.emitWarning(
2688
+ `Kanban dispatch skill "${skillName}" could not be loaded, skipped: ${err instanceof Error ? err.message : String(err)}`,
2689
+ { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2690
+ );
2691
+ }
2692
+ }
2693
+ if (loaded.length > 0) {
2694
+ agentDescription = `${description}
2695
+
2696
+ # Required agentic skill instructions
2697
+
2698
+ ${loaded.join("\n\n")}`;
2699
+ }
2346
2700
  }
2701
+ void (async () => {
2702
+ let built;
2703
+ let completionReported = false;
2704
+ try {
2705
+ if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2706
+ built = await sddSubagentFactory(
2707
+ {
2708
+ id: subagentId,
2709
+ name,
2710
+ role: "kanban-agent",
2711
+ prompt: agentDescription,
2712
+ allowedCapabilities: spawnOpts?.allowedCapabilities ?? WIDE_SUBAGENT_CAPABILITIES,
2713
+ ...resolvedProvider ? { provider: resolvedProvider } : {},
2714
+ ...resolvedModel ? { model: resolvedModel } : {},
2715
+ ...resolvedFallbackModels ? { fallbackModels: resolvedFallbackModels } : {},
2716
+ ...spawnOpts?.tools ? { tools: spawnOpts.tools } : {}
2717
+ },
2718
+ ...spawnOpts?.context ? [{ id: taskId, description: agentDescription, context: spawnOpts.context }] : []
2719
+ );
2720
+ if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2721
+ const result = await built.agent.run(
2722
+ agentDescription,
2723
+ ...spawnOpts?.signal ? [{ signal: spawnOpts.signal }] : []
2724
+ );
2725
+ completionReported = true;
2726
+ await spawnOpts?.onDone?.({
2727
+ status: result.status === "done" ? "completed" : "failed",
2728
+ result: result.finalText,
2729
+ ..."error" in result && result.error?.message ? { error: result.error.message } : {}
2730
+ });
2731
+ } catch (err) {
2732
+ if (!completionReported) {
2733
+ await spawnOpts?.onDone?.({
2734
+ status: "failed",
2735
+ error: err instanceof Error ? err.message : String(err)
2736
+ });
2737
+ }
2738
+ throw err;
2739
+ } finally {
2740
+ await built?.dispose?.();
2741
+ }
2742
+ })().catch((err) => {
2743
+ events.emit("error", {
2744
+ err: err instanceof Error ? err : new Error(String(err)),
2745
+ phase: "kanban.dispatch"
2746
+ });
2747
+ });
2748
+ const tags = [];
2749
+ if (resolvedProvider) tags.push(resolvedProvider);
2750
+ if (resolvedModel) tags.push(resolvedModel);
2751
+ if (resolvedFallbackModels?.length) {
2752
+ tags.push(`fallback=${resolvedFallbackModels.join(",")}`);
2753
+ }
2754
+ if (spawnOpts?.fallbackProfile) tags.push(`profile=${spawnOpts.fallbackProfile}`);
2755
+ if (spawnOpts?.skills?.length) tags.push(`skills=${spawnOpts.skills.join(",")}`);
2756
+ if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
2757
+ const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
2758
+ return `Spawned subagent ${subagentId}${tag} for task ${taskId}.`;
2347
2759
  };
2348
- const currentPool = () => brainRuntime.getSnapshot().models.map(compactEntry);
2349
- const currentVoters = () => brainRuntime.getSnapshot().council.voters.map((v) => ({ ...v }));
2350
- const applyVoters = (voters) => apply({ council: { voters: voters.length > 0 ? voters : null } });
2351
- return {
2352
- getSettings() {
2353
- const snap = brainRuntime.getSnapshot();
2354
- return {
2355
- mode: snap.mode,
2356
- riskLevel: snap.maxAutoRisk,
2357
- strategy: snap.strategy,
2358
- decisionTimeoutMs: snap.decisionTimeoutMs,
2359
- humanTimeoutMs: snap.humanTimeoutMs,
2360
- pool: snap.models.map(compactEntry),
2361
- poolResolved: snap.poolLabels,
2362
- usingSessionModel: snap.usingSessionModel,
2363
- councilEnabled: snap.council.enabled,
2364
- councilMinRisk: snap.council.minRisk,
2365
- voters: snap.council.voters.map((v) => ({
2366
- label: compactEntry(v),
2367
- persona: v.persona,
2368
- veto: v.veto,
2369
- weight: v.weight
2370
- })),
2371
- councilSeats: snap.councilLabels,
2372
- personaCatalog: PERSONA_CATALOG.map((persona) => ({ ...persona })),
2373
- councilQuorum: snap.council.quorum,
2374
- councilApproval: snap.council.approval,
2375
- councilDistinctness: snap.council.distinctness,
2376
- councilPerCallTimeoutMs: snap.council.perCallTimeoutMs,
2377
- councilMaxConcurrency: snap.council.maxConcurrency,
2378
- councilVoterMaxTokens: snap.council.voterMaxTokens,
2379
- councilDeliberationRounds: snap.council.deliberationRounds,
2380
- councilJudgeMaxTokens: snap.council.judgeMaxTokens,
2381
- // The EFFECTIVE judge, not `council.judge`. The configured one is
2382
- // usually absent, so the panel used to read "auto" and say nothing
2383
- // about who actually breaks the panel's ties — including when that is
2384
- // one of the seats that produced the tie.
2385
- judgeLabel: snap.judgeLabel,
2386
- judgeConfigured: snap.council.judge !== void 0,
2387
- judgeIsVoter: snap.judgeIsVoter,
2388
- ledgerEnabled: snap.ledger.enabled,
2389
- autoDenyAfterFailures: snap.ledger.autoDenyAfterFailures,
2390
- terminalPolicy: snap.terminalPolicy,
2391
- heuristics: {
2392
- lowRiskAutoAnswer: snap.heuristics.lowRiskAutoAnswer,
2393
- blockedResolved: snap.heuristics.blockedResolved,
2394
- deadlockSkip: snap.heuristics.deadlockSkip,
2395
- retryExhausted: snap.heuristics.retryExhausted,
2396
- continuePing: snap.heuristics.continuePing,
2397
- blockedResolvedMarkers: snap.heuristics.blockedResolvedMarkers
2398
- },
2399
- llmMaxTokens: snap.llm.maxTokens,
2400
- llmRejectUncertain: snap.llm.rejectUncertain,
2401
- llmMinConfidence: snap.llm.minConfidence,
2402
- llmDenyIsTerminal: snap.llm.denyIsTerminal,
2403
- cacheEnabled: snap.cache.enabled,
2404
- cacheTtlMs: snap.cache.ttlMs,
2405
- cacheMaxEntries: snap.cache.maxEntries,
2406
- cacheHits: snap.cache.hits,
2407
- cacheMisses: snap.cache.misses,
2408
- cacheSize: snap.cache.size,
2409
- traceEnabled: snap.trace.enabled,
2410
- traceContent: snap.trace.content,
2411
- tracePath: snap.trace.path,
2412
- circuitState: snap.circuit?.state,
2413
- circuitFailures: snap.circuit?.consecutiveFailures,
2414
- ruleCount: snap.rules.length,
2415
- ruleErrors: [...snap.ruleErrors]
2416
- };
2417
- },
2418
- setMode: (mode) => apply({ mode }),
2419
- setRisk: (level) => apply({ maxAutoRisk: level }),
2420
- setStrategy: (strategy) => apply({ strategy }),
2421
- setDecisionTimeout: (ms) => apply({ decisionTimeoutMs: ms ?? null }),
2422
- setHumanTimeout: (ms) => apply({ humanTimeoutMs: ms ?? null }),
2423
- addPoolModel: (providerId, model) => apply({ models: [...currentPool(), `${providerId}/${model}`] }),
2424
- removePoolModel: (index) => {
2425
- const next = currentPool().filter((_, i) => i !== index);
2426
- return apply({ models: next.length > 0 ? next : null });
2427
- },
2428
- clearPool: () => apply({ models: null }),
2429
- setCouncilEnabled: (on) => apply({ council: { enabled: on } }),
2430
- setCouncilMinRisk: (risk) => apply({ council: { minRisk: risk } }),
2431
- addVoter: (providerId, model) => {
2432
- const voters = currentVoters();
2433
- const persona = PERSONA_CYCLE[voters.length % PERSONA_CYCLE.length];
2434
- return applyVoters([...voters, { provider: providerId, model, persona }]);
2435
- },
2436
- removeVoter: (index) => applyVoters(currentVoters().filter((_, i) => i !== index)),
2437
- cycleVoterPersona: (index) => {
2438
- const voters = currentVoters();
2439
- const voter = voters[index];
2440
- if (!voter) return Promise.resolve("No such voter.");
2441
- const at = PERSONA_CYCLE.indexOf(voter.persona ?? "");
2442
- voter.persona = PERSONA_CYCLE[(at + 1) % PERSONA_CYCLE.length];
2443
- return applyVoters(voters);
2444
- },
2445
- toggleVoterVeto: (index) => {
2446
- const voters = currentVoters();
2447
- const voter = voters[index];
2448
- if (!voter) return Promise.resolve("No such voter.");
2449
- voter.veto = !voter.veto;
2450
- return applyVoters(voters);
2451
- },
2452
- setJudge: (providerId, model) => apply({ council: { judge: `${providerId}/${model}` } }),
2453
- clearJudge: () => apply({ council: { judge: null } }),
2454
- // Resolution + budget knobs. `BrainCouncilPatch` accepts null for the
2455
- // three integer fields, so `undefined` here really does clear to default.
2456
- setCouncilQuorum: (fraction) => apply({ council: { quorum: fraction } }),
2457
- setCouncilApproval: (fraction) => apply({ council: { approval: fraction } }),
2458
- setCouncilDistinctness: (mode) => apply({ council: { distinctness: mode } }),
2459
- setCouncilPerCallTimeout: (ms) => apply({ council: { perCallTimeoutMs: ms ?? null } }),
2460
- setCouncilMaxConcurrency: (count) => apply({ council: { maxConcurrency: count ?? null } }),
2461
- setCouncilVoterMaxTokens: (tokens) => apply({ council: { voterMaxTokens: tokens ?? null } }),
2462
- setCouncilDeliberationRounds: (rounds) => apply({ council: { deliberationRounds: rounds ?? null } }),
2463
- setCouncilJudgeMaxTokens: (tokens) => apply({ council: { judgeMaxTokens: tokens ?? null } }),
2464
- setLedgerEnabled: (on) => apply({ ledger: { enabled: on } }),
2465
- setAutoDeny: (count) => apply({ ledger: { autoDenyAfterFailures: count ?? null } }),
2466
- setTerminalPolicy: (policy) => apply({ terminalPolicy: policy }),
2467
- // Every sub-block below is MERGED field-by-field by `mergePatch`, so a
2468
- // single-key patch leaves the operator's other knobs alone.
2469
- setHeuristic: (key, on) => apply({ heuristics: { [key]: on } }),
2470
- setLlmMaxTokens: (tokens) => apply({ llm: { maxTokens: tokens } }),
2471
- setLlmRejectUncertain: (on) => apply({ llm: { rejectUncertain: on } }),
2472
- setLlmMinConfidence: (value) => apply({ llm: { minConfidence: value } }),
2473
- setLlmDenyIsTerminal: (mode) => apply({ llm: { denyIsTerminal: mode } }),
2474
- setCacheEnabled: (on) => apply({ cache: { enabled: on } }),
2475
- setCacheTtl: (ms) => apply({ cache: { ttlMs: ms } }),
2476
- setCacheMaxEntries: (count) => apply({ cache: { maxEntries: count } }),
2477
- setTraceEnabled: (on) => apply({ trace: { enabled: on } }),
2478
- setTraceContent: (content) => apply({ trace: { content } })
2479
- };
2760
+ return { onKanbanDispatch };
2480
2761
  }
2481
2762
 
2482
- // src/chimera-reviewer-policy.ts
2483
- import { parseModelRef } from "@wrongstack/core/agent";
2484
- import {
2485
- buildReviewerModelPool,
2486
- selectRoundRobinReviewerAssignment
2487
- } from "@wrongstack/core/plugin";
2488
- function resolveReviewerFallbackModels(reviewFallbackModels, sessionRef) {
2489
- const base = reviewFallbackModels && reviewFallbackModels.length > 0 ? [...reviewFallbackModels] : [];
2490
- if (sessionRef && !base.includes(sessionRef)) {
2491
- base.push(sessionRef);
2492
- }
2493
- return base;
2494
- }
2495
- var reviewerRoundRobinCursor = 0;
2496
- function __resetReviewerRoundRobinCursor(value = 0) {
2497
- reviewerRoundRobinCursor = value;
2498
- }
2499
- function assignReviewerModels(provider, model, fallbackModels, selection = "round-robin", statusTracker, random = Math.random) {
2500
- const pool = buildReviewerModelPool(provider, model, fallbackModels, statusTracker);
2501
- if (pool.length <= 1) {
2502
- return {
2503
- provider,
2504
- model,
2505
- fallbackModels: [...fallbackModels]
2506
- };
2507
- }
2508
- const cursor = selection === "random" ? Math.min(pool.length - 1, Math.floor(Math.max(0, random()) * pool.length)) : reviewerRoundRobinCursor;
2509
- const assignment = selectRoundRobinReviewerAssignment(
2510
- pool,
2511
- cursor,
2512
- provider,
2513
- model,
2514
- statusTracker
2515
- );
2516
- if (selection === "round-robin") reviewerRoundRobinCursor = assignment.nextCursor;
2763
+ // src/execution-repl-fleet-callbacks.ts
2764
+ function createReplFleetCallbacks({
2765
+ director,
2766
+ getDirector
2767
+ }) {
2768
+ const resolveDirector = () => getDirector?.() ?? director;
2517
2769
  return {
2518
- provider: assignment.provider,
2519
- model: assignment.model,
2520
- fallbackModels: assignment.fallbackModels
2770
+ onInterruptFleet: () => {
2771
+ const dir = resolveDirector();
2772
+ if (!dir) return 0;
2773
+ let killed = 0;
2774
+ for (const sa of dir.status().subagents) {
2775
+ if (sa.status === "running" || sa.status === "idle") {
2776
+ try {
2777
+ void dir.remove(sa.id);
2778
+ killed++;
2779
+ } catch {
2780
+ }
2781
+ }
2782
+ }
2783
+ return killed;
2784
+ },
2785
+ onAgentIterationComplete: (tokens) => {
2786
+ const dir = resolveDirector();
2787
+ dir?.setLeaderContextPressure(tokens);
2788
+ }
2521
2789
  };
2522
2790
  }
2523
- function assignReviewerModelsRoundRobin(provider, model, fallbackModels, statusTracker) {
2524
- return assignReviewerModels(provider, model, fallbackModels, "round-robin", statusTracker);
2791
+
2792
+ // src/fleet-statusline.ts
2793
+ import { color as color5, onResize } from "@wrongstack/core/utils";
2794
+ function fmtElapsed(ms) {
2795
+ const s = Math.floor(ms / 1e3);
2796
+ if (s < 60) return `${s}s`;
2797
+ const m = Math.floor(s / 60);
2798
+ const rem = s % 60;
2799
+ if (m < 60) return `${m}m${rem.toString().padStart(2, "0")}s`;
2800
+ const h = Math.floor(m / 60);
2801
+ return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
2525
2802
  }
2526
- var MAX_REVIEWER_ATTEMPTS = 4;
2527
- var REVIEWER_ATTEMPT_TIMEOUT_MS = 9e5;
2528
- var REVIEWER_LADDER_BUDGET_MS = 12e5;
2529
- var REVIEWER_MIN_ATTEMPT_MS = 12e4;
2530
- function buildReviewerAttemptLadder(input) {
2531
- const assignedChain = [...input.assigned.fallbackModels ?? []];
2532
- const profileChain = [...input.profileChain ?? []];
2533
- const rungs = [];
2534
- const seen = /* @__PURE__ */ new Set();
2535
- const key = (provider, model) => `${provider.trim()}/${model.trim()}`;
2536
- const push = (provider, model, fallbackModels, tier) => {
2537
- const p = provider.trim();
2538
- const m = model.trim();
2539
- if (!p || !m) return;
2540
- if (seen.has(key(p, m))) return;
2541
- seen.add(key(p, m));
2542
- rungs.push({ provider: p, model: m, fallbackModels, tier });
2543
- };
2544
- push(input.assigned.provider, input.assigned.model, [...assignedChain], "primary");
2545
- for (const [chain, tier] of [
2546
- [assignedChain, "fallback"],
2547
- [profileChain, "fallback"]
2548
- ]) {
2549
- for (let i = 0; i < chain.length; i++) {
2550
- const parsed = parseModelRef(chain[i]);
2551
- push(parsed.provider ?? "", parsed.model, chain.slice(i + 1), tier);
2803
+ function renderFleetLine(states, now, columns, version) {
2804
+ const all = [...states.values()];
2805
+ if (all.length === 0) return "";
2806
+ const running = all.filter((a) => a.status === "running");
2807
+ const done = all.filter((a) => a.status === "done").length;
2808
+ const failed = all.filter((a) => a.status === "failed").length;
2809
+ const versionChip = version ? `${color5.bold("WS")}${color5.dim(` v${version}`)} ${color5.dim("\u2502")} ` : "";
2810
+ const counts = versionChip + `${color5.cyan("\u27F3 fleet")} ${color5.yellow(`\u25B6${running.length}`)} ${color5.green(`\u2713${done}`)}` + (failed > 0 ? ` ${color5.red(`\u2717${failed}`)}` : "");
2811
+ const shown = running.sort((a, b) => a.startedAt - b.startedAt).slice(0, 4).map((a) => {
2812
+ const elapsed = fmtElapsed(Math.max(0, now - a.startedAt));
2813
+ const tool = a.lastTool ? ` ${color5.dim(a.lastTool)}` : "";
2814
+ const ext = a.extensions && a.extensions > 0 ? ` ${color5.yellow(`\u26A1${a.extensions}`)}` : "";
2815
+ return `${color5.bold(a.name)} ${color5.yellow("\u25B6")} ${color5.dim(elapsed)} ${color5.dim(`L${a.iterations}`)} ${color5.dim(`${a.toolCalls}t`)}${tool}${ext}`;
2816
+ });
2817
+ let line = shown.length > 0 ? `${counts} ${color5.dim("\u2502")} ${shown.join(color5.dim(" \xB7 "))}` : counts;
2818
+ const max = Math.max(20, columns - 1);
2819
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
2820
+ if (visible.length > max) {
2821
+ let count = 0;
2822
+ const out = [];
2823
+ let i = 0;
2824
+ while (i < line.length && count < max - 1) {
2825
+ if (line[i] === "\x1B") {
2826
+ const end = line.indexOf("m", i);
2827
+ if (end !== -1) {
2828
+ out.push(line.slice(i, end + 1));
2829
+ i = end + 1;
2830
+ continue;
2831
+ }
2832
+ }
2833
+ if (line[i] !== void 0) out.push(line[i]);
2834
+ count++;
2835
+ i++;
2552
2836
  }
2837
+ line = out.join("") + "\u2026";
2553
2838
  }
2554
- const sessionKey = key(input.session.provider, input.session.model);
2555
- push(input.session.provider, input.session.model, [], "session");
2556
- const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_REVIEWER_ATTEMPTS));
2557
- if (rungs.length <= cap) return rungs;
2558
- const trimmed = rungs.slice(0, cap);
2559
- if (!trimmed.some((r) => key(r.provider, r.model) === sessionKey)) {
2560
- const sessionRung = rungs.find((r) => key(r.provider, r.model) === sessionKey);
2561
- if (sessionRung) trimmed[trimmed.length - 1] = sessionRung;
2562
- }
2563
- return trimmed;
2564
- }
2565
- var MAX_MUTATING_ATTEMPTS = 2;
2566
- function buildMutatingAgentLadder(input) {
2567
- const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_MUTATING_ATTEMPTS));
2568
- const inherit = {
2569
- provider: "",
2570
- model: "",
2571
- fallbackModels: [],
2572
- tier: "inherit"
2573
- };
2574
- if (cap === 1) return [inherit];
2575
- return [
2576
- inherit,
2577
- ...buildReviewerAttemptLadder({
2578
- assigned: { provider: "", model: "" },
2579
- profileChain: input.profileChain,
2580
- session: input.session,
2581
- maxAttempts: cap - 1
2582
- })
2583
- ];
2584
- }
2585
- function shouldAdvanceReviewerLadder(status, error) {
2586
- if (status === "success") return false;
2587
- if (status === "stopped") return false;
2588
- if (error?.kind === "aborted_by_parent") return false;
2589
- return true;
2590
- }
2591
- function reviewerAttemptTimeoutMs(remainingMs, attemptTimeoutMs = REVIEWER_ATTEMPT_TIMEOUT_MS, minAttemptMs = REVIEWER_MIN_ATTEMPT_MS) {
2592
- if (remainingMs < minAttemptMs) return 0;
2593
- return Math.min(attemptTimeoutMs, Math.floor(remainingMs));
2594
- }
2595
- var CHIMERA_REVIEW_READ_ONLY_TOOLS = Object.freeze([
2596
- "read",
2597
- "grep",
2598
- "glob",
2599
- "tree",
2600
- "codebase-stats",
2601
- "codebase-search",
2602
- "codebase-skeleton",
2603
- "codebase-context",
2604
- "codebase-repo-map",
2605
- "codebase-incoming-calls",
2606
- "codebase-outgoing-calls"
2607
- ]);
2608
- function applyChimeraReviewerReadOnlyPolicy(config) {
2609
- return {
2610
- ...config,
2611
- // A security boundary, not an additive default: discard any inherited
2612
- // mutation tools/capabilities so Chimera reviewers cannot repair files.
2613
- tools: [...CHIMERA_REVIEW_READ_ONLY_TOOLS],
2614
- allowedCapabilities: ["fs.read"],
2615
- worktree: "off",
2616
- // Mandatory model-driven completion: a reviewer must never be killed by
2617
- // the wall-clock watchdog. At its deadline (or when the leader's session
2618
- // ends and `Director.requestFinish()` fires) it receives an in-band
2619
- // `subagent.finish_requested` notification between tool batches and
2620
- // finishes its report in its own turn within the granted grace window.
2621
- // Only if that window also elapses does the terminal stop apply — the
2622
- // reviewer's bounded maximum lifetime.
2623
- gracefulFinish: true
2624
- };
2839
+ return line;
2625
2840
  }
2626
-
2627
- // src/execution-cleanup.ts
2628
- async function finalizeExecutionCleanup(input) {
2629
- const {
2630
- offStorageObservability,
2631
- fleetStatusLine,
2632
- onCoordinatorStop,
2633
- stats,
2634
- renderer,
2635
- detachTodosCheckpoint,
2636
- mcpRegistry,
2637
- agent,
2638
- session,
2639
- tokenCounter,
2640
- events,
2641
- chimeraWork,
2642
- director,
2643
- reader
2644
- } = input;
2645
- offStorageObservability();
2646
- fleetStatusLine?.stop();
2647
- onCoordinatorStop?.();
2648
- try {
2649
- stats.render(renderer);
2650
- } catch (_err) {
2841
+ var FleetStatusLine = class {
2842
+ events;
2843
+ out;
2844
+ throttleMs;
2845
+ now;
2846
+ version;
2847
+ states = /* @__PURE__ */ new Map();
2848
+ unsubs = [];
2849
+ active = false;
2850
+ rows = 0;
2851
+ repaintTimer = null;
2852
+ tickTimer = null;
2853
+ deactivateTimer = null;
2854
+ resizeCleanup = null;
2855
+ lastPaint = 0;
2856
+ onResize = () => {
2857
+ if (this.active) {
2858
+ this.rows = this.out.rows ?? 24;
2859
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
2860
+ this.paint(true);
2861
+ }
2862
+ };
2863
+ constructor(opts) {
2864
+ this.events = opts.events;
2865
+ this.out = opts.out ?? process.stdout;
2866
+ this.throttleMs = opts.throttleMs ?? 150;
2867
+ this.now = opts.now ?? Date.now;
2868
+ this.version = opts.version;
2651
2869
  }
2652
- await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
2653
- try {
2654
- const { getProcessRegistry: getProcessRegistry2 } = await import("@wrongstack/tools");
2655
- getProcessRegistry2().killAll({ force: true, includeProtected: true });
2656
- } catch (err) {
2657
- console.warn(
2658
- JSON.stringify({
2659
- level: "warn",
2660
- event: "shutdown.process_kill_all_failed",
2661
- message: `Process registry killAll failed: ${err instanceof Error ? err.message : String(err)}`,
2662
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2870
+ /** Subscribe to host fleet events. No terminal output until a subagent spawns. */
2871
+ start() {
2872
+ const ensure = (id, name) => {
2873
+ let s = this.states.get(id);
2874
+ if (!s) {
2875
+ s = {
2876
+ id,
2877
+ name: name ?? id,
2878
+ status: "running",
2879
+ iterations: 0,
2880
+ toolCalls: 0,
2881
+ startedAt: this.now()
2882
+ };
2883
+ this.states.set(id, s);
2884
+ } else if (name && s.name === s.id) {
2885
+ s.name = name;
2886
+ }
2887
+ return s;
2888
+ };
2889
+ this.unsubs.push(
2890
+ this.events.on("subagent.spawned", (e) => {
2891
+ ensure(e.subagentId, e.name);
2892
+ this.activate();
2893
+ this.schedulePaint();
2894
+ }),
2895
+ this.events.on("subagent.task_started", (e) => {
2896
+ const s = ensure(e.subagentId);
2897
+ s.status = "running";
2898
+ this.activate();
2899
+ this.schedulePaint();
2900
+ }),
2901
+ this.events.on("subagent.tool_executed", (e) => {
2902
+ const s = ensure(e.subagentId);
2903
+ s.toolCalls++;
2904
+ s.lastTool = e.name;
2905
+ this.activate();
2906
+ this.schedulePaint();
2907
+ }),
2908
+ this.events.on("subagent.iteration_summary", (e) => {
2909
+ const s = ensure(e.subagentId);
2910
+ s.iterations = e.iteration;
2911
+ if (typeof e.toolCalls === "number") s.toolCalls = e.toolCalls;
2912
+ if (e.currentTool) s.lastTool = e.currentTool;
2913
+ this.schedulePaint();
2914
+ }),
2915
+ this.events.on("subagent.budget_extended", (e) => {
2916
+ const s = ensure(e.subagentId);
2917
+ s.extensions = e.totalExtensions;
2918
+ this.activate();
2919
+ this.schedulePaint();
2920
+ }),
2921
+ this.events.on("subagent.task_completed", (e) => {
2922
+ const s = ensure(e.subagentId);
2923
+ s.status = e.status === "success" ? "done" : "failed";
2924
+ s.iterations = e.iterations;
2925
+ s.toolCalls = e.toolCalls;
2926
+ s.endedAt = this.now();
2927
+ this.schedulePaint();
2928
+ if (![...this.states.values()].some((a) => a.status === "running")) {
2929
+ if (this.deactivateTimer) clearTimeout(this.deactivateTimer);
2930
+ this.deactivateTimer = setTimeout(() => {
2931
+ this.deactivateTimer = null;
2932
+ if (![...this.states.values()].some((a) => a.status === "running")) {
2933
+ this.deactivate();
2934
+ }
2935
+ }, 800);
2936
+ }
2937
+ }),
2938
+ this.events.on("subagent.removed", (e) => {
2939
+ this.states.delete(e.subagentId);
2940
+ this.schedulePaint();
2663
2941
  })
2664
2942
  );
2665
2943
  }
2666
- await mcpRegistry.stopAll().catch((err) => {
2667
- console.warn(
2668
- JSON.stringify({
2669
- level: "warn",
2670
- event: "shutdown.mcp_stop_failed",
2671
- message: `MCP registry stopAll failed: ${err instanceof Error ? err.message : String(err)}`,
2672
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2673
- })
2674
- );
2675
- });
2676
- const activeSession = agent.ctx.session ?? session;
2677
- const pending = activeSession.pendingToolUses;
2678
- await activeSession.append({
2679
- type: "session_end",
2680
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2681
- usage: tokenCounter.total(),
2682
- pendingToolUses: pending.length > 0 ? pending : void 0
2683
- }).catch((err) => {
2684
- console.warn(
2685
- JSON.stringify({
2686
- level: "warn",
2687
- event: "shutdown.session_end_append_failed",
2688
- message: `session_end append failed: ${err instanceof Error ? err.message : String(err)}`,
2689
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2690
- })
2691
- );
2692
- });
2693
- const sessionEndProducers = /* @__PURE__ */ new Set();
2694
- events.emit("session.ended", {
2695
- id: activeSession.id,
2696
- sessionId: activeSession.id,
2697
- usage: tokenCounter.total(),
2698
- waitUntil: (work) => {
2699
- const tracked = Promise.resolve(work).finally(() => sessionEndProducers.delete(tracked));
2700
- void tracked.catch(() => void 0);
2701
- sessionEndProducers.add(tracked);
2944
+ /** Unsubscribe and restore the terminal. Idempotent. */
2945
+ stop() {
2946
+ for (const u of this.unsubs.splice(0)) u();
2947
+ this.deactivate();
2948
+ }
2949
+ isTty() {
2950
+ return !!this.out.isTTY;
2951
+ }
2952
+ activate() {
2953
+ if (this.active || !this.isTty()) return;
2954
+ this.active = true;
2955
+ this.rows = this.out.rows ?? 24;
2956
+ this.out.write("\n");
2957
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
2958
+ this.out.write(`\x1B[${this.rows - 1};1H`);
2959
+ this.resizeCleanup = onResize(() => this.onResize(), this.out);
2960
+ this.tickTimer = setInterval(() => this.paint(true), 1e3);
2961
+ if (this.tickTimer.unref) this.tickTimer.unref();
2962
+ this.paint(true);
2963
+ }
2964
+ deactivate() {
2965
+ if (!this.active) return;
2966
+ this.active = false;
2967
+ if (this.tickTimer) {
2968
+ clearInterval(this.tickTimer);
2969
+ this.tickTimer = null;
2702
2970
  }
2703
- });
2704
- try {
2705
- const model = agent.ctx.model;
2706
- if (!model) {
2707
- console.warn(
2708
- JSON.stringify({
2709
- level: "warn",
2710
- event: "shutdown.wrongtrace_telemetry_skipped",
2711
- message: "Skipping WrongTrace telemetry: agent model unknown at session end",
2712
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2713
- })
2714
- );
2715
- } else {
2716
- const { reportWrongTraceSessionTelemetry } = await import("./wrongtrace-telemetry-3ASFKOZO.js");
2717
- const usage = tokenCounter.total();
2718
- const report = reportWrongTraceSessionTelemetry({
2719
- sessionId: activeSession.id,
2720
- agentName: "wrongstack-cli",
2721
- model,
2722
- provider: agent.ctx.provider?.id ?? "",
2723
- usage: {
2724
- input: usage.input,
2725
- output: usage.output,
2726
- cacheRead: usage.cacheRead,
2727
- cacheWrite: usage.cacheWrite
2728
- },
2729
- costUsd: tokenCounter.estimateCost().total
2971
+ if (this.repaintTimer) {
2972
+ clearTimeout(this.repaintTimer);
2973
+ this.repaintTimer = null;
2974
+ }
2975
+ if (this.deactivateTimer) {
2976
+ clearTimeout(this.deactivateTimer);
2977
+ this.deactivateTimer = null;
2978
+ }
2979
+ this.resizeCleanup?.();
2980
+ this.resizeCleanup = null;
2981
+ if (this.isTty()) {
2982
+ this.out.write("\x1B7");
2983
+ this.out.write(`\x1B[${this.rows};1H`);
2984
+ this.out.write("\x1B[2K");
2985
+ this.out.write("\x1B8");
2986
+ this.out.write("\x1B[r");
2987
+ }
2988
+ }
2989
+ schedulePaint() {
2990
+ if (!this.active) return;
2991
+ const since = this.now() - this.lastPaint;
2992
+ if (since >= this.throttleMs) {
2993
+ this.paint(false);
2994
+ return;
2995
+ }
2996
+ if (this.repaintTimer) return;
2997
+ this.repaintTimer = setTimeout(() => {
2998
+ this.repaintTimer = null;
2999
+ this.paint(false);
3000
+ }, this.throttleMs - since);
3001
+ if (this.repaintTimer.unref) this.repaintTimer.unref();
3002
+ }
3003
+ paint(force) {
3004
+ if (!this.active || !this.isTty()) return;
3005
+ if (!force) this.lastPaint = this.now();
3006
+ else this.lastPaint = this.now();
3007
+ const line = renderFleetLine(this.states, this.now(), this.out.columns ?? 80, this.version);
3008
+ this.out.write("\x1B7");
3009
+ this.out.write(`\x1B[${this.rows};1H`);
3010
+ this.out.write("\x1B[2K");
3011
+ this.out.write(line);
3012
+ this.out.write("\x1B8");
3013
+ }
3014
+ };
3015
+
3016
+ // src/subagent-models/panel-service.ts
3017
+ import {
3018
+ emptySubagentModelPlan,
3019
+ formatSubagentSlot,
3020
+ getSessionSubagentModelPlan,
3021
+ setSessionSubagentModelPlan,
3022
+ subagentSlotOccupancy
3023
+ } from "@wrongstack/core/coordination";
3024
+ function currentPlan(sessionId) {
3025
+ return getSessionSubagentModelPlan(sessionId) ?? emptySubagentModelPlan();
3026
+ }
3027
+ function createSubagentModelsPanelHost(deps) {
3028
+ const write = async (mutate) => {
3029
+ const ctx = deps.getContext();
3030
+ if (!ctx?.session) return "No active session \u2014 the plan cannot be saved.";
3031
+ const plan = structuredClone(currentPlan(ctx.session.id));
3032
+ mutate(plan);
3033
+ try {
3034
+ await setSessionSubagentModelPlan(ctx, plan);
3035
+ return null;
3036
+ } catch (err) {
3037
+ return err instanceof Error ? err.message : String(err);
3038
+ }
3039
+ };
3040
+ return {
3041
+ snapshot() {
3042
+ const sessionId = deps.getContext()?.session?.id;
3043
+ const plan = currentPlan(sessionId);
3044
+ const occupancy = subagentSlotOccupancy(sessionId);
3045
+ const session = deps.getSessionTarget?.();
3046
+ return {
3047
+ enabled: plan.enabled,
3048
+ lock: plan.lock,
3049
+ followSessionModel: plan.followSessionModel === true,
3050
+ sessionTarget: session?.provider ? `${session.provider}/${session.model ?? "?"}` : session?.model ?? "",
3051
+ lanes: plan.slots.map((slot, i) => ({
3052
+ target: formatSubagentSlot(slot),
3053
+ busy: occupancy[i]?.subagentIds.length ?? 0,
3054
+ ...slot.label ? { label: slot.label } : {}
3055
+ })),
3056
+ roles: Object.entries(plan.roles ?? {}).map(([role, slot]) => ({ role, target: formatSubagentSlot(slot) })).sort((a, b) => a.role.localeCompare(b.role))
3057
+ };
3058
+ },
3059
+ setLane(index, target) {
3060
+ return write((plan) => {
3061
+ if (index < 0 || index >= plan.slots.length) return;
3062
+ const slot = {};
3063
+ if (target.provider) slot.provider = target.provider;
3064
+ if (target.model) slot.model = target.model;
3065
+ if (target.tier) slot.tier = target.tier;
3066
+ if (target.fallbackProfile) slot.fallbackProfile = target.fallbackProfile;
3067
+ if (target.label) slot.label = target.label;
3068
+ plan.slots[index] = slot;
2730
3069
  });
2731
- const deadline = new Promise((resolve4) => {
2732
- const t = setTimeout(resolve4, 5e3);
2733
- t.unref?.();
3070
+ },
3071
+ clearLane(index) {
3072
+ return write((plan) => {
3073
+ if (index < 0 || index >= plan.slots.length) return;
3074
+ plan.slots[index] = {};
3075
+ });
3076
+ },
3077
+ toggle(field) {
3078
+ return write((plan) => {
3079
+ plan[field] = plan[field] !== true;
2734
3080
  });
2735
- const tracked = Promise.race([report, deadline]).finally(
2736
- () => sessionEndProducers.delete(tracked)
2737
- );
2738
- void tracked.catch(() => void 0);
2739
- sessionEndProducers.add(tracked);
2740
3081
  }
2741
- } catch {
3082
+ };
3083
+ }
3084
+
3085
+ // src/tui-resume-callback.ts
3086
+ import { hasOpenTodos } from "@wrongstack/core/utils";
3087
+ import { parseNextSteps } from "@wrongstack/tools/next-steps";
3088
+
3089
+ // src/boot/tui-session-resume.ts
3090
+ import * as path10 from "node:path";
3091
+ import {
3092
+ restoreSessionSubagentModelPlan,
3093
+ restoreSessionSubagentPolicy
3094
+ } from "@wrongstack/core/coordination";
3095
+ import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
3096
+ import { restoreSessionPermissionOverrides as restoreSessionPermissionOverrides2 } from "@wrongstack/core/security";
3097
+ import { attachTodosCheckpoint, loadTodosCheckpoint } from "@wrongstack/core/storage";
3098
+ import { projectLastRequestTokens } from "@wrongstack/core/types/session-timeline";
3099
+ import { sessionScopedPath as sessionScopedPath2, toErrorMessage } from "@wrongstack/core/utils";
3100
+ var RESERVATION_WINDOW_MS = 6e4;
3101
+ var RESERVATION_RENEW_MS = 2e4;
3102
+ function messageText(content) {
3103
+ if (typeof content === "string") return content;
3104
+ if (!Array.isArray(content)) return "";
3105
+ return content.map(
3106
+ (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
3107
+ ).filter(Boolean).join("\n");
3108
+ }
3109
+ function lastAssistantTextOf(messages) {
3110
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3111
+ const message = messages[index];
3112
+ if (message?.role !== "assistant") continue;
3113
+ const text = messageText(message.content).trim();
3114
+ if (text) return text;
2742
3115
  }
2743
- try {
2744
- const { snapshotGateDecisions, persistWrongTraceGateCounters } = await import("./wrongtrace-gate-counters-7C2EAAZL.js");
2745
- const projectRoot = agent.ctx.projectRoot;
2746
- if (projectRoot) {
2747
- await persistWrongTraceGateCounters(projectRoot, snapshotGateDecisions());
3116
+ return void 0;
3117
+ }
3118
+ async function resumeSession(ctx, sessionId) {
3119
+ const { state, agent, tokenCounter, switchProviderAndModel, events, onLoadProgress } = ctx;
3120
+ const fail = (stage2, message) => {
3121
+ try {
3122
+ ctx.onFailure?.({ stage: stage2, message: message || "unknown error" });
3123
+ } catch {
2748
3124
  }
2749
- } catch {
2750
- }
2751
- if (director) {
3125
+ return null;
3126
+ };
3127
+ const warnings = [];
3128
+ const warn = (label, err) => {
3129
+ warnings.push(`${label}: ${toErrorMessage(err)}`);
3130
+ };
3131
+ let stage = "start";
3132
+ const setStage = (next) => {
3133
+ stage = next;
2752
3134
  try {
2753
- director.requestFinish("leader session ended \u2014 finish your review now");
2754
- } catch (finishErr) {
2755
- console.warn(
2756
- JSON.stringify({
2757
- level: "warn",
2758
- event: "shutdown.subagent_finish_notify_failed",
2759
- message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2760
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2761
- })
2762
- );
3135
+ ctx.onStage?.(next);
3136
+ } catch {
2763
3137
  }
3138
+ };
3139
+ if (!state.activeSessionStore) {
3140
+ return fail("no_session_store", "No session store is bound to this project.");
2764
3141
  }
3142
+ setStage("load_replay_module");
3143
+ let replaySessionMessages;
2765
3144
  try {
2766
- while (sessionEndProducers.size > 0) {
2767
- await Promise.allSettled([...sessionEndProducers]);
3145
+ ({ replaySessionMessages } = await import("@wrongstack/tui"));
3146
+ if (typeof replaySessionMessages !== "function") {
3147
+ throw new TypeError("@wrongstack/tui does not export replaySessionMessages");
2768
3148
  }
2769
- if (director) {
2770
- try {
2771
- director.requestFinish("leader session ended \u2014 finish your review now");
2772
- } catch {
2773
- }
3149
+ } catch (err) {
3150
+ return fail(stage, toErrorMessage(err));
3151
+ }
3152
+ setStage("resolve_id");
3153
+ let canonicalSessionId = sessionId;
3154
+ try {
3155
+ if (state.activeSessionStore.resolveId) {
3156
+ canonicalSessionId = await state.activeSessionStore.resolveId(sessionId);
2774
3157
  }
2775
- await chimeraWork?.drainAndClose();
2776
3158
  } catch (err) {
2777
- console.warn(
3159
+ console.error(
2778
3160
  JSON.stringify({
2779
- level: "warn",
2780
- event: "shutdown.chimera_work_failed",
2781
- message: `Pending chimera work failed: ${err instanceof Error ? err.message : String(err)}`,
3161
+ level: "error",
3162
+ event: "execution.resume_id_resolve_failed",
3163
+ sessionId,
3164
+ message: toErrorMessage(err),
2782
3165
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2783
3166
  })
2784
3167
  );
3168
+ return fail(stage, toErrorMessage(err));
2785
3169
  }
2786
- if (director) {
3170
+ const previousSessionId = agent.ctx.session?.id;
3171
+ let hydrated;
3172
+ let identityClaimed = false;
3173
+ let resumeClaim;
3174
+ let writerSwapped = false;
3175
+ let openedWriter;
3176
+ let oldWriter;
3177
+ let oldMessages;
3178
+ let oldSessionRefCurrent;
3179
+ let previousDetachFn;
3180
+ let oldTodos = [];
3181
+ let oldPlanPath;
3182
+ let oldTaskPath;
3183
+ try {
3184
+ setStage("reserve_ownership");
3185
+ if (state.wpaths.projectSlug && state.wpaths.globalRoot) {
3186
+ const { getSessionRegistry } = await import("@wrongstack/core/storage");
3187
+ const registry = getSessionRegistry(state.wpaths.globalRoot);
3188
+ resumeClaim = await registry.reserveResume({
3189
+ sessionId: canonicalSessionId,
3190
+ projectSlug: state.wpaths.projectSlug,
3191
+ projectRoot: state.projectRoot,
3192
+ reservationMs: RESERVATION_WINDOW_MS
3193
+ });
3194
+ } else if (state.activateSessionIdentity) {
3195
+ await state.activateSessionIdentity(canonicalSessionId);
3196
+ identityClaimed = true;
3197
+ }
3198
+ setStage("open_journal");
3199
+ const claimToRenew = resumeClaim;
3200
+ let renewFailures = 0;
3201
+ const renew = () => {
3202
+ if (!claimToRenew) return;
3203
+ void claimToRenew.renew(RESERVATION_WINDOW_MS).catch((err) => {
3204
+ if (renewFailures++ > 0) return;
3205
+ console.error(
3206
+ JSON.stringify({
3207
+ level: "warn",
3208
+ event: "execution.resume_reservation_renew_failed",
3209
+ sessionId: canonicalSessionId,
3210
+ message: toErrorMessage(err),
3211
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3212
+ })
3213
+ );
3214
+ });
3215
+ };
3216
+ const renewTimer = setInterval(renew, RESERVATION_RENEW_MS);
3217
+ renewTimer.unref?.();
3218
+ let resumed;
2787
3219
  try {
2788
- director.requestFinish("leader session ended \u2014 finish your review now");
2789
- } catch (finishErr) {
2790
- console.warn(
2791
- JSON.stringify({
2792
- level: "warn",
2793
- event: "shutdown.subagent_finish_notify_failed",
2794
- message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2795
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2796
- })
2797
- );
3220
+ resumed = await state.activeSessionStore.resume(canonicalSessionId, onLoadProgress);
3221
+ } finally {
3222
+ clearInterval(renewTimer);
3223
+ }
3224
+ openedWriter = resumed.writer;
3225
+ hydrated = resumed.data;
3226
+ setStage("activate_ownership");
3227
+ await Promise.resolve(resumeClaim?.renew(RESERVATION_WINDOW_MS)).catch(() => void 0);
3228
+ if (resumeClaim) {
3229
+ await resumeClaim.activate({
3230
+ sessionId: canonicalSessionId,
3231
+ projectSlug: state.wpaths.projectSlug,
3232
+ projectRoot: state.projectRoot,
3233
+ projectName: path10.basename(state.projectRoot),
3234
+ workingDir: agent.ctx.workingDir,
3235
+ clientType: "tui",
3236
+ pid: process.pid,
3237
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3238
+ });
3239
+ identityClaimed = true;
3240
+ await state.activateSessionIdentity?.(canonicalSessionId);
3241
+ }
3242
+ setStage("replay_history");
3243
+ const meta = resumed.data.metadata;
3244
+ const entries = replaySessionMessages(
3245
+ resumed.data.messages,
3246
+ resumed.data.events,
3247
+ /* startId */
3248
+ 1
3249
+ );
3250
+ try {
3251
+ agent.container?.safeResolve?.(TOKENS2.DelegationTracker)?.rehydrate(canonicalSessionId, resumed.data.events ?? []);
3252
+ } catch (err) {
3253
+ warn("delegation rehydrate", err);
2798
3254
  }
3255
+ setStage("read_sidecars");
3256
+ const sessionsDir = state.wpaths.projectSessions;
3257
+ const resumedTodosPath = sessionScopedPath2(sessionsDir, resumed.writer.id, ".todos.json");
3258
+ const restoredTodos = await loadTodosCheckpoint(
3259
+ resumedTodosPath,
3260
+ events,
3261
+ agent.ctx.traceId,
3262
+ resumed.writer.id
3263
+ ).catch(() => null);
3264
+ setStage("swap_writer");
3265
+ oldWriter = agent.ctx.session;
3266
+ oldMessages = [...agent.ctx.messages];
3267
+ agent.ctx.session = resumed.writer;
2799
3268
  try {
2800
- await director.terminateAll();
2801
- } catch (termErr) {
2802
- console.warn(
3269
+ agent.ctx.state.replaceMessages(resumed.data.messages);
3270
+ restoreSessionSubagentPolicy(agent.ctx, resumed.data.events, resumed.data.subagentsAllowed);
3271
+ restoreSessionSubagentModelPlan(agent.ctx, resumed.data.events);
3272
+ restoreSessionPermissionOverrides2(agent.ctx.meta, resumed.data);
3273
+ } catch (err) {
3274
+ agent.ctx.session = oldWriter;
3275
+ agent.ctx.state.replaceMessages(oldMessages);
3276
+ throw err;
3277
+ }
3278
+ writerSwapped = true;
3279
+ if (state.sessionRef) {
3280
+ oldSessionRefCurrent = state.sessionRef.current;
3281
+ state.sessionRef.current = resumed.writer;
3282
+ }
3283
+ setStage("flush_journal");
3284
+ await agent.ctx.flushConversationJournal().catch((err) => {
3285
+ console.error(
2803
3286
  JSON.stringify({
2804
- level: "warn",
2805
- event: "shutdown.director_terminate_all_failed",
2806
- message: termErr instanceof Error ? termErr.message : String(termErr),
3287
+ level: "error",
3288
+ event: "execution.resume_journal_flush_failed",
3289
+ message: toErrorMessage(err),
2807
3290
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2808
3291
  })
2809
3292
  );
2810
- }
2811
- }
2812
- await activeSession.close().catch((err) => {
2813
- console.warn(
2814
- JSON.stringify({
2815
- level: "warn",
2816
- event: "shutdown.session_close_failed",
2817
- message: `Session close failed: ${err instanceof Error ? err.message : String(err)}`,
2818
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2819
- })
2820
- );
2821
- });
2822
- await reader.close().catch((err) => {
2823
- console.warn(
2824
- JSON.stringify({
2825
- level: "warn",
2826
- event: "shutdown.reader_close_failed",
2827
- message: `Input reader close failed: ${err instanceof Error ? err.message : String(err)}`,
2828
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2829
- })
2830
- );
2831
- });
2832
- }
2833
-
2834
- // src/execution-kanban-dispatch.ts
2835
- import { randomUUID } from "node:crypto";
2836
- import { missingRequiredRuntimeTools } from "@wrongstack/core/agent-catalog";
2837
- import { WIDE_SUBAGENT_CAPABILITIES } from "@wrongstack/core/security";
2838
- import { stripFrontmatter } from "@wrongstack/core/skills";
2839
-
2840
- // src/kanban-dispatch-route.ts
2841
- import { fallbackProfileChain, parseModelRef as parseModelRef2 } from "@wrongstack/core/agent";
2842
- import { resolveTier } from "@wrongstack/core/coordination";
2843
- function resolveKanbanDispatchRoute(config, spawnOpts) {
2844
- let provider = spawnOpts?.provider;
2845
- let model = spawnOpts?.model;
2846
- let fallbackModels = spawnOpts?.fallbackModels;
2847
- let tier;
2848
- if (spawnOpts?.fallbackProfile) {
2849
- const chain = fallbackProfileChain(config, spawnOpts.fallbackProfile);
2850
- const primary = chain[0] ? parseModelRef2(chain[0]) : void 0;
2851
- if (primary?.model) {
2852
- provider = primary.provider ?? config.provider;
2853
- model = primary.model;
2854
- fallbackModels = spawnOpts.fallbackModels ?? chain.slice(1);
2855
- }
2856
- } else if (spawnOpts?.tier || spawnOpts?.role) {
2857
- const resolved = resolveTier(config, {
2858
- ...spawnOpts.tier ? { tier: spawnOpts.tier } : {},
2859
- ...spawnOpts.role ? { role: spawnOpts.role } : {}
3293
+ warn("conversation journal did not flush", err);
2860
3294
  });
2861
- if (resolved?.model) {
2862
- tier = resolved.tier;
2863
- provider = provider ?? resolved.provider ?? config.provider;
2864
- model = model ?? resolved.model;
2865
- fallbackModels = fallbackModels ?? resolved.fallbackModels?.slice() ?? void 0;
2866
- }
2867
- }
2868
- return { provider, model, fallbackModels, ...tier ? { tier } : {} };
2869
- }
2870
-
2871
- // src/execution-kanban-dispatch.ts
2872
- function createKanbanDispatchHandler({
2873
- config,
2874
- events,
2875
- skillLoader,
2876
- sddSubagentFactory
2877
- }) {
2878
- if (!sddSubagentFactory) return {};
2879
- const onKanbanDispatch = async (description, spawnOpts) => {
2880
- const subagentId = `kanban-${randomUUID().slice(0, 8)}`;
2881
- const taskId = randomUUID();
2882
- const name = spawnOpts?.name ?? "kanban-agent";
2883
- const {
2884
- provider: resolvedProvider,
2885
- model: resolvedModel,
2886
- fallbackModels: resolvedFallbackModels
2887
- } = resolveKanbanDispatchRoute(config, spawnOpts);
2888
- let agentDescription = description;
2889
- if (spawnOpts?.skills?.length && skillLoader) {
2890
- const availableToolNames = spawnOpts.tools;
2891
- const loaded = [];
2892
- for (const skillName of spawnOpts.skills) {
2893
- try {
2894
- const manifest = await skillLoader.find(skillName);
2895
- if (!manifest) {
2896
- process.emitWarning(`Kanban dispatch skill not found, skipped: ${skillName}`, {
2897
- code: "WRONGSTACK_KANBAN_SKILL_SKIPPED"
2898
- });
2899
- continue;
2900
- }
2901
- if (availableToolNames !== void 0 && missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames).length > 0) {
2902
- process.emitWarning(
2903
- `Kanban dispatch skill "${skillName}" needs tools this worker does not have, skipped.`,
2904
- { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2905
- );
2906
- continue;
2907
- }
2908
- const body = stripFrontmatter(await skillLoader.readBody(skillName)).trim();
2909
- if (!body) continue;
2910
- loaded.push(`## Required skill: ${skillName}
2911
-
2912
- ${body}`);
2913
- } catch (err) {
2914
- process.emitWarning(
2915
- `Kanban dispatch skill "${skillName}" could not be loaded, skipped: ${err instanceof Error ? err.message : String(err)}`,
2916
- { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2917
- );
2918
- }
3295
+ setStage("repoint_sidecars");
3296
+ previousDetachFn = state.detachActiveTodosCheckpoint;
3297
+ oldTodos = agent.ctx.state.todos ? [...agent.ctx.state.todos] : [];
3298
+ oldPlanPath = agent.ctx.state.meta?.["plan.path"];
3299
+ oldTaskPath = agent.ctx.state.meta?.["task.path"];
3300
+ try {
3301
+ await Promise.resolve(previousDetachFn?.()).catch(() => void 0);
3302
+ agent.ctx.state.setMeta(
3303
+ "plan.path",
3304
+ sessionScopedPath2(sessionsDir, resumed.writer.id, ".plan.json")
3305
+ );
3306
+ agent.ctx.state.setMeta(
3307
+ "task.path",
3308
+ sessionScopedPath2(sessionsDir, resumed.writer.id, ".tasks.json")
3309
+ );
3310
+ agent.ctx.state.replaceTodos(restoredTodos ?? []);
3311
+ try {
3312
+ state.detachActiveTodosCheckpoint = attachTodosCheckpoint(
3313
+ agent.ctx.state,
3314
+ resumedTodosPath,
3315
+ resumed.writer.id,
3316
+ events,
3317
+ agent.ctx.traceId
3318
+ );
3319
+ } catch {
3320
+ state.detachActiveTodosCheckpoint = void 0;
2919
3321
  }
2920
- if (loaded.length > 0) {
2921
- agentDescription = `${description}
2922
-
2923
- # Required agentic skill instructions
2924
-
2925
- ${loaded.join("\n\n")}`;
3322
+ } catch (err) {
3323
+ warn("todo/plan sidecars were not re-pointed", err);
3324
+ try {
3325
+ state.detachActiveTodosCheckpoint = previousDetachFn;
3326
+ agent.ctx.state.replaceTodos([...oldTodos]);
3327
+ if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
3328
+ if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
3329
+ } catch {
2926
3330
  }
2927
3331
  }
2928
- void (async () => {
2929
- let built;
2930
- let completionReported = false;
3332
+ setStage("restore_model");
3333
+ const currentProviderId = agent.ctx.provider.id;
3334
+ const savedProvider = typeof meta.provider === "string" ? meta.provider.trim() : "";
3335
+ const savedModel = typeof meta.model === "string" ? meta.model.trim() : "";
3336
+ const hasSavedRoute = savedProvider.length > 0 && savedProvider.toLowerCase() !== "unknown" && savedModel.length > 0 && savedModel.toLowerCase() !== "unknown";
3337
+ const targetProviderId = hasSavedRoute ? savedProvider : currentProviderId;
3338
+ const targetModel = hasSavedRoute ? savedModel : agent.ctx.model;
3339
+ if (switchProviderAndModel && targetProviderId && (targetProviderId !== currentProviderId || targetModel !== agent.ctx.model)) {
2931
3340
  try {
2932
- if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2933
- built = await sddSubagentFactory(
2934
- {
2935
- id: subagentId,
2936
- name,
2937
- role: "kanban-agent",
2938
- prompt: agentDescription,
2939
- allowedCapabilities: spawnOpts?.allowedCapabilities ?? WIDE_SUBAGENT_CAPABILITIES,
2940
- ...resolvedProvider ? { provider: resolvedProvider } : {},
2941
- ...resolvedModel ? { model: resolvedModel } : {},
2942
- ...resolvedFallbackModels ? { fallbackModels: resolvedFallbackModels } : {},
2943
- ...spawnOpts?.tools ? { tools: spawnOpts.tools } : {}
2944
- },
2945
- ...spawnOpts?.context ? [{ id: taskId, description: agentDescription, context: spawnOpts.context }] : []
2946
- );
2947
- if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2948
- const result = await built.agent.run(
2949
- agentDescription,
2950
- ...spawnOpts?.signal ? [{ signal: spawnOpts.signal }] : []
2951
- );
2952
- completionReported = true;
2953
- await spawnOpts?.onDone?.({
2954
- status: result.status === "done" ? "completed" : "failed",
2955
- result: result.finalText,
2956
- ..."error" in result && result.error?.message ? { error: result.error.message } : {}
2957
- });
3341
+ const error = await Promise.resolve(switchProviderAndModel(targetProviderId, targetModel));
3342
+ if (typeof error === "string" && error)
3343
+ warn(`could not restore ${targetProviderId}/${targetModel}`, error);
2958
3344
  } catch (err) {
2959
- if (!completionReported) {
2960
- await spawnOpts?.onDone?.({
2961
- status: "failed",
2962
- error: err instanceof Error ? err.message : String(err)
3345
+ console.error(
3346
+ JSON.stringify({
3347
+ level: "error",
3348
+ event: "execution.resume_model_restore_failed",
3349
+ provider: targetProviderId,
3350
+ model: targetModel,
3351
+ message: toErrorMessage(err),
3352
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3353
+ })
3354
+ );
3355
+ warn(`could not restore ${targetProviderId}/${targetModel}`, err);
3356
+ }
3357
+ } else if (targetModel !== agent.ctx.model) {
3358
+ agent.ctx.model = targetModel;
3359
+ }
3360
+ setStage("finalize_previous_session");
3361
+ let finalizeOldWriter;
3362
+ if (oldWriter && oldWriter !== resumed.writer) {
3363
+ const staleWriter = oldWriter;
3364
+ const endedUsage = tokenCounter.total();
3365
+ finalizeOldWriter = async () => {
3366
+ let appendOk = false;
3367
+ try {
3368
+ await staleWriter.append({
3369
+ type: "session_end",
3370
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3371
+ usage: endedUsage
2963
3372
  });
3373
+ appendOk = true;
3374
+ } catch (err) {
3375
+ console.error(
3376
+ JSON.stringify({
3377
+ level: "error",
3378
+ event: "execution.session_end_append_failed",
3379
+ message: toErrorMessage(err),
3380
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3381
+ })
3382
+ );
2964
3383
  }
2965
- throw err;
2966
- } finally {
2967
- await built?.dispose?.();
2968
- }
2969
- })().catch((err) => {
2970
- events.emit("error", {
2971
- err: err instanceof Error ? err : new Error(String(err)),
2972
- phase: "kanban.dispatch"
2973
- });
2974
- });
2975
- const tags = [];
2976
- if (resolvedProvider) tags.push(resolvedProvider);
2977
- if (resolvedModel) tags.push(resolvedModel);
2978
- if (resolvedFallbackModels?.length) {
2979
- tags.push(`fallback=${resolvedFallbackModels.join(",")}`);
2980
- }
2981
- if (spawnOpts?.fallbackProfile) tags.push(`profile=${spawnOpts.fallbackProfile}`);
2982
- if (spawnOpts?.skills?.length) tags.push(`skills=${spawnOpts.skills.join(",")}`);
2983
- if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
2984
- const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
2985
- return `Spawned subagent ${subagentId}${tag} for task ${taskId}.`;
2986
- };
2987
- return { onKanbanDispatch };
2988
- }
2989
-
2990
- // src/execution-repl-fleet-callbacks.ts
2991
- function createReplFleetCallbacks({
2992
- director,
2993
- getDirector
2994
- }) {
2995
- const resolveDirector = () => getDirector?.() ?? director;
2996
- return {
2997
- onInterruptFleet: () => {
2998
- const dir = resolveDirector();
2999
- if (!dir) return 0;
3000
- let killed = 0;
3001
- for (const sa of dir.status().subagents) {
3002
- if (sa.status === "running" || sa.status === "idle") {
3384
+ if (appendOk) {
3003
3385
  try {
3004
- void dir.remove(sa.id);
3005
- killed++;
3006
- } catch {
3386
+ await staleWriter.close();
3387
+ } catch (err) {
3388
+ console.error(
3389
+ JSON.stringify({
3390
+ level: "error",
3391
+ event: "execution.session_close_failed",
3392
+ message: toErrorMessage(err),
3393
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3394
+ })
3395
+ );
3007
3396
  }
3008
3397
  }
3009
- }
3010
- return killed;
3011
- },
3012
- onAgentIterationComplete: (tokens) => {
3013
- const dir = resolveDirector();
3014
- dir?.setLeaderContextPressure(tokens);
3015
- }
3016
- };
3017
- }
3018
-
3019
- // src/fleet-statusline.ts
3020
- import { color as color5, onResize } from "@wrongstack/core/utils";
3021
- function fmtElapsed(ms) {
3022
- const s = Math.floor(ms / 1e3);
3023
- if (s < 60) return `${s}s`;
3024
- const m = Math.floor(s / 60);
3025
- const rem = s % 60;
3026
- if (m < 60) return `${m}m${rem.toString().padStart(2, "0")}s`;
3027
- const h = Math.floor(m / 60);
3028
- return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
3029
- }
3030
- function renderFleetLine(states, now, columns, version) {
3031
- const all = [...states.values()];
3032
- if (all.length === 0) return "";
3033
- const running = all.filter((a) => a.status === "running");
3034
- const done = all.filter((a) => a.status === "done").length;
3035
- const failed = all.filter((a) => a.status === "failed").length;
3036
- const versionChip = version ? `${color5.bold("WS")}${color5.dim(` v${version}`)} ${color5.dim("\u2502")} ` : "";
3037
- const counts = versionChip + `${color5.cyan("\u27F3 fleet")} ${color5.yellow(`\u25B6${running.length}`)} ${color5.green(`\u2713${done}`)}` + (failed > 0 ? ` ${color5.red(`\u2717${failed}`)}` : "");
3038
- const shown = running.sort((a, b) => a.startedAt - b.startedAt).slice(0, 4).map((a) => {
3039
- const elapsed = fmtElapsed(Math.max(0, now - a.startedAt));
3040
- const tool = a.lastTool ? ` ${color5.dim(a.lastTool)}` : "";
3041
- const ext = a.extensions && a.extensions > 0 ? ` ${color5.yellow(`\u26A1${a.extensions}`)}` : "";
3042
- return `${color5.bold(a.name)} ${color5.yellow("\u25B6")} ${color5.dim(elapsed)} ${color5.dim(`L${a.iterations}`)} ${color5.dim(`${a.toolCalls}t`)}${tool}${ext}`;
3043
- });
3044
- let line = shown.length > 0 ? `${counts} ${color5.dim("\u2502")} ${shown.join(color5.dim(" \xB7 "))}` : counts;
3045
- const max = Math.max(20, columns - 1);
3046
- const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
3047
- if (visible.length > max) {
3048
- let count = 0;
3049
- const out = [];
3050
- let i = 0;
3051
- while (i < line.length && count < max - 1) {
3052
- if (line[i] === "\x1B") {
3053
- const end = line.indexOf("m", i);
3054
- if (end !== -1) {
3055
- out.push(line.slice(i, end + 1));
3056
- i = end + 1;
3057
- continue;
3058
- }
3059
- }
3060
- if (line[i] !== void 0) out.push(line[i]);
3061
- count++;
3062
- i++;
3398
+ };
3063
3399
  }
3064
- line = out.join("") + "\u2026";
3065
- }
3066
- return line;
3067
- }
3068
- var FleetStatusLine = class {
3069
- events;
3070
- out;
3071
- throttleMs;
3072
- now;
3073
- version;
3074
- states = /* @__PURE__ */ new Map();
3075
- unsubs = [];
3076
- active = false;
3077
- rows = 0;
3078
- repaintTimer = null;
3079
- tickTimer = null;
3080
- deactivateTimer = null;
3081
- resizeCleanup = null;
3082
- lastPaint = 0;
3083
- onResize = () => {
3084
- if (this.active) {
3085
- this.rows = this.out.rows ?? 24;
3086
- this.out.write(`\x1B[1;${this.rows - 1}r`);
3087
- this.paint(true);
3400
+ setStage("token_accounting");
3401
+ try {
3402
+ tokenCounter.reset();
3403
+ tokenCounter.account(resumed.data.usage, targetModel, targetProviderId);
3404
+ } catch (err) {
3405
+ warn("token accounting for the resumed session failed", err);
3088
3406
  }
3089
- };
3090
- constructor(opts) {
3091
- this.events = opts.events;
3092
- this.out = opts.out ?? process.stdout;
3093
- this.throttleMs = opts.throttleMs ?? 150;
3094
- this.now = opts.now ?? Date.now;
3095
- this.version = opts.version;
3096
- }
3097
- /** Subscribe to host fleet events. No terminal output until a subagent spawns. */
3098
- start() {
3099
- const ensure = (id, name) => {
3100
- let s = this.states.get(id);
3101
- if (!s) {
3102
- s = {
3103
- id,
3104
- name: name ?? id,
3105
- status: "running",
3106
- iterations: 0,
3107
- toolCalls: 0,
3108
- startedAt: this.now()
3109
- };
3110
- this.states.set(id, s);
3111
- } else if (name && s.name === s.id) {
3112
- s.name = name;
3113
- }
3114
- return s;
3115
- };
3116
- this.unsubs.push(
3117
- this.events.on("subagent.spawned", (e) => {
3118
- ensure(e.subagentId, e.name);
3119
- this.activate();
3120
- this.schedulePaint();
3121
- }),
3122
- this.events.on("subagent.task_started", (e) => {
3123
- const s = ensure(e.subagentId);
3124
- s.status = "running";
3125
- this.activate();
3126
- this.schedulePaint();
3127
- }),
3128
- this.events.on("subagent.tool_executed", (e) => {
3129
- const s = ensure(e.subagentId);
3130
- s.toolCalls++;
3131
- s.lastTool = e.name;
3132
- this.activate();
3133
- this.schedulePaint();
3134
- }),
3135
- this.events.on("subagent.iteration_summary", (e) => {
3136
- const s = ensure(e.subagentId);
3137
- s.iterations = e.iteration;
3138
- if (typeof e.toolCalls === "number") s.toolCalls = e.toolCalls;
3139
- if (e.currentTool) s.lastTool = e.currentTool;
3140
- this.schedulePaint();
3141
- }),
3142
- this.events.on("subagent.budget_extended", (e) => {
3143
- const s = ensure(e.subagentId);
3144
- s.extensions = e.totalExtensions;
3145
- this.activate();
3146
- this.schedulePaint();
3147
- }),
3148
- this.events.on("subagent.task_completed", (e) => {
3149
- const s = ensure(e.subagentId);
3150
- s.status = e.status === "success" ? "done" : "failed";
3151
- s.iterations = e.iterations;
3152
- s.toolCalls = e.toolCalls;
3153
- s.endedAt = this.now();
3154
- this.schedulePaint();
3155
- if (![...this.states.values()].some((a) => a.status === "running")) {
3156
- if (this.deactivateTimer) clearTimeout(this.deactivateTimer);
3157
- this.deactivateTimer = setTimeout(() => {
3158
- this.deactivateTimer = null;
3159
- if (![...this.states.values()].some((a) => a.status === "running")) {
3160
- this.deactivate();
3161
- }
3162
- }, 800);
3163
- }
3164
- }),
3165
- this.events.on("subagent.removed", (e) => {
3166
- this.states.delete(e.subagentId);
3167
- this.schedulePaint();
3168
- })
3169
- );
3170
- }
3171
- /** Unsubscribe and restore the terminal. Idempotent. */
3172
- stop() {
3173
- for (const u of this.unsubs.splice(0)) u();
3174
- this.deactivate();
3175
- }
3176
- isTty() {
3177
- return !!this.out.isTTY;
3178
- }
3179
- activate() {
3180
- if (this.active || !this.isTty()) return;
3181
- this.active = true;
3182
- this.rows = this.out.rows ?? 24;
3183
- this.out.write("\n");
3184
- this.out.write(`\x1B[1;${this.rows - 1}r`);
3185
- this.out.write(`\x1B[${this.rows - 1};1H`);
3186
- this.resizeCleanup = onResize(() => this.onResize(), this.out);
3187
- this.tickTimer = setInterval(() => this.paint(true), 1e3);
3188
- if (this.tickTimer.unref) this.tickTimer.unref();
3189
- this.paint(true);
3190
- }
3191
- deactivate() {
3192
- if (!this.active) return;
3193
- this.active = false;
3194
- if (this.tickTimer) {
3195
- clearInterval(this.tickTimer);
3196
- this.tickTimer = null;
3407
+ const resumedRequestTokens = projectLastRequestTokens(resumed.data.events);
3408
+ agent.ctx.lastRequestTokens = resumedRequestTokens;
3409
+ tokenCounter.setCurrentRequestTokens?.(resumedRequestTokens ?? 0);
3410
+ const tokens = resumedRequestTokens ?? 0;
3411
+ const maxContext = agent.ctx.provider?.capabilities?.maxContext ?? 0;
3412
+ const finalText = lastAssistantTextOf(resumed.data.messages);
3413
+ const result = {
3414
+ entries,
3415
+ nextId: entries.length + 1,
3416
+ sessionId: resumed.writer.id,
3417
+ contextSnapshot: { tokens, maxContext },
3418
+ attached: true,
3419
+ warnings,
3420
+ ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
3421
+ };
3422
+ if (finalizeOldWriter) void finalizeOldWriter();
3423
+ return result;
3424
+ } catch (err) {
3425
+ if (!identityClaimed) await resumeClaim?.cancel().catch(() => void 0);
3426
+ if (identityClaimed && previousSessionId) {
3427
+ await state.activateSessionIdentity?.(previousSessionId).catch((rollbackErr) => {
3428
+ console.error(
3429
+ JSON.stringify({
3430
+ level: "error",
3431
+ event: "execution.resume_identity_rollback_failed",
3432
+ sessionId: previousSessionId,
3433
+ message: toErrorMessage(rollbackErr),
3434
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3435
+ })
3436
+ );
3437
+ });
3197
3438
  }
3198
- if (this.repaintTimer) {
3199
- clearTimeout(this.repaintTimer);
3200
- this.repaintTimer = null;
3439
+ if (!writerSwapped) {
3440
+ await openedWriter?.close().catch(() => void 0);
3441
+ } else {
3442
+ try {
3443
+ if (oldWriter !== void 0) agent.ctx.session = oldWriter;
3444
+ if (state.sessionRef) state.sessionRef.current = oldSessionRefCurrent;
3445
+ if (oldMessages !== void 0) agent.ctx.state.replaceMessages(oldMessages);
3446
+ if (previousDetachFn !== void 0) state.detachActiveTodosCheckpoint = previousDetachFn;
3447
+ agent.ctx.state.replaceTodos([...oldTodos]);
3448
+ if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
3449
+ if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
3450
+ } catch (rollbackErr) {
3451
+ console.error(
3452
+ JSON.stringify({
3453
+ level: "error",
3454
+ event: "execution.resume_post_swap_rollback_failed",
3455
+ message: toErrorMessage(rollbackErr),
3456
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3457
+ })
3458
+ );
3459
+ }
3460
+ if (openedWriter && openedWriter !== oldWriter) {
3461
+ await openedWriter.close().catch((closeErr) => {
3462
+ console.error(
3463
+ JSON.stringify({
3464
+ level: "error",
3465
+ event: "execution.resume_opened_writer_close_failed",
3466
+ message: toErrorMessage(closeErr),
3467
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3468
+ })
3469
+ );
3470
+ });
3471
+ }
3201
3472
  }
3202
- if (this.deactivateTimer) {
3203
- clearTimeout(this.deactivateTimer);
3204
- this.deactivateTimer = null;
3473
+ const reason = err instanceof Error ? err.stack ?? err.message : String(err);
3474
+ console.error(
3475
+ JSON.stringify({
3476
+ level: "error",
3477
+ event: "execution.resume_session_failed",
3478
+ sessionId: canonicalSessionId,
3479
+ stage,
3480
+ message: reason,
3481
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3482
+ })
3483
+ );
3484
+ const message = toErrorMessage(err);
3485
+ if (err instanceof Error && err.name === "SessionOwnershipConflictError") {
3486
+ return fail(stage, message);
3205
3487
  }
3206
- this.resizeCleanup?.();
3207
- this.resizeCleanup = null;
3208
- if (this.isTty()) {
3209
- this.out.write("\x1B7");
3210
- this.out.write(`\x1B[${this.rows};1H`);
3211
- this.out.write("\x1B[2K");
3212
- this.out.write("\x1B8");
3213
- this.out.write("\x1B[r");
3488
+ let data = hydrated;
3489
+ if (!data) {
3490
+ try {
3491
+ data = await state.activeSessionStore.load(canonicalSessionId, onLoadProgress);
3492
+ } catch {
3493
+ data = void 0;
3494
+ }
3214
3495
  }
3215
- }
3216
- schedulePaint() {
3217
- if (!this.active) return;
3218
- const since = this.now() - this.lastPaint;
3219
- if (since >= this.throttleMs) {
3220
- this.paint(false);
3221
- return;
3496
+ if (data) {
3497
+ try {
3498
+ const entries = replaySessionMessages(
3499
+ data.messages,
3500
+ data.events,
3501
+ /* startId */
3502
+ 1
3503
+ );
3504
+ const finalText = lastAssistantTextOf(data.messages);
3505
+ fail(stage, message);
3506
+ return {
3507
+ entries,
3508
+ nextId: entries.length + 1,
3509
+ sessionId: canonicalSessionId,
3510
+ // Deliberately no contextSnapshot: the chip reports the LIVE
3511
+ // window, and the live session is still the one we rolled back to.
3512
+ // Painting the viewed session's fill over it would be a lie.
3513
+ attached: false,
3514
+ warnings: [...warnings, `not attached (at ${stage}): ${message}`],
3515
+ ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
3516
+ };
3517
+ } catch (renderErr) {
3518
+ console.error(
3519
+ JSON.stringify({
3520
+ level: "error",
3521
+ event: "execution.resume_readonly_render_failed",
3522
+ sessionId: canonicalSessionId,
3523
+ message: toErrorMessage(renderErr),
3524
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3525
+ })
3526
+ );
3527
+ }
3222
3528
  }
3223
- if (this.repaintTimer) return;
3224
- this.repaintTimer = setTimeout(() => {
3225
- this.repaintTimer = null;
3226
- this.paint(false);
3227
- }, this.throttleMs - since);
3228
- if (this.repaintTimer.unref) this.repaintTimer.unref();
3229
- }
3230
- paint(force) {
3231
- if (!this.active || !this.isTty()) return;
3232
- if (!force) this.lastPaint = this.now();
3233
- else this.lastPaint = this.now();
3234
- const line = renderFleetLine(this.states, this.now(), this.out.columns ?? 80, this.version);
3235
- this.out.write("\x1B7");
3236
- this.out.write(`\x1B[${this.rows};1H`);
3237
- this.out.write("\x1B[2K");
3238
- this.out.write(line);
3239
- this.out.write("\x1B8");
3529
+ return fail(stage, message);
3240
3530
  }
3241
- };
3242
-
3243
- // src/subagent-models/panel-service.ts
3244
- import {
3245
- emptySubagentModelPlan,
3246
- formatSubagentSlot,
3247
- getSessionSubagentModelPlan,
3248
- setSessionSubagentModelPlan,
3249
- subagentSlotOccupancy
3250
- } from "@wrongstack/core/coordination";
3251
- function currentPlan(sessionId) {
3252
- return getSessionSubagentModelPlan(sessionId) ?? emptySubagentModelPlan();
3253
3531
  }
3254
- function createSubagentModelsPanelHost(deps) {
3255
- const write = async (mutate) => {
3256
- const ctx = deps.getContext();
3257
- if (!ctx?.session) return "No active session \u2014 the plan cannot be saved.";
3258
- const plan = structuredClone(currentPlan(ctx.session.id));
3259
- mutate(plan);
3260
- try {
3261
- await setSessionSubagentModelPlan(ctx, plan);
3262
- return null;
3263
- } catch (err) {
3264
- return err instanceof Error ? err.message : String(err);
3265
- }
3266
- };
3267
- return {
3268
- snapshot() {
3269
- const sessionId = deps.getContext()?.session?.id;
3270
- const plan = currentPlan(sessionId);
3271
- const occupancy = subagentSlotOccupancy(sessionId);
3272
- const session = deps.getSessionTarget?.();
3273
- return {
3274
- enabled: plan.enabled,
3275
- lock: plan.lock,
3276
- followSessionModel: plan.followSessionModel === true,
3277
- sessionTarget: session?.provider ? `${session.provider}/${session.model ?? "?"}` : session?.model ?? "",
3278
- lanes: plan.slots.map((slot, i) => ({
3279
- target: formatSubagentSlot(slot),
3280
- busy: occupancy[i]?.subagentIds.length ?? 0,
3281
- ...slot.label ? { label: slot.label } : {}
3282
- })),
3283
- roles: Object.entries(plan.roles ?? {}).map(([role, slot]) => ({ role, target: formatSubagentSlot(slot) })).sort((a, b) => a.role.localeCompare(b.role))
3284
- };
3285
- },
3286
- setLane(index, target) {
3287
- return write((plan) => {
3288
- if (index < 0 || index >= plan.slots.length) return;
3289
- const slot = {};
3290
- if (target.provider) slot.provider = target.provider;
3291
- if (target.model) slot.model = target.model;
3292
- if (target.tier) slot.tier = target.tier;
3293
- if (target.fallbackProfile) slot.fallbackProfile = target.fallbackProfile;
3294
- if (target.label) slot.label = target.label;
3295
- plan.slots[index] = slot;
3296
- });
3297
- },
3298
- clearLane(index) {
3299
- return write((plan) => {
3300
- if (index < 0 || index >= plan.slots.length) return;
3301
- plan.slots[index] = {};
3302
- });
3303
- },
3304
- toggle(field) {
3305
- return write((plan) => {
3306
- plan[field] = plan[field] !== true;
3307
- });
3532
+
3533
+ // src/tui-resume-callback.ts
3534
+ function createTuiResumeCallback({
3535
+ state,
3536
+ agent,
3537
+ tokenCounter,
3538
+ switchProviderAndModel,
3539
+ events
3540
+ }) {
3541
+ return async (sessionId, onLoadProgress, onStage) => {
3542
+ let failure;
3543
+ const result = await resumeSession(
3544
+ {
3545
+ state,
3546
+ agent,
3547
+ tokenCounter,
3548
+ switchProviderAndModel,
3549
+ events,
3550
+ onLoadProgress,
3551
+ onStage,
3552
+ onFailure: (info) => {
3553
+ failure = info;
3554
+ }
3555
+ },
3556
+ sessionId
3557
+ );
3558
+ if (!result) {
3559
+ throw new Error(
3560
+ failure ? `${failure.message} (at ${failure.stage})` : `Session "${sessionId}" could not be resumed.`
3561
+ );
3308
3562
  }
3563
+ setAutoSuggestions([]);
3564
+ const resumedNextSteps = result.attached && !hasOpenTodos(agent.ctx.todos) && result.lastAssistantText ? parseNextSteps(result.lastAssistantText).texts : [];
3565
+ return { ...result, nextSteps: resumedNextSteps };
3309
3566
  };
3310
3567
  }
3311
3568
 
@@ -3348,7 +3605,7 @@ function createChimeraWorkRegistry() {
3348
3605
 
3349
3606
  // src/execution-chimera-cascade.ts
3350
3607
  import * as fsp from "node:fs/promises";
3351
- import * as path10 from "node:path";
3608
+ import * as path11 from "node:path";
3352
3609
  import { emitReviewIfChanged } from "@wrongstack/core/plugin";
3353
3610
 
3354
3611
  // src/chimera-cascade-evidence.ts
@@ -3404,14 +3661,9 @@ function parseSingleCheck(raw) {
3404
3661
  if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
3405
3662
  return void 0;
3406
3663
  }
3407
- const output = typeof check.output === "string" ? truncateOutput(check.output) : void 0;
3664
+ const output = typeof check.output === "string" ? check.output : void 0;
3408
3665
  return { command: command.trim(), exitCode, output };
3409
3666
  }
3410
- var MAX_EVIDENCE_OUTPUT_CHARS = 2e3;
3411
- function truncateOutput(output) {
3412
- return output.length > MAX_EVIDENCE_OUTPUT_CHARS ? `${output.slice(0, MAX_EVIDENCE_OUTPUT_CHARS)}
3413
- \u2026 (truncated)` : output;
3414
- }
3415
3667
  var SAFE_EXECUTABLES = /* @__PURE__ */ new Set([
3416
3668
  "pnpm",
3417
3669
  "npm",
@@ -3442,7 +3694,7 @@ var runCascadeVerificationCommand = (command, cwd, timeoutMs) => {
3442
3694
  const tokens = command.trim().split(/\s+/);
3443
3695
  const executable = tokens[0];
3444
3696
  const args = tokens.slice(1);
3445
- return new Promise((resolve4) => {
3697
+ return new Promise((resolve5) => {
3446
3698
  let child;
3447
3699
  try {
3448
3700
  child = spawn2(executable, args, {
@@ -3452,11 +3704,11 @@ var runCascadeVerificationCommand = (command, cwd, timeoutMs) => {
3452
3704
  signal: AbortSignal.timeout(timeoutMs)
3453
3705
  });
3454
3706
  } catch {
3455
- resolve4({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
3707
+ resolve5({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
3456
3708
  return;
3457
3709
  }
3458
- child.on("error", () => resolve4({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3459
- child.on("close", (code) => resolve4({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3710
+ child.on("error", () => resolve5({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3711
+ child.on("close", (code) => resolve5({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3460
3712
  });
3461
3713
  };
3462
3714
  async function verifyCascadeEvidence(evidence, cwd, runCommand = runCascadeVerificationCommand, timeoutMs = CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS) {
@@ -3974,7 +4226,7 @@ async function maybeReReviewCascade({
3974
4226
  const reReadFiles = [];
3975
4227
  for (const f of evidenceBundle.files) {
3976
4228
  try {
3977
- const absPath = path10.join(evidenceBundle.cwd, f.path);
4229
+ const absPath = path11.join(evidenceBundle.cwd, f.path);
3978
4230
  const content = await fsp.readFile(absPath, "utf8");
3979
4231
  reReadFiles.push({ path: f.path, status: "modified", content });
3980
4232
  } catch {
@@ -4051,7 +4303,7 @@ function errorText2(error) {
4051
4303
 
4052
4304
  // src/execution-chimera-review.ts
4053
4305
  import { randomUUID as randomUUID3 } from "node:crypto";
4054
- import path11 from "node:path";
4306
+ import path12 from "node:path";
4055
4307
  import { effectiveFallbackChain, fallbackProfileChain as fallbackProfileChain2 } from "@wrongstack/core/agent";
4056
4308
  import {
4057
4309
  areSubagentsAllowedForSession
@@ -4069,7 +4321,7 @@ import {
4069
4321
  function normalizeFileKeyForCitation(raw, cwd) {
4070
4322
  const forward = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4071
4323
  const isAbsolute = forward.startsWith("/") || /^[a-zA-Z]:\//.test(forward);
4072
- const pathMod = process.platform === "win32" ? path11.win32 : path11;
4324
+ const pathMod = process.platform === "win32" ? path12.win32 : path12;
4073
4325
  const relative = isAbsolute ? pathMod.relative(cwd, forward).replace(/\\/g, "/").replace(/^\.\//, "") : forward;
4074
4326
  return process.platform === "win32" ? relative.toLowerCase() : relative;
4075
4327
  }
@@ -4296,10 +4548,10 @@ function installChimeraReviewHandler({
4296
4548
  const changedBasenameIndex = /* @__PURE__ */ new Map();
4297
4549
  for (const file of p.files) {
4298
4550
  const normalized = normalizeFileKeyForCitation(file.path, p.cwd);
4299
- const basename7 = path11.basename(file.path).toLowerCase();
4300
- const bucket = changedBasenameIndex.get(basename7);
4551
+ const basename8 = path12.basename(file.path).toLowerCase();
4552
+ const bucket = changedBasenameIndex.get(basename8);
4301
4553
  if (bucket) bucket.push(normalized);
4302
- else changedBasenameIndex.set(basename7, [normalized]);
4554
+ else changedBasenameIndex.set(basename8, [normalized]);
4303
4555
  }
4304
4556
  const inScopeCited = citedFindings.filter((finding) => {
4305
4557
  const citedKey = normalizeFileKeyForCitation(finding.location.file, p.cwd);
@@ -4308,8 +4560,8 @@ function installChimeraReviewHandler({
4308
4560
  (cp) => cp.endsWith(`/${citedKey}`) || cp === citedKey
4309
4561
  );
4310
4562
  if (suffixMatches.length === 1) return true;
4311
- const basename7 = path11.basename(finding.location.file).toLowerCase();
4312
- const bucket = changedBasenameIndex.get(basename7);
4563
+ const basename8 = path12.basename(finding.location.file).toLowerCase();
4564
+ const bucket = changedBasenameIndex.get(basename8);
4313
4565
  return !!(bucket && bucket.length === 1);
4314
4566
  });
4315
4567
  const droppedCitations = citedFindings.filter(
@@ -4609,21 +4861,13 @@ function installStorageObservability(events, rootTraceId) {
4609
4861
  // src/next-task-predictor.ts
4610
4862
  import { readBundledInstructionText } from "@wrongstack/core/utils";
4611
4863
  var SYSTEM_PROMPT = readBundledInstructionText("cli/next-task-predictor.md");
4612
- var MAX_REQUEST_CHARS = 1200;
4613
- var MAX_SUMMARY_CHARS = 1200;
4614
- function clamp(text, n) {
4615
- const t = text.trim();
4616
- return t.length <= n ? t : `${t.slice(0, n)}\u2026`;
4617
- }
4618
4864
  function buildPredictionPrompt(input) {
4619
4865
  const parts = [];
4620
4866
  parts.push(`The user asked:
4621
- ${clamp(input.userRequest, MAX_REQUEST_CHARS) || "(no text)"}`);
4867
+ ${input.userRequest.trim() || "(no text)"}`);
4622
4868
  if (input.assistantSummary.trim()) {
4623
- parts.push(
4624
- `The assistant just finished and reported:
4625
- ${clamp(input.assistantSummary, MAX_SUMMARY_CHARS)}`
4626
- );
4869
+ parts.push(`The assistant just finished and reported:
4870
+ ${input.assistantSummary.trim()}`);
4627
4871
  }
4628
4872
  const pending = input.todos.filter((t) => t.status !== "completed");
4629
4873
  if (pending.length > 0) {
@@ -4678,7 +4922,6 @@ async function predictNextTasks(input, opts) {
4678
4922
  messages: [
4679
4923
  { role: "user", content: [{ type: "text", text: buildPredictionPrompt(input) }] }
4680
4924
  ],
4681
- maxTokens: 160,
4682
4925
  temperature: 0.3
4683
4926
  },
4684
4927
  { signal: internal.signal }
@@ -4697,7 +4940,7 @@ import { detectContinueIntent, InputBuilder, resolveContinuation } from "@wrongs
4697
4940
  import {
4698
4941
  color as color11,
4699
4942
  estimateRequestTokensCalibrated as estimateRequestTokensCalibrated2,
4700
- hasOpenTodos as hasOpenTodos2,
4943
+ hasOpenTodos as hasOpenTodos3,
4701
4944
  toErrorMessage as toErrorMessage4
4702
4945
  } from "@wrongstack/core/utils";
4703
4946
  import { readClipboardImage, routeImagesForModel } from "@wrongstack/runtime";
@@ -4711,14 +4954,14 @@ import {
4711
4954
  } from "@wrongstack/tools/auto-proceed-loop-guard";
4712
4955
 
4713
4956
  // src/repl-suggestions.ts
4714
- import { hasOpenTodos } from "@wrongstack/core/utils";
4715
- import { parseNextSteps } from "@wrongstack/tools/next-steps";
4957
+ import { hasOpenTodos as hasOpenTodos2 } from "@wrongstack/core/utils";
4958
+ import { parseNextSteps as parseNextSteps2 } from "@wrongstack/tools/next-steps";
4716
4959
  function parseSuggestionsFromOutput(finalText, todos) {
4717
- if (hasOpenTodos(todos)) {
4960
+ if (hasOpenTodos2(todos)) {
4718
4961
  setAutoSuggestions([]);
4719
4962
  return null;
4720
4963
  }
4721
- const { texts, autoTexts } = parseNextSteps(finalText);
4964
+ const { texts, autoTexts } = parseNextSteps2(finalText);
4722
4965
  if (autoTexts.length > 0) {
4723
4966
  setAutoSuggestions(autoTexts);
4724
4967
  }
@@ -4822,12 +5065,12 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4822
5065
  let interval;
4823
5066
  let lastTickedSecond = sec + 1;
4824
5067
  let onAbort;
4825
- return new Promise((resolve4) => {
4826
- onAbort = () => resolve4(false);
5068
+ return new Promise((resolve5) => {
5069
+ onAbort = () => resolve5(false);
4827
5070
  signal.addEventListener("abort", onAbort, { once: true });
4828
5071
  interval = setInterval(() => {
4829
5072
  if (signal.aborted) {
4830
- resolve4(false);
5073
+ resolve5(false);
4831
5074
  return;
4832
5075
  }
4833
5076
  const elapsed = Date.now() - start;
@@ -4835,7 +5078,7 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4835
5078
  if (remaining <= 0) {
4836
5079
  opts.renderer.write(color6.dim(` \u21B3 ${truncated}
4837
5080
  `));
4838
- resolve4(true);
5081
+ resolve5(true);
4839
5082
  return;
4840
5083
  }
4841
5084
  if (opts.onCountdownTick && remaining !== lastTickedSecond) {
@@ -4846,7 +5089,7 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4846
5089
  opts.renderer.write(
4847
5090
  color6.yellow(" \u21B3 Countdown cancelled \u2014 switching to manual mode\n")
4848
5091
  );
4849
- resolve4(false);
5092
+ resolve5(false);
4850
5093
  return;
4851
5094
  }
4852
5095
  } catch {
@@ -4878,7 +5121,7 @@ async function runContextCompaction(opts, _signal) {
4878
5121
 
4879
5122
  // src/repl-client-registration.ts
4880
5123
  import * as crypto from "node:crypto";
4881
- import * as path12 from "node:path";
5124
+ import * as path13 from "node:path";
4882
5125
  import { getSharedProjectMailbox, resolveProjectDir } from "@wrongstack/core/coordination";
4883
5126
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
4884
5127
  function registerReplClient(opts) {
@@ -4888,7 +5131,7 @@ function registerReplClient(opts) {
4888
5131
  const hqConnection = startCliHqConnection({
4889
5132
  clientKind: "repl",
4890
5133
  projectRoot: replProjectRoot,
4891
- projectName: path12.basename(replProjectRoot),
5134
+ projectName: path13.basename(replProjectRoot),
4892
5135
  appConfig: opts.appConfig,
4893
5136
  capabilities: ["telemetry.publish", "mailbox.summary"]
4894
5137
  });
@@ -4902,7 +5145,7 @@ function registerReplClient(opts) {
4902
5145
  clientMailbox.registerClient({
4903
5146
  clientId,
4904
5147
  sessionId: opts.getSessionId?.() ?? replProjectRoot,
4905
- name: `REPL [${path12.basename(replProjectRoot)}]`,
5148
+ name: `REPL [${path13.basename(replProjectRoot)}]`,
4906
5149
  source: "repl",
4907
5150
  pid: process.pid
4908
5151
  }).then(() => {
@@ -5046,7 +5289,7 @@ async function runReplEternalLoop(opts, onInterruptsReset) {
5046
5289
  } catch (err) {
5047
5290
  opts.renderer.writeError(`[eternal] ${toErrorMessage3(err)}`);
5048
5291
  }
5049
- await new Promise((resolve4) => setTimeout(resolve4, 250));
5292
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
5050
5293
  return true;
5051
5294
  }
5052
5295
  if (autonomy === "eternal-parallel") {
@@ -5117,7 +5360,7 @@ async function runReplEternalLoop(opts, onInterruptsReset) {
5117
5360
  } catch (err) {
5118
5361
  opts.renderer.writeError(`[parallel] ${toErrorMessage3(err)}`);
5119
5362
  }
5120
- await new Promise((resolve4) => setTimeout(resolve4, 250));
5363
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
5121
5364
  return true;
5122
5365
  }
5123
5366
  return false;
@@ -5154,7 +5397,7 @@ async function readPossiblyMultiline(opts) {
5154
5397
  // src/repl-rendering.ts
5155
5398
  import { color as color10 } from "@wrongstack/core/utils";
5156
5399
  var FILLED = "\u2588";
5157
- var EMPTY = "\u2591";
5400
+ var EMPTY2 = "\u2591";
5158
5401
  function renderContextChip(used, max) {
5159
5402
  const ratio = Math.max(0, Math.min(1, used / max));
5160
5403
  const pct = Math.round(ratio * 100);
@@ -5165,7 +5408,7 @@ function renderProgress(ratio, width) {
5165
5408
  const clamped = Math.max(0, Math.min(1, ratio));
5166
5409
  const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
5167
5410
  const capped = Math.min(width, filled);
5168
- return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
5411
+ return FILLED.repeat(capped) + EMPTY2.repeat(width - capped);
5169
5412
  }
5170
5413
  function printBanner(renderer, projectName) {
5171
5414
  const lines = [
@@ -5657,7 +5900,7 @@ ${color11.dim(
5657
5900
  }
5658
5901
  if (result.status === "done" && opts.getAutonomy) {
5659
5902
  const autonomy2 = opts.getAutonomy();
5660
- if (autonomy2 === "suggest" && !hasOpenTodos2(opts.agent.ctx.todos)) {
5903
+ if (autonomy2 === "suggest" && !hasOpenTodos3(opts.agent.ctx.todos)) {
5661
5904
  const suggestPrompt = 'Based on what you just did, suggest 3 exact prompt messages that can be submitted back to you through the TUI or WebUI. Each prompt must ask the agent to perform work; never assign a manual chore to the user. If you include suggestions, wrap them in a balanced <nextsteps>...</nextsteps> block, with one numbered prompt per line and no explanation. If there is nothing meaningful left, say "No further steps needed."';
5662
5905
  const suggestBlocks = [{ type: "text", text: suggestPrompt }];
5663
5906
  const suggestCtrl = new AbortController();
@@ -5694,7 +5937,7 @@ ${suggestResult.finalText}
5694
5937
  }
5695
5938
  }
5696
5939
  }
5697
- if (result.status === "done" && opts.getNextPredict?.() && !hasOpenTodos2(opts.agent.ctx.todos)) {
5940
+ if (result.status === "done" && opts.getNextPredict?.() && !hasOpenTodos3(opts.agent.ctx.todos)) {
5698
5941
  const autonomy2 = opts.getAutonomy?.() ?? "off";
5699
5942
  if (autonomy2 === "off") {
5700
5943
  const predictCtrl = new AbortController();
@@ -6001,8 +6244,8 @@ async function profileMenu(store, paths) {
6001
6244
  }
6002
6245
  const items = await Promise.all(
6003
6246
  names.map(async (name) => {
6004
- const path14 = paths.profileConfig(name);
6005
- const data = await readJsonObjectFile(path14);
6247
+ const path15 = paths.profileConfig(name);
6248
+ const data = await readJsonObjectFile(path15);
6006
6249
  const provider = typeof data["provider"] === "string" ? data["provider"] : "unset";
6007
6250
  const model = typeof data["model"] === "string" ? data["model"] : "unset";
6008
6251
  const fallbackCount = Array.isArray(data["fallbackModels"]) ? data["fallbackModels"].length : 0;
@@ -6023,7 +6266,7 @@ async function profileMenu(store, paths) {
6023
6266
  },
6024
6267
  { label: "fallbacks", value: String(fallbackCount) },
6025
6268
  { label: "fallback profiles", value: String(profileCount) },
6026
- { label: "config", value: path14 }
6269
+ { label: "config", value: path15 }
6027
6270
  ],
6028
6271
  body: isActive ? "This profile owns the current session configuration." : "Switching profiles closes this session so every service restarts with the selected configuration.",
6029
6272
  actions: isActive ? [] : [{ key: "s", label: "switch", command: `/profile switch ${name}`, confirm: true }]
@@ -6153,19 +6396,19 @@ async function gitMenu(projectRoot) {
6153
6396
  },
6154
6397
  ...lines.map((line, index) => {
6155
6398
  const code = line.slice(0, 2);
6156
- const path14 = line.slice(3);
6399
+ const path15 = line.slice(3);
6157
6400
  const staged = code[0] !== " " && code[0] !== "?";
6158
6401
  const unstaged = code[1] !== " " || code === "??";
6159
6402
  return {
6160
- id: `change:${index}:${path14}`,
6161
- label: path14,
6403
+ id: `change:${index}:${path15}`,
6404
+ label: path15,
6162
6405
  status: staged ? "good" : "warn",
6163
6406
  summary: `${code} \xB7 ${staged ? "staged" : "not staged"}${unstaged ? " \xB7 working tree" : ""}`,
6164
6407
  details: [
6165
6408
  { label: "status", value: code },
6166
6409
  { label: "staged", value: staged ? "yes" : "no" },
6167
6410
  { label: "unstaged", value: unstaged ? "yes" : "no" },
6168
- { label: "path", value: path14 }
6411
+ { label: "path", value: path15 }
6169
6412
  ],
6170
6413
  actions: [
6171
6414
  { key: "d", label: "diff summary", command: staged ? "/git diff --staged" : "/git diff" }
@@ -6194,20 +6437,20 @@ async function worktreeMenu(projectRoot) {
6194
6437
  return split < 0 ? [line, "yes"] : [line.slice(0, split), line.slice(split + 1)];
6195
6438
  })
6196
6439
  );
6197
- const path14 = fields["worktree"] ?? "(unknown path)";
6440
+ const path15 = fields["worktree"] ?? "(unknown path)";
6198
6441
  const branchRef = fields["branch"];
6199
6442
  const branch = branchRef?.replace(/^refs\/heads\//, "") ?? (fields["detached"] ? "(detached)" : "(bare)");
6200
- const status = path14 === "(unknown path)" || fields["prunable"] ? null : await runGit(["status", "--porcelain=v1"], path14);
6443
+ const status = path15 === "(unknown path)" || fields["prunable"] ? null : await runGit(["status", "--porcelain=v1"], path15);
6201
6444
  const changes = status?.code === 0 ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
6202
6445
  const dirty = changes.length > 0;
6203
6446
  const isMain = index === 0;
6204
6447
  return {
6205
- id: path14,
6448
+ id: path15,
6206
6449
  label: branch,
6207
6450
  status: fields["prunable"] ? "bad" : dirty ? "warn" : "good",
6208
6451
  summary: `${isMain ? "main" : "linked"} \xB7 ${dirty ? `${changes.length} changes` : "clean"}`,
6209
6452
  details: [
6210
- { label: "path", value: path14 },
6453
+ { label: "path", value: path15 },
6211
6454
  { label: "branch", value: branch },
6212
6455
  { label: "HEAD", value: fields["HEAD"]?.slice(0, 12) ?? "unknown" },
6213
6456
  { label: "working tree", value: dirty ? `${changes.length} changed paths` : "clean" },
@@ -6442,6 +6685,7 @@ async function execute(deps) {
6442
6685
  });
6443
6686
  let code = 0;
6444
6687
  let backgroundKanbanSupervisor;
6688
+ let autoUpdate;
6445
6689
  let fleetStatusLine = null;
6446
6690
  try {
6447
6691
  const visionAdapters = () => createToolVisionAdapters(agent.tools);
@@ -6453,7 +6697,8 @@ async function execute(deps) {
6453
6697
  modelsRegistry,
6454
6698
  context.provider.id,
6455
6699
  context.model,
6456
- mergedModels
6700
+ mergedModels,
6701
+ { catalogProviderId: catalogProviderIdFor(context.provider.id, providerConfig?.type) }
6457
6702
  );
6458
6703
  return caps.vision;
6459
6704
  } catch {
@@ -6484,18 +6729,34 @@ async function execute(deps) {
6484
6729
  log: (message) => console.log(message)
6485
6730
  });
6486
6731
  }
6732
+ if (executionMode !== "single-shot" && !webuiSessionChild) {
6733
+ autoUpdate = (await import("./standalone-auto-update-TOTRGCSA.js")).startStandaloneAutoUpdate({
6734
+ getConfig: () => configStore.get(),
6735
+ updateInfo: bootUpdateInfo
6736
+ });
6737
+ }
6487
6738
  const enteringTui = executionMode === "tui";
6488
6739
  if (!enteringTui) {
6489
6740
  fleetStatusLine = new FleetStatusLine({ events, version: CLI_VERSION });
6490
6741
  fleetStatusLine.start();
6491
6742
  }
6492
6743
  if (executionMode === "single-shot") {
6744
+ const piped = await readPipedStdin(process.stdin);
6745
+ if (piped.timedOut) {
6746
+ renderer.writeWarning(
6747
+ "No stdin data received in time; continuing without it. Redirect stdin (< /dev/null) to skip the wait."
6748
+ );
6749
+ } else if (piped.truncated) {
6750
+ renderer.writeWarning("Piped stdin exceeded 2 MB and was truncated.");
6751
+ }
6752
+ await mcpRegistry.whenStarted();
6493
6753
  code = await runSingleShotDispatch({
6494
6754
  agent,
6495
- query: positional.join(" "),
6755
+ query: appendPipedStdin(positional.join(" "), piped.text),
6496
6756
  flags,
6497
6757
  tokenCounter,
6498
- renderer
6758
+ renderer,
6759
+ events
6499
6760
  });
6500
6761
  } else if (executionMode === "tui") {
6501
6762
  agent.disableInteractiveConfirmation();
@@ -6558,6 +6819,7 @@ async function execute(deps) {
6558
6819
  const switchProjectInPlace2 = async (targetRoot, displayName) => {
6559
6820
  return switchProjectInPlace(switchCtx, targetRoot, displayName);
6560
6821
  };
6822
+ const worktreeSessions = /* @__PURE__ */ new Map();
6561
6823
  const pickerCtx = {
6562
6824
  state,
6563
6825
  renderer,
@@ -6582,6 +6844,10 @@ async function execute(deps) {
6582
6844
  code = await runTuiDispatch({
6583
6845
  leaderAutoWake,
6584
6846
  agent,
6847
+ // The session store's root. Without it /rewind, the checkpoint
6848
+ // timeline and /rewind redo resolve every session file against ''
6849
+ // and fail, and the layout store and history archive stay ephemeral.
6850
+ sessionsDir: wpaths.projectSessions,
6585
6851
  events,
6586
6852
  slashRegistry,
6587
6853
  skillLoader,
@@ -6600,6 +6866,17 @@ async function execute(deps) {
6600
6866
  model: context.model,
6601
6867
  banner: !flags["no-banner"],
6602
6868
  queueStore,
6869
+ // `/resume` brings the resumed session's queue along; the WebUI
6870
+ // keeps a session's queue in the same file.
6871
+ queueStoreFor: (sessionId) => {
6872
+ try {
6873
+ return new QueueStore({
6874
+ dir: sessionScopedPath3(state.wpaths.projectSessions, sessionId, "")
6875
+ });
6876
+ } catch {
6877
+ return void 0;
6878
+ }
6879
+ },
6603
6880
  onQueueChange: (items) => {
6604
6881
  setQueuedMessagesSnapshot2(context, items);
6605
6882
  },
@@ -6623,6 +6900,7 @@ async function execute(deps) {
6623
6900
  appVersion: CLI_VERSION,
6624
6901
  latestVersion: bootUpdateInfo?.latest,
6625
6902
  updateAvailable: bootUpdateInfo?.outdated,
6903
+ subscribeUpdateReady: autoUpdate?.subscribe,
6626
6904
  provider: config.provider,
6627
6905
  family: banneredFamily,
6628
6906
  keyTail: banneredKeyTail,
@@ -6663,7 +6941,14 @@ async function execute(deps) {
6663
6941
  onClearHistory: (dispatch) => {
6664
6942
  void attachments.clear().catch(() => {
6665
6943
  });
6666
- dispatch({ type: "clearHistory", model: context.model, provider: context.provider.id });
6944
+ dispatch({
6945
+ type: "clearHistory",
6946
+ model: context.model,
6947
+ provider: context.provider.id,
6948
+ restoredMessages: null,
6949
+ restoredToolCalls: null,
6950
+ restoredEvents: null
6951
+ });
6667
6952
  dispatch({ type: "resetContextChip" });
6668
6953
  dispatch({ type: "streamReset" });
6669
6954
  dispatch({ type: "toolStreamClear" });
@@ -6766,53 +7051,49 @@ async function execute(deps) {
6766
7051
  } catch {
6767
7052
  }
6768
7053
  const currentId = agent.ctx.session?.id ?? session.id;
6769
- return summaries.map((s) => ({
6770
- id: s.id,
6771
- title: s.title ?? "",
6772
- name: s.name,
6773
- lastUserMessage: s.lastUserMessage,
6774
- messageCount: s.messageCount,
6775
- lastActivityAt: s.lastActivityAt,
6776
- startedAt: s.startedAt ?? "",
6777
- endedAt: s.endedAt,
6778
- tokenTotal: s.tokenTotal ?? 0,
6779
- iterationCount: s.iterationCount ?? 0,
6780
- toolCallCount: s.toolCallCount ?? 0,
6781
- toolErrorCount: s.toolErrorCount ?? 0,
6782
- outcome: s.outcome,
6783
- isCurrent: s.id === currentId,
6784
- // The session this process owns is `isCurrent`, not "live
6785
- // elsewhere" — it holds its own lease and would otherwise be
6786
- // labelled as owned by another surface.
6787
- ...s.id !== currentId && liveBySession.has(s.id) ? { live: liveBySession.get(s.id) } : {}
6788
- }));
7054
+ const worktrees = await listGitWorktrees(state.projectRoot);
7055
+ worktreeSessions.clear();
7056
+ const toEntry = (s, worktree = worktreeOfSession(s, worktrees)) => {
7057
+ if (worktree) worktreeSessions.set(s.id, worktree);
7058
+ return {
7059
+ id: s.id,
7060
+ title: s.title ?? "",
7061
+ name: s.name,
7062
+ lastUserMessage: s.lastUserMessage,
7063
+ messageCount: s.messageCount,
7064
+ lastActivityAt: s.lastActivityAt,
7065
+ startedAt: s.startedAt ?? "",
7066
+ endedAt: s.endedAt,
7067
+ tokenTotal: s.tokenTotal ?? 0,
7068
+ iterationCount: s.iterationCount ?? 0,
7069
+ toolCallCount: s.toolCallCount ?? 0,
7070
+ toolErrorCount: s.toolErrorCount ?? 0,
7071
+ outcome: s.outcome,
7072
+ forkedFrom: s.forkedFrom,
7073
+ ...worktree ? { worktree } : {},
7074
+ isCurrent: s.id === currentId,
7075
+ // The session this process owns is `isCurrent`, not "live
7076
+ // elsewhere" — it holds its own lease and would otherwise be
7077
+ // labelled as owned by another surface.
7078
+ ...s.id !== currentId && liveBySession.has(s.id) ? { live: liveBySession.get(s.id) } : {}
7079
+ };
7080
+ };
7081
+ return summaries.map((s) => toEntry(s));
6789
7082
  },
6790
- onResumeSession: async (sessionId, onLoadProgress, onStage) => {
6791
- let failure;
6792
- const result = await resumeSession(
6793
- {
6794
- state,
6795
- agent,
6796
- tokenCounter,
6797
- switchProviderAndModel,
6798
- events,
6799
- onLoadProgress,
6800
- onStage,
6801
- onFailure: (info) => {
6802
- failure = info;
6803
- }
6804
- },
6805
- sessionId
6806
- );
6807
- if (!result) {
6808
- throw new Error(
6809
- failure ? `${failure.message} (at ${failure.stage})` : `Session "${sessionId}" could not be resumed.`
6810
- );
6811
- }
6812
- setAutoSuggestions([]);
6813
- const resumedNextSteps = result.attached && !hasOpenTodos3(agent.ctx.todos) && result.lastAssistantText ? parseNextSteps2(result.lastAssistantText).texts : [];
6814
- return { ...result, nextSteps: resumedNextSteps };
7083
+ forkSession: async (sessionId, checkpointPromptIndex) => {
7084
+ const store = state.activeSessionStore;
7085
+ if (!store?.fork) throw new Error("This session store cannot fork sessions.");
7086
+ const forked = await store.fork(sessionId, {
7087
+ checkpointPromptIndex,
7088
+ beforeCheckpointPrompt: true
7089
+ });
7090
+ return { id: forked.id };
6815
7091
  },
7092
+ onResumeSession: withWorktreeSwitch(
7093
+ createTuiResumeCallback({ state, agent, tokenCounter, switchProviderAndModel, events }),
7094
+ (id) => worktreeSessions.get(id),
7095
+ switchProjectInPlace2
7096
+ ),
6816
7097
  getProjectPickerItems: () => getProjectPickerItems(pickerCtx),
6817
7098
  onProjectSelect: (slug, kind) => onProjectSelect(pickerCtx, slug, kind),
6818
7099
  initialAgentsMonitorOpen: !!flags.quick,
@@ -6940,7 +7221,7 @@ async function execute(deps) {
6940
7221
  attachments,
6941
7222
  effectiveMaxContext,
6942
7223
  getEffectiveMaxContext,
6943
- projectName: path13.basename(projectRoot) || void 0,
7224
+ projectName: path14.basename(projectRoot) || void 0,
6944
7225
  projectRoot,
6945
7226
  appConfig: config,
6946
7227
  getSessionId: () => agent.ctx.session?.id ?? session.id,
@@ -6972,6 +7253,7 @@ async function execute(deps) {
6972
7253
  }
6973
7254
  } finally {
6974
7255
  backgroundKanbanSupervisor?.dispose();
7256
+ autoUpdate?.dispose();
6975
7257
  for (const off of chimeraTeardowns.splice(0)) {
6976
7258
  try {
6977
7259
  off();
@@ -7006,4 +7288,4 @@ export {
7006
7288
  execute,
7007
7289
  resolveReviewerFallbackModels
7008
7290
  };
7009
- //# sourceMappingURL=execution-PYC5FCS4.js.map
7291
+ //# sourceMappingURL=execution-7QIP6GRY.js.map