@wrongstack/tools 0.289.0 → 0.291.1

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 (46) hide show
  1. package/dist/auto-proceed-loop-guard.d.ts +128 -0
  2. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  3. package/dist/auto-proceed-loop-guard.js +46 -0
  4. package/dist/auto-proceed-loop-guard.js.map +7 -0
  5. package/dist/bash-kill-guard.d.ts +10 -1
  6. package/dist/bash-kill-guard.d.ts.map +1 -1
  7. package/dist/bash.js +140 -14
  8. package/dist/bash.js.map +2 -2
  9. package/dist/builtin.js +662 -203
  10. package/dist/builtin.js.map +4 -4
  11. package/dist/codebase-index/background-indexer.d.ts +1 -1
  12. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  13. package/dist/codebase-index/index.js +181 -104
  14. package/dist/codebase-index/index.js.map +3 -3
  15. package/dist/codebase-index/indexer.d.ts.map +1 -1
  16. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  17. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  18. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  19. package/dist/codebase-index/worker.js +164 -96
  20. package/dist/codebase-index/worker.js.map +3 -3
  21. package/dist/codebase-index/writer.d.ts +33 -0
  22. package/dist/codebase-index/writer.d.ts.map +1 -1
  23. package/dist/exec-kill-guard.d.ts +29 -0
  24. package/dist/exec-kill-guard.d.ts.map +1 -0
  25. package/dist/exec.d.ts.map +1 -1
  26. package/dist/exec.js +670 -10
  27. package/dist/exec.js.map +4 -4
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +765 -237
  31. package/dist/index.js.map +4 -4
  32. package/dist/kanban.d.ts +10 -0
  33. package/dist/kanban.d.ts.map +1 -1
  34. package/dist/kanban.js +34 -17
  35. package/dist/kanban.js.map +2 -2
  36. package/dist/pack.js +662 -203
  37. package/dist/pack.js.map +4 -4
  38. package/dist/plan.js.map +2 -2
  39. package/dist/read.d.ts.map +1 -1
  40. package/dist/read.js.map +2 -2
  41. package/dist/session-kanban.d.ts.map +1 -1
  42. package/dist/session-kanban.js +60 -3
  43. package/dist/session-kanban.js.map +2 -2
  44. package/dist/task.js.map +2 -2
  45. package/dist/todo.js.map +2 -2
  46. package/package.json +9 -5
