@wrongstack/cli 1.0.24 → 1.0.27

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 (146) hide show
  1. package/data/providers.json +44 -0
  2. package/dist/{acp-AJWSKWE5.js → acp-M4565YUW.js} +33 -20
  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 +12 -1
  10. package/dist/boot/dispatch-webui.d.ts +2 -0
  11. package/dist/boot/goal-tui-default.d.ts +10 -0
  12. package/dist/boot/mcp-config-flag.d.ts +22 -0
  13. package/dist/boot/piped-stdin.d.ts +40 -0
  14. package/dist/boot/restricted-mode.d.ts +15 -0
  15. package/dist/boot/safe-mode.d.ts +30 -0
  16. package/dist/boot/single-shot-budget.d.ts +30 -0
  17. package/dist/boot/stream-json.d.ts +36 -0
  18. package/dist/boot/structured-output.d.ts +16 -0
  19. package/dist/boot/system-prompt-builder.d.ts +2 -0
  20. package/dist/boot/tool-restriction-flags.d.ts +36 -0
  21. package/dist/boot/worktree-sessions.d.ts +21 -0
  22. package/dist/boot-provider-selection.d.ts +67 -0
  23. package/dist/boot.d.ts +1 -38
  24. package/dist/{chunk-LFMVHT3R.js → chunk-2ERX3RPN.js} +11 -13
  25. package/dist/chunk-4TVYGG6G.js +62 -0
  26. package/dist/chunk-5JYDKG5B.js +154 -0
  27. package/dist/{chunk-3VXYY5JC.js → chunk-5UZBUV2H.js} +3 -19
  28. package/dist/chunk-CZTXF6HY.js +140 -0
  29. package/dist/{chunk-DRM7PSW7.js → chunk-ENRYW73H.js} +527 -381
  30. package/dist/{chunk-SKPDE62W.js → chunk-EPR42SWM.js} +36 -6
  31. package/dist/{chunk-YBUCXXYZ.js → chunk-F5DCFAKI.js} +1 -1
  32. package/dist/chunk-HDMFKTT3.js +157 -0
  33. package/dist/{chunk-PGKJ2OQP.js → chunk-HFZH2XJN.js} +5 -5
  34. package/dist/chunk-HLSHFXTO.js +97 -0
  35. package/dist/{chunk-QFPQXKCG.js → chunk-KOEO4CJG.js} +21 -6
  36. package/dist/chunk-LBPLTTZ6.js +105 -0
  37. package/dist/{chunk-3OCUS3VN.js → chunk-LDCGZJTC.js} +4 -4
  38. package/dist/chunk-LKPEBZBE.js +143 -0
  39. package/dist/chunk-N57IU5FG.js +58 -0
  40. package/dist/{chunk-2EBVTYJR.js → chunk-NRXWOMK7.js} +17 -3
  41. package/dist/chunk-PZQBGUVZ.js +18 -0
  42. package/dist/{chunk-MHEBWVXL.js → chunk-Q4INH2DG.js} +46 -2
  43. package/dist/{chunk-5SEYGOCH.js → chunk-RDGGWMTI.js} +2 -2
  44. package/dist/{chunk-BLD5DSVA.js → chunk-RGFWGUAB.js} +27 -15
  45. package/dist/{chunk-ZZRSG7VK.js → chunk-RZ4ZDRYA.js} +3 -3
  46. package/dist/{chunk-S4FFHDJG.js → chunk-SDJCLEK6.js} +6 -2
  47. package/dist/{chunk-U46QR4WR.js → chunk-TI7KLHYX.js} +132 -3
  48. package/dist/chunk-X756ZS72.js +329 -0
  49. package/dist/{chunk-JF2MULWF.js → chunk-YHYINL47.js} +47 -15
  50. package/dist/{cli-context-34Q5IJRA.js → cli-context-C7RBSWHF.js} +434 -327
  51. package/dist/{cli-main-AELJDBGU.js → cli-main-TQTH73ZK.js} +2948 -2315
  52. package/dist/cli-update-notice.d.ts +2 -1
  53. package/dist/config-doctor.d.ts +1 -1
  54. package/dist/{diag-doctor-WIFJH3XH.js → diag-doctor-BIZAFIAT.js} +2 -2
  55. package/dist/{execution-PYC5FCS4.js → execution-3STBD2U7.js} +2167 -1831
  56. package/dist/execution-chimera-review.d.ts +18 -1
  57. package/dist/fleet/host-types.d.ts +3 -1
  58. package/dist/goal-commands.d.ts +47 -0
  59. package/dist/goal-host.d.ts +1 -14
  60. package/dist/{hq-JTUTFI4L.js → hq-44F4OQH2.js} +9 -8
  61. package/dist/hq-fleet-control.d.ts +36 -0
  62. package/dist/hq-live-status.d.ts +22 -0
  63. package/dist/hq-publisher.d.ts +18 -0
  64. package/dist/hq-server/routes/command-handlers.d.ts +12 -1
  65. package/dist/hq-server/routes/data-handlers.d.ts +1 -1
  66. package/dist/hq-server/socket-credentials.d.ts +63 -0
  67. package/dist/hq-server/upgrade-handler.d.ts +2 -0
  68. package/dist/hq-server/ws.d.ts +24 -0
  69. package/dist/{hq-server-PC7GZMPT.js → hq-server-UIB24LU6.js} +2 -2
  70. package/dist/{hq-service-NW56MUP2.js → hq-service-BMMJAHRT.js} +4 -4
  71. package/dist/import-claude-code-AYG24RG2.js +268 -0
  72. package/dist/index.js +12 -8
  73. package/dist/live-settings-input.d.ts +1 -0
  74. package/dist/{mailbox-serve-WDOCH6KR.js → mailbox-serve-GNGXEQ2R.js} +3 -3
  75. package/dist/{modeldiag-36OGAL5C.js → modeldiag-W7XAE625.js} +9 -9
  76. package/dist/{per-subcommand-help-XE77MTGJ.js → per-subcommand-help-QDAQASPZ.js} +2 -2
  77. package/dist/permission-prompt.d.ts +14 -0
  78. package/dist/permission-rules-view.d.ts +18 -0
  79. package/dist/{permissions-CFDLXETM.js → permissions-PL27UHYD.js} +67 -45
  80. package/dist/{plugin-usage-S5LVGMBW.js → plugin-usage-CIGOOTX3.js} +2 -2
  81. package/dist/{plugins-RHBIVLI7.js → plugins-HQTXUD6A.js} +4 -2
  82. package/dist/preflight.d.ts +1 -1
  83. package/dist/{projects-LMNAGGEU.js → projects-T4CZCA25.js} +3 -6
  84. package/dist/provider-helpers.d.ts +7 -11
  85. package/dist/{providers-models-2CFG46RD.js → providers-models-5Z2PJTEW.js} +5 -5
  86. package/dist/release-asset-download.d.ts +10 -0
  87. package/dist/release-attestation.d.ts +28 -0
  88. package/dist/remote/remote-session.d.ts +64 -0
  89. package/dist/remote/remote-target.d.ts +69 -0
  90. package/dist/remote-GB5VXCCT.js +576 -0
  91. package/dist/{replay-Z3FTJEMM.js → replay-DEI566CW.js} +2 -2
  92. package/dist/services/commit-message.d.ts +2 -1
  93. package/dist/session-move-P2EC2COA.js +44 -0
  94. package/dist/session-move.d.ts +20 -0
  95. package/dist/session-stats.d.ts +10 -0
  96. package/dist/{sessions-config-IQUH2GXT.js → sessions-config-XHLXJ5C3.js} +109 -10
  97. package/dist/short-circuit-flags-5TNTJPQR.js +10 -0
  98. package/dist/slash-commands/command-context.d.ts +15 -5
  99. package/dist/slash-commands/goal-refiner.d.ts +4 -49
  100. package/dist/slash-commands/mcp.d.ts +2 -1
  101. package/dist/slash-commands/permissions.d.ts +4 -0
  102. package/dist/slash-commands/settings-context-mutations.d.ts +5 -0
  103. package/dist/slash-commands/settings-limits-mutations.d.ts +12 -0
  104. package/dist/slash-commands/theme.d.ts +15 -1
  105. package/dist/standalone-auto-update-TOTRGCSA.js +249 -0
  106. package/dist/standalone-auto-update.d.ts +65 -0
  107. package/dist/standalone-update-LZO3MNLH.js +24 -0
  108. package/dist/standalone-update.d.ts +37 -1
  109. package/dist/subcommands/handlers/import-claude-code.d.ts +52 -0
  110. package/dist/subcommands/handlers/permissions.d.ts +9 -3
  111. package/dist/subcommands/handlers/remote.d.ts +6 -0
  112. package/dist/subcommands/handlers/sessions-fleet.d.ts +0 -12
  113. package/dist/subcommands/handlers/version-help.d.ts +5 -0
  114. package/dist/{subcommands-ZTSIN7LG.js → subcommands-BHQYLDSD.js} +2 -2
  115. package/dist/tui-resume-callback.d.ts +18 -0
  116. package/dist/{typesafe-HVEVPA6R.js → typesafe-U7YB2RRV.js} +5 -5
  117. package/dist/{typesafe-topic-replay-4W73BOLV.js → typesafe-topic-replay-ZILIU3WS.js} +18 -2
  118. package/dist/{update-7XIOEXTD.js → update-TDZX4IL2.js} +4 -4
  119. package/dist/{update-check-2POMBHFB.js → update-check-J5ZVKS73.js} +3 -3
  120. package/dist/version-JB2UAV2A.js +11 -0
  121. package/dist/version-help-XJJQ7YSH.js +10 -0
  122. package/dist/webui-server/route-contexts.d.ts +1 -1
  123. package/dist/webui-server/session-start-payload.d.ts +5 -1
  124. package/dist/{webui-server-L2SICPTB.js → webui-server-GCXIO3JY.js} +515 -365
  125. package/dist/webui-server-lifecycle.d.ts +57 -0
  126. package/dist/webui-server-options.d.ts +10 -0
  127. package/dist/webui-server.d.ts +0 -14
  128. package/dist/wiring/brain-and-orchestration.d.ts +3 -1
  129. package/dist/wiring/cli-prompt-and-tools-setup.d.ts +9 -0
  130. package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
  131. package/dist/wiring/command-host-state.d.ts +2 -0
  132. package/dist/wiring/hq-telemetry.d.ts +12 -0
  133. package/dist/wiring/lifecycle-plugins.d.ts +4 -2
  134. package/dist/wiring/metrics.d.ts +9 -1
  135. package/dist/wiring/pipeline.d.ts +2 -7
  136. package/dist/wiring/plugins.d.ts +3 -0
  137. package/dist/wiring/provider-status.d.ts +1 -0
  138. package/dist/wiring/provider-utility-tools.d.ts +3 -1
  139. package/package.json +29 -28
  140. package/dist/chunk-2BKFWIER.js +0 -98
  141. package/dist/chunk-424ORSE7.js +0 -56
  142. package/dist/chunk-OWNC7LMD.js +0 -78
  143. package/dist/chunk-QBBF7GIK.js +0 -65
  144. package/dist/short-circuit-flags-SDT53OML.js +0 -10
  145. package/dist/standalone-update-MW47J3AK.js +0 -164
  146. package/dist/version-help-WZ623NWM.js +0 -10
@@ -1,14 +1,35 @@
1
1
  import {
2
+ createHqFleetControl,
2
3
  getSuggestions,
3
4
  normalizeTuiThinkingWord,
5
+ resolveExecutionMode,
4
6
  runGit,
5
7
  setAutoSuggestions,
6
8
  setSuggestions
7
- } from "./chunk-OWNC7LMD.js";
9
+ } from "./chunk-HDMFKTT3.js";
8
10
  import {
9
11
  deriveFsAccessPair,
10
12
  persistConfigSetting
11
13
  } from "./chunk-SFBFYIOW.js";
14
+ import {
15
+ checkStructuredAnswer,
16
+ parseOutputFormat,
17
+ repairPrompt,
18
+ startStreamJson,
19
+ streamJsonInit,
20
+ withSchemaInstruction
21
+ } from "./chunk-LKPEBZBE.js";
22
+ import {
23
+ theme
24
+ } from "./chunk-QD544J2B.js";
25
+ import {
26
+ WEBUI_SESSION_CHILD_CAPABILITIES,
27
+ writeWebuiSessionChildError,
28
+ writeWebuiSessionChildReady
29
+ } from "./chunk-ZMX5HRLI.js";
30
+ import {
31
+ fmtTok
32
+ } from "./chunk-EWKESL4L.js";
12
33
  import "./chunk-WHOIR577.js";
13
34
  import {
14
35
  advanceToNextTask,
@@ -23,49 +44,38 @@ import {
23
44
  trySaveSpecFromAIOutput,
24
45
  trySaveTasksFromAIOutput
25
46
  } 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
47
  import {
38
48
  terminalLink,
39
49
  terminalText
40
50
  } from "./chunk-HXJYHAR4.js";
41
- import {
42
- activeProfileConfigPath
43
- } from "./chunk-YMXXOOFN.js";
44
- import {
45
- backupCurrent
46
- } from "./chunk-WSTVGC2C.js";
47
51
  import {
48
52
  startCliHqConnection
49
- } from "./chunk-BLD5DSVA.js";
53
+ } from "./chunk-RGFWGUAB.js";
54
+ import {
55
+ CLI_VERSION
56
+ } from "./chunk-F5DCFAKI.js";
50
57
  import {
51
58
  resolveActiveApiKey
52
59
  } from "./chunk-LUMETJFO.js";
53
60
  import {
54
- CLI_VERSION
55
- } from "./chunk-YBUCXXYZ.js";
61
+ activeProfileConfigPath
62
+ } from "./chunk-YMXXOOFN.js";
63
+ import {
64
+ backupCurrent
65
+ } from "./chunk-WSTVGC2C.js";
56
66
 
57
67
  // src/execution.ts
58
- import * as path13 from "node:path";
68
+ import * as path14 from "node:path";
59
69
  import { effectiveFallbackChain as effectiveFallbackChain2, setQueuedMessagesSnapshot as setQueuedMessagesSnapshot2 } from "@wrongstack/core/agent";
60
70
  import { LeaderAutoWakeController } from "@wrongstack/core/coordination";
61
71
  import { updateReviewReportEvidence } from "@wrongstack/core/plugin";
62
72
  import { noOpVault } from "@wrongstack/core/security";
63
- import { attachTodosCheckpoint as attachTodosCheckpoint2 } from "@wrongstack/core/storage";
73
+ import { attachTodosCheckpoint as attachTodosCheckpoint2, QueueStore } from "@wrongstack/core/storage";
64
74
  import { normalizeTokenSavingTier as normalizeTokenSavingTier2 } from "@wrongstack/core/types";
65
- import { hasOpenTodos as hasOpenTodos3, mergeCustomModelDefs } from "@wrongstack/core/utils";
66
- import { capabilitiesFor } from "@wrongstack/providers";
75
+ import { mergeCustomModelDefs, sessionScopedPath as sessionScopedPath3 } from "@wrongstack/core/utils";
76
+ import { listGitWorktrees } from "@wrongstack/core/worktree";
77
+ import { capabilitiesFor, catalogProviderIdFor } from "@wrongstack/providers";
67
78
  import { createToolVisionAdapters } from "@wrongstack/runtime/vision";
68
- import { parseNextSteps as parseNextSteps2 } from "@wrongstack/tools/next-steps";
69
79
 
70
80
  // src/boot/dispatch-singleshot.ts
71
81
  import { color, writeOut } from "@wrongstack/core/utils";
