@wrongstack/tools 0.155.0 → 0.236.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/audit.js +21 -1
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +116 -24
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +673 -202
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/circuit-breaker.d.ts +9 -2
  8. package/dist/circuit-breaker.js +11 -2
  9. package/dist/circuit-breaker.js.map +1 -1
  10. package/dist/codebase-index/index.js +19 -10
  11. package/dist/codebase-index/index.js.map +1 -1
  12. package/dist/diff.js +1 -1
  13. package/dist/diff.js.map +1 -1
  14. package/dist/document.js +1 -1
  15. package/dist/document.js.map +1 -1
  16. package/dist/edit.js +1 -1
  17. package/dist/edit.js.map +1 -1
  18. package/dist/exec.js +60 -11
  19. package/dist/exec.js.map +1 -1
  20. package/dist/fetch.js.map +1 -1
  21. package/dist/format.js +21 -1
  22. package/dist/format.js.map +1 -1
  23. package/dist/git.js.map +1 -1
  24. package/dist/glob.js +1 -1
  25. package/dist/glob.js.map +1 -1
  26. package/dist/grep.js +1 -1
  27. package/dist/grep.js.map +1 -1
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +680 -209
  30. package/dist/index.js.map +1 -1
  31. package/dist/install.js +65 -14
  32. package/dist/install.js.map +1 -1
  33. package/dist/lint.js +21 -1
  34. package/dist/lint.js.map +1 -1
  35. package/dist/logs.js +1 -1
  36. package/dist/logs.js.map +1 -1
  37. package/dist/outdated.js +1 -1
  38. package/dist/outdated.js.map +1 -1
  39. package/dist/pack.js +673 -202
  40. package/dist/pack.js.map +1 -1
  41. package/dist/patch.js +1 -1
  42. package/dist/patch.js.map +1 -1
  43. package/dist/process-registry.d.ts +21 -16
  44. package/dist/process-registry.js +48 -10
  45. package/dist/process-registry.js.map +1 -1
  46. package/dist/read.js +1 -1
  47. package/dist/read.js.map +1 -1
  48. package/dist/replace.js +1 -1
  49. package/dist/replace.js.map +1 -1
  50. package/dist/scaffold.js +1 -1
  51. package/dist/scaffold.js.map +1 -1
  52. package/dist/search.js +19 -16
  53. package/dist/search.js.map +1 -1
  54. package/dist/test.js +21 -1
  55. package/dist/test.js.map +1 -1
  56. package/dist/todo.js +44 -0
  57. package/dist/todo.js.map +1 -1
  58. package/dist/tree.js +1 -1
  59. package/dist/tree.js.map +1 -1
  60. package/dist/typecheck.js +21 -1
  61. package/dist/typecheck.js.map +1 -1
  62. package/dist/write.js +1 -1
  63. package/dist/write.js.map +1 -1
  64. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs4 from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { resolve, sep, dirname } from 'node:path';
3
+ import { resolve, sep, dirname, join } from 'node:path';
4
4
  import * as Core from '@wrongstack/core';
5
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, loadTasks, emptyTaskFile, saveTasks, computeTaskItemProgress, formatTaskList, resolveWstackPaths } from '@wrongstack/core';
5
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
6
6
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
7
7
  import * as os from 'node:os';
8
8
  import * as fs7 from 'node:fs';
@@ -12,6 +12,7 @@ import * as net from 'node:net';
12
12
  import { Agent } from 'undici';
13
13
  import { createRequire } from 'node:module';
14
14
  import * as ts from 'typescript';
15
+ import { randomUUID } from 'node:crypto';
15
16
 
16
17
  // src/read.ts
