@wrongstack/tools 0.274.0 → 0.275.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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs7 from 'node:fs/promises';
2
2
  import * as Core from '@wrongstack/core';
3
- import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as path from 'node:path';
5
5
  import { resolve, sep, dirname, join } from 'node:path';
6
6
  import { spawn, execFileSync } from 'node:child_process';
@@ -375,7 +375,7 @@ var writeTool = {
375
375
  if (existed) {
376
376
  if (!ctx.hasRead(absPath)) {
377
377
  prev = await fs7.readFile(absPath, "utf8");
378
- ctx.recordRead(absPath, stat12.mtimeMs);
378
+ ctx.recordRead(absPath, stat12.mtimeMs, "write");
379
379
  } else {
380
380
  prev = await fs7.readFile(absPath, "utf8");
381
381
  }
@@ -389,7 +389,7 @@ var writeTool = {
389
389
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
390
390
  + (new file, ${input.content.split("\n").length} lines)`;
391
391
  const stat11 = await fs7.stat(absPath);
392
- ctx.recordRead(absPath, stat11.mtimeMs);
392
+ ctx.recordRead(absPath, stat11.mtimeMs, "write");
393
393
  ctx.session.recordFileChange({
394
394
  path: absPath,
395
395
  action: existed ? "modified" : "created",
@@ -486,7 +486,7 @@ var editTool = {
486
486
  const newFile = toStyle(newFileLf, style);
487
487
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
488
488
  const written = await fs7.stat(absPath);
489
- ctx.recordRead(absPath, written.mtimeMs);
489
+ ctx.recordRead(absPath, written.mtimeMs, "write");
490
490
  ctx.session.recordFileChange({
491
491
  path: absPath,
492
492
  action: "modified",
@@ -1522,6 +1522,7 @@ var CircuitBreaker = class {
1522
1522
  if (this.state === "open") return;
1523
1523
  this.state = "open";
1524
1524
  this.openedAt = Date.now();
1525
+ this.window = [];
1525
1526
  try {
1526
1527
  this.onTrip?.();
1527
1528
  } catch {
@@ -1561,7 +1562,7 @@ var SENSITIVE_FLAG_PATTERNS = [
1561
1562
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
1562
1563
  // -f "value" style short flags
1563
1564
  /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
1564
- /(?<!\w)-p(?:ssword)?(?:\s+|\s*=\s*)[^\s,]+/gi,
1565
+ /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
1565
1566
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
1566
1567
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
1567
1568
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -1622,12 +1623,35 @@ var ProcessRegistryImpl = class {
1622
1623
  register(info) {
1623
1624
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
1624
1625
  }
1626
+ _isSafeSignalPid(pid) {
1627
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
1628
+ }
1629
+ _canSignalProcessGroup(p) {
1630
+ return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
1631
+ }
1632
+ _killChildDirect(p, signal) {
1633
+ try {
1634
+ p.child.kill(signal);
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ _killPosix(p, signal) {
1639
+ if (this._canSignalProcessGroup(p)) {
1640
+ try {
1641
+ process.kill(-p.pid, signal);
1642
+ return;
1643
+ } catch {
1644
+ }
1645
+ }
1646
+ this._killChildDirect(p, signal);
1647
+ }
1625
1648
  /** Unregister a process by PID. Called on 'close' / 'exit' events. */
1626
1649
  unregister(pid) {
1627
1650
  this.processes.delete(pid);
1628
1651
  }
1629
1652
  /** Get a single process by PID. */
1630
1653
  get(pid) {
1654
+ this._pruneStale(pid);
1631
1655
  return this.processes.get(pid);
1632
1656
  }
1633
1657
  /** Get all tracked processes. */
@@ -1791,13 +1815,14 @@ var ProcessRegistryImpl = class {
1791
1815
  * Returns true if the process was found and kill was attempted.
1792
1816
  */
1793
1817
  kill(pid, opts = {}) {
1818
+ this._pruneStale(pid);
1794
1819
  const p = this.processes.get(pid);
1795
1820
  if (!p) return false;
1796
1821
  if (p.killed) return true;
1797
1822
  if (p.protected) return false;
1798
1823
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1799
- const isWin3 = os2.platform() === "win32";
1800
- if (isWin3) {
1824
+ const isWin4 = os2.platform() === "win32";
1825
+ if (isWin4) {
1801
1826
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
1802
1827
  if (liveRealChild && killWin32Tree(pid)) {
1803
1828
  const fallback = setTimeout(() => {
@@ -1820,27 +1845,12 @@ var ProcessRegistryImpl = class {
1820
1845
  }
1821
1846
  try {
1822
1847
  if (force) {
1823
- try {
1824
- process.kill(-pid, "SIGKILL");
1825
- } catch {
1826
- p.child.kill("SIGKILL");
1827
- }
1848
+ this._killPosix(p, "SIGKILL");
1828
1849
  } else {
1829
- try {
1830
- process.kill(-pid, "SIGTERM");
1831
- } catch {
1832
- p.child.kill("SIGTERM");
1833
- }
1850
+ this._killPosix(p, "SIGTERM");
1834
1851
  const timer = setTimeout(() => {
1835
1852
  if (this.processes.has(pid) && !p.child.killed) {
1836
- try {
1837
- process.kill(-pid, "SIGKILL");
1838
- } catch {
1839
- try {
1840
- p.child.kill("SIGKILL");
1841
- } catch {
1842
- }
1843
- }
1853
+ this._killPosix(p, "SIGKILL");
1844
1854
  }
1845
1855
  }, graceMs);
1846
1856
  timer.unref?.();
@@ -1875,6 +1885,34 @@ var ProcessRegistryImpl = class {
1875
1885
  }
1876
1886
  return killed;
1877
1887
  }
1888
+ /**
1889
+ * Check whether a tracked process entry is stale — the child has exited
1890
+ * (exitCode !== null) AND it's been in the registry long enough that the
1891
+ * OS may have reused the PID for a new, unrelated process.
1892
+ *
1893
+ * P3 #24 (before-release.md): on POSIX, PIDs are reused after process
1894
+ * exit. If a tracked process exits but its 'close' event hasn't fired yet
1895
+ * (or was missed), the registry still holds the entry. A new process
1896
+ * gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
1897
+ * may incorrectly protect or target the wrong process.
1898
+ *
1899
+ * The 60s threshold is conservative — the OS typically waits much longer
1900
+ * before reusing a PID, but we want to clean up before that becomes a risk.
1901
+ */
1902
+ _isStaleEntry(entry) {
1903
+ return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
1904
+ }
1905
+ /**
1906
+ * Remove a stale entry for a specific PID before any PID-based lookup.
1907
+ * This prevents PID reuse from causing the registry to act on a dead
1908
+ * process that has been replaced by a new one with the same PID.
1909
+ */
1910
+ _pruneStale(pid) {
1911
+ const entry = this.processes.get(pid);
1912
+ if (entry && this._isStaleEntry(entry)) {
1913
+ this.processes.delete(pid);
1914
+ }
1915
+ }
1878
1916
  };
1879
1917
  var _registry;
1880
1918
  function getProcessRegistry() {
@@ -2299,17 +2337,18 @@ function resetPersistentProcessRegistry() {
2299
2337
  }
2300
2338
 
2301
2339
  // src/bash-kill-guard.ts
2340
+ var isWin = os2.platform() === "win32";
2302
2341
  function extractKillCommand(command) {
2303
2342
  const normalized = command.replace(/\s+/g, " ").trim();
2304
2343
  const shellCMatch = normalized.match(
2305
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
2344
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
2306
2345
  );
2307
2346
  if (shellCMatch?.[1]) {
2308
2347
  const inner = shellCMatch[1].trim();
2309
2348
  return isKillRelatedCommand(inner) ? inner : null;
2310
2349
  }
2311
2350
  const shellCUnquoted = normalized.match(
2312
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
2351
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
2313
2352
  );
2314
2353
  if (shellCUnquoted?.[1]) {
2315
2354
  return shellCUnquoted[1];
@@ -2318,15 +2357,43 @@ function extractKillCommand(command) {
2318
2357
  }
2319
2358
  function isKillRelatedCommand(cmd) {
2320
2359
  const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
2360
+ if (isWin) {
2361
+ if (/^taskkill\s/i.test(normalized)) return true;
2362
+ if (/^tskill\s/i.test(normalized)) return true;
2363
+ return false;
2364
+ }
2321
2365
  if (/^kill(\s|$)/.test(normalized)) return true;
2322
2366
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
2323
- if (/^taskkill\s/i.test(normalized)) return true;
2324
- if (/^tskill\s/i.test(normalized)) return true;
2325
2367
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
2326
2368
  return false;
2327
2369
  }
2328
2370
  function parseKillCommand(command) {
2329
2371
  const normalized = command.replace(/\s+/g, " ").trim();
2372
+ if (isWin) {
2373
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
2374
+ if (taskkillMatch?.[1]) {
2375
+ const pidStr = taskkillMatch[1];
2376
+ return {
2377
+ pid: parseInt(pidStr, 10),
2378
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
2379
+ isGroupKill: false,
2380
+ isAllKill: false,
2381
+ originalCommand: command
2382
+ };
2383
+ }
2384
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
2385
+ if (tskillMatch?.[1]) {
2386
+ const pidStr = tskillMatch[1];
2387
+ return {
2388
+ pid: parseInt(pidStr, 10),
2389
+ signal: "TERM",
2390
+ isGroupKill: false,
2391
+ isAllKill: false,
2392
+ originalCommand: command
2393
+ };
2394
+ }
2395
+ return null;
2396
+ }
2330
2397
  const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
2331
2398
  if (simpleMatch) {
2332
2399
  const signal = simpleMatch[1] ?? "-TERM";
@@ -2370,28 +2437,6 @@ function parseKillCommand(command) {
2370
2437
  if (pgrepMatch) {
2371
2438
  return null;
2372
2439
  }
2373
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
2374
- if (taskkillMatch?.[1]) {
2375
- const pidStr = taskkillMatch[1];
2376
- return {
2377
- pid: parseInt(pidStr, 10),
2378
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
2379
- isGroupKill: false,
2380
- isAllKill: false,
2381
- originalCommand: command
2382
- };
2383
- }
2384
- const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
2385
- if (tskillMatch?.[1]) {
2386
- const pidStr = tskillMatch[1];
2387
- return {
2388
- pid: parseInt(pidStr, 10),
2389
- signal: "TERM",
2390
- isGroupKill: false,
2391
- isAllKill: false,
2392
- originalCommand: command
2393
- };
2394
- }
2395
2440
  return null;
2396
2441
  }
2397
2442
  async function getProtectedEntries() {
@@ -2468,7 +2513,7 @@ async function checkAndBlockKillCommand(command) {
2468
2513
  }
2469
2514
  return { blocked: false };
2470
2515
  }
2471
- function pickShell(platform4, command, env) {
2516
+ function pickShell(platform5, command, env) {
2472
2517
  const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
2473
2518
  if (override === "cmd" || override === "cmd.exe") return "cmd";
2474
2519
  if (override === "powershell" || override === "powershell.exe") return "powershell";
@@ -2522,7 +2567,7 @@ function looksLikePowerShellExtended(command) {
2522
2567
  }
2523
2568
  function wrapPowerShellScript(command) {
2524
2569
  const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
2525
- return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
2570
+ return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
2526
2571
  }
2527
2572
  var PS_VERB_RE = new RegExp(
2528
2573
  // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
@@ -2712,10 +2757,10 @@ var bashTool = {
2712
2757
  }));
2713
2758
  }
2714
2759
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
2715
- const isWin3 = os2.platform() === "win32";
2760
+ const isWin4 = os2.platform() === "win32";
2716
2761
  let plan;
2717
2762
  let winShellKind;
2718
- if (isWin3) {
2763
+ if (isWin4) {
2719
2764
  const shell2 = pickShell("win32", input.command, {
2720
2765
  get: (k) => process.env[k]
2721
2766
  });
@@ -2744,7 +2789,7 @@ var bashTool = {
2744
2789
  const shell = plan.bin;
2745
2790
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
2746
2791
  const env = buildChildEnv(ctx.session?.id);
2747
- const detached = !isWin3;
2792
+ const detached = !isWin4;
2748
2793
  const startedAt = Date.now();
2749
2794
  if (input.background) {
2750
2795
  let buf2 = "";
@@ -2762,7 +2807,7 @@ var bashTool = {
2762
2807
  // apply: the child gets a hidden console that grandchildren inherit.
2763
2808
  // Windows children survive parent exit either way. POSIX keeps
2764
2809
  // detached for the process-group kill semantics.
2765
- detached: !isWin3,
2810
+ detached: !isWin4,
2766
2811
  windowsHide: true
2767
2812
  });
2768
2813
  if (plan.useStdin) {
@@ -2780,7 +2825,8 @@ var bashTool = {
2780
2825
  command: redactCommand(input.command),
2781
2826
  startedAt: Date.now(),
2782
2827
  sessionId: ctx.session?.id,
2783
- child: child2
2828
+ child: child2,
2829
+ processGroupLeader: detached && child2.pid === pid2
2784
2830
  });
2785
2831
  child2.on("close", () => registry.unregister(pid2));
2786
2832
  }
@@ -2821,6 +2867,14 @@ var bashTool = {
2821
2867
  pid: pid2
2822
2868
  }
2823
2869
  };
2870
+ ctx.recordSideEffect?.({
2871
+ toolUseId: `bash-bg-${Date.now()}`,
2872
+ toolName: "bash",
2873
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2874
+ input: { command: redactCommand(input.command), background: true },
2875
+ outcome: `launched (pid ${pid2 ?? "unknown"})`,
2876
+ risk: "shell"
2877
+ });
2824
2878
  return;
2825
2879
  }
2826
2880
  const child = spawn(shell, args, {
@@ -2831,7 +2885,7 @@ var bashTool = {
2831
2885
  stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
2832
2886
  detached,
2833
2887
  windowsHide: true,
2834
- ...isWin3 ? {} : { signal: opts.signal }
2888
+ ...isWin4 ? {} : { signal: opts.signal }
2835
2889
  });
2836
2890
  if (plan.useStdin) {
2837
2891
  try {
@@ -2848,7 +2902,8 @@ var bashTool = {
2848
2902
  command: redactCommand(input.command),
2849
2903
  startedAt: Date.now(),
2850
2904
  sessionId: ctx.session?.id,
2851
- child
2905
+ child,
2906
+ processGroupLeader: detached && child.pid === pid
2852
2907
  });
2853
2908
  }
2854
2909
  let buf = "";
@@ -2857,7 +2912,7 @@ var bashTool = {
2857
2912
  const timers = [];
2858
2913
  const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
2859
2914
  function killWithTimeout(child2, timeoutMs2) {
2860
- if (isWin3) {
2915
+ if (isWin4) {
2861
2916
  if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
2862
2917
  const fallback = setTimeout(() => {
2863
2918
  if (child2.exitCode === null) {
@@ -2877,34 +2932,22 @@ var bashTool = {
2877
2932
  }
2878
2933
  return;
2879
2934
  }
2880
- try {
2881
- if (typeof child2.pid === "number") {
2882
- try {
2883
- process.kill(-child2.pid, "SIGTERM");
2884
- } catch {
2885
- child2.kill("SIGTERM");
2886
- }
2887
- } else {
2935
+ if (typeof child2.pid === "number") {
2936
+ registry.kill(child2.pid, { graceMs: timeoutMs2 });
2937
+ } else {
2938
+ try {
2888
2939
  child2.kill("SIGTERM");
2940
+ } catch {
2889
2941
  }
2890
- } catch {
2891
- }
2892
- const killTimer = setTimeout(() => {
2893
- try {
2894
- if (typeof child2.pid === "number") {
2895
- try {
2896
- process.kill(-child2.pid, "SIGKILL");
2897
- } catch {
2898
- child2.kill("SIGKILL");
2899
- }
2900
- } else {
2942
+ const killTimer = setTimeout(() => {
2943
+ try {
2901
2944
  child2.kill("SIGKILL");
2945
+ } catch {
2902
2946
  }
2903
- } catch {
2904
- }
2905
- }, timeoutMs2);
2906
- timers.push(killTimer);
2907
- killTimer.unref?.();
2947
+ }, timeoutMs2);
2948
+ timers.push(killTimer);
2949
+ killTimer.unref?.();
2950
+ }
2908
2951
  }
2909
2952
  const timer = setTimeout(() => {
2910
2953
  timedOut = true;
@@ -2913,7 +2956,7 @@ var bashTool = {
2913
2956
  timers.push(timer);
2914
2957
  timer.unref?.();
2915
2958
  const onAbort = () => killWithTimeout(child, 2e3);
2916
- if (isWin3) {
2959
+ if (isWin4) {
2917
2960
  if (opts.signal.aborted) onAbort();
2918
2961
  else opts.signal.addEventListener("abort", onAbort, { once: true });
2919
2962
  }
@@ -3001,6 +3044,14 @@ ${hint}` : ""),
3001
3044
  timed_out: timedOut
3002
3045
  }
3003
3046
  };
