@wrongstack/tools 0.5.5 → 0.5.7

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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs4 from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { dirname } from 'path';
4
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, removePlanItem, setPlanItemStatus, addPlanItem, formatPlan } from '@wrongstack/core';
4
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
5
5
  import { spawn } from 'child_process';
6
6
  import * as os from 'os';
7
7
  import * as dns from 'dns/promises';
@@ -854,6 +854,326 @@ async function runNative(input, base, mode, limit, signal) {
854
854
  };
855
855
  }
856
856
 
857
+ // src/circuit-breaker.ts
858
+ var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
859
+ var DEFAULT_SLOW_CALL_THRESHOLD_MS = 6e4;
860
+ var DEFAULT_MAX_SLOW_CALLS = 3;
861
+ var DEFAULT_WINDOW_MS = 6e4;
862
+ var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
863
+ var DEFAULT_COOLDOWN_MS = 3e4;
864
+ var CircuitBreaker = class {
865
+ maxConsecutiveFailures;
866
+ slowCallThresholdMs;
867
+ maxSlowCalls;
868
+ windowMs;
869
+ maxCallsPerWindow;
870
+ cooldownMs;
871
+ state = "closed";
872
+ consecutiveFailures = 0;
873
+ window = [];
874
+ lastFailureAt = null;
875
+ lastSlowAt = null;
876
+ /** Timestamp when the breaker was opened (for cooldown calculation). */
877
+ openedAt = null;
878
+ /** Timestamp when the last call ran (for half-open gate). */
879
+ lastCallAt = null;
880
+ constructor(config = {}) {
881
+ this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
882
+ this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
883
+ this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
884
+ this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
885
+ this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
886
+ this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
887
+ }
888
+ /**
889
+ * Returns true if the circuit allows a new call to proceed.
890
+ * When false, callers should abort the tool call and return a
891
+ * circuit-breaker error instead of spawning a process.
892
+ */
893
+ get canProceed() {
894
+ this._checkStateTransition();
895
+ return this.state !== "open";
896
+ }
897
+ /**
898
+ * Snapshot of the current breaker state for observability (`/kill`).
899
+ */
900
+ snapshot() {
901
+ this._checkStateTransition();
902
+ const now = Date.now();
903
+ let cooldownRemaining = null;
904
+ if (this.openedAt !== null && this.state === "open") {
905
+ const elapsed = now - this.openedAt;
906
+ cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
907
+ }
908
+ return {
909
+ state: this.state,
910
+ consecutiveFailures: this.consecutiveFailures,
911
+ slowCallsInWindow: this.window.filter((c) => c.slow).length,
912
+ callsInWindow: this.window.length,
913
+ windowMs: this.windowMs,
914
+ cooldownRemainingMs: cooldownRemaining,
915
+ lastFailureAt: this.lastFailureAt,
916
+ lastSlowAt: this.lastSlowAt
917
+ };
918
+ }
919
+ /**
920
+ * Call this BEFORE spawning a bash/exec process.
921
+ * Returns true if the call is allowed; false if the breaker is open.
922
+ * When false, callers MUST NOT spawn a process.
923
+ */
924
+ beforeCall() {
925
+ this._checkStateTransition();
926
+ if (this.state === "open") return false;
927
+ return true;
928
+ }
929
+ /**
930
+ * Call this AFTER a bash/exec process finishes (success or failure).
931
+ * `durationMs` is the wall-clock time the process ran.
932
+ * `failed` is true when the process returned a non-zero exit code or
933
+ * threw an exception before spawning.
934
+ */
935
+ afterCall(durationMs, failed) {
936
+ const now = Date.now();
937
+ this.lastCallAt = now;
938
+ if (this.state === "half-open") {
939
+ if (failed) {
940
+ this._trip();
941
+ return;
942
+ }
943
+ this._reset();
944
+ return;
945
+ }
946
+ this._pruneWindow(now);
947
+ const slow = durationMs >= this.slowCallThresholdMs;
948
+ this.window.push({ at: now, failed, slow });
949
+ if (failed) {
950
+ this.consecutiveFailures++;
951
+ this.lastFailureAt = now;
952
+ if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
953
+ this._trip();
954
+ }
955
+ return;
956
+ }
957
+ this.consecutiveFailures = 0;
958
+ if (slow) {
959
+ this.lastSlowAt = now;
960
+ const slowCount = this.window.filter((c) => c.slow).length;
961
+ if (slowCount >= this.maxSlowCalls) {
962
+ this._trip();
963
+ }
964
+ }
965
+ const callCount = this.window.length;
966
+ if (callCount >= this.maxCallsPerWindow) {
967
+ this._trip();
968
+ }
969
+ }
970
+ /** Force the breaker open. Used by /kill force and Ctrl+C. */
971
+ forceOpen() {
972
+ this._trip();
973
+ }
974
+ /** Force a reset to closed. Used by tests and /kill reset. */
975
+ forceReset() {
976
+ this._reset();
977
+ }
978
+ _trip() {
979
+ if (this.state === "open") return;
980
+ this.state = "open";
981
+ this.openedAt = Date.now();
982
+ }
983
+ _reset() {
984
+ this.state = "closed";
985
+ this.consecutiveFailures = 0;
986
+ this.window = [];
987
+ this.openedAt = null;
988
+ }
989
+ /** Transition from open → half-open when cooldown elapses. */
990
+ _checkStateTransition() {
991
+ if (this.state !== "open" || this.openedAt === null) return;
992
+ const elapsed = Date.now() - this.openedAt;
993
+ if (elapsed >= this.cooldownMs) {
994
+ this.state = "half-open";
995
+ this.openedAt = null;
996
+ }
997
+ }
998
+ _pruneWindow(now) {
999
+ const cutoff = now - this.windowMs;
1000
+ this.window = this.window.filter((c) => c.at >= cutoff);
1001
+ }
1002
+ };
1003
+
1004
+ // src/process-registry.ts
1005
+ var DEFAULT_GRACE_MS = 2e3;
1006
+ var ProcessRegistryImpl = class {
1007
+ processes = /* @__PURE__ */ new Map();
1008
+ breaker;
1009
+ constructor(breakerConfig) {
1010
+ this.breaker = new CircuitBreaker(breakerConfig);
1011
+ }
1012
+ register(info) {
1013
+ this.processes.set(info.pid, { ...info, killed: false });
1014
+ }
1015
+ /** Unregister a process by PID. Called on 'close' / 'exit' events. */
1016
+ unregister(pid) {
1017
+ this.processes.delete(pid);
1018
+ }
1019
+ /** Get a single process by PID. */
1020
+ get(pid) {
1021
+ return this.processes.get(pid);
1022
+ }
1023
+ /** Get all tracked processes. */
1024
+ list() {
1025
+ return Array.from(this.processes.values());
1026
+ }
1027
+ /** Get processes filtered by name (e.g. 'bash', 'exec'). */
1028
+ byName(name) {
1029
+ return this.list().filter((p) => p.name === name);
1030
+ }
1031
+ /** Get processes filtered by session. */
1032
+ bySession(sessionId) {
1033
+ return this.list().filter((p) => p.sessionId === sessionId);
1034
+ }
1035
+ /** Count of active (non-killed) processes. */
1036
+ get activeCount() {
1037
+ let n = 0;
1038
+ for (const p of this.processes.values()) {
1039
+ if (!p.killed) n++;
1040
+ }
1041
+ return n;
1042
+ }
1043
+ /**
1044
+ * Combined stats for observability — used by /ps and the TUI status bar.
1045
+ */
1046
+ stats() {
1047
+ return {
1048
+ activeCount: this.activeCount,
1049
+ totalCount: this.processes.size,
1050
+ breaker: this.breaker.snapshot()
1051
+ };
1052
+ }
1053
+ /**
1054
+ * Returns true if the circuit allows a new bash/exec call to proceed.
1055
+ * When false, callers MUST NOT spawn a process.
1056
+ */
1057
+ get canProceed() {
1058
+ return this.breaker.canProceed;
1059
+ }
1060
+ /**
1061
+ * Called before spawning a process. Returns true if allowed; false if
1062
+ * the circuit breaker is open.
1063
+ */
1064
+ beforeCall() {
1065
+ return this.breaker.beforeCall();
1066
+ }
1067
+ /**
1068
+ * Called after a process finishes. `durationMs` is wall-clock time;
1069
+ * `failed` is true for non-zero exit codes.
1070
+ */
1071
+ afterCall(durationMs, failed) {
1072
+ this.breaker.afterCall(durationMs, failed);
1073
+ }
1074
+ /** Force-open the circuit breaker (Ctrl+C, /kill force). */
1075
+ forceBreakerOpen() {
1076
+ this.breaker.forceOpen();
1077
+ }
1078
+ /** Force-reset the circuit breaker to closed (/kill reset). */
1079
+ forceBreakerReset() {
1080
+ this.breaker.forceReset();
1081
+ }
1082
+ /** Kill a single process by PID.
1083
+ *
1084
+ * On POSIX: sends SIGTERM to the *process group* (-pid) so that
1085
+ * runaway grandchild processes (`sleep 9999 & disown`) are also killed.
1086
+ * After `graceMs` a SIGKILL is sent if the process hasn't exited.
1087
+ *
1088
+ * On Windows: `child.kill()` maps to TerminateProcess — process groups
1089
+ * are not meaningfully supported. A second `force=true` call sends
1090
+ * SIGKILL (which maps to TerminateProcess again — the distinction is
1091
+ * in the exit code, not the signal).
1092
+ *
1093
+ * Returns true if the process was found and kill was attempted.
1094
+ */
1095
+ kill(pid, opts = {}) {
1096
+ const p = this.processes.get(pid);
1097
+ if (!p) return false;
1098
+ if (p.killed) return true;
1099
+ const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1100
+ const isWin = os.platform() === "win32";
1101
+ if (isWin) {
1102
+ try {
1103
+ p.child.kill(force ? "SIGKILL" : "SIGTERM");
1104
+ } catch {
1105
+ }
1106
+ p.killed = true;
1107
+ return true;
1108
+ }
1109
+ try {
1110
+ if (force) {
1111
+ try {
1112
+ process.kill(-pid, "SIGKILL");
1113
+ } catch {
1114
+ p.child.kill("SIGKILL");
1115
+ }
1116
+ } else {
1117
+ try {
1118
+ process.kill(-pid, "SIGTERM");
1119
+ } catch {
1120
+ p.child.kill("SIGTERM");
1121
+ }
1122
+ const timer = setTimeout(() => {
1123
+ if (this.processes.has(pid) && !p.child.killed) {
1124
+ try {
1125
+ process.kill(-pid, "SIGKILL");
1126
+ } catch {
1127
+ try {
1128
+ p.child.kill("SIGKILL");
1129
+ } catch {
1130
+ }
1131
+ }
1132
+ }
1133
+ }, graceMs);
1134
+ timer.unref?.();
1135
+ }
1136
+ } catch {
1137
+ }
1138
+ p.killed = true;
1139
+ return true;
1140
+ }
1141
+ /**
1142
+ * Kill all tracked processes.
1143
+ * Returns the PIDs that were kill()ed.
1144
+ */
1145
+ killAll(opts = {}) {
1146
+ const pids = Array.from(this.processes.keys());
1147
+ const killed = [];
1148
+ for (const pid of pids) {
1149
+ if (this.kill(pid, opts)) killed.push(pid);
1150
+ }
1151
+ return killed;
1152
+ }
1153
+ /**
1154
+ * Kill all processes for a specific session.
1155
+ * Returns the PIDs that were kill()ed.
1156
+ */
1157
+ killSession(sessionId, opts = {}) {
1158
+ const pids = this.bySession(sessionId).map((p) => p.pid);
1159
+ const killed = [];
1160
+ for (const pid of pids) {
1161
+ if (this.kill(pid, opts)) killed.push(pid);
1162
+ }
1163
+ return killed;
1164
+ }
1165
+ };
1166
+ var _registry;
1167
+ function getProcessRegistry() {
1168
+ if (!_registry) {
1169
+ _registry = new ProcessRegistryImpl();
1170
+ }
1171
+ return _registry;
1172
+ }
1173
+ function _resetProcessRegistry() {
1174
+ _registry = void 0;
1175
+ }
1176
+
857
1177
  // src/bash.ts