17
18
  async function detectPackageManager(cwd) {
@@ -29,7 +30,7 @@ async function detectPackageManager(cwd) {
29
30
  return "npm";
30
31
  }
31
32
  function resolvePath(input, ctx) {
32
- return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
33
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
33
34
  }
34
35
  function ensureInsideRoot(absPath, ctx) {
35
36
  const root = path.resolve(ctx.projectRoot);
@@ -1098,8 +1099,13 @@ var CircuitBreaker = class {
1098
1099
  * Call this BEFORE spawning a bash/exec process.
1099
1100
  * Returns true if the call is allowed; false if the breaker is open.
1100
1101
  * When false, callers MUST NOT spawn a process.
1102
+ *
1103
+ * @param bypass - If true, skip the circuit breaker check entirely.
1104
+ * Use for background/fire-and-forget processes that should
1105
+ * not affect breaker state.
1101
1106
  */
1102
- beforeCall() {
1107
+ beforeCall(bypass = false) {
1108
+ if (bypass) return true;
1103
1109
  this._checkStateTransition();
1104
1110
  if (this.state === "open") return false;
1105
1111
  return true;
@@ -1109,8 +1115,12 @@ var CircuitBreaker = class {
1109
1115
  * `durationMs` is the wall-clock time the process ran.
1110
1116
  * `failed` is true when the process returned a non-zero exit code or
1111
1117
  * threw an exception before spawning.
1118
+ *
1119
+ * @param bypass - If true, do not update breaker state.
1120
+ * Use for background/fire-and-forget processes.
1112
1121
  */
1113
- afterCall(durationMs, failed) {
1122
+ afterCall(durationMs, failed, bypass = false) {
1123
+ if (bypass) return;
1114
1124
  const now = Date.now();
1115
1125
  if (this.state === "half-open") {
1116
1126
  if (failed) {
@@ -1209,6 +1219,17 @@ function redactCommand(cmd) {
1209
1219
  return result;
1210
1220
  }
1211
1221
  var DEFAULT_GRACE_MS = 2e3;
1222
+ function killWin32Tree(pid) {
1223
+ try {
1224
+ spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
1225
+ stdio: "ignore",
1226
+ windowsHide: true
1227
+ }).unref();
1228
+ return true;
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ }
1212
1233
  var ProcessRegistryImpl = class {
1213
1234
  processes = /* @__PURE__ */ new Map();
1214
1235
  breaker;
@@ -1266,16 +1287,20 @@ var ProcessRegistryImpl = class {
1266
1287
  /**
1267
1288
  * Called before spawning a process. Returns true if allowed; false if
1268
1289
  * the circuit breaker is open.
1290
+ *
1291
+ * @param bypass - If true, skip circuit breaker check (for background processes).
1269
1292
  */
1270
- beforeCall() {
1271
- return this.breaker.beforeCall();
1293
+ beforeCall(bypass = false) {
1294
+ return this.breaker.beforeCall(bypass);
1272
1295
  }
1273
1296
  /**
1274
1297
  * Called after a process finishes. `durationMs` is wall-clock time;
1275
1298
  * `failed` is true for non-zero exit codes.
1299
+ *
1300
+ * @param bypass - If true, do not update circuit breaker state (for background processes).
1276
1301
  */
1277
- afterCall(durationMs, failed) {
1278
- this.breaker.afterCall(durationMs, failed);
1302
+ afterCall(durationMs, failed, bypass = false) {
1303
+ this.breaker.afterCall(durationMs, failed, bypass);
1279
1304
  }
1280
1305
  /** Force-open the circuit breaker (Ctrl+C, /kill force). */
1281
1306
  forceBreakerOpen() {
@@ -1306,9 +1331,22 @@ var ProcessRegistryImpl = class {
1306
1331
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1307
1332
  const isWin = os.platform() === "win32";
1308
1333
  if (isWin) {
1309
- try {
1310
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
1311
- } catch {
1334
+ const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
1335
+ if (liveRealChild && killWin32Tree(pid)) {
1336
+ const fallback = setTimeout(() => {
1337
+ if (p.child.exitCode === null) {
1338
+ try {
1339
+ p.child.kill("SIGKILL");
1340
+ } catch {
1341
+ }
1342
+ }
1343
+ }, graceMs);
1344
+ fallback.unref?.();
1345
+ } else {
1346
+ try {
1347
+ p.child.kill(force ? "SIGKILL" : "SIGTERM");
1348
+ } catch {
1349
+ }
1312
1350
  }
1313
1351
  p.killed = true;
1314
1352
  return true;
@@ -1387,6 +1425,7 @@ var MAX_OUTPUT = 32768;
1387
1425
  var DEFAULT_TIMEOUT_MS = 3e5;
1388
1426
  var STREAM_FLUSH_INTERVAL_MS = 200;
1389
1427
  var STREAM_FLUSH_BYTES = 4 * 1024;
1428
+ var MAX_QUEUE_CHUNKS = 500;
1390
1429
  var bashTool = {
1391
1430
  name: "bash",
1392
1431
  category: "Shell",
@@ -1434,7 +1473,8 @@ var bashTool = {
1434
1473
  async *executeStream(input, ctx, opts) {
1435
1474
  if (!input?.command) throw new Error("bash: command is required");
1436
1475
  const registry = getProcessRegistry();
1437
- if (!registry.beforeCall()) {
1476
+ const bypassBreaker = !!input.background;
1477
+ if (!registry.beforeCall(bypassBreaker)) {
1438
1478
  yield {
1439
1479
  type: "final",
1440
1480
  output: {
@@ -1447,6 +1487,17 @@ var bashTool = {
1447
1487
  };
1448
1488
  return;
1449
1489
  }
1490
+ const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
1491
+ if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
1492
+ console.warn(JSON.stringify({
1493
+ level: "warn",
1494
+ event: "bash.pipe_to_shell_detected",
1495
+ message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
1496
+ command_prefix: input.command.slice(0, 100),
1497
+ // Log first 100 chars for review
1498
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1499
+ }));
1500
+ }
1450
1501
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1451
1502
  const isWin = os.platform() === "win32";
1452
1503
  const shell = (() => {
@@ -1505,7 +1556,7 @@ var bashTool = {
1505
1556
  }
1506
1557
  });
1507
1558
  child2.on("close", () => {
1508
- registry.afterCall(Date.now() - startedAt, false);
1559
+ registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
1509
1560
  });
1510
1561
  if (typeof pid2 === "number") child2.unref();
1511
1562
  yield {
@@ -1524,7 +1575,7 @@ var bashTool = {
1524
1575
  env,
1525
1576
  stdio: ["ignore", "pipe", "pipe"],
1526
1577
  detached,
1527
- signal: opts.signal
1578
+ ...isWin ? {} : { signal: opts.signal }
1528
1579
  });
1529
1580
  const pid = child.pid;
1530
1581
  if (typeof pid === "number") {
@@ -1543,9 +1594,22 @@ var bashTool = {
1543
1594
  const timers = [];
1544
1595
  function killWithTimeout(child2, timeoutMs2) {
1545
1596
  if (isWin) {
1546
- try {
1547
- child2.kill();
1548
- } catch {
1597
+ if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
1598
+ const fallback = setTimeout(() => {
1599
+ if (child2.exitCode === null) {
1600
+ try {
1601
+ child2.kill();
1602
+ } catch {
1603
+ }
1604
+ }
1605
+ }, 2e3);
1606
+ timers.push(fallback);
1607
+ fallback.unref?.();
1608
+ } else {
1609
+ try {
1610
+ child2.kill();
1611
+ } catch {
1612
+ }
1549
1613
  }
1550
1614
  return;
1551
1615
  }
@@ -1584,6 +1648,11 @@ var bashTool = {
1584
1648
  }, timeoutMs);
1585
1649
  timers.push(timer);
1586
1650
  timer.unref?.();
1651
+ const onAbort = () => killWithTimeout(child, 2e3);
1652
+ if (isWin) {
1653
+ if (opts.signal.aborted) onAbort();
1654
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
1655
+ }
1587
1656
  const queue = [];
1588
1657
  let resolveNext = null;
1589
1658
  const push = (c) => {
@@ -1608,18 +1677,32 @@ var bashTool = {
1608
1677
  lastFlush = Date.now();
1609
1678
  return text;
1610
1679
  };
1611
- child.stdout?.on("data", (chunk) => {
1612
- const text = chunk.toString();
1613
- buf += text;
1614
- pending += text;
1615
- push({ kind: "data", text });
1616
- });
1617
- child.stderr?.on("data", (chunk) => {
1680
+ let paused = false;
1681
+ const pauseIfFlooded = () => {
1682
+ if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
1683
+ paused = true;
1684
+ child.stdout?.pause();
1685
+ child.stderr?.pause();
1686
+ }
1687
+ };
1688
+ const resumeIfDrained = () => {
1689
+ if (paused && queue.length < MAX_QUEUE_CHUNKS) {
1690
+ paused = false;
1691
+ child.stdout?.resume();
1692
+ child.stderr?.resume();
1693
+ }
1694
+ };
1695
+ const onData = (chunk) => {
1618
1696
  const text = chunk.toString();
1619
- buf += text;
1697
+ if (buf.length < MAX_OUTPUT) {
1698
+ buf += text.slice(0, MAX_OUTPUT - buf.length);
1699
+ }
1620
1700
  pending += text;
1621
1701
  push({ kind: "data", text });
1622
- });
1702
+ pauseIfFlooded();
1703
+ };
1704
+ child.stdout?.on("data", onData);
1705
+ child.stderr?.on("data", onData);
1623
1706
  child.on("error", (err) => {
1624
1707
  for (const t of timers) clearTimeout(t);
1625
1708
  registry.afterCall(Date.now() - startedAt, true);
@@ -1634,6 +1717,7 @@ var bashTool = {
1634
1717
  try {
1635
1718
  while (true) {
1636
1719
  const c = await next();
1720
+ resumeIfDrained();
1637
1721
  if (c.kind === "error") throw c.err;
1638
1722
  if (c.kind === "end") {
1639
1723
  const remainder = flush();
@@ -1658,6 +1742,15 @@ var bashTool = {
1658
1742
  }
1659
1743
  } finally {
1660
1744
  for (const t of timers) clearTimeout(t);
1745
+ if (isWin) opts.signal.removeEventListener("abort", onAbort);
1746
+ child.stdout?.off("data", onData);
1747
+ child.stderr?.off("data", onData);
1748
+ child.stdout?.destroy();
1749
+ child.stderr?.destroy();
1750
+ if (child.exitCode === null && !child.killed) {
1751
+ if (typeof pid === "number") registry.kill(pid, { force: true });
1752
+ else killWithTimeout(child, 2e3);
1753
+ }
1661
1754
  }
1662
1755
  }
1663
1756
  };
@@ -1890,12 +1983,13 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1890
1983
  let killed = false;
1891
1984
  const startedAt = Date.now();
1892
1985
  const resolved = resolveWin32Command(cmd);
1893
- const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
1986
+ const isWin = process.platform === "win32";
1987
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
1894
1988
  const child = spawn(resolved, args, {
1895
1989
  cwd,
1896
- signal,
1897
1990
  env: buildChildEnv(sessionId),
1898
1991
  stdio: ["ignore", "pipe", "pipe"],
1992
+ ...isWin ? {} : { signal },
1899
1993
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
1900
1994
  });
1901
1995
  const registry = getProcessRegistry();
@@ -1909,6 +2003,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1909
2003
  if (typeof pid === "number") registry.kill(pid);
1910
2004
  else child.kill("SIGTERM");
1911
2005
  }, timeout);
2006
+ const onAbort = () => {
2007
+ killed = true;
2008
+ if (typeof pid === "number") registry.kill(pid, { force: true });
2009
+ else child.kill("SIGTERM");
2010
+ };
2011
+ if (isWin) {
2012
+ if (signal.aborted) onAbort();
2013
+ else signal.addEventListener("abort", onAbort, { once: true });
2014
+ }
1912
2015
  child.stdout?.on("data", (chunk) => {
1913
2016
  if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
1914
2017
  });
@@ -1917,6 +2020,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1917
2020
  });
1918
2021
  child.on("close", (code) => {
1919
2022
  clearTimeout(timer);
2023
+ if (isWin) signal.removeEventListener("abort", onAbort);
1920
2024
  if (typeof pid === "number") registry.unregister(pid);
1921
2025
  const durationMs = Date.now() - startedAt;
1922
2026
  const exitCode = killed ? 124 : code ?? 1;
@@ -1933,6 +2037,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1933
2037
  });
1934
2038
  child.on("error", (err) => {
1935
2039
  clearTimeout(timer);
2040
+ if (isWin) signal.removeEventListener("abort", onAbort);
1936
2041
  if (typeof pid === "number") registry.unregister(pid);
1937
2042
  registry.afterCall(Date.now() - startedAt, true);
1938
2043
  resolve7({
@@ -2350,13 +2455,24 @@ var searchTool = {
2350
2455
  async function duckduckgoSearch(query2, num, signal) {
2351
2456
  const encoded = encodeURIComponent(query2);
2352
2457
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
2353
- const results = await fetchWithTimeout(url, signal, TIMEOUT_MS2).then((r) => r.text()).then((html) => parseDuckDuckGo(html, num)).catch(() => [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }]);
2354
- return {
2355
- query: query2,
2356
- results,
2357
- source: "duckduckgo",
2358
- truncated: results.length >= num
2359
- };
2458
+ try {
2459
+ const response = await fetchWithTimeout(url, signal, TIMEOUT_MS2);
2460
+ const html = await response.text();
2461
+ const results = parseDuckDuckGo(html, num);
2462
+ return {
2463
+ query: query2,
2464
+ results,
2465
+ source: "duckduckgo",
2466
+ truncated: results.length >= num
2467
+ };
2468
+ } catch {
2469
+ return {
2470
+ query: query2,
2471
+ results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
2472
+ source: "duckduckgo",
2473
+ truncated: false
2474
+ };
2475
+ }
2360
2476
  }
2361
2477
  function takeFrom(iter, max) {
2362
2478
  const out = [];
@@ -2475,21 +2591,11 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
2475
2591
  }
2476
2592
  }
2477
2593
  function anySignal(...signals) {
2478
- const controller = new AbortController();
2479
- for (const s of signals) {
2480
- if (s.aborted) {
2481
- controller.abort();
2482
- break;
2483
- }
2484
- s.addEventListener("abort", () => controller.abort());
2485
- }
2486
- return controller.signal;
2594
+ return AbortSignal.any(signals);
2487
2595
  }
2488
2596
  function stripTags2(html) {
2489
2597
  return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
2490
2598
  }
2491
-
2492
- // src/todo.ts
2493
2599
  var todoTool = {
2494
2600
  name: "todo",
2495
2601
  category: "Session",
@@ -2548,6 +2654,48 @@ var todoTool = {
2548
2654
  }
2549
2655
  }
2550
2656
  ctx.state.replaceTodos(items);
2657
+ const completedPlanIds = /* @__PURE__ */ new Set();
2658
+ const completedTaskIds = /* @__PURE__ */ new Set();
2659
+ const pendingPlanIds = /* @__PURE__ */ new Set();
2660
+ const pendingTaskIds = /* @__PURE__ */ new Set();
2661
+ for (const item of items) {
2662
+ if (item.promotedFromPlan) {
2663
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
2664
+ }
2665
+ if (item.promotedFromTask) {
2666
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
2667
+ }
2668
+ }
2669
+ for (const planId of completedPlanIds) {
2670
+ if (pendingPlanIds.has(planId)) continue;
2671
+ const planPath = ctx.meta["plan.path"];
2672
+ if (typeof planPath !== "string" || !planPath) continue;
2673
+ try {
2674
+ const plan = await loadPlan(planPath);
2675
+ if (plan) {
2676
+ const updated = setPlanItemStatus(plan, planId, "done");
2677
+ await savePlan(planPath, updated);
2678
+ }
2679
+ } catch {
2680
+ }
2681
+ }
2682
+ for (const taskId of completedTaskIds) {
2683
+ if (pendingTaskIds.has(taskId)) continue;
2684
+ const taskPath = ctx.meta["task.path"];
2685
+ if (typeof taskPath !== "string" || !taskPath) continue;
2686
+ try {
2687
+ const file = await loadTasks(taskPath);
2688
+ if (file) {
2689
+ const task = file.tasks.find((t) => t.id === taskId);
2690
+ if (task && task.status !== "completed") {
2691
+ task.status = "completed";
2692
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2693
+ await saveTasks(taskPath, file);
2694
+ }
2695
+ }
2696
+ } catch {
2697
+ }
2698
+ }
2551
2699
  return {
2552
2700
  count: items.length,
2553
2701
  in_progress: items.filter((t) => t.status === "in_progress").length
@@ -2558,7 +2706,7 @@ var planTool = {
2558
2706
  name: "plan",
2559
2707
  category: "Session",
2560
2708
  description: "Manage a persistent strategic plan for the current session. Unlike todos, plans are meant for higher-level, multi-phase approaches and survive across conversation resumptions. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute.",
2561
- usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
2709
+ usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
2562
2710
  permission: "confirm",
2563
2711
  mutating: true,
2564
2712
  capabilities: ["fs.write"],
@@ -2575,9 +2723,9 @@ var planTool = {
2575
2723
  "done",
2576
2724
  "remove",
2577
2725
  "promote",
2578
- "derive",
2579
2726
  "template_use",
2580
- "clear"
2727
+ "clear",
2728
+ "taskify"
2581
2729
  ],
2582
2730
  description: "The operation to perform on the plan board."
2583
2731
  },
@@ -2596,7 +2744,7 @@ var planTool = {
2596
2744
  subtasks: {
2597
2745
  type: "array",
2598
2746
  items: { type: "string" },
2599
- description: "List of subtask titles. Used with promote or derive to break a plan item into multiple todos."
2747
+ description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
2600
2748
  },
2601
2749
  template: {
2602
2750
  type: "string",
@@ -2617,92 +2765,151 @@ var planTool = {
2617
2765
  };
2618
2766
  }
2619
2767
  const sessionId = ctx.session?.id ?? "unknown";
2620
- let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
2621
- switch (input.action) {
2622
- case "show":
2623
- break;
2624
- case "add": {
2625
- const title = input.title?.trim();
2626
- if (!title) {
2627
- return mkResult(plan, false, "add requires `title`.");
2628
- }
2629
- ({ plan } = addPlanItem(plan, title, input.details?.trim() || void 0));
2630
- await savePlan(planPath, plan);
2631
- break;
2632
- }
2633
- case "start":
2634
- case "done": {
2635
- if (!input.target) {
2636
- return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
2768
+ let early = null;
2769
+ const taskifyMeta = { title: "", details: "" };
2770
+ let didTaskify = false;
2771
+ const plan = await mutatePlan(planPath, sessionId, async (p) => {
2772
+ switch (input.action) {
2773
+ case "show":
2774
+ break;
2775
+ case "add": {
2776
+ const title = input.title?.trim();
2777
+ if (!title) {
2778
+ early = mkResult(p, false, "add requires `title`.");
2779
+ return p;
2780
+ }
2781
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
2782
+ return updated;
2637
2783
  }
2638
- const next = setPlanItemStatus(
2639
- plan,
2640
- input.target,
2641
- input.action === "start" ? "in_progress" : "done"
2642
- );
2643
- if (next === plan) {
2644
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
2784
+ case "start":
2785
+ case "done": {
2786
+ if (!input.target) {
2787
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2788
+ return p;
2789
+ }
2790
+ const next = setPlanItemStatus(
2791
+ p,
2792
+ input.target,
2793
+ input.action === "start" ? "in_progress" : "done"
2794
+ );
2795
+ if (next === p) {
2796
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
2797
+ return p;
2798
+ }
2799
+ return next;
2645
2800
  }
2646
- plan = next;
2647
- await savePlan(planPath, plan);
2648
- break;
2649
- }
2650
- case "remove": {
2651
- if (!input.target) {
2652
- return mkResult(plan, false, "remove requires `target` (id|index|substring).");
2801
+ case "remove": {
2802
+ if (!input.target) {
2803
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
2804
+ return p;
2805
+ }
2806
+ const next = removePlanItem(p, input.target);
2807
+ if (next === p) {
2808
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
2809
+ return p;
2810
+ }
2811
+ return next;
2653
2812
  }
2654
- const next = removePlanItem(plan, input.target);
2655
- if (next === plan) {
2656
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
2813
+ case "promote": {
2814
+ if (!input.target) {
2815
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2816
+ return p;
2817
+ }
2818
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
2819
+ if (!derived) {
2820
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
2821
+ return p;
2822
+ }
2823
+ ctx.state.replaceTodos(derived.todos);
2824
+ early = mkResult(
2825
+ derived.plan,
2826
+ true,
2827
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
2828
+ derived.todos
2829
+ );
2830
+ return derived.plan;
2657
2831
  }
2658
- plan = next;
2659
- await savePlan(planPath, plan);
2660
- break;
2661
- }
2662
- case "promote":
2663
- case "derive": {
2664
- if (!input.target) {
2665
- return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
2832
+ case "template_use": {
2833
+ const templateName = input.template?.trim();
2834
+ if (!templateName) {
2835
+ early = mkResult(p, false, "template_use requires `template` name.");
2836
+ return p;
2837
+ }
2838
+ const template = getPlanTemplate(templateName);
2839
+ if (!template) {
2840
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
2841
+ return p;
2842
+ }
2843
+ let updated = p;
2844
+ for (const item of template.items) {
2845
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
2846
+ }
2847
+ early = mkResult(
2848
+ updated,
2849
+ true,
2850
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
2851
+ );
2852
+ return updated;
2666
2853
  }
2667
- const derived = deriveTodosFromPlanItem(plan, input.target, input.subtasks);
2668
- if (!derived) {
2669
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
2854
+ case "clear":
2855
+ return clearPlan(p);
2856
+ case "taskify": {
2857
+ if (!input.target) {
2858
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
2859
+ return p;
2860
+ }
2861
+ let itemIdx = -1;
2862
+ const asNum = Number.parseInt(input.target, 10);
2863
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
2864
+ itemIdx = asNum - 1;
2865
+ } else {
2866
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
2867
+ if (itemIdx === -1) {
2868
+ const lower = input.target.toLowerCase();
2869
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
2870
+ }
2871
+ }
2872
+ if (itemIdx === -1 || !p.items[itemIdx]) {
2873
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
2874
+ return p;
2875
+ }
2876
+ const item = p.items[itemIdx];
2877
+ taskifyMeta.title = item.title;
2878
+ taskifyMeta.details = item.details ?? "";
2879
+ didTaskify = true;
2880
+ break;
2670
2881
  }
2671
- plan = derived.plan;
2672
- await savePlan(planPath, plan);
2673
- ctx.state.replaceTodos(derived.todos);
2674
- return mkResult(
2675
- plan,
2676
- true,
2677
- `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
2678
- derived.todos
2679
- );
2882
+ default:
2883
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
2884
+ return p;
2680
2885
  }
2681
- case "template_use": {
2682
- const templateName = input.template?.trim();
2683
- if (!templateName) {
2684
- return mkResult(plan, false, "template_use requires `template` name.");
2685
- }
2686
- const template = getPlanTemplate(templateName);
2687
- if (!template) {
2688
- return mkResult(plan, false, `Unknown template "${templateName}".`);
2689
- }
2690
- for (const item of template.items) {
2691
- ({ plan } = addPlanItem(plan, item.title, item.details));
2692
- }
2693
- await savePlan(planPath, plan);
2694
- return mkResult(
2695
- plan,
2696
- true,
2697
- `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
2698
- );
2886
+ return p;
2887
+ });
2888
+ if (early) return early;
2889
+ if (didTaskify) {
2890
+ const taskPath = ctx.meta["task.path"];
2891
+ if (typeof taskPath !== "string" || !taskPath) {
2892
+ return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
2699
2893
  }
2700
- case "clear":
2701
- plan = clearPlan(plan);
2702
- await savePlan(planPath, plan);
2703
- break;
2704
- default:
2705
- return mkResult(plan, false, `Unknown action "${input.action}".`);
2894
+ const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
2895
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2896
+ taskFile.tasks.push({
2897
+ id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
2898
+ title: taskifyMeta.title,
2899
+ description: taskifyMeta.details || void 0,
2900
+ type: "feature",
2901
+ priority: "medium",
2902
+ status: "pending",
2903
+ createdAt: now,
2904
+ updatedAt: now
2905
+ });
2906
+ await saveTasks(taskPath, taskFile);
2907
+ return mkResult(
2908
+ plan,
2909
+ true,
2910
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
2911
+ ${formatTaskList(taskFile.tasks)}`
2912
+ );
2706
2913
  }
2707
2914
  return mkResult(plan, true, `Plan ${input.action} ok.`);
2708
2915
  }
@@ -3531,6 +3738,7 @@ async function walkDir(dir, depth, opts) {
3531
3738
  async function* spawnStream(opts) {
3532
3739
  const max = opts.maxBytes ?? 2e5;
3533
3740
  const flushAt = opts.flushBytes ?? 4 * 1024;
3741
+ const maxQueue = opts.maxQueueSize ?? 500;
3534
3742
  let stdout = "";
3535
3743
  let stderr = "";
3536
3744
  let pending = "";
@@ -3546,6 +3754,7 @@ async function* spawnStream(opts) {
3546
3754
  });
3547
3755
  const queue = [];
3548
3756
  let waiter;
3757
+ let paused = false;
3549
3758
  const wake = () => {
3550
3759
  if (waiter) {
3551
3760
  const w = waiter;
@@ -3553,17 +3762,34 @@ async function* spawnStream(opts) {
3553
3762
  w();
3554
3763
  }
3555
3764
  };
3765
+ const resume = () => {
3766
+ if (paused && queue.length < maxQueue) {
3767
+ paused = false;
3768
+ child.stdout?.resume();
3769
+ child.stderr?.resume();
3770
+ }
3771
+ };
3556
3772
  child.stdout?.on("data", (c) => {
3557
3773
  const s = c.toString();
3558
3774
  if (stdout.length < max) stdout += s;
3559
3775
  queue.push({ kind: "out", data: s });
3560
3776
  wake();
3777
+ if (!paused && queue.length >= maxQueue) {
3778
+ paused = true;
3779
+ child.stdout?.pause();
3780
+ child.stderr?.pause();
3781
+ }
3561
3782
  });
3562
3783
  child.stderr?.on("data", (c) => {
3563
3784
  const s = c.toString();
3564
3785
  if (stderr.length < max) stderr += s;
3565
3786
  queue.push({ kind: "err", data: s });
3566
3787
  wake();
3788
+ if (!paused && queue.length >= maxQueue) {
3789
+ paused = true;
3790
+ child.stdout?.pause();
3791
+ child.stderr?.pause();
3792
+ }
3567
3793
  });
3568
3794
  child.on("error", (e) => {
3569
3795
  error = e.message;
@@ -3583,6 +3809,7 @@ async function* spawnStream(opts) {
3583
3809
  });
3584
3810
  }
3585
3811
  const chunk = queue.shift();
3812
+ resume();
3586
3813
  if (chunk.kind === "close") {
3587
3814
  if (!spawnFailed) exitCode = chunk.code ?? 0;
3588
3815
  break;
@@ -4035,8 +4262,6 @@ function parseResult(runner, result, duration) {
4035
4262
  truncated: result.truncated
4036
4263
  };
4037
4264
  }
4038
-
4039
- // src/install.ts
4040
4265
  var installTool = {
4041
4266
  name: "install",
4042
4267
  category: "Package Management",
@@ -4131,18 +4356,48 @@ var installTool = {
4131
4356
  signal: opts.signal,
4132
4357
  maxBytes: 1e5
4133
4358
  });
4134
- yield {
4135
- type: "final",
4136
- output: {
4137
- packages: pkgList,
4138
- exit_code: result.exitCode,
4139
- output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
4140
- dry_run: args.includes("--dry-run"),
4141
- truncated: result.truncated
4142
- }
4359
+ const output = {
4360
+ packages: pkgList,
4361
+ exit_code: result.exitCode,
4362
+ output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
4363
+ dry_run: args.includes("--dry-run"),
4364
+ truncated: result.truncated
4143
4365
  };
4366
+ const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
4367
+ if (isSuccess && pkgList.length > 0) {
4368
+ const trackerOpts = ctx.meta?.["packageTrackerOpts"];
4369
+ if (trackerOpts) {
4370
+ const manifestPath = resolveManifestPath(cwd, pkgManager);
4371
+ for (const pkg of pkgList) {
4372
+ try {
4373
+ await recordPackageAction(trackerOpts, {
4374
+ manifestPath,
4375
+ packageName: pkg,
4376
+ versionSpec: "latest",
4377
+ // exact version resolved by package manager at install time
4378
+ ecosystem: detectPackageEcosystem(manifestPath),
4379
+ agentId: ctx.agentId,
4380
+ agentName: ctx.agentName,
4381
+ sessionId: ctx.session.id
4382
+ });
4383
+ } catch {
4384
+ }
4385
+ }
4386
+ }
4387
+ }
4388
+ yield { type: "final", output };
4144
4389
  }
4145
4390
  };
4391
+ function resolveManifestPath(cwd, pkgManager) {
4392
+ switch (pkgManager) {
4393
+ case "pnpm":
4394
+ case "yarn":
4395
+ case "npm":
4396
+ return join(cwd, "package.json");
4397
+ default:
4398
+ return join(cwd, "package.json");
4399
+ }
4400
+ }
4146
4401
 
4147
4402
  // src/audit.ts
4148
4403
  var auditTool = {
@@ -7193,19 +7448,19 @@ function isIndexableFile(filePath) {
7193
7448
  }
7194
7449
  async function runStartupIndex(opts) {
7195
7450
  _indexing = true;
7196
- _currentFile = 0;
7197
- _totalFiles = 0;
7198
- _lastError = null;
7199
7451
  emitState();
7200
7452
  try {
7201
- const result = await withMutex(
7202
- () => runIndexer(stubCtx(opts.projectRoot), {
7453
+ const result = await withMutex(() => {
7454
+ _currentFile = 0;
7455
+ _totalFiles = 0;
7456
+ _lastError = null;
7457
+ return runIndexer(stubCtx(opts.projectRoot), {
7203
7458
  projectRoot: opts.projectRoot,
7204
7459
  indexDir: opts.indexDir,
7205
7460
  force: opts.force,
7206
7461
  signal: opts.signal
7207
- })
7208
- );
7462
+ });
7463
+ });
7209
7464
  _ready = true;
7210
7465
  return result;
7211
7466
  } catch (err) {
@@ -7343,8 +7598,18 @@ async function parseFile(file, content, lang) {
7343
7598
  }
7344
7599
  }
7345
7600
  async function runIndexer(_ctx, opts) {
7346
- const { projectRoot, force = false, langs, ignore = [], indexDir, signal } = opts;
7347
- const store = new IndexStore(projectRoot, { indexDir });
7601
+ const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
7602
+ try {
7603
+ return await runIndexerWithStore(store, opts);
7604
+ } finally {
7605
+ try {
7606
+ store.close();
7607
+ } catch {
7608
+ }
7609
+ }
7610
+ }
7611
+ async function runIndexerWithStore(store, opts) {
7612
+ const { projectRoot, force = false, langs, ignore = [], signal } = opts;
7348
7613
  const startMs = Date.now();
7349
7614
  const errors = [];
7350
7615
  const langStats = {};
@@ -7457,7 +7722,6 @@ async function runIndexer(_ctx, opts) {
7457
7722
  }
7458
7723
  const durationMs = Date.now() - startMs;
7459
7724
  store.setLastIndexed(Date.now());
7460
- store.close();
7461
7725
  return {
7462
7726
  filesIndexed,
7463
7727
  symbolsIndexed,
@@ -7771,21 +8035,87 @@ var codebaseStatsTool = {
7771
8035
  }
7772
8036
  }
7773
8037
  };
8038
+ var setWorkingDirTool = {
8039
+ name: "set_working_dir",
8040
+ category: "Context",
8041
+ description: "Change the current working directory for all subsequent file operations. The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
8042
+ usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
8043
+ permission: "confirm",
8044
+ mutating: true,
8045
+ capabilities: ["fs.read"],
8046
+ timeoutMs: 5e3,
8047
+ inputSchema: {
8048
+ type: "object",
8049
+ properties: {
8050
+ path: {
8051
+ type: "string",
8052
+ description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
8053
+ }
8054
+ }
8055
+ },
8056
+ async execute(input, ctx, _opts) {
8057
+ if (!input.path) {
8058
+ return {
8059
+ current: ctx.workingDir,
8060
+ message: `Current working directory is ${ctx.workingDir}`
8061
+ };
8062
+ }
8063
+ const previous = ctx.workingDir;
8064
+ let resolved;
8065
+ try {
8066
+ resolved = ctx.setWorkingDir(input.path);
8067
+ } catch (err) {
8068
+ return {
8069
+ current: ctx.workingDir,
8070
+ error: err instanceof Error ? err.message : String(err)
8071
+ };
8072
+ }
8073
+ try {
8074
+ await fs4.access(resolved);
8075
+ } catch {
8076
+ try {
8077
+ ctx.setWorkingDir(previous);
8078
+ } catch {
8079
+ }
8080
+ return {
8081
+ current: ctx.workingDir,
8082
+ error: `Directory does not exist: ${resolved}`
8083
+ };
8084
+ }
8085
+ return {
8086
+ current: resolved,
8087
+ previous,
8088
+ message: `Working directory changed to ${resolved}`
8089
+ };
8090
+ }
8091
+ };
8092
+ function findTaskIndex(tasks, query2) {
8093
+ const asNum = Number.parseInt(query2, 10);
8094
+ if (!Number.isNaN(asNum)) {
8095
+ const idx = asNum - 1;
8096
+ if (tasks[idx]) return idx;
8097
+ }
8098
+ const byId = tasks.findIndex((t) => t.id === query2);
8099
+ if (byId >= 0) return byId;
8100
+ const lower = query2.toLowerCase();
8101
+ return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
8102
+ }
7774
8103
  var taskTool = {
7775
8104
  name: "task",
7776
8105
  category: "Session",
7777
8106
  description: "Manage structured work items with dependencies, types, and priorities. Use this for complex, multi-step work where tasks have ordering constraints. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. The task list persists across session resumes.",
7778
- usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
7779
- permission: "auto",
7780
- mutating: false,
8107
+ usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
8108
+ permission: "confirm",
8109
+ mutating: true,
8110
+ capabilities: ["fs.write"],
7781
8111
  timeoutMs: 2e3,
7782
8112
  inputSchema: {
7783
8113
  type: "object",
7784
8114
  properties: {
7785
8115
  action: {
7786
8116
  type: "string",
7787
- enum: ["replace", "add", "status", "show"],
7788
- description: "replace = set full list, add = append, status = update task status, show = view only."
8117
+ enum: ["replace", "add", "status", "show", "promote", "planify"],
8118
+ description: "replace = set full list, add = append, status = update task status, show = view only, promote = convert task to todos, planify = convert task to plan item."
7789
8119
  },
7790
8120
  tasks: {
7791
8121
  type: "array",
@@ -7829,11 +8159,20 @@ var taskTool = {
7829
8159
  required: ["title", "type", "priority"],
7830
8160
  description: "Single task to append (id/createdAt/updatedAt auto-generated)."
7831
8161
  },
7832
- id: { type: "string", description: "Task id for action=status." },
8162
+ id: { type: "string", description: "Task id for action=status or target for action=promote." },
7833
8163
  status: {
7834
8164
  type: "string",
7835
8165
  enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
7836
8166
  description: "New status for action=status."
8167
+ },
8168
+ target: {
8169
+ type: "string",
8170
+ description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
8171
+ },
8172
+ subtasks: {
8173
+ type: "array",
8174
+ items: { type: "string" },
8175
+ description: "Optional subtask titles for action=promote. Each becomes a pending todo."
7837
8176
  }
7838
8177
  },
7839
8178
  required: ["action"]
@@ -7844,65 +8183,196 @@ var taskTool = {
7844
8183
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
7845
8184
  }
7846
8185
  const sessionId = ctx.session?.id ?? "unknown";
7847
- const file = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
7848
- switch (input.action) {
7849
- case "show":
7850
- break;
7851
- case "replace": {
7852
- if (!Array.isArray(input.tasks)) {
7853
- return { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
8186
+ let early = null;
8187
+ const promoteMeta = { count: 0, title: "" };
8188
+ const planifyMeta = { title: "", details: "" };
8189
+ let didPlanify = false;
8190
+ const file = await mutateTasks(taskPath, sessionId, async (f) => {
8191
+ switch (input.action) {
8192
+ case "show":
8193
+ break;
8194
+ case "replace": {
8195
+ if (!Array.isArray(input.tasks)) {
8196
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
8197
+ return f;
8198
+ }
8199
+ const newIds = new Set(input.tasks.map((t) => t.id));
8200
+ for (const t of input.tasks) {
8201
+ if (t.dependsOn && t.dependsOn.length > 0) {
8202
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
8203
+ if (missing.length > 0) {
8204
+ early = {
8205
+ ok: false,
8206
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
8207
+ count: 0,
8208
+ completed: 0,
8209
+ inProgress: 0
8210
+ };
8211
+ return f;
8212
+ }
8213
+ }
8214
+ }
8215
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8216
+ f.tasks = input.tasks.map((t) => ({
8217
+ ...t,
8218
+ createdAt: t.createdAt || now,
8219
+ updatedAt: now
8220
+ }));
8221
+ break;
7854
8222
  }
7855
- const now = (/* @__PURE__ */ new Date()).toISOString();
7856
- file.tasks = input.tasks.map((t) => ({
7857
- ...t,
7858
- createdAt: t.createdAt || now,
7859
- updatedAt: now
7860
- }));
7861
- await saveTasks(taskPath, file);
7862
- break;
7863
- }
7864
- case "add": {
7865
- const t = input.task;
7866
- if (!t || !t.title) {
7867
- return { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
8223
+ case "add": {
8224
+ const t = input.task;
8225
+ if (!t || !t.title) {
8226
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
8227
+ return f;
8228
+ }
8229
+ if (t.dependsOn && t.dependsOn.length > 0) {
8230
+ const existingIds = new Set(f.tasks.map((e) => e.id));
8231
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8232
+ if (missing.length > 0) {
8233
+ early = {
8234
+ ok: false,
8235
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8236
+ count: 0,
8237
+ completed: 0,
8238
+ inProgress: 0
8239
+ };
8240
+ return f;
8241
+ }
8242
+ }
8243
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8244
+ const newTask = {
8245
+ id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8246
+ title: t.title,
8247
+ description: t.description,
8248
+ type: t.type || "feature",
8249
+ priority: t.priority || "medium",
8250
+ status: t.status || "pending",
8251
+ dependsOn: t.dependsOn,
8252
+ assignee: t.assignee,
8253
+ estimateHours: t.estimateHours,
8254
+ tags: t.tags,
8255
+ createdAt: now,
8256
+ updatedAt: now
8257
+ };
8258
+ f.tasks.push(newTask);
8259
+ break;
7868
8260
  }
7869
- const now = (/* @__PURE__ */ new Date()).toISOString();
7870
- const newTask = {
7871
- id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7872
- title: t.title,
7873
- description: t.description,
7874
- type: t.type || "feature",
7875
- priority: t.priority || "medium",
7876
- status: t.status || "pending",
7877
- dependsOn: t.dependsOn,
7878
- assignee: t.assignee,
7879
- estimateHours: t.estimateHours,
7880
- tags: t.tags,
7881
- createdAt: now,
7882
- updatedAt: now
7883
- };
7884
- file.tasks.push(newTask);
7885
- await saveTasks(taskPath, file);
7886
- break;
7887
- }
7888
- case "status": {
7889
- if (!input.id || !input.status) {
7890
- return { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8261
+ case "status": {
8262
+ if (!input.id || !input.status) {
8263
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8264
+ return f;
8265
+ }
8266
+ const task = f.tasks.find((t) => t.id === input.id);
8267
+ if (!task) {
8268
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
8269
+ return f;
8270
+ }
8271
+ task.status = input.status;
8272
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8273
+ break;
8274
+ }
8275
+ case "promote": {
8276
+ const target = input.target?.trim();
8277
+ if (!target) {
8278
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8279
+ return f;
8280
+ }
8281
+ const idx = findTaskIndex(f.tasks, target);
8282
+ if (idx === -1) {
8283
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8284
+ return f;
8285
+ }
8286
+ const match = f.tasks[idx];
8287
+ if (!match) {
8288
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8289
+ return f;
8290
+ }
8291
+ if (match.status !== "completed" && match.status !== "failed") {
8292
+ match.status = "in_progress";
8293
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8294
+ }
8295
+ const todos = [];
8296
+ const ts2 = Date.now();
8297
+ todos.push({
8298
+ id: `todo_${ts2}_task`,
8299
+ content: match.title,
8300
+ status: "in_progress",
8301
+ activeForm: match.title,
8302
+ promotedFromTask: match.id
8303
+ });
8304
+ if (match.description) {
8305
+ todos.push({
8306
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8307
+ content: match.description.slice(0, 200),
8308
+ status: "pending",
8309
+ promotedFromTask: match.id
8310
+ });
8311
+ }
8312
+ if (input.subtasks && input.subtasks.length > 0) {
8313
+ for (const st of input.subtasks) {
8314
+ todos.push({
8315
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8316
+ content: st,
8317
+ status: "pending",
8318
+ promotedFromTask: match.id
8319
+ });
8320
+ }
8321
+ }
8322
+ ctx.state.replaceTodos(todos);
8323
+ promoteMeta.count = todos.length;
8324
+ promoteMeta.title = match.title;
8325
+ break;
7891
8326
  }
7892
- const task = file.tasks.find((t) => t.id === input.id);
7893
- if (!task) {
7894
- return { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
8327
+ case "planify": {
8328
+ const target = input.target?.trim();
8329
+ if (!target) {
8330
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8331
+ return f;
8332
+ }
8333
+ const idx = findTaskIndex(f.tasks, target);
8334
+ if (idx === -1) {
8335
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8336
+ return f;
8337
+ }
8338
+ const match = f.tasks[idx];
8339
+ if (!match) {
8340
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8341
+ return f;
8342
+ }
8343
+ planifyMeta.title = match.title;
8344
+ planifyMeta.details = match.description ?? "";
8345
+ didPlanify = true;
8346
+ break;
7895
8347
  }
7896
- task.status = input.status;
7897
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7898
- await saveTasks(taskPath, file);
7899
- break;
8348
+ default:
8349
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
8350
+ return f;
7900
8351
  }
7901
- default:
7902
- return { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show.`, count: 0, completed: 0, inProgress: 0 };
8352
+ return f;
8353
+ });
8354
+ if (early) return early;
8355
+ if (didPlanify) {
8356
+ const { title, details } = planifyMeta;
8357
+ const planPath = ctx.meta["plan.path"];
8358
+ if (typeof planPath === "string" && planPath) {
8359
+ const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
8360
+ const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
8361
+ await savePlan(planPath, updated);
8362
+ return {
8363
+ ok: true,
8364
+ message: `planify ok \u2014 added "${title}" to plan.
8365
+ ${formatPlan(updated)}`,
8366
+ count: file.tasks.length,
8367
+ completed: computeTaskItemProgress(file.tasks).completed,
8368
+ inProgress: computeTaskItemProgress(file.tasks).inProgress
8369
+ };
8370
+ }
8371
+ return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
7903
8372
  }
7904
8373
  const p = computeTaskItemProgress(file.tasks);
7905
- const summary = file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
8374
+ const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
8375
+ ${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
7906
8376
  return {
7907
8377
  ok: true,
7908
8378
  message: summary,
@@ -7949,7 +8419,8 @@ var builtinTools = [
7949
8419
  toolHelpTool,
7950
8420
  codebaseIndexTool,
7951
8421
  codebaseSearchTool,
7952
- codebaseStatsTool
8422
+ codebaseStatsTool,
8423
+ setWorkingDirTool
7953
8424
  ];
7954
8425
 
7955
8426
  // src/pack.ts