@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/pack.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, loadTasks, emptyTaskFile, saveTasks, computeTaskItemProgress, formatTaskList, resolveWstackPaths } from '@wrongstack/core';
3
+ import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, writeFileSync, mkdirSync } from 'node:fs';
6
6
  import * as path2 from 'node:path';
7
- import { resolve, sep, dirname } from 'node:path';
7
+ import { resolve, sep, dirname, join } from 'node:path';
8
8
  import * as fs13 from 'node:fs/promises';
9
9
  import * as os from 'node:os';
10
10
  import { createRequire } from 'node:module';
@@ -12,6 +12,7 @@ import * as ts from 'typescript';
12
12
  import * as dns from 'node:dns/promises';
13
13
  import * as net from 'node:net';
14
14
  import { Agent } from 'undici';
15
+ import { randomUUID } from 'node:crypto';
15
16
 
16
17
  // src/_spawn-stream.ts
17
18
  function resolveWin32Command(cmd) {
@@ -39,6 +40,7 @@ function resolveWin32Command(cmd) {
39
40
  async function* spawnStream(opts) {
40
41
  const max = opts.maxBytes ?? 2e5;
41
42
  const flushAt = opts.flushBytes ?? 4 * 1024;
43
+ const maxQueue = opts.maxQueueSize ?? 500;
42
44
  let stdout = "";
43
45
  let stderr = "";
44
46
  let pending = "";
@@ -54,6 +56,7 @@ async function* spawnStream(opts) {
54
56
  });
55
57
  const queue = [];
56
58
  let waiter;
59
+ let paused = false;
57
60
  const wake = () => {
58
61
  if (waiter) {
59
62
  const w = waiter;
@@ -61,17 +64,34 @@ async function* spawnStream(opts) {
61
64
  w();
62
65
  }
63
66
  };
67
+ const resume = () => {
68
+ if (paused && queue.length < maxQueue) {
69
+ paused = false;
70
+ child.stdout?.resume();
71
+ child.stderr?.resume();
72
+ }
73
+ };
64
74
  child.stdout?.on("data", (c) => {
65
75
  const s = c.toString();
66
76
  if (stdout.length < max) stdout += s;
67
77
  queue.push({ kind: "out", data: s });
68
78
  wake();
79
+ if (!paused && queue.length >= maxQueue) {
80
+ paused = true;
81
+ child.stdout?.pause();
82
+ child.stderr?.pause();
83
+ }
69
84
  });
70
85
  child.stderr?.on("data", (c) => {
71
86
  const s = c.toString();
72
87
  if (stderr.length < max) stderr += s;
73
88
  queue.push({ kind: "err", data: s });
74
89
  wake();
90
+ if (!paused && queue.length >= maxQueue) {
91
+ paused = true;
92
+ child.stdout?.pause();
93
+ child.stderr?.pause();
94
+ }
75
95
  });
76
96
  child.on("error", (e) => {
77
97
  error = e.message;
@@ -91,6 +111,7 @@ async function* spawnStream(opts) {
91
111
  });
92
112
  }
93
113
  const chunk = queue.shift();
114
+ resume();
94
115
  if (chunk.kind === "close") {
95
116
  if (!spawnFailed) exitCode = chunk.code ?? 0;
96
117
  break;
@@ -132,7 +153,7 @@ async function detectPackageManager(cwd) {
132
153
  return "npm";
133
154
  }
134
155
  function resolvePath(input, ctx) {
135
- return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.cwd, input);
156
+ return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
136
157
  }
137
158
  function ensureInsideRoot(absPath, ctx) {
138
159
  const root = path2.resolve(ctx.projectRoot);
@@ -423,8 +444,13 @@ var CircuitBreaker = class {
423
444
  * Call this BEFORE spawning a bash/exec process.
424
445
  * Returns true if the call is allowed; false if the breaker is open.
425
446
  * When false, callers MUST NOT spawn a process.
447
+ *
448
+ * @param bypass - If true, skip the circuit breaker check entirely.
449
+ * Use for background/fire-and-forget processes that should
450
+ * not affect breaker state.
426
451
  */
427
- beforeCall() {
452
+ beforeCall(bypass = false) {
453
+ if (bypass) return true;
428
454
  this._checkStateTransition();
429
455
  if (this.state === "open") return false;
430
456
  return true;
@@ -434,8 +460,12 @@ var CircuitBreaker = class {
434
460
  * `durationMs` is the wall-clock time the process ran.
435
461
  * `failed` is true when the process returned a non-zero exit code or
436
462
  * threw an exception before spawning.
463
+ *
464
+ * @param bypass - If true, do not update breaker state.
465
+ * Use for background/fire-and-forget processes.
437
466
  */
438
- afterCall(durationMs, failed) {
467
+ afterCall(durationMs, failed, bypass = false) {
468
+ if (bypass) return;
439
469
  const now = Date.now();
440
470
  if (this.state === "half-open") {
441
471
  if (failed) {
@@ -534,6 +564,17 @@ function redactCommand(cmd) {
534
564
  return result;
535
565
  }
536
566
  var DEFAULT_GRACE_MS = 2e3;
567
+ function killWin32Tree(pid) {
568
+ try {
569
+ spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
570
+ stdio: "ignore",
571
+ windowsHide: true
572
+ }).unref();
573
+ return true;
574
+ } catch {
575
+ return false;
576
+ }
577
+ }
537
578
  var ProcessRegistryImpl = class {
538
579
  processes = /* @__PURE__ */ new Map();
539
580
  breaker;
@@ -591,16 +632,20 @@ var ProcessRegistryImpl = class {
591
632
  /**
592
633
  * Called before spawning a process. Returns true if allowed; false if
593
634
  * the circuit breaker is open.
635
+ *
636
+ * @param bypass - If true, skip circuit breaker check (for background processes).
594
637
  */
595
- beforeCall() {
596
- return this.breaker.beforeCall();
638
+ beforeCall(bypass = false) {
639
+ return this.breaker.beforeCall(bypass);
597
640
  }
598
641
  /**
599
642
  * Called after a process finishes. `durationMs` is wall-clock time;
600
643
  * `failed` is true for non-zero exit codes.
644
+ *
645
+ * @param bypass - If true, do not update circuit breaker state (for background processes).
601
646
  */
602
- afterCall(durationMs, failed) {
603
- this.breaker.afterCall(durationMs, failed);
647
+ afterCall(durationMs, failed, bypass = false) {
648
+ this.breaker.afterCall(durationMs, failed, bypass);
604
649
  }
605
650
  /** Force-open the circuit breaker (Ctrl+C, /kill force). */
606
651
  forceBreakerOpen() {
@@ -631,9 +676,22 @@ var ProcessRegistryImpl = class {
631
676
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
632
677
  const isWin = os.platform() === "win32";
633
678
  if (isWin) {
634
- try {
635
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
636
- } catch {
679
+ const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
680
+ if (liveRealChild && killWin32Tree(pid)) {
681
+ const fallback = setTimeout(() => {
682
+ if (p.child.exitCode === null) {
683
+ try {
684
+ p.child.kill("SIGKILL");
685
+ } catch {
686
+ }
687
+ }
688
+ }, graceMs);
689
+ fallback.unref?.();
690
+ } else {
691
+ try {
692
+ p.child.kill(force ? "SIGKILL" : "SIGTERM");
693
+ } catch {
694
+ }
637
695
  }
638
696
  p.killed = true;
639
697
  return true;
@@ -709,6 +767,7 @@ var MAX_OUTPUT = 32768;
709
767
  var DEFAULT_TIMEOUT_MS = 3e5;
710
768
  var STREAM_FLUSH_INTERVAL_MS = 200;
711
769
  var STREAM_FLUSH_BYTES = 4 * 1024;
770
+ var MAX_QUEUE_CHUNKS = 500;
712
771
  var bashTool = {
713
772
  name: "bash",
714
773
  category: "Shell",
@@ -756,7 +815,8 @@ var bashTool = {
756
815
  async *executeStream(input, ctx, opts) {
757
816
  if (!input?.command) throw new Error("bash: command is required");
758
817
  const registry = getProcessRegistry();
759
- if (!registry.beforeCall()) {
818
+ const bypassBreaker = !!input.background;
819
+ if (!registry.beforeCall(bypassBreaker)) {
760
820
  yield {
761
821
  type: "final",
762
822
  output: {
@@ -769,6 +829,17 @@ var bashTool = {
769
829
  };
770
830
  return;
771
831
  }
832
+ const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
833
+ if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
834
+ console.warn(JSON.stringify({
835
+ level: "warn",
836
+ event: "bash.pipe_to_shell_detected",
837
+ message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
838
+ command_prefix: input.command.slice(0, 100),
839
+ // Log first 100 chars for review
840
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
841
+ }));
842
+ }
772
843
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
773
844
  const isWin = os.platform() === "win32";
774
845
  const shell = (() => {
@@ -827,7 +898,7 @@ var bashTool = {
827
898
  }
828
899
  });
829
900
  child2.on("close", () => {
830
- registry.afterCall(Date.now() - startedAt, false);
901
+ registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
831
902
  });
832
903
  if (typeof pid2 === "number") child2.unref();
833
904
  yield {
@@ -846,7 +917,7 @@ var bashTool = {
846
917
  env,
847
918
  stdio: ["ignore", "pipe", "pipe"],
848
919
  detached,
849
- signal: opts.signal
920
+ ...isWin ? {} : { signal: opts.signal }
850
921
  });
851
922
  const pid = child.pid;
852
923
  if (typeof pid === "number") {
@@ -865,9 +936,22 @@ var bashTool = {
865
936
  const timers = [];
866
937
  function killWithTimeout(child2, timeoutMs2) {
867
938
  if (isWin) {
868
- try {
869
- child2.kill();
870
- } catch {
939
+ if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
940
+ const fallback = setTimeout(() => {
941
+ if (child2.exitCode === null) {
942
+ try {
943
+ child2.kill();
944
+ } catch {
945
+ }
946
+ }
947
+ }, 2e3);
948
+ timers.push(fallback);
949
+ fallback.unref?.();
950
+ } else {
951
+ try {
952
+ child2.kill();
953
+ } catch {
954
+ }
871
955
  }
872
956
  return;
873
957
  }
@@ -906,6 +990,11 @@ var bashTool = {
906
990
  }, timeoutMs);
907
991
  timers.push(timer);
908
992
  timer.unref?.();
993
+ const onAbort = () => killWithTimeout(child, 2e3);
994
+ if (isWin) {
995
+ if (opts.signal.aborted) onAbort();
996
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
997
+ }
909
998
  const queue = [];
910
999
  let resolveNext = null;
911
1000
  const push = (c) => {
@@ -930,18 +1019,32 @@ var bashTool = {
930
1019
  lastFlush = Date.now();
931
1020
  return text;
932
1021
  };
933
- child.stdout?.on("data", (chunk) => {
934
- const text = chunk.toString();
935
- buf += text;
936
- pending += text;
937
- push({ kind: "data", text });
938
- });
939
- child.stderr?.on("data", (chunk) => {
1022
+ let paused = false;
1023
+ const pauseIfFlooded = () => {
1024
+ if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
1025
+ paused = true;
1026
+ child.stdout?.pause();
1027
+ child.stderr?.pause();
1028
+ }
1029
+ };
1030
+ const resumeIfDrained = () => {
1031
+ if (paused && queue.length < MAX_QUEUE_CHUNKS) {
1032
+ paused = false;
1033
+ child.stdout?.resume();
1034
+ child.stderr?.resume();
1035
+ }
1036
+ };
1037
+ const onData = (chunk) => {
940
1038
  const text = chunk.toString();
941
- buf += text;
1039
+ if (buf.length < MAX_OUTPUT) {
1040
+ buf += text.slice(0, MAX_OUTPUT - buf.length);
1041
+ }
942
1042
  pending += text;
943
1043
  push({ kind: "data", text });
944
- });
1044
+ pauseIfFlooded();
1045
+ };
1046
+ child.stdout?.on("data", onData);
1047
+ child.stderr?.on("data", onData);
945
1048
  child.on("error", (err) => {
946
1049
  for (const t of timers) clearTimeout(t);
947
1050
  registry.afterCall(Date.now() - startedAt, true);
@@ -956,6 +1059,7 @@ var bashTool = {
956
1059
  try {
957
1060
  while (true) {
958
1061
  const c = await next();
1062
+ resumeIfDrained();
959
1063
  if (c.kind === "error") throw c.err;
960
1064
  if (c.kind === "end") {
961
1065
  const remainder = flush();
@@ -980,6 +1084,15 @@ var bashTool = {
980
1084
  }
981
1085
  } finally {
982
1086
  for (const t of timers) clearTimeout(t);
1087
+ if (isWin) opts.signal.removeEventListener("abort", onAbort);
1088
+ child.stdout?.off("data", onData);
1089
+ child.stderr?.off("data", onData);
1090
+ child.stdout?.destroy();
1091
+ child.stderr?.destroy();
1092
+ if (child.exitCode === null && !child.killed) {
1093
+ if (typeof pid === "number") registry.kill(pid, { force: true });
1094
+ else killWithTimeout(child, 2e3);
1095
+ }
983
1096
  }
984
1097
  }
985
1098
  };
@@ -2842,8 +2955,18 @@ async function parseFile(file, content, lang) {
2842
2955
  }
2843
2956
  }
2844
2957
  async function runIndexer(_ctx, opts) {
2845
- const { projectRoot, force = false, langs, ignore = [], indexDir, signal } = opts;
2846
- const store = new IndexStore(projectRoot, { indexDir });
2958
+ const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2959
+ try {
2960
+ return await runIndexerWithStore(store, opts);
2961
+ } finally {
2962
+ try {
2963
+ store.close();
2964
+ } catch {
2965
+ }
2966
+ }
2967
+ }
2968
+ async function runIndexerWithStore(store, opts) {
2969
+ const { projectRoot, force = false, langs, ignore = [], signal } = opts;
2847
2970
  const startMs = Date.now();
2848
2971
  const errors = [];
2849
2972
  const langStats = {};
@@ -2956,7 +3079,6 @@ async function runIndexer(_ctx, opts) {
2956
3079
  }
2957
3080
  const durationMs = Date.now() - startMs;
2958
3081
  store.setLastIndexed(Date.now());
2959
- store.close();
2960
3082
  return {
2961
3083
  filesIndexed,
2962
3084
  symbolsIndexed,
@@ -3878,12 +4000,13 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3878
4000
  let killed = false;
3879
4001
  const startedAt = Date.now();
3880
4002
  const resolved = resolveWin32Command(cmd);
3881
- const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4003
+ const isWin = process.platform === "win32";
4004
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3882
4005
  const child = spawn(resolved, args, {
3883
4006
  cwd,
3884
- signal,
3885
4007
  env: buildChildEnv(sessionId),
3886
4008
  stdio: ["ignore", "pipe", "pipe"],
4009
+ ...isWin ? {} : { signal },
3887
4010
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3888
4011
  });
3889
4012
  const registry = getProcessRegistry();
@@ -3897,6 +4020,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3897
4020
  if (typeof pid === "number") registry.kill(pid);
3898
4021
  else child.kill("SIGTERM");
3899
4022
  }, timeout);
4023
+ const onAbort = () => {
4024
+ killed = true;
4025
+ if (typeof pid === "number") registry.kill(pid, { force: true });
4026
+ else child.kill("SIGTERM");
4027
+ };
4028
+ if (isWin) {
4029
+ if (signal.aborted) onAbort();
4030
+ else signal.addEventListener("abort", onAbort, { once: true });
4031
+ }
3900
4032
  child.stdout?.on("data", (chunk) => {
3901
4033
  if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
3902
4034
  });
@@ -3905,6 +4037,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3905
4037
  });
3906
4038
  child.on("close", (code) => {
3907
4039
  clearTimeout(timer);
4040
+ if (isWin) signal.removeEventListener("abort", onAbort);
3908
4041
  if (typeof pid === "number") registry.unregister(pid);
3909
4042
  const durationMs = Date.now() - startedAt;
3910
4043
  const exitCode = killed ? 124 : code ?? 1;
@@ -3921,6 +4054,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3921
4054
  });
3922
4055
  child.on("error", (err) => {
3923
4056
  clearTimeout(timer);
4057
+ if (isWin) signal.removeEventListener("abort", onAbort);
3924
4058
  if (typeof pid === "number") registry.unregister(pid);
3925
4059
  registry.afterCall(Date.now() - startedAt, true);
3926
4060
  resolve7({
@@ -5042,8 +5176,6 @@ async function runNative(input, base, mode, limit, signal) {
5042
5176
  used: "native"
5043
5177
  };
5044
5178
  }
5045
-
5046
- // src/install.ts
5047
5179
  var installTool = {
5048
5180
  name: "install",
5049
5181
  category: "Package Management",
@@ -5138,18 +5270,48 @@ var installTool = {
5138
5270
  signal: opts.signal,
5139
5271
  maxBytes: 1e5
5140
5272
  });
5141
- yield {
5142
- type: "final",
5143
- output: {
5144
- packages: pkgList,
5145
- exit_code: result.exitCode,
5146
- output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
5147
- dry_run: args.includes("--dry-run"),
5148
- truncated: result.truncated
5149
- }
5273
+ const output = {
5274
+ packages: pkgList,
5275
+ exit_code: result.exitCode,
5276
+ output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
5277
+ dry_run: args.includes("--dry-run"),
5278
+ truncated: result.truncated
5150
5279
  };
5280
+ const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
5281
+ if (isSuccess && pkgList.length > 0) {
5282
+ const trackerOpts = ctx.meta?.["packageTrackerOpts"];
5283
+ if (trackerOpts) {
5284
+ const manifestPath = resolveManifestPath(cwd, pkgManager);
5285
+ for (const pkg of pkgList) {
5286
+ try {
5287
+ await recordPackageAction(trackerOpts, {
5288
+ manifestPath,
5289
+ packageName: pkg,
5290
+ versionSpec: "latest",
5291
+ // exact version resolved by package manager at install time
5292
+ ecosystem: detectPackageEcosystem(manifestPath),
5293
+ agentId: ctx.agentId,
5294
+ agentName: ctx.agentName,
5295
+ sessionId: ctx.session.id
5296
+ });
5297
+ } catch {
5298
+ }
5299
+ }
5300
+ }
5301
+ }
5302
+ yield { type: "final", output };
5151
5303
  }
5152
5304
  };
5305
+ function resolveManifestPath(cwd, pkgManager) {
5306
+ switch (pkgManager) {
5307
+ case "pnpm":
5308
+ case "yarn":
5309
+ case "npm":
5310
+ return join(cwd, "package.json");
5311
+ default:
5312
+ return join(cwd, "package.json");
5313
+ }
5314
+ }
5153
5315
  var jsonTool = {
5154
5316
  name: "json",
5155
5317
  category: "Data",
@@ -5779,7 +5941,7 @@ var planTool = {
5779
5941
  name: "plan",
5780
5942
  category: "Session",
5781
5943
  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.",
5782
- 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.',
5944
+ 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.',
5783
5945
  permission: "confirm",
5784
5946
  mutating: true,
5785
5947
  capabilities: ["fs.write"],
@@ -5796,9 +5958,9 @@ var planTool = {
5796
5958
  "done",
5797
5959
  "remove",
5798
5960
  "promote",
5799
- "derive",
5800
5961
  "template_use",
5801
- "clear"
5962
+ "clear",
5963
+ "taskify"
5802
5964
  ],
5803
5965
  description: "The operation to perform on the plan board."
5804
5966
  },
@@ -5817,7 +5979,7 @@ var planTool = {
5817
5979
  subtasks: {
5818
5980
  type: "array",
5819
5981
  items: { type: "string" },
5820
- description: "List of subtask titles. Used with promote or derive to break a plan item into multiple todos."
5982
+ description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
5821
5983
  },
5822
5984
  template: {
5823
5985
  type: "string",
@@ -5838,92 +6000,151 @@ var planTool = {
5838
6000
  };
5839
6001
  }
5840
6002
  const sessionId = ctx.session?.id ?? "unknown";
5841
- let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
5842
- switch (input.action) {
5843
- case "show":
5844
- break;
5845
- case "add": {
5846
- const title = input.title?.trim();
5847
- if (!title) {
5848
- return mkResult(plan, false, "add requires `title`.");
5849
- }
5850
- ({ plan } = addPlanItem(plan, title, input.details?.trim() || void 0));
5851
- await savePlan(planPath, plan);
5852
- break;
5853
- }
5854
- case "start":
5855
- case "done": {
5856
- if (!input.target) {
5857
- return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
6003
+ let early = null;
6004
+ const taskifyMeta = { title: "", details: "" };
6005
+ let didTaskify = false;
6006
+ const plan = await mutatePlan(planPath, sessionId, async (p) => {
6007
+ switch (input.action) {
6008
+ case "show":
6009
+ break;
6010
+ case "add": {
6011
+ const title = input.title?.trim();
6012
+ if (!title) {
6013
+ early = mkResult(p, false, "add requires `title`.");
6014
+ return p;
6015
+ }
6016
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
6017
+ return updated;
5858
6018
  }
5859
- const next = setPlanItemStatus(
5860
- plan,
5861
- input.target,
5862
- input.action === "start" ? "in_progress" : "done"
5863
- );
5864
- if (next === plan) {
5865
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
6019
+ case "start":
6020
+ case "done": {
6021
+ if (!input.target) {
6022
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6023
+ return p;
6024
+ }
6025
+ const next = setPlanItemStatus(
6026
+ p,
6027
+ input.target,
6028
+ input.action === "start" ? "in_progress" : "done"
6029
+ );
6030
+ if (next === p) {
6031
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6032
+ return p;
6033
+ }
6034
+ return next;
5866
6035
  }
5867
- plan = next;
5868
- await savePlan(planPath, plan);
5869
- break;
5870
- }
5871
- case "remove": {
5872
- if (!input.target) {
5873
- return mkResult(plan, false, "remove requires `target` (id|index|substring).");
6036
+ case "remove": {
6037
+ if (!input.target) {
6038
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
6039
+ return p;
6040
+ }
6041
+ const next = removePlanItem(p, input.target);
6042
+ if (next === p) {
6043
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6044
+ return p;
6045
+ }
6046
+ return next;
5874
6047
  }
5875
- const next = removePlanItem(plan, input.target);
5876
- if (next === plan) {
5877
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
6048
+ case "promote": {
6049
+ if (!input.target) {
6050
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6051
+ return p;
6052
+ }
6053
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
6054
+ if (!derived) {
6055
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6056
+ return p;
6057
+ }
6058
+ ctx.state.replaceTodos(derived.todos);
6059
+ early = mkResult(
6060
+ derived.plan,
6061
+ true,
6062
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
6063
+ derived.todos
6064
+ );
6065
+ return derived.plan;
5878
6066
  }
5879
- plan = next;
5880
- await savePlan(planPath, plan);
5881
- break;
5882
- }
5883
- case "promote":
5884
- case "derive": {
5885
- if (!input.target) {
5886
- return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
6067
+ case "template_use": {
6068
+ const templateName = input.template?.trim();
6069
+ if (!templateName) {
6070
+ early = mkResult(p, false, "template_use requires `template` name.");
6071
+ return p;
6072
+ }
6073
+ const template = getPlanTemplate(templateName);
6074
+ if (!template) {
6075
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
6076
+ return p;
6077
+ }
6078
+ let updated = p;
6079
+ for (const item of template.items) {
6080
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
6081
+ }
6082
+ early = mkResult(
6083
+ updated,
6084
+ true,
6085
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
6086
+ );
6087
+ return updated;
5887
6088
  }
5888
- const derived = deriveTodosFromPlanItem(plan, input.target, input.subtasks);
5889
- if (!derived) {
5890
- return mkResult(plan, false, `No plan item matched "${input.target}".`);
6089
+ case "clear":
6090
+ return clearPlan(p);
6091
+ case "taskify": {
6092
+ if (!input.target) {
6093
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
6094
+ return p;
6095
+ }
6096
+ let itemIdx = -1;
6097
+ const asNum = Number.parseInt(input.target, 10);
6098
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
6099
+ itemIdx = asNum - 1;
6100
+ } else {
6101
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
6102
+ if (itemIdx === -1) {
6103
+ const lower = input.target.toLowerCase();
6104
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
6105
+ }
6106
+ }
6107
+ if (itemIdx === -1 || !p.items[itemIdx]) {
6108
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6109
+ return p;
6110
+ }
6111
+ const item = p.items[itemIdx];
6112
+ taskifyMeta.title = item.title;
6113
+ taskifyMeta.details = item.details ?? "";
6114
+ didTaskify = true;
6115
+ break;
5891
6116
  }
5892
- plan = derived.plan;
5893
- await savePlan(planPath, plan);
5894
- ctx.state.replaceTodos(derived.todos);
5895
- return mkResult(
5896
- plan,
5897
- true,
5898
- `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
5899
- derived.todos
5900
- );
6117
+ default:
6118
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
6119
+ return p;
5901
6120
  }
5902
- case "template_use": {
5903
- const templateName = input.template?.trim();
5904
- if (!templateName) {
5905
- return mkResult(plan, false, "template_use requires `template` name.");
5906
- }
5907
- const template = getPlanTemplate(templateName);
5908
- if (!template) {
5909
- return mkResult(plan, false, `Unknown template "${templateName}".`);
5910
- }
5911
- for (const item of template.items) {
5912
- ({ plan } = addPlanItem(plan, item.title, item.details));
5913
- }
5914
- await savePlan(planPath, plan);
5915
- return mkResult(
5916
- plan,
5917
- true,
5918
- `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
5919
- );
6121
+ return p;
6122
+ });
6123
+ if (early) return early;
6124
+ if (didTaskify) {
6125
+ const taskPath = ctx.meta["task.path"];
6126
+ if (typeof taskPath !== "string" || !taskPath) {
6127
+ return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
5920
6128
  }
5921
- case "clear":
5922
- plan = clearPlan(plan);
5923
- await savePlan(planPath, plan);
5924
- break;
5925
- default:
5926
- return mkResult(plan, false, `Unknown action "${input.action}".`);
6129
+ const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
6130
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6131
+ taskFile.tasks.push({
6132
+ id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
6133
+ title: taskifyMeta.title,
6134
+ description: taskifyMeta.details || void 0,
6135
+ type: "feature",
6136
+ priority: "medium",
6137
+ status: "pending",
6138
+ createdAt: now,
6139
+ updatedAt: now
6140
+ });
6141
+ await saveTasks(taskPath, taskFile);
6142
+ return mkResult(
6143
+ plan,
6144
+ true,
6145
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
6146
+ ${formatTaskList(taskFile.tasks)}`
6147
+ );
5927
6148
  }
5928
6149
  return mkResult(plan, true, `Plan ${input.action} ok.`);
5929
6150
  }
@@ -6469,13 +6690,24 @@ var searchTool = {
6469
6690
  async function duckduckgoSearch(query2, num, signal) {
6470
6691
  const encoded = encodeURIComponent(query2);
6471
6692
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
6472
- const results = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).then((html) => parseDuckDuckGo(html, num)).catch(() => [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }]);
6473
- return {
6474
- query: query2,
6475
- results,
6476
- source: "duckduckgo",
6477
- truncated: results.length >= num
6478
- };
6693
+ try {
6694
+ const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
6695
+ const html = await response.text();
6696
+ const results = parseDuckDuckGo(html, num);
6697
+ return {
6698
+ query: query2,
6699
+ results,
6700
+ source: "duckduckgo",
6701
+ truncated: results.length >= num
6702
+ };
6703
+ } catch {
6704
+ return {
6705
+ query: query2,
6706
+ results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
6707
+ source: "duckduckgo",
6708
+ truncated: false
6709
+ };
6710
+ }
6479
6711
  }
6480
6712
  function takeFrom(iter, max) {
6481
6713
  const out = [];
@@ -6594,34 +6826,92 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
6594
6826
  }
6595
6827
  }
6596
6828
  function anySignal(...signals) {
6597
- const controller = new AbortController();
6598
- for (const s of signals) {
6599
- if (s.aborted) {
6600
- controller.abort();
6601
- break;
6602
- }
6603
- s.addEventListener("abort", () => controller.abort());
6604
- }
6605
- return controller.signal;
6829
+ return AbortSignal.any(signals);
6606
6830
  }
6607
6831
  function stripTags2(html) {
6608
6832
  return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
6609
6833
  }
6834
+ var setWorkingDirTool = {
6835
+ name: "set_working_dir",
6836
+ category: "Context",
6837
+ 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.",
6838
+ 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.",
6839
+ permission: "confirm",
6840
+ mutating: true,
6841
+ capabilities: ["fs.read"],
6842
+ timeoutMs: 5e3,
6843
+ inputSchema: {
6844
+ type: "object",
6845
+ properties: {
6846
+ path: {
6847
+ type: "string",
6848
+ description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
6849
+ }
6850
+ }
6851
+ },
6852
+ async execute(input, ctx, _opts) {
6853
+ if (!input.path) {
6854
+ return {
6855
+ current: ctx.workingDir,
6856
+ message: `Current working directory is ${ctx.workingDir}`
6857
+ };
6858
+ }
6859
+ const previous = ctx.workingDir;
6860
+ let resolved;
6861
+ try {
6862
+ resolved = ctx.setWorkingDir(input.path);
6863
+ } catch (err) {
6864
+ return {
6865
+ current: ctx.workingDir,
6866
+ error: err instanceof Error ? err.message : String(err)
6867
+ };
6868
+ }
6869
+ try {
6870
+ await fs13.access(resolved);
6871
+ } catch {
6872
+ try {
6873
+ ctx.setWorkingDir(previous);
6874
+ } catch {
6875
+ }
6876
+ return {
6877
+ current: ctx.workingDir,
6878
+ error: `Directory does not exist: ${resolved}`
6879
+ };
6880
+ }
6881
+ return {
6882
+ current: resolved,
6883
+ previous,
6884
+ message: `Working directory changed to ${resolved}`
6885
+ };
6886
+ }
6887
+ };
6888
+ function findTaskIndex(tasks, query2) {
6889
+ const asNum = Number.parseInt(query2, 10);
6890
+ if (!Number.isNaN(asNum)) {
6891
+ const idx = asNum - 1;
6892
+ if (tasks[idx]) return idx;
6893
+ }
6894
+ const byId = tasks.findIndex((t) => t.id === query2);
6895
+ if (byId >= 0) return byId;
6896
+ const lower = query2.toLowerCase();
6897
+ return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
6898
+ }
6610
6899
  var taskTool = {
6611
6900
  name: "task",
6612
6901
  category: "Session",
6613
6902
  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.",
6614
- 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',
6615
- permission: "auto",
6616
- mutating: false,
6903
+ 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',
6904
+ permission: "confirm",
6905
+ mutating: true,
6906
+ capabilities: ["fs.write"],
6617
6907
  timeoutMs: 2e3,
6618
6908
  inputSchema: {
6619
6909
  type: "object",
6620
6910
  properties: {
6621
6911
  action: {
6622
6912
  type: "string",
6623
- enum: ["replace", "add", "status", "show"],
6624
- description: "replace = set full list, add = append, status = update task status, show = view only."
6913
+ enum: ["replace", "add", "status", "show", "promote", "planify"],
6914
+ 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."
6625
6915
  },
6626
6916
  tasks: {
6627
6917
  type: "array",
@@ -6665,11 +6955,20 @@ var taskTool = {
6665
6955
  required: ["title", "type", "priority"],
6666
6956
  description: "Single task to append (id/createdAt/updatedAt auto-generated)."
6667
6957
  },
6668
- id: { type: "string", description: "Task id for action=status." },
6958
+ id: { type: "string", description: "Task id for action=status or target for action=promote." },
6669
6959
  status: {
6670
6960
  type: "string",
6671
6961
  enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
6672
6962
  description: "New status for action=status."
6963
+ },
6964
+ target: {
6965
+ type: "string",
6966
+ description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
6967
+ },
6968
+ subtasks: {
6969
+ type: "array",
6970
+ items: { type: "string" },
6971
+ description: "Optional subtask titles for action=promote. Each becomes a pending todo."
6673
6972
  }
6674
6973
  },
6675
6974
  required: ["action"]
@@ -6680,65 +6979,196 @@ var taskTool = {
6680
6979
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
6681
6980
  }
6682
6981
  const sessionId = ctx.session?.id ?? "unknown";
6683
- const file = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
6684
- switch (input.action) {
6685
- case "show":
6686
- break;
6687
- case "replace": {
6688
- if (!Array.isArray(input.tasks)) {
6689
- return { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
6982
+ let early = null;
6983
+ const promoteMeta = { count: 0, title: "" };
6984
+ const planifyMeta = { title: "", details: "" };
6985
+ let didPlanify = false;
6986
+ const file = await mutateTasks(taskPath, sessionId, async (f) => {
6987
+ switch (input.action) {
6988
+ case "show":
6989
+ break;
6990
+ case "replace": {
6991
+ if (!Array.isArray(input.tasks)) {
6992
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
6993
+ return f;
6994
+ }
6995
+ const newIds = new Set(input.tasks.map((t) => t.id));
6996
+ for (const t of input.tasks) {
6997
+ if (t.dependsOn && t.dependsOn.length > 0) {
6998
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
6999
+ if (missing.length > 0) {
7000
+ early = {
7001
+ ok: false,
7002
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
7003
+ count: 0,
7004
+ completed: 0,
7005
+ inProgress: 0
7006
+ };
7007
+ return f;
7008
+ }
7009
+ }
7010
+ }
7011
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7012
+ f.tasks = input.tasks.map((t) => ({
7013
+ ...t,
7014
+ createdAt: t.createdAt || now,
7015
+ updatedAt: now
7016
+ }));
7017
+ break;
6690
7018
  }
6691
- const now = (/* @__PURE__ */ new Date()).toISOString();
6692
- file.tasks = input.tasks.map((t) => ({
6693
- ...t,
6694
- createdAt: t.createdAt || now,
6695
- updatedAt: now
6696
- }));
6697
- await saveTasks(taskPath, file);
6698
- break;
6699
- }
6700
- case "add": {
6701
- const t = input.task;
6702
- if (!t || !t.title) {
6703
- return { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
7019
+ case "add": {
7020
+ const t = input.task;
7021
+ if (!t || !t.title) {
7022
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
7023
+ return f;
7024
+ }
7025
+ if (t.dependsOn && t.dependsOn.length > 0) {
7026
+ const existingIds = new Set(f.tasks.map((e) => e.id));
7027
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
7028
+ if (missing.length > 0) {
7029
+ early = {
7030
+ ok: false,
7031
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
7032
+ count: 0,
7033
+ completed: 0,
7034
+ inProgress: 0
7035
+ };
7036
+ return f;
7037
+ }
7038
+ }
7039
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7040
+ const newTask = {
7041
+ id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7042
+ title: t.title,
7043
+ description: t.description,
7044
+ type: t.type || "feature",
7045
+ priority: t.priority || "medium",
7046
+ status: t.status || "pending",
7047
+ dependsOn: t.dependsOn,
7048
+ assignee: t.assignee,
7049
+ estimateHours: t.estimateHours,
7050
+ tags: t.tags,
7051
+ createdAt: now,
7052
+ updatedAt: now
7053
+ };
7054
+ f.tasks.push(newTask);
7055
+ break;
6704
7056
  }
6705
- const now = (/* @__PURE__ */ new Date()).toISOString();
6706
- const newTask = {
6707
- id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
6708
- title: t.title,
6709
- description: t.description,
6710
- type: t.type || "feature",
6711
- priority: t.priority || "medium",
6712
- status: t.status || "pending",
6713
- dependsOn: t.dependsOn,
6714
- assignee: t.assignee,
6715
- estimateHours: t.estimateHours,
6716
- tags: t.tags,
6717
- createdAt: now,
6718
- updatedAt: now
6719
- };
6720
- file.tasks.push(newTask);
6721
- await saveTasks(taskPath, file);
6722
- break;
6723
- }
6724
- case "status": {
6725
- if (!input.id || !input.status) {
6726
- return { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
7057
+ case "status": {
7058
+ if (!input.id || !input.status) {
7059
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
7060
+ return f;
7061
+ }
7062
+ const task = f.tasks.find((t) => t.id === input.id);
7063
+ if (!task) {
7064
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
7065
+ return f;
7066
+ }
7067
+ task.status = input.status;
7068
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7069
+ break;
6727
7070
  }
6728
- const task = file.tasks.find((t) => t.id === input.id);
6729
- if (!task) {
6730
- return { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
7071
+ case "promote": {
7072
+ const target = input.target?.trim();
7073
+ if (!target) {
7074
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
7075
+ return f;
7076
+ }
7077
+ const idx = findTaskIndex(f.tasks, target);
7078
+ if (idx === -1) {
7079
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7080
+ return f;
7081
+ }
7082
+ const match = f.tasks[idx];
7083
+ if (!match) {
7084
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7085
+ return f;
7086
+ }
7087
+ if (match.status !== "completed" && match.status !== "failed") {
7088
+ match.status = "in_progress";
7089
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7090
+ }
7091
+ const todos = [];
7092
+ const ts2 = Date.now();
7093
+ todos.push({
7094
+ id: `todo_${ts2}_task`,
7095
+ content: match.title,
7096
+ status: "in_progress",
7097
+ activeForm: match.title,
7098
+ promotedFromTask: match.id
7099
+ });
7100
+ if (match.description) {
7101
+ todos.push({
7102
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
7103
+ content: match.description.slice(0, 200),
7104
+ status: "pending",
7105
+ promotedFromTask: match.id
7106
+ });
7107
+ }
7108
+ if (input.subtasks && input.subtasks.length > 0) {
7109
+ for (const st of input.subtasks) {
7110
+ todos.push({
7111
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
7112
+ content: st,
7113
+ status: "pending",
7114
+ promotedFromTask: match.id
7115
+ });
7116
+ }
7117
+ }
7118
+ ctx.state.replaceTodos(todos);
7119
+ promoteMeta.count = todos.length;
7120
+ promoteMeta.title = match.title;
7121
+ break;
6731
7122
  }
6732
- task.status = input.status;
6733
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6734
- await saveTasks(taskPath, file);
6735
- break;
7123
+ case "planify": {
7124
+ const target = input.target?.trim();
7125
+ if (!target) {
7126
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
7127
+ return f;
7128
+ }
7129
+ const idx = findTaskIndex(f.tasks, target);
7130
+ if (idx === -1) {
7131
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7132
+ return f;
7133
+ }
7134
+ const match = f.tasks[idx];
7135
+ if (!match) {
7136
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7137
+ return f;
7138
+ }
7139
+ planifyMeta.title = match.title;
7140
+ planifyMeta.details = match.description ?? "";
7141
+ didPlanify = true;
7142
+ break;
7143
+ }
7144
+ default:
7145
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
7146
+ return f;
6736
7147
  }
6737
- default:
6738
- return { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show.`, count: 0, completed: 0, inProgress: 0 };
7148
+ return f;
7149
+ });
7150
+ if (early) return early;
7151
+ if (didPlanify) {
7152
+ const { title, details } = planifyMeta;
7153
+ const planPath = ctx.meta["plan.path"];
7154
+ if (typeof planPath === "string" && planPath) {
7155
+ const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
7156
+ const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
7157
+ await savePlan(planPath, updated);
7158
+ return {
7159
+ ok: true,
7160
+ message: `planify ok \u2014 added "${title}" to plan.
7161
+ ${formatPlan(updated)}`,
7162
+ count: file.tasks.length,
7163
+ completed: computeTaskItemProgress(file.tasks).completed,
7164
+ inProgress: computeTaskItemProgress(file.tasks).inProgress
7165
+ };
7166
+ }
7167
+ return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
6739
7168
  }
6740
7169
  const p = computeTaskItemProgress(file.tasks);
6741
- const summary = file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
7170
+ const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
7171
+ ${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
6742
7172
  return {
6743
7173
  ok: true,
6744
7174
  message: summary,
@@ -6896,8 +7326,6 @@ function parseResult(runner, result, duration) {
6896
7326
  truncated: result.truncated
6897
7327
  };
6898
7328
  }
6899
-
6900
- // src/todo.ts
6901
7329
  var todoTool = {
6902
7330
  name: "todo",
6903
7331
  category: "Session",
@@ -6956,6 +7384,48 @@ var todoTool = {
6956
7384
  }
6957
7385
  }
6958
7386
  ctx.state.replaceTodos(items);
7387
+ const completedPlanIds = /* @__PURE__ */ new Set();
7388
+ const completedTaskIds = /* @__PURE__ */ new Set();
7389
+ const pendingPlanIds = /* @__PURE__ */ new Set();
7390
+ const pendingTaskIds = /* @__PURE__ */ new Set();
7391
+ for (const item of items) {
7392
+ if (item.promotedFromPlan) {
7393
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
7394
+ }
7395
+ if (item.promotedFromTask) {
7396
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
7397
+ }
7398
+ }
7399
+ for (const planId of completedPlanIds) {
7400
+ if (pendingPlanIds.has(planId)) continue;
7401
+ const planPath = ctx.meta["plan.path"];
7402
+ if (typeof planPath !== "string" || !planPath) continue;
7403
+ try {
7404
+ const plan = await loadPlan(planPath);
7405
+ if (plan) {
7406
+ const updated = setPlanItemStatus(plan, planId, "done");
7407
+ await savePlan(planPath, updated);
7408
+ }
7409
+ } catch {
7410
+ }
7411
+ }
7412
+ for (const taskId of completedTaskIds) {
7413
+ if (pendingTaskIds.has(taskId)) continue;
7414
+ const taskPath = ctx.meta["task.path"];
7415
+ if (typeof taskPath !== "string" || !taskPath) continue;
7416
+ try {
7417
+ const file = await loadTasks(taskPath);
7418
+ if (file) {
7419
+ const task = file.tasks.find((t) => t.id === taskId);
7420
+ if (task && task.status !== "completed") {
7421
+ task.status = "completed";
7422
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7423
+ await saveTasks(taskPath, file);
7424
+ }
7425
+ }
7426
+ } catch {
7427
+ }
7428
+ }
6959
7429
  return {
6960
7430
  count: items.length,
6961
7431
  in_progress: items.filter((t) => t.status === "in_progress").length
@@ -7591,7 +8061,8 @@ var builtinTools = [
7591
8061
  toolHelpTool,
7592
8062
  codebaseIndexTool,
7593
8063
  codebaseSearchTool,
7594
- codebaseStatsTool
8064
+ codebaseStatsTool,
8065
+ setWorkingDirTool
7595
8066
  ];
7596
8067
 
7597
8068
  // src/pack.ts