858
1178
  var MAX_OUTPUT = 32768;
859
1179
  var DEFAULT_TIMEOUT = 3e4;
@@ -892,12 +1212,27 @@ var bashTool = {
892
1212
  },
893
1213
  async *executeStream(input, ctx, opts) {
894
1214
  if (!input?.command) throw new Error("bash: command is required");
1215
+ const registry = getProcessRegistry();
1216
+ if (!registry.beforeCall()) {
1217
+ yield {
1218
+ type: "final",
1219
+ output: {
1220
+ output: "",
1221
+ exit_code: 1,
1222
+ timed_out: false,
1223
+ pid: null,
1224
+ error: "bash: circuit breaker open \u2014 too many consecutive failures or slow calls. Use /kill to inspect or /kill reset to recover."
1225
+ }
1226
+ };
1227
+ return;
1228
+ }
895
1229
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 6e5));
896
1230
  const isWin = os.platform() === "win32";
897
1231
  const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
898
1232
  const args = isWin ? ["/c", input.command] : ["-c", input.command];
899
1233
  const env = buildChildEnv(ctx.session?.id);
900
1234
  const detached = isWin ? !!input.background : true;
1235
+ const startedAt = Date.now();
901
1236
  if (input.background) {
902
1237
  let buf2 = "";
903
1238
  let truncated = false;
@@ -908,7 +1243,18 @@ var bashTool = {
908
1243
  detached: true,
909
1244
  signal: opts.signal
910
1245
  });
