@wrongstack/tools 0.305.0 → 0.306.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/_shell-pick.d.ts +4 -5
  2. package/dist/_util.d.ts +22 -5
  3. package/dist/audit.d.ts +0 -1
  4. package/dist/audit.js +135 -46
  5. package/dist/bash.js +61 -37
  6. package/dist/browser/index.js +29 -9
  7. package/dist/browser/types.d.ts +7 -1
  8. package/dist/builtin.js +1280 -728
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.d.ts +1 -0
  11. package/dist/codebase-index/index.js +225 -153
  12. package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
  13. package/dist/codebase-index/project-server.js +19 -20
  14. package/dist/diff.d.ts +5 -0
  15. package/dist/diff.js +78 -12
  16. package/dist/document.js +18 -6
  17. package/dist/edit.js +69 -16
  18. package/dist/exec.js +44 -22
  19. package/dist/fetch.js +13 -1
  20. package/dist/format.d.ts +4 -2
  21. package/dist/format.js +81 -31
  22. package/dist/glob.js +12 -4
  23. package/dist/grep.d.ts +2 -0
  24. package/dist/grep.js +15 -4
  25. package/dist/index.js +1357 -765
  26. package/dist/install.js +96 -37
  27. package/dist/kanban-tool-types.d.ts +6 -1
  28. package/dist/kanban.js +60 -0
  29. package/dist/languages/index.js +28 -13
  30. package/dist/lint.js +28 -13
  31. package/dist/logs.d.ts +0 -1
  32. package/dist/logs.js +44 -13
  33. package/dist/memory.d.ts +8 -0
  34. package/dist/memory.js +23 -3
  35. package/dist/mode.d.ts +1 -1
  36. package/dist/mode.js +3 -0
  37. package/dist/next-steps.d.ts +2 -3
  38. package/dist/next-steps.js +3 -3
  39. package/dist/outdated.d.ts +0 -3
  40. package/dist/outdated.js +89 -48
  41. package/dist/pack.js +1280 -728
  42. package/dist/plan.js +76 -3
  43. package/dist/process-registry.d.ts +8 -2
  44. package/dist/process-registry.js +28 -13
  45. package/dist/ps-slash.js +22 -12
  46. package/dist/read.js +10 -4
  47. package/dist/replace.d.ts +4 -0
  48. package/dist/replace.js +104 -7
  49. package/dist/search.d.ts +6 -0
  50. package/dist/search.js +47 -26
  51. package/dist/session-kanban.d.ts +25 -0
  52. package/dist/session-kanban.js +16 -0
  53. package/dist/skill.d.ts +6 -0
  54. package/dist/skill.js +9 -10
  55. package/dist/task.js +66 -2
  56. package/dist/test.js +28 -13
  57. package/dist/todo.js +64 -2
  58. package/dist/tool-icons.js +4 -2
  59. package/dist/tool-summary.d.ts +1 -1
  60. package/dist/tool-summary.js +76 -1
  61. package/dist/tool-tier.js +1280 -728
  62. package/dist/tree.js +9 -10
  63. package/dist/typecheck.d.ts +0 -2
  64. package/dist/typecheck.js +98 -31
  65. package/dist/write.js +58 -10
  66. package/package.json +4 -3
package/dist/search.js CHANGED
@@ -102,6 +102,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
102
102
  if (res.status < 300 || res.status > 399) {
103
103
  return res;
104
104
  }
105
+ try {
106
+ await res.body?.cancel();
107
+ } catch {
108
+ }
105
109
  redirectCount++;