@@ -87,20 +97,130 @@ ${err.describe()}`);
87
97
  ].join("\n");
88
98
  }
89
99
 
100
+ // src/boot/single-shot-budget.ts
101
+ function parseMaxBudgetUsd(value) {
102
+ if (value === void 0) return void 0;
103
+ const amount = typeof value === "string" ? Number(value.trim()) : Number.NaN;
104
+ if (!Number.isFinite(amount) || amount <= 0) {
105
+ throw new Error("--max-budget-usd needs a positive dollar amount (e.g. --max-budget-usd 0.50)");
106
+ }
107
+ return amount;
108
+ }
109
+ function watchSingleShotBudget(opts) {
110
+ const leaderStart = opts.tokenCounter.estimateCost().total;
111
+ const subagentCost = /* @__PURE__ */ new Map();
112
+ let exceeded = false;
113
+ const spent = () => {
114
+ let total = opts.tokenCounter.estimateCost().total - leaderStart;
115
+ for (const cost of subagentCost.values()) total += cost;
116
+ return total;
117
+ };
118
+ const check = () => {
119
+ if (exceeded || spent() <= opts.limitUsd) return;
120
+ exceeded = true;
121
+ opts.onExceeded();
122
+ };
123
+ const offLeader = opts.events.on("token.accounted", check);
124
+ const offSubagent = opts.events.on("subagent.token_accounted", (payload) => {
125
+ subagentCost.set(payload.subagentId, payload.cost.total);
126
+ check();
127
+ });
128
+ return {
129
+ spent,
130
+ get exceeded() {
131
+ return exceeded;
132
+ },
133
+ dispose() {
134
+ offLeader();
135
+ offSubagent();
136
+ }
137
+ };
138
+ }
139
+
90
140
  // src/boot/dispatch-singleshot.ts
141
+ function exitCodeForStatus(status) {
142
+ if (status === "failed" || status === "max_iterations") return 1;
143
+ if (status === "aborted") return 130;
144
+ return 0;
145
+ }
91
146
  async function runSingleShotDispatch(ctx) {
92
147
  const { agent, query, flags, tokenCounter, renderer } = ctx;
148
+ let limitUsd;
149
+ let format;
150
+ try {
151
+ limitUsd = parseMaxBudgetUsd(flags["max-budget-usd"]);
152
+ format = parseOutputFormat(flags["output-format"]) ?? (flags["output-json"] ? "json" : "text");
153
+ } catch (err) {
154
+ renderer.writeError(err instanceof Error ? err.message : String(err));
155
+ return 2;
156
+ }
93
157
  const ctrl = new AbortController();
94
158
  const onSigint = () => ctrl.abort();
95
159
  process.on("SIGINT", onSigint);
160
+ const interrupt = ctx.interruptController;
161
+ const previousAbortLeader = interrupt?.abortLeader;
162
+ if (interrupt !== void 0) {
163
+ interrupt.abortLeader = () => {
164
+ if (ctrl.signal.aborted) return false;
165
+ ctrl.abort();
166
+ return true;
167
+ };
168
+ }
169
+ let budget;
170
+ if (limitUsd !== void 0 && ctx.events) {
171
+ budget = watchSingleShotBudget({
172
+ limitUsd,
173
+ tokenCounter,
174
+ events: ctx.events,
175
+ onExceeded: () => ctrl.abort()
176
+ });
177
+ }
178
+ let stopStream;
179
+ if (format === "stream-json") {
180
+ const tools = agent.tools?.listForProvider?.() ?? agent.tools?.list?.() ?? [];
181
+ writeOut(
182
+ streamJsonInit({
183
+ sessionId: agent.ctx?.session?.id ?? null,
184
+ provider: agent.ctx?.provider?.id ?? null,
185
+ model: agent.ctx?.model ?? null,
186
+ cwd: agent.ctx?.cwd ?? process.cwd(),
187
+ tools: tools.map((tool) => tool.name)
188
+ })
189
+ );
190
+ if (ctx.events) {
191
+ stopStream = startStreamJson({
192
+ events: ctx.events,
193
+ write: (line) => writeOut(line),
194
+ includePartialMessages: flags["include-partial-messages"] === true
195
+ });
196
+ }
197
+ }
96
198
  const startedAt = Date.now();
97
199
  const before = tokenCounter.total();
98
200
  const costBefore = tokenCounter.estimateCost().total;
201
+ const schema = typeof flags["json-schema"] === "string" ? JSON.parse(flags["json-schema"]) : void 0;
99
202
  let result;
203
+ let structured;
100
204
  try {
101
- result = await agent.run(query, { signal: ctrl.signal });
205
+ result = await agent.run(schema ? withSchemaInstruction(query, schema) : query, {
206
+ signal: ctrl.signal
207
+ });
208
+ if (schema && result.status === "done") {
209
+ structured = checkStructuredAnswer(result.finalText, schema);
210
+ if (!structured.ok && !ctrl.signal.aborted) {
211
+ const first = result;
212
+ result = await agent.run(repairPrompt(structured.errors), { signal: ctrl.signal });
213
+ result = { ...result, iterations: first.iterations + result.iterations };
214
+ if (result.status === "done") structured = checkStructuredAnswer(result.finalText, schema);
215
+ }
216
+ }
102
217
  } finally {
218
+ stopStream?.();
219
+ budget?.dispose();
103
220
  process.off("SIGINT", onSigint);
221
+ if (interrupt !== void 0 && previousAbortLeader !== void 0) {
222
+ interrupt.abortLeader = previousAbortLeader;
223
+ }
104
224
  const { getProcessRegistry: getProcessRegistry2 } = await import("@wrongstack/tools");
105
225
  getProcessRegistry2().killAll({ preserveBackground: true });
106
226
  }
@@ -113,9 +233,17 @@ async function runSingleShotDispatch(ctx) {
113
233
  cost: costAfter - costBefore,
114
234
  elapsedMs: Date.now() - startedAt
115
235
  };
116
- if (flags["output-json"]) {
236
+ const schemaFailed = structured !== void 0 && !structured.ok;
237
+ const exitCode = budget?.exceeded || schemaFailed ? 1 : exitCodeForStatus(result.status);
238
+ if (format !== "text") {
117
239
  const json = JSON.stringify({
240
+ // In a stream the result is one event among many; name it.
241
+ ...format === "stream-json" ? { type: "result" } : {},
118
242
  status: result.status,
243
+ // Lets a script continue the conversation: `wstack --resume <sessionId>`.
244
+ // Read defensively — the session is swapped in by /resume and absent in
245
+ // some embedders.
246
+ sessionId: agent.ctx?.session?.id ?? null,
119
247
  finalText: result.finalText ?? null,
120
248
  error: result.error ? {
121
249
  code: result.error.code,
@@ -125,14 +253,22 @@ async function runSingleShotDispatch(ctx) {
125
253
  message: result.error.message,
126
254
  context: result.error.context ?? null
127
255
  } : null,
128
- usage
256
+ usage,
257
+ ...limitUsd !== void 0 ? { budget: { limitUsd, exceeded: budget?.exceeded ?? false } } : {},
258
+ ...schema ? {
259
+ structuredOutput: structured?.ok ? structured.value : null,
260
+ schemaErrors: structured && !structured.ok ? structured.errors : null
261
+ } : {}
129
262
  });
130
263
  writeOut(json + "\n");
131
- return 0;
264
+ return exitCode;
132
265
  }
133
- let code = 0;
134
- if (result.status === "failed") {
135
- code = 1;
266
+ const code = exitCode;
267
+ if (budget?.exceeded) {
268
+ renderer.writeError(
269
+ `Stopped: spend $${budget.spent().toFixed(4)} exceeded --max-budget-usd $${limitUsd}.`
270
+ );
271
+ } else if (result.status === "failed") {
136
272
  const err = result.error;
137
273
  if (err) {
138
274
  const tag = err.recoverable ? " (recoverable)" : "";
@@ -143,11 +279,11 @@ async function runSingleShotDispatch(ctx) {
143
279
  renderer.writeError("Failed.");
144
280
  }
145
281
  } else if (result.status === "aborted") {
146
- code = 130;
147
282
  renderer.writeWarning("Aborted.");
148
283
  } else if (result.status === "max_iterations") {
149
- code = 1;
150
284
  renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
285
+ } else if (structured && !structured.ok) {
286
+ renderer.writeError(`Answer did not match --json-schema: ${structured.errors.join("; ")}`);
151
287
  }
152
288
  if (result.finalText) renderer.write("\n" + result.finalText + "\n");
153
289
  const r = result;
@@ -208,6 +344,8 @@ async function runWebUIDispatch(ctx) {
208
344
  onKanbanDispatch,
209
345
  getFleetBudget,
210
346
  stopSessionFleet,
347
+ interruptController,
348
+ hqFleetControl,
211
349
  onSessionRetired,
212
350
  leaderAutoWake,
213
351
  webuiSessionChild
@@ -216,7 +354,7 @@ async function runWebUIDispatch(ctx) {
216
354
  const isSimpleUi = !isSessionChild && flags["simpleui"] === true;
217
355
  agent.disableInteractiveConfirmation();
218
356
  renderer.setSilent(true);
219
- const { runWebUI } = await import("./webui-server-L2SICPTB.js");
357
+ const { runWebUI } = await import("./webui-server-GCXIO3JY.js");
220
358
  const flagValue = (names) => {
221
359
  for (const name of names) {
222
360
  if (!Object.hasOwn(flags, name)) continue;
@@ -318,6 +456,8 @@ async function runWebUIDispatch(ctx) {
318
456
  sessionStore,
319
457
  ...getFleetBudget ? { getFleetBudget } : {},
320
458
  ...stopSessionFleet ? { stopSessionFleet } : {},
459
+ ...interruptController ? { interruptController } : {},
460
+ ...hqFleetControl ? { hqFleetControl } : {},
321
461
  ...onSessionRetired ? { onSessionRetired } : {},
322
462
  ...leaderAutoWake ? { leaderAutoWake } : {},
323
463
  sessionsDir: projectSessionsDir,
@@ -426,14 +566,14 @@ async function runWebUIDispatch(ctx) {
426
566
  );
427
567
  }
428
568
  });
429
- const webuiExit = new Promise((resolve4) => {
569
+ const webuiExit = new Promise((resolve5) => {
430
570
  webuiPromise.then(() => {
431
571
  renderer.setSilent(false);
432
572
  renderer.write("\n");
433
573
  renderer.writeInfo(
434
574
  color2.yellow(` Shutting down ${isSimpleUi ? "SimpleUI" : "WebUI"} server\u2026`)
435
575
  );
436
- resolve4(0);
576
+ resolve5(0);
437
577
  }).catch((err) => {
438
578
  if (webuiSessionChild) {
439
579
  void writeWebuiSessionChildError(webuiSessionChild.readyFile, {
@@ -451,20 +591,72 @@ async function runWebUIDispatch(ctx) {
451
591
  ` \u2717 ${isSimpleUi ? "SimpleUI" : "WebUI"} failed to start: ${err instanceof Error ? err.message : String(err)}`
452
592
  )
453
593
  );
454
- resolve4(1);
594
+ resolve5(1);
455
595
  });
456
596
  });
457
597
  return webuiExit;
458
598
  }
459
599
 
460
- // src/boot/execution-mode.ts
461
- function resolveExecutionMode(positional, flags) {
462
- const prompt = typeof flags["prompt"] === "string" ? flags["prompt"] : "";
463
- if (positional.length > 0 || prompt.length > 0) return "single-shot";
464
- if (flags.tui && flags["no-tui"] !== true && !flags.webui && !flags["webui-session-child"])
465
- return "tui";
466
- if (flags.webui || flags["webui-session-child"]) return "webui";
467
- return "repl";
600
+ // src/boot/piped-stdin.ts
601
+ var PIPED_STDIN_FIRST_BYTE_TIMEOUT_MS = 3e3;
602
+ var PIPED_STDIN_MAX_BYTES = 2 * 1024 * 1024;
603
+ var EMPTY = { text: "", truncated: false, timedOut: false };
604
+ function readPipedStdin(source, opts = {}) {
605
+ if (source.isTTY) return Promise.resolve(EMPTY);
606
+ const firstByteTimeoutMs = opts.firstByteTimeoutMs ?? PIPED_STDIN_FIRST_BYTE_TIMEOUT_MS;
607
+ const maxBytes = opts.maxBytes ?? PIPED_STDIN_MAX_BYTES;
608
+ return new Promise((resolve5) => {
609
+ const chunks = [];
610
+ let size = 0;
611
+ let truncated = false;
612
+ let settled = false;
613
+ const finish = (timedOut) => {
614
+ if (settled) return;
615
+ settled = true;
616
+ clearTimeout(timer);
617
+ source.removeListener("data", onData);
618
+ source.removeListener("end", onEnd);
619
+ source.removeListener("close", onEnd);
620
+ source.removeListener("error", onError);
621
+ source.pause();
622
+ if (timedOut || truncated) source.unref?.();
623
+ const text = timedOut ? "" : Buffer.concat(chunks).toString("utf8").trimEnd();
624
+ resolve5({ text, truncated, timedOut });
625
+ };
626
+ const onData = (chunk) => {
627
+ clearTimeout(timer);
628
+ const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
629
+ const room = maxBytes - size;
630
+ if (buf.length >= room) {
631
+ chunks.push(buf.subarray(0, Math.max(0, room)));
632
+ size = maxBytes;
633
+ truncated = true;
634
+ finish(false);
635
+ return;
636
+ }
637
+ chunks.push(buf);
638
+ size += buf.length;
639
+ };
640
+ const onEnd = () => finish(false);
641
+ const onError = () => finish(false);
642
+ const timer = setTimeout(() => {
643
+ if (size === 0) finish(true);
644
+ }, firstByteTimeoutMs);
645
+ source.on("data", onData);
646
+ source.on("end", onEnd);
647
+ source.on("close", onEnd);
648
+ source.on("error", onError);
649
+ source.resume();
650
+ });
651
+ }
652
+ function appendPipedStdin(query, piped) {
653
+ if (!piped) return query;
654
+ if (!query.trim()) return piped;
655
+ return `${query}
656
+
657
+ <stdin>
658
+ ${piped}
659
+ </stdin>`;
468
660
  }
469
661
 
470
662
  // src/boot/tui-coordinator-callbacks.ts
@@ -656,7 +848,6 @@ Reply with ONLY the JSON object.`
656
848
  content: [userPrompt]
657
849
  }
658
850
  ],
659
- maxTokens: 1024,
660
851
  temperature: 0
661
852
  },
662
853
  { signal: context.signal }
@@ -1022,6 +1213,8 @@ import {
1022
1213
  } from "@wrongstack/core/agent";
1023
1214
  import { resetSessionSubagentPolicy } from "@wrongstack/core/coordination";
1024
1215
  import { TOKENS } from "@wrongstack/core/kernel";
1216
+ import { restoreSessionPermissionOverrides } from "@wrongstack/core/security";
1217
+ import { restoreRequiredSkillsFromEvents } from "@wrongstack/core/skills";
1025
1218
  import { DefaultSessionStore } from "@wrongstack/core/storage";
1026
1219
  import {
1027
1220
  activateProjectStateGuard,
@@ -1056,7 +1249,8 @@ async function switchProjectInPlace(ctx, targetRoot, displayName) {
1056
1249
  await fs2.mkdir(nextWpaths.projectSessions, { recursive: true });
1057
1250
  const nextSessionStore = new DefaultSessionStore({
1058
1251
  dir: nextWpaths.projectSessions,
1059
- projectRoot: resolved
1252
+ projectRoot: resolved,
1253
+ events
1060
1254
  });
1061
1255
  const nextWriter = await nextSessionStore.create({
1062
1256
  id: "",
@@ -1130,8 +1324,11 @@ async function switchProjectInPlace(ctx, targetRoot, displayName) {
1130
1324
  context.projectRoot = resolved;
1131
1325
  context.workingDir = resolved;
1132
1326
  context.session = nextWriter;
1327
+ if (state.sessionRef) state.sessionRef.current = nextWriter;
1133
1328
  context.state.replaceMessages([]);
1134
1329
  resetSessionSubagentPolicy(context);
1330
+ restoreSessionPermissionOverrides(context.meta, {});
1331
+ restoreRequiredSkillsFromEvents(context, []);
1135
1332
  context.state.replaceTodos([]);
1136
1333
  context.clearFileTracking();
1137
1334
  context.clearMemoryEvidence?.();
@@ -1266,574 +1463,132 @@ async function onSDDOutput(output) {
1266
1463
  return messages;
1267
1464
  }
1268
1465
 
1269
- // src/boot/tui-session-resume.ts
1466
+ // src/boot/tui-session-stub-enrich.ts
1467
+ import * as fs3 from "node:fs/promises";
1270
1468
  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");
1469
+ var SLICE_BYTES = 256 * 1024;
1470
+ var MAX_TITLE_CHARS = 80;
1471
+ var PICKER_POOL_CEILING = 1500;
1472
+ var ENRICH_CONCURRENCY = 16;
1473
+ function isStubSummary(s) {
1474
+ return !s.title && !s.endedAt;
1287
1475
  }
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;
1476
+ function containedJsonlPath(sessionsDir, id) {
1477
+ const root = path7.resolve(sessionsDir);
1478
+ const file = path7.resolve(root, `${id}.jsonl`);
1479
+ if (file !== root && !file.startsWith(root + path7.sep)) return void 0;
1480
+ return file;
1481
+ }
1482
+ function stripControlChars(text) {
1483
+ return text.replaceAll(
1484
+ /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|[\u0000-\u001f\u007f-\u009f]/g,
1485
+ ""
1486
+ );
1487
+ }
1488
+ function truncate(text) {
1489
+ const oneLine = stripControlChars(text).replaceAll(/\s+/g, " ").trim();
1490
+ return oneLine.length > MAX_TITLE_CHARS ? `${oneLine.slice(0, MAX_TITLE_CHARS - 1)}\u2026` : oneLine;
1491
+ }
1492
+ function contentToText(content) {
1493
+ if (typeof content === "string") return content;
1494
+ if (Array.isArray(content)) {
1495
+ return content.map(
1496
+ (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
1497
+ ).filter(Boolean).join(" ");
1294
1498
  }
1295
- return void 0;
1499
+ return "";
1296
1500
  }
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;
1501
+ function scanUserInputs(chunk) {
1502
+ let first;
1503
+ let last;
1504
+ for (const line of chunk.split("\n")) {
1505
+ const trimmed = line.trim();
1506
+ if (!trimmed.includes('"user_input"')) continue;
1313
1507
  try {
1314
- ctx.onStage?.(next);
1508
+ const event = JSON.parse(trimmed);
1509
+ if (event.type !== "user_input" || typeof event.ts !== "string") continue;
1510
+ const text = contentToText(event.content).trim();
1511
+ if (!text) continue;
1512
+ const hit = { ts: event.ts, text };
1513
+ if (!first) first = hit;
1514
+ last = hit;
1315
1515
  } catch {
1316
1516
  }
1317
- };
1318
- if (!state.activeSessionStore) {
1319
- return fail("no_session_store", "No session store is bound to this project.");
1320
1517
  }
1321
- setStage("load_replay_module");
1322
- let replaySessionMessages;
1518
+ return { first, last };
1519
+ }
1520
+ async function firstUserInput(file) {
1521
+ let handle;
1323
1522
  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));
1523
+ handle = await fs3.open(file, "r");
1524
+ const { size } = await handle.stat();
1525
+ const length = Math.min(SLICE_BYTES, size);
1526
+ const buffer = Buffer.alloc(length);
1527
+ await handle.read(buffer, 0, length, 0);
1528
+ const text = buffer.toString("utf8");
1529
+ const cutoff = text.lastIndexOf("\n");
1530
+ return scanUserInputs(cutoff > 0 ? text.slice(0, cutoff) : text).first;
1531
+ } catch {
1532
+ return void 0;
1533
+ } finally {
1534
+ await handle?.close().catch(() => void 0);
1330
1535
  }
1331
- setStage("resolve_id");
1332
- let canonicalSessionId = sessionId;
1536
+ }
1537
+ async function lastUserInput(file) {
1538
+ let handle;
1333
1539
  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));
1540
+ handle = await fs3.open(file, "r");
1541
+ const { size } = await handle.stat();
1542
+ const start = Math.max(0, size - SLICE_BYTES);
1543
+ const length = size - start;
1544
+ const buffer = Buffer.alloc(length);
1545
+ await handle.read(buffer, 0, length, start);
1546
+ const text = buffer.toString("utf8");
1547
+ const from = text.indexOf("\n");
1548
+ return scanUserInputs(from >= 0 ? text.slice(from + 1) : text).last;
1549
+ } catch {
1550
+ return void 0;
1551
+ } finally {
1552
+ await handle?.close().catch(() => void 0);
1348
1553
  }
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);
1554
+ }
1555
+ function effectiveLastActivity(s) {
1556
+ return s.lastActivityAt ?? s.endedAt ?? s.startedAt ?? "";
1557
+ }
1558
+ function hasActivity(s) {
1559
+ 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());
1560
+ }
1561
+ async function selectPickerSessions(storeList, sessionsDir, limit) {
1562
+ const poolSize = Math.max(limit, Math.min(PICKER_POOL_CEILING, limit * 3));
1563
+ const enriched = await enrichStubSummaries(await storeList(poolSize), sessionsDir);
1564
+ return enriched.filter(hasActivity).sort((a, b) => effectiveLastActivity(b).localeCompare(effectiveLastActivity(a))).slice(0, limit);
1565
+ }
1566
+ async function enrichStubSummaries(summaries, sessionsDir) {
1567
+ const out = [...summaries];
1568
+ const pending = summaries.reduce((indexes, summary, index) => {
1569
+ if (isStubSummary(summary)) indexes.push(index);
1570
+ return indexes;
1571
+ }, []);
1572
+ if (pending.length === 0) return out;
1573
+ let cursor = 0;
1574
+ const worker = async () => {
1575
+ for (; ; ) {
1576
+ const slot = cursor++;
1577
+ if (slot >= pending.length) return;
1578
+ const index = pending[slot];
1579
+ const summary = out[index];
1580
+ const file = containedJsonlPath(sessionsDir, summary.id);
1581
+ if (!file) continue;
1502
1582
  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
- };
1583
+ const stat3 = await fs3.stat(file);
1584
+ const [first, last] = await Promise.all([firstUserInput(file), lastUserInput(file)]);
1585
+ const title = truncate(first?.text ?? "");
1586
+ out[index] = {
1587
+ ...summary,
1588
+ ...title ? { title } : {},
1589
+ ...last ? { lastUserMessage: truncate(last.text) } : {},
1590
+ lastActivityAt: stat3.mtime.toISOString()
1591
+ };
1837
1592
  } catch {
1838
1593
  }
1839
1594
  }