911
- const pid = child2.pid;
1246
+ const pid2 = child2.pid;
1247
+ if (typeof pid2 === "number") {
1248
+ registry.register({
1249
+ pid: pid2,
1250
+ name: "bash",
1251
+ command: input.command,
1252
+ startedAt: Date.now(),
1253
+ sessionId: ctx.session?.id,
1254
+ child: child2
1255
+ });
1256
+ child2.on("close", () => registry.unregister(pid2));
1257
+ }
912
1258
  child2.stdout?.on("data", (chunk) => {
913
1259
  if (!truncated) {
914
1260
  const remain = MAX_OUTPUT - buf2.length;
@@ -928,15 +1274,16 @@ var bashTool = {
928
1274
  }
929
1275
  });
930
1276
  child2.on("close", () => {
1277
+ registry.afterCall(Date.now() - startedAt, false);
931
1278
  });
932
- if (typeof pid === "number") child2.unref();
1279
+ if (typeof pid2 === "number") child2.unref();
933
1280
  yield {
934
1281
  type: "final",
935
1282
  output: {
936
1283
  output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
937
1284
  exit_code: null,
938
1285
  timed_out: false,
939
- pid
1286
+ pid: pid2
940
1287
  }
941
1288
  };
942
1289
  return;
@@ -948,6 +1295,17 @@ var bashTool = {
948
1295
  detached,
949
1296
  signal: opts.signal
950
1297
  });
