@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/builtin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn, execFileSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, mkdirSync, createWriteStream } from 'node:fs';
6
6
  import * as fs2 from 'node:fs/promises';
@@ -280,6 +280,7 @@ var CircuitBreaker = class {
280
280
  if (this.state === "open") return;
281
281
  this.state = "open";
282
282
  this.openedAt = Date.now();
283
+ this.window = [];
283
284
  try {
284
285
  this.onTrip?.();
285
286
  } catch {
@@ -319,7 +320,7 @@ var SENSITIVE_FLAG_PATTERNS = [
319
320
  /--(?: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,
320
321
  // -f "value" style short flags
321
322
  /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
322
- /(?<!\w)-p(?:ssword)?(?:\s+|\s*=\s*)[^\s,]+/gi,
323
+ /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
323
324
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
324
325
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
325
326
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -380,12 +381,35 @@ var ProcessRegistryImpl = class {
380
381
  register(info) {
381
382
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
382
383
  }
384
+ _isSafeSignalPid(pid) {
385
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
386
+ }
387
+ _canSignalProcessGroup(p) {
388
+ return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
389
+ }
390
+ _killChildDirect(p, signal) {
391
+ try {
392
+ p.child.kill(signal);
393
+ } catch {
394
+ }
395
+ }
396
+ _killPosix(p, signal) {
397
+ if (this._canSignalProcessGroup(p)) {
398
+ try {
399
+ process.kill(-p.pid, signal);
400
+ return;
401
+ } catch {
402
+ }
403
+ }
404
+ this._killChildDirect(p, signal);
405
+ }
383
406
  /** Unregister a process by PID. Called on 'close' / 'exit' events. */
384
407
  unregister(pid) {
385
408
  this.processes.delete(pid);
386
409
  }
387
410
  /** Get a single process by PID. */
388
411
  get(pid) {
412
+ this._pruneStale(pid);
389
413
  return this.processes.get(pid);
390
414
  }
391
415
  /** Get all tracked processes. */
@@ -549,13 +573,14 @@ var ProcessRegistryImpl = class {
549
573
  * Returns true if the process was found and kill was attempted.
550
574
  */
551
575
  kill(pid, opts = {}) {
576
+ this._pruneStale(pid);
552
577
  const p = this.processes.get(pid);
553
578
  if (!p) return false;
554
579
  if (p.killed) return true;
555
580
  if (p.protected) return false;
556
581
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
557
- const isWin3 = os2.platform() === "win32";
558
- if (isWin3) {
582
+ const isWin4 = os2.platform() === "win32";
583
+ if (isWin4) {
559
584
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
560
585
  if (liveRealChild && killWin32Tree(pid)) {
561
586
  const fallback = setTimeout(() => {
@@ -578,27 +603,12 @@ var ProcessRegistryImpl = class {
578
603
  }
579
604
  try {
580
605
  if (force) {
581
- try {
582
- process.kill(-pid, "SIGKILL");
583
- } catch {
584
- p.child.kill("SIGKILL");
585
- }
606
+ this._killPosix(p, "SIGKILL");
586
607
  } else {
587
- try {
588
- process.kill(-pid, "SIGTERM");
589
- } catch {
590
- p.child.kill("SIGTERM");
591
- }
608
+ this._killPosix(p, "SIGTERM");
592
609
  const timer = setTimeout(() => {
593
610
  if (this.processes.has(pid) && !p.child.killed) {
594
- try {
595
- process.kill(-pid, "SIGKILL");
596
- } catch {
597
- try {
598
- p.child.kill("SIGKILL");
599
- } catch {
600
- }
601
- }
611
+ this._killPosix(p, "SIGKILL");
602
612
  }
603
613
  }, graceMs);
604
614
  timer.unref?.();
@@ -633,6 +643,34 @@ var ProcessRegistryImpl = class {
633
643
  }
634
644
  return killed;
635
645
  }
646
+ /**
647
+ * Check whether a tracked process entry is stale — the child has exited
648
+ * (exitCode !== null) AND it's been in the registry long enough that the
649
+ * OS may have reused the PID for a new, unrelated process.
650
+ *
651
+ * P3 #24 (before-release.md): on POSIX, PIDs are reused after process
652
+ * exit. If a tracked process exits but its 'close' event hasn't fired yet
653
+ * (or was missed), the registry still holds the entry. A new process
654
+ * gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
655
+ * may incorrectly protect or target the wrong process.
656
+ *
657
+ * The 60s threshold is conservative — the OS typically waits much longer
658
+ * before reusing a PID, but we want to clean up before that becomes a risk.
659
+ */
660
+ _isStaleEntry(entry) {
661
+ return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
662
+ }
663
+ /**
664
+ * Remove a stale entry for a specific PID before any PID-based lookup.
665
+ * This prevents PID reuse from causing the registry to act on a dead
666
+ * process that has been replaced by a new one with the same PID.
667
+ */
668
+ _pruneStale(pid) {
669
+ const entry = this.processes.get(pid);
670
+ if (entry && this._isStaleEntry(entry)) {
671
+ this.processes.delete(pid);
672
+ }
673
+ }
636
674
  };
637
675
  var _registry;
638
676
  function getProcessRegistry() {
@@ -1506,17 +1544,18 @@ function getPersistentProcessRegistry() {
1506
1544
  }
1507
1545
 
1508
1546
  // src/bash-kill-guard.ts
1547
+ var isWin2 = os2.platform() === "win32";
1509
1548
  function extractKillCommand(command) {
1510
1549
  const normalized = command.replace(/\s+/g, " ").trim();
1511
1550
  const shellCMatch = normalized.match(
1512
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
1551
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
1513
1552
  );
1514
1553
  if (shellCMatch?.[1]) {
1515
1554
  const inner = shellCMatch[1].trim();
1516
1555
  return isKillRelatedCommand(inner) ? inner : null;
1517
1556
  }
1518
1557
  const shellCUnquoted = normalized.match(
1519
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1558
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1520
1559
  );
1521
1560
  if (shellCUnquoted?.[1]) {
1522
1561
  return shellCUnquoted[1];
@@ -1525,15 +1564,43 @@ function extractKillCommand(command) {
1525
1564
  }
1526
1565
  function isKillRelatedCommand(cmd) {
1527
1566
  const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
1567
+ if (isWin2) {
1568
+ if (/^taskkill\s/i.test(normalized)) return true;
1569
+ if (/^tskill\s/i.test(normalized)) return true;
1570
+ return false;
1571
+ }
1528
1572
  if (/^kill(\s|$)/.test(normalized)) return true;
1529
1573
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1530
- if (/^taskkill\s/i.test(normalized)) return true;
1531
- if (/^tskill\s/i.test(normalized)) return true;
1532
1574
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1533
1575
  return false;
1534
1576
  }
1535
1577
  function parseKillCommand(command) {
1536
1578
  const normalized = command.replace(/\s+/g, " ").trim();
1579
+ if (isWin2) {
1580
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1581
+ if (taskkillMatch?.[1]) {
1582
+ const pidStr = taskkillMatch[1];
1583
+ return {
1584
+ pid: parseInt(pidStr, 10),
1585
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
1586
+ isGroupKill: false,
1587
+ isAllKill: false,
1588
+ originalCommand: command
1589
+ };
1590
+ }
1591
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1592
+ if (tskillMatch?.[1]) {
1593
+ const pidStr = tskillMatch[1];
1594
+ return {
1595
+ pid: parseInt(pidStr, 10),
1596
+ signal: "TERM",
1597
+ isGroupKill: false,
1598
+ isAllKill: false,
1599
+ originalCommand: command
1600
+ };
1601
+ }
1602
+ return null;
1603
+ }
1537
1604
  const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1538
1605
  if (simpleMatch) {
1539
1606
  const signal = simpleMatch[1] ?? "-TERM";
@@ -1577,28 +1644,6 @@ function parseKillCommand(command) {
1577
1644
  if (pgrepMatch) {
1578
1645
  return null;
1579
1646
  }
1580
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1581
- if (taskkillMatch?.[1]) {
1582
- const pidStr = taskkillMatch[1];
1583
- return {
1584
- pid: parseInt(pidStr, 10),
1585
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
1586
- isGroupKill: false,
1587
- isAllKill: false,
1588
- originalCommand: command
1589
- };
1590
- }
1591
- const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1592
- if (tskillMatch?.[1]) {
1593
- const pidStr = tskillMatch[1];
1594
- return {
1595
- pid: parseInt(pidStr, 10),
1596
- signal: "TERM",
1597
- isGroupKill: false,
1598
- isAllKill: false,
1599
- originalCommand: command
1600
- };
1601
- }
1602
1647
  return null;
1603
1648
  }
1604
1649
  async function getProtectedEntries() {
@@ -1675,7 +1720,7 @@ async function checkAndBlockKillCommand(command) {
1675
1720
  }
1676
1721
  return { blocked: false };
1677
1722
  }
1678
- function pickShell(platform3, command, env) {
1723
+ function pickShell(platform4, command, env) {
1679
1724
  const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
1680
1725
  if (override === "cmd" || override === "cmd.exe") return "cmd";
1681
1726
  if (override === "powershell" || override === "powershell.exe") return "powershell";
@@ -1729,7 +1774,7 @@ function looksLikePowerShellExtended(command) {
1729
1774
  }
1730
1775
  function wrapPowerShellScript(command) {
1731
1776
  const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
1732
- return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
1777
+ return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
1733
1778
  }
1734
1779
  var PS_VERB_RE = new RegExp(
1735
1780
  // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
@@ -1874,10 +1919,10 @@ var bashTool = {
1874
1919
  }));
1875
1920
  }
1876
1921
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1877
- const isWin3 = os2.platform() === "win32";
1922
+ const isWin4 = os2.platform() === "win32";
1878
1923
  let plan;
1879
1924
  let winShellKind;
1880
- if (isWin3) {
1925
+ if (isWin4) {
1881
1926
  const shell2 = pickShell("win32", input.command, {
1882
1927
  get: (k) => process.env[k]
1883
1928
  });
@@ -1906,7 +1951,7 @@ var bashTool = {
1906
1951
  const shell = plan.bin;
1907
1952
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
1908
1953
  const env = buildChildEnv(ctx.session?.id);
1909
- const detached = !isWin3;
1954
+ const detached = !isWin4;
1910
1955
  const startedAt = Date.now();
1911
1956
  if (input.background) {
1912
1957
  let buf2 = "";
@@ -1924,7 +1969,7 @@ var bashTool = {
1924
1969
  // apply: the child gets a hidden console that grandchildren inherit.
1925
1970
  // Windows children survive parent exit either way. POSIX keeps
1926
1971
  // detached for the process-group kill semantics.
1927
- detached: !isWin3,
1972
+ detached: !isWin4,
1928
1973
  windowsHide: true
1929
1974
  });
1930
1975
  if (plan.useStdin) {
@@ -1942,7 +1987,8 @@ var bashTool = {
1942
1987
  command: redactCommand(input.command),
1943
1988
  startedAt: Date.now(),
1944
1989
  sessionId: ctx.session?.id,
1945
- child: child2
1990
+ child: child2,
1991
+ processGroupLeader: detached && child2.pid === pid2
1946
1992
  });
1947
1993
  child2.on("close", () => registry.unregister(pid2));
1948
1994
  }
@@ -1983,6 +2029,14 @@ var bashTool = {
1983
2029
  pid: pid2
1984
2030
  }
1985
2031
  };
2032
+ ctx.recordSideEffect?.({
2033
+ toolUseId: `bash-bg-${Date.now()}`,
2034
+ toolName: "bash",
2035
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2036
+ input: { command: redactCommand(input.command), background: true },
2037
+ outcome: `launched (pid ${pid2 ?? "unknown"})`,
2038
+ risk: "shell"
2039
+ });
1986
2040
  return;
1987
2041
  }
1988
2042
  const child = spawn(shell, args, {
@@ -1993,7 +2047,7 @@ var bashTool = {
1993
2047
  stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
1994
2048
  detached,
1995
2049
  windowsHide: true,
1996
- ...isWin3 ? {} : { signal: opts.signal }
2050
+ ...isWin4 ? {} : { signal: opts.signal }
1997
2051
  });
1998
2052
  if (plan.useStdin) {
1999
2053
  try {
@@ -2010,7 +2064,8 @@ var bashTool = {
2010
2064
  command: redactCommand(input.command),
2011
2065
  startedAt: Date.now(),
2012
2066
  sessionId: ctx.session?.id,
2013
- child
2067
+ child,
2068
+ processGroupLeader: detached && child.pid === pid
2014
2069
  });
2015
2070
  }
2016
2071
  let buf = "";
@@ -2019,7 +2074,7 @@ var bashTool = {
2019
2074
  const timers = [];
2020
2075
  const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
2021
2076
  function killWithTimeout(child2, timeoutMs2) {
2022
- if (isWin3) {
2077
+ if (isWin4) {
2023
2078
  if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
2024
2079
  const fallback = setTimeout(() => {
2025
2080
  if (child2.exitCode === null) {
@@ -2039,34 +2094,22 @@ var bashTool = {
2039
2094
  }
2040
2095
  return;
2041
2096
  }
2042
- try {
2043
- if (typeof child2.pid === "number") {
2044
- try {
2045
- process.kill(-child2.pid, "SIGTERM");
2046
- } catch {
2047
- child2.kill("SIGTERM");
2048
- }
2049
- } else {
2097
+ if (typeof child2.pid === "number") {
2098
+ registry.kill(child2.pid, { graceMs: timeoutMs2 });
2099
+ } else {
2100
+ try {
2050
2101
  child2.kill("SIGTERM");
2102
+ } catch {
2051
2103
  }
2052
- } catch {
2053
- }
2054
- const killTimer = setTimeout(() => {
2055
- try {
2056
- if (typeof child2.pid === "number") {
2057
- try {
2058
- process.kill(-child2.pid, "SIGKILL");
2059
- } catch {
2060
- child2.kill("SIGKILL");
2061
- }
2062
- } else {
2104
+ const killTimer = setTimeout(() => {
2105
+ try {
2063
2106
  child2.kill("SIGKILL");
2107
+ } catch {
2064
2108
  }
2065
- } catch {
2066
- }
2067
- }, timeoutMs2);
2068
- timers.push(killTimer);
2069
- killTimer.unref?.();
2109
+ }, timeoutMs2);
2110
+ timers.push(killTimer);
2111
+ killTimer.unref?.();
2112
+ }
2070
2113
  }
2071
2114
  const timer = setTimeout(() => {
2072
2115
  timedOut = true;
@@ -2075,7 +2118,7 @@ var bashTool = {
2075
2118
  timers.push(timer);
2076
2119
  timer.unref?.();
2077
2120
  const onAbort = () => killWithTimeout(child, 2e3);
2078
- if (isWin3) {
2121
+ if (isWin4) {
2079
2122
  if (opts.signal.aborted) onAbort();
2080
2123
  else opts.signal.addEventListener("abort", onAbort, { once: true });
2081
2124
  }
@@ -2163,6 +2206,14 @@ ${hint}` : ""),
2163
2206
  timed_out: timedOut
2164
2207
  }
2165
2208
  };
2209
+ ctx.recordSideEffect?.({
2210
+ toolUseId: `bash-${Date.now()}`,
2211
+ toolName: "bash",
2212
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2213
+ input: { command: redactCommand(input.command) },
2214
+ outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
2215
+ risk: "shell"
2216
+ });
2166
2217
  return;
2167
2218
  }
2168
2219
  const now = Date.now();
@@ -2174,7 +2225,7 @@ ${hint}` : ""),
2174
2225
  } finally {
2175
2226
  for (const t of timers) clearTimeout(t);
2176
2227
  spool.finalize();
2177
- if (isWin3) opts.signal.removeEventListener("abort", onAbort);
2228
+ if (isWin4) opts.signal.removeEventListener("abort", onAbort);
2178
2229
  child.stdout?.off("data", onData);
2179
2230
  child.stderr?.off("data", onData);
2180
2231
  child.stdout?.destroy();
@@ -2184,6 +2235,38 @@ ${hint}` : ""),
2184
2235
  else killWithTimeout(child, 2e3);
2185
2236
  }
2186
2237
  }
2238
+ },
2239
+ /**
2240
+ * Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
2241
+ * tool's run is aborted/timeout'd. The generator's `finally` block above
2242
+ * already force-kills the direct child, but that only runs if the
2243
+ * executor closes the async iterator (via `iter.return()`). When the
2244
+ * executor tears down without iterating — or a re-entrant abort races
2245
+ * with the generator — a bash-spawned process tree can survive in the
2246
+ * ProcessRegistry with `killed === false`, continuing to write files,
2247
+ * consume CPU, or hold inherited stdio pipes open for the rest of the
2248
+ * session.
2249
+ *
2250
+ * This is the defensive layer the executor calls via `tool.cleanup()`
2251
+ * (see `types/tool.ts`): kill every bash-owned process still tracked
2252
+ * for this session that hasn't exited yet. `registry.kill()` already
2253
+ * handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
2254
+ * grace window, so this just scopes the registry's existing kill path
2255
+ * to "this session's runaway bash children". Idempotent — a process
2256
+ * that already exited is skipped by `kill()` (it returns false), and a
2257
+ * `protected` infrastructure process (dev server the user intentionally
2258
+ * backgrounded) is left alone by design.
2259
+ */
2260
+ async cleanup(_input, ctx) {
2261
+ const registry = getProcessRegistry();
2262
+ const sessionId = ctx.session?.id;
2263
+ if (!sessionId) return;
2264
+ for (const entry of registry.bySession(sessionId)) {
2265
+ if (entry.name !== "bash") continue;
2266
+ if (entry.child.exitCode !== null) continue;
2267
+ if (entry.protected) continue;
2268
+ registry.kill(entry.pid, { force: true });
2269
+ }
2187
2270
  }
2188
2271
  };
2189
2272
 
@@ -5134,6 +5217,229 @@ var codebaseStatsTool = {
5134
5217
  };
5135
5218
  }
5136
5219
  };
5220
+ function normalizeOverrides(set) {
5221
+ const out = {};
5222
+ if (set && typeof set === "object") {
5223
+ for (const [k, v] of Object.entries(set)) {
5224
+ if (typeof v === "string") out[k] = v;
5225
+ }
5226
+ }
5227
+ return out;
5228
+ }
5229
+ var designTool = {
5230
+ name: "design",
5231
+ category: "Design",
5232
+ 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).',
5233
+ 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"}`.',
5234
+ permission: "auto",
5235
+ mutating: false,
5236
+ capabilities: [],
5237
+ timeoutMs: 15e3,
5238
+ inputSchema: {
5239
+ type: "object",
5240
+ properties: {
5241
+ action: {
5242
+ type: "string",
5243
+ enum: ["list", "use", "foundations", "set", "materialize", "verify"],
5244
+ 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."
5245
+ },
5246
+ kit: {
5247
+ type: "string",
5248
+ description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
5249
+ },
5250
+ stack: {
5251
+ type: "string",
5252
+ enum: ["web", "react-native", "flutter", "swiftui", "compose"],
5253
+ description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
5254
+ },
5255
+ set: {
5256
+ type: "object",
5257
+ 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.',
5258
+ additionalProperties: { type: "string" }
5259
+ },
5260
+ out: {
5261
+ type: "string",
5262
+ description: "Materialize output path (project-relative). Defaults to a per-stack convention."
5263
+ },
5264
+ force: {
5265
+ type: "boolean",
5266
+ description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
5267
+ },
5268
+ files: {
5269
+ type: "array",
5270
+ items: { type: "string" },
5271
+ description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
5272
+ }
5273
+ },
5274
+ required: []
5275
+ },
5276
+ async execute(input, ctx) {
5277
+ const loader = getDesignKitLoader(ctx.projectRoot);
5278
+ const action = input.action ?? "list";
5279
+ const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
5280
+ if (action === "foundations") {
5281
+ const text = await loader.foundationsText(stack);
5282
+ return { action, stack, output: text || "No foundations document is installed." };
5283
+ }
5284
+ if (action === "use") {
5285
+ const kitId = input.kit?.trim();
5286
+ if (!kitId) {
5287
+ const menu2 = await loader.menuText();
5288
+ return { action, output: `No kit id provided.
5289
+
5290
+ ${menu2}` };
5291
+ }
5292
+ const manifest = await loader.find(kitId);
5293
+ if (!manifest) {
5294
+ const menu2 = await loader.menuText();
5295
+ return { action, kit: kitId, output: `Kit "${kitId}" not found.
5296
+
5297
+ ${menu2}` };
5298
+ }
5299
+ const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
5300
+ const body = await loader.readBody(manifest.id, resolvedStack);
5301
+ const rawTokens = await loader.readTokens(manifest.id);
5302
+ const persisted = await loadActiveKit(ctx.projectRoot);
5303
+ const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
5304
+ const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
5305
+ const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
5306
+ setActiveKit(ctx, manifest.id, resolvedStack, overrides);
5307
+ await recordKitChoice(
5308
+ ctx.projectRoot,
5309
+ manifest.id,
5310
+ resolvedStack,
5311
+ "design-tool",
5312
+ (/* @__PURE__ */ new Date()).toISOString(),
5313
+ Object.keys(overrides).length ? overrides : void 0
5314
+ );
5315
+ const ovLine = Object.keys(overrides).length ? `
5316
+ Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
5317
+ ` : "";
5318
+ const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
5319
+ ${manifest.aesthetic}
5320
+ ${ovLine}
5321
+ Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
5322
+ `;
5323
+ const tokenBlock = tokens ? `
5324
+ ## Token snapshot (overrides applied)
5325
+ \`\`\`json
5326
+ ${JSON.stringify(tokens, null, 2)}
5327
+ \`\`\`
5328
+ Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
5329
+ ` : "";
5330
+ return {
5331
+ action,
5332
+ kit: manifest.id,
5333
+ stack: resolvedStack,
5334
+ output: `${header}${tokenBlock}
5335
+ ${body}`
5336
+ };
5337
+ }
5338
+ if (action === "set") {
5339
+ const patch = normalizeOverrides(input.set);
5340
+ if (Object.keys(patch).length === 0) {
5341
+ return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
5342
+ }
5343
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
5344
+ if (!merged) {
5345
+ return {
5346
+ action,
5347
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
5348
+ };
5349
+ }
5350
+ setDesignOverrides(ctx, merged);
5351
+ return {
5352
+ action,
5353
+ output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
5354
+ These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
5355
+ };
5356
+ }
5357
+ if (action === "materialize") {
5358
+ const active = await loadActiveKit(ctx.projectRoot);
5359
+ if (!active) {
5360
+ return {
5361
+ action,
5362
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
5363
+ };
5364
+ }
5365
+ const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
5366
+ const rawTokens = await loader.readTokens(active.kit);
5367
+ if (!rawTokens) {
5368
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
5369
+ }
5370
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
5371
+ const result = materializeTokens({
5372
+ tokens,
5373
+ stack: resolvedStack,
5374
+ kitId: active.kit,
5375
+ outPath: input.out
5376
+ });
5377
+ const abs = path3.join(ctx.projectRoot, result.path);
5378
+ let exists = false;
5379
+ try {
5380
+ await fs2.access(abs);
5381
+ exists = true;
5382
+ } catch {
5383
+ }
5384
+ if (exists && !input.force) {
5385
+ return {
5386
+ action,
5387
+ kit: active.kit,
5388
+ stack: resolvedStack,
5389
+ path: result.path,
5390
+ output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
5391
+
5392
+ \`\`\`
5393
+ ${result.content}
5394
+ \`\`\``
5395
+ };
5396
+ }
5397
+ await fs2.mkdir(path3.dirname(abs), { recursive: true });
5398
+ await fs2.writeFile(abs, result.content);
5399
+ return {
5400
+ action,
5401
+ kit: active.kit,
5402
+ stack: resolvedStack,
5403
+ path: result.path,
5404
+ 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)" : ""}`
5405
+ };
5406
+ }
5407
+ if (action === "verify") {
5408
+ const active = await loadActiveKit(ctx.projectRoot);
5409
+ if (!active) {
5410
+ return {
5411
+ action,
5412
+ output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
5413
+ };
5414
+ }
5415
+ const rawTokens = await loader.readTokens(active.kit);
5416
+ if (!rawTokens) {
5417
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
5418
+ }
5419
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
5420
+ const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
5421
+ const pct = Math.round(report.score * 100);
5422
+ const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
5423
+ const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
5424
+ ${top}${report.violations.length > 25 ? `
5425
+ \u2026and ${report.violations.length - 25} more` : ""}
5426
+
5427
+ 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.");
5428
+ return {
5429
+ action,
5430
+ kit: active.kit,
5431
+ output: summary,
5432
+ score: report.score,
5433
+ violations: report.violations.length
5434
+ };
5435
+ }
5436
+ const menu = await loader.menuText();
5437
+ return {
5438
+ action: "list",
5439
+ output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
5440
+ };
5441
+ }
5442
+ };
5137
5443
  var diffTool = {
5138
5444
  name: "diff",
5139
5445
  category: "Filesystem",
@@ -5508,7 +5814,7 @@ var editTool = {
5508
5814
  const newFile = toStyle(newFileLf, style);
5509
5815
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
5510
5816
  const written = await fs2.stat(absPath);
5511
- ctx.recordRead(absPath, written.mtimeMs);
5817
+ ctx.recordRead(absPath, written.mtimeMs, "write");
5512
5818
  ctx.session.recordFileChange({
5513
5819
  path: absPath,
5514
5820
  action: "modified",
@@ -5551,7 +5857,7 @@ function findSimilarity(haystack, needle) {
5551
5857
  }
5552
5858
  return line;
5553
5859
  }
5554
- var isWin2 = process.platform === "win32";
5860
+ var isWin3 = process.platform === "win32";
5555
5861
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
5556
5862
  // JS / TS toolchain
5557
5863
  "node",
@@ -5561,12 +5867,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
5561
5867
  "npx",
5562
5868
  "bun",
5563
5869
  "deno",
5870
+ "corepack",
5564
5871
  "tsc",
5872
+ "tsx",
5873
+ "ts-node",
5874
+ "vite",
5565
5875
  "vitest",
5566
5876
  "jest",
5567
5877
  "biome",
5568
5878
  "eslint",
5569
5879
  "prettier",
5880
+ "turbo",
5881
+ "nx",
5882
+ "webpack",
5883
+ "rollup",
5884
+ "parcel",
5885
+ "next",
5886
+ "astro",
5887
+ "playwright",
5888
+ "cypress",
5570
5889
  // version control
5571
5890
  "git",
5572
5891
  // Rust
@@ -5579,10 +5898,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
5579
5898
  "python3",
5580
5899
  "pip",
5581
5900
  "pip3",
5901
+ "pytest",
5902
+ "ruff",
5903
+ "mypy",
5904
+ "uv",
5905
+ "uvx",
5906
+ "poetry",
5907
+ "hatch",
5908
+ "tox",
5582
5909
  // Ruby
5583
5910
  "ruby",
5584
5911
  "gem",
5585
5912
  "bundle",
5913
+ // PHP
5914
+ "php",
5915
+ "composer",
5916
+ "phpunit",
5586
5917
  // JVM
5587
5918
  "java",
5588
5919
  "javac",
@@ -5594,18 +5925,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
5594
5925
  // C / C++ / native build
5595
5926
  "make",
5596
5927
  "cmake",
5597
- // containers / orchestration (read-only subcommands; see BLOCKED_ARG_PATTERNS)
5928
+ // containers / orchestration
5598
5929
  "docker",
5930
+ "podman",
5599
5931
  "kubectl",
5600
5932
  // common POSIX file/text utilities
5933
+ "pwd",
5601
5934
  "ls",
5602
5935
  "cat",
5603
5936
  "head",
5604
5937
  "tail",
5605
5938
  "wc",
5606
5939
  "grep",
5940
+ "rg",
5607
5941
  "find",
5608
5942
  "echo",
5943
+ "sort",
5944
+ "uniq",
5945
+ "sed",
5946
+ "awk",
5609
5947
  "mkdir",
5610
5948
  "cp",
5611
5949
  "mv",
@@ -5621,8 +5959,7 @@ var MAX_ARGS = 20;
5621
5959
  var MAX_OUTPUT2 = 2e5;
5622
5960
  var DEFAULT_TIMEOUT_MS2 = 3e4;
5623
5961
  var BLOCKED_ARG_PATTERNS = {
5624
- // python -c/--command executes arbitrary code; python -m runs modules
5625
- python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
5962
+ python: [],
5626
5963
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
5627
5964
  // -C <dir> changes working directory, bypassing cwd sandbox;
5628
5965
  // -c/--config <k>=<v> injects config that runs commands
@@ -5638,15 +5975,10 @@ var BLOCKED_ARG_PATTERNS = {
5638
5975
  /^--config=/,
5639
5976
  /^--config-env=/
5640
5977
  ],
5641
- // node -r/--require preloads arbitrary modules; --eval executes code
5642
- node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
5643
- // go run could execute arbitrary .go files; -ldflags could inject build-time code
5644
- go: [/^-ldflags$/],
5645
- // bun --preload is similar to node --require
5646
- bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
5647
- // docker build/run can create containers with host access;
5648
- // only allow read-only commands (ps, images, version)
5649
- docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
5978
+ node: [],
5979
+ go: [],
5980
+ bun: [],
5981
+ docker: [],
5650
5982
  // find -exec/-ok/-execdir execute arbitrary commands
5651
5983
  find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
5652
5984
  // rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
@@ -5654,15 +5986,49 @@ var BLOCKED_ARG_PATTERNS = {
5654
5986
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
5655
5987
  // `rm -rf /*` targets the filesystem root. All are blocked.
5656
5988
  rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
5657
- // npm run/exec/create/pack/publish can execute arbitrary scripts or publish malware
5658
- npm: [/^run$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
5659
- // pnpm run/dlx/exec/create can execute arbitrary scripts
5660
- pnpm: [/^run$/, /^dlx$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
5661
- // npx should only be used for --version; any package name is a vector for
5662
- // malicious package execution (typosquatting, dependency confusion)
5663
- npx: [/^[^\s]+$/]
5989
+ // npm/pnpm subcommands are checked separately below. Matching every arg here
5990
+ // over-blocked normal dev flows such as `pnpm vitest run ...`.
5991
+ npm: [],
5992
+ pnpm: [],
5993
+ npx: []
5994
+ };
5995
+ var BLOCKED_SUBCOMMANDS = {
5996
+ docker: /* @__PURE__ */ new Set(["push"]),
5997
+ podman: /* @__PURE__ */ new Set(["push"]),
5998
+ npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
5999
+ pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
6000
+ yarn: /* @__PURE__ */ new Set(["publish"])
5664
6001
  };
6002
+ var BLOCKED_SUBCOMMAND_SEQUENCES = {
6003
+ yarn: [["npm", "publish"]]
6004
+ };
6005
+ function firstSubcommand(args) {
6006
+ for (const arg of args) {
6007
+ if (arg === "--") return null;
6008
+ if (!arg.startsWith("-")) return arg;
6009
+ }
6010
+ return null;
6011
+ }
6012
+ function subcommandArgs(args) {
6013
+ const out = [];
6014
+ for (const arg of args) {
6015
+ if (arg === "--") break;
6016
+ if (!arg.startsWith("-")) out.push(arg);
6017
+ }
6018
+ return out;
6019
+ }
5665
6020
  function validateArgs(cmd, args) {
6021
+ const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
6022
+ const subcommand = firstSubcommand(args);
6023
+ if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
6024
+ return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
6025
+ }
6026
+ const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
6027
+ if (blockedSequences) {
6028
+ const actual = subcommandArgs(args);
6029
+ const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
6030
+ if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
6031
+ }
5666
6032
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
5667
6033
  if (!blocked) return null;
5668
6034
  for (const arg of args) {
@@ -5789,7 +6155,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5789
6155
  const startedAt = Date.now();
5790
6156
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
5791
6157
  const resolved = resolveWin32Command(cmd);
5792
- const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
6158
+ const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5793
6159
  const spawnCmd = needsShell ? cmd : resolved;
5794
6160
  if (needsShell) assertSafeWin32ShellArgs(args);
5795
6161
  let child;
@@ -5799,7 +6165,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5799
6165
  env: buildChildEnv(sessionId),
5800
6166
  stdio: ["ignore", "pipe", "pipe"],
5801
6167
  windowsHide: true,
5802
- ...isWin2 ? {} : { signal },
6168
+ ...isWin3 ? {} : { signal },
5803
6169
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
5804
6170
  });
5805
6171
  } catch (err) {
@@ -5819,7 +6185,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5819
6185
  const isAbort = err && err.code === "ABORT_ERR";
5820
6186
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
5821
6187
  clearTimeout(timer);
5822
- if (isWin2) signal.removeEventListener("abort", onAbort);
6188
+ if (isWin3) signal.removeEventListener("abort", onAbort);
5823
6189
  if (typeof pid === "number") registry.unregister(pid);
5824
6190
  registry.afterCall(Date.now() - startedAt, true);
5825
6191
  spool.finalize();
@@ -5849,7 +6215,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5849
6215
  if (typeof pid === "number") registry.kill(pid, { force: true });
5850
6216
  else child.kill("SIGTERM");
5851
6217
  };
5852
- if (isWin2) {
6218
+ if (isWin3) {
5853
6219
  if (signal.aborted) onAbort();
5854
6220
  else signal.addEventListener("abort", onAbort, { once: true });
5855
6221
  }
@@ -5865,7 +6231,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5865
6231
  });
5866
6232
  child.on("close", (code) => {
5867
6233
  clearTimeout(timer);
5868
- if (isWin2) signal.removeEventListener("abort", onAbort);
6234
+ if (isWin3) signal.removeEventListener("abort", onAbort);
5869
6235
  if (typeof pid === "number") registry.unregister(pid);
5870
6236
  const durationMs = Date.now() - startedAt;
5871
6237
  const exitCode = killed ? 124 : code ?? 1;
@@ -6029,7 +6395,7 @@ var fetchTool = {
6029
6395
  if (!final) throw new Error("fetch: stream ended without final event");
6030
6396
  return final;
6031
6397
  },
6032
- async *executeStream(input, _ctx, opts) {
6398
+ async *executeStream(input, ctx, opts) {
6033
6399
  if (!input?.url) throw new Error("fetch: url is required");
6034
6400
  const u = new URL(input.url);
6035
6401
  if (u.protocol !== "https:" && u.protocol !== "http:") {
@@ -6101,6 +6467,14 @@ var fetchTool = {
6101
6467
  url: res.url
6102
6468
  }
6103
6469
  };
6470
+ ctx.recordSideEffect?.({
6471
+ toolUseId: `fetch-${Date.now()}`,
6472
+ toolName: "fetch",
6473
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
6474
+ input: { url: input.url, format: input.format },
6475
+ outcome: `HTTP ${res.status} (${ct})`,
6476
+ risk: "network"
6477
+ });
6104
6478
  } finally {
6105
6479
  clearTimeout(timer);
6106
6480
  }
@@ -7193,6 +7567,14 @@ var installTool = {
7193
7567
  }
7194
7568
  }
7195
7569
  }
7570
+ ctx.recordSideEffect?.({
7571
+ toolUseId: `install-${Date.now()}`,
7572
+ toolName: "install",
7573
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7574
+ input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
7575
+ outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
7576
+ risk: "package"
7577
+ });
7196
7578
  yield { type: "final", output };
7197
7579
  }
7198
7580
  };
@@ -7282,8 +7664,8 @@ var jsonTool = {
7282
7664
  };
7283
7665
  }
7284
7666
  };
7285
- function query(data, path21) {
7286
- const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
7667
+ function query(data, path22) {
7668
+ const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
7287
7669
  let current = data;
7288
7670
  for (const part of parts) {
7289
7671
  if (current === null || current === void 0) return void 0;
@@ -7560,7 +7942,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
7560
7942
  }
7561
7943
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
7562
7944
  var MAX_TAIL_LINES = 1e5;
7563
- async function fileLogs(path21, lines, filterRe, stream) {
7945
+ async function fileLogs(path22, lines, filterRe, stream) {
7564
7946
  const { createInterface } = await import('node:readline');
7565
7947
  const { createReadStream } = await import('node:fs');
7566
7948
  const entries = [];
@@ -7569,7 +7951,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
7569
7951
  let writeIdx = 0;
7570
7952
  let totalLines = 0;
7571
7953
  const rl = createInterface({
7572
- input: createReadStream(path21),
7954
+ input: createReadStream(path22),
7573
7955
  crlfDelay: Number.POSITIVE_INFINITY
7574
7956
  });
7575
7957
  for await (const line of rl) {
@@ -7590,7 +7972,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
7590
7972
  if (parsed) entries.push(parsed);
7591
7973
  }
7592
7974
  return {
7593
- source: path21,
7975
+ source: path22,
7594
7976
  entries,
7595
7977
  total: entries.length,
7596
7978
  truncated: totalLines > effLines,
@@ -10155,7 +10537,7 @@ var writeTool = {
10155
10537
  if (existed) {
10156
10538
  if (!ctx.hasRead(absPath)) {
10157
10539
  prev = await fs2.readFile(absPath, "utf8");
10158
- ctx.recordRead(absPath, stat12.mtimeMs);
10540
+ ctx.recordRead(absPath, stat12.mtimeMs, "write");
10159
10541
  } else {
10160
10542
  prev = await fs2.readFile(absPath, "utf8");
10161
10543
  }
@@ -10169,7 +10551,7 @@ var writeTool = {
10169
10551
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
10170
10552
  + (new file, ${input.content.split("\n").length} lines)`;
10171
10553
  const stat11 = await fs2.stat(absPath);
10172
- ctx.recordRead(absPath, stat11.mtimeMs);
10554
+ ctx.recordRead(absPath, stat11.mtimeMs, "write");
10173
10555
  ctx.session.recordFileChange({
10174
10556
  path: absPath,
10175
10557
  action: existed ? "modified" : "created",
@@ -10228,7 +10610,8 @@ var TIER2_TOOLS = [
10228
10610
  planTool,
10229
10611
  taskTool,
10230
10612
  installTool,
10231
- auditTool
10613
+ auditTool,
10614
+ designTool
10232
10615
  ];
10233
10616
  var TIER3_TOOLS = [
10234
10617
  outdatedTool,
@@ -10273,6 +10656,7 @@ var builtinTools = [
10273
10656
  logsTool,
10274
10657
  documentTool,
10275
10658
  scaffoldTool,
10659
+ designTool,
10276
10660
  toolSearchTool,
10277
10661
  toolUseTool,
10278
10662
  batchToolUseTool,