106
110
  if (redirectCount > maxRedirects) {
107
111
  throw new FetchError({
@@ -167,6 +171,7 @@ async function assertNotPrivate(hostname) {
167
171
  import { toErrorMessage } from "@wrongstack/core/utils";
168
172
  var DEFAULT_NUM = 10;
169
173
  var MAX_RESULTS = 50;
174
+ var MAX_SNIPPET_CHARS = 300;
170
175
  var TIMEOUT_MS = 15e3;
171
176
  var CACHE_TTL_MS = 3e5;
172
177
  var CACHE_MAX_ENTRIES = 200;
@@ -174,7 +179,7 @@ var cache = /* @__PURE__ */ new Map();
174
179
  var searchTool = {
175
180
  name: "search",
176
181
  category: "Search",
177
- description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
182
+ description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL. google and bing are best-effort HTML scrapes that fall back to duckduckgo when they return nothing usable.",
178
183
  usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
179
184
  permission: "auto",
180
185
  mutating: false,
@@ -248,7 +253,7 @@ var searchTool = {
248
253
  query: input.query,
249
254
  results: results.slice(0, num),
250
255
  source: entry.source,
251
- truncated: results.length >= num,
256
+ truncated: results.length > num,
252
257
  cached: true
253
258
  }
254
259
  };
@@ -260,17 +265,17 @@ var searchTool = {
260
265
  text: `Querying ${source} for "${input.query}"\u2026`,
261
266
  data: { source, query: input.query, cached: false }
262
267
  };
263
- let rawResults;
268
+ let engine;
264
269
  let effectiveSource = source;
265
270
  switch (source) {
266
271
  case "duckduckgo":
267
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
272
+ engine = await duckduckgoSearch(input.query, opts.signal);
268
273
  break;
269
274
  case "google":
270
- rawResults = await googleSearch(input.query, num, opts.signal);
275
+ engine = await googleSearch(input.query, opts.signal);
271
276
  break;
272
277
  case "bing":
273
- rawResults = await bingSearch(input.query, num, opts.signal);
278
+ engine = await bingSearch(input.query, opts.signal);
274
279
  break;
275
280
  default:
276
281
  throw new ToolValidationError2({
@@ -278,23 +283,27 @@ var searchTool = {
278
283
  field: "source"
279
284
  });
280
285
  }
281
- let ranked = rankSearchResults(rawResults, input.query);
286
+ let ranked = rankSearchResults(engine.results, input.query);
287
+ let engineError = engine.error;
282
288
  if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
283
289
  yield {
284
290
  type: "log",
285
291
  text: `${source} returned no relevant static results; falling back to duckduckgo`,
286
292
  data: { source, fallback: "duckduckgo", query: input.query }
287
293
  };
288
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
289
- ranked = rankSearchResults(rawResults, input.query);
294
+ const fallback = await duckduckgoSearch(input.query, opts.signal);
295
+ ranked = rankSearchResults(fallback.results, input.query);
296
+ engineError = fallback.error;
290
297
  effectiveSource = "duckduckgo";
291
298
  }
292
299
  const finalResults = ranked.slice(0, num);
293
- cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
294
- pruneCacheEntries();
300
+ if (!engineError) {
301
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
302
+ pruneCacheEntries();
303
+ }
295
304
  yield {
296
305
  type: "partial_output",
297
- text: `${finalResults.length} results from ${effectiveSource}`,
306
+ text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
298
307
  data: { count: finalResults.length, cached: false, source: effectiveSource }
299
308
  };
300
309
  yield {
@@ -307,8 +316,9 @@ var searchTool = {
307
316
  snippet: r.snippet
308
317
  })),
309
318
  source: effectiveSource,
310
- truncated: finalResults.length >= num,
311
- cached: false
319
+ truncated: ranked.length > num,
320
+ cached: false,
321
+ ...engineError ? { error: engineError } : {}
312
322
  }
313
323
  };
314
324
  }
@@ -362,18 +372,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
362
372
  return terms.some((term) => haystack.includes(term));
363
373
  });
364
374
  }
365
- async function duckduckgoSearch(query, num, signal) {
375
+ async function duckduckgoSearch(query, signal) {
366
376
  const encoded = encodeURIComponent(query);
367
377
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
368
378
  try {
369
379
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS);
370
380
  const html = await response.text();
371
- return parseDuckDuckGo(html, num);
381
+ return { results: parseDuckDuckGo(html, MAX_RESULTS) };
372
382
  } catch (err) {
373
383
  console.log(
374
384
  JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage(err) })
375
385
  );
376
- return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
386
+ return { results: [], error: `duckduckgo unreachable: ${toErrorMessage(err)}` };
377
387
  }
378
388
  }
379
389
  function takeFrom(iter, max) {
@@ -407,7 +417,7 @@ function parseDuckDuckGo(html, num) {
407
417
  results.push({
408
418
  title: entry.title ?? "",
409
419
  url: entry.url ?? "",
410
- snippet: snippetMatches[i] ?? "",
420
+ snippet: capSnippet(snippetMatches[i] ?? ""),
411
421
  score: 1
412
422
  });
413
423
  }
@@ -432,11 +442,15 @@ function normalizeDuckDuckGoUrl(raw) {
432
442
  return raw;
433
443
  }
434
444
  }