3047
+ ctx.recordSideEffect?.({
3048
+ toolUseId: `bash-${Date.now()}`,
3049
+ toolName: "bash",
3050
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3051
+ input: { command: redactCommand(input.command) },
3052
+ outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
3053
+ risk: "shell"
3054
+ });
3004
3055
  return;
3005
3056
  }
3006
3057
  const now2 = Date.now();
@@ -3012,7 +3063,7 @@ ${hint}` : ""),
3012
3063
  } finally {
3013
3064
  for (const t of timers) clearTimeout(t);
3014
3065
  spool.finalize();
3015
- if (isWin3) opts.signal.removeEventListener("abort", onAbort);
3066
+ if (isWin4) opts.signal.removeEventListener("abort", onAbort);
3016
3067
  child.stdout?.off("data", onData);
3017
3068
  child.stderr?.off("data", onData);
3018
3069
  child.stdout?.destroy();
@@ -3022,6 +3073,38 @@ ${hint}` : ""),
3022
3073
  else killWithTimeout(child, 2e3);
3023
3074
  }
3024
3075
  }
3076
+ },
3077
+ /**
3078
+ * Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
3079
+ * tool's run is aborted/timeout'd. The generator's `finally` block above
3080
+ * already force-kills the direct child, but that only runs if the
3081
+ * executor closes the async iterator (via `iter.return()`). When the
3082
+ * executor tears down without iterating — or a re-entrant abort races
3083
+ * with the generator — a bash-spawned process tree can survive in the
3084
+ * ProcessRegistry with `killed === false`, continuing to write files,
3085
+ * consume CPU, or hold inherited stdio pipes open for the rest of the
3086
+ * session.
3087
+ *
3088
+ * This is the defensive layer the executor calls via `tool.cleanup()`
3089
+ * (see `types/tool.ts`): kill every bash-owned process still tracked
3090
+ * for this session that hasn't exited yet. `registry.kill()` already
3091
+ * handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
3092
+ * grace window, so this just scopes the registry's existing kill path
3093
+ * to "this session's runaway bash children". Idempotent — a process
3094
+ * that already exited is skipped by `kill()` (it returns false), and a
3095
+ * `protected` infrastructure process (dev server the user intentionally
3096
+ * backgrounded) is left alone by design.
3097
+ */
3098
+ async cleanup(_input, ctx) {
3099
+ const registry = getProcessRegistry();
3100
+ const sessionId = ctx.session?.id;
3101
+ if (!sessionId) return;
3102
+ for (const entry of registry.bySession(sessionId)) {
3103
+ if (entry.name !== "bash") continue;
3104
+ if (entry.child.exitCode !== null) continue;
3105
+ if (entry.protected) continue;
3106
+ registry.kill(entry.pid, { force: true });
3107
+ }
3025
3108
  }
