indusagi-coding-agent 0.2.13 → 0.2.14

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 (56) hide show
  1. package/README.md +19 -0
  2. package/dist/entry.js +1762 -958
  3. package/dist/guardrails.js +145 -35
  4. package/dist/index.js +1810 -1006
  5. package/dist/review-agent.js +344 -60
  6. package/dist/review-server.js +346 -62
  7. package/package.json +12 -3
  8. package/src/boot/auth-vault.ts +4 -5
  9. package/src/boot/runners/addon-wiring.ts +111 -3
  10. package/src/boot/runners/benchmark-integrity.ts +107 -0
  11. package/src/boot/runners/repl-runner.ts +2 -1
  12. package/src/boot/runners/session.ts +18 -8
  13. package/src/boot/server-token.ts +20 -11
  14. package/src/briefing/context-docs.ts +144 -12
  15. package/src/channels/oneshot.ts +6 -0
  16. package/src/conductor/conductor.ts +67 -12
  17. package/src/conductor/contract.ts +4 -3
  18. package/src/conductor/transcript-store/store.ts +4 -8
  19. package/src/console/auth-status.ts +9 -4
  20. package/src/console/components/AgentsView.ts +2 -2
  21. package/src/console/components/BackgroundAgents.ts +15 -2
  22. package/src/console/components/Banner.ts +20 -9
  23. package/src/console/components/Composer.ts +57 -82
  24. package/src/console/components/ScrollViewport.ts +46 -0
  25. package/src/console/components/TerminalConsole.ts +165 -162
  26. package/src/console/components/WorkingIndicator.ts +6 -5
  27. package/src/console/components/banner-sweep.ts +5 -1
  28. package/src/console/components/welcome.ts +3 -3
  29. package/src/console/contract.ts +10 -5
  30. package/src/console/index.ts +7 -7
  31. package/src/console/input/complete.ts +2 -2
  32. package/src/console/input/dir-reader.ts +3 -1
  33. package/src/console/input/graphemes.ts +22 -0
  34. package/src/console/input/keymap.ts +6 -4
  35. package/src/console/mount.ts +45 -3
  36. package/src/console/overlays/auth.ts +28 -8
  37. package/src/console/overlays/boards.ts +13 -40
  38. package/src/console/overlays/host.ts +10 -0
  39. package/src/console/overlays/pickers.ts +35 -30
  40. package/src/console/reducer.ts +12 -5
  41. package/src/console/slash/builtins.ts +4 -3
  42. package/src/console/slash/commands/integrations.ts +13 -14
  43. package/src/console/slash/commands/transcript.ts +3 -8
  44. package/src/console/slash/commands/workbench.ts +2 -2
  45. package/src/console/test-support.ts +48 -0
  46. package/src/integrations/sarvam/config.ts +15 -13
  47. package/src/kit/external-editor.ts +17 -1
  48. package/src/kit/run-shell.ts +31 -0
  49. package/src/launch/invocation/flags.ts +12 -0
  50. package/src/launch/login.ts +23 -10
  51. package/src/launch/oauth.ts +3 -0
  52. package/src/models.ts +78 -2
  53. package/src/security/private-state.ts +122 -0
  54. package/src/window-budget/summarize/condense.ts +4 -1
  55. package/src/workspace/brand.ts +1 -1
  56. package/src/workspace/locator.ts +2 -2
@@ -442,6 +442,56 @@ import { Agent } from "indusagi/agent";
442
442
  import { createZeroUsage as createZeroUsage2 } from "indusagi/ai";
443
443
  import * as path2 from "node:path";
444
444
 
