@wrongstack/tools 0.305.1 → 0.306.2

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 (63) 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 +1294 -726
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.js +223 -152
  11. package/dist/codebase-index/project-server.js +10 -11
  12. package/dist/diff.d.ts +5 -0
  13. package/dist/diff.js +78 -12
  14. package/dist/document.js +18 -6
  15. package/dist/edit.js +69 -16
  16. package/dist/exec.js +44 -22
  17. package/dist/fetch.js +13 -1
  18. package/dist/format.d.ts +4 -2
  19. package/dist/format.js +81 -31
  20. package/dist/glob.js +12 -4
  21. package/dist/grep.d.ts +2 -0
  22. package/dist/grep.js +15 -4
  23. package/dist/index.js +1359 -762
  24. package/dist/install.js +96 -37
  25. package/dist/kanban-tool-types.d.ts +6 -1
  26. package/dist/kanban.js +60 -0
  27. package/dist/languages/index.js +28 -13
  28. package/dist/lint.js +28 -13
  29. package/dist/logs.d.ts +0 -1
  30. package/dist/logs.js +44 -13
  31. package/dist/memory.d.ts +8 -0
  32. package/dist/memory.js +23 -3
  33. package/dist/mode.d.ts +1 -1
  34. package/dist/mode.js +3 -0
  35. package/dist/next-steps.d.ts +2 -3
  36. package/dist/next-steps.js +3 -3
  37. package/dist/outdated.d.ts +0 -3
  38. package/dist/outdated.js +89 -48
  39. package/dist/pack.js +1294 -726
  40. package/dist/plan.js +91 -3
  41. package/dist/process-registry.d.ts +8 -2
  42. package/dist/process-registry.js +28 -13
  43. package/dist/ps-slash.js +22 -12
  44. package/dist/read.js +10 -3
  45. package/dist/replace.d.ts +4 -0
  46. package/dist/replace.js +104 -7
  47. package/dist/search.d.ts +6 -0
  48. package/dist/search.js +47 -26
  49. package/dist/session-kanban.js +3 -1
  50. package/dist/skill.d.ts +6 -0
  51. package/dist/skill.js +9 -10
  52. package/dist/task.js +81 -2
  53. package/dist/test.js +28 -13
  54. package/dist/todo.js +79 -2
  55. package/dist/tool-icons.js +4 -2
  56. package/dist/tool-summary.d.ts +1 -1
  57. package/dist/tool-summary.js +76 -1
  58. package/dist/tool-tier.js +1294 -726
  59. package/dist/tree.js +9 -10
  60. package/dist/typecheck.d.ts +0 -2
  61. package/dist/typecheck.js +98 -31
  62. package/dist/write.js +58 -10
  63. package/package.json +4 -4
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
  }
@@ -770,6 +770,7 @@ function sourceStatus(task) {
770
770
  function todoStatus(task) {
771
771
  const status = sourceStatus(task);
772
772
  if (status === "completed") return "completed";
773
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
773
774
  if (status === "in_progress" || status === "review") return "in_progress";
774
775
  return "pending";
775
776
  }
@@ -891,11 +892,12 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
891
892
  const graphId = task.origin?.graphId ?? "";
892
893
  if (!originId) return { source: null };
893
894
  if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
895
+ const mappedStatus = todoStatus(task);
894
896
  const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
895
897
  (todo) => todo.id === originId ? {
896
898
  ...todo,
897
899
  content: task.title,
898
- status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
900
+ status: mappedStatus
899
901
  } : todo
900
902
  );
