@wrongstack/tools 0.296.4 → 0.297.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 (46) hide show
  1. package/dist/audit.js +15 -0
  2. package/dist/audit.js.map +2 -2
  3. package/dist/bash.js +15 -0
  4. package/dist/bash.js.map +2 -2
  5. package/dist/builtin.js +42 -0
  6. package/dist/builtin.js.map +2 -2
  7. package/dist/codebase-index/index.js +27 -0
  8. package/dist/codebase-index/index.js.map +2 -2
  9. package/dist/codebase-index/project-server-client.d.ts.map +1 -1
  10. package/dist/codebase-index/project-server.js +16 -0
  11. package/dist/codebase-index/project-server.js.map +2 -2
  12. package/dist/exec.js +15 -0
  13. package/dist/exec.js.map +2 -2
  14. package/dist/format.js +15 -0
  15. package/dist/format.js.map +2 -2
  16. package/dist/index.js +42 -0
  17. package/dist/index.js.map +2 -2
  18. package/dist/install.js +15 -0
  19. package/dist/install.js.map +2 -2
  20. package/dist/languages/index.js +15 -0
  21. package/dist/languages/index.js.map +2 -2
  22. package/dist/lint.js +15 -0
  23. package/dist/lint.js.map +2 -2
  24. package/dist/next-steps.d.ts +23 -0
  25. package/dist/next-steps.d.ts.map +1 -1
  26. package/dist/next-steps.js +4 -0
  27. package/dist/next-steps.js.map +2 -2
  28. package/dist/outdated.js +15 -0
  29. package/dist/outdated.js.map +2 -2
  30. package/dist/pack.js +42 -0
  31. package/dist/pack.js.map +2 -2
  32. package/dist/process-registry.d.ts +9 -0
  33. package/dist/process-registry.d.ts.map +1 -1
  34. package/dist/process-registry.js +15 -0
  35. package/dist/process-registry.js.map +2 -2
  36. package/dist/ps-slash.js +15 -0
  37. package/dist/ps-slash.js.map +2 -2
  38. package/dist/read.js +27 -0
  39. package/dist/read.js.map +2 -2
  40. package/dist/test.js +15 -0
  41. package/dist/test.js.map +2 -2
  42. package/dist/tool-tier.js +42 -0
  43. package/dist/tool-tier.js.map +2 -2
  44. package/dist/typecheck.js +15 -0
  45. package/dist/typecheck.js.map +2 -2
  46. package/package.json +3 -3
@@ -46,6 +46,29 @@ export interface ParseNextStepsResult {
46
46
  /** The first item's text when it has auto="true"; otherwise empty. */
47
47
  autoTexts: string[];
48
48
  }