445
+ // src/kit/run-shell.ts
446
+ import { spawn } from "node:child_process";
447
+ function runShell(command, cwd, signal) {
448
+ if (signal?.aborted) return Promise.resolve({ output: "Command cancelled.", exitCode: 130 });
449
+ return new Promise((resolve3) => {
450
+ let output = "", truncated = false, settled = false;
451
+ let forceKill;
452
+ const child = spawn(command, { cwd, shell: true, detached: process.platform !== "win32" });
453
+ const capture = (chunk) => {
454
+ output += String(chunk);
455
+ if (output.length > 1024 * 1024) {
456
+ output = output.slice(-1024 * 1024);
457
+ truncated = true;
458
+ }
459
+ };
460
+ const kill = (signalName) => {
461
+ try {
462
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signalName);
463
+ else child.kill(signalName);
464
+ } catch {
465
+ try {
466
+ child.kill(signalName);
467
+ } catch {
468
+ }
469
+ }
470
+ };
471
+ const abort = () => {
472
+ kill("SIGTERM");
473
+ forceKill = setTimeout(() => kill("SIGKILL"), 1e3);
474
+ forceKill.unref();
475
+ };
476
+ const finish = (exitCode) => {
477
+ if (settled) return;
478
+ settled = true;
479
+ signal?.removeEventListener("abort", abort);
480
+ if (forceKill) clearTimeout(forceKill);
481
+ resolve3({ output: (truncated ? "[older output truncated]\n" : "") + output, exitCode: signal?.aborted ? 130 : exitCode });
482
+ };
483
+ child.stdout?.on("data", capture);
484
+ child.stderr?.on("data", capture);
485
+ child.once("error", (error) => {
486
+ capture(error.message);
487
+ finish(1);
488
+ });
489
+ child.once("close", (code) => finish(code ?? 1));
490
+ signal?.addEventListener("abort", abort, { once: true });
491
+ if (signal?.aborted) abort();
492
+ });
493
+ }
494
+
445
495
  // src/conductor/catalog/catalog.ts
446
496
  import { z } from "zod";
447
497
  import { getModels, getProviders, modelRegistry } from "indusagi/ai";
@@ -473,7 +523,74 @@ var GPT_6_ASTRA_BASE = {
473
523
  }
474
524
  }
475
525
  };
526
+ var GPT_6_SOL_BASE = {
527
+ id: "gpt-6-sol",
528
+ name: "GPT-6 Sol",
529
+ api: "openai-responses",
530
+ reasoning: true,
531
+ reasoningEfforts: ["high"],
532
+ input: ["text", "image"],
533
+ cost: {
534
+ input: 10,
535
+ cacheRead: 1,
536
+ cacheWrite: 12.5,
537
+ output: 50
538
+ },
539
+ contextWindow: 105e4,
540
+ maxTokens: 128e3,
541
+ longContextPricing: {
542
+ threshold: 272e3,
543
+ multipliers: {
544
+ input: 2,
545
+ cacheRead: 2,
546
+ cacheWrite: 2,
547
+ output: 1.5
548
+ }
549
+ }
550
+ };
551
+ var GPT_6_LUNA_BASE = {
552
+ id: "gpt-6-luna",
553
+ name: "GPT-6 Luna",
554
+ api: "openai-responses",
555
+ reasoning: true,
556
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
557
+ input: ["text", "image"],
558
+ cost: {
559
+ input: 0.1,
560
+ cacheRead: 0.01,
561
+ cacheWrite: 0.125,
562
+ output: 0.5
563
+ },
564
+ contextWindow: 105e4,
565
+ maxTokens: 128e3,
566
+ longContextPricing: {
567
+ threshold: 272e3,
568
+ multipliers: { input: 2, cacheRead: 2, cacheWrite: 2, output: 1.5 }
569
+ }
570
+ };
476
571
  var CODING_AGENT_MODELS = [
572
+ {
573
+ ...GPT_6_SOL_BASE,
574
+ provider: "openai",
575
+ baseUrl: "https://api.openai.com/v1"
576
+ },
577
+ {
578
+ ...GPT_6_SOL_BASE,
579
+ api: "openai-codex-responses",
580
+ provider: "openai-codex",
581
+ baseUrl: "https://chatgpt.com/backend-api"
582
+ },
583
+ {
584
+ ...GPT_6_LUNA_BASE,
585
+ provider: "openai",
586
+ baseUrl: "https://api.openai.com/v1"
587
+ },
588
+ {
589
+ ...GPT_6_LUNA_BASE,
590
+ api: "openai-codex-responses",
591
+ provider: "openai-codex",
592
+ baseUrl: "https://chatgpt.com/backend-api"
593
+ },
477
594
  {
478
595
  ...GPT_6_ASTRA_BASE,
479
596
  provider: "openai",
@@ -509,8 +626,15 @@ function applyCodingAgentModelOverlay(registry) {
509
626
  return registry;
510
627
  }
511
628
  function isReasoningEffortSupported(modelId, effort) {
512
- if (modelId?.split("/").at(-1) !== "gpt-6-astra") return true;
513
- return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort);
629
+ const id = modelId?.split("/").at(-1);
630
+ if (id === "gpt-6-astra") {
631
+ return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort);
632
+ }
633
+ if (id === "gpt-6-sol") return effort === "high";
634
+ if (id === "gpt-6-luna") {
635
+ return GPT_6_LUNA_BASE.reasoningEfforts.includes(effort);
636
+ }
637
+ return true;
514
638
  }