package/dist/builtin.js CHANGED
@@ -639,8 +639,8 @@ var init_process_registry = __esm({
639
639
  if (p.killed) return true;
640
640
  if (p.protected) return false;
641
641
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
642
- const isWin4 = os.platform() === "win32";
643
- if (isWin4) {
642
+ const isWin5 = os.platform() === "win32";
643
+ if (isWin5) {
644
644
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
645
645
  const directFallback = () => {
646
646
  if (p.child.exitCode === null) {
@@ -2605,8 +2605,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2605
2605
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2606
2606
  }
2607
2607
  }
2608
- function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2609
- const lower = basename9.toLowerCase();
2608
+ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2609
+ const lower = basename10.toLowerCase();
2610
2610
  const extension = path4.extname(lower);
2611
2611
  for (const profile of profiles) {
2612
2612
  const detector = profile.detectors.find(
@@ -2617,7 +2617,7 @@ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2617
2617
  candidate.evidence.push({
2618
2618
  kind: detector.kind,
2619
2619
  path: fullPath,
2620
- value: basename9,
2620
+ value: basename10,
2621
2621
  weight: detector.weight
2622
2622
  });
2623
2623
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3934,15 +3934,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3934
3934
  const beforeSet = new Set(before);
3935
3935
  const afterSet = new Set(after);
3936
3936
  const changed = /* @__PURE__ */ new Set();
3937
- for (const path32 of after) {
3938
- if (!beforeSet.has(path32)) changed.add(path32);
3937
+ for (const path33 of after) {
3938
+ if (!beforeSet.has(path33)) changed.add(path33);
3939
3939
  }
3940
- for (const path32 of before) {
3941
- if (!afterSet.has(path32)) changed.add(path32);
3940
+ for (const path33 of before) {
3941
+ if (!afterSet.has(path33)) changed.add(path33);
3942
3942
  }
3943
3943
  if (beforeSizes && afterSizes) {
3944
- for (const path32 of after) {
3945
- if (beforeSizes.get(path32) !== afterSizes.get(path32)) changed.add(path32);
3944
+ for (const path33 of after) {
3945
+ if (beforeSizes.get(path33) !== afterSizes.get(path33)) changed.add(path33);
3946
3946
  }
3947
3947
  }
3948
3948
  return [...changed].sort();
@@ -5374,17 +5374,18 @@ function getPersistentProcessRegistry() {
5374
5374
 
5375
5375
  // src/bash-kill-guard.ts
5376
5376
  var isWin2 = os3.platform() === "win32";
5377
+ var SCRIPT_KILL_RE = /^(?:\.\\|\.\/)?(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)(?:\s|$)/i;
5378
+ var SCRIPT_KILL_RE_POSIX = /^(?:\.\/)?(?:kill|terminate|stop)\S*\.sh(?:\s|$)/i;
5379
+ var SCRIPT_KILL_FALLBACK_RE = /^\S*(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)\b/i;
5377
5380
  function extractKillCommand(command) {
5378
5381
  const normalized = command.replace(/\s+/g, " ").trim();
5379
- const shellCMatch = normalized.match(
5380
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
5381
- );
5382
- if (shellCMatch?.[1]) {
5383
- const inner = shellCMatch[1].trim();
5382
+ const shellCMatch = normalized.match(/^.+?\s+-c\s+(['"])([\s\S]+)\1$/);
5383
+ if (shellCMatch?.[2]) {
5384
+ const inner = shellCMatch[2].trim();
5384
5385
  return isKillRelatedCommand(inner) ? inner : null;
5385
5386
  }
5386
5387
  const shellCUnquoted = normalized.match(
5387
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
5388
+ /^.+?\s+-c\s+(kill(?:\s+-s\s+[a-zA-Z0-9]+|\s+-[a-zA-Z0-9]+)?\s+\d+)$/
5388
5389
  );
5389
5390
  if (shellCUnquoted?.[1]) {
5390
5391
  return shellCUnquoted[1];
@@ -5396,22 +5397,47 @@ function isKillRelatedCommand(cmd) {
5396
5397
  if (isWin2) {
5397
5398
  if (/^taskkill\s/i.test(normalized)) return true;
5398
5399
  if (/^tskill\s/i.test(normalized)) return true;
5400
+ if (/^(stop-process|kill|stop)\s/i.test(normalized)) return true;
5401
+ if (/^wmic\s+process\s/i.test(normalized) && /\bdelete\b/i.test(normalized)) return true;
5402
+ if (SCRIPT_KILL_RE.test(normalized)) return true;
5399
5403
  return false;
5400
5404
  }
5401
5405
  if (/^kill(\s|$)/.test(normalized)) return true;
5402
5406
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
5403
5407
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
5408
+ if (SCRIPT_KILL_RE_POSIX.test(normalized)) return true;
5404
5409
  return false;
5405
5410
  }
5406
5411
  function parseKillCommand(command) {
5407
5412
  const normalized = command.replace(/\s+/g, " ").trim();
5408
5413
  if (isWin2) {
5409
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
5410
- if (taskkillMatch?.[1]) {
5411
- const pidStr = taskkillMatch[1];
5414
+ const hasTaskkillForce = /(?:^|\s)\/F(?=\s|$)/i.test(normalized);
5415
+ const isSimpleTaskkill = /^taskkill\s+/i.test(normalized) && !/[|&<>]/.test(normalized);
5416
+ const taskkillPidMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/PID\s+(\d+)(?=\s|$)/i) : null;
5417
+ if (taskkillPidMatch?.[1]) {
5418
+ return {
5419
+ pid: parseInt(taskkillPidMatch[1], 10),
5420
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
5421
+ isGroupKill: false,
5422
+ isAllKill: false,
5423
+ originalCommand: command
5424
+ };
5425
+ }
5426
+ const taskkillImMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/IM\s+([^\s/]+)(?=\s|$)/i) : null;
5427
+ if (taskkillImMatch?.[1]) {
5428
+ return {
5429
+ name: taskkillImMatch[1],
5430
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
5431
+ isGroupKill: false,
5432
+ isAllKill: false,
5433
+ originalCommand: command
5434
+ };
5435
+ }
5436
+ const taskkillFiMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/FI\s+"IMAGENAME\s+eq\s+([^"]+)"(?=\s|$)/i) : null;
5437
+ if (taskkillFiMatch?.[1]) {
5412
5438
  return {
5413
- pid: parseInt(pidStr, 10),
5414
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
5439
+ name: taskkillFiMatch[1],
5440
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
5415
5441
  isGroupKill: false,
5416
5442
  isAllKill: false,
5417
5443
  originalCommand: command
@@ -5419,18 +5445,109 @@ function parseKillCommand(command) {
5419
5445
  }
5420
5446
  const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
5421
5447
  if (tskillMatch?.[1]) {
5422
- const pidStr = tskillMatch[1];
5423
5448
  return {
5424
- pid: parseInt(pidStr, 10),
5449
+ pid: parseInt(tskillMatch[1], 10),
5425
5450
  signal: "TERM",
5426
5451
  isGroupKill: false,
5427
5452
  isAllKill: false,
5428
5453
  originalCommand: command
5429
5454
  };
5430
5455
  }
5456
+ const isStopProcIdCommand = /^(?:stop-process|kill)(?:\s+-(?:id|pid)\s+\d+|\s+-[a-zA-Z]+)+$/i.test(normalized);
5457
+ const stopProcIdMatch = normalized.match(/(?:^|\s)-(?:id|pid)\s+(\d+)(?=\s|$)/i);
5458
+ if (isStopProcIdCommand && stopProcIdMatch?.[1]) {
5459
+ return {
5460
+ pid: parseInt(stopProcIdMatch[1], 10),
5461
+ signal: "FORCE",
5462
+ isGroupKill: false,
5463
+ isAllKill: false,
5464
+ originalCommand: command
5465
+ };
5466
+ }
5467
+ const killSignalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+)$/i);
5468
+ if (killSignalOptionMatch?.[1] && killSignalOptionMatch[2]) {
5469
+ return {
5470
+ pid: parseInt(killSignalOptionMatch[2], 10),
5471
+ signal: killSignalOptionMatch[1].toUpperCase(),
5472
+ isGroupKill: false,
5473
+ isAllKill: false,
5474
+ originalCommand: command
5475
+ };
5476
+ }
5477
+ const killPosixMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+)$/);
5478
+ if (killPosixMatch?.[2]) {
5479
+ const sig = killPosixMatch[1] ? killPosixMatch[1].slice(1).toUpperCase() : "TERM";
5480
+ return {
5481
+ pid: parseInt(killPosixMatch[2], 10),
5482
+ signal: sig,
5483
+ isGroupKill: false,
5484
+ isAllKill: false,
5485
+ originalCommand: command
5486
+ };
5487
+ }
5488
+ const stopProcNameMatch = normalized.match(
5489
+ /^(?:stop-process|kill)\s+-(?:name|n)\s+(?:['"]([a-zA-Z0-9_.-]+)['"]|([a-zA-Z0-9_.-]+))(?:\s|$)/i
5490
+ );
5491
+ const stopProcName = stopProcNameMatch?.[1] ?? stopProcNameMatch?.[2];
5492
+ if (stopProcName) {
5493
+ return {
5494
+ name: stopProcName,
5495
+ signal: "FORCE",
5496
+ isGroupKill: false,
5497
+ isAllKill: false,
5498
+ originalCommand: command
5499
+ };
5500
+ }
5501
+ const stopProcStandalone = normalized.match(
5502
+ /^(?:stop-process|kill)\s+['"]?([a-zA-Z][a-zA-Z0-9_.-]+)['"]?$/i
5503
+ );
5504
+ if (stopProcStandalone?.[1]) {
5505
+ return {
5506
+ name: stopProcStandalone[1],
5507
+ signal: "FORCE",
5508
+ isGroupKill: false,
5509
+ isAllKill: false,
5510
+ originalCommand: command
5511
+ };
5512
+ }
5513
+ const wmicMatch = normalized.match(
5514
+ /^wmic\s+process\s+where\s+['"]?(?:name\s*=\s*['"]?)([a-zA-Z0-9_.-]+)/i
5515
+ );
5516
+ if (wmicMatch?.[1]) {
5517
+ return {
5518
+ name: wmicMatch[1],
5519
+ signal: "FORCE",
5520
+ isGroupKill: false,
5521
+ isAllKill: false,
5522
+ originalCommand: command
5523
+ };
5524
+ }
5525
+ const killScriptMatch = normalized.match(SCRIPT_KILL_RE);
5526
+ if (killScriptMatch) {
5527
+ return {
5528
+ name: "kill-script",
5529
+ // sentinel — isKillProtected always blocks "kill-script"
5530
+ signal: "FORCE",
5531
+ isGroupKill: false,
5532
+ isAllKill: false,
5533
+ originalCommand: command
5534
+ };
5535
+ }
5431
5536
  return null;
5432
5537
  }
5433
- const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
5538
+ const signalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+|-?\d+)$/i);
5539
+ if (signalOptionMatch?.[1] && signalOptionMatch[2]) {
5540
+ const pidOrGroup = signalOptionMatch[2];
5541
+ const isGroupKill = pidOrGroup.startsWith("-");
5542
+ return {
5543
+ pid: parseInt(isGroupKill ? pidOrGroup.slice(1) : pidOrGroup, 10),
5544
+ signal: signalOptionMatch[1].toUpperCase(),
5545
+ isGroupKill,
5546
+ isAllKill: false,
5547
+ originalCommand: command
5548
+ };
5549
+ }
5550
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+|-?\d+)$/);
5434
5551
  if (simpleMatch) {
5435
5552
  const signal = simpleMatch[1] ?? "-TERM";
5436
5553
  const pidOrGroup = simpleMatch[2];
@@ -5490,6 +5607,9 @@ async function getProtectedEntries() {
5490
5607
  }
5491
5608
  async function isKillProtected(kill) {
5492
5609
  const registry = getPersistentProcessRegistry();
5610
+ if (kill.name === "kill-script") {
5611
+ return true;
5612
+ }
5493
5613
  if (kill.name) {
5494
5614
  const entries = await getProtectedEntries();
5495
5615
  const killNameLower = kill.name.toLowerCase();
@@ -5529,6 +5649,12 @@ async function checkAndBlockKillCommand(command) {
5529
5649
  reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
5530
5650
  };
5531
5651
  }
5652
+ if (SCRIPT_KILL_FALLBACK_RE.test(killCmd)) {
5653
+ return {
5654
+ blocked: true,
5655
+ reason: `Blocked: script-based kill detected \u2014 "${killCmd.slice(0, 80)}" may target protected WrongStack processes (cannot inspect script body).`
5656
+ };
5657
+ }
5532
5658
  return { blocked: false };
5533
5659
  }
5534
5660
  if (await isKillProtected(parsed)) {
@@ -5552,8 +5678,8 @@ async function checkAndBlockKillCommand(command) {
5552
5678
 
5553
5679
  // src/_shell-pick.ts
5554
5680
  var POSIX_DEFAULT = "cmd";
5555
- function pickShell(platform4, command, env) {
5556
- if (platform4 !== "win32") return POSIX_DEFAULT;
5681
+ function pickShell(platform5, command, env) {
5682
+ if (platform5 !== "win32") return POSIX_DEFAULT;
5557
5683
  const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
5558
5684
  if (override === "cmd" || override === "cmd.exe") return "cmd";
5559
5685
  if (override === "powershell" || override === "powershell.exe") return "powershell";
@@ -5757,10 +5883,10 @@ var bashTool = {
5757
5883
  }));
5758
5884
  }
5759
5885
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
5760
- const isWin4 = os4.platform() === "win32";
5886
+ const isWin5 = os4.platform() === "win32";
5761
5887
  let plan;
5762
5888
  let winShellKind;
5763
- if (isWin4) {
5889
+ if (isWin5) {
5764
5890
  const shell2 = pickShell("win32", input.command, {
5765
5891
  get: (k) => process.env[k]
5766
5892
  });
@@ -5789,7 +5915,7 @@ var bashTool = {
5789
5915
  const shell = plan.bin;
5790
5916
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
5791
5917
  const env = buildChildEnv2(ctx.session?.id);
5792
- const detached = !isWin4;
5918
+ const detached = !isWin5;
5793
5919
  const startedAt = Date.now();
5794
5920
  if (input.background) {
5795
5921
  let buf2 = "";
@@ -5807,7 +5933,7 @@ var bashTool = {
5807
5933
  // apply: the child gets a hidden console that grandchildren inherit.
5808
5934
  // Windows children survive parent exit either way. POSIX keeps
5809
5935
  // detached for the process-group kill semantics.
5810
- detached: !isWin4,
5936
+ detached: !isWin5,
5811
5937
  windowsHide: true
5812
5938
  });
5813
5939
  if (plan.useStdin) {
@@ -5918,7 +6044,7 @@ var bashTool = {
5918
6044
  stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
5919
6045
  detached,
5920
6046
  windowsHide: true,
5921
- ...isWin4 ? {} : { signal: opts.signal }
6047
+ ...isWin5 ? {} : { signal: opts.signal }
5922
6048
  });
5923
6049
  if (plan.useStdin) {
5924
6050
  try {
@@ -5971,7 +6097,7 @@ var bashTool = {
5971
6097
  const timers = [];
5972
6098
  const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
5973
6099
  function killWithTimeout(child2, timeoutMs2) {
5974
- if (isWin4) {
6100
+ if (isWin5) {
5975
6101
  if (typeof child2.pid === "number" && child2.exitCode === null) {
5976
6102
  const attempted = registry.kill(child2.pid, { force: true, graceMs: timeoutMs2 });
5977
6103
  if (!attempted) {
@@ -6012,7 +6138,7 @@ var bashTool = {
6012
6138
  timers.push(timer);
6013
6139
  timer.unref?.();
6014
6140
  const onAbort = () => killWithTimeout(child, 2e3);
6015
- if (isWin4) {
6141
+ if (isWin5) {
6016
6142
  if (opts.signal.aborted) onAbort();
6017
6143
  else opts.signal.addEventListener("abort", onAbort, { once: true });
6018
6144
  }
@@ -6126,7 +6252,7 @@ ${hint}` : ""),
6126
6252
  } finally {
6127
6253
  for (const t of timers) clearTimeout(t);
6128
6254
  spool.finalize();
6129
- if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6255
+ if (isWin5) opts.signal.removeEventListener("abort", onAbort);
6130
6256
  child.stdout?.off("data", onStdoutData);
6131
6257
  child.stderr?.off("data", onStderrData);
6132
6258
  child.stdout?.destroy();
@@ -7774,6 +7900,53 @@ function codebaseIndexDirOverride(ctx) {
7774
7900
  const v = ctx.meta?.["codebaseIndexDir"];
7775
7901
  return typeof v === "string" ? v : void 0;
7776
7902
  }
7903
+ var StorePool = class {
7904
+ stores = /* @__PURE__ */ new Map();
7905
+ key(projectRoot, indexDir) {
7906
+ return `${projectRoot}\0${indexDir ?? ""}`;
7907
+ }
7908
+ /** Borrow a store. Creates it on first access for this key. */
7909
+ acquire(projectRoot, opts) {
7910
+ const k = this.key(projectRoot, opts?.indexDir);
7911
+ let store = this.stores.get(k);
7912
+ if (!store) {
7913
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
7914
+ this.stores.set(k, store);
7915
+ }
7916
+ return store;
7917
+ }
7918
+ /** Return the store to the pool. The connection stays warm for subsequent
7919
+ * operations on the same (projectRoot, indexDir). */
7920
+ release(_store) {
7921
+ }
7922
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
7923
+ closeAll() {
7924
+ for (const store of this.stores.values()) {
7925
+ try {
7926
+ store.close();
7927
+ } catch {
7928
+ }
7929
+ }
7930
+ this.stores.clear();
7931
+ }
7932
+ /** Remove one store from the pool. Used by tests that need isolation. */
7933
+ evict(projectRoot, indexDir) {
7934
+ const k = this.key(projectRoot, indexDir);
7935
+ const store = this.stores.get(k);
7936
+ if (store) {
7937
+ try {
7938
+ store.close();
7939
+ } catch {
7940
+ }
7941
+ this.stores.delete(k);
7942
+ }
7943
+ }
7944
+ /** True when the pool holds a connection for the given key. */
7945
+ has(projectRoot, indexDir) {
7946
+ return this.stores.has(this.key(projectRoot, indexDir));
7947
+ }
7948
+ };
7949
+ var indexStorePool = new StorePool();
7777
7950
  var warningSilenced = false;
7778
7951
  function silenceSqliteExperimentalWarning() {
7779
7952
  if (warningSilenced) return;
@@ -7991,7 +8164,7 @@ var IndexStore = class _IndexStore {
7991
8164
  return this.runWithRetry(() => {
7992
8165
  this.db.exec("BEGIN IMMEDIATE");
7993
8166
  try {
7994
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8167
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
7995
8168
  let nextId = (maxRows[0]?.m ?? 0) + 1;
7996
8169
  const stmt = this.db.prepare(
7997
8170
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8263,7 +8436,7 @@ var IndexStore = class _IndexStore {
8263
8436
  return { results, total: candidates.length };
8264
8437
  }
8265
8438
  getAllIndexable() {
8266
- return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8439
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8267
8440
  }
8268
8441
  /**
8269
8442
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8273,7 +8446,7 @@ var IndexStore = class _IndexStore {
8273
8446
  * `symbols.id`). Ids may have gaps — that is fine.
8274
8447
  */
8275
8448
  getMaxSymbolId() {
8276
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8449
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8277
8450
  return rows[0]?.m ?? 0;
8278
8451
  }
8279
8452
  // ─── Stats ───────────────────────────────────────────────────────────────────
@@ -8281,14 +8454,14 @@ var IndexStore = class _IndexStore {
8281
8454
  const sizeBytes = this.sizeBytes();
8282
8455
  const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8283
8456
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8284
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8457
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8285
8458
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8286
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8459
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
8287
8460
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8288
- const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8461
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8289
8462
  const byLang = {};
8290
8463
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8291
- const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8464
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8292
8465
  const byKind = {};
8293
8466
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8294
8467
  return {
@@ -8320,11 +8493,14 @@ var IndexStore = class _IndexStore {
8320
8493
  this.runWithRetry(() => {
8321
8494
  this.db.exec("BEGIN IMMEDIATE");
8322
8495
  try {
8323
- this.db.exec("DELETE FROM refs");
8324
- this.db.exec("DELETE FROM symbols");
8325
- this.db.exec("DELETE FROM files");
8326
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8496
+ this.db.exec("DROP TABLE IF EXISTS refs");
8497
+ this.db.exec("DROP TABLE IF EXISTS symbols");
8498
+ this.db.exec("DROP TABLE IF EXISTS files");
8499
+ this.db.exec("DROP TABLE IF EXISTS metadata");
8500
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8327
8501
  this.db.exec("COMMIT");
8502
+ this.stmtCache.clear();
8503
+ this.initSchema();
8328
8504
  } catch (err) {
8329
8505
  this.db.exec("ROLLBACK");
8330
8506
  throw err;
@@ -8409,7 +8585,7 @@ var IndexStore = class _IndexStore {
8409
8585
  ).run(...options.deleteForFiles);
8410
8586
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8411
8587
  }
8412
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8588
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8413
8589
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8414
8590
  const symStmt = this.db.prepare(
8415
8591
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8593,7 +8769,7 @@ var IndexStore = class _IndexStore {
8593
8769
  * symbol resolved in package B). Node metadata includes symbol/file counts.
8594
8770
  */
8595
8771
  getPackageGraph() {
8596
- const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8772
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
8597
8773
  const pkgNodes = /* @__PURE__ */ new Map();
8598
8774
  const fileToPkg = /* @__PURE__ */ new Map();
8599
8775
  const symbolToPkg = /* @__PURE__ */ new Map();
@@ -8684,15 +8860,18 @@ var IndexStore = class _IndexStore {
8684
8860
  * derived from cross-file symbol references within the package.
8685
8861
  */
8686
8862
  getFileGraph(packageFilter) {
8687
- const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8688
- const pkgSyms = allSymbols.filter(
8689
- (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
8690
- );
8691
- if (pkgSyms.length === 0) return { nodes: [], edges: [] };
8863
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
8864
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
8865
+ const localFiles = new Set(pkgFilePaths);
8866
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
8867
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
8868
+ const pkgSyms = this.db.prepare(
8869
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
8870
+ ).all(...pkgFilePaths);
8692
8871
  const fileNodes = /* @__PURE__ */ new Map();
8693
8872
  const symToFile = /* @__PURE__ */ new Map();
8694
8873
  const fileStats = /* @__PURE__ */ new Map();
8695
- for (const s of allSymbols) {
8874
+ for (const s of pkgSyms) {
8696
8875
  symToFile.set(s.id, s.file);
8697
8876
  const current = fileStats.get(s.file);
8698
8877
  fileStats.set(s.file, {
@@ -8700,8 +8879,6 @@ var IndexStore = class _IndexStore {
8700
8879
  lang: current?.lang ?? s.lang
8701
8880
  });
8702
8881
  }
8703
- const localFiles = new Set(pkgSyms.map((s) => s.file));
8704
- const indexedFiles = new Set(allSymbols.map((s) => s.file));
8705
8882
  const ensureFileNode = (file) => {
8706
8883
  if (fileNodes.has(file)) return;
8707
8884
  const stats = fileStats.get(file);
@@ -8719,11 +8896,32 @@ var IndexStore = class _IndexStore {
8719
8896
  for (const file of localFiles) {
8720
8897
  ensureFileNode(file);
8721
8898
  }
8899
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
8722
8900
  const refRows = this.db.prepare(
8723
8901
  `SELECT r.from_id, r.to_id, r.call_type
8724
8902
  FROM refs r
8725
- WHERE r.to_id IS NOT NULL`
8726
- ).all();
8903
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
8904
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
8905
+ AND r.to_id IS NOT NULL`
8906
+ ).all(...pkgFilePaths, ...pkgFilePaths);
8907
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
8908
+ const crossRefIds = /* @__PURE__ */ new Set();
8909
+ for (const r of refRows) {
8910
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
8911
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
8912
+ }
8913
+ if (crossRefIds.size > 0) {
8914
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
8915
+ const extras = this.db.prepare(
8916
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
8917
+ ).all(...crossRefIds);
8918
+ for (const x of extras) {
8919
+ symToFile.set(x.id, x.file);
8920
+ if (!fileStats.has(x.file)) {
8921
+ fileStats.set(x.file, { count: 0, lang: "ts" });
8922
+ }
8923
+ }
8924
+ }
8727
8925
  const edgeMap = /* @__PURE__ */ new Map();
8728
8926
  for (const r of refRows) {
8729
8927
  if (r.call_type === "import") continue;
@@ -8745,8 +8943,9 @@ var IndexStore = class _IndexStore {
8745
8943
  const importRows = this.db.prepare(
8746
8944
  `SELECT r.from_id, r.to_name
8747
8945
  FROM refs r
8748
- WHERE r.call_type = 'import'`
8749
- ).all();
8946
+ WHERE r.call_type = 'import'
8947
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
8948
+ ).all(...pkgFilePaths);
8750
8949
  for (const r of importRows) {
8751
8950
  const fromFile = symToFile.get(r.from_id);
8752
8951
  if (!fromFile || !localFiles.has(fromFile)) continue;
@@ -8788,12 +8987,11 @@ var IndexStore = class _IndexStore {
8788
8987
  * derived from intra-file and cross-file symbol references (who calls whom).
8789
8988
  */
8790
8989
  getSymbolGraph(fileFilter) {
8791
- const allSymbols = this.db.prepare(
8792
- "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
8793
- ).all();
8794
- const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
8990
+ const syms = this.db.prepare(
8991
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
8992
+ ).all(fileFilter);
8795
8993
  if (syms.length === 0) return { nodes: [], edges: [] };
8796
- const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
8994
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
8797
8995
  const relatedIds = new Set(syms.map((symbol) => symbol.id));
8798
8996
  const toGraphNode = (s) => ({
8799
8997
  id: `sym:${s.id}`,
@@ -8849,6 +9047,15 @@ var IndexStore = class _IndexStore {
8849
9047
  refType: bestType
8850
9048
  });
8851
9049
  }
9050
+ const loadedIds = new Set(syms.map((s) => s.id));
9051
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9052
+ if (missingIds.length > 0) {
9053
+ const placeholders = missingIds.map(() => "?").join(",");
9054
+ const extras = this.db.prepare(
9055
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9056
+ ).all(...missingIds);
9057
+ for (const s of extras) symById.set(s.id, s);
9058
+ }
8852
9059
  const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
8853
9060
  const aExternal = a.file === fileFilter ? 0 : 1;
8854
9061
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -8874,6 +9081,7 @@ import { Worker } from "node:worker_threads";
8874
9081
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
8875
9082
  import * as fs14 from "node:fs/promises";
8876
9083
  import * as path19 from "node:path";
9084
+ import { availableParallelism } from "node:os";
8877
9085
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
8878
9086
 
8879
9087
  // src/codebase-index/ts-parser.ts
@@ -8930,8 +9138,7 @@ function extToLang(ext) {
8930
9138
  return null;
8931
9139
  }
8932
9140
  }
8933
- function getSignature(node, sourceFile) {
8934
- const printer = ts.createPrinter({});
9141
+ function getSignature(printer, node, sourceFile) {
8935
9142
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
8936
9143
  return raw.replace(/\s+/g, " ").slice(0, 500);
8937
9144
  }
@@ -8950,28 +9157,14 @@ function getJsDoc(node, sourceFile) {
8950
9157
  }
8951
9158
  return "";
8952
9159
  }
8953
- function hasFunctionLikeAncestor(node) {
8954
- let current = node.parent;
8955
- while (current) {
8956
- if (ts.isFunctionLike(current)) return true;
8957
- current = current.parent;
8958
- }
8959
- return false;
8960
- }
8961
- function buildScope(node) {
8962
- const parts = [];
8963
- let current = node.parent;
8964
- while (current) {
8965
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
8966
- parts.unshift(current.name?.text ?? "Anon");
8967
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
8968
- if (current.name && ts.isIdentifier(current.name)) {
8969
- parts.unshift(current.name.text);
8970
- }
9160
+ function pushScopeName(node, parts) {
9161
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
9162
+ parts.push(node.name?.text ?? "Anon");
9163
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
9164
+ if (node.name && ts.isIdentifier(node.name)) {
9165
+ parts.push(node.name.text);
8971
9166
  }
8972
- current = current.parent;
8973
9167
  }
8974
- return parts.join(".");
8975
9168
  }
8976
9169
  function parseSymbols(opts) {
8977
9170
  const { file, content, lang } = opts;
@@ -8982,45 +9175,39 @@ function parseSymbols(opts) {
8982
9175
  return { file, lang, symbols: [], mtimeMs: Date.now() };
8983
9176
  }
8984
9177
  const symbols = [];
8985
- function visit(node) {
9178
+ const refs = [];
9179
+ const printer = ts.createPrinter({});
9180
+ function visit(node, funcDepth, scopeParts) {
8986
9181
  const kind = kindOf(node);
8987
9182
  if (kind) {
8988
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
8989
- ts.forEachChild(node, visit);
8990
- return;
9183
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
9184
+ } else {
9185
+ const nameNode = node.name;
9186
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
9187
+ return;
9188
+ }
9189
+ const name = nameNode.text;
9190
+ const pos2 = nameNode.getStart(sourceFile);
9191
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
9192
+ const scope = scopeParts.join(".");
9193
+ const signature = getSignature(printer, node, sourceFile);
9194
+ const docComment = getJsDoc(node, sourceFile);
9195
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
9196
+ symbols.push({
9197
+ id: 0,
9198
+ lang,
9199
+ kind,
9200
+ name,
9201
+ file,
9202
+ line: line2 + 1,
9203
+ col: character,
9204
+ signature,
9205
+ docComment,
9206
+ scope,
9207
+ text
9208
+ });
8991
9209
  }
8992
- const nameNode = node.name;
8993
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
8994
- const name = nameNode.text;
8995
- const pos = nameNode.getStart(sourceFile);
8996
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
8997
- const scope = buildScope(node);
8998
- const signature = getSignature(node, sourceFile);
8999
- const docComment = getJsDoc(node, sourceFile);
9000
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
9001
- symbols.push({
9002
- id: 0,
9003
- lang,
9004
- kind,
9005
- name,
9006
- file,
9007
- line: line + 1,
9008
- col: character,
9009
- signature,
9010
- docComment,
9011
- scope,
9012
- text
9013
- });
9014
9210
  }
9015
- ts.forEachChild(node, visit);
9016
- }
9017
- visit(sourceFile);
9018
- const refs = extractRefs(sourceFile);
9019
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
9020
- }
9021
- function extractRefs(sourceFile) {
9022
- const refs = [];
9023
- function visit(node) {
9024
9211
  const pos = node.getStart(sourceFile);
9025
9212
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
9026
9213
  const lineNum = line + 1;
@@ -9045,10 +9232,14 @@ function extractRefs(sourceFile) {
9045
9232
  const moduleName = getModuleName(node);
9046
9233
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
9047
9234
  }
9048
- ts.forEachChild(node, visit);
9235
+ const scopeIdx = scopeParts.length;
9236
+ pushScopeName(node, scopeParts);
9237
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
9238
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
9239
+ scopeParts.length = scopeIdx;
9049
9240
  }
9050
- visit(sourceFile);
9051
- return deduplicateRefs(refs);
9241
+ visit(sourceFile, 0, []);
9242
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
9052
9243
  }
9053
9244
  function getTypeName(name) {
9054
9245
  if (ts.isIdentifier(name)) return name.text;
@@ -9933,9 +10124,9 @@ function parseSymbols5(opts) {
9933
10124
  function regexParse2(opts) {
9934
10125
  const { file, content, lang } = opts;
9935
10126
  const symbols = [];
9936
- const basename9 = path17.basename(file).toLowerCase();
9937
- const isPackageJson = basename9 === "package.json";
9938
- const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
10127
+ const basename10 = path17.basename(file).toLowerCase();
10128
+ const isPackageJson = basename10 === "package.json";
10129
+ const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
9939
10130
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9940
10131
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9941
10132
  const lines = content.split("\n");
@@ -10330,7 +10521,7 @@ async function loadGitignoreMatcher(projectRoot) {
10330
10521
 
10331
10522
  // src/codebase-index/indexer.ts
10332
10523
  var YIELD_EVERY_N = 50;
10333
- var PARALLEL_BATCH = 20;
10524
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
10334
10525
  function yieldEventLoop() {
10335
10526
  return new Promise((resolve14) => setImmediate(resolve14));
10336
10527
  }
@@ -10508,11 +10699,14 @@ async function runIndexerWithStore(store, opts) {
10508
10699
  if (!force) {
10509
10700
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10510
10701
  }
10702
+ let filesSinceLastYield = 0;
10511
10703
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10512
10704
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
10513
10705
  const batchFiles = files.slice(batchStart, batchEnd);
10514
10706
  opts.onProgress?.(batchEnd, files.length);
10515
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
10707
+ filesSinceLastYield += batchFiles.length;
10708
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
10709
+ filesSinceLastYield = 0;
10516
10710
  await yieldEventLoop();
10517
10711
  throwIfAborted(signal);
10518
10712
  }
@@ -10723,7 +10917,7 @@ async function indexService(args, hooks = {}) {
10723
10917
  });
10724
10918
  }
10725
10919
  function searchService(args) {
10726
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10920
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10727
10921
  try {
10728
10922
  return store.searchRanked(
10729
10923
  args.query,
@@ -10736,15 +10930,15 @@ function searchService(args) {
10736
10930
  args.limit
10737
10931
  );
10738
10932
  } finally {
10739
- store.close();
10933
+ indexStorePool.release(store);
10740
10934
  }
10741
10935
  }
10742
10936
  function statsService(args) {
10743
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10937
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10744
10938
  try {
10745
10939
  return store.getStats();
10746
10940
  } finally {
10747
- store.close();
10941
+ indexStorePool.release(store);
10748
10942
  }
10749
10943
  }
10750
10944
 
@@ -13086,8 +13280,243 @@ function levelRank(level) {
13086
13280
  }
13087
13281
  }
13088
13282
 
13283
+ // src/exec-kill-guard.ts
13284
+ import * as os7 from "node:os";
13285
+ import * as path24 from "node:path";
13286
+ var isWin3 = os7.platform() === "win32";
13287
+ async function checkExecKillCommand(cmd, args) {
13288
+ if (!cmd) return { blocked: false };
13289
+ const cmdLower = cmd.toLowerCase().trim();
13290
+ const fullCommand = [cmdLower, ...args].join(" ").replace(/\s+/g, " ").trim();
13291
+ if (isWin3) {
13292
+ if (cmdLower === "taskkill" || cmdLower === "taskkill.exe") {
13293
+ const hasForce = args.some((a) => a.toUpperCase() === "/F" || a.toUpperCase() === "-F");
13294
+ const signal = hasForce ? "FORCE" : "TERM";
13295
+ for (let i = 0; i < args.length; i++) {
13296
+ const a = args[i];
13297
+ if (a.toUpperCase() === "/IM" || a.toUpperCase() === "-IM") {
13298
+ const nameArg = args[i + 1];
13299
+ if (nameArg) {
13300
+ const result = await checkKillTarget({ name: nameArg, signal, cmd: fullCommand });
13301
+ if (result.blocked) return result;
13302
+ }
13303
+ }
13304
+ }
13305
+ for (let i = 0; i < args.length; i++) {
13306
+ const a = args[i];
13307
+ if (a.toUpperCase() === "/PID" || a.toUpperCase() === "-PID") {
13308
+ const pidArg = args[i + 1];
13309
+ if (pidArg && /^\d+$/.test(pidArg)) {
13310
+ const result = await checkKillTarget({
13311
+ pid: parseInt(pidArg, 10),
13312
+ signal,
13313
+ cmd: fullCommand
13314
+ });
13315
+ if (result.blocked) return result;
13316
+ }
13317
+ }
13318
+ }
13319
+ for (let i = 0; i < args.length; i++) {
13320
+ const a = args[i];
13321
+ if (a.toUpperCase() === "/FI" || a.toUpperCase() === "-FI") {
13322
+ const filterArg = args[i + 1];
13323
+ if (filterArg) {
13324
+ const nameMatch = filterArg.match(/IMAGENAME\s+eq\s+"?([^"\s]+)/i);
13325
+ if (nameMatch?.[1]) {
13326
+ const result = await checkKillTarget({ name: nameMatch[1], signal, cmd: fullCommand });
13327
+ if (result.blocked) return result;
13328
+ }
13329
+ }
13330
+ }
13331
+ }
13332
+ return { blocked: false };
13333
+ }
13334
+ if (cmdLower === "powershell" || cmdLower === "powershell.exe" || cmdLower === "pwsh" || cmdLower === "pwsh.exe" || cmdLower === "cmd" || cmdLower === "cmd.exe") {
13335
+ const shellFlagIndex = args.findIndex((arg) => {
13336
+ const lower = arg.toLowerCase();
13337
+ return lower === "-c" || lower === "-command" || lower === "/c";
13338
+ });
13339
+ if (shellFlagIndex >= 0) {
13340
+ const innerTokens = tokenizeShellCommand(args.slice(shellFlagIndex + 1).join(" "));
13341
+ const innerCommand = innerTokens[0];
13342
+ if (innerCommand) {
13343
+ const result = await checkExecKillCommand(innerCommand, innerTokens.slice(1));
13344
+ if (result.blocked) return result;
13345
+ }
13346
+ }
13347
+ }
13348
+ if (cmdLower === "stop-process" || cmdLower === "kill") {
13349
+ for (let i = 0; i < args.length; i++) {
13350
+ const a = args[i];
13351
+ if (a === "-Name" || a === "-n") {
13352
+ const nameArg = args[i + 1]?.replace(/^['"]|['"]$/g, "");
13353
+ if (nameArg) {
13354
+ const result = await checkKillTarget({
13355
+ name: nameArg,
13356
+ signal: "FORCE",
13357
+ cmd: fullCommand
13358
+ });
13359
+ if (result.blocked) return result;
13360
+ }
13361
+ }
13362
+ if (a === "-Id" || a === "-PID" || a === "-pid") {
13363
+ const pidArg = args[i + 1];
13364
+ if (pidArg && /^\d+$/.test(pidArg)) {
13365
+ const result = await checkKillTarget({
13366
+ pid: parseInt(pidArg, 10),
13367
+ signal: "FORCE",
13368
+ cmd: fullCommand
13369
+ });
13370
+ if (result.blocked) return result;
13371
+ }
13372
+ }
13373
+ }
13374
+ const firstNonFlag = args.find((a) => !a.startsWith("-"));
13375
+ if (firstNonFlag) {
13376
+ const name = firstNonFlag.replace(/^['"]|['"]$/g, "");
13377
+ const result = await checkKillTarget({ name, signal: "TERM", cmd: fullCommand });
13378
+ if (result.blocked) return result;
13379
+ }
13380
+ }
13381
+ if (cmdLower === "wmic" || cmdLower === "wmic.exe") {
13382
+ const joined = args.join(" ").toLowerCase();
13383
+ if (/\bprocess\b/.test(joined) && /\bdelete\b/.test(joined)) {
13384
+ const nameMatch = joined.match(/name\s*=\s*['"]?([^'"]+)/);
13385
+ if (nameMatch?.[1]) {
13386
+ const result = await checkKillTarget({
13387
+ name: nameMatch[1].trim(),
13388
+ signal: "FORCE",
13389
+ cmd: fullCommand
13390
+ });
13391
+ if (result.blocked) return result;
13392
+ }
13393
+ return {
13394
+ blocked: true,
13395
+ reason: "Blocked: wmic process delete targets all matched processes \u2014 would include protected WrongStack processes."
13396
+ };
13397
+ }
13398
+ }
13399
+ if (cmdLower === "node" || cmdLower === "node.exe") {
13400
+ if (args.includes("-e") || args.includes("--eval")) {
13401
+ const evalIdx = args.indexOf("-e") !== -1 ? args.indexOf("-e") : args.indexOf("--eval");
13402
+ const evalCode = args[evalIdx + 1] ?? "";
13403
+ if (/\bprocess\.kill\s*\(/.test(evalCode)) {
13404
+ const pidMatch = evalCode.match(/process\.kill\s*\(\s*(\d+)/);
13405
+ if (pidMatch?.[1]) {
13406
+ const pid = parseInt(pidMatch[1], 10);
13407
+ const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
13408
+ if (result.blocked) return result;
13409
+ }
13410
+ return {
13411
+ blocked: true,
13412
+ reason: "Blocked: node -e with process.kill() \u2014 would target protected WrongStack process(es)."
13413
+ };
13414
+ }
13415
+ }
13416
+ }
13417
+ } else {
13418
+ if (cmdLower === "kill") {
13419
+ for (const a of args) {
13420
+ const num = a.replace(/^-/, "");
13421
+ if (/^\d+$/.test(num)) {
13422
+ const pid = parseInt(num, 10);
13423
+ const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
13424
+ if (result.blocked) return result;
13425
+ }
13426
+ }
13427
+ }
13428
+ if (cmdLower === "pkill" || cmdLower === "killall") {
13429
+ const firstNonFlag = args.find((a) => !a.startsWith("-"));
13430
+ if (firstNonFlag) {
13431
+ const result = await checkKillTarget({
13432
+ name: firstNonFlag,
13433
+ signal: "SIGTERM",
13434
+ cmd: fullCommand
13435
+ });
13436
+ if (result.blocked) return result;
13437
+ }
13438
+ }
13439
+ }
13440
+ return { blocked: false };
13441
+ }
13442
+ function tokenizeShellCommand(command) {
13443
+ const tokens = [];
13444
+ let current = "";
13445
+ let quote = null;
13446
+ for (const char of command.trim()) {
13447
+ if (quote) {
13448
+ if (char === quote) quote = null;
13449
+ else current += char;
13450
+ continue;
13451
+ }
13452
+ if (char === '"' || char === "'") {
13453
+ quote = char;
13454
+ } else if (/\s/.test(char)) {
13455
+ if (current) {
13456
+ tokens.push(current);
13457
+ current = "";
13458
+ }
13459
+ } else {
13460
+ current += char;
13461
+ }
13462
+ }
13463
+ if (current) tokens.push(current);
13464
+ return tokens;
13465
+ }
13466
+ async function checkKillTarget(target) {
13467
+ const registry = getPersistentProcessRegistry();
13468
+ if (target.pid !== void 0) {
13469
+ const blocked = await registry.shouldBlockKill(target.pid);
13470
+ if (blocked) {
13471
+ return {
13472
+ blocked: true,
13473
+ reason: `Blocked: kill ${target.signal} PID ${target.pid} targets a protected WrongStack process (${target.cmd.slice(0, 80)}).`
13474
+ };
13475
+ }
13476
+ if (target.pid === process.pid) {
13477
+ return {
13478
+ blocked: true,
13479
+ reason: "Blocked: cannot kill the current WrongStack process."
13480
+ };
13481
+ }
13482
+ if (target.pid === process.ppid) {
13483
+ return {
13484
+ blocked: true,
13485
+ reason: "Blocked: cannot kill the parent terminal hosting WrongStack."
13486
+ };
13487
+ }
13488
+ return { blocked: false };
13489
+ }
13490
+ if (target.name) {
13491
+ const nameLower = target.name.toLowerCase().replace(/\.exe$/, "");
13492
+ if (nameLower.includes("wrongstack")) {
13493
+ return {
13494
+ blocked: true,
13495
+ reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
13496
+ };
13497
+ }
13498
+ const currentImage = path24.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
13499
+ const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
13500
+ if (targetsNodeRuntime && currentImage === "node") {
13501
+ return {
13502
+ blocked: true,
13503
+ reason: `Blocked: kill ${target.signal} '${target.name}' would kill the active WrongStack node.exe runtime.`
13504
+ };
13505
+ }
13506
+ const protectedPids = await registry.getAllProtectedPids();
13507
+ if (protectedPids.length > 0 && targetsNodeRuntime) {
13508
+ return {
13509
+ blocked: true,
13510
+ reason: `Blocked: kill ${target.signal} '${target.name}' would kill all node.exe processes including active WrongStack instance(s).`
13511
+ };
13512
+ }
13513
+ return { blocked: false };
13514
+ }
13515
+ return { blocked: false };
13516
+ }
13517
+
13089
13518
  // src/exec.ts
13090
- var isWin3 = process.platform === "win32";
13519
+ var isWin4 = process.platform === "win32";
13091
13520
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
13092
13521
  // JS / TS toolchain
13093
13522
  "node",
@@ -13863,6 +14292,19 @@ var execTool = {
13863
14292
  const args = (input.args ?? []).slice(0, MAX_ARGS);
13864
14293
  const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3));
13865
14294
  const danger = detectDanger(cmd, args, dangerBypass);
14295
+ const killCheck = await checkExecKillCommand(cmd, args);
14296
+ if (killCheck.blocked) {
14297
+ return {
14298
+ command: cmd,
14299
+ args,
14300
+ stdout: "",
14301
+ stderr: killCheck.reason ?? "Kill command blocked: targets a protected WrongStack process.",
14302
+ exitCode: 1,
14303
+ truncated: false,
14304
+ allowed: false,
14305
+ danger
14306
+ };
14307
+ }
13866
14308
  const argError = validateArgs(cmd, args);
13867
14309
  if (argError) {
13868
14310
  return {
@@ -13913,7 +14355,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13913
14355
  let timedOut = false;
13914
14356
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13915
14357
  const resolved = resolveWin32Command(cmd);
13916
- const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
14358
+ const needsShell = isWin4 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13917
14359
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13918
14360
  const spawnCmd = shim?.command ?? resolved;
13919
14361
  const spawnArgs = shim?.args ?? args;
@@ -13938,7 +14380,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13938
14380
  env: buildChildEnv2(sessionId),
13939
14381
  stdio: ["ignore", "pipe", "pipe"],
13940
14382
  windowsHide: true,
13941
- ...isWin3 ? {} : { signal },
14383
+ ...isWin4 ? {} : { signal },
13942
14384
  ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
13943
14385
  });
13944
14386
  } catch (err) {
@@ -13977,7 +14419,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13977
14419
  const isAbort = err && err.code === "ABORT_ERR";
13978
14420
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
13979
14421
  clearTimeout(timer);
13980
- if (isWin3) signal.removeEventListener("abort", onAbort);
14422
+ if (isWin4) signal.removeEventListener("abort", onAbort);
13981
14423
  if (typeof pid === "number") registry.unregister(pid);
13982
14424
  registry.afterCall(Date.now() - startedAt, true);
13983
14425
  spool.finalize();
@@ -14010,7 +14452,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14010
14452
  if (typeof pid === "number") registry.kill(pid, { force: true });
14011
14453
  else child.kill("SIGTERM");
14012
14454
  };
14013
- if (isWin3) {
14455
+ if (isWin4) {
14014
14456
  if (signal.aborted) onAbort();
14015
14457
  else signal.addEventListener("abort", onAbort, { once: true });
14016
14458
  }
@@ -14030,7 +14472,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14030
14472
  });
14031
14473
  child.on("close", (code) => {
14032
14474
  clearTimeout(timer);
14033
- if (isWin3) signal.removeEventListener("abort", onAbort);
14475
+ if (isWin4) signal.removeEventListener("abort", onAbort);
14034
14476
  if (typeof pid === "number") registry.unregister(pid);
14035
14477
  const durationMs = Date.now() - startedAt;
14036
14478
  const exitCode = killed ? 124 : code ?? 1;
@@ -14841,7 +15283,7 @@ function runGit2(args, cwd, signal) {
14841
15283
 
14842
15284
  // src/glob.ts
14843
15285
  import * as fs20 from "node:fs/promises";
14844
- import * as path24 from "node:path";
15286
+ import * as path25 from "node:path";
14845
15287
  import { compileGlob as compileGlob3 } from "@wrongstack/core";
14846
15288
 
14847
15289
  // src/_concurrency.ts
@@ -14945,7 +15387,7 @@ var globTool = {
14945
15387
  if (DEFAULT_IGNORE2.includes(name)) continue;
14946
15388
  if (ignored.includes(name)) continue;
14947
15389
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
14948
- const full = path24.join(dir, name);
15390
+ const full = path25.join(dir, name);
14949
15391
  if (e.isDirectory()) {
14950
15392
  subdirs.push({ full, rel });
14951
15393
  } else if (e.isFile()) {
@@ -14989,7 +15431,7 @@ var globTool = {
14989
15431
  };
14990
15432
  async function readGitignore(dir) {
14991
15433
  try {
14992
- const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
15434
+ const raw = await fs20.readFile(path25.join(dir, ".gitignore"), "utf8");
14993
15435
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
14994
15436
  } catch {
14995
15437
  return [];
@@ -15000,7 +15442,7 @@ async function readGitignore(dir) {
15000
15442
  import { expectDefined as expectDefined7 } from "@wrongstack/core";
15001
15443
  import { spawn as spawn10 } from "node:child_process";
15002
15444
  import * as fs21 from "node:fs/promises";
15003
- import * as path25 from "node:path";
15445
+ import * as path26 from "node:path";
15004
15446
  import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
15005
15447
 
15006
15448
  // src/_regex.ts
@@ -15393,7 +15835,7 @@ async function runNative(input, base, mode, limit, signal) {
15393
15835
  if (stopped) return;
15394
15836
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
15395
15837
  if (e.isSymbolicLink()) continue;
15396
- const full = path25.join(dir, e.name);
15838
+ const full = path26.join(dir, e.name);
15397
15839
  if (e.isDirectory()) {
15398
15840
  subdirs.push(full);
15399
15841
  } else if (e.isFile()) {
@@ -15981,56 +16423,56 @@ function jmespathSearch(data, query) {
15981
16423
  }
15982
16424
  function validateJsonSchema(data, schema) {
15983
16425
  const errors = [];
15984
- function check(value, s, path32) {
16426
+ function check(value, s, path33) {
15985
16427
  if (s["type"]) {
15986
16428
  const expectedType = s["type"];
15987
16429
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
15988
16430
  if (expectedType === "integer") {
15989
- if (!Number.isInteger(value)) errors.push(`${path32}: expected integer, got ${actualType}`);
16431
+ if (!Number.isInteger(value)) errors.push(`${path33}: expected integer, got ${actualType}`);
15990
16432
  } else if (expectedType !== actualType) {
15991
- errors.push(`${path32}: expected ${expectedType}, got ${actualType}`);
16433
+ errors.push(`${path33}: expected ${expectedType}, got ${actualType}`);
15992
16434
  }
15993
16435
  }
15994
16436
  if (typeof value === "string" && s["format"] === "uri" && value) {
15995
16437
  try {
15996
16438
  new URL(value);
15997
16439
  } catch {
15998
- errors.push(`${path32}: not a valid URI`);
16440
+ errors.push(`${path33}: not a valid URI`);
15999
16441
  }
16000
16442
  }
16001
16443
  if (typeof value === "string" && s["pattern"]) {
16002
16444
  const re = new RegExp(s["pattern"]);
16003
- if (!re.test(value)) errors.push(`${path32}: does not match pattern ${s["pattern"]}`);
16445
+ if (!re.test(value)) errors.push(`${path33}: does not match pattern ${s["pattern"]}`);
16004
16446
  }
16005
16447
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
16006
- errors.push(`${path32}: string too short (min ${s["minLength"]})`);
16448
+ errors.push(`${path33}: string too short (min ${s["minLength"]})`);
16007
16449
  }
16008
16450
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
16009
- errors.push(`${path32}: string too long (max ${s["maxLength"]})`);
16451
+ errors.push(`${path33}: string too long (max ${s["maxLength"]})`);
16010
16452
  }
16011
16453
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
16012
- errors.push(`${path32}: below minimum ${s["minimum"]}`);
16454
+ errors.push(`${path33}: below minimum ${s["minimum"]}`);
16013
16455
  }
16014
16456
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
16015
- errors.push(`${path32}: above maximum ${s["maximum"]}`);
16457
+ errors.push(`${path33}: above maximum ${s["maximum"]}`);
16016
16458
  }
16017
16459
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
16018
16460
  for (let i = 0; i < value.length; i++) {
16019
- check(value[i], s["items"], `${path32}[${i}]`);
16461
+ check(value[i], s["items"], `${path33}[${i}]`);
16020
16462
  }
16021
16463
  }
16022
16464
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
16023
16465
  const props = s["properties"];
16024
16466
  for (const [k, propSchema] of Object.entries(props)) {
16025
- check(value[k], propSchema, `${path32}.${k}`);
16467
+ check(value[k], propSchema, `${path33}.${k}`);
16026
16468
  }
16027
16469
  }
16028
16470
  }
16029
16471
  check(data, schema, "$");
16030
16472
  return { valid: errors.length === 0, errors };
16031
16473
  }
16032
- function simpleQuery(data, path32) {
16033
- const parts = path32.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16474
+ function simpleQuery(data, path33) {
16475
+ const parts = path33.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16034
16476
  let current = data;
16035
16477
  for (const part of parts) {
16036
16478
  if (current === null || current === void 0) return void 0;
@@ -16098,7 +16540,7 @@ import {
16098
16540
  duplicateBoard,
16099
16541
  exportBoardAsMarkdown,
16100
16542
  exportBoardToTaskGraph,
16101
- generateBoardFromDescription,
16543
+ createBoardFromText,
16102
16544
  getBoard as getBoard2,
16103
16545
  getKanbanOrchestrationSnapshot,
16104
16546
  getKanbanQueueHealth,
@@ -16622,7 +17064,7 @@ var kanbanTool = {
16622
17064
  }
16623
17065
  case "generate_board": {
16624
17066
  if (!input.description) return fail("generate_board requires description.");
16625
- const boardInput = generateBoardFromDescription({
17067
+ const boardInput = createBoardFromText({
16626
17068
  description: input.description,
16627
17069
  ...input.title !== void 0 ? { title: input.title } : {},
16628
17070
  ...input.context !== void 0 ? { context: input.context } : {},
@@ -17007,20 +17449,31 @@ var kanbanTool = {
17007
17449
  if (!input.boardId || !input.taskId)
17008
17450
  return fail("mark_assignment requires boardId and taskId.");
17009
17451
  const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17010
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17011
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17012
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17013
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17014
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17015
- ...input.error !== void 0 ? { error: input.error } : {},
17016
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17017
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17018
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17019
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17020
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17021
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17022
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17023
- });
17452
+ const board = await updateTaskAssignment(
17453
+ projectRoot,
17454
+ input.boardId,
17455
+ input.taskId,
17456
+ {
17457
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17458
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17459
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17460
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17461
+ ...input.error !== void 0 ? { error: input.error } : {},
17462
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17463
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17464
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17465
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17466
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17467
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17468
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17469
+ },
17470
+ // Ownership fence: when expectedLeaseId is supplied, the write is
17471
+ // applied only if the current assignment still holds this lease.
17472
+ // This prevents a recovered+reassigned stale worker's terminal
17473
+ // mark_assignment from overwriting the successor's state. The check
17474
+ // is atomic inside updateTaskAssignment's mutateBoard lock.
17475
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17476
+ );
17024
17477
  return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17025
17478
  }
17026
17479
  case "heartbeat_assignment": {
@@ -17029,7 +17482,13 @@ var kanbanTool = {
17029
17482
  }
17030
17483
  const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17031
17484
  ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17032
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17485
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17486
+ // Ownership fence: when expectedLeaseId is supplied, the renewal
17487
+ // is applied only if the current assignment still holds this lease.
17488
+ // This prevents a recovered+reassigned stale worker's heartbeat
17489
+ // from renewing the successor's lease. The check is atomic inside
17490
+ // heartbeatTaskAssignment's mutateBoard lock.
17491
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17033
17492
  });
17034
17493
  return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17035
17494
  }
@@ -17620,7 +18079,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
17620
18079
  }
17621
18080
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
17622
18081
  var MAX_TAIL_LINES = 1e5;
17623
- async function fileLogs(path32, lines, filterRe, stream) {
18082
+ async function fileLogs(path33, lines, filterRe, stream) {
17624
18083
  const { createInterface } = await import("node:readline");
17625
18084
  const { createReadStream: createReadStream2 } = await import("node:fs");
17626
18085
  const entries = [];
@@ -17629,7 +18088,7 @@ async function fileLogs(path32, lines, filterRe, stream) {
17629
18088
  let writeIdx = 0;
17630
18089
  let totalLines = 0;
17631
18090
  const rl = createInterface({
17632
- input: createReadStream2(path32),
18091
+ input: createReadStream2(path33),
17633
18092
  crlfDelay: Number.POSITIVE_INFINITY
17634
18093
  });
17635
18094
  for await (const line of rl) {
@@ -17650,7 +18109,7 @@ async function fileLogs(path32, lines, filterRe, stream) {
17650
18109
  if (parsed) entries.push(parsed);
17651
18110
  }
17652
18111
  return {
17653
- source: path32,
18112
+ source: path33,
17654
18113
  entries,
17655
18114
  total: entries.length,
17656
18115
  truncated: totalLines > effLines,
@@ -17881,8 +18340,8 @@ function parseOutdatedOutput(json2, exitCode) {
17881
18340
  init_util();
17882
18341
  import { spawn as spawn13 } from "node:child_process";
17883
18342
  import * as fs23 from "node:fs/promises";
17884
- import * as os7 from "node:os";
17885
- import * as path26 from "node:path";
18343
+ import * as os8 from "node:os";
18344
+ import * as path27 from "node:path";
17886
18345
  import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
17887
18346
  var patchTool = {
17888
18347
  name: "patch",
@@ -17918,9 +18377,9 @@ var patchTool = {
17918
18377
  for (const t of targets) {
17919
18378
  const stripped = stripPathComponents(t, strip);
17920
18379
  if (!stripped) continue;
17921
- const candidate = path26.resolve(dir, stripped);
17922
- const rel = path26.relative(ctx.projectRoot, candidate);
17923
- if (rel.startsWith("..") || path26.isAbsolute(rel)) {
18380
+ const candidate = path27.resolve(dir, stripped);
18381
+ const rel = path27.relative(ctx.projectRoot, candidate);
18382
+ if (rel.startsWith("..") || path27.isAbsolute(rel)) {
17924
18383
  return {
17925
18384
  applied: 0,
17926
18385
  rejected: 1,
@@ -17937,11 +18396,11 @@ var patchTool = {
17937
18396
  beforeContents.set(target, await readTextForTracking(target));
17938
18397
  }
17939
18398
  }
17940
- const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
18399
+ const tmpDir = await fs23.mkdtemp(path27.join(os8.tmpdir(), ".wstack_patch_"));
17941
18400
  try {
17942
18401
  await fs23.chmod(tmpDir, 448).catch(() => {
17943
18402
  });
17944
- const patchFile = path26.join(tmpDir, "in.diff");
18403
+ const patchFile = path27.join(tmpDir, "in.diff");
17945
18404
  await fs23.writeFile(patchFile, input.patch, { mode: 384 });
17946
18405
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
17947
18406
  const result = await runPatch(args, dir, opts.signal);
@@ -18527,7 +18986,7 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
18527
18986
  import { expectDefined as expectDefined8 } from "@wrongstack/core";
18528
18987
  import { spawn as spawn14 } from "node:child_process";
18529
18988
  import * as fs25 from "node:fs/promises";
18530
- import * as path27 from "node:path";
18989
+ import * as path28 from "node:path";
18531
18990
  import {
18532
18991
  atomicWrite as atomicWrite3,
18533
18992
  buildChildEnv as buildChildEnv9,
@@ -18616,8 +19075,8 @@ var replaceTool = {
18616
19075
  } catch {
18617
19076
  continue;
18618
19077
  }
18619
- const rel = path27.relative(realRoot, realPath);
18620
- if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
19078
+ const rel = path28.relative(realRoot, realPath);
19079
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
18621
19080
  const stat17 = await fs25.stat(realPath).catch(() => null);
18622
19081
  if (!stat17?.isFile()) continue;
18623
19082
  let content;
@@ -18746,7 +19205,7 @@ async function globNative(pattern, base, extraGlob) {
18746
19205
  }
18747
19206
  for (const e of entries) {
18748
19207
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
18749
- const full = path27.join(dir, e.name);
19208
+ const full = path28.join(dir, e.name);
18750
19209
  try {
18751
19210
  const stat17 = await fs25.lstat(full);
18752
19211
  if (stat17.isSymbolicLink()) continue;
@@ -18773,7 +19232,7 @@ async function globNative(pattern, base, extraGlob) {
18773
19232
  // src/scaffold.ts
18774
19233
  init_util();
18775
19234
  import * as fs26 from "node:fs/promises";
18776
- import * as path28 from "node:path";
19235
+ import * as path29 from "node:path";
18777
19236
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
18778
19237
  var BUILT_IN_TEMPLATES = {
18779
19238
  "npm-package": {
@@ -18923,16 +19382,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
18923
19382
  let filesCreated = 0;
18924
19383
  for (const [filePath, content] of Object.entries(templateFiles)) {
18925
19384
  const resolvedPath = substituteVars(filePath, name, vars);
18926
- const joinedPath = path28.join(cwd, resolvedPath);
18927
- const root = path28.resolve(ctx.projectRoot);
18928
- const target = path28.resolve(joinedPath);
18929
- const rel = path28.relative(root, target);
18930
- if (rel.startsWith("..") || path28.isAbsolute(rel)) {
19385
+ const joinedPath = path29.join(cwd, resolvedPath);
19386
+ const root = path29.resolve(ctx.projectRoot);
19387
+ const target = path29.resolve(joinedPath);
19388
+ const rel = path29.relative(root, target);
19389
+ if (rel.startsWith("..") || path29.isAbsolute(rel)) {
18931
19390
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
18932
19391
  }
18933
19392
  const fullPath = target;
18934
19393
  if (!dryRun) {
18935
- await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
19394
+ await fs26.mkdir(path29.dirname(fullPath), { recursive: true });
18936
19395
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
18937
19396
  }
18938
19397
  files.push(resolvedPath);
@@ -19789,7 +20248,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
19789
20248
  init_spawn_stream();
19790
20249
  init_util();
19791
20250
  init_legacy_bridge();
19792
- import * as path29 from "node:path";
20251
+ import * as path30 from "node:path";
19793
20252
  var testTool = {
19794
20253
  name: "test",
19795
20254
  category: "Code Quality",
@@ -19896,7 +20355,7 @@ async function detectRunner(cwd) {
19896
20355
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
19897
20356
  for (const f of candidates) {
19898
20357
  try {
19899
- await stat17(path29.join(cwd, f));
20358
+ await stat17(path30.join(cwd, f));
19900
20359
  if (f.includes("vitest")) return "vitest";
19901
20360
  if (f.includes("jest")) return "jest";
19902
20361
  if (f.includes("mocha")) return "mocha";
@@ -20367,7 +20826,7 @@ var toolUseTool = {
20367
20826
  init_util();
20368
20827
  import { expectDefined as expectDefined10 } from "@wrongstack/core";
20369
20828
  import * as fs28 from "node:fs/promises";
20370
- import * as path30 from "node:path";
20829
+ import * as path31 from "node:path";
20371
20830
  var DEFAULT_IGNORE5 = [
20372
20831
  "node_modules",
20373
20832
  ".git",
@@ -20538,7 +20997,7 @@ async function walkDir(dir, depth, opts) {
20538
20997
  opts.lines.push(opts.prefix + branch + displayName);
20539
20998
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
20540
20999
  const childPrefix = opts.prefix + connector;
20541
- await walkDir(path30.join(dir, entry.name), depth + 1, {
21000
+ await walkDir(path31.join(dir, entry.name), depth + 1, {
20542
21001
  ...opts,
20543
21002
  prefix: childPrefix,
20544
21003
  isLast
@@ -20551,7 +21010,7 @@ async function walkDir(dir, depth, opts) {
20551
21010
  init_spawn_stream();
20552
21011
  init_util();
20553
21012
  init_legacy_bridge();
20554
- import * as path31 from "node:path";
21013
+ import * as path32 from "node:path";
20555
21014
  var typecheckTool = {
20556
21015
  name: "typecheck",
20557
21016
  category: "Code Quality",
@@ -20654,8 +21113,8 @@ async function findTsConfig(cwd) {
20654
21113
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
20655
21114
  for (const f of candidates) {
20656
21115
  try {
20657
- const s = await stat17(path31.join(cwd, f));
20658
- if (s.isFile()) return path31.join(cwd, f);
21116
+ const s = await stat17(path32.join(cwd, f));
21117
+ if (s.isFile()) return path32.join(cwd, f);
20659
21118
  } catch {
20660
21119
  }
20661
21120
  }