49
+ /**
50
+ * Whether an LLM response's stop reason means the message is the FINAL one of
51
+ * a turn.
52
+ *
53
+ * The agent loop calls the provider once per iteration, so a multi-step turn
54
+ * emits several assistant messages. Only the last one — the response that
55
+ * stopped without asking for another tool — is the model's actual answer;
56
+ * everything before it is mid-turn prose the model wrote on its way to a tool
57
+ * call. A `<nextsteps>` block in mid-turn text is noise: surfacing it would
58
+ * offer the user suggestions while the work is still in flight, and would let
59
+ * `/next` and auto-submit pick up a suggestion the model has already moved on
60
+ * from.
61
+ *
62
+ * Every surface that renders suggestions gates on this so the three of them
63
+ * cannot disagree about where a turn ends. Callers still strip the block from
64
+ * the message body regardless — the raw XML must never reach the user.
65
+ *
66
+ * `tool_call` is the spelling some providers put on the WebUI wire (typed
67
+ * loosely as `stopReason: string`); the canonical `StopReason` union uses
68
+ * `tool_use`. An absent stop reason counts as final so legacy paths that never
69
+ * carried one keep their suggestions.
70
+ */
71
+ export declare function isFinalTurnStopReason(stopReason: string | undefined): boolean;
49
72
  /**
50
73
  * Parse canonical "<nextsteps>" blocks from assistant output (or raw numbered lines).
51
74
  *
@@ -1 +1 @@
1
- {"version":3,"file":"next-steps.d.ts","sourceRoot":"","sources":["../src/next-steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oEAAoE;IACpE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAoBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,UAAQ,EACd,cAAc,UAAO,GACpB,oBAAoB,CAKtB;AAuKD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxD"}
1
+ {"version":3,"file":"next-steps.d.ts","sourceRoot":"","sources":["../src/next-steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oEAAoE;IACpE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE7E;AAoBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,UAAQ,EACd,cAAc,UAAO,GACpB,oBAAoB,CAKtB;AAuKD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxD"}
@@ -1,4 +1,7 @@
1
1
  // src/next-steps.ts
2
+ function isFinalTurnStopReason(stopReason) {
3
+ return stopReason !== "tool_use" && stopReason !== "tool_call";
4
+ }
2
5
  var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>\s*\n+/i;
3
6
  var ITEM_RE = /^(?:(\d+)[.)]\s*|[-*•]\s*)(.+?)(\s+auto="true")?$/;
4
7
  var MAX_STEPS = 6;
@@ -111,6 +114,7 @@ function stripNextStepsBlock(text) {
111
114
  return text.replace(/<next_?steps\b[^>]*>[\s\S]*?<\/next_?steps>/gi, "").replace(/<next_?steps\b[^>]*\/?>/gi, "").replace(/\n{3,}/g, "\n\n").trim();
112
115
  }
113
116
  export {
117
+ isFinalTurnStopReason,
114
118
  parseNextSteps,
115
119
  stripNextStepsBlock
116
120
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/next-steps.ts"],
4
- "sourcesContent": ["/**\n * Canonical `<nextsteps>` block parser \u2014 the single source of truth shared by\n * the TUI history renderer, the CLI REPL suggestion store, the CLI /suggest\n * subagent output, and the WebUI MessageBubble/NextStepsBar.\n *\n * Three code paths feed into the parser:\n * 1. TUI rendering \u2014 entry.tsx parses only \"<nextsteps>\" from assistant output\n * 2. REPL store \u2014 repl.ts parses only \"<nextsteps>\" from final agent output\n * 3. /suggest output \u2014 suggest.ts parses LLM-generated numbered lists (raw mode)\n *\n * Heading mode (`requireHeading = true`, the default):\n * Assistant-output paths accept only balanced <nextsteps>...</nextsteps> blocks.\n * Loose headings like \"Next steps:\" are intentionally ignored so `/next` only\n * activates for the canonical machine-readable format.\n *\n * Raw mode (`requireHeading = false`):\n * Parses numbered/bullet items from anywhere in text (subagent /suggest output).\n *\n * Supported assistant-output format:\n * <nextsteps> (canonical XML tag format)\n *\n * The parser also tolerates attributes on the opening tag so malformed model\n * output such as `<nextsteps auto=\"true\">` does not leak into the UI. Opening\n * tag attributes are ignored; `auto=\"true\"` has meaning only on the first item.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it is safe to import\n * from Vite-bundled WebUI as well as Node-based CLI/TUI. Mirrors the proven\n * `tool-summary` / `tool-icons` extraction pattern (PRs #236 / #237 / #238).\n */\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedNextStep {\n index: number;\n text: string;\n /** Whether the first item has auto=\"true\" for YOLO+auto autonomy mode. */\n auto?: boolean;\n}\n\nexport interface ParseNextStepsResult {\n /** Matched steps with their original index and stripped text. */\n steps: ParsedNextStep[];\n /** Flat string array \u2014 what gets stored in the suggestion store. */\n texts: string[];\n /**\n * Content with the entire canonical \"<nextsteps>\" block removed.\n * Used by entry.tsx to strip suggestions from the rendered message body.\n */\n stripped: string;\n /** The first item's text when it has auto=\"true\"; otherwise empty. */\n autoTexts: string[];\n}\n\n// \u2500\u2500 Patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Matches the canonical <nextsteps> tag before numbered items.\n *\n * Attributes are accepted defensively because models occasionally attach the\n * item-only `auto=\"true\"` marker to this tag. They never affect parser state.\n */\nconst NEXT_STEPS_TAG_RE = /<nextsteps\\b[^>]*>\\s*\\n+/i;\n\n/** Matches an item line: \"1. text\", \"1) text\", \"- text\", \"* text\". */\n/** Also captures optional auto=\"true\" attribute at the end. */\nconst ITEM_RE = /^(?:(\\d+)[.)]\\s*|[-*\u2022]\\s*)(.+?)(\\s+auto=\"true\")?$/;\n\nconst MAX_STEPS = 6;\n\n// \u2500\u2500 Core parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse canonical \"<nextsteps>\" blocks from assistant output (or raw numbered lines).\n *\n * @param content \u2014 raw assistant message text or subagent output\n * @param strict \u2014 retained for compatibility; assistant-output paths always require the canonical XML tag.\n * @param requireHeading \u2014 when true, a canonical XML tag must precede the item list.\n * when false, numbered/bullet items are parsed from anywhere in text\n * (used by /suggest subagent output which has no heading).\n */\nexport function parseNextSteps(\n content: string,\n strict = false,\n requireHeading = true,\n): ParseNextStepsResult {\n if (requireHeading) {\n return parseWithHeading(content, strict);\n }\n return parseRawNumbered(content);\n}\n\n/** Push a step into the array, omitting `auto` when false (cleaner shape, matches WebUI parity). */\nfunction pushStep(\n steps: ParsedNextStep[],\n index: number,\n text: string,\n hasAuto: boolean,\n): void {\n steps.push(hasAuto ? { index, text, auto: true } : { index, text });\n}\n\n/** Parse numbered/bullet items from raw text without a heading. */\nfunction parseRawNumbered(content: string): ParseNextStepsResult {\n const lines = content.split('\\n');\n const steps: ParsedNextStep[] = [];\n const seenNumbers = new Set<number>();\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const m = ITEM_RE.exec(line);\n if (!m) continue; // skip non-item lines in raw mode\n\n const numPart = m[1];\n const text = m[2]!.trim();\n let index: number;\n\n if (numPart !== undefined) {\n index = Number.parseInt(numPart, 10);\n } else {\n index = steps.length + 1; // bullet items get sequential indices\n }\n\n if (seenNumbers.has(index)) continue;\n seenNumbers.add(index);\n // Auto-submit is deliberately restricted to the first accepted item.\n // Later misplaced markers are stripped from display text but ignored.\n const hasAuto = !!m[3] && steps.length === 0 && index === 1;\n pushStep(steps, index, text, hasAuto);\n\n if (steps.length >= MAX_STEPS) break;\n }\n\n return {\n steps,\n texts: steps.map((s) => s.text),\n stripped: content,\n autoTexts: steps.filter((s) => s.auto).map((s) => s.text),\n };\n}\n\n/** Parse a heading + item block (the main assistant-message path). */\nfunction parseWithHeading(content: string, _strict: boolean): ParseNextStepsResult {\n const headingMatch = NEXT_STEPS_TAG_RE.exec(content);\n\n if (!headingMatch) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n const headingEnd = headingMatch.index + headingMatch[0]!.length;\n const afterHeading = content.slice(headingEnd);\n const lines = afterHeading.split('\\n');\n const steps: ParsedNextStep[] = [];\n const seenNumbers = new Set<number>();\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const m = ITEM_RE.exec(line);\n if (!m) break; // non-item line \u2014 block ends\n\n const numPart = m[1];\n const text = m[2]!.trim();\n let index: number;\n\n if (numPart !== undefined) {\n index = Number.parseInt(numPart, 10);\n } else {\n index = steps.length + 1;\n }\n\n if (seenNumbers.has(index)) continue;\n seenNumbers.add(index);\n // Only the first accepted item may opt into unattended auto-submit.\n const hasAuto = !!m[3] && steps.length === 0 && index === 1;\n pushStep(steps, index, text, hasAuto);\n\n if (steps.length >= MAX_STEPS) break;\n }\n\n if (steps.length === 0) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n // Require a closing tag. Malformed XML is rejected so raw text remains\n // visible instead of being partially consumed as automation input.\n if (!afterHeading.includes('</nextsteps>')) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n const texts = steps.map((s) => s.text);\n const autoTexts = steps.filter((s) => s.auto).map((s) => s.text);\n\n // Strip the entire XML block from the content. `blockEnd` is the LENGTH of\n // that block, so `content.slice(blockStart + blockEnd)` is the rest of the content.\n const blockStart = headingMatch.index;\n const blockEnd = headingMatch[0]!.length + findBlockEnd(afterHeading, steps.length);\n const stripped =\n (content.slice(0, blockStart) + content.slice(blockStart + blockEnd))\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n return { steps, texts, stripped, autoTexts };\n}\n\n/**\n * Find the byte offset in `afterHeading` where the block ends.\n *\n * The block to strip is the items (one per line) plus the `</nextsteps>`\n * closing tag (and the trailing newline after it).\n *\n * Returns the byte offset of the first character AFTER the block. The\n * caller's `content.slice(0, blockStart) + content.slice(blockStart + offset)`\n * then produces the stripped content.\n *\n * Walks line-by-line. Stops at the first non-item line, the closing XML\n * tag, or the end of the input \u2014 whichever comes first.\n */\nfunction findBlockEnd(afterHeading: string, stepCount: number): number {\n // Fast path: if the block is the <nextsteps> XML form, find the closing\n // tag and return its end (consuming the tag + trailing newline).\n const closeIdx = afterHeading.indexOf('</nextsteps>');\n if (closeIdx !== -1) {\n let end = closeIdx + '</nextsteps>'.length;\n if (afterHeading[end] === '\\n') end += 1;\n return end;\n }\n\n // Defensive fallback for malformed input that reached this helper without a\n // closing tag. The caller rejects such input before using the stripped text.\n const lines = afterHeading.split('\\n');\n let consumed = 0;\n let found = 0;\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) break; // blank line ends the block\n\n const m = ITEM_RE.exec(line);\n if (!m) break; // non-item line ends the block\n\n consumed += rawLine.length + 1; // +1 for the \\n separator\n found++;\n if (found >= stepCount) {\n // Don't include the trailing newline of the last item \u2014 the slice\n // logic in the caller handles whitespace cleanup.\n consumed -= 1;\n break;\n }\n }\n\n return consumed;\n}\n\n/**\n * Strip <nextsteps>...</nextsteps> blocks from subagent output text.\n * Subagent results should not contain suggestion blocks \u2014 those belong to\n * the main assistant's output. This prevents raw XML tags from appearing\n * as literal text in the fleet panel.\n *\n * Also strips the legacy `<next_steps>` spelling that older persisted\n * subagent output may contain (WebUI parity).\n */\nexport function stripNextStepsBlock(text: string): string {\n return text\n .replace(/<next_?steps\\b[^>]*>[\\s\\S]*?<\\/next_?steps>/gi, '')\n .replace(/<next_?steps\\b[^>]*\\/?>/gi, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Canonical `<nextsteps>` block parser \u2014 the single source of truth shared by\n * the TUI history renderer, the CLI REPL suggestion store, the CLI /suggest\n * subagent output, and the WebUI MessageBubble/NextStepsBar.\n *\n * Three code paths feed into the parser:\n * 1. TUI rendering \u2014 entry.tsx parses only \"<nextsteps>\" from assistant output\n * 2. REPL store \u2014 repl.ts parses only \"<nextsteps>\" from final agent output\n * 3. /suggest output \u2014 suggest.ts parses LLM-generated numbered lists (raw mode)\n *\n * Heading mode (`requireHeading = true`, the default):\n * Assistant-output paths accept only balanced <nextsteps>...</nextsteps> blocks.\n * Loose headings like \"Next steps:\" are intentionally ignored so `/next` only\n * activates for the canonical machine-readable format.\n *\n * Raw mode (`requireHeading = false`):\n * Parses numbered/bullet items from anywhere in text (subagent /suggest output).\n *\n * Supported assistant-output format:\n * <nextsteps> (canonical XML tag format)\n *\n * The parser also tolerates attributes on the opening tag so malformed model\n * output such as `<nextsteps auto=\"true\">` does not leak into the UI. Opening\n * tag attributes are ignored; `auto=\"true\"` has meaning only on the first item.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it is safe to import\n * from Vite-bundled WebUI as well as Node-based CLI/TUI. Mirrors the proven\n * `tool-summary` / `tool-icons` extraction pattern (PRs #236 / #237 / #238).\n */\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedNextStep {\n index: number;\n text: string;\n /** Whether the first item has auto=\"true\" for YOLO+auto autonomy mode. */\n auto?: boolean;\n}\n\nexport interface ParseNextStepsResult {\n /** Matched steps with their original index and stripped text. */\n steps: ParsedNextStep[];\n /** Flat string array \u2014 what gets stored in the suggestion store. */\n texts: string[];\n /**\n * Content with the entire canonical \"<nextsteps>\" block removed.\n * Used by entry.tsx to strip suggestions from the rendered message body.\n */\n stripped: string;\n /** The first item's text when it has auto=\"true\"; otherwise empty. */\n autoTexts: string[];\n}\n\n// \u2500\u2500 Turn boundary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Whether an LLM response's stop reason means the message is the FINAL one of\n * a turn.\n *\n * The agent loop calls the provider once per iteration, so a multi-step turn\n * emits several assistant messages. Only the last one \u2014 the response that\n * stopped without asking for another tool \u2014 is the model's actual answer;\n * everything before it is mid-turn prose the model wrote on its way to a tool\n * call. A `<nextsteps>` block in mid-turn text is noise: surfacing it would\n * offer the user suggestions while the work is still in flight, and would let\n * `/next` and auto-submit pick up a suggestion the model has already moved on\n * from.\n *\n * Every surface that renders suggestions gates on this so the three of them\n * cannot disagree about where a turn ends. Callers still strip the block from\n * the message body regardless \u2014 the raw XML must never reach the user.\n *\n * `tool_call` is the spelling some providers put on the WebUI wire (typed\n * loosely as `stopReason: string`); the canonical `StopReason` union uses\n * `tool_use`. An absent stop reason counts as final so legacy paths that never\n * carried one keep their suggestions.\n */\nexport function isFinalTurnStopReason(stopReason: string | undefined): boolean {\n return stopReason !== 'tool_use' && stopReason !== 'tool_call';\n}\n\n// \u2500\u2500 Patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Matches the canonical <nextsteps> tag before numbered items.\n *\n * Attributes are accepted defensively because models occasionally attach the\n * item-only `auto=\"true\"` marker to this tag. They never affect parser state.\n */\nconst NEXT_STEPS_TAG_RE = /<nextsteps\\b[^>]*>\\s*\\n+/i;\n\n/** Matches an item line: \"1. text\", \"1) text\", \"- text\", \"* text\". */\n/** Also captures optional auto=\"true\" attribute at the end. */\nconst ITEM_RE = /^(?:(\\d+)[.)]\\s*|[-*\u2022]\\s*)(.+?)(\\s+auto=\"true\")?$/;\n\nconst MAX_STEPS = 6;\n\n// \u2500\u2500 Core parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse canonical \"<nextsteps>\" blocks from assistant output (or raw numbered lines).\n *\n * @param content \u2014 raw assistant message text or subagent output\n * @param strict \u2014 retained for compatibility; assistant-output paths always require the canonical XML tag.\n * @param requireHeading \u2014 when true, a canonical XML tag must precede the item list.\n * when false, numbered/bullet items are parsed from anywhere in text\n * (used by /suggest subagent output which has no heading).\n */\nexport function parseNextSteps(\n content: string,\n strict = false,\n requireHeading = true,\n): ParseNextStepsResult {\n if (requireHeading) {\n return parseWithHeading(content, strict);\n }\n return parseRawNumbered(content);\n}\n\n/** Push a step into the array, omitting `auto` when false (cleaner shape, matches WebUI parity). */\nfunction pushStep(\n steps: ParsedNextStep[],\n index: number,\n text: string,\n hasAuto: boolean,\n): void {\n steps.push(hasAuto ? { index, text, auto: true } : { index, text });\n}\n\n/** Parse numbered/bullet items from raw text without a heading. */\nfunction parseRawNumbered(content: string): ParseNextStepsResult {\n const lines = content.split('\\n');\n const steps: ParsedNextStep[] = [];\n const seenNumbers = new Set<number>();\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const m = ITEM_RE.exec(line);\n if (!m) continue; // skip non-item lines in raw mode\n\n const numPart = m[1];\n const text = m[2]!.trim();\n let index: number;\n\n if (numPart !== undefined) {\n index = Number.parseInt(numPart, 10);\n } else {\n index = steps.length + 1; // bullet items get sequential indices\n }\n\n if (seenNumbers.has(index)) continue;\n seenNumbers.add(index);\n // Auto-submit is deliberately restricted to the first accepted item.\n // Later misplaced markers are stripped from display text but ignored.\n const hasAuto = !!m[3] && steps.length === 0 && index === 1;\n pushStep(steps, index, text, hasAuto);\n\n if (steps.length >= MAX_STEPS) break;\n }\n\n return {\n steps,\n texts: steps.map((s) => s.text),\n stripped: content,\n autoTexts: steps.filter((s) => s.auto).map((s) => s.text),\n };\n}\n\n/** Parse a heading + item block (the main assistant-message path). */\nfunction parseWithHeading(content: string, _strict: boolean): ParseNextStepsResult {\n const headingMatch = NEXT_STEPS_TAG_RE.exec(content);\n\n if (!headingMatch) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n const headingEnd = headingMatch.index + headingMatch[0]!.length;\n const afterHeading = content.slice(headingEnd);\n const lines = afterHeading.split('\\n');\n const steps: ParsedNextStep[] = [];\n const seenNumbers = new Set<number>();\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const m = ITEM_RE.exec(line);\n if (!m) break; // non-item line \u2014 block ends\n\n const numPart = m[1];\n const text = m[2]!.trim();\n let index: number;\n\n if (numPart !== undefined) {\n index = Number.parseInt(numPart, 10);\n } else {\n index = steps.length + 1;\n }\n\n if (seenNumbers.has(index)) continue;\n seenNumbers.add(index);\n // Only the first accepted item may opt into unattended auto-submit.\n const hasAuto = !!m[3] && steps.length === 0 && index === 1;\n pushStep(steps, index, text, hasAuto);\n\n if (steps.length >= MAX_STEPS) break;\n }\n\n if (steps.length === 0) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n // Require a closing tag. Malformed XML is rejected so raw text remains\n // visible instead of being partially consumed as automation input.\n if (!afterHeading.includes('</nextsteps>')) {\n return { steps: [], texts: [], stripped: content, autoTexts: [] };\n }\n\n const texts = steps.map((s) => s.text);\n const autoTexts = steps.filter((s) => s.auto).map((s) => s.text);\n\n // Strip the entire XML block from the content. `blockEnd` is the LENGTH of\n // that block, so `content.slice(blockStart + blockEnd)` is the rest of the content.\n const blockStart = headingMatch.index;\n const blockEnd = headingMatch[0]!.length + findBlockEnd(afterHeading, steps.length);\n const stripped =\n (content.slice(0, blockStart) + content.slice(blockStart + blockEnd))\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n return { steps, texts, stripped, autoTexts };\n}\n\n/**\n * Find the byte offset in `afterHeading` where the block ends.\n *\n * The block to strip is the items (one per line) plus the `</nextsteps>`\n * closing tag (and the trailing newline after it).\n *\n * Returns the byte offset of the first character AFTER the block. The\n * caller's `content.slice(0, blockStart) + content.slice(blockStart + offset)`\n * then produces the stripped content.\n *\n * Walks line-by-line. Stops at the first non-item line, the closing XML\n * tag, or the end of the input \u2014 whichever comes first.\n */\nfunction findBlockEnd(afterHeading: string, stepCount: number): number {\n // Fast path: if the block is the <nextsteps> XML form, find the closing\n // tag and return its end (consuming the tag + trailing newline).\n const closeIdx = afterHeading.indexOf('</nextsteps>');\n if (closeIdx !== -1) {\n let end = closeIdx + '</nextsteps>'.length;\n if (afterHeading[end] === '\\n') end += 1;\n return end;\n }\n\n // Defensive fallback for malformed input that reached this helper without a\n // closing tag. The caller rejects such input before using the stripped text.\n const lines = afterHeading.split('\\n');\n let consumed = 0;\n let found = 0;\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line) break; // blank line ends the block\n\n const m = ITEM_RE.exec(line);\n if (!m) break; // non-item line ends the block\n\n consumed += rawLine.length + 1; // +1 for the \\n separator\n found++;\n if (found >= stepCount) {\n // Don't include the trailing newline of the last item \u2014 the slice\n // logic in the caller handles whitespace cleanup.\n consumed -= 1;\n break;\n }\n }\n\n return consumed;\n}\n\n/**\n * Strip <nextsteps>...</nextsteps> blocks from subagent output text.\n * Subagent results should not contain suggestion blocks \u2014 those belong to\n * the main assistant's output. This prevents raw XML tags from appearing\n * as literal text in the fleet panel.\n *\n * Also strips the legacy `<next_steps>` spelling that older persisted\n * subagent output may contain (WebUI parity).\n */\nexport function stripNextStepsBlock(text: string): string {\n return text\n .replace(/<next_?steps\\b[^>]*>[\\s\\S]*?<\\/next_?steps>/gi, '')\n .replace(/<next_?steps\\b[^>]*\\/?>/gi, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/outdated.js CHANGED
@@ -2840,6 +2840,7 @@ var init_process_registry = __esm({
2840
2840
  }
2841
2841
  /** Get all tracked processes. */
2842
2842
  list() {
2843
+ this._pruneAllStale();
2843
2844
  return Array.from(this.processes.values());
2844
2845
  }
2845
2846
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -2870,6 +2871,7 @@ var init_process_registry = __esm({
2870
2871
  * Combined stats for observability — used by /ps and the TUI status bar.
2871
2872
  */
2872
2873
  stats() {
2874
+ this._pruneAllStale();
2873
2875
  return {
2874
2876
  activeCount: this.activeCount,
2875
2877
  backgroundCount: this.activeBackgroundCount,
@@ -3109,6 +3111,19 @@ var init_process_registry = __esm({
3109
3111
  this.processes.delete(pid);
3110
3112
  }
3111
3113
  }
3114
+ /**
3115
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
3116
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
3117
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
3118
+ * open) would otherwise linger in the registry until someone looks up its
3119
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
3120
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
3121
+ */
3122
+ _pruneAllStale() {
3123
+ for (const [pid, entry] of this.processes) {
3124
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
3125
+ }
3126
+ }
3112
3127
  };
3113
3128
  }
3114
3129
  });