@@ -1919,6 +1674,7 @@ function createSettingsAdapter(ctx) {
1919
1674
  featureMemory: cfg.features?.memory !== false,
1920
1675
  featureSkills: cfg.features?.skills !== false,
1921
1676
  featureModelsRegistry: cfg.features?.modelsRegistry !== false,
1677
+ featureToolCoach: cfg.features?.toolCoach !== false,
1922
1678
  // Preserve the 'auto' sentinel for the picker DISPLAY (normalize would
1923
1679
  // collapse it to 'off', which would then overwrite 'auto' on save);
1924
1680
  // everything else normalizes to a concrete tier.
@@ -1989,7 +1745,7 @@ function createSettingsAdapter(ctx) {
1989
1745
  }
1990
1746
  async function saveSettings(s) {
1991
1747
  try {
1992
- if (s.mode !== void 0 || s.delayMs !== void 0 || s.titleAnimation !== void 0 || s.yolo !== void 0 || s.fleetChatVerbosity !== void 0 || s.chime !== void 0 || s.confirmExit !== void 0 || s.mouseMode !== void 0 || s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0 || s.allowOutsideProjectRoot !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.contextMode !== void 0 || s.maxConcurrent !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.multiDiffSummaryThreshold !== void 0 || s.nextStepsTool !== void 0 || s.restrictFsToRoot !== void 0 || s.nextPrediction !== void 0 || s.debugStream !== void 0 || s.shellBangWarningDontShowAgain !== void 0 || s.configScope !== void 0 || s.enhanceDelayMs !== void 0 || s.enhanceEnabled !== void 0 || s.preRefineSeconds !== void 0 || s.enhanceLanguage !== void 0 || s.midRunSendPicker !== void 0 || s.statuslineMode !== void 0 || s.thinkingWord !== void 0 || s.animationStyle !== void 0 || s.autonomyNextPrompt !== void 0 || s.autoProceedMaxIterations !== void 0 || s.reasoningMode !== void 0 || s.reasoningEffort !== void 0 || s.reasoningPreserve !== void 0 || s.cacheTtl !== void 0 || s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0 || s.showModelReasoning !== void 0 || s.toolResultViewMode !== void 0 || s.showAgentSwarmPanel !== void 0 || s.showSidebar !== void 0 || s.panelPositions !== void 0 || s.lastSettingsField !== void 0 || s.showSageMemoryInject !== void 0 || s.sageMemoryInjectThreshold !== void 0 || s.readSymbols !== void 0 || // WrongProxy / WrongTrace: gate the persisted-section write on
1748
+ if (s.mode !== void 0 || s.delayMs !== void 0 || s.titleAnimation !== void 0 || s.yolo !== void 0 || s.fleetChatVerbosity !== void 0 || s.chime !== void 0 || s.confirmExit !== void 0 || s.mouseMode !== void 0 || s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureToolCoach !== void 0 || s.featureTokenSaving !== void 0 || s.allowOutsideProjectRoot !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.contextMode !== void 0 || s.maxConcurrent !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.multiDiffSummaryThreshold !== void 0 || s.nextStepsTool !== void 0 || s.restrictFsToRoot !== void 0 || s.nextPrediction !== void 0 || s.debugStream !== void 0 || s.shellBangWarningDontShowAgain !== void 0 || s.configScope !== void 0 || s.enhanceDelayMs !== void 0 || s.enhanceEnabled !== void 0 || s.preRefineSeconds !== void 0 || s.enhanceLanguage !== void 0 || s.midRunSendPicker !== void 0 || s.statuslineMode !== void 0 || s.thinkingWord !== void 0 || s.animationStyle !== void 0 || s.autonomyNextPrompt !== void 0 || s.autoProceedMaxIterations !== void 0 || s.reasoningMode !== void 0 || s.reasoningEffort !== void 0 || s.reasoningPreserve !== void 0 || s.cacheTtl !== void 0 || s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0 || s.showModelReasoning !== void 0 || s.toolResultViewMode !== void 0 || s.showAgentSwarmPanel !== void 0 || s.showSidebar !== void 0 || s.panelPositions !== void 0 || s.lastSettingsField !== void 0 || s.showSageMemoryInject !== void 0 || s.sageMemoryInjectThreshold !== void 0 || s.readSymbols !== void 0 || // WrongProxy / WrongTrace: gate the persisted-section write on
1993
1749
  // either key being present in the live patch. Without this, a
1994
1750
  // picker toggle round-trip would silently no-op the persistence
1995
1751
  // layer (the runtime probe would never read the change).
@@ -2052,7 +1808,7 @@ function createSettingsAdapter(ctx) {
2052
1808
  if (s.nextPrediction !== void 0) decrypted.nextPrediction = s.nextPrediction;
2053
1809
  if (s.yolo !== void 0) decrypted.yolo = s.yolo;
2054
1810
  fsAccess = deriveFsAccess(s);
2055
- if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0 || fsAccess !== void 0) {
1811
+ if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureToolCoach !== void 0 || s.featureTokenSaving !== void 0 || fsAccess !== void 0) {
2056
1812
  const feats = decrypted.features ?? {};
2057
1813
  if (s.featureMcp !== void 0) feats.mcp = s.featureMcp;
2058
1814
  if (s.featurePlugins !== void 0) feats.plugins = s.featurePlugins;
@@ -2060,6 +1816,7 @@ function createSettingsAdapter(ctx) {
2060
1816
  if (s.featureSkills !== void 0) feats.skills = s.featureSkills;
2061
1817
  if (s.featureModelsRegistry !== void 0)
2062
1818
  feats.modelsRegistry = s.featureModelsRegistry;
1819
+ if (s.featureToolCoach !== void 0) feats.toolCoach = s.featureToolCoach;
2063
1820
  if (s.featureTokenSaving !== void 0) feats.tokenSavingMode = s.featureTokenSaving;
2064
1821
  if (fsAccess !== void 0)
2065
1822
  feats.allowOutsideProjectRoot = fsAccess.allowOutsideProjectRoot;
@@ -2141,1171 +1898,1689 @@ function createSettingsAdapter(ctx) {
2141
1898
  sageSec.inject = inject;
2142
1899
  decrypted.Sage = sageSec;
2143
1900
  }
2144
- if (s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0) {
2145
- const cb = decrypted.circuitBreaker ?? {};
2146
- if (s.breakerEnabled !== void 0) cb.enabled = s.breakerEnabled;
2147
- if (s.breakerAutoKillResetMs !== void 0)
2148
- cb.autoKillResetMs = s.breakerAutoKillResetMs;
2149
- decrypted.circuitBreaker = cb;
1901
+ if (s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0) {
1902
+ const cb = decrypted.circuitBreaker ?? {};
1903
+ if (s.breakerEnabled !== void 0) cb.enabled = s.breakerEnabled;
1904
+ if (s.breakerAutoKillResetMs !== void 0)
1905
+ cb.autoKillResetMs = s.breakerAutoKillResetMs;
1906
+ decrypted.circuitBreaker = cb;
1907
+ }
1908
+ });
1909
+ const effectiveScope = s.configScope ?? cfg.configScope;
1910
+ const hasProfileOnlySettings = s.mode !== void 0 || s.yolo !== void 0 || fsAccess !== void 0 || s.maxIterations !== void 0 || s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0;
1911
+ if (effectiveScope === "project" && hasProfileOnlySettings) {
1912
+ await persistConfigSetting(
1913
+ { ...persistDeps, forceGlobal: true, updateStore: false },
1914
+ (profileConfig) => {
1915
+ if (s.mode !== void 0 || s.yolo !== void 0) {
1916
+ const autonomy = profileConfig.autonomy ?? {};
1917
+ if (s.mode !== void 0) autonomy.defaultMode = s.mode;
1918
+ if (s.yolo !== void 0) autonomy.yolo = s.yolo;
1919
+ profileConfig.autonomy = autonomy;
1920
+ }
1921
+ if (s.yolo !== void 0) profileConfig.yolo = s.yolo;
1922
+ if (fsAccess !== void 0) {
1923
+ const features = profileConfig.features ?? {};
1924
+ features.allowOutsideProjectRoot = fsAccess.allowOutsideProjectRoot;
1925
+ profileConfig.features = features;
1926
+ }
1927
+ if (fsAccess !== void 0 || s.maxIterations !== void 0 || s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0) {
1928
+ const tools = profileConfig.tools ?? {};
1929
+ if (fsAccess !== void 0)
1930
+ tools.restrictToProjectRoot = fsAccess.restrictToProjectRoot;
1931
+ if (s.maxIterations !== void 0) tools.maxIterations = s.maxIterations;
1932
+ if (s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0) {
1933
+ const wrongProxy = tools.wrongProxy ?? {};
1934
+ if (s.wrongProxyEnabled !== void 0) wrongProxy.enabled = s.wrongProxyEnabled;
1935
+ if (s.wrongProxyUrl !== void 0) wrongProxy.url = s.wrongProxyUrl;
1936
+ tools.wrongProxy = wrongProxy;
1937
+ }
1938
+ profileConfig.tools = tools;
1939
+ }
1940
+ }
1941
+ );
1942
+ }
1943
+ if (s.debugStream !== void 0) {
1944
+ const { setDebugStreamEnabled } = await import("@wrongstack/providers");
1945
+ setDebugStreamEnabled(s.debugStream);
1946
+ }
1947
+ const currentConfig = configStore.get();
1948
+ const nextModelRuntime = {
1949
+ ...currentConfig.modelRuntime,
1950
+ ...decrypted.modelRuntime ?? {}
1951
+ };
1952
+ if (s.cacheTtl === "default") {
1953
+ delete nextModelRuntime.cache;
1954
+ }
1955
+ configStore.update({
1956
+ ...s.nextPrediction !== void 0 ? { nextPrediction: s.nextPrediction } : {},
1957
+ ...s.yolo !== void 0 ? { yolo: s.yolo } : {},
1958
+ ...s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureToolCoach !== void 0 || s.featureTokenSaving !== void 0 || fsAccess !== void 0 ? {
1959
+ features: {
1960
+ ...currentConfig.features,
1961
+ ...decrypted.features ?? {}
1962
+ }
1963
+ } : {},
1964
+ ...s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.contextMode !== void 0 ? {
1965
+ context: {
1966
+ ...currentConfig.context,
1967
+ ...decrypted.context ?? {}
1968
+ }
1969
+ } : {},
1970
+ ...s.maxConcurrent !== void 0 ? { maxConcurrent: s.maxConcurrent } : {},
1971
+ ...s.logLevel !== void 0 ? {
1972
+ log: {
1973
+ ...currentConfig.log,
1974
+ ...decrypted.log ?? {}
1975
+ }
1976
+ } : {},
1977
+ ...s.auditLevel !== void 0 ? {
1978
+ session: {
1979
+ ...currentConfig.session,
1980
+ ...decrypted.session ?? {}
1981
+ }
1982
+ } : {},
1983
+ ...s.indexOnStart !== void 0 ? {
1984
+ indexing: {
1985
+ ...currentConfig.indexing,
1986
+ ...decrypted.indexing ?? {}
1987
+ }
1988
+ } : {},
1989
+ ...s.maxIterations !== void 0 || s.multiDiffSummaryThreshold !== void 0 || s.nextStepsTool !== void 0 || fsAccess !== void 0 || // WrongProxy / WrongTrace: must be in the tools guard or the
1990
+ // in-memory ConfigStore never sees the freshly-saved values,
1991
+ // and the next picker open in the same process would overwrite
1992
+ // the on-disk selection with stale state. See Chimera review.
1993
+ s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0 ? {
1994
+ tools: {
1995
+ ...currentConfig.tools,
1996
+ ...decrypted.tools ?? {}
1997
+ }
1998
+ } : {},
1999
+ ...s.debugStream !== void 0 ? { debugStream: s.debugStream } : {},
2000
+ ...s.configScope !== void 0 ? { configScope: s.configScope } : {},
2001
+ autonomy: {
2002
+ ...currentConfig.autonomy,
2003
+ ...decrypted.autonomy ?? {}
2004
+ },
2005
+ ...s.reasoningMode !== void 0 || s.reasoningEffort !== void 0 || s.reasoningPreserve !== void 0 || s.cacheTtl !== void 0 ? {
2006
+ modelRuntime: nextModelRuntime
2007
+ } : {},
2008
+ ...s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0 ? {
2009
+ circuitBreaker: {
2010
+ ...currentConfig.circuitBreaker,
2011
+ ...decrypted.circuitBreaker ?? {}
2012
+ }
2013
+ } : {},
2014
+ ...s.sageMemoryInjectThreshold !== void 0 ? {
2015
+ Sage: {
2016
+ ...currentConfig.Sage,
2017
+ ...decrypted.Sage ?? {},
2018
+ inject: {
2019
+ ...currentConfig.Sage?.inject ?? {},
2020
+ ...decrypted.Sage?.inject ?? {}
2021
+ }
2022
+ }
2023
+ } : {}
2024
+ });
2025
+ }
2026
+ if (s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0) {
2027
+ getProcessRegistry().setBreakerConfig({
2028
+ ...s.breakerEnabled !== void 0 ? { enabled: s.breakerEnabled } : {},
2029
+ ...s.breakerAutoKillResetMs !== void 0 ? { autoKillResetMs: s.breakerAutoKillResetMs } : {}
2030
+ });
2031
+ }
2032
+ if (s.fleetChatVerbosity !== void 0) {
2033
+ if (fleetStreamController?.setMode) fleetStreamController.setMode(s.fleetChatVerbosity);
2034
+ }
2035
+ applyLiveSettings?.(s);
2036
+ return null;
2037
+ } catch (err) {
2038
+ const message = err instanceof Error ? err.message : String(err);
2039
+ console.debug(
2040
+ JSON.stringify({
2041
+ level: "error",
2042
+ event: "execution.settings_persist_failed",
2043
+ message,
2044
+ errorName: err instanceof Error ? err.name : void 0,
2045
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2046
+ })
2047
+ );
2048
+ return message;
2049
+ }
2050
+ }
2051
+ return { getSettings, saveSettings };
2052
+ }
2053
+
2054
+ // src/boot/tui-theme-adapter.ts
2055
+ import * as fs4 from "node:fs/promises";
2056
+ import * as path8 from "node:path";
2057
+ import { THEME_PRESET_IDS } from "@wrongstack/core/types";
2058
+ import { updateJsonObjectFile } from "@wrongstack/core/utils";
2059
+ var VALID_PRESETS = new Set(THEME_PRESET_IDS);
2060
+ function isThemePresetId(candidate) {
2061
+ return VALID_PRESETS.has(candidate);
2062
+ }
2063
+ function createThemeAdapter({ configStore, wpaths }) {
2064
+ return {
2065
+ getThemePreset: () => {
2066
+ const live = configStore.get();
2067
+ const candidate = live.themePreset;
2068
+ return candidate && isThemePresetId(candidate) ? candidate : void 0;
2069
+ },
2070
+ saveThemePreset: async (preset) => {
2071
+ if (!isThemePresetId(preset)) {
2072
+ throw new Error(`Unknown theme preset: ${preset}`);
2073
+ }
2074
+ const configPath = activeProfileConfigPath(wpaths, configStore.get());
2075
+ await fs4.mkdir(path8.dirname(configPath), { recursive: true });
2076
+ await updateJsonObjectFile(configPath, async (config) => {
2077
+ await backupCurrent(void 0, configPath);
2078
+ config.themePreset = preset;
2079
+ });
2080
+ configStore.update({ themePreset: preset });
2081
+ }
2082
+ };
2083
+ }
2084
+
2085
+ // src/boot/worktree-sessions.ts
2086
+ import * as path9 from "node:path";
2087
+ function samePath(a, b) {
2088
+ const ra = path9.resolve(a);
2089
+ const rb = path9.resolve(b);
2090
+ return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
2091
+ }
2092
+ function worktreeOfSession(summary, worktrees) {
2093
+ const checkout = summary.checkout;
2094
+ if (!checkout) return void 0;
2095
+ const wt = worktrees.find((w) => samePath(w.root, checkout));
2096
+ if (!wt || wt.current) return void 0;
2097
+ return {
2098
+ root: wt.root,
2099
+ name: path9.basename(wt.root) || wt.root,
2100
+ ...wt.branch ? { branch: wt.branch } : {}
2101
+ };
2102
+ }
2103
+ function withWorktreeSwitch(resume, lookup, switchProject) {
2104
+ return async (sessionId, ...rest) => {
2105
+ const target = lookup(sessionId);
2106
+ if (target) {
2107
+ const error = await switchProject(target.root, target.name);
2108
+ if (error) throw new Error(`Could not switch to worktree ${target.name}: ${error}`);
2109
+ }
2110
+ return resume(sessionId, ...rest);
2111
+ };
2112
+ }
2113
+
2114
+ // src/brain-menu/panel-service.ts
2115
+ import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
2116
+ var PERSONA_CATALOG = BUILTIN_COUNCIL_PERSONAS.map((persona) => ({
2117
+ id: persona.id,
2118
+ name: persona.name,
2119
+ description: persona.description,
2120
+ defaultVeto: persona.defaultVeto
2121
+ }));
2122
+ var PERSONA_CYCLE = PERSONA_CATALOG.map((persona) => persona.id);
2123
+ function compactEntry(entry) {
2124
+ return entry.provider ? `${entry.provider}/${entry.model}` : entry.model;
2125
+ }
2126
+ function createBrainPanelHost(deps) {
2127
+ const { brainRuntime } = deps;
2128
+ const apply = async (patch) => {
2129
+ try {
2130
+ const { persisted } = brainRuntime.apply(patch);
2131
+ const result = await persisted;
2132
+ return result.ok ? null : `Applied live but NOT saved: ${result.error ?? "unknown error"}`;
2133
+ } catch (err) {
2134
+ return err instanceof Error ? err.message : String(err);
2135
+ }
2136
+ };
2137
+ const currentPool = () => brainRuntime.getSnapshot().models.map(compactEntry);
2138
+ const currentVoters = () => brainRuntime.getSnapshot().council.voters.map((v) => ({ ...v }));
2139
+ const applyVoters = (voters) => apply({ council: { voters: voters.length > 0 ? voters : null } });
2140
+ return {
2141
+ getSettings() {
2142
+ const snap = brainRuntime.getSnapshot();
2143
+ return {
2144
+ mode: snap.mode,
2145
+ riskLevel: snap.maxAutoRisk,
2146
+ strategy: snap.strategy,
2147
+ decisionTimeoutMs: snap.decisionTimeoutMs,
2148
+ humanTimeoutMs: snap.humanTimeoutMs,
2149
+ pool: snap.models.map(compactEntry),
2150
+ poolResolved: snap.poolLabels,
2151
+ usingSessionModel: snap.usingSessionModel,
2152
+ councilEnabled: snap.council.enabled,
2153
+ councilMinRisk: snap.council.minRisk,
2154
+ voters: snap.council.voters.map((v) => ({
2155
+ label: compactEntry(v),
2156
+ persona: v.persona,
2157
+ veto: v.veto,
2158
+ weight: v.weight
2159
+ })),
2160
+ councilSeats: snap.councilLabels,
2161
+ personaCatalog: PERSONA_CATALOG.map((persona) => ({ ...persona })),
2162
+ councilQuorum: snap.council.quorum,
2163
+ councilApproval: snap.council.approval,
2164
+ councilDistinctness: snap.council.distinctness,
2165
+ councilPerCallTimeoutMs: snap.council.perCallTimeoutMs,
2166
+ councilMaxConcurrency: snap.council.maxConcurrency,
2167
+ councilVoterMaxTokens: snap.council.voterMaxTokens,
2168
+ councilDeliberationRounds: snap.council.deliberationRounds,
2169
+ councilJudgeMaxTokens: snap.council.judgeMaxTokens,
2170
+ // The EFFECTIVE judge, not `council.judge`. The configured one is
2171
+ // usually absent, so the panel used to read "auto" and say nothing
2172
+ // about who actually breaks the panel's ties — including when that is
2173
+ // one of the seats that produced the tie.
2174
+ judgeLabel: snap.judgeLabel,
2175
+ judgeConfigured: snap.council.judge !== void 0,
2176
+ judgeIsVoter: snap.judgeIsVoter,
2177
+ ledgerEnabled: snap.ledger.enabled,
2178
+ autoDenyAfterFailures: snap.ledger.autoDenyAfterFailures,
2179
+ terminalPolicy: snap.terminalPolicy,
2180
+ heuristics: {
2181
+ lowRiskAutoAnswer: snap.heuristics.lowRiskAutoAnswer,
2182
+ blockedResolved: snap.heuristics.blockedResolved,
2183
+ deadlockSkip: snap.heuristics.deadlockSkip,
2184
+ retryExhausted: snap.heuristics.retryExhausted,
2185
+ continuePing: snap.heuristics.continuePing,
2186
+ blockedResolvedMarkers: snap.heuristics.blockedResolvedMarkers
2187
+ },
2188
+ llmMaxTokens: snap.llm.maxTokens,
2189
+ llmRejectUncertain: snap.llm.rejectUncertain,
2190
+ llmMinConfidence: snap.llm.minConfidence,
2191
+ llmDenyIsTerminal: snap.llm.denyIsTerminal,
2192
+ cacheEnabled: snap.cache.enabled,
2193
+ cacheTtlMs: snap.cache.ttlMs,
2194
+ cacheMaxEntries: snap.cache.maxEntries,
2195
+ cacheHits: snap.cache.hits,
2196
+ cacheMisses: snap.cache.misses,
2197
+ cacheSize: snap.cache.size,
2198
+ traceEnabled: snap.trace.enabled,
2199
+ traceContent: snap.trace.content,
2200
+ tracePath: snap.trace.path,
2201
+ circuitState: snap.circuit?.state,
2202
+ circuitFailures: snap.circuit?.consecutiveFailures,
2203
+ ruleCount: snap.rules.length,
2204
+ ruleErrors: [...snap.ruleErrors]
2205
+ };
2206
+ },
2207
+ setMode: (mode) => apply({ mode }),
2208
+ setRisk: (level) => apply({ maxAutoRisk: level }),
2209
+ setStrategy: (strategy) => apply({ strategy }),
2210
+ setDecisionTimeout: (ms) => apply({ decisionTimeoutMs: ms ?? null }),
2211
+ setHumanTimeout: (ms) => apply({ humanTimeoutMs: ms ?? null }),
2212
+ addPoolModel: (providerId, model) => apply({ models: [...currentPool(), `${providerId}/${model}`] }),
2213
+ removePoolModel: (index) => {
2214
+ const next = currentPool().filter((_, i) => i !== index);
2215
+ return apply({ models: next.length > 0 ? next : null });
2216
+ },
2217
+ clearPool: () => apply({ models: null }),
2218
+ setCouncilEnabled: (on) => apply({ council: { enabled: on } }),
2219
+ setCouncilMinRisk: (risk) => apply({ council: { minRisk: risk } }),
2220
+ addVoter: (providerId, model) => {
2221
+ const voters = currentVoters();
2222
+ const persona = PERSONA_CYCLE[voters.length % PERSONA_CYCLE.length];
2223
+ return applyVoters([...voters, { provider: providerId, model, persona }]);
2224
+ },
2225
+ removeVoter: (index) => applyVoters(currentVoters().filter((_, i) => i !== index)),
2226
+ cycleVoterPersona: (index) => {
2227
+ const voters = currentVoters();
2228
+ const voter = voters[index];
2229
+ if (!voter) return Promise.resolve("No such voter.");
2230
+ const at = PERSONA_CYCLE.indexOf(voter.persona ?? "");
2231
+ voter.persona = PERSONA_CYCLE[(at + 1) % PERSONA_CYCLE.length];
2232
+ return applyVoters(voters);
2233
+ },
2234
+ toggleVoterVeto: (index) => {
2235
+ const voters = currentVoters();
2236
+ const voter = voters[index];
2237
+ if (!voter) return Promise.resolve("No such voter.");
2238
+ voter.veto = !voter.veto;
2239
+ return applyVoters(voters);
2240
+ },
2241
+ setJudge: (providerId, model) => apply({ council: { judge: `${providerId}/${model}` } }),
2242
+ clearJudge: () => apply({ council: { judge: null } }),
2243
+ // Resolution + budget knobs. `BrainCouncilPatch` accepts null for the
2244
+ // three integer fields, so `undefined` here really does clear to default.
2245
+ setCouncilQuorum: (fraction) => apply({ council: { quorum: fraction } }),
2246
+ setCouncilApproval: (fraction) => apply({ council: { approval: fraction } }),
2247
+ setCouncilDistinctness: (mode) => apply({ council: { distinctness: mode } }),
2248
+ setCouncilPerCallTimeout: (ms) => apply({ council: { perCallTimeoutMs: ms ?? null } }),
2249
+ setCouncilMaxConcurrency: (count) => apply({ council: { maxConcurrency: count ?? null } }),
2250
+ setCouncilVoterMaxTokens: (tokens) => apply({ council: { voterMaxTokens: tokens ?? null } }),
2251
+ setCouncilDeliberationRounds: (rounds) => apply({ council: { deliberationRounds: rounds ?? null } }),
2252
+ setCouncilJudgeMaxTokens: (tokens) => apply({ council: { judgeMaxTokens: tokens ?? null } }),
2253
+ setLedgerEnabled: (on) => apply({ ledger: { enabled: on } }),
2254
+ setAutoDeny: (count) => apply({ ledger: { autoDenyAfterFailures: count ?? null } }),
2255
+ setTerminalPolicy: (policy) => apply({ terminalPolicy: policy }),
2256
+ // Every sub-block below is MERGED field-by-field by `mergePatch`, so a
2257
+ // single-key patch leaves the operator's other knobs alone.
2258
+ setHeuristic: (key, on) => apply({ heuristics: { [key]: on } }),
2259
+ setLlmMaxTokens: (tokens) => apply({ llm: { maxTokens: tokens ?? null } }),
2260
+ setLlmRejectUncertain: (on) => apply({ llm: { rejectUncertain: on } }),
2261
+ setLlmMinConfidence: (value) => apply({ llm: { minConfidence: value } }),
2262
+ setLlmDenyIsTerminal: (mode) => apply({ llm: { denyIsTerminal: mode } }),
2263
+ setCacheEnabled: (on) => apply({ cache: { enabled: on } }),
2264
+ setCacheTtl: (ms) => apply({ cache: { ttlMs: ms } }),
2265
+ setCacheMaxEntries: (count) => apply({ cache: { maxEntries: count } }),
2266
+ setTraceEnabled: (on) => apply({ trace: { enabled: on } }),
2267
+ setTraceContent: (content) => apply({ trace: { content } })
2268
+ };
2269
+ }
2270
+
2271
+ // src/chimera-reviewer-policy.ts
2272
+ import { parseModelRef } from "@wrongstack/core/agent";
2273
+ import {
2274
+ buildReviewerModelPool,
2275
+ selectRoundRobinReviewerAssignment
2276
+ } from "@wrongstack/core/plugin";
2277
+ function resolveReviewerFallbackModels(reviewFallbackModels, sessionRef) {
2278
+ const base = reviewFallbackModels && reviewFallbackModels.length > 0 ? [...reviewFallbackModels] : [];
2279
+ if (sessionRef && !base.includes(sessionRef)) {
2280
+ base.push(sessionRef);
2281
+ }
2282
+ return base;
2283
+ }
2284
+ var reviewerRoundRobinCursor = 0;
2285
+ function __resetReviewerRoundRobinCursor(value = 0) {
2286
+ reviewerRoundRobinCursor = value;
2287
+ }
2288
+ function assignReviewerModels(provider, model, fallbackModels, selection = "round-robin", statusTracker, random = Math.random) {
2289
+ const pool = buildReviewerModelPool(provider, model, fallbackModels, statusTracker);
2290
+ if (pool.length <= 1) {
2291
+ return {
2292
+ provider,
2293
+ model,
2294
+ fallbackModels: [...fallbackModels]
2295
+ };
2296
+ }
2297
+ const cursor = selection === "random" ? Math.min(pool.length - 1, Math.floor(Math.max(0, random()) * pool.length)) : reviewerRoundRobinCursor;
2298
+ const assignment = selectRoundRobinReviewerAssignment(
2299
+ pool,
2300
+ cursor,
2301
+ provider,
2302
+ model,
2303
+ statusTracker
2304
+ );
2305
+ if (selection === "round-robin") reviewerRoundRobinCursor = assignment.nextCursor;
2306
+ return {
2307
+ provider: assignment.provider,
2308
+ model: assignment.model,
2309
+ fallbackModels: assignment.fallbackModels
2310
+ };
2311
+ }
2312
+ function assignReviewerModelsRoundRobin(provider, model, fallbackModels, statusTracker) {
2313
+ return assignReviewerModels(provider, model, fallbackModels, "round-robin", statusTracker);
2314
+ }
2315
+ var MAX_REVIEWER_ATTEMPTS = 4;
2316
+ var REVIEWER_ATTEMPT_TIMEOUT_MS = 9e5;
2317
+ var REVIEWER_LADDER_BUDGET_MS = 12e5;
2318
+ var REVIEWER_MIN_ATTEMPT_MS = 12e4;
2319
+ function buildReviewerAttemptLadder(input) {
2320
+ const assignedChain = [...input.assigned.fallbackModels ?? []];
2321
+ const profileChain = [...input.profileChain ?? []];
2322
+ const rungs = [];
2323
+ const seen = /* @__PURE__ */ new Set();
2324
+ const key = (provider, model) => `${provider.trim()}/${model.trim()}`;
2325
+ const push = (provider, model, fallbackModels, tier) => {
2326
+ const p = provider.trim();
2327
+ const m = model.trim();
2328
+ if (!p || !m) return;
2329
+ if (seen.has(key(p, m))) return;
2330
+ seen.add(key(p, m));
2331
+ rungs.push({ provider: p, model: m, fallbackModels, tier });
2332
+ };
2333
+ push(input.assigned.provider, input.assigned.model, [...assignedChain], "primary");
2334
+ for (const [chain, tier] of [
2335
+ [assignedChain, "fallback"],
2336
+ [profileChain, "fallback"]
2337
+ ]) {
2338
+ for (let i = 0; i < chain.length; i++) {
2339
+ const parsed = parseModelRef(chain[i]);
2340
+ push(parsed.provider ?? "", parsed.model, chain.slice(i + 1), tier);
2341
+ }
2342
+ }
2343
+ const sessionKey = key(input.session.provider, input.session.model);
2344
+ push(input.session.provider, input.session.model, [], "session");
2345
+ const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_REVIEWER_ATTEMPTS));
2346
+ if (rungs.length <= cap) return rungs;
2347
+ const trimmed = rungs.slice(0, cap);
2348
+ if (!trimmed.some((r) => key(r.provider, r.model) === sessionKey)) {
2349
+ const sessionRung = rungs.find((r) => key(r.provider, r.model) === sessionKey);
2350
+ if (sessionRung) trimmed[trimmed.length - 1] = sessionRung;
2351
+ }
2352
+ return trimmed;
2353
+ }
2354
+ var MAX_MUTATING_ATTEMPTS = 2;
2355
+ function buildMutatingAgentLadder(input) {
2356
+ const cap = Math.max(1, Math.floor(input.maxAttempts ?? MAX_MUTATING_ATTEMPTS));
2357
+ const inherit = {
2358
+ provider: "",
2359
+ model: "",
2360
+ fallbackModels: [],
2361
+ tier: "inherit"
2362
+ };
2363
+ if (cap === 1) return [inherit];
2364
+ return [
2365
+ inherit,
2366
+ ...buildReviewerAttemptLadder({
2367
+ assigned: { provider: "", model: "" },
2368
+ profileChain: input.profileChain,
2369
+ session: input.session,
2370
+ maxAttempts: cap - 1
2371
+ })
2372
+ ];
2373
+ }
2374
+ function shouldAdvanceReviewerLadder(status, error) {
2375
+ if (status === "success") return false;
2376
+ if (status === "stopped") return false;
2377
+ if (error?.kind === "aborted_by_parent") return false;
2378
+ return true;
2379
+ }
2380
+ function reviewerAttemptTimeoutMs(remainingMs, attemptTimeoutMs = REVIEWER_ATTEMPT_TIMEOUT_MS, minAttemptMs = REVIEWER_MIN_ATTEMPT_MS) {
2381
+ if (remainingMs < minAttemptMs) return 0;
2382
+ return Math.min(attemptTimeoutMs, Math.floor(remainingMs));
2383
+ }
2384
+ var CHIMERA_REVIEW_READ_ONLY_TOOLS = Object.freeze([
2385
+ "read",
2386
+ "grep",
2387
+ "glob",
2388
+ "tree",
2389
+ "codebase-stats",
2390
+ "codebase-search",
2391
+ "codebase-skeleton",
2392
+ "codebase-context",
2393
+ "codebase-repo-map",
2394
+ "codebase-incoming-calls",
2395
+ "codebase-outgoing-calls"
2396
+ ]);
2397
+ function applyChimeraReviewerReadOnlyPolicy(config) {
2398
+ return {
2399
+ ...config,
2400
+ // A security boundary, not an additive default: discard any inherited
2401
+ // mutation tools/capabilities so Chimera reviewers cannot repair files.
2402
+ tools: [...CHIMERA_REVIEW_READ_ONLY_TOOLS],
2403
+ allowedCapabilities: ["fs.read"],
2404
+ worktree: "off",
2405
+ // Mandatory model-driven completion: a reviewer must never be killed by
2406
+ // the wall-clock watchdog. At its deadline (or when the leader's session
2407
+ // ends and `Director.requestFinish()` fires) it receives an in-band
2408
+ // `subagent.finish_requested` notification between tool batches and
2409
+ // finishes its report in its own turn within the granted grace window.
2410
+ // Only if that window also elapses does the terminal stop apply — the
2411
+ // reviewer's bounded maximum lifetime.
2412
+ gracefulFinish: true
2413
+ };
2414
+ }
2415
+
2416
+ // src/execution-cleanup.ts
2417
+ async function finalizeExecutionCleanup(input) {
2418
+ const {
2419
+ offStorageObservability,
2420
+ fleetStatusLine,
2421
+ onCoordinatorStop,
2422
+ stats,
2423
+ renderer,
2424
+ detachTodosCheckpoint,
2425
+ mcpRegistry,
2426
+ agent,
2427
+ session,
2428
+ tokenCounter,
2429
+ events,
2430
+ chimeraWork,
2431
+ director,
2432
+ reader
2433
+ } = input;
2434
+ offStorageObservability();
2435
+ fleetStatusLine?.stop();
2436
+ onCoordinatorStop?.();
2437
+ try {
2438
+ stats.render(renderer);
2439
+ } catch (_err) {
2440
+ }
2441
+ await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
2442
+ try {
2443
+ const { getProcessRegistry: getProcessRegistry2 } = await import("@wrongstack/tools");
2444
+ getProcessRegistry2().killAll({ force: true, includeProtected: true });
2445
+ } catch (err) {
2446
+ console.warn(
2447
+ JSON.stringify({
2448
+ level: "warn",
2449
+ event: "shutdown.process_kill_all_failed",
2450
+ message: `Process registry killAll failed: ${err instanceof Error ? err.message : String(err)}`,
2451
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2452
+ })
2453
+ );
2454
+ }
2455
+ await mcpRegistry.stopAll().catch((err) => {
2456
+ console.warn(
2457
+ JSON.stringify({
2458
+ level: "warn",
2459
+ event: "shutdown.mcp_stop_failed",
2460
+ message: `MCP registry stopAll failed: ${err instanceof Error ? err.message : String(err)}`,
2461
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2462
+ })
2463
+ );
2464
+ });
2465
+ const activeSession = agent.ctx.session ?? session;
2466
+ const pending = activeSession.pendingToolUses;
2467
+ await activeSession.append({
2468
+ type: "session_end",
2469
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2470
+ usage: tokenCounter.total(),
2471
+ pendingToolUses: pending.length > 0 ? pending : void 0
2472
+ }).catch((err) => {
2473
+ console.warn(
2474
+ JSON.stringify({
2475
+ level: "warn",
2476
+ event: "shutdown.session_end_append_failed",
2477
+ message: `session_end append failed: ${err instanceof Error ? err.message : String(err)}`,
2478
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2479
+ })
2480
+ );
2481
+ });
2482
+ const sessionEndProducers = /* @__PURE__ */ new Set();
2483
+ events.emit("session.ended", {
2484
+ id: activeSession.id,
2485
+ sessionId: activeSession.id,
2486
+ usage: tokenCounter.total(),
2487
+ waitUntil: (work) => {
2488
+ const tracked = Promise.resolve(work).finally(() => sessionEndProducers.delete(tracked));
2489
+ void tracked.catch(() => void 0);
2490
+ sessionEndProducers.add(tracked);
2491
+ }
2492
+ });
2493
+ try {
2494
+ const model = agent.ctx.model;
2495
+ if (!model) {
2496
+ console.warn(
2497
+ JSON.stringify({
2498
+ level: "warn",
2499
+ event: "shutdown.wrongtrace_telemetry_skipped",
2500
+ message: "Skipping WrongTrace telemetry: agent model unknown at session end",
2501
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2502
+ })
2503
+ );
2504
+ } else {
2505
+ const { reportWrongTraceSessionTelemetry } = await import("./wrongtrace-telemetry-3ASFKOZO.js");
2506
+ const usage = tokenCounter.total();
2507
+ const report = reportWrongTraceSessionTelemetry({
2508
+ sessionId: activeSession.id,
2509
+ agentName: "wrongstack-cli",
2510
+ model,
2511
+ provider: agent.ctx.provider?.id ?? "",
2512
+ usage: {
2513
+ input: usage.input,
2514
+ output: usage.output,
2515
+ cacheRead: usage.cacheRead,
2516
+ cacheWrite: usage.cacheWrite
2517
+ },
2518
+ costUsd: tokenCounter.estimateCost().total
2519
+ });
2520
+ const deadline = new Promise((resolve5) => {
2521
+ const t = setTimeout(resolve5, 5e3);
2522
+ t.unref?.();
2523
+ });
2524
+ const tracked = Promise.race([report, deadline]).finally(
2525
+ () => sessionEndProducers.delete(tracked)
2526
+ );
2527
+ void tracked.catch(() => void 0);
2528
+ sessionEndProducers.add(tracked);
2529
+ }
2530
+ } catch {
2531
+ }
2532
+ try {
2533
+ const { snapshotGateDecisions, persistWrongTraceGateCounters } = await import("./wrongtrace-gate-counters-7C2EAAZL.js");
2534
+ const projectRoot = agent.ctx.projectRoot;
2535
+ if (projectRoot) {
2536
+ await persistWrongTraceGateCounters(projectRoot, snapshotGateDecisions());
2537
+ }
2538
+ } catch {
2539
+ }
2540
+ if (director) {
2541
+ try {
2542
+ director.requestFinish("leader session ended \u2014 finish your review now");
2543
+ } catch (finishErr) {
2544
+ console.warn(
2545
+ JSON.stringify({
2546
+ level: "warn",
2547
+ event: "shutdown.subagent_finish_notify_failed",
2548
+ message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2549
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2550
+ })
2551
+ );
2552
+ }
2553
+ }
2554
+ try {
2555
+ while (sessionEndProducers.size > 0) {
2556
+ await Promise.allSettled([...sessionEndProducers]);
2557
+ }
2558
+ if (director) {
2559
+ try {
2560
+ director.requestFinish("leader session ended \u2014 finish your review now");
2561
+ } catch {
2562
+ }
2563
+ }
2564
+ await chimeraWork?.drainAndClose();
2565
+ } catch (err) {
2566
+ console.warn(
2567
+ JSON.stringify({
2568
+ level: "warn",
2569
+ event: "shutdown.chimera_work_failed",
2570
+ message: `Pending chimera work failed: ${err instanceof Error ? err.message : String(err)}`,
2571
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2572
+ })
2573
+ );
2574
+ }
2575
+ if (director) {
2576
+ try {
2577
+ director.requestFinish("leader session ended \u2014 finish your review now");
2578
+ } catch (finishErr) {
2579
+ console.warn(
2580
+ JSON.stringify({
2581
+ level: "warn",
2582
+ event: "shutdown.subagent_finish_notify_failed",
2583
+ message: finishErr instanceof Error ? finishErr.message : String(finishErr),
2584
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2585
+ })
2586
+ );
2587
+ }
2588
+ try {
2589
+ await director.terminateAll();
2590
+ } catch (termErr) {
2591
+ console.warn(
2592
+ JSON.stringify({
2593
+ level: "warn",
2594
+ event: "shutdown.director_terminate_all_failed",
2595
+ message: termErr instanceof Error ? termErr.message : String(termErr),
2596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2597
+ })
2598
+ );
2599
+ }
2600
+ }
2601
+ await activeSession.close().catch((err) => {
2602
+ console.warn(
2603
+ JSON.stringify({
2604
+ level: "warn",
2605
+ event: "shutdown.session_close_failed",
2606
+ message: `Session close failed: ${err instanceof Error ? err.message : String(err)}`,
2607
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2608
+ })
2609
+ );
2610
+ });
2611
+ await reader.close().catch((err) => {
2612
+ console.warn(
2613
+ JSON.stringify({
2614
+ level: "warn",
2615
+ event: "shutdown.reader_close_failed",
2616
+ message: `Input reader close failed: ${err instanceof Error ? err.message : String(err)}`,
2617
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2618
+ })
2619
+ );
2620
+ });
2621
+ }
2622
+
2623
+ // src/execution-kanban-dispatch.ts
2624
+ import { randomUUID } from "node:crypto";
2625
+ import { missingRequiredRuntimeTools } from "@wrongstack/core/agent-catalog";
2626
+ import { WIDE_SUBAGENT_CAPABILITIES } from "@wrongstack/core/security";
2627
+ import { stripFrontmatter } from "@wrongstack/core/skills";
2628
+
2629
+ // src/kanban-dispatch-route.ts
2630
+ import { fallbackProfileChain, parseModelRef as parseModelRef2 } from "@wrongstack/core/agent";
2631
+ import { resolveTier } from "@wrongstack/core/coordination";
2632
+ function resolveKanbanDispatchRoute(config, spawnOpts) {
2633
+ let provider = spawnOpts?.provider;
2634
+ let model = spawnOpts?.model;
2635
+ let fallbackModels = spawnOpts?.fallbackModels;
2636
+ let tier;
2637
+ if (spawnOpts?.fallbackProfile) {
2638
+ const chain = fallbackProfileChain(config, spawnOpts.fallbackProfile);
2639
+ const primary = chain[0] ? parseModelRef2(chain[0]) : void 0;
2640
+ if (primary?.model) {
2641
+ provider = primary.provider ?? config.provider;
2642
+ model = primary.model;
2643
+ fallbackModels = spawnOpts.fallbackModels ?? chain.slice(1);
2644
+ }
2645
+ } else if (spawnOpts?.tier || spawnOpts?.role) {
2646
+ const resolved = resolveTier(config, {
2647
+ ...spawnOpts.tier ? { tier: spawnOpts.tier } : {},
2648
+ ...spawnOpts.role ? { role: spawnOpts.role } : {}
2649
+ });
2650
+ if (resolved?.model) {
2651
+ tier = resolved.tier;
2652
+ provider = provider ?? resolved.provider ?? config.provider;
2653
+ model = model ?? resolved.model;
2654
+ fallbackModels = fallbackModels ?? resolved.fallbackModels?.slice() ?? void 0;
2655
+ }
2656
+ }
2657
+ return { provider, model, fallbackModels, ...tier ? { tier } : {} };
2658
+ }
2659
+
2660
+ // src/execution-kanban-dispatch.ts
2661
+ function createKanbanDispatchHandler({
2662
+ config,
2663
+ events,
2664
+ skillLoader,
2665
+ sddSubagentFactory
2666
+ }) {
2667
+ if (!sddSubagentFactory) return {};
2668
+ const onKanbanDispatch = async (description, spawnOpts) => {
2669
+ const subagentId = `kanban-${randomUUID().slice(0, 8)}`;
2670
+ const taskId = randomUUID();
2671
+ const name = spawnOpts?.name ?? "kanban-agent";
2672
+ const {
2673
+ provider: resolvedProvider,
2674
+ model: resolvedModel,
2675
+ fallbackModels: resolvedFallbackModels
2676
+ } = resolveKanbanDispatchRoute(config, spawnOpts);
2677
+ let agentDescription = description;
2678
+ if (spawnOpts?.skills?.length && skillLoader) {
2679
+ const availableToolNames = spawnOpts.tools;
2680
+ const loaded = [];
2681
+ for (const skillName of spawnOpts.skills) {
2682
+ try {
2683
+ const manifest = await skillLoader.find(skillName);
2684
+ if (!manifest) {
2685
+ process.emitWarning(`Kanban dispatch skill not found, skipped: ${skillName}`, {
2686
+ code: "WRONGSTACK_KANBAN_SKILL_SKIPPED"
2687
+ });
2688
+ continue;
2689
+ }
2690
+ if (availableToolNames !== void 0 && missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames).length > 0) {
2691
+ process.emitWarning(
2692
+ `Kanban dispatch skill "${skillName}" needs tools this worker does not have, skipped.`,
2693
+ { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2694
+ );
2695
+ continue;
2150
2696
  }
2151
- });
2152
- const effectiveScope = s.configScope ?? cfg.configScope;
2153
- const hasProfileOnlySettings = s.mode !== void 0 || s.yolo !== void 0 || fsAccess !== void 0 || s.maxIterations !== void 0 || s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0;
2154
- if (effectiveScope === "project" && hasProfileOnlySettings) {
2155
- await persistConfigSetting(
2156
- { ...persistDeps, forceGlobal: true, updateStore: false },
2157
- (profileConfig) => {
2158
- if (s.mode !== void 0 || s.yolo !== void 0) {
2159
- const autonomy = profileConfig.autonomy ?? {};
2160
- if (s.mode !== void 0) autonomy.defaultMode = s.mode;
2161
- if (s.yolo !== void 0) autonomy.yolo = s.yolo;
2162
- profileConfig.autonomy = autonomy;
2163
- }
2164
- if (s.yolo !== void 0) profileConfig.yolo = s.yolo;
2165
- if (fsAccess !== void 0) {
2166
- const features = profileConfig.features ?? {};
2167
- features.allowOutsideProjectRoot = fsAccess.allowOutsideProjectRoot;
2168
- profileConfig.features = features;
2169
- }
2170
- if (fsAccess !== void 0 || s.maxIterations !== void 0 || s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0) {
2171
- const tools = profileConfig.tools ?? {};
2172
- if (fsAccess !== void 0)
2173
- tools.restrictToProjectRoot = fsAccess.restrictToProjectRoot;
2174
- if (s.maxIterations !== void 0) tools.maxIterations = s.maxIterations;
2175
- if (s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0) {
2176
- const wrongProxy = tools.wrongProxy ?? {};
2177
- if (s.wrongProxyEnabled !== void 0) wrongProxy.enabled = s.wrongProxyEnabled;
2178
- if (s.wrongProxyUrl !== void 0) wrongProxy.url = s.wrongProxyUrl;
2179
- tools.wrongProxy = wrongProxy;
2180
- }
2181
- profileConfig.tools = tools;
2182
- }
2183
- }
2697
+ const body = stripFrontmatter(await skillLoader.readBody(skillName)).trim();
2698
+ if (!body) continue;
2699
+ loaded.push(`## Required skill: ${skillName}
2700
+
2701
+ ${body}`);
2702
+ } catch (err) {
2703
+ process.emitWarning(
2704
+ `Kanban dispatch skill "${skillName}" could not be loaded, skipped: ${err instanceof Error ? err.message : String(err)}`,
2705
+ { code: "WRONGSTACK_KANBAN_SKILL_SKIPPED" }
2184
2706
  );
2185
2707
  }
2186
- if (s.debugStream !== void 0) {
2187
- const { setDebugStreamEnabled } = await import("@wrongstack/providers");
2188
- setDebugStreamEnabled(s.debugStream);
2189
- }
2190
- const currentConfig = configStore.get();
2191
- const nextModelRuntime = {
2192
- ...currentConfig.modelRuntime,
2193
- ...decrypted.modelRuntime ?? {}
2194
- };
2195
- if (s.cacheTtl === "default") {
2196
- delete nextModelRuntime.cache;
2197
- }
2198
- configStore.update({
2199
- ...s.nextPrediction !== void 0 ? { nextPrediction: s.nextPrediction } : {},
2200
- ...s.yolo !== void 0 ? { yolo: s.yolo } : {},
2201
- ...s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0 || fsAccess !== void 0 ? {
2202
- features: {
2203
- ...currentConfig.features,
2204
- ...decrypted.features ?? {}
2205
- }
2206
- } : {},
2207
- ...s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.contextMode !== void 0 ? {
2208
- context: {
2209
- ...currentConfig.context,
2210
- ...decrypted.context ?? {}
2211
- }
2212
- } : {},
2213
- ...s.maxConcurrent !== void 0 ? { maxConcurrent: s.maxConcurrent } : {},
2214
- ...s.logLevel !== void 0 ? {
2215
- log: {
2216
- ...currentConfig.log,
2217
- ...decrypted.log ?? {}
2218
- }
2219
- } : {},
2220
- ...s.auditLevel !== void 0 ? {
2221
- session: {
2222
- ...currentConfig.session,
2223
- ...decrypted.session ?? {}
2224
- }
2225
- } : {},
2226
- ...s.indexOnStart !== void 0 ? {
2227
- indexing: {
2228
- ...currentConfig.indexing,
2229
- ...decrypted.indexing ?? {}
2230
- }
2231
- } : {},
2232
- ...s.maxIterations !== void 0 || s.multiDiffSummaryThreshold !== void 0 || s.nextStepsTool !== void 0 || fsAccess !== void 0 || // WrongProxy / WrongTrace: must be in the tools guard or the
2233
- // in-memory ConfigStore never sees the freshly-saved values,
2234
- // and the next picker open in the same process would overwrite
2235
- // the on-disk selection with stale state. See Chimera review.
2236
- s.wrongProxyEnabled !== void 0 || s.wrongProxyUrl !== void 0 ? {
2237
- tools: {
2238
- ...currentConfig.tools,
2239
- ...decrypted.tools ?? {}
2240
- }
2241
- } : {},
2242
- ...s.debugStream !== void 0 ? { debugStream: s.debugStream } : {},
2243
- ...s.configScope !== void 0 ? { configScope: s.configScope } : {},
2244
- autonomy: {
2245
- ...currentConfig.autonomy,
2246
- ...decrypted.autonomy ?? {}
2247
- },
2248
- ...s.reasoningMode !== void 0 || s.reasoningEffort !== void 0 || s.reasoningPreserve !== void 0 || s.cacheTtl !== void 0 ? {
2249
- modelRuntime: nextModelRuntime
2250
- } : {},
2251
- ...s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0 ? {
2252
- circuitBreaker: {
2253
- ...currentConfig.circuitBreaker,
2254
- ...decrypted.circuitBreaker ?? {}
2255
- }
2256
- } : {},
2257
- ...s.sageMemoryInjectThreshold !== void 0 ? {
2258
- Sage: {
2259
- ...currentConfig.Sage,
2260
- ...decrypted.Sage ?? {},
2261
- inject: {
2262
- ...currentConfig.Sage?.inject ?? {},
2263
- ...decrypted.Sage?.inject ?? {}
2264
- }
2265
- }
2266
- } : {}
2267
- });
2268
- }
2269
- if (s.breakerEnabled !== void 0 || s.breakerAutoKillResetMs !== void 0) {
2270
- getProcessRegistry().setBreakerConfig({
2271
- ...s.breakerEnabled !== void 0 ? { enabled: s.breakerEnabled } : {},
2272
- ...s.breakerAutoKillResetMs !== void 0 ? { autoKillResetMs: s.breakerAutoKillResetMs } : {}
2273
- });
2274
- }
2275
- if (s.fleetChatVerbosity !== void 0) {
2276
- if (fleetStreamController?.setMode) fleetStreamController.setMode(s.fleetChatVerbosity);
2277
2708
  }
2278
- applyLiveSettings?.(s);
2279
- return null;
2280
- } catch (err) {
2281
- const message = err instanceof Error ? err.message : String(err);
2282
- console.debug(
2283
- JSON.stringify({
2284
- level: "error",
2285
- event: "execution.settings_persist_failed",
2286
- message,
2287
- errorName: err instanceof Error ? err.name : void 0,
2288
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2289
- })
2290
- );
2291
- return message;
2292
- }
2293
- }
2294
- return { getSettings, saveSettings };
2295
- }
2709
+ if (loaded.length > 0) {
2710
+ agentDescription = `${description}
2296
2711
 
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}`);
2712
+ # Required agentic skill instructions
2713
+
2714
+ ${loaded.join("\n\n")}`;
2313
2715
  }
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 });
2321
2716
  }
2322
- };
2323
- }
2324
-
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);
2717
+ void (async () => {
2718
+ let built;
2719
+ let completionReported = false;
2720
+ try {
2721
+ if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2722
+ built = await sddSubagentFactory(
2723
+ {
2724
+ id: subagentId,
2725
+ name,
2726
+ role: "kanban-agent",
2727
+ prompt: agentDescription,
2728
+ allowedCapabilities: spawnOpts?.allowedCapabilities ?? WIDE_SUBAGENT_CAPABILITIES,
2729
+ ...resolvedProvider ? { provider: resolvedProvider } : {},
2730
+ ...resolvedModel ? { model: resolvedModel } : {},
2731
+ ...resolvedFallbackModels ? { fallbackModels: resolvedFallbackModels } : {},
2732
+ ...spawnOpts?.tools ? { tools: spawnOpts.tools } : {}
2733
+ },
2734
+ ...spawnOpts?.context ? [{ id: taskId, description: agentDescription, context: spawnOpts.context }] : []
2735
+ );
2736
+ if (spawnOpts?.signal?.aborted) throw new Error("Kanban dispatch cancelled.");
2737
+ const result = await built.agent.run(
2738
+ agentDescription,
2739
+ ...spawnOpts?.signal ? [{ signal: spawnOpts.signal }] : []
2740
+ );
2741
+ completionReported = true;
2742
+ await spawnOpts?.onDone?.({
2743
+ status: result.status === "done" ? "completed" : "failed",
2744
+ result: result.finalText,
2745
+ ..."error" in result && result.error?.message ? { error: result.error.message } : {}
2746
+ });
2747
+ } catch (err) {
2748
+ if (!completionReported) {
2749
+ await spawnOpts?.onDone?.({
2750
+ status: "failed",
2751
+ error: err instanceof Error ? err.message : String(err)
2752
+ });
2753
+ }
2754
+ throw err;
2755
+ } finally {
2756
+ await built?.dispose?.();
2757
+ }
2758
+ })().catch((err) => {
2759
+ events.emit("error", {
2760
+ err: err instanceof Error ? err : new Error(String(err)),
2761
+ phase: "kanban.dispatch"
2762
+ });
2763
+ });
2764
+ const tags = [];
2765
+ if (resolvedProvider) tags.push(resolvedProvider);
2766
+ if (resolvedModel) tags.push(resolvedModel);
2767
+ if (resolvedFallbackModels?.length) {
2768
+ tags.push(`fallback=${resolvedFallbackModels.join(",")}`);
2346
2769
  }
2770
+ if (spawnOpts?.fallbackProfile) tags.push(`profile=${spawnOpts.fallbackProfile}`);
2771
+ if (spawnOpts?.skills?.length) tags.push(`skills=${spawnOpts.skills.join(",")}`);
2772
+ if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
2773
+ const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
2774
+ return `Spawned subagent ${subagentId}${tag} for task ${taskId}.`;
2347
2775
  };
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
- };
2776
+ return { onKanbanDispatch };
2480
2777
  }
2481
2778
 
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;
2779
+ // src/execution-repl-fleet-callbacks.ts
2780
+ function createReplFleetCallbacks({
2781
+ director,
2782
+ getDirector
2783
+ }) {
2784
+ const resolveDirector = () => getDirector?.() ?? director;
2517
2785
  return {
2518
- provider: assignment.provider,
2519
- model: assignment.model,
2520
- fallbackModels: assignment.fallbackModels
2786
+ onInterruptFleet: () => {
2787
+ const dir = resolveDirector();
2788
+ if (!dir) return 0;
2789
+ let killed = 0;
2790
+ for (const sa of dir.status().subagents) {
2791
+ if (sa.status === "running" || sa.status === "idle") {
2792
+ try {
2793
+ void dir.remove(sa.id);
2794
+ killed++;
2795
+ } catch {
2796
+ }
2797
+ }
2798
+ }
2799
+ return killed;
2800
+ },
2801
+ onAgentIterationComplete: (tokens) => {
2802
+ const dir = resolveDirector();
2803
+ dir?.setLeaderContextPressure(tokens);
2804
+ }
2521
2805
  };
2522
2806
  }
2523
- function assignReviewerModelsRoundRobin(provider, model, fallbackModels, statusTracker) {
2524
- return assignReviewerModels(provider, model, fallbackModels, "round-robin", statusTracker);
2807
+
2808
+ // src/fleet-statusline.ts
2809
+ import { color as color5, onResize } from "@wrongstack/core/utils";
2810
+ function fmtElapsed(ms) {
2811
+ const s = Math.floor(ms / 1e3);
2812
+ if (s < 60) return `${s}s`;
2813
+ const m = Math.floor(s / 60);
2814
+ const rem = s % 60;
2815
+ if (m < 60) return `${m}m${rem.toString().padStart(2, "0")}s`;
2816
+ const h = Math.floor(m / 60);
2817
+ return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
2525
2818
  }
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 });
2819
+ function renderFleetLine(states, now, columns, version) {
2820
+ const all = [...states.values()];
2821
+ if (all.length === 0) return "";
2822
+ const running = all.filter((a) => a.status === "running");
2823
+ const done = all.filter((a) => a.status === "done").length;
2824
+ const failed = all.filter((a) => a.status === "failed").length;
2825
+ const versionChip = version ? `${color5.bold("WS")}${color5.dim(` v${version}`)} ${color5.dim("\u2502")} ` : "";
2826
+ const counts = versionChip + `${color5.cyan("\u27F3 fleet")} ${color5.yellow(`\u25B6${running.length}`)} ${color5.green(`\u2713${done}`)}` + (failed > 0 ? ` ${color5.red(`\u2717${failed}`)}` : "");
2827
+ const shown = running.sort((a, b) => a.startedAt - b.startedAt).slice(0, 4).map((a) => {
2828
+ const elapsed = fmtElapsed(Math.max(0, now - a.startedAt));
2829
+ const tool = a.lastTool ? ` ${color5.dim(a.lastTool)}` : "";
2830
+ const ext = a.extensions && a.extensions > 0 ? ` ${color5.yellow(`\u26A1${a.extensions}`)}` : "";
2831
+ return `${color5.bold(a.name)} ${color5.yellow("\u25B6")} ${color5.dim(elapsed)} ${color5.dim(`L${a.iterations}`)} ${color5.dim(`${a.toolCalls}t`)}${tool}${ext}`;
2832
+ });
2833
+ let line = shown.length > 0 ? `${counts} ${color5.dim("\u2502")} ${shown.join(color5.dim(" \xB7 "))}` : counts;
2834
+ const max = Math.max(20, columns - 1);
2835
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
2836
+ if (visible.length > max) {
2837
+ let count = 0;
2838
+ const out = [];
2839
+ let i = 0;
2840
+ while (i < line.length && count < max - 1) {
2841
+ if (line[i] === "\x1B") {
2842
+ const end = line.indexOf("m", i);
2843
+ if (end !== -1) {
2844
+ out.push(line.slice(i, end + 1));
2845
+ i = end + 1;
2846
+ continue;
2847
+ }
2848
+ }
2849
+ if (line[i] !== void 0) out.push(line[i]);
2850
+ count++;
2851
+ i++;
2852
+ }
2853
+ line = out.join("") + "\u2026";
2854
+ }
2855
+ return line;
2856
+ }
2857
+ var FleetStatusLine = class {
2858
+ events;
2859
+ out;
2860
+ throttleMs;
2861
+ now;
2862
+ version;
2863
+ states = /* @__PURE__ */ new Map();
2864
+ unsubs = [];
2865
+ active = false;
2866
+ rows = 0;
2867
+ repaintTimer = null;
2868
+ tickTimer = null;
2869
+ deactivateTimer = null;
2870
+ resizeCleanup = null;
2871
+ lastPaint = 0;
2872
+ onResize = () => {
2873
+ if (this.active) {
2874
+ this.rows = this.out.rows ?? 24;
2875
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
2876
+ this.paint(true);
2877
+ }
2543
2878
  };
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);
2879
+ constructor(opts) {
2880
+ this.events = opts.events;
2881
+ this.out = opts.out ?? process.stdout;
2882
+ this.throttleMs = opts.throttleMs ?? 150;
2883
+ this.now = opts.now ?? Date.now;
2884
+ this.version = opts.version;
2885
+ }
2886
+ /** Subscribe to host fleet events. No terminal output until a subagent spawns. */
2887
+ start() {
2888
+ const ensure = (id, name) => {
2889
+ let s = this.states.get(id);
2890
+ if (!s) {
2891
+ s = {
2892
+ id,
2893
+ name: name ?? id,
2894
+ status: "running",
2895
+ iterations: 0,
2896
+ toolCalls: 0,
2897
+ startedAt: this.now()
2898
+ };
2899
+ this.states.set(id, s);
2900
+ } else if (name && s.name === s.id) {
2901
+ s.name = name;
2902
+ }
2903
+ return s;
2904
+ };
2905
+ this.unsubs.push(
2906
+ this.events.on("subagent.spawned", (e) => {
2907
+ ensure(e.subagentId, e.name);
2908
+ this.activate();
2909
+ this.schedulePaint();
2910
+ }),
2911
+ this.events.on("subagent.task_started", (e) => {
2912
+ const s = ensure(e.subagentId);
2913
+ s.status = "running";
2914
+ this.activate();
2915
+ this.schedulePaint();
2916
+ }),
2917
+ this.events.on("subagent.tool_executed", (e) => {
2918
+ const s = ensure(e.subagentId);
2919
+ s.toolCalls++;
2920
+ s.lastTool = e.name;
2921
+ this.activate();
2922
+ this.schedulePaint();
2923
+ }),
2924
+ this.events.on("subagent.iteration_summary", (e) => {
2925
+ const s = ensure(e.subagentId);
2926
+ s.iterations = e.iteration;
2927
+ if (typeof e.toolCalls === "number") s.toolCalls = e.toolCalls;
2928
+ if (e.currentTool) s.lastTool = e.currentTool;
2929
+ this.schedulePaint();
2930
+ }),
2931
+ this.events.on("subagent.budget_extended", (e) => {
2932
+ const s = ensure(e.subagentId);
2933
+ s.extensions = e.totalExtensions;
2934
+ this.activate();
2935
+ this.schedulePaint();
2936
+ }),
2937
+ this.events.on("subagent.task_completed", (e) => {
2938
+ const s = ensure(e.subagentId);
2939
+ s.status = e.status === "success" ? "done" : "failed";
2940
+ s.iterations = e.iterations;
2941
+ s.toolCalls = e.toolCalls;
2942
+ s.endedAt = this.now();
2943
+ this.schedulePaint();
2944
+ if (![...this.states.values()].some((a) => a.status === "running")) {
2945
+ if (this.deactivateTimer) clearTimeout(this.deactivateTimer);
2946
+ this.deactivateTimer = setTimeout(() => {
2947
+ this.deactivateTimer = null;
2948
+ if (![...this.states.values()].some((a) => a.status === "running")) {
2949
+ this.deactivate();
2950
+ }
2951
+ }, 800);
2952
+ }
2953
+ }),
2954
+ this.events.on("subagent.removed", (e) => {
2955
+ this.states.delete(e.subagentId);
2956
+ this.schedulePaint();
2957
+ })
2958
+ );
2959
+ }
2960
+ /** Unsubscribe and restore the terminal. Idempotent. */
2961
+ stop() {
2962
+ for (const u of this.unsubs.splice(0)) u();
2963
+ this.deactivate();
2964
+ }
2965
+ isTty() {
2966
+ return !!this.out.isTTY;
2967
+ }
2968
+ activate() {
2969
+ if (this.active || !this.isTty()) return;
2970
+ this.active = true;
2971
+ this.rows = this.out.rows ?? 24;
2972
+ this.out.write("\n");
2973
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
2974
+ this.out.write(`\x1B[${this.rows - 1};1H`);
2975
+ this.resizeCleanup = onResize(() => this.onResize(), this.out);
2976
+ this.tickTimer = setInterval(() => this.paint(true), 1e3);
2977
+ if (this.tickTimer.unref) this.tickTimer.unref();
2978
+ this.paint(true);
2979
+ }
2980
+ deactivate() {
2981
+ if (!this.active) return;
2982
+ this.active = false;
2983
+ if (this.tickTimer) {
2984
+ clearInterval(this.tickTimer);
2985
+ this.tickTimer = null;
2986
+ }
2987
+ if (this.repaintTimer) {
2988
+ clearTimeout(this.repaintTimer);
2989
+ this.repaintTimer = null;
2990
+ }
2991
+ if (this.deactivateTimer) {
2992
+ clearTimeout(this.deactivateTimer);
2993
+ this.deactivateTimer = null;
2994
+ }
2995
+ this.resizeCleanup?.();
2996
+ this.resizeCleanup = null;
2997
+ if (this.isTty()) {
2998
+ this.out.write("\x1B7");
2999
+ this.out.write(`\x1B[${this.rows};1H`);
3000
+ this.out.write("\x1B[2K");
3001
+ this.out.write("\x1B8");
3002
+ this.out.write("\x1B[r");
2552
3003
  }
2553
3004
  }
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;
3005
+ schedulePaint() {
3006
+ if (!this.active) return;
3007
+ const since = this.now() - this.lastPaint;
3008
+ if (since >= this.throttleMs) {
3009
+ this.paint(false);
3010
+ return;
3011
+ }
3012
+ if (this.repaintTimer) return;
3013
+ this.repaintTimer = setTimeout(() => {
3014
+ this.repaintTimer = null;
3015
+ this.paint(false);
3016
+ }, this.throttleMs - since);
3017
+ if (this.repaintTimer.unref) this.repaintTimer.unref();
2562
3018
  }
2563
- return trimmed;
3019
+ paint(force) {
3020
+ if (!this.active || !this.isTty()) return;
3021
+ if (!force) this.lastPaint = this.now();
3022
+ else this.lastPaint = this.now();
3023
+ const line = renderFleetLine(this.states, this.now(), this.out.columns ?? 80, this.version);
3024
+ this.out.write("\x1B7");
3025
+ this.out.write(`\x1B[${this.rows};1H`);
3026
+ this.out.write("\x1B[2K");
3027
+ this.out.write(line);
3028
+ this.out.write("\x1B8");
3029
+ }
3030
+ };
3031
+
3032
+ // src/subagent-models/panel-service.ts
3033
+ import {
3034
+ emptySubagentModelPlan,
3035
+ formatSubagentSlot,
3036
+ getSessionSubagentModelPlan,
3037
+ setSessionSubagentModelPlan,
3038
+ subagentSlotOccupancy
3039
+ } from "@wrongstack/core/coordination";
3040
+ function currentPlan(sessionId) {
3041
+ return getSessionSubagentModelPlan(sessionId) ?? emptySubagentModelPlan();
2564
3042
  }
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"
3043
+ function createSubagentModelsPanelHost(deps) {
3044
+ const write = async (mutate) => {
3045
+ const ctx = deps.getContext();
3046
+ if (!ctx?.session) return "No active session \u2014 the plan cannot be saved.";
3047
+ const plan = structuredClone(currentPlan(ctx.session.id));
3048
+ mutate(plan);
3049
+ try {
3050
+ await setSessionSubagentModelPlan(ctx, plan);
3051
+ return null;
3052
+ } catch (err) {
3053
+ return err instanceof Error ? err.message : String(err);
3054
+ }
2573
3055
  };
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
3056
  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
- };
2625
- }
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) {
2651
- }
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()
2663
- })
2664
- );
2665
- }
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);
2702
- }
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
3057
+ snapshot() {
3058
+ const sessionId = deps.getContext()?.session?.id;
3059
+ const plan = currentPlan(sessionId);
3060
+ const occupancy = subagentSlotOccupancy(sessionId);
3061
+ const session = deps.getSessionTarget?.();
3062
+ return {
3063
+ enabled: plan.enabled,
3064
+ lock: plan.lock,
3065
+ followSessionModel: plan.followSessionModel === true,
3066
+ sessionTarget: session?.provider ? `${session.provider}/${session.model ?? "?"}` : session?.model ?? "",
3067
+ lanes: plan.slots.map((slot, i) => ({
3068
+ target: formatSubagentSlot(slot),
3069
+ busy: occupancy[i]?.subagentIds.length ?? 0,
3070
+ ...slot.label ? { label: slot.label } : {}
3071
+ })),
3072
+ roles: Object.entries(plan.roles ?? {}).map(([role, slot]) => ({ role, target: formatSubagentSlot(slot) })).sort((a, b) => a.role.localeCompare(b.role))
3073
+ };
3074
+ },
3075
+ setLane(index, target) {
3076
+ return write((plan) => {
3077
+ if (index < 0 || index >= plan.slots.length) return;
3078
+ const slot = {};
3079
+ if (target.provider) slot.provider = target.provider;
3080
+ if (target.model) slot.model = target.model;
3081
+ if (target.tier) slot.tier = target.tier;
3082
+ if (target.fallbackProfile) slot.fallbackProfile = target.fallbackProfile;
3083
+ if (target.label) slot.label = target.label;
3084
+ plan.slots[index] = slot;
2730
3085
  });
2731
- const deadline = new Promise((resolve4) => {
2732
- const t = setTimeout(resolve4, 5e3);
2733
- t.unref?.();
3086
+ },
3087
+ clearLane(index) {
3088
+ return write((plan) => {
3089
+ if (index < 0 || index >= plan.slots.length) return;
3090
+ plan.slots[index] = {};
3091
+ });
3092
+ },
3093
+ toggle(field) {
3094
+ return write((plan) => {
3095
+ plan[field] = plan[field] !== true;
2734
3096
  });
2735
- const tracked = Promise.race([report, deadline]).finally(
2736
- () => sessionEndProducers.delete(tracked)
2737
- );
2738
- void tracked.catch(() => void 0);
2739
- sessionEndProducers.add(tracked);
2740
3097
  }
2741
- } catch {
3098
+ };
3099
+ }
3100
+
3101
+ // src/tui-resume-callback.ts
3102
+ import { hasOpenTodos } from "@wrongstack/core/utils";
3103
+ import { parseNextSteps } from "@wrongstack/tools/next-steps";
3104
+
3105
+ // src/boot/tui-session-resume.ts
3106
+ import * as path10 from "node:path";
3107
+ import {
3108
+ restoreSessionSubagentModelPlan,
3109
+ restoreSessionSubagentPolicy
3110
+ } from "@wrongstack/core/coordination";
3111
+ import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
3112
+ import { restoreSessionPermissionOverrides as restoreSessionPermissionOverrides2 } from "@wrongstack/core/security";
3113
+ import { restoreRequiredSkillsFromEvents as restoreRequiredSkillsFromEvents2 } from "@wrongstack/core/skills";
3114
+ import { attachTodosCheckpoint, loadTodosCheckpoint } from "@wrongstack/core/storage";
3115
+ import { projectLastRequestTokens } from "@wrongstack/core/types/session-timeline";
3116
+ import { sessionScopedPath as sessionScopedPath2, toErrorMessage } from "@wrongstack/core/utils";
3117
+ var RESERVATION_WINDOW_MS = 6e4;
3118
+ var RESERVATION_RENEW_MS = 2e4;
3119
+ function messageText(content) {
3120
+ if (typeof content === "string") return content;
3121
+ if (!Array.isArray(content)) return "";
3122
+ return content.map(
3123
+ (block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : ""
3124
+ ).filter(Boolean).join("\n");
3125
+ }
3126
+ function lastAssistantTextOf(messages) {
3127
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3128
+ const message = messages[index];
3129
+ if (message?.role !== "assistant") continue;
3130
+ const text = messageText(message.content).trim();
3131
+ if (text) return text;
2742
3132
  }
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());
3133
+ return void 0;
3134
+ }
3135
+ async function resumeSession(ctx, sessionId) {
3136
+ const { state, agent, tokenCounter, switchProviderAndModel, events, onLoadProgress } = ctx;
3137
+ const fail = (stage2, message) => {
3138
+ try {
3139
+ ctx.onFailure?.({ stage: stage2, message: message || "unknown error" });
3140
+ } catch {
2748
3141
  }
2749
- } catch {
2750
- }
2751
- if (director) {
3142
+ return null;
3143
+ };
3144
+ const warnings = [];
3145
+ const warn = (label, err) => {
3146
+ warnings.push(`${label}: ${toErrorMessage(err)}`);
3147
+ };
3148
+ let stage = "start";
3149
+ const setStage = (next) => {
3150
+ stage = next;
2752
3151
  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
- );
3152
+ ctx.onStage?.(next);
3153
+ } catch {
2763
3154
  }
3155
+ };
3156
+ if (!state.activeSessionStore) {
3157
+ return fail("no_session_store", "No session store is bound to this project.");
2764
3158
  }
3159
+ setStage("load_replay_module");
3160
+ let replaySessionMessages;
2765
3161
  try {
2766
- while (sessionEndProducers.size > 0) {
2767
- await Promise.allSettled([...sessionEndProducers]);
3162
+ ({ replaySessionMessages } = await import("@wrongstack/tui"));
3163
+ if (typeof replaySessionMessages !== "function") {
3164
+ throw new TypeError("@wrongstack/tui does not export replaySessionMessages");
2768
3165
  }
2769
- if (director) {
2770
- try {
2771
- director.requestFinish("leader session ended \u2014 finish your review now");
2772
- } catch {
2773
- }
3166
+ } catch (err) {
3167
+ return fail(stage, toErrorMessage(err));
3168
+ }
3169
+ setStage("resolve_id");
3170
+ let canonicalSessionId = sessionId;
3171
+ try {
3172
+ if (state.activeSessionStore.resolveId) {
3173
+ canonicalSessionId = await state.activeSessionStore.resolveId(sessionId);
2774
3174
  }
2775
- await chimeraWork?.drainAndClose();
2776
3175
  } catch (err) {
2777
- console.warn(
3176
+ console.error(
2778
3177
  JSON.stringify({
2779
- level: "warn",
2780
- event: "shutdown.chimera_work_failed",
2781
- message: `Pending chimera work failed: ${err instanceof Error ? err.message : String(err)}`,
3178
+ level: "error",
3179
+ event: "execution.resume_id_resolve_failed",
3180
+ sessionId,
3181
+ message: toErrorMessage(err),
2782
3182
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2783
3183
  })
2784
3184
  );
3185
+ return fail(stage, toErrorMessage(err));
2785
3186
  }
2786
- if (director) {
3187
+ const previousSessionId = agent.ctx.session?.id;
3188
+ let hydrated;
3189
+ let identityClaimed = false;
3190
+ let resumeClaim;
3191
+ let writerSwapped = false;
3192
+ let openedWriter;
3193
+ let oldWriter;
3194
+ let oldMessages;
3195
+ let oldSessionRefCurrent;
3196
+ let previousDetachFn;
3197
+ let oldTodos = [];
3198
+ let oldPlanPath;
3199
+ let oldTaskPath;
3200
+ try {
3201
+ setStage("reserve_ownership");
3202
+ if (state.wpaths.projectSlug && state.wpaths.globalRoot) {
3203
+ const { getSessionRegistry } = await import("@wrongstack/core/storage");
3204
+ const registry = getSessionRegistry(state.wpaths.globalRoot);
3205
+ resumeClaim = await registry.reserveResume({
3206
+ sessionId: canonicalSessionId,
3207
+ projectSlug: state.wpaths.projectSlug,
3208
+ projectRoot: state.projectRoot,
3209
+ reservationMs: RESERVATION_WINDOW_MS
3210
+ });
3211
+ } else if (state.activateSessionIdentity) {
3212
+ await state.activateSessionIdentity(canonicalSessionId);
3213
+ identityClaimed = true;
3214
+ }
3215
+ setStage("open_journal");
3216
+ const claimToRenew = resumeClaim;
3217
+ let renewFailures = 0;
3218
+ const renew = () => {
3219
+ if (!claimToRenew) return;
3220
+ void claimToRenew.renew(RESERVATION_WINDOW_MS).catch((err) => {
3221
+ if (renewFailures++ > 0) return;
3222
+ console.error(
3223
+ JSON.stringify({
3224
+ level: "warn",
3225
+ event: "execution.resume_reservation_renew_failed",
3226
+ sessionId: canonicalSessionId,
3227
+ message: toErrorMessage(err),
3228
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3229
+ })
3230
+ );
3231
+ });
3232
+ };
3233
+ const renewTimer = setInterval(renew, RESERVATION_RENEW_MS);
3234
+ renewTimer.unref?.();
3235
+ let resumed;
2787
3236
  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
- );
3237
+ resumed = await state.activeSessionStore.resume(canonicalSessionId, onLoadProgress);
3238
+ } finally {
3239
+ clearInterval(renewTimer);
3240
+ }
3241
+ openedWriter = resumed.writer;
3242
+ hydrated = resumed.data;
3243
+ setStage("activate_ownership");
3244
+ await Promise.resolve(resumeClaim?.renew(RESERVATION_WINDOW_MS)).catch(() => void 0);
3245
+ if (resumeClaim) {
3246
+ await resumeClaim.activate({
3247
+ sessionId: canonicalSessionId,
3248
+ projectSlug: state.wpaths.projectSlug,
3249
+ projectRoot: state.projectRoot,
3250
+ projectName: path10.basename(state.projectRoot),
3251
+ workingDir: agent.ctx.workingDir,
3252
+ clientType: "tui",
3253
+ pid: process.pid,
3254
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3255
+ });
3256
+ identityClaimed = true;
3257
+ await state.activateSessionIdentity?.(canonicalSessionId);
3258
+ }
3259
+ setStage("replay_history");
3260
+ const meta = resumed.data.metadata;
3261
+ const entries = replaySessionMessages(
3262
+ resumed.data.messages,
3263
+ resumed.data.events,
3264
+ /* startId */
3265
+ 1
3266
+ );
3267
+ try {
3268
+ agent.container?.safeResolve?.(TOKENS2.DelegationTracker)?.rehydrate(canonicalSessionId, resumed.data.events ?? []);
3269
+ } catch (err) {
3270
+ warn("delegation rehydrate", err);
3271
+ }
3272
+ setStage("read_sidecars");
3273
+ const sessionsDir = state.wpaths.projectSessions;
3274
+ const resumedTodosPath = sessionScopedPath2(sessionsDir, resumed.writer.id, ".todos.json");
3275
+ const restoredTodos = await loadTodosCheckpoint(
3276
+ resumedTodosPath,
3277
+ events,
3278
+ agent.ctx.traceId,
3279
+ resumed.writer.id
3280
+ ).catch(() => null);
3281
+ setStage("swap_writer");
3282
+ oldWriter = agent.ctx.session;
3283
+ oldMessages = [...agent.ctx.messages];
3284
+ agent.ctx.session = resumed.writer;
3285
+ try {
3286
+ agent.ctx.state.replaceMessages(resumed.data.messages);
3287
+ restoreSessionSubagentPolicy(agent.ctx, resumed.data.events, resumed.data.subagentsAllowed);
3288
+ restoreSessionSubagentModelPlan(agent.ctx, resumed.data.events);
3289
+ restoreSessionPermissionOverrides2(agent.ctx.meta, resumed.data);
3290
+ restoreRequiredSkillsFromEvents2(agent.ctx, resumed.data.events);
3291
+ } catch (err) {
3292
+ agent.ctx.session = oldWriter;
3293
+ agent.ctx.state.replaceMessages(oldMessages);
3294
+ throw err;
3295
+ }
3296
+ writerSwapped = true;
3297
+ if (state.sessionRef) {
3298
+ oldSessionRefCurrent = state.sessionRef.current;
3299
+ state.sessionRef.current = resumed.writer;
2798
3300
  }
2799
- try {
2800
- await director.terminateAll();
2801
- } catch (termErr) {
2802
- console.warn(
3301
+ setStage("flush_journal");
3302
+ await agent.ctx.flushConversationJournal().catch((err) => {
3303
+ console.error(
2803
3304
  JSON.stringify({
2804
- level: "warn",
2805
- event: "shutdown.director_terminate_all_failed",
2806
- message: termErr instanceof Error ? termErr.message : String(termErr),
3305
+ level: "error",
3306
+ event: "execution.resume_journal_flush_failed",
3307
+ message: toErrorMessage(err),
2807
3308
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2808
3309
  })
2809
3310
  );
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 } : {}
3311
+ warn("conversation journal did not flush", err);
2860
3312
  });
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
- }
3313
+ setStage("repoint_sidecars");
3314
+ previousDetachFn = state.detachActiveTodosCheckpoint;
3315
+ oldTodos = agent.ctx.state.todos ? [...agent.ctx.state.todos] : [];
3316
+ oldPlanPath = agent.ctx.state.meta?.["plan.path"];
3317
+ oldTaskPath = agent.ctx.state.meta?.["task.path"];
3318
+ try {
3319
+ await Promise.resolve(previousDetachFn?.()).catch(() => void 0);
3320
+ agent.ctx.state.setMeta(
3321
+ "plan.path",
3322
+ sessionScopedPath2(sessionsDir, resumed.writer.id, ".plan.json")
3323
+ );
3324
+ agent.ctx.state.setMeta(
3325
+ "task.path",
3326
+ sessionScopedPath2(sessionsDir, resumed.writer.id, ".tasks.json")
3327
+ );
3328
+ agent.ctx.state.replaceTodos(restoredTodos ?? []);
3329
+ try {
3330
+ state.detachActiveTodosCheckpoint = attachTodosCheckpoint(
3331
+ agent.ctx.state,
3332
+ resumedTodosPath,
3333
+ resumed.writer.id,
3334
+ events,
3335
+ agent.ctx.traceId
3336
+ );
3337
+ } catch {
3338
+ state.detachActiveTodosCheckpoint = void 0;
2919
3339
  }
2920
- if (loaded.length > 0) {
2921
- agentDescription = `${description}
2922
-
2923
- # Required agentic skill instructions
2924
-
2925
- ${loaded.join("\n\n")}`;
3340
+ } catch (err) {
3341
+ warn("todo/plan sidecars were not re-pointed", err);
3342
+ try {
3343
+ state.detachActiveTodosCheckpoint = previousDetachFn;
3344
+ agent.ctx.state.replaceTodos([...oldTodos]);
3345
+ if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
3346
+ if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
3347
+ } catch {
2926
3348
  }
2927
3349
  }
2928
- void (async () => {
2929
- let built;
2930
- let completionReported = false;
3350
+ setStage("restore_model");
3351
+ const currentProviderId = agent.ctx.provider.id;
3352
+ const savedProvider = typeof meta.provider === "string" ? meta.provider.trim() : "";
3353
+ const savedModel = typeof meta.model === "string" ? meta.model.trim() : "";
3354
+ const hasSavedRoute = savedProvider.length > 0 && savedProvider.toLowerCase() !== "unknown" && savedModel.length > 0 && savedModel.toLowerCase() !== "unknown";
3355
+ const targetProviderId = hasSavedRoute ? savedProvider : currentProviderId;
3356
+ const targetModel = hasSavedRoute ? savedModel : agent.ctx.model;
3357
+ if (switchProviderAndModel && targetProviderId && (targetProviderId !== currentProviderId || targetModel !== agent.ctx.model)) {
2931
3358
  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
- });
3359
+ const error = await Promise.resolve(switchProviderAndModel(targetProviderId, targetModel));
3360
+ if (typeof error === "string" && error)
3361
+ warn(`could not restore ${targetProviderId}/${targetModel}`, error);
2958
3362
  } catch (err) {
2959
- if (!completionReported) {
2960
- await spawnOpts?.onDone?.({
2961
- status: "failed",
2962
- error: err instanceof Error ? err.message : String(err)
2963
- });
2964
- }
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") {
3003
- try {
3004
- void dir.remove(sa.id);
3005
- killed++;
3006
- } catch {
3007
- }
3008
- }
3363
+ console.error(
3364
+ JSON.stringify({
3365
+ level: "error",
3366
+ event: "execution.resume_model_restore_failed",
3367
+ provider: targetProviderId,
3368
+ model: targetModel,
3369
+ message: toErrorMessage(err),
3370
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3371
+ })
3372
+ );
3373
+ warn(`could not restore ${targetProviderId}/${targetModel}`, err);
3009
3374
  }
3010
- return killed;
3011
- },
3012
- onAgentIterationComplete: (tokens) => {
3013
- const dir = resolveDirector();
3014
- dir?.setLeaderContextPressure(tokens);
3375
+ } else if (targetModel !== agent.ctx.model) {
3376
+ agent.ctx.model = targetModel;
3015
3377
  }
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;
3378
+ setStage("finalize_previous_session");
3379
+ let finalizeOldWriter;
3380
+ if (oldWriter && oldWriter !== resumed.writer) {
3381
+ const staleWriter = oldWriter;
3382
+ const endedUsage = tokenCounter.total();
3383
+ finalizeOldWriter = async () => {
3384
+ let appendOk = false;
3385
+ try {
3386
+ await staleWriter.append({
3387
+ type: "session_end",
3388
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3389
+ usage: endedUsage
3390
+ });
3391
+ appendOk = true;
3392
+ } catch (err) {
3393
+ console.error(
3394
+ JSON.stringify({
3395
+ level: "error",
3396
+ event: "execution.session_end_append_failed",
3397
+ message: toErrorMessage(err),
3398
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3399
+ })
3400
+ );
3058
3401
  }
3059
- }
3060
- if (line[i] !== void 0) out.push(line[i]);
3061
- count++;
3062
- i++;
3402
+ if (appendOk) {
3403
+ try {
3404
+ await staleWriter.close();
3405
+ } catch (err) {
3406
+ console.error(
3407
+ JSON.stringify({
3408
+ level: "error",
3409
+ event: "execution.session_close_failed",
3410
+ message: toErrorMessage(err),
3411
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3412
+ })
3413
+ );
3414
+ }
3415
+ }
3416
+ };
3063
3417
  }
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);
3418
+ setStage("token_accounting");
3419
+ try {
3420
+ tokenCounter.reset();
3421
+ tokenCounter.account(resumed.data.usage, targetModel, targetProviderId);
3422
+ } catch (err) {
3423
+ warn("token accounting for the resumed session failed", err);
3088
3424
  }
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;
3425
+ const resumedRequestTokens = projectLastRequestTokens(resumed.data.events);
3426
+ agent.ctx.lastRequestTokens = resumedRequestTokens;
3427
+ tokenCounter.setCurrentRequestTokens?.(resumedRequestTokens ?? 0);
3428
+ const tokens = resumedRequestTokens ?? 0;
3429
+ const maxContext = agent.ctx.provider?.capabilities?.maxContext ?? 0;
3430
+ const finalText = lastAssistantTextOf(resumed.data.messages);
3431
+ const result = {
3432
+ entries,
3433
+ nextId: entries.length + 1,
3434
+ sessionId: resumed.writer.id,
3435
+ contextSnapshot: { tokens, maxContext },
3436
+ attached: true,
3437
+ warnings,
3438
+ ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
3115
3439
  };
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;
3440
+ if (finalizeOldWriter) void finalizeOldWriter();
3441
+ return result;
3442
+ } catch (err) {
3443
+ if (!identityClaimed) await resumeClaim?.cancel().catch(() => void 0);
3444
+ if (identityClaimed && previousSessionId) {
3445
+ await state.activateSessionIdentity?.(previousSessionId).catch((rollbackErr) => {
3446
+ console.error(
3447
+ JSON.stringify({
3448
+ level: "error",
3449
+ event: "execution.resume_identity_rollback_failed",
3450
+ sessionId: previousSessionId,
3451
+ message: toErrorMessage(rollbackErr),
3452
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3453
+ })
3454
+ );
3455
+ });
3197
3456
  }
3198
- if (this.repaintTimer) {
3199
- clearTimeout(this.repaintTimer);
3200
- this.repaintTimer = null;
3457
+ if (!writerSwapped) {
3458
+ await openedWriter?.close().catch(() => void 0);
3459
+ } else {
3460
+ try {
3461
+ if (oldWriter !== void 0) agent.ctx.session = oldWriter;
3462
+ if (state.sessionRef) state.sessionRef.current = oldSessionRefCurrent;
3463
+ if (oldMessages !== void 0) agent.ctx.state.replaceMessages(oldMessages);
3464
+ if (previousDetachFn !== void 0) state.detachActiveTodosCheckpoint = previousDetachFn;
3465
+ agent.ctx.state.replaceTodos([...oldTodos]);
3466
+ if (oldPlanPath !== void 0) agent.ctx.state.setMeta("plan.path", oldPlanPath);
3467
+ if (oldTaskPath !== void 0) agent.ctx.state.setMeta("task.path", oldTaskPath);
3468
+ } catch (rollbackErr) {
3469
+ console.error(
3470
+ JSON.stringify({
3471
+ level: "error",
3472
+ event: "execution.resume_post_swap_rollback_failed",
3473
+ message: toErrorMessage(rollbackErr),
3474
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3475
+ })
3476
+ );
3477
+ }
3478
+ if (openedWriter && openedWriter !== oldWriter) {
3479
+ await openedWriter.close().catch((closeErr) => {
3480
+ console.error(
3481
+ JSON.stringify({
3482
+ level: "error",
3483
+ event: "execution.resume_opened_writer_close_failed",
3484
+ message: toErrorMessage(closeErr),
3485
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3486
+ })
3487
+ );
3488
+ });
3489
+ }
3201
3490
  }
3202
- if (this.deactivateTimer) {
3203
- clearTimeout(this.deactivateTimer);
3204
- this.deactivateTimer = null;
3491
+ const reason = err instanceof Error ? err.stack ?? err.message : String(err);
3492
+ console.error(
3493
+ JSON.stringify({
3494
+ level: "error",
3495
+ event: "execution.resume_session_failed",
3496
+ sessionId: canonicalSessionId,
3497
+ stage,
3498
+ message: reason,
3499
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3500
+ })
3501
+ );
3502
+ const message = toErrorMessage(err);
3503
+ if (err instanceof Error && err.name === "SessionOwnershipConflictError") {
3504
+ return fail(stage, message);
3205
3505
  }
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");
3506
+ let data = hydrated;
3507
+ if (!data) {
3508
+ try {
3509
+ data = await state.activeSessionStore.load(canonicalSessionId, onLoadProgress);
3510
+ } catch {
3511
+ data = void 0;
3512
+ }
3214
3513
  }
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;
3514
+ if (data) {
3515
+ try {
3516
+ const entries = replaySessionMessages(
3517
+ data.messages,
3518
+ data.events,
3519
+ /* startId */
3520
+ 1
3521
+ );
3522
+ const finalText = lastAssistantTextOf(data.messages);
3523
+ fail(stage, message);
3524
+ return {
3525
+ entries,
3526
+ nextId: entries.length + 1,
3527
+ sessionId: canonicalSessionId,
3528
+ // Deliberately no contextSnapshot: the chip reports the LIVE
3529
+ // window, and the live session is still the one we rolled back to.
3530
+ // Painting the viewed session's fill over it would be a lie.
3531
+ attached: false,
3532
+ warnings: [...warnings, `not attached (at ${stage}): ${message}`],
3533
+ ...finalText !== void 0 ? { lastAssistantText: finalText } : {}
3534
+ };
3535
+ } catch (renderErr) {
3536
+ console.error(
3537
+ JSON.stringify({
3538
+ level: "error",
3539
+ event: "execution.resume_readonly_render_failed",
3540
+ sessionId: canonicalSessionId,
3541
+ message: toErrorMessage(renderErr),
3542
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3543
+ })
3544
+ );
3545
+ }
3222
3546
  }
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");
3547
+ return fail(stage, message);
3240
3548
  }
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
3549
  }
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
- });
3550
+
3551
+ // src/tui-resume-callback.ts
3552
+ function createTuiResumeCallback({
3553
+ state,
3554
+ agent,
3555
+ tokenCounter,
3556
+ switchProviderAndModel,
3557
+ events
3558
+ }) {
3559
+ return async (sessionId, onLoadProgress, onStage) => {
3560
+ let failure;
3561
+ const result = await resumeSession(
3562
+ {
3563
+ state,
3564
+ agent,
3565
+ tokenCounter,
3566
+ switchProviderAndModel,
3567
+ events,
3568
+ onLoadProgress,
3569
+ onStage,
3570
+ onFailure: (info) => {
3571
+ failure = info;
3572
+ }
3573
+ },
3574
+ sessionId
3575
+ );
3576
+ if (!result) {
3577
+ throw new Error(
3578
+ failure ? `${failure.message} (at ${failure.stage})` : `Session "${sessionId}" could not be resumed.`
3579
+ );
3308
3580
  }
3581
+ setAutoSuggestions([]);
3582
+ const resumedNextSteps = result.attached && !hasOpenTodos(agent.ctx.todos) && result.lastAssistantText ? parseNextSteps(result.lastAssistantText).texts : [];
3583
+ return { ...result, nextSteps: resumedNextSteps };
3309
3584
  };
3310
3585
  }
3311
3586
 
@@ -3348,11 +3623,16 @@ function createChimeraWorkRegistry() {
3348
3623
 
3349
3624
  // src/execution-chimera-cascade.ts
3350
3625
  import * as fsp from "node:fs/promises";
3351
- import * as path10 from "node:path";
3626
+ import * as path11 from "node:path";
3352
3627
  import { emitReviewIfChanged } from "@wrongstack/core/plugin";
3353
3628
 
3354
3629
  // src/chimera-cascade-evidence.ts
3355
3630
  import { spawn as spawn2 } from "node:child_process";
3631
+ import {
3632
+ buildWin32CmdShimInvocation,
3633
+ isWinCmdShim,
3634
+ resolveWin32Command
3635
+ } from "@wrongstack/tools/win32";
3356
3636
  var JSON_BLOCK_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
3357
3637
  function tryParseJson(raw) {
3358
3638
  try {
@@ -3404,14 +3684,9 @@ function parseSingleCheck(raw) {
3404
3684
  if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
3405
3685
  return void 0;
3406
3686
  }
3407
- const output = typeof check.output === "string" ? truncateOutput(check.output) : void 0;
3687
+ const output = typeof check.output === "string" ? check.output : void 0;
3408
3688
  return { command: command.trim(), exitCode, output };
3409
3689
  }
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
3690
  var SAFE_EXECUTABLES = /* @__PURE__ */ new Set([
3416
3691
  "pnpm",
3417
3692
  "npm",
@@ -3440,23 +3715,39 @@ var runCascadeVerificationCommand = (command, cwd, timeoutMs) => {
3440
3715
  return Promise.resolve({ exitCode: CASCADE_EVIDENCE_UNSAFE_EXIT });
3441
3716
  }
3442
3717
  const tokens = command.trim().split(/\s+/);
3443
- const executable = tokens[0];
3444
- const args = tokens.slice(1);
3445
- return new Promise((resolve4) => {
3718
+ let executable = tokens[0];
3719
+ let args = tokens.slice(1);
3720
+ let windowsVerbatimArguments = false;
3721
+ if (process.platform === "win32") {
3722
+ const resolved = resolveWin32Command(executable);
3723
+ if (isWinCmdShim(resolved)) {
3724
+ let shim;
3725
+ try {
3726
+ shim = buildWin32CmdShimInvocation(resolved, args);
3727
+ } catch {
3728
+ return Promise.resolve({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
3729
+ }
3730
+ executable = shim.command;
3731
+ args = shim.args;
3732
+ windowsVerbatimArguments = true;
3733
+ }
3734
+ }
3735
+ return new Promise((resolve5) => {
3446
3736
  let child;
3447
3737
  try {
3448
3738
  child = spawn2(executable, args, {
3449
3739
  cwd,
3450
3740
  stdio: "ignore",
3451
3741
  windowsHide: true,
3452
- signal: AbortSignal.timeout(timeoutMs)
3742
+ signal: AbortSignal.timeout(timeoutMs),
3743
+ ...windowsVerbatimArguments ? { windowsVerbatimArguments } : {}
3453
3744
  });
3454
3745
  } catch {
3455
- resolve4({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
3746
+ resolve5({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
3456
3747
  return;
3457
3748
  }
3458
- child.on("error", () => resolve4({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3459
- child.on("close", (code) => resolve4({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3749
+ child.on("error", () => resolve5({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3750
+ child.on("close", (code) => resolve5({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
3460
3751
  });
3461
3752
  };
3462
3753
  async function verifyCascadeEvidence(evidence, cwd, runCommand = runCascadeVerificationCommand, timeoutMs = CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS) {
@@ -3974,7 +4265,7 @@ async function maybeReReviewCascade({
3974
4265
  const reReadFiles = [];
3975
4266
  for (const f of evidenceBundle.files) {
3976
4267
  try {
3977
- const absPath = path10.join(evidenceBundle.cwd, f.path);
4268
+ const absPath = path11.join(evidenceBundle.cwd, f.path);
3978
4269
  const content = await fsp.readFile(absPath, "utf8");
3979
4270
  reReadFiles.push({ path: f.path, status: "modified", content });
3980
4271
  } catch {
@@ -4051,7 +4342,7 @@ function errorText2(error) {
4051
4342
 
4052
4343
  // src/execution-chimera-review.ts
4053
4344
  import { randomUUID as randomUUID3 } from "node:crypto";
4054
- import path11 from "node:path";
4345
+ import path12 from "node:path";
4055
4346
  import { effectiveFallbackChain, fallbackProfileChain as fallbackProfileChain2 } from "@wrongstack/core/agent";
4056
4347
  import {
4057
4348
  areSubagentsAllowedForSession
@@ -4069,10 +4360,14 @@ import {
4069
4360
  function normalizeFileKeyForCitation(raw, cwd) {
4070
4361
  const forward = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4071
4362
  const isAbsolute = forward.startsWith("/") || /^[a-zA-Z]:\//.test(forward);
4072
- const pathMod = process.platform === "win32" ? path11.win32 : path11;
4363
+ const pathMod = process.platform === "win32" ? path12.win32 : path12;
4073
4364
  const relative = isAbsolute ? pathMod.relative(cwd, forward).replace(/\\/g, "/").replace(/^\.\//, "") : forward;
4074
4365
  return process.platform === "win32" ? relative.toLowerCase() : relative;
4075
4366
  }
4367
+ function readPositiveNumber(config, key) {
4368
+ const raw = config.extensions?.["wstack-chimera"]?.[key];
4369
+ return typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? raw : void 0;
4370
+ }
4076
4371
  function installChimeraReviewHandler({
4077
4372
  events,
4078
4373
  director,
@@ -4122,7 +4417,12 @@ function installChimeraReviewHandler({
4122
4417
  };
4123
4418
  const persistAndEmitCompletion = async (completion) => {
4124
4419
  try {
4125
- await persistReview(completion, projectDir);
4420
+ await persistReview(completion, projectDir, {
4421
+ // Read per completion, not once at install time, so an operator
4422
+ // editing either key takes effect without a restart.
4423
+ openRetentionDays: readPositiveNumber(config, "openRetentionDays"),
4424
+ activeRetentionDays: readPositiveNumber(config, "activeRetentionDays")
4425
+ });
4126
4426
  events.emitCustom("chimera.review_persisted", {
4127
4427
  reportId: completion.reportId,
4128
4428
  sessionId: completion.sessionId,
@@ -4296,10 +4596,10 @@ function installChimeraReviewHandler({
4296
4596
  const changedBasenameIndex = /* @__PURE__ */ new Map();
4297
4597
  for (const file of p.files) {
4298
4598
  const normalized = normalizeFileKeyForCitation(file.path, p.cwd);
4299
- const basename7 = path11.basename(file.path).toLowerCase();
4300
- const bucket = changedBasenameIndex.get(basename7);
4599
+ const basename8 = path12.basename(file.path).toLowerCase();
4600
+ const bucket = changedBasenameIndex.get(basename8);
4301
4601
  if (bucket) bucket.push(normalized);
4302
- else changedBasenameIndex.set(basename7, [normalized]);
4602
+ else changedBasenameIndex.set(basename8, [normalized]);
4303
4603
  }
4304
4604
  const inScopeCited = citedFindings.filter((finding) => {
4305
4605
  const citedKey = normalizeFileKeyForCitation(finding.location.file, p.cwd);
@@ -4308,8 +4608,8 @@ function installChimeraReviewHandler({
4308
4608
  (cp) => cp.endsWith(`/${citedKey}`) || cp === citedKey
4309
4609
  );
4310
4610
  if (suffixMatches.length === 1) return true;
4311
- const basename7 = path11.basename(finding.location.file).toLowerCase();
4312
- const bucket = changedBasenameIndex.get(basename7);
4611
+ const basename8 = path12.basename(finding.location.file).toLowerCase();
4612
+ const bucket = changedBasenameIndex.get(basename8);
4313
4613
  return !!(bucket && bucket.length === 1);
4314
4614
  });
4315
4615
  const droppedCitations = citedFindings.filter(
@@ -4458,13 +4758,13 @@ ${reviewBody}`;
4458
4758
  );
4459
4759
  if (off) teardownHandlers?.push(off);
4460
4760
  }
4461
- async function persistChimeraReview(payload, projectDir) {
4761
+ async function persistChimeraReview(payload, projectDir, maintenance = {}) {
4462
4762
  const reportId = payload.reportId;
4463
4763
  if (!reportId) throw new Error("Chimera completion is missing reportId");
4464
4764
  await persistReviewReport(payload, reportId, projectDir);
4465
4765
  await integrateFindings(payload, projectDir, reportId);
4466
4766
  try {
4467
- await maybeCompactReviewStores(projectDir);
4767
+ await maybeCompactReviewStores(projectDir, maintenance);
4468
4768
  } catch (error) {
4469
4769
  console.error(
4470
4770
  JSON.stringify({
@@ -4609,21 +4909,13 @@ function installStorageObservability(events, rootTraceId) {
4609
4909
  // src/next-task-predictor.ts
4610
4910
  import { readBundledInstructionText } from "@wrongstack/core/utils";
4611
4911
  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
4912
  function buildPredictionPrompt(input) {
4619
4913
  const parts = [];
4620
4914
  parts.push(`The user asked:
4621
- ${clamp(input.userRequest, MAX_REQUEST_CHARS) || "(no text)"}`);
4915
+ ${input.userRequest.trim() || "(no text)"}`);
4622
4916
  if (input.assistantSummary.trim()) {
4623
- parts.push(
4624
- `The assistant just finished and reported:
4625
- ${clamp(input.assistantSummary, MAX_SUMMARY_CHARS)}`
4626
- );
4917
+ parts.push(`The assistant just finished and reported:
4918
+ ${input.assistantSummary.trim()}`);
4627
4919
  }
4628
4920
  const pending = input.todos.filter((t) => t.status !== "completed");
4629
4921
  if (pending.length > 0) {
@@ -4678,7 +4970,6 @@ async function predictNextTasks(input, opts) {
4678
4970
  messages: [
4679
4971
  { role: "user", content: [{ type: "text", text: buildPredictionPrompt(input) }] }
4680
4972
  ],
4681
- maxTokens: 160,
4682
4973
  temperature: 0.3
4683
4974
  },
4684
4975
  { signal: internal.signal }
@@ -4697,7 +4988,7 @@ import { detectContinueIntent, InputBuilder, resolveContinuation } from "@wrongs
4697
4988
  import {
4698
4989
  color as color11,
4699
4990
  estimateRequestTokensCalibrated as estimateRequestTokensCalibrated2,
4700
- hasOpenTodos as hasOpenTodos2,
4991
+ hasOpenTodos as hasOpenTodos3,
4701
4992
  toErrorMessage as toErrorMessage4
4702
4993
  } from "@wrongstack/core/utils";
4703
4994
  import { readClipboardImage, routeImagesForModel } from "@wrongstack/runtime";
@@ -4711,14 +5002,14 @@ import {
4711
5002
  } from "@wrongstack/tools/auto-proceed-loop-guard";
4712
5003
 
4713
5004
  // src/repl-suggestions.ts
4714
- import { hasOpenTodos } from "@wrongstack/core/utils";
4715
- import { parseNextSteps } from "@wrongstack/tools/next-steps";
5005
+ import { hasOpenTodos as hasOpenTodos2 } from "@wrongstack/core/utils";
5006
+ import { parseNextSteps as parseNextSteps2 } from "@wrongstack/tools/next-steps";
4716
5007
  function parseSuggestionsFromOutput(finalText, todos) {
4717
- if (hasOpenTodos(todos)) {
5008
+ if (hasOpenTodos2(todos)) {
4718
5009
  setAutoSuggestions([]);
4719
5010
  return null;
4720
5011
  }
4721
- const { texts, autoTexts } = parseNextSteps(finalText);
5012
+ const { texts, autoTexts } = parseNextSteps2(finalText);
4722
5013
  if (autoTexts.length > 0) {
4723
5014
  setAutoSuggestions(autoTexts);
4724
5015
  }
@@ -4822,12 +5113,12 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4822
5113
  let interval;
4823
5114
  let lastTickedSecond = sec + 1;
4824
5115
  let onAbort;
4825
- return new Promise((resolve4) => {
4826
- onAbort = () => resolve4(false);
5116
+ return new Promise((resolve5) => {
5117
+ onAbort = () => resolve5(false);
4827
5118
  signal.addEventListener("abort", onAbort, { once: true });
4828
5119
  interval = setInterval(() => {
4829
5120
  if (signal.aborted) {
4830
- resolve4(false);
5121
+ resolve5(false);
4831
5122
  return;
4832
5123
  }
4833
5124
  const elapsed = Date.now() - start;
@@ -4835,7 +5126,7 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4835
5126
  if (remaining <= 0) {
4836
5127
  opts.renderer.write(color6.dim(` \u21B3 ${truncated}
4837
5128
  `));
4838
- resolve4(true);
5129
+ resolve5(true);
4839
5130
  return;
4840
5131
  }
4841
5132
  if (opts.onCountdownTick && remaining !== lastTickedSecond) {
@@ -4846,7 +5137,7 @@ ${color6.cyan("\u23F3 Auto")} ${color6.dim("(Ctrl+C to cancel)")}
4846
5137
  opts.renderer.write(
4847
5138
  color6.yellow(" \u21B3 Countdown cancelled \u2014 switching to manual mode\n")
4848
5139
  );
4849
- resolve4(false);
5140
+ resolve5(false);
4850
5141
  return;
4851
5142
  }
4852
5143
  } catch {
@@ -4878,7 +5169,7 @@ async function runContextCompaction(opts, _signal) {
4878
5169
 
4879
5170
  // src/repl-client-registration.ts
4880
5171
  import * as crypto from "node:crypto";
4881
- import * as path12 from "node:path";
5172
+ import * as path13 from "node:path";
4882
5173
  import { getSharedProjectMailbox, resolveProjectDir } from "@wrongstack/core/coordination";
4883
5174
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
4884
5175
  function registerReplClient(opts) {
@@ -4888,9 +5179,11 @@ function registerReplClient(opts) {
4888
5179
  const hqConnection = startCliHqConnection({
4889
5180
  clientKind: "repl",
4890
5181
  projectRoot: replProjectRoot,
4891
- projectName: path12.basename(replProjectRoot),
5182
+ projectName: path13.basename(replProjectRoot),
4892
5183
  appConfig: opts.appConfig,
4893
- capabilities: ["telemetry.publish", "mailbox.summary"]
5184
+ capabilities: ["telemetry.publish", "mailbox.summary"],
5185
+ // The CLI host's own connection in this process syncs the boards.
5186
+ ownKanbanSync: false
4894
5187
  });
4895
5188
  const clientMailbox = getSharedProjectMailbox(
4896
5189
  projectDir,
@@ -4902,7 +5195,7 @@ function registerReplClient(opts) {
4902
5195
  clientMailbox.registerClient({
4903
5196
  clientId,
4904
5197
  sessionId: opts.getSessionId?.() ?? replProjectRoot,
4905
- name: `REPL [${path12.basename(replProjectRoot)}]`,
5198
+ name: `REPL [${path13.basename(replProjectRoot)}]`,
4906
5199
  source: "repl",
4907
5200
  pid: process.pid
4908
5201
  }).then(() => {
@@ -4914,6 +5207,7 @@ function registerReplClient(opts) {
4914
5207
  clientMailbox.clientHeartbeat({ clientId, sessionId: opts.getSessionId?.() ?? replProjectRoot }).catch(() => {
4915
5208
  });
4916
5209
  }, 15e3);
5210
+ clientHeartbeat.unref?.();
4917
5211
  }).catch(() => {
4918
5212
  });
4919
5213
  return {
@@ -5046,7 +5340,7 @@ async function runReplEternalLoop(opts, onInterruptsReset) {
5046
5340
  } catch (err) {
5047
5341
  opts.renderer.writeError(`[eternal] ${toErrorMessage3(err)}`);
5048
5342
  }
5049
- await new Promise((resolve4) => setTimeout(resolve4, 250));
5343
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
5050
5344
  return true;
5051
5345
  }
5052
5346
  if (autonomy === "eternal-parallel") {
@@ -5117,7 +5411,7 @@ async function runReplEternalLoop(opts, onInterruptsReset) {
5117
5411
  } catch (err) {
5118
5412
  opts.renderer.writeError(`[parallel] ${toErrorMessage3(err)}`);
5119
5413
  }
5120
- await new Promise((resolve4) => setTimeout(resolve4, 250));
5414
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
5121
5415
  return true;
5122
5416
  }
5123
5417
  return false;
@@ -5154,7 +5448,7 @@ async function readPossiblyMultiline(opts) {
5154
5448
  // src/repl-rendering.ts
5155
5449
  import { color as color10 } from "@wrongstack/core/utils";
5156
5450
  var FILLED = "\u2588";
5157
- var EMPTY = "\u2591";
5451
+ var EMPTY2 = "\u2591";
5158
5452
  function renderContextChip(used, max) {
5159
5453
  const ratio = Math.max(0, Math.min(1, used / max));
5160
5454
  const pct = Math.round(ratio * 100);
@@ -5165,7 +5459,7 @@ function renderProgress(ratio, width) {
5165
5459
  const clamped = Math.max(0, Math.min(1, ratio));
5166
5460
  const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
5167
5461
  const capped = Math.min(width, filled);
5168
- return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
5462
+ return FILLED.repeat(capped) + EMPTY2.repeat(width - capped);
5169
5463
  }
5170
5464
  function printBanner(renderer, projectName) {
5171
5465
  const lines = [
@@ -5657,7 +5951,7 @@ ${color11.dim(
5657
5951
  }
5658
5952
  if (result.status === "done" && opts.getAutonomy) {
5659
5953
  const autonomy2 = opts.getAutonomy();
5660
- if (autonomy2 === "suggest" && !hasOpenTodos2(opts.agent.ctx.todos)) {
5954
+ if (autonomy2 === "suggest" && !hasOpenTodos3(opts.agent.ctx.todos)) {
5661
5955
  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
5956
  const suggestBlocks = [{ type: "text", text: suggestPrompt }];
5663
5957
  const suggestCtrl = new AbortController();
@@ -5694,7 +5988,7 @@ ${suggestResult.finalText}
5694
5988
  }
5695
5989
  }
5696
5990
  }
5697
- if (result.status === "done" && opts.getNextPredict?.() && !hasOpenTodos2(opts.agent.ctx.todos)) {
5991
+ if (result.status === "done" && opts.getNextPredict?.() && !hasOpenTodos3(opts.agent.ctx.todos)) {
5698
5992
  const autonomy2 = opts.getAutonomy?.() ?? "off";
5699
5993
  if (autonomy2 === "off") {
5700
5994
  const predictCtrl = new AbortController();
@@ -6001,8 +6295,8 @@ async function profileMenu(store, paths) {
6001
6295
  }
6002
6296
  const items = await Promise.all(
6003
6297
  names.map(async (name) => {
6004
- const path14 = paths.profileConfig(name);
6005
- const data = await readJsonObjectFile(path14);
6298
+ const path15 = paths.profileConfig(name);
6299
+ const data = await readJsonObjectFile(path15);
6006
6300
  const provider = typeof data["provider"] === "string" ? data["provider"] : "unset";
6007
6301
  const model = typeof data["model"] === "string" ? data["model"] : "unset";
6008
6302
  const fallbackCount = Array.isArray(data["fallbackModels"]) ? data["fallbackModels"].length : 0;
@@ -6023,7 +6317,7 @@ async function profileMenu(store, paths) {
6023
6317
  },
6024
6318
  { label: "fallbacks", value: String(fallbackCount) },
6025
6319
  { label: "fallback profiles", value: String(profileCount) },
6026
- { label: "config", value: path14 }
6320
+ { label: "config", value: path15 }
6027
6321
  ],
6028
6322
  body: isActive ? "This profile owns the current session configuration." : "Switching profiles closes this session so every service restarts with the selected configuration.",
6029
6323
  actions: isActive ? [] : [{ key: "s", label: "switch", command: `/profile switch ${name}`, confirm: true }]
@@ -6153,19 +6447,19 @@ async function gitMenu(projectRoot) {
6153
6447
  },
6154
6448
  ...lines.map((line, index) => {
6155
6449
  const code = line.slice(0, 2);
6156
- const path14 = line.slice(3);
6450
+ const path15 = line.slice(3);
6157
6451
  const staged = code[0] !== " " && code[0] !== "?";
6158
6452
  const unstaged = code[1] !== " " || code === "??";
6159
6453
  return {
6160
- id: `change:${index}:${path14}`,
6161
- label: path14,
6454
+ id: `change:${index}:${path15}`,
6455
+ label: path15,
6162
6456
  status: staged ? "good" : "warn",
6163
6457
  summary: `${code} \xB7 ${staged ? "staged" : "not staged"}${unstaged ? " \xB7 working tree" : ""}`,
6164
6458
  details: [
6165
6459
  { label: "status", value: code },
6166
6460
  { label: "staged", value: staged ? "yes" : "no" },
6167
6461
  { label: "unstaged", value: unstaged ? "yes" : "no" },
6168
- { label: "path", value: path14 }
6462
+ { label: "path", value: path15 }
6169
6463
  ],
6170
6464
  actions: [
6171
6465
  { key: "d", label: "diff summary", command: staged ? "/git diff --staged" : "/git diff" }
@@ -6194,20 +6488,20 @@ async function worktreeMenu(projectRoot) {
6194
6488
  return split < 0 ? [line, "yes"] : [line.slice(0, split), line.slice(split + 1)];
6195
6489
  })
6196
6490
  );
6197
- const path14 = fields["worktree"] ?? "(unknown path)";
6491
+ const path15 = fields["worktree"] ?? "(unknown path)";
6198
6492
  const branchRef = fields["branch"];
6199
6493
  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);
6494
+ const status = path15 === "(unknown path)" || fields["prunable"] ? null : await runGit(["status", "--porcelain=v1"], path15);
6201
6495
  const changes = status?.code === 0 ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
6202
6496
  const dirty = changes.length > 0;
6203
6497
  const isMain = index === 0;
6204
6498
  return {
6205
- id: path14,
6499
+ id: path15,
6206
6500
  label: branch,
6207
6501
  status: fields["prunable"] ? "bad" : dirty ? "warn" : "good",
6208
6502
  summary: `${isMain ? "main" : "linked"} \xB7 ${dirty ? `${changes.length} changes` : "clean"}`,
6209
6503
  details: [
6210
- { label: "path", value: path14 },
6504
+ { label: "path", value: path15 },
6211
6505
  { label: "branch", value: branch },
6212
6506
  { label: "HEAD", value: fields["HEAD"]?.slice(0, 12) ?? "unknown" },
6213
6507
  { label: "working tree", value: dirty ? `${changes.length} changed paths` : "clean" },
@@ -6442,6 +6736,7 @@ async function execute(deps) {
6442
6736
  });
6443
6737
  let code = 0;
6444
6738
  let backgroundKanbanSupervisor;
6739
+ let autoUpdate;
6445
6740
  let fleetStatusLine = null;
6446
6741
  try {
6447
6742
  const visionAdapters = () => createToolVisionAdapters(agent.tools);
@@ -6453,7 +6748,8 @@ async function execute(deps) {
6453
6748
  modelsRegistry,
6454
6749
  context.provider.id,
6455
6750
  context.model,
6456
- mergedModels
6751
+ mergedModels,
6752
+ { catalogProviderId: catalogProviderIdFor(context.provider.id, providerConfig?.type) }
6457
6753
  );
6458
6754
  return caps.vision;
6459
6755
  } catch {
@@ -6466,9 +6762,6 @@ async function execute(deps) {
6466
6762
  }
6467
6763
  const goalFlag = typeof flags["goal"] === "string" ? flags["goal"] : void 0;
6468
6764
  const askFlag = typeof flags["ask"] === "string" ? flags["ask"] : void 0;
6469
- if ((goalFlag || askFlag) && positional.length === 0 && !promptFlag) {
6470
- flags.tui = true;
6471
- }
6472
6765
  const executionMode = resolveExecutionMode(positional, flags);
6473
6766
  if (projectRoot && executionMode !== "webui") {
6474
6767
  backgroundKanbanSupervisor = (await import("./kanban-supervisor-67ECABTD.js")).createKanbanSupervisor({
@@ -6484,18 +6777,35 @@ async function execute(deps) {
6484
6777
  log: (message) => console.log(message)
6485
6778
  });
6486
6779
  }
6780
+ if (executionMode !== "single-shot" && !webuiSessionChild) {
6781
+ autoUpdate = (await import("./standalone-auto-update-TOTRGCSA.js")).startStandaloneAutoUpdate({
6782
+ getConfig: () => configStore.get(),
6783
+ updateInfo: bootUpdateInfo
6784
+ });
6785
+ }
6487
6786
  const enteringTui = executionMode === "tui";
6488
6787
  if (!enteringTui) {
6489
6788
  fleetStatusLine = new FleetStatusLine({ events, version: CLI_VERSION });
6490
6789
  fleetStatusLine.start();
6491
6790
  }
6492
6791
  if (executionMode === "single-shot") {
6792
+ const piped = await readPipedStdin(process.stdin);
6793
+ if (piped.timedOut) {
6794
+ renderer.writeWarning(
6795
+ "No stdin data received in time; continuing without it. Redirect stdin (< /dev/null) to skip the wait."
6796
+ );
6797
+ } else if (piped.truncated) {
6798
+ renderer.writeWarning("Piped stdin exceeded 2 MB and was truncated.");
6799
+ }
6800
+ await mcpRegistry.whenStarted();
6493
6801
  code = await runSingleShotDispatch({
6494
6802
  agent,
6495
- query: positional.join(" "),
6803
+ query: appendPipedStdin(positional.join(" "), piped.text),
6496
6804
  flags,
6497
6805
  tokenCounter,
6498
- renderer
6806
+ renderer,
6807
+ events,
6808
+ interruptController
6499
6809
  });
6500
6810
  } else if (executionMode === "tui") {
6501
6811
  agent.disableInteractiveConfirmation();
@@ -6558,6 +6868,7 @@ async function execute(deps) {
6558
6868
  const switchProjectInPlace2 = async (targetRoot, displayName) => {
6559
6869
  return switchProjectInPlace(switchCtx, targetRoot, displayName);
6560
6870
  };
6871
+ const worktreeSessions = /* @__PURE__ */ new Map();
6561
6872
  const pickerCtx = {
6562
6873
  state,
6563
6874
  renderer,
@@ -6582,6 +6893,10 @@ async function execute(deps) {
6582
6893
  code = await runTuiDispatch({
6583
6894
  leaderAutoWake,
6584
6895
  agent,
6896
+ // The session store's root. Without it /rewind, the checkpoint
6897
+ // timeline and /rewind redo resolve every session file against ''
6898
+ // and fail, and the layout store and history archive stay ephemeral.
6899
+ sessionsDir: wpaths.projectSessions,
6585
6900
  events,
6586
6901
  slashRegistry,
6587
6902
  skillLoader,
@@ -6600,6 +6915,17 @@ async function execute(deps) {
6600
6915
  model: context.model,
6601
6916
  banner: !flags["no-banner"],
6602
6917
  queueStore,
6918
+ // `/resume` brings the resumed session's queue along; the WebUI
6919
+ // keeps a session's queue in the same file.
6920
+ queueStoreFor: (sessionId) => {
6921
+ try {
6922
+ return new QueueStore({
6923
+ dir: sessionScopedPath3(state.wpaths.projectSessions, sessionId, "")
6924
+ });
6925
+ } catch {
6926
+ return void 0;
6927
+ }
6928
+ },
6603
6929
  onQueueChange: (items) => {
6604
6930
  setQueuedMessagesSnapshot2(context, items);
6605
6931
  },
@@ -6623,6 +6949,7 @@ async function execute(deps) {
6623
6949
  appVersion: CLI_VERSION,
6624
6950
  latestVersion: bootUpdateInfo?.latest,
6625
6951
  updateAvailable: bootUpdateInfo?.outdated,
6952
+ subscribeUpdateReady: autoUpdate?.subscribe,
6626
6953
  provider: config.provider,
6627
6954
  family: banneredFamily,
6628
6955
  keyTail: banneredKeyTail,
@@ -6663,7 +6990,14 @@ async function execute(deps) {
6663
6990
  onClearHistory: (dispatch) => {
6664
6991
  void attachments.clear().catch(() => {
6665
6992
  });
6666
- dispatch({ type: "clearHistory", model: context.model, provider: context.provider.id });
6993
+ dispatch({
6994
+ type: "clearHistory",
6995
+ model: context.model,
6996
+ provider: context.provider.id,
6997
+ restoredMessages: null,
6998
+ restoredToolCalls: null,
6999
+ restoredEvents: null
7000
+ });
6667
7001
  dispatch({ type: "resetContextChip" });
6668
7002
  dispatch({ type: "streamReset" });
6669
7003
  dispatch({ type: "toolStreamClear" });
@@ -6766,53 +7100,49 @@ async function execute(deps) {
6766
7100
  } catch {
6767
7101
  }
6768
7102
  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
- }));
7103
+ const worktrees = await listGitWorktrees(state.projectRoot);
7104
+ worktreeSessions.clear();
7105
+ const toEntry = (s, worktree = worktreeOfSession(s, worktrees)) => {
7106
+ if (worktree) worktreeSessions.set(s.id, worktree);
7107
+ return {
7108
+ id: s.id,
7109
+ title: s.title ?? "",
7110
+ name: s.name,
7111
+ lastUserMessage: s.lastUserMessage,
7112
+ messageCount: s.messageCount,
7113
+ lastActivityAt: s.lastActivityAt,
7114
+ startedAt: s.startedAt ?? "",
7115
+ endedAt: s.endedAt,
7116
+ tokenTotal: s.tokenTotal ?? 0,
7117
+ iterationCount: s.iterationCount ?? 0,
7118
+ toolCallCount: s.toolCallCount ?? 0,
7119
+ toolErrorCount: s.toolErrorCount ?? 0,
7120
+ outcome: s.outcome,
7121
+ forkedFrom: s.forkedFrom,
7122
+ ...worktree ? { worktree } : {},
7123
+ isCurrent: s.id === currentId,
7124
+ // The session this process owns is `isCurrent`, not "live
7125
+ // elsewhere" — it holds its own lease and would otherwise be
7126
+ // labelled as owned by another surface.
7127
+ ...s.id !== currentId && liveBySession.has(s.id) ? { live: liveBySession.get(s.id) } : {}
7128
+ };
7129
+ };
7130
+ return summaries.map((s) => toEntry(s));
6789
7131
  },
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 };
7132
+ forkSession: async (sessionId, checkpointPromptIndex) => {
7133
+ const store = state.activeSessionStore;
7134
+ if (!store?.fork) throw new Error("This session store cannot fork sessions.");
7135
+ const forked = await store.fork(sessionId, {
7136
+ checkpointPromptIndex,
7137
+ beforeCheckpointPrompt: true
7138
+ });
7139
+ return { id: forked.id };
6815
7140
  },
7141
+ onResumeSession: withWorktreeSwitch(
7142
+ createTuiResumeCallback({ state, agent, tokenCounter, switchProviderAndModel, events }),
7143
+ (id) => worktreeSessions.get(id),
7144
+ switchProjectInPlace2
7145
+ ),
6816
7146
  getProjectPickerItems: () => getProjectPickerItems(pickerCtx),
6817
7147
  onProjectSelect: (slug, kind) => onProjectSelect(pickerCtx, slug, kind),
6818
7148
  initialAgentsMonitorOpen: !!flags.quick,
@@ -6845,6 +7175,7 @@ async function execute(deps) {
6845
7175
  try {
6846
7176
  code = await runWebUIDispatch({
6847
7177
  leaderAutoWake: webuiLeaderAutoWake,
7178
+ interruptController,
6848
7179
  agent,
6849
7180
  events,
6850
7181
  session,
@@ -6887,9 +7218,13 @@ async function execute(deps) {
6887
7218
  // workers it is BLOCKED on; anything started with `spawn_subagent` +
6888
7219
  // `assign_task` keeps going unless asked to stop. Scoped to the
6889
7220
  // session so one tab's Stop never reaches another tab's fleet.
6890
- stopSessionFleet: async (sessionId) => {
6891
- await getDirector?.()?.terminateSession(sessionId);
6892
- },
7221
+ stopSessionFleet: (sessionId) => getDirector?.()?.terminateSession(sessionId),
7222
+ // HQ's per-tab abort fleet / abort agent / spawn, same scoping.
7223
+ hqFleetControl: createHqFleetControl(
7224
+ () => getDirector?.() ?? null,
7225
+ () => "",
7226
+ { multiConversation: true }
7227
+ ),
6893
7228
  // Closing a tab is not stopping it. The run keeps going and keeps its
6894
7229
  // fleet; what goes is the background help pinned to that conversation
6895
7230
  // — the explore companion's poll timer and the shadow reviewer's
@@ -6940,7 +7275,7 @@ async function execute(deps) {
6940
7275
  attachments,
6941
7276
  effectiveMaxContext,
6942
7277
  getEffectiveMaxContext,
6943
- projectName: path13.basename(projectRoot) || void 0,
7278
+ projectName: path14.basename(projectRoot) || void 0,
6944
7279
  projectRoot,
6945
7280
  appConfig: config,
6946
7281
  getSessionId: () => agent.ctx.session?.id ?? session.id,
@@ -6972,6 +7307,7 @@ async function execute(deps) {
6972
7307
  }
6973
7308
  } finally {
6974
7309
  backgroundKanbanSupervisor?.dispose();
7310
+ autoUpdate?.dispose();
6975
7311
  for (const off of chimeraTeardowns.splice(0)) {
6976
7312
  try {
6977
7313
  off();
@@ -7006,4 +7342,4 @@ export {
7006
7342
  execute,
7007
7343
  resolveReviewerFallbackModels
7008
7344
  };
7009
- //# sourceMappingURL=execution-PYC5FCS4.js.map
7345
+ //# sourceMappingURL=execution-3STBD2U7.js.map