435
- async function googleSearch(query, num, signal) {
445
+ async function googleSearch(query, signal) {
436
446
  const encoded = encodeURIComponent(query);
437
447
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
438
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text()).catch(() => "");
439
- return parseGoogleResults(html, num);
448
+ try {
449
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text());
450
+ return { results: parseGoogleResults(html, MAX_RESULTS) };
451
+ } catch (err) {
452
+ return { results: [], error: `google unreachable: ${toErrorMessage(err)}` };
453
+ }
440
454
  }
441
455
  function parseGoogleResults(html, num) {
442
456
  const results = [];
@@ -459,17 +473,21 @@ function parseGoogleResults(html, num) {
459
473
  results.push({
460
474
  title: titles[i] ?? "",
461
475
  url: urls[i] ?? "",
462
- snippet: snippets[i] ?? "",
476
+ snippet: capSnippet(snippets[i] ?? ""),
463
477
  score: 1
464
478
  });
465
479
  }
466
480
  return results;
467
481
  }
468
- async function bingSearch(query, num, signal) {
482
+ async function bingSearch(query, signal) {
469
483
  const encoded = encodeURIComponent(query);
470
484
  const url = `https://www.bing.com/search?q=${encoded}`;
471
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text()).catch(() => "");
472
- return parseBingResults(html, num);
485
+ try {
486
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text());
487
+ return { results: parseBingResults(html, MAX_RESULTS) };
488
+ } catch (err) {
489
+ return { results: [], error: `bing unreachable: ${toErrorMessage(err)}` };
490
+ }
473
491
  }
474
492
  function parseBingResults(html, num) {
475
493
  const results = [];
@@ -482,7 +500,7 @@ function parseBingResults(html, num) {
482
500
  const title = stripTags(expectDefined(titleMatch[2]));
483
501
  if (!href || !title) return [];
484
502
  const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
485
- const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : "";
503
+ const snippet = snippetMatch ? capSnippet(stripTags(expectDefined(snippetMatch.at(-1)))) : "";
486
504
  return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
487
505
  }), num);