901
903
  suppressedTodoMirrors.add(context);
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
@@ -386,6 +386,7 @@ function sourceStatus(task) {
386
386
  function todoStatus(task) {
387
387
  const status = sourceStatus(task);
388
388
  if (status === "completed") return "completed";
389
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
389
390
  if (status === "in_progress" || status === "review") return "in_progress";
390
391
  return "pending";
391
392
  }
@@ -1350,6 +1351,18 @@ var KANBAN_INPUT_SCHEMA = {
1350
1351
  },
1351
1352
  transitionAction: { type: "string" },
1352
1353
  transitionComment: { type: "string" },
1354
+ tickChecks: {
1355
+ type: "array",
1356
+ items: {
1357
+ type: "object",
1358
+ properties: {
1359
+ checkId: { type: "string" },
1360
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
1361
+ },
1362
+ required: ["checkId", "checkStatus"]
1363
+ },
1364
+ 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."
1365
+ },
1353
1366
  attachmentUrl: { type: "string" },
1354
1367
  attachmentTitle: { type: "string" },
1355
1368
  attachmentType: {
@@ -1537,6 +1550,9 @@ var kanbanTool = {
1537
1550
  description: KANBAN_TOOL_DESCRIPTION,
1538
1551
  usageHint: KANBAN_TOOL_USAGE_HINT,
1539
1552
  permission: "confirm",
1553
+ // WS-046: gives permission decisions something to key on.
1554
+ // The action performed; kanban has no single file or path subject.
1555
+ subjectKey: "action",
1540
1556
  mutating: true,
1541
1557
  capabilities: ["fs.write"],
1542
1558
  icon: "task",
@@ -1978,6 +1994,7 @@ var kanbanTool = {
1978
1994
  actor: input.author,
1979
1995
  comment: input.transitionComment,
1980
1996
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1997
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1981
1998
  ...input.attachmentUrl !== void 0 ? {
1982
1999
  attachment: {
1983
2000
  url: input.attachmentUrl,
@@ -2370,8 +2387,52 @@ var kanbanTool = {
2370
2387
  } catch (err) {
2371
2388
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2372
2389
  }
2390
+ },
2391
+ serialize(output, input) {
2392
+ return serializeKanbanOutput(output, input);
2373
2393
  }
2374
2394
  };
2395
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2396
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2397
+ "get_board",
2398
+ "export_markdown",
2399
+ "export_task_graph"
2400
+ ]);
2401
+ function serializeKanbanOutput(output, input) {
2402
+ const action = input && typeof input === "object" ? input.action : void 0;
2403
+ const board = output.board;
2404
+ if (board) {
2405
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2406
+ let boardBytes = 0;
2407
+ if (!keepFull) {
2408
+ try {
2409
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2410
+ } catch {
2411
+ boardBytes = 0;
2412
+ }
2413
+ }
2414
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2415
+ const columns = {};
2416
+ for (const column of board.columns) {
2417
+ columns[column.title || column.id] = board.tasks.filter(
2418
+ (task) => task.columnId === column.id
2419
+ ).length;
2420
+ }
2421
+ const compact = {
2422
+ ...output,
2423
+ board: {
2424
+ id: board.id,
2425
+ title: board.title,
2426
+ columns,
2427
+ totalTasks: board.tasks.length,
2428
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2429
+ }
2430
+ };
2431
+ return JSON.stringify(compact, null, 2);
2432
+ }
2433
+ }
2434
+ return JSON.stringify(output, null, 2);
2435
+ }
2375
2436
 
2376
2437
  // src/todo.ts
2377
2438
  function normalizedTitle(value) {
@@ -2499,6 +2560,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2499
2560
  }
2500
2561
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2501
2562
  if (!task || task.status === "completed") continue;
2563
+ const stage = task.lifecycle?.currentStage;
2564
+ if (stage === "backlog" || stage === "todo") {
2565
+ const started = await execute({
2566
+ action: "start_task",
2567
+ boardId: board.id,
2568
+ taskId: task.id,
2569
+ author: actor,
2570
+ agentId: actor,
2571
+ transitionComment: `Auto-started for completion: ${item.content}`
2572
+ });
2573
+ if (!started.ok) {
2574
+ continue;
2575
+ }
2576
+ }
2502
2577
  await execute({
2503
2578
  action: "mark_assignment",
2504
2579
  boardId: board.id,
@@ -2710,7 +2785,8 @@ var todoTool = {
2710
2785
  }
2711
2786
  for (const planId of completedPlanIds) {
2712
2787
  if (pendingPlanIds.has(planId)) continue;
2713
- const planPath = ctx.meta["plan.path"];
2788
+ const meta = ctx.meta;
2789
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
2714
2790
  if (typeof planPath !== "string" || !planPath) continue;
2715
2791
  try {
2716
2792
  const plan = await loadPlan2(planPath);
@@ -2723,7 +2799,8 @@ var todoTool = {
2723
2799
  }
2724
2800
  for (const taskId of completedTaskIds) {
2725
2801
  if (pendingTaskIds.has(taskId)) continue;
2726
- const taskPath = ctx.meta["task.path"];
2802
+ const meta = ctx.meta;
2803
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
2727
2804
  if (typeof taskPath !== "string" || !taskPath) continue;
2728
2805
  try {
2729
2806
  const file = await loadTasks3(taskPath);
@@ -3204,6 +3281,7 @@ var taskTool = {
3204
3281
  inProgress: 0
3205
3282
  };
3206
3283
  }
3284
+ ctx.meta["task.path.resolved"] = taskPath;
3207
3285
  if (todosToReplace) {
3208
3286
  await todoTool.execute({ todos: todosToReplace }, ctx, {
3209
3287
  signal: AbortSignal.timeout(3e4)
@@ -3228,6 +3306,7 @@ var taskTool = {
3228
3306
  formatted = formatPlan(updated);
3229
3307
  return updated;
3230
3308
  });
3309
+ ctx.meta["plan.path.resolved"] = planPath;
3231
3310
  } catch (err) {
3232
3311
  return {
3233
3312
  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: {
@@ -1425,6 +1437,7 @@ function sourceStatus(task) {
1425
1437
  function todoStatus(task) {
1426
1438
  const status = sourceStatus(task);
1427
1439
  if (status === "completed") return "completed";
1440
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
1428
1441
  if (status === "in_progress" || status === "review") return "in_progress";
1429
1442
  return "pending";
1430
1443
  }
@@ -1524,6 +1537,9 @@ var kanbanTool = {
1524
1537
  description: KANBAN_TOOL_DESCRIPTION,
1525
1538
  usageHint: KANBAN_TOOL_USAGE_HINT,
1526
1539
  permission: "confirm",
1540
+ // WS-046: gives permission decisions something to key on.
1541
+ // The action performed; kanban has no single file or path subject.
1542
+ subjectKey: "action",
1527
1543
  mutating: true,
1528
1544
  capabilities: ["fs.write"],
1529
1545
  icon: "task",
@@ -1965,6 +1981,7 @@ var kanbanTool = {
1965
1981
  actor: input.author,
1966
1982
  comment: input.transitionComment,
1967
1983
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1984
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1968
1985
  ...input.attachmentUrl !== void 0 ? {
1969
1986
  attachment: {
1970
1987
  url: input.attachmentUrl,
@@ -2357,8 +2374,52 @@ var kanbanTool = {
2357
2374
  } catch (err) {
2358
2375
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2359
2376
  }
2377
+ },
2378
+ serialize(output, input) {
2379
+ return serializeKanbanOutput(output, input);
2360
2380
  }
2361
2381
  };
2382
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2383
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2384
+ "get_board",
2385
+ "export_markdown",
2386
+ "export_task_graph"
2387
+ ]);
2388
+ function serializeKanbanOutput(output, input) {
2389
+ const action = input && typeof input === "object" ? input.action : void 0;
2390
+ const board = output.board;
2391
+ if (board) {
2392
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2393
+ let boardBytes = 0;
2394
+ if (!keepFull) {
2395
+ try {
2396
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2397
+ } catch {
2398
+ boardBytes = 0;
2399
+ }
2400
+ }
2401
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2402
+ const columns = {};
2403
+ for (const column of board.columns) {
2404
+ columns[column.title || column.id] = board.tasks.filter(
2405
+ (task) => task.columnId === column.id
2406
+ ).length;
2407
+ }
2408
+ const compact = {
2409
+ ...output,
2410
+ board: {
2411
+ id: board.id,
2412
+ title: board.title,
2413
+ columns,
2414
+ totalTasks: board.tasks.length,
2415
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2416
+ }
2417
+ };
2418
+ return JSON.stringify(compact, null, 2);
2419
+ }
2420
+ }
2421
+ return JSON.stringify(output, null, 2);
2422
+ }
2362
2423
 
2363
2424
  // src/todo.ts
2364
2425
  function normalizedTitle(value) {
@@ -2486,6 +2547,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2486
2547
  }
2487
2548
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2488
2549
  if (!task || task.status === "completed") continue;
2550
+ const stage = task.lifecycle?.currentStage;
2551
+ if (stage === "backlog" || stage === "todo") {
2552
+ const started = await execute({
2553
+ action: "start_task",
2554
+ boardId: board.id,
2555
+ taskId: task.id,
2556
+ author: actor,
2557
+ agentId: actor,
2558
+ transitionComment: `Auto-started for completion: ${item.content}`
2559
+ });
2560
+ if (!started.ok) {
2561
+ continue;
2562
+ }
2563
+ }
2489
2564
  await execute({
2490
2565
  action: "mark_assignment",
2491
2566
  boardId: board.id,
@@ -2697,7 +2772,8 @@ var todoTool = {
2697
2772
  }
2698
2773
  for (const planId of completedPlanIds) {
2699
2774
  if (pendingPlanIds.has(planId)) continue;
2700
- const planPath = ctx.meta["plan.path"];
2775
+ const meta = ctx.meta;
2776
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
2701
2777
  if (typeof planPath !== "string" || !planPath) continue;
2702
2778
  try {
2703
2779
  const plan = await loadPlan2(planPath);
@@ -2710,7 +2786,8 @@ var todoTool = {
2710
2786
  }
2711
2787
  for (const taskId of completedTaskIds) {
2712
2788
  if (pendingTaskIds.has(taskId)) continue;
2713
- const taskPath = ctx.meta["task.path"];
2789
+ const meta = ctx.meta;
2790
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
2714
2791
  if (typeof taskPath !== "string" || !taskPath) continue;
2715
2792
  try {
2716
2793
  const file = await loadTasks3(taskPath);