1298
+ const pid = child.pid;
1299
+ if (typeof pid === "number") {
1300
+ registry.register({
1301
+ pid,
1302
+ name: "bash",
1303
+ command: input.command,
1304
+ startedAt: Date.now(),
1305
+ sessionId: ctx.session?.id,
1306
+ child
1307
+ });
1308
+ }
951
1309
  let buf = "";
952
1310
  let pending = "";
953
1311
  let timedOut = false;
@@ -1030,10 +1388,13 @@ var bashTool = {
1030
1388
  });
1031
1389
  child.on("error", (err) => {
1032
1390
  for (const t of timers) clearTimeout(t);
1391
+ registry.afterCall(Date.now() - startedAt, true);
1033
1392
  push({ kind: "error", err });
1034
1393
  });
1035
1394
  child.on("close", (code) => {
1036
1395
  for (const t of timers) clearTimeout(t);
1396
+ if (typeof pid === "number") registry.unregister(pid);
1397
+ registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
1037
1398
  push({ kind: "end", code });
1038
1399
  });
1039
1400
  try {
@@ -1164,6 +1525,18 @@ var execTool = {
1164
1525
  required: ["command"]
1165
1526
  },
1166
1527
  async execute(input, ctx, opts) {
1528
+ const registry = getProcessRegistry();
1529
+ if (!registry.canProceed) {
1530
+ return {
1531
+ command: input.command,
1532
+ args: input.args ?? [],
1533
+ stdout: "",
1534
+ stderr: "Circuit breaker is open \u2014 too many consecutive failures. Use /kill reset to recover.",
1535
+ exitCode: 1,
1536
+ truncated: false,
1537
+ allowed: false
1538
+ };
1539
+ }
1167
1540
  const cmd = input.command.trim();
1168
1541
  if (!cmd)
1169
1542
  return {
@@ -1223,15 +1596,23 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1223
1596
  let stdout = "";
1224
1597
  let stderr = "";
1225
1598
  let killed = false;
1599
+ const startedAt = Date.now();
1226
1600
  const child = spawn(cmd, args, {
1227
1601
  cwd,
1228
1602
  signal,
1229
1603
  env: buildChildEnv(sessionId),
1230
1604
  stdio: ["ignore", "pipe", "pipe"]
1231
1605
  });
1606
+ const registry = getProcessRegistry();
1607
+ const pid = child.pid;
1608
+ if (typeof pid === "number") {
1609
+ const fullCommand = `${cmd} ${args.join(" ")}`;
1610
+ registry.register({ pid, name: "exec", command: fullCommand, startedAt: Date.now(), sessionId, child });
1611
+ }
1232
1612
  const timer = setTimeout(() => {
1233
1613
  killed = true;
1234
- child.kill("SIGTERM");
1614
+ if (typeof pid === "number") registry.kill(pid);
1615
+ else child.kill("SIGTERM");
1235
1616
  }, timeout);
1236
1617
  child.stdout?.on("data", (chunk) => {
1237
1618
  if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
@@ -1241,18 +1622,24 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1241
1622
  });
1242
1623
  child.on("close", (code) => {
1243
1624
  clearTimeout(timer);
1625
+ if (typeof pid === "number") registry.unregister(pid);
1626
+ const durationMs = Date.now() - startedAt;
1627
+ const exitCode = killed ? 124 : code ?? 1;
1628
+ registry.afterCall(durationMs, exitCode !== 0);
1244
1629
  resolve5({
1245
1630
  command: cmd,
1246
1631
  args,
1247
1632
  stdout: stdout.slice(0, MAX_OUTPUT2),
1248
1633
  stderr: stderr.slice(0, MAX_OUTPUT2),
1249
- exitCode: killed ? 124 : code ?? 1,
1634
+ exitCode,
1250
1635
  truncated: stdout.length >= MAX_OUTPUT2 || stderr.length >= MAX_OUTPUT2,
1251
1636
  allowed: true
1252
1637
  });
1253
1638
  });
1254
1639
  child.on("error", (err) => {
1255
1640
  clearTimeout(timer);
1641
+ if (typeof pid === "number") registry.unregister(pid);
1642
+ registry.afterCall(Date.now() - startedAt, true);
1256
1643
  resolve5({
1257
1644
  command: cmd,
1258
1645
  args,
@@ -1821,8 +2208,8 @@ var todoTool = {
1821
2208
  var planTool = {
1822
2209
  name: "plan",
1823
2210
  category: "Session",
1824
- description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds.",
1825
- usageHint: "Set action to one of: show | add | start | done | remove | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove. Always returns the formatted plan plus open/total counts.",
2211
+ description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds. Promote a plan item to todos to start working on it. Apply templates for common workflows.",
2212
+ usageHint: 'Set action to one of: show | add | start | done | remove | promote | derive | template_use | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove/promote/derive. Pass `subtasks` for promote/derive to break the plan item into multiple todos. Pass `template` (e.g. "new-feature", "bug-fix", "refactor", "release") for template_use. Always returns the formatted plan plus open/total counts.',
1826
2213
  permission: "auto",
1827
2214
  mutating: false,
1828
2215
  timeoutMs: 2e3,
@@ -1831,13 +2218,22 @@ var planTool = {
1831
2218
  properties: {
1832
2219
  action: {
1833
2220
  type: "string",
1834
- enum: ["show", "add", "start", "done", "remove", "clear"]
2221
+ enum: ["show", "add", "start", "done", "remove", "promote", "derive", "template_use", "clear"]
1835
2222
  },
1836
2223
  title: { type: "string", description: "Required when action = add." },
1837
2224
  details: { type: "string", description: "Optional extra context for add." },
1838
2225
  target: {
1839
2226
  type: "string",
1840
- description: "Plan item id, 1-based index, or title substring. Required for start/done/remove."
2227
+ description: "Plan item id, 1-based index, or title substring. Required for start/done/remove/promote/derive."
2228
+ },
2229
+ subtasks: {
2230
+ type: "array",
2231
+ items: { type: "string" },
2232
+ description: "Optional subtasks for promote/derive. If omitted, a single todo is created from the plan item title."
2233
+ },
2234
+ template: {
2235
+ type: "string",
2236
+ description: "Template name for template_use action. Available: new-feature, bug-fix, refactor, release, security-audit, onboarding."
1841
2237
  }
1842
2238
  },
1843
2239
  required: ["action"]
@@ -1896,6 +2292,35 @@ var planTool = {
1896
2292
  await savePlan(planPath, plan);
1897
2293
  break;
1898
2294
  }
2295
+ case "promote":
2296
+ case "derive": {
2297
+ if (!input.target) {
2298
+ return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
2299
+ }
2300
+ const derived = deriveTodosFromPlanItem(plan, input.target, input.subtasks);
2301
+ if (!derived) {
2302
+ return mkResult(plan, false, `No plan item matched "${input.target}".`);
2303
+ }
2304
+ plan = derived.plan;
2305
+ await savePlan(planPath, plan);
2306
+ ctx.state.replaceTodos(derived.todos);
2307
+ return mkResult(plan, true, `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`, derived.todos);
2308
+ }
2309
+ case "template_use": {
2310
+ const templateName = input.template?.trim();
2311
+ if (!templateName) {
2312
+ return mkResult(plan, false, "template_use requires `template` name.");
2313
+ }
2314
+ const template = getPlanTemplate(templateName);
2315
+ if (!template) {
2316
+ return mkResult(plan, false, `Unknown template "${templateName}".`);
2317
+ }
2318
+ for (const item of template.items) {
2319
+ ({ plan } = addPlanItem(plan, item.title, item.details));
2320
+ }
2321
+ await savePlan(planPath, plan);
2322
+ return mkResult(plan, true, `Applied template "${template.name}" \u2014 ${template.items.length} items added.`);
2323
+ }
1899
2324
  case "clear":
1900
2325
  plan = clearPlan(plan);
1901
2326
  await savePlan(planPath, plan);
@@ -1906,14 +2331,15 @@ var planTool = {
1906
2331
  return mkResult(plan, true, `Plan ${input.action} ok.`);
1907
2332
  }
1908
2333
  };
1909
- function mkResult(plan, ok, message) {
2334
+ function mkResult(plan, ok, message, todos) {
1910
2335
  const open = plan.items.filter((i) => i.status !== "done").length;
1911
2336
  return {
1912
2337
  ok,
1913
2338
  message,
1914
2339
  plan: formatPlan(plan),
1915
2340
  count: plan.items.length,
1916
- open
2341
+ open,
2342
+ todos
1917
2343
  };
1918
2344
  }
1919
2345
  var TIMEOUT_MS4 = 3e4;
@@ -4486,6 +4912,6 @@ var builtinToolsPack = {
4486
4912
  tools: builtinTools
4487
4913
  };
4488
4914
 
4489
- export { auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
4915
+ export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, getProcessRegistry, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
4490
4916
  //# sourceMappingURL=index.js.map
4491
4917
  //# sourceMappingURL=index.js.map