3026
3109
  };
3027
3110
 
@@ -3033,8 +3116,8 @@ function normalizeShell(value) {
3033
3116
  if (v === "pwsh" || v === "pwsh.exe") return "pwsh";
3034
3117
  return void 0;
3035
3118
  }
3036
- function resolveSessionShell(platform4, env, deps = {}) {
3037
- if (platform4 !== "win32") return void 0;
3119
+ function resolveSessionShell(platform5, env, deps = {}) {
3120
+ if (platform5 !== "win32") return void 0;
3038
3121
  const override = normalizeShell(env.get("WRONGSTACK_SHELL"));
3039
3122
  if (override) return override;
3040
3123
  const hasBinary = deps.hasBinary ?? ((bin) => resolveWin32Command(bin) !== bin);
@@ -3044,15 +3127,15 @@ function resolveSessionShell(platform4, env, deps = {}) {
3044
3127
  }
3045
3128
  function ensureSessionShell(opts = {}) {
3046
3129
  const env = opts.env ?? process.env;
3047
- const platform4 = opts.platform ?? process.platform;
3048
- if (platform4 !== "win32") return void 0;
3130
+ const platform5 = opts.platform ?? process.platform;
3131
+ if (platform5 !== "win32") return void 0;
3049
3132
  const existing = normalizeShell(env["WRONGSTACK_SHELL"]);
3050
3133
  if (existing) return existing;
3051
- const chosen = resolveSessionShell(platform4, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
3134
+ const chosen = resolveSessionShell(platform5, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
3052
3135
  env["WRONGSTACK_SHELL"] = chosen;
3053
3136
  return chosen;
3054
3137
  }
3055
- var isWin = process.platform === "win32";
3138
+ var isWin2 = process.platform === "win32";
3056
3139
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3057
3140
  // JS / TS toolchain
3058
3141
  "node",
@@ -3062,12 +3145,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3062
3145
  "npx",
3063
3146
  "bun",
3064
3147
  "deno",
3148
+ "corepack",
3065
3149
  "tsc",
3150
+ "tsx",
3151
+ "ts-node",
3152
+ "vite",
3066
3153
  "vitest",
3067
3154
  "jest",
3068
3155
  "biome",
3069
3156
  "eslint",
3070
3157
  "prettier",
3158
+ "turbo",
3159
+ "nx",
3160
+ "webpack",
3161
+ "rollup",
3162
+ "parcel",
3163
+ "next",
3164
+ "astro",
3165
+ "playwright",
3166
+ "cypress",
3071
3167
  // version control
3072
3168
  "git",
3073
3169
  // Rust
@@ -3080,10 +3176,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3080
3176
  "python3",
3081
3177
  "pip",
3082
3178
  "pip3",
3179
+ "pytest",
3180
+ "ruff",
3181
+ "mypy",
3182
+ "uv",
3183
+ "uvx",
3184
+ "poetry",
3185
+ "hatch",
3186
+ "tox",
3083
3187
  // Ruby
3084
3188
  "ruby",
3085
3189
  "gem",
3086
3190
  "bundle",
3191
+ // PHP
3192
+ "php",
3193
+ "composer",
3194
+ "phpunit",
3087
3195
  // JVM
3088
3196
  "java",
3089
3197
  "javac",
@@ -3095,18 +3203,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3095
3203
  // C / C++ / native build
3096
3204
  "make",
3097
3205
  "cmake",
3098
- // containers / orchestration (read-only subcommands; see BLOCKED_ARG_PATTERNS)
3206
+ // containers / orchestration
3099
3207
  "docker",
3208
+ "podman",
3100
3209
  "kubectl",
3101
3210
  // common POSIX file/text utilities
3211
+ "pwd",
3102
3212
  "ls",
3103
3213
  "cat",
3104
3214
  "head",
3105
3215
  "tail",
3106
3216
  "wc",
3107
3217
  "grep",
3218
+ "rg",
3108
3219
  "find",
3109
3220
  "echo",
3221
+ "sort",
3222
+ "uniq",
3223
+ "sed",
3224
+ "awk",
3110
3225
  "mkdir",
3111
3226
  "cp",
3112
3227
  "mv",
@@ -3137,8 +3252,7 @@ var MAX_ARGS = 20;
3137
3252
  var MAX_OUTPUT2 = 2e5;
3138
3253
  var DEFAULT_TIMEOUT_MS2 = 3e4;
3139
3254
  var BLOCKED_ARG_PATTERNS = {
3140
- // python -c/--command executes arbitrary code; python -m runs modules
3141
- python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
3255
+ python: [],
3142
3256
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
3143
3257
  // -C <dir> changes working directory, bypassing cwd sandbox;
3144
3258
  // -c/--config <k>=<v> injects config that runs commands
@@ -3154,15 +3268,10 @@ var BLOCKED_ARG_PATTERNS = {
3154
3268
  /^--config=/,
3155
3269
  /^--config-env=/
3156
3270
  ],
3157
- // node -r/--require preloads arbitrary modules; --eval executes code
3158
- node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
3159
- // go run could execute arbitrary .go files; -ldflags could inject build-time code
3160
- go: [/^-ldflags$/],
3161
- // bun --preload is similar to node --require
3162
- bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
3163
- // docker build/run can create containers with host access;
3164
- // only allow read-only commands (ps, images, version)
3165
- docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
3271
+ node: [],
3272
+ go: [],
3273
+ bun: [],
3274
+ docker: [],
3166
3275
  // find -exec/-ok/-execdir execute arbitrary commands
3167
3276
  find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
3168
3277
  // rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
@@ -3170,15 +3279,49 @@ var BLOCKED_ARG_PATTERNS = {
3170
3279
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
3171
3280
  // `rm -rf /*` targets the filesystem root. All are blocked.
3172
3281
  rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
3173
- // npm run/exec/create/pack/publish can execute arbitrary scripts or publish malware
3174
- npm: [/^run$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
3175
- // pnpm run/dlx/exec/create can execute arbitrary scripts
3176
- pnpm: [/^run$/, /^dlx$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
3177
- // npx should only be used for --version; any package name is a vector for
3178
- // malicious package execution (typosquatting, dependency confusion)
3179
- npx: [/^[^\s]+$/]
3282
+ // npm/pnpm subcommands are checked separately below. Matching every arg here
3283
+ // over-blocked normal dev flows such as `pnpm vitest run ...`.
3284
+ npm: [],
3285
+ pnpm: [],
3286
+ npx: []
3287
+ };
3288
+ var BLOCKED_SUBCOMMANDS = {
3289
+ docker: /* @__PURE__ */ new Set(["push"]),
3290
+ podman: /* @__PURE__ */ new Set(["push"]),
3291
+ npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
3292
+ pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
3293
+ yarn: /* @__PURE__ */ new Set(["publish"])
3294
+ };
3295
+ var BLOCKED_SUBCOMMAND_SEQUENCES = {
3296
+ yarn: [["npm", "publish"]]
3180
3297
  };
3298
+ function firstSubcommand(args) {
3299
+ for (const arg of args) {
3300
+ if (arg === "--") return null;
3301
+ if (!arg.startsWith("-")) return arg;
3302
+ }
3303
+ return null;
3304
+ }
3305
+ function subcommandArgs(args) {
3306
+ const out = [];
3307
+ for (const arg of args) {
3308
+ if (arg === "--") break;
3309
+ if (!arg.startsWith("-")) out.push(arg);
3310
+ }
3311
+ return out;
3312
+ }
3181
3313
  function validateArgs(cmd, args) {
3314
+ const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
3315
+ const subcommand = firstSubcommand(args);
3316
+ if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
3317
+ return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
3318
+ }
3319
+ const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
3320
+ if (blockedSequences) {
3321
+ const actual = subcommandArgs(args);
3322
+ const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
3323
+ if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
3324
+ }
3182
3325
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
3183
3326
  if (!blocked) return null;
3184
3327
  for (const arg of args) {
@@ -3305,7 +3448,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3305
3448
  const startedAt = Date.now();
3306
3449
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
3307
3450
  const resolved = resolveWin32Command(cmd);
3308
- const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3451
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3309
3452
  const spawnCmd = needsShell ? cmd : resolved;
3310
3453
  if (needsShell) assertSafeWin32ShellArgs(args);
3311
3454
  let child;
@@ -3315,7 +3458,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3315
3458
  env: buildChildEnv(sessionId),
3316
3459
  stdio: ["ignore", "pipe", "pipe"],
3317
3460
  windowsHide: true,
3318
- ...isWin ? {} : { signal },
3461
+ ...isWin2 ? {} : { signal },
3319
3462
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3320
3463
  });
3321
3464
  } catch (err) {
@@ -3335,7 +3478,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3335
3478
  const isAbort = err && err.code === "ABORT_ERR";
3336
3479
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
3337
3480
  clearTimeout(timer);
3338
- if (isWin) signal.removeEventListener("abort", onAbort);
3481
+ if (isWin2) signal.removeEventListener("abort", onAbort);
3339
3482
  if (typeof pid === "number") registry.unregister(pid);
3340
3483
  registry.afterCall(Date.now() - startedAt, true);
3341
3484
  spool.finalize();
@@ -3365,7 +3508,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3365
3508
  if (typeof pid === "number") registry.kill(pid, { force: true });
3366
3509
  else child.kill("SIGTERM");
3367
3510
  };
3368
- if (isWin) {
3511
+ if (isWin2) {
3369
3512
  if (signal.aborted) onAbort();
3370
3513
  else signal.addEventListener("abort", onAbort, { once: true });
3371
3514
  }
@@ -3381,7 +3524,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3381
3524
  });
3382
3525
  child.on("close", (code) => {
3383
3526
  clearTimeout(timer);
3384
- if (isWin) signal.removeEventListener("abort", onAbort);
3527
+ if (isWin2) signal.removeEventListener("abort", onAbort);
3385
3528
  if (typeof pid === "number") registry.unregister(pid);
3386
3529
  const durationMs = Date.now() - startedAt;
3387
3530
  const exitCode = killed ? 124 : code ?? 1;
@@ -3545,7 +3688,7 @@ var fetchTool = {
3545
3688
  if (!final) throw new Error("fetch: stream ended without final event");
3546
3689
  return final;
3547
3690
  },
3548
- async *executeStream(input, _ctx, opts) {
3691
+ async *executeStream(input, ctx, opts) {
3549
3692
  if (!input?.url) throw new Error("fetch: url is required");
3550
3693
  const u = new URL(input.url);
3551
3694
  if (u.protocol !== "https:" && u.protocol !== "http:") {
@@ -3617,6 +3760,14 @@ var fetchTool = {
3617
3760
  url: res.url
3618
3761
  }
3619
3762
  };
3763
+ ctx.recordSideEffect?.({
3764
+ toolUseId: `fetch-${Date.now()}`,
3765
+ toolName: "fetch",
3766
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3767
+ input: { url: input.url, format: input.format },
3768
+ outcome: `HTTP ${res.status} (${ct})`,
3769
+ risk: "network"
3770
+ });
3620
3771
  } finally {
3621
3772
  clearTimeout(timer);
3622
3773
  }
@@ -4724,8 +4875,8 @@ var jsonTool = {
4724
4875
  };
4725
4876
  }
4726
4877
  };
4727
- function query(data, path21) {
4728
- const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4878
+ function query(data, path22) {
4879
+ const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4729
4880
  let current = data;
4730
4881
  for (const part of parts) {
4731
4882
  if (current === null || current === void 0) return void 0;
@@ -5097,7 +5248,7 @@ async function walkDir(dir, depth, opts) {
5097
5248
  }
5098
5249
  }
5099
5250
  }
5100
- var isWin2 = process.platform === "win32";
5251
+ var isWin3 = process.platform === "win32";
5101
5252
  async function* spawnStream(opts) {
5102
5253
  const max = opts.maxBytes ?? 2e5;
5103
5254
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -5108,7 +5259,7 @@ async function* spawnStream(opts) {
5108
5259
  let error;
5109
5260
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
5110
5261
  const resolved = resolveWin32Command(opts.cmd);
5111
- const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5262
+ const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5112
5263
  const cmd = needsShell ? opts.cmd : resolved;
5113
5264
  if (needsShell) assertSafeWin32ShellArgs(opts.args);
5114
5265
  const child = spawn(cmd, opts.args, {
@@ -5116,7 +5267,7 @@ async function* spawnStream(opts) {
5116
5267
  env: buildChildEnv(),
5117
5268
  stdio: ["ignore", "pipe", "pipe"],
5118
5269
  windowsHide: true,
5119
- ...isWin2 ? {} : { signal: opts.signal },
5270
+ ...isWin3 ? {} : { signal: opts.signal },
5120
5271
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
5121
5272
  });
5122
5273
  const registry = getProcessRegistry();
@@ -5195,7 +5346,7 @@ async function* spawnStream(opts) {
5195
5346
  queue.push({ kind: "close", data: "", code: 124 });
5196
5347
  wake();
5197
5348
  };
5198
- if (isWin2) {
5349
+ if (isWin3) {
5199
5350
  if (opts.signal.aborted) onAbort();
5200
5351
  else opts.signal.addEventListener("abort", onAbort, { once: true });
5201
5352
  }
@@ -5242,7 +5393,7 @@ async function* spawnStream(opts) {
5242
5393
  };
5243
5394
  } finally {
5244
5395
  spool.finalize();
5245
- if (isWin2) opts.signal.removeEventListener("abort", onAbort);
5396
+ if (isWin3) opts.signal.removeEventListener("abort", onAbort);
5246
5397
  child.stdout?.off("data", onOut);
5247
5398
  child.stderr?.off("data", onErr);
5248
5399
  child.stdout?.destroy();
@@ -5828,6 +5979,14 @@ var installTool = {
5828
5979
  }
5829
5980
  }
5830
5981
  }
5982
+ ctx.recordSideEffect?.({
5983
+ toolUseId: `install-${Date.now()}`,
5984
+ toolName: "install",
5985
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5986
+ input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
5987
+ outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
5988
+ risk: "package"
5989
+ });
5831
5990
  yield { type: "final", output };
5832
5991
  }
5833
5992
  };
@@ -6194,7 +6353,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6194
6353
  }
6195
6354
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6196
6355
  var MAX_TAIL_LINES = 1e5;
6197
- async function fileLogs(path21, lines, filterRe, stream) {
6356
+ async function fileLogs(path22, lines, filterRe, stream) {
6198
6357
  const { createInterface } = await import('node:readline');
6199
6358
  const { createReadStream } = await import('node:fs');
6200
6359
  const entries = [];
@@ -6203,7 +6362,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6203
6362
  let writeIdx = 0;
6204
6363
  let totalLines = 0;
6205
6364
  const rl = createInterface({
6206
- input: createReadStream(path21),
6365
+ input: createReadStream(path22),
6207
6366
  crlfDelay: Number.POSITIVE_INFINITY
6208
6367
  });
6209
6368
  for await (const line of rl) {
@@ -6224,7 +6383,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6224
6383
  if (parsed) entries.push(parsed);
6225
6384
  }
6226
6385
  return {
6227
- source: path21,
6386
+ source: path22,
6228
6387
  entries,
6229
6388
  total: entries.length,
6230
6389
  truncated: totalLines > effLines,
@@ -6596,6 +6755,229 @@ function substituteVars(content, name, vars) {
6596
6755
  }
6597
6756
  return result;
6598
6757
  }
6758
+ function normalizeOverrides(set) {
6759
+ const out = {};
6760
+ if (set && typeof set === "object") {
6761
+ for (const [k, v] of Object.entries(set)) {
6762
+ if (typeof v === "string") out[k] = v;
6763
+ }
6764
+ }
6765
+ return out;
6766
+ }
6767
+ var designTool = {
6768
+ name: "design",
6769
+ category: "Design",
6770
+ description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
6771
+ usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
6772
+ permission: "auto",
6773
+ mutating: false,
6774
+ capabilities: [],
6775
+ timeoutMs: 15e3,
6776
+ inputSchema: {
6777
+ type: "object",
6778
+ properties: {
6779
+ action: {
6780
+ type: "string",
6781
+ enum: ["list", "use", "foundations", "set", "materialize", "verify"],
6782
+ description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
6783
+ },
6784
+ kit: {
6785
+ type: "string",
6786
+ description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
6787
+ },
6788
+ stack: {
6789
+ type: "string",
6790
+ enum: ["web", "react-native", "flutter", "swiftui", "compose"],
6791
+ description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
6792
+ },
6793
+ set: {
6794
+ type: "object",
6795
+ description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
6796
+ additionalProperties: { type: "string" }
6797
+ },
6798
+ out: {
6799
+ type: "string",
6800
+ description: "Materialize output path (project-relative). Defaults to a per-stack convention."
6801
+ },
6802
+ force: {
6803
+ type: "boolean",
6804
+ description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
6805
+ },
6806
+ files: {
6807
+ type: "array",
6808
+ items: { type: "string" },
6809
+ description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
6810
+ }
6811
+ },
6812
+ required: []
6813
+ },
6814
+ async execute(input, ctx) {
6815
+ const loader = getDesignKitLoader(ctx.projectRoot);
6816
+ const action = input.action ?? "list";
6817
+ const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
6818
+ if (action === "foundations") {
6819
+ const text = await loader.foundationsText(stack);
6820
+ return { action, stack, output: text || "No foundations document is installed." };
6821
+ }
6822
+ if (action === "use") {
6823
+ const kitId = input.kit?.trim();
6824
+ if (!kitId) {
6825
+ const menu2 = await loader.menuText();
6826
+ return { action, output: `No kit id provided.
6827
+
6828
+ ${menu2}` };
6829
+ }
6830
+ const manifest = await loader.find(kitId);
6831
+ if (!manifest) {
6832
+ const menu2 = await loader.menuText();
6833
+ return { action, kit: kitId, output: `Kit "${kitId}" not found.
6834
+
6835
+ ${menu2}` };
6836
+ }
6837
+ const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
6838
+ const body = await loader.readBody(manifest.id, resolvedStack);
6839
+ const rawTokens = await loader.readTokens(manifest.id);
6840
+ const persisted = await loadActiveKit(ctx.projectRoot);
6841
+ const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
6842
+ const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
6843
+ const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
6844
+ setActiveKit(ctx, manifest.id, resolvedStack, overrides);
6845
+ await recordKitChoice(
6846
+ ctx.projectRoot,
6847
+ manifest.id,
6848
+ resolvedStack,
6849
+ "design-tool",
6850
+ (/* @__PURE__ */ new Date()).toISOString(),
6851
+ Object.keys(overrides).length ? overrides : void 0
6852
+ );
6853
+ const ovLine = Object.keys(overrides).length ? `
6854
+ Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
6855
+ ` : "";
6856
+ const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
6857
+ ${manifest.aesthetic}
6858
+ ${ovLine}
6859
+ Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
6860
+ `;
6861
+ const tokenBlock = tokens ? `
6862
+ ## Token snapshot (overrides applied)
6863
+ \`\`\`json
6864
+ ${JSON.stringify(tokens, null, 2)}
6865
+ \`\`\`
6866
+ Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
6867
+ ` : "";
6868
+ return {
6869
+ action,
6870
+ kit: manifest.id,
6871
+ stack: resolvedStack,
6872
+ output: `${header}${tokenBlock}
6873
+ ${body}`
6874
+ };
6875
+ }
6876
+ if (action === "set") {
6877
+ const patch = normalizeOverrides(input.set);
6878
+ if (Object.keys(patch).length === 0) {
6879
+ return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
6880
+ }
6881
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
6882
+ if (!merged) {
6883
+ return {
6884
+ action,
6885
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
6886
+ };
6887
+ }
6888
+ setDesignOverrides(ctx, merged);
6889
+ return {
6890
+ action,
6891
+ output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
6892
+ These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
6893
+ };
6894
+ }
6895
+ if (action === "materialize") {
6896
+ const active = await loadActiveKit(ctx.projectRoot);
6897
+ if (!active) {
6898
+ return {
6899
+ action,
6900
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
6901
+ };
6902
+ }
6903
+ const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
6904
+ const rawTokens = await loader.readTokens(active.kit);
6905
+ if (!rawTokens) {
6906
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
6907
+ }
6908
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
6909
+ const result = materializeTokens({
6910
+ tokens,
6911
+ stack: resolvedStack,
6912
+ kitId: active.kit,
6913
+ outPath: input.out
6914
+ });
6915
+ const abs = path.join(ctx.projectRoot, result.path);
6916
+ let exists = false;
6917
+ try {
6918
+ await fs7.access(abs);
6919
+ exists = true;
6920
+ } catch {
6921
+ }
6922
+ if (exists && !input.force) {
6923
+ return {
6924
+ action,
6925
+ kit: active.kit,
6926
+ stack: resolvedStack,
6927
+ path: result.path,
6928
+ output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
6929
+
6930
+ \`\`\`
6931
+ ${result.content}
6932
+ \`\`\``
6933
+ };
6934
+ }
6935
+ await fs7.mkdir(path.dirname(abs), { recursive: true });
6936
+ await fs7.writeFile(abs, result.content);
6937
+ return {
6938
+ action,
6939
+ kit: active.kit,
6940
+ stack: resolvedStack,
6941
+ path: result.path,
6942
+ output: `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit palette is the source of truth. ${exists ? "(overwrote existing file)" : ""}`
6943
+ };
6944
+ }
6945
+ if (action === "verify") {
6946
+ const active = await loadActiveKit(ctx.projectRoot);
6947
+ if (!active) {
6948
+ return {
6949
+ action,
6950
+ output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
6951
+ };
6952
+ }
6953
+ const rawTokens = await loader.readTokens(active.kit);
6954
+ if (!rawTokens) {
6955
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
6956
+ }
6957
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
6958
+ const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
6959
+ const pct = Math.round(report.score * 100);
6960
+ const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
6961
+ const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
6962
+ ${top}${report.violations.length > 25 ? `
6963
+ \u2026and ${report.violations.length - 25} more` : ""}
6964
+
6965
+ Replace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).` : "\nNo off-palette colors found \u2014 UI adheres to the kit palette.");
6966
+ return {
6967
+ action,
6968
+ kit: active.kit,
6969
+ output: summary,
6970
+ score: report.score,
6971
+ violations: report.violations.length
6972
+ };
6973
+ }
6974
+ const menu = await loader.menuText();
6975
+ return {
6976
+ action: "list",
6977
+ output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
6978
+ };
6979
+ }
6980
+ };
6599
6981
 
6600
6982
  // src/tool-search.ts
6601
6983
  var toolSearchTool = {
@@ -11127,7 +11509,8 @@ var TIER2_TOOLS = [
11127
11509
  planTool,
11128
11510
  taskTool,
11129
11511
  installTool,
11130
- auditTool
11512
+ auditTool,
11513
+ designTool
11131
11514
  ];
11132
11515
  var TIER3_TOOLS = [
11133
11516
  outdatedTool,
@@ -11172,6 +11555,7 @@ var builtinTools = [
11172
11555
  logsTool,
11173
11556
  documentTool,
11174
11557
  scaffoldTool,
11558
+ designTool,
11175
11559
  toolSearchTool,
11176
11560
  toolUseTool,
11177
11561
  batchToolUseTool,
@@ -11345,6 +11729,6 @@ var TOOL_ICON_CONFIG = {
11345
11729
  };
11346
11730
  var FALLBACK_ICON = "fallback";
11347
11731
 
11348
- export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
11732
+ export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, designTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
11349
11733
  //# sourceMappingURL=index.js.map
11350
11734
  //# sourceMappingURL=index.js.map