515
639
 
516
640
  // src/conductor/catalog/catalog.ts
@@ -917,6 +1041,125 @@ function parseSessionText(sessionId, text) {
917
1041
  };
918
1042
  }
919
1043
 
1044
+ // src/security/private-state.ts
1045
+ import {
1046
+ chmodSync,
1047
+ closeSync,
1048
+ constants,
1049
+ fchmodSync,
1050
+ fstatSync,
1051
+ lstatSync,
1052
+ mkdirSync,
1053
+ openSync,
1054
+ readFileSync,
1055
+ renameSync,
1056
+ rmSync,
1057
+ writeFileSync
1058
+ } from "node:fs";
1059
+ import { randomUUID } from "node:crypto";
1060
+ import { dirname } from "node:path";
1061
+ var PRIVATE_DIRECTORY_MODE = 448;
1062
+ var PRIVATE_FILE_MODE = 384;
1063
+ var NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
1064
+ function isPosix() {
1065
+ return process.platform !== "win32";
1066
+ }
1067
+ function assertOwned(uid, path3) {
1068
+ if (!isPosix() || process.getuid === void 0) return;
1069
+ if (uid !== process.getuid()) {
1070
+ throw new Error(`Refusing sensitive path not owned by the current user: ${path3}`);
1071
+ }
1072
+ }
1073
+ function ensurePrivateDirectorySync(path3) {
1074
+ try {
1075
+ const current = lstatSync(path3);
1076
+ if (current.isSymbolicLink() || !current.isDirectory()) {
1077
+ throw new Error(`Refusing non-directory sensitive state path: ${path3}`);
1078
+ }
1079
+ assertOwned(current.uid, path3);
1080
+ } catch (error) {
1081
+ if (error.code !== "ENOENT") throw error;
1082
+ mkdirSync(path3, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
1083
+ }
1084
+ if (isPosix()) chmodSync(path3, PRIVATE_DIRECTORY_MODE);
1085
+ }
1086
+ function validateExistingFile(path3) {
1087
+ try {
1088
+ const current = lstatSync(path3);
1089
+ if (current.isSymbolicLink() || !current.isFile()) {
1090
+ throw new Error(`Refusing non-regular sensitive file: ${path3}`);
1091
+ }
1092
+ assertOwned(current.uid, path3);
1093
+ } catch (error) {
1094
+ if (error.code === "ENOENT") return;
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ function readPrivateFileSync(path3) {
1099
+ ensurePrivateDirectorySync(dirname(path3));
1100
+ try {
1101
+ validateExistingFile(path3);
1102
+ const fd = openSync(path3, constants.O_RDONLY | NO_FOLLOW);
1103
+ try {
1104
+ const current = fstatSync(fd);
1105
+ if (!current.isFile()) throw new Error(`Refusing non-regular sensitive file: ${path3}`);
1106
+ assertOwned(current.uid, path3);
1107
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
1108
+ return readFileSync(fd, "utf8");
1109
+ } finally {
1110
+ closeSync(fd);
1111
+ }
1112
+ } catch (error) {
1113
+ if (error.code === "ENOENT") return null;
1114
+ throw error;
1115
+ }
1116
+ }
1117
+ function atomicWritePrivateFileSync(path3, content) {
1118
+ const parent = dirname(path3);
1119
+ ensurePrivateDirectorySync(parent);
1120
+ validateExistingFile(path3);
1121
+ const temporary = `${path3}.tmp-${process.pid}-${randomUUID()}`;
1122
+ let fd;
1123
+ try {
1124
+ fd = openSync(
1125
+ temporary,
1126
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NO_FOLLOW,
1127
+ PRIVATE_FILE_MODE
1128
+ );
1129
+ writeFileSync(fd, content, "utf8");
1130
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
1131
+ closeSync(fd);
1132
+ fd = void 0;
1133
+ renameSync(temporary, path3);
1134
+ if (isPosix()) chmodSync(path3, PRIVATE_FILE_MODE);
1135
+ } catch (error) {
1136
+ if (fd !== void 0) closeSync(fd);
1137
+ try {
1138
+ rmSync(temporary, { force: true });
1139
+ } catch {
1140
+ }
1141
+ throw error;
1142
+ }
1143
+ }
1144
+ function appendPrivateFileSync(path3, content) {
1145
+ ensurePrivateDirectorySync(dirname(path3));
1146
+ validateExistingFile(path3);
1147
+ const fd = openSync(
1148
+ path3,
1149
+ constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | NO_FOLLOW,
1150
+ PRIVATE_FILE_MODE
1151
+ );
1152
+ try {
1153
+ const current = fstatSync(fd);
1154
+ if (!current.isFile()) throw new Error(`Refusing non-regular sensitive file: ${path3}`);
1155
+ assertOwned(current.uid, path3);
1156
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
1157
+ writeFileSync(fd, content, "utf8");
1158
+ } finally {
1159
+ closeSync(fd);
1160
+ }
1161
+ }
1162
+
920
1163
  // src/conductor/transcript-store/store.ts
921
1164
  function memoryBackend() {
922
1165
  const files = /* @__PURE__ */ new Map();
@@ -944,24 +1187,15 @@ function fsBackend(dir) {
944
1187
  return {
945
1188
  locate: (sessionId) => `${dir}/${sessionId}.ndjson`,
946
1189
  read: async (sessionId) => {
947
- const fs = await import("node:fs/promises");
948
- try {
949
- return await fs.readFile(await fileOf(sessionId), "utf8");
950
- } catch {
951
- return null;
952
- }
1190
+ return readPrivateFileSync(await fileOf(sessionId));
953
1191
  },
954
1192
  append: async (sessionId, line) => {
955
- const fs = await import("node:fs/promises");
956
- await fs.mkdir(dir, { recursive: true });
957
- await fs.appendFile(await fileOf(sessionId), `${line}
958
- `, "utf8");
1193
+ appendPrivateFileSync(await fileOf(sessionId), `${line}
1194
+ `);
959
1195
  },
960
1196
  rewrite: async (sessionId, text) => {
961
- const fs = await import("node:fs/promises");
962
- await fs.mkdir(dir, { recursive: true });
963
- await fs.writeFile(await fileOf(sessionId), text.length === 0 ? "" : `${text}
964
- `, "utf8");
1197
+ atomicWritePrivateFileSync(await fileOf(sessionId), text.length === 0 ? "" : `${text}
1198
+ `);
965
1199
  }
966
1200
  };
967
1201
  }
@@ -1479,7 +1713,7 @@ async function loadSpawn() {
1479
1713
  return void 0;
1480
1714
  }
1481
1715
  }
1482
- function captureOutput(spawn2, bin, args, cwd, signal) {
1716
+ function captureOutput(spawn3, bin, args, cwd, signal) {
1483
1717
  return new Promise((resolve3) => {
1484
1718
  let settled = false;
1485
1719
  const finish = (value) => {
@@ -1490,7 +1724,7 @@ function captureOutput(spawn2, bin, args, cwd, signal) {
1490
1724
  };
1491
1725
  let child;
1492
1726
  try {
1493
- child = spawn2(bin, args, { cwd, shell: false });
1727
+ child = spawn3(bin, args, { cwd, shell: false });
1494
1728
  } catch {
1495
1729
  finish();
1496
1730
  return;
@@ -1519,9 +1753,9 @@ function captureOutput(spawn2, bin, args, cwd, signal) {
1519
1753
  });
1520
1754
  }
1521
1755
  async function runTscDiagnostics(cwd, signal) {
1522
- const spawn2 = await loadSpawn();
1523
- if (!spawn2 || signal?.aborted) return [];
1524
- const output = await captureOutput(spawn2, path.join(cwd, "node_modules", ".bin", "tsc"), ["--noEmit", "--pretty", "false"], cwd, signal);
1756
+ const spawn3 = await loadSpawn();
1757
+ if (!spawn3 || signal?.aborted) return [];
1758
+ const output = await captureOutput(spawn3, path.join(cwd, "node_modules", ".bin", "tsc"), ["--noEmit", "--pretty", "false"], cwd, signal);
1525
1759
  return output === void 0 ? [] : parseTscOutput(output, cwd);
1526
1760
  }
1527
1761
  function parseTscOutput(output, cwd) {
@@ -1541,9 +1775,9 @@ function parseTscOutput(output, cwd) {
1541
1775
  }
1542
1776
  async function runEslintDiagnostics(files, cwd, signal) {
1543
1777
  if (files.length === 0) return [];
1544
- const spawn2 = await loadSpawn();
1545
- if (!spawn2 || signal?.aborted) return [];
1546
- const output = await captureOutput(spawn2, path.join(cwd, "node_modules", ".bin", "eslint"), ["--format", "json", ...files], cwd, signal);
1778
+ const spawn3 = await loadSpawn();
1779
+ if (!spawn3 || signal?.aborted) return [];
1780
+ const output = await captureOutput(spawn3, path.join(cwd, "node_modules", ".bin", "eslint"), ["--format", "json", ...files], cwd, signal);
1547
1781
  return output === void 0 ? [] : parseEslintOutput(output, cwd);
1548
1782
  }
1549
1783
  function parseEslintOutput(output, cwd) {
@@ -1808,6 +2042,8 @@ var SessionConductorImpl = class {
1808
2042
  #matcher;
1809
2043
  #state;
1810
2044
  #busy = false;
2045
+ #activeWork;
2046
+ #shellAbort;
1811
2047
  #draining = false;
1812
2048
  #queue = [];
1813
2049
  #boundModel;
@@ -1867,6 +2103,7 @@ var SessionConductorImpl = class {
1867
2103
  abort() {
1868
2104
  this.clearQueue();
1869
2105
  this.#condenseAbort?.abort();
2106
+ this.#shellAbort?.abort();
1870
2107
  this.agent.abort();
1871
2108
  const fault = conductorFault("aborted", "turn aborted by caller");
1872
2109
  this.#dispatch({ type: "fault", fault });
@@ -1879,13 +2116,22 @@ var SessionConductorImpl = class {
1879
2116
  return this.#state;
1880
2117
  }
1881
2118
  this.#busy = true;
1882
- try {
1883
- await this.#runTurn(input);
1884
- await this.#drainQueue();
1885
- return this.#state;
1886
- } finally {
1887
- this.#busy = false;
1888
- }
2119
+ const work = (async () => {
2120
+ try {
2121
+ await this.#runTurn(input);
2122
+ await this.#drainQueue();
2123
+ return this.#state;
2124
+ } finally {
2125
+ this.#busy = false;
2126
+ }
2127
+ })();
2128
+ this.#activeWork = work;
2129
+ return work;
2130
+ }
2131
+ async #stopWork() {
2132
+ if (!this.#busy) return;
2133
+ this.abort();
2134
+ await this.#activeWork;
1889
2135
  }
1890
2136
  async #drainQueue() {
1891
2137
  if (this.#draining) return;
@@ -1974,8 +2220,16 @@ var SessionConductorImpl = class {
1974
2220
  }
1975
2221
  this.#applyModel(card);
1976
2222
  }
1977
- cycleModel(id) {
1978
- this.selectModel(id);
2223
+ cycleModel(id, direction = 1) {
2224
+ if (id.trim()) {
2225
+ this.selectModel(id);
2226
+ return;
2227
+ }
2228
+ const cards = this.availableModels();
2229
+ if (!cards.length) return;
2230
+ const at = cards.findIndex((card) => card.id === this.#state.modelId);
2231
+ const index = at < 0 ? 0 : (at + direction + cards.length) % cards.length;
2232
+ this.selectModel(cards[index].id);
1979
2233
  }
1980
2234
  #applyModel(card) {
1981
2235
  const model = withGatewayBaseUrl(card.model, this.#gatewayBaseUrls);
@@ -1990,28 +2244,36 @@ var SessionConductorImpl = class {
1990
2244
  registerTools(tools) {
1991
2245
  this.agent.setTools([...this.#deckTools, ...tools]);
1992
2246
  }
1993
- async condense() {
2247
+ async condense(instructions) {
1994
2248
  if (this.#busy) return "busy";
1995
2249
  this.#busy = true;
1996
2250
  this.#condenseAbort = new AbortController();
1997
- try {
1998
- const result = await this.#runCondense(true, this.#condenseAbort.signal);
1999
- await this.#drainQueue();
2000
- return result;
2001
- } finally {
2002
- this.#busy = false;
2003
- this.#condenseAbort = void 0;
2004
- }
2251
+ const work = (async () => {
2252
+ try {
2253
+ const result = await this.#runCondense(true, this.#condenseAbort.signal, instructions);
2254
+ await this.#drainQueue();
2255
+ return result;
2256
+ } finally {
2257
+ this.#busy = false;
2258
+ this.#condenseAbort = void 0;
2259
+ }
2260
+ })();
2261
+ this.#activeWork = work;
2262
+ return work;
2005
2263
  }
2006
2264
  cancelCondense() {
2007
2265
  this.#condenseAbort?.abort();
2008
2266
  }
2009
- async #runCondense(force, signal) {
2267
+ async #runCondense(force, signal, instructions) {
2010
2268
  const before = [...this.agent.state.messages];
2011
2269
  this.#dispatch({ type: "phase", phase: "condensing" });
2012
2270
  try {
2013
- const after = await (this.deps.condense ?? noopCondense)(before, { force, model: this.#boundModel, contextTokens: this.#state.contextTokens, signal });
2014
- if (signal?.aborted) return "cancelled";
2271
+ const after = await (this.deps.condense ?? noopCondense)(before, { force, model: this.#boundModel, contextTokens: this.#state.contextTokens, signal, instructions });
2272
+ if (signal?.aborted) {
2273
+ this.#dispatch({ type: "settled" });
2274
+ this.#hub.emit({ kind: "idle" });
2275
+ return "cancelled";
2276
+ }
2015
2277
  if (after.length >= before.length) {
2016
2278
  this.#dispatch({ type: "settled" });
2017
2279
  return "nothing";
@@ -2038,10 +2300,12 @@ var SessionConductorImpl = class {
2038
2300
  }
2039
2301
  async #move(id, message) {
2040
2302
  try {
2303
+ await this.#stopWork();
2041
2304
  await this.store.branchAt(id);
2042
2305
  this.agent.replaceMessages(this.store.messages());
2043
2306
  this.#dispatch({ type: "head", head: this.store.head });
2044
2307
  this.#dispatch({ type: "settled" });
2308
+ this.#hub.emit({ kind: "reset" });
2045
2309
  this.#hub.emit({ kind: "idle" });
2046
2310
  } catch (error) {
2047
2311
  const fault = conductorFault("persistence", message, error);
@@ -2056,18 +2320,33 @@ var SessionConductorImpl = class {
2056
2320
  return { canRestore: this.options.checkpoint?.hasSnapshot(id) ?? false };
2057
2321
  }
2058
2322
  async executeBash(command, opts = {}) {
2059
- const { spawn: spawn2 } = await import("node:child_process");
2060
- const outcome = await new Promise((resolve3) => {
2061
- const child = spawn2(command, { cwd: this.#workspace, shell: true });
2062
- let output = "";
2063
- child.stdout?.on("data", (chunk) => output += String(chunk));
2064
- child.stderr?.on("data", (chunk) => output += String(chunk));
2065
- child.on("error", (error) => resolve3({ output: output + errorText(error), exitCode: 1 }));
2066
- child.on("close", (code) => resolve3({ output, exitCode: code ?? 1 }));
2067
- });
2068
- if (!opts.excludeFromContext) await this.store.append({ role: "user", content: `$ ${command}
2069
- ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
2070
- return outcome;
2323
+ if (this.#busy) throw new Error("A turn is running \u2014 stop it before running a shell command.");
2324
+ this.#busy = true;
2325
+ this.#shellAbort = new AbortController();
2326
+ const signal = opts.signal ? AbortSignal.any([opts.signal, this.#shellAbort.signal]) : this.#shellAbort.signal;
2327
+ const work = (async () => {
2328
+ try {
2329
+ this.#dispatch({ type: "phase", phase: "tooling" });
2330
+ const outcome = await runShell(command, this.#workspace, signal);
2331
+ if (!opts.excludeFromContext) {
2332
+ const message = { role: "user", content: `$ ${command}
2333
+ ${outcome.output}`.trimEnd(), timestamp: Date.now() };
2334
+ const entry = await this.store.append(message, "note");
2335
+ this.agent.replaceMessages([...this.agent.state.messages, message]);
2336
+ this.#dispatch({ type: "head", head: this.store.head });
2337
+ this.#hub.emit({ kind: "persisted", entryId: entry.id });
2338
+ }
2339
+ this.#dispatch({ type: "settled" });
2340
+ this.#hub.emit({ kind: "idle" });
2341
+ await this.#drainQueue();
2342
+ return outcome;
2343
+ } finally {
2344
+ this.#busy = false;
2345
+ this.#shellAbort = void 0;
2346
+ }
2347
+ })();
2348
+ this.#activeWork = work;
2349
+ return work;
2071
2350
  }
2072
2351
  stats() {
2073
2352
  const messages = this.agent.state.messages;
@@ -2137,6 +2416,7 @@ ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
2137
2416
  this.#sessionName = name;
2138
2417
  }
2139
2418
  async resume(id) {
2419
+ await this.#stopWork();
2140
2420
  if (!await this.store.load(id)) {
2141
2421
  const fault = conductorFault("persistence", `no session "${id}" to resume`);
2142
2422
  this.#dispatch({ type: "fault", fault });
@@ -2145,19 +2425,23 @@ ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
2145
2425
  }
2146
2426
  this.agent.replaceMessages(this.store.messages());
2147
2427
  this.#state = { ...this.#state, head: this.store.head, usage: this.store.usage ?? createZeroUsage2(), phase: "idle" };
2428
+ this.#hub.emit({ kind: "reset" });
2148
2429
  this.#hub.emit({ kind: "idle" });
2149
2430
  }
2150
2431
  async newSession() {
2151
- this.agent.abort();
2432
+ await this.#stopWork();
2152
2433
  this.agent.replaceMessages([]);
2153
2434
  this.store.startNewSession(newSessionId());
2154
2435
  this.#queue.length = 0;
2155
2436
  this.#sessionName = void 0;
2156
2437
  this.#state = { phase: "idle", head: this.store.head, usage: createZeroUsage2(), contextTokens: 0, modelId: this.#state.modelId };
2438
+ this.#hub.emit({ kind: "reset" });
2157
2439
  this.#hub.emit({ kind: "idle" });
2158
2440
  }
2159
2441
  #dispatch(action) {
2442
+ const phase = this.#state.phase;
2160
2443
  this.#state = reduceState(this.#state, action);
2444
+ if (phase !== this.#state.phase) this.#hub.emit({ kind: "phase", phase: this.#state.phase });
2161
2445
  }
2162
2446
  };
2163
2447
 
@@ -2260,7 +2544,7 @@ var todoCard = {
2260
2544
  };
2261
2545
 
2262
2546
  // src/capability-deck/cards/bg-process-card.ts
2263
- import { spawn } from "node:child_process";
2547
+ import { spawn as spawn2 } from "node:child_process";
2264
2548
  import { Type as Type2 } from "@sinclair/typebox";
2265
2549
  var MAX_BUFFERED_LINES = 2e3;
2266
2550
  var DaemonTable = class {
@@ -2272,7 +2556,7 @@ var DaemonTable = class {
2272
2556
  }
2273
2557
  start(command) {
2274
2558
  const id = `bg-${this.nextSeq++}`;
2275
- const child = spawn(command, {
2559
+ const child = spawn2(command, {
2276
2560
  cwd: this.cwd,
2277
2561
  shell: true,
2278
2562
  stdio: ["ignore", "pipe", "pipe"],