488
506
  for (let i = 0; i < entries.length; i++) {
@@ -549,6 +567,9 @@ function anySignal(...signals) {
549
567
  function stripTags(html) {
550
568
  return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
551
569
  }
570
+ function capSnippet(snippet) {
571
+ return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
572
+ }
552
573
  function decodeHtmlEntities(text) {
553
574
  return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
554
575
  }
@@ -97,6 +97,31 @@ export declare function rebindSessionKanbanTask(context: Context): Promise<{
97
97
  boardId: string;
98
98
  taskId: string;
99
99
  } | null>;
100
+ /**
101
+ * Why session Kanban is unavailable, or undefined while it is healthy.
102
+ *
103
+ * Surfaces exist so a startup path can tell the user their board will not sync
104
+ * without having to catch and classify the failure itself. Cleared on the
105
+ * first hydrate that succeeds, so a daemon that comes back stops being
106
+ * reported as down.
107
+ */
108
+ export declare function sessionKanbanDegradation(): string | undefined;
109
+ /**
110
+ * Project the session's todos, plan, and tasks onto its Kanban board.
111
+ *
112
+ * Kanban is a projection: every card here is derived from state that lives
113
+ * somewhere else and is authoritative there. Losing the board costs the user a
114
+ * view, not any work — so an unreachable Kanban daemon degrades this to `null`
115
+ * rather than propagating.
116
+ *
117
+ * That distinction used to be missing, and it cost an entire session. When a
118
+ * killed daemon left a stale IPC endpoint behind, the connect error thrown
119
+ * here unwound through `setupSession` and killed `wstack` at startup: no
120
+ * prompt, no session, just a socket path in a stack trace — for a board the
121
+ * user had not asked to see. `bindProjectEndpoint` now stops the daemon from
122
+ * wedging in the first place, but the call site must still hold: an optional
123
+ * view may never take down the session it decorates.
124
+ */
100
125
  export declare function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null>;
101
126
  export interface SessionKanbanSourceUpdate {
102
127
  source: 'todo' | 'task' | 'plan' | null;
@@ -720,9 +720,24 @@ async function rebindSessionKanbanTask(context) {
720
720
  context.setCurrentKanbanTask(best.taskId, best.boardId);
721
721
  return { boardId: best.boardId, taskId: best.taskId };
722
722
  }
723
+ var degradationReason;
724
+ function sessionKanbanDegradation() {
725
+ return degradationReason;
726
+ }
723
727
  async function hydrateSessionKanban(context) {
724
728
  const id = context.session?.id ?? "";
725
729
  if (!id) return null;
730
+ try {
731
+ const board = await hydrateSessionKanbanBoard(context, id);
732
+ degradationReason = void 0;
733
+ return board;
734
+ } catch (error) {
735
+ degradationReason = error instanceof Error ? error.message : String(error);
736
+ fireAndForget("hydrate", Promise.reject(error));
737
+ return null;
738
+ }
739
+ }
740
+ async function hydrateSessionKanbanBoard(context, id) {
726
741
  await rebindSessionKanbanTask(context);
727
742
  await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
728
743
  if (context.projectRoot) {
@@ -958,6 +973,7 @@ export {
958
973
  projectSessionTasksToKanban,
959
974
  projectSessionTodosToKanban,
960
975
  rebindSessionKanbanTask,
976
+ sessionKanbanDegradation,
961
977
  takeSessionMirrorFailure,
962
978
  taskFileToSerializedGraph,
963
979
  todoListToSerializedGraph,
package/dist/skill.d.ts CHANGED
@@ -27,6 +27,12 @@ interface SkillToolOutput {
27
27
  dir: string;
28
28
  /** When `resource` was requested: the loaded file. */
29
29
  loadedResource?: LoadedResource | undefined;
30
+ /**
31
+ * Soft advisory: prose in the skill body mentions tools not registered in
32
+ * this runtime (e.g. hidden by a token-saving tier). The skill still loads —
33
+ * only `manifest.requiredTools` hard-fails.
34
+ */
35
+ warning?: string | undefined;
30
36
  }
31
37
  /**
32
38
  * Skill tool — the agentskills.io progressive-disclosure primitive.
package/dist/skill.js CHANGED
@@ -74,12 +74,7 @@ function makeSkillTool(skillLoader) {
74
74
  runtimeToolReferencesFromText(raw),
75
75
  availableToolNames
76
76
  );
77
- if (missingBodyTools.length > 0) {
78
- throw new ToolValidationError({
79
- message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
80
- field: "name"
81
- });
82
- }
77
+ const warning = missingBodyTools.length > 0 ? `Warning: skill "${name}" references tools not registered in this runtime: ${missingBodyTools.join(", ")}. Steps that call them may be unavailable.` : void 0;
83
78
  const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
84
79
  const resources = loadedResource ? [] : await listResources(dir);
85
80
  try {
@@ -96,29 +91,33 @@ function makeSkillTool(skillLoader) {
96
91
  body,
97
92
  resources,
98
93
  dir,
99
- loadedResource
94
+ loadedResource,
95
+ warning
100
96
  };
101
97
  },
102
98
  serialize(output) {
99
+ const warningLine = output.warning ? `
100
+
101
+ ${output.warning}` : "";
103
102
  if (output.loadedResource) {
104
103
  const lr = output.loadedResource;
105
104
  const note = lr.truncated ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)` : "";
106
105
  return `# Resource: ${output.name}/${lr.rel}
107
106
  (abs path: ${lr.absPath})${note}
108
107
 
109
- ${lr.content}`;
108
+ ${lr.content}${warningLine}`;
110
109
  }
111
110
  const head = `# Skill: ${output.name}
112
111
  ${output.description}
113
112
 
114
113
  ${output.body}`;
115
- if (output.resources.length === 0) return head;
114
+ if (output.resources.length === 0) return `${head}${warningLine}`;
116
115
  const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join("\n");
117
116
  return `${head}
118
117
 
119
118
  ## Bundled resources (load on demand)
120
119
  Load any with: \`skill({ name: "${output.name}", resource: "<path>" })\`. Run scripts via bash using their abs path under ${output.dir}.
121
- ${listing}`;
120
+ ${listing}${warningLine}`;
122
121
  }
123
122
  };
124
123
  }
package/dist/task.js CHANGED
@@ -1350,6 +1350,18 @@ var KANBAN_INPUT_SCHEMA = {
1350
1350
  },
1351
1351
  transitionAction: { type: "string" },
1352
1352
  transitionComment: { type: "string" },
1353
+ tickChecks: {
1354
+ type: "array",
1355
+ items: {
1356
+ type: "object",
1357
+ properties: {
1358
+ checkId: { type: "string" },
1359
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
1360
+ },
1361
+ required: ["checkId", "checkStatus"]
1362
+ },
1363
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
1364
+ },
1353
1365
  attachmentUrl: { type: "string" },
1354
1366
  attachmentTitle: { type: "string" },
1355
1367
  attachmentType: {
@@ -1537,6 +1549,9 @@ var kanbanTool = {
1537
1549
  description: KANBAN_TOOL_DESCRIPTION,
1538
1550
  usageHint: KANBAN_TOOL_USAGE_HINT,
1539
1551
  permission: "confirm",
1552
+ // WS-046: gives permission decisions something to key on.
1553
+ // The action performed; kanban has no single file or path subject.
1554
+ subjectKey: "action",
1540
1555
  mutating: true,
1541
1556
  capabilities: ["fs.write"],
1542
1557
  icon: "task",
@@ -1978,6 +1993,7 @@ var kanbanTool = {
1978
1993
  actor: input.author,
1979
1994
  comment: input.transitionComment,
1980
1995
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1996
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1981
1997
  ...input.attachmentUrl !== void 0 ? {
1982
1998
  attachment: {
1983
1999
  url: input.attachmentUrl,
@@ -2370,8 +2386,52 @@ var kanbanTool = {
2370
2386
  } catch (err) {
2371
2387
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2372
2388
  }
2389
+ },
2390
+ serialize(output, input) {
2391
+ return serializeKanbanOutput(output, input);
2373
2392
  }
2374
2393
  };
2394
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2395
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2396
+ "get_board",
2397
+ "export_markdown",
2398
+ "export_task_graph"
2399
+ ]);
2400
+ function serializeKanbanOutput(output, input) {
2401
+ const action = input && typeof input === "object" ? input.action : void 0;
2402
+ const board = output.board;
2403
+ if (board) {
2404
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2405
+ let boardBytes = 0;
2406
+ if (!keepFull) {
2407
+ try {
2408
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2409
+ } catch {
2410
+ boardBytes = 0;
2411
+ }
2412
+ }
2413
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2414
+ const columns = {};
2415
+ for (const column of board.columns) {
2416
+ columns[column.title || column.id] = board.tasks.filter(
2417
+ (task) => task.columnId === column.id
2418
+ ).length;
2419
+ }
2420
+ const compact = {
2421
+ ...output,
2422
+ board: {
2423
+ id: board.id,
2424
+ title: board.title,
2425
+ columns,
2426
+ totalTasks: board.tasks.length,
2427
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2428
+ }
2429
+ };
2430
+ return JSON.stringify(compact, null, 2);
2431
+ }
2432
+ }
2433
+ return JSON.stringify(output, null, 2);
2434
+ }
2375
2435
 
2376
2436
  // src/todo.ts
2377
2437
  function normalizedTitle(value) {
@@ -2710,7 +2770,8 @@ var todoTool = {
2710
2770
  }
2711
2771
  for (const planId of completedPlanIds) {
2712
2772
  if (pendingPlanIds.has(planId)) continue;
2713
- const planPath = ctx.meta["plan.path"];
2773
+ const meta = ctx.meta;
2774
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
2714
2775
  if (typeof planPath !== "string" || !planPath) continue;
2715
2776
  try {
2716
2777
  const plan = await loadPlan2(planPath);
@@ -2723,7 +2784,8 @@ var todoTool = {
2723
2784
  }
2724
2785
  for (const taskId of completedTaskIds) {
2725
2786
  if (pendingTaskIds.has(taskId)) continue;
2726
- const taskPath = ctx.meta["task.path"];
2787
+ const meta = ctx.meta;
2788
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
2727
2789
  if (typeof taskPath !== "string" || !taskPath) continue;
2728
2790
  try {
2729
2791
  const file = await loadTasks3(taskPath);
@@ -3204,6 +3266,7 @@ var taskTool = {
3204
3266
  inProgress: 0
3205
3267
  };
3206
3268
  }
3269
+ ctx.meta["task.path.resolved"] = taskPath;
3207
3270
  if (todosToReplace) {
3208
3271
  await todoTool.execute({ todos: todosToReplace }, ctx, {
3209
3272
  signal: AbortSignal.timeout(3e4)
@@ -3228,6 +3291,7 @@ var taskTool = {
3228
3291
  formatted = formatPlan(updated);
3229
3292
  return updated;
3230
3293
  });
3294
+ ctx.meta["plan.path.resolved"] = planPath;
3231
3295
  } catch (err) {
3232
3296
  return {
3233
3297
  ok: false,
package/dist/test.js CHANGED
@@ -367,8 +367,11 @@ var SENSITIVE_FLAG_PATTERNS = [
367
367
  /--(?: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,
368
368
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
369
369
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
370
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
371
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
372
+ // redacted, not just the first.
370
373
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
371
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
374
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
372
375
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
373
376
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
374
377
  // redaction function. Synced with core copy.
@@ -376,8 +379,9 @@ var SENSITIVE_FLAG_PATTERNS = [
376
379
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
377
380
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
378
381
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
379
- // when preceded by a flag name (e.g. --github-token=EyJ...).
380
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
382
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
383
+ // every such flag in the command line is redacted, not just the first.
384
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
381
385
  ];
382
386
  function redactCommand(cmd) {
383
387
  let result = cmd;
@@ -466,11 +470,15 @@ var ProcessRegistryImpl = class {
466
470
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
467
471
  }
468
472
  _canSignalProcessGroup(p) {
469
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
473
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
470
474
  }
471
475
  _killChildDirect(p, signal) {
472
476
  try {
473
- p.child.kill(signal);
477
+ if (p.child) {
478
+ p.child.kill(signal);
479
+ return;
480
+ }
481
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
474
482
  } catch {
475
483
  }
476
484
  }
@@ -668,15 +676,15 @@ var ProcessRegistryImpl = class {
668
676
  this._pruneStale(pid);
669
677
  const p = this.processes.get(pid);
670
678
  if (!p) return false;
671
- if (p.killed) return true;
679
+ if (p.killed && opts.force !== true) return true;
672
680
  if (p.protected && opts.includeProtected !== true) return false;
673
681
  if (opts.preserveBackground && p.background) return false;
674
682
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
675
683
  const isWin2 = os.platform() === "win32";
676
684
  if (isWin2) {
677
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
685
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
678
686
  const directFallback = () => {
679
- if (p.child.exitCode === null) {
687
+ if (p.child && p.child.exitCode === null) {
680
688
  try {
681
689
  p.child.kill("SIGKILL");
682
690
  } catch {
@@ -688,10 +696,7 @@ var ProcessRegistryImpl = class {
688
696
  onSettled: directFallback
689
697
  })) {
690
698
  } else {
691
- try {
692
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
693
- } catch {
694
- }
699
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
695
700
  }
696
701
  p.killed = true;
697
702
  return true;
@@ -702,7 +707,7 @@ var ProcessRegistryImpl = class {
702
707
  } else {
703
708
  this._killPosix(p, "SIGTERM");
704
709
  const timer = setTimeout(() => {
705
- if (this.processes.has(pid) && !p.child.killed) {
710
+ if (this.processes.has(pid) && !p.child?.killed) {
706
711
  this._killPosix(p, "SIGKILL");
707
712
  }
708
713
  }, graceMs);
@@ -757,6 +762,16 @@ var ProcessRegistryImpl = class {
757
762
  * before reusing a PID, but we want to clean up before that becomes a risk.
758
763
  */
759
764
  _isStaleEntry(entry) {
765
+ if (entry.child === null) {
766
+ if (Date.now() - entry.startedAt <= 6e4) return false;
767
+ if (os.platform() === "win32") return false;
768
+ try {
769
+ process.kill(entry.pid, 0);
770
+ return false;
771
+ } catch (err) {
772
+ return err.code !== "EPERM";
773
+ }
774
+ }
760
775
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
761
776
  }
762
777
  /**
package/dist/todo.js CHANGED
@@ -870,6 +870,18 @@ var KANBAN_INPUT_SCHEMA = {
870
870
  },
871
871
  transitionAction: { type: "string" },
872
872
  transitionComment: { type: "string" },
873
+ tickChecks: {
874
+ type: "array",
875
+ items: {
876
+ type: "object",
877
+ properties: {
878
+ checkId: { type: "string" },
879
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
880
+ },
881
+ required: ["checkId", "checkStatus"]
882
+ },
883
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
884
+ },
873
885
  attachmentUrl: { type: "string" },
874
886
  attachmentTitle: { type: "string" },
875
887
  attachmentType: {
@@ -1524,6 +1536,9 @@ var kanbanTool = {
1524
1536
  description: KANBAN_TOOL_DESCRIPTION,
1525
1537
  usageHint: KANBAN_TOOL_USAGE_HINT,
1526
1538
  permission: "confirm",
1539
+ // WS-046: gives permission decisions something to key on.
1540
+ // The action performed; kanban has no single file or path subject.
1541
+ subjectKey: "action",
1527
1542
  mutating: true,
1528
1543
  capabilities: ["fs.write"],
1529
1544
  icon: "task",
@@ -1965,6 +1980,7 @@ var kanbanTool = {
1965
1980
  actor: input.author,
1966
1981
  comment: input.transitionComment,
1967
1982
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1983
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1968
1984
  ...input.attachmentUrl !== void 0 ? {
1969
1985
  attachment: {
1970
1986
  url: input.attachmentUrl,
@@ -2357,8 +2373,52 @@ var kanbanTool = {
2357
2373
  } catch (err) {
2358
2374
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2359
2375
  }
2376
+ },
2377
+ serialize(output, input) {
2378
+ return serializeKanbanOutput(output, input);
2360
2379
  }
2361
2380
  };
2381
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2382
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2383
+ "get_board",
2384
+ "export_markdown",
2385
+ "export_task_graph"
2386
+ ]);
2387
+ function serializeKanbanOutput(output, input) {
2388
+ const action = input && typeof input === "object" ? input.action : void 0;
2389
+ const board = output.board;
2390
+ if (board) {
2391
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2392
+ let boardBytes = 0;
2393
+ if (!keepFull) {
2394
+ try {
2395
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2396
+ } catch {
2397
+ boardBytes = 0;
2398
+ }
2399
+ }
2400
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2401
+ const columns = {};
2402
+ for (const column of board.columns) {
2403
+ columns[column.title || column.id] = board.tasks.filter(
2404
+ (task) => task.columnId === column.id
2405
+ ).length;
2406
+ }
2407
+ const compact = {
2408
+ ...output,
2409
+ board: {
2410
+ id: board.id,
2411
+ title: board.title,
2412
+ columns,
2413
+ totalTasks: board.tasks.length,
2414
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2415
+ }
2416
+ };
2417
+ return JSON.stringify(compact, null, 2);
2418
+ }
2419
+ }
2420
+ return JSON.stringify(output, null, 2);
2421
+ }
2362
2422
 
2363
2423
  // src/todo.ts
2364
2424
  function normalizedTitle(value) {
@@ -2697,7 +2757,8 @@ var todoTool = {
2697
2757
  }
2698
2758
  for (const planId of completedPlanIds) {
2699
2759
  if (pendingPlanIds.has(planId)) continue;
2700
- const planPath = ctx.meta["plan.path"];
2760
+ const meta = ctx.meta;
2761
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
2701
2762
  if (typeof planPath !== "string" || !planPath) continue;
2702
2763
  try {
2703
2764
  const plan = await loadPlan2(planPath);
@@ -2710,7 +2771,8 @@ var todoTool = {
2710
2771
  }
2711
2772
  for (const taskId of completedTaskIds) {
2712
2773
  if (pendingTaskIds.has(taskId)) continue;
2713
- const taskPath = ctx.meta["task.path"];
2774
+ const meta = ctx.meta;
2775
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
2714
2776
  if (typeof taskPath !== "string" || !taskPath) continue;
2715
2777
  try {
2716
2778
  const file = await loadTasks3(taskPath);
@@ -60,13 +60,15 @@ var TOOL_ICON_MAP = {
60
60
  replace: "edit",
61
61
  str_replace: "edit",
62
62
  multi_edit: "edit",
63
- patch: "diff",
63
+ // Matches patchTool's own `icon: 'edit'` (tool-icon-map.ts agrees).
64
+ patch: "edit",
64
65
  // ── search ──
65
66
  grep: "search",
66
67
  search: "search",
67
68
  rg: "search",
68
69
  ripgrep: "search",
69
- glob: "search",
70
+ // Matches globTool's own `icon: 'folder'` (tool-icon-map.ts agrees).
71
+ glob: "folder",
70
72
  find: "search",
71
73
  // ── navigation ──
72
74
  folder: "folder",