@kolisachint/hoocode-agent 0.4.124 → 0.4.126

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 (57) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +9 -2
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/context-gc.d.ts +37 -0
  6. package/dist/core/context-gc.d.ts.map +1 -0
  7. package/dist/core/context-gc.js +109 -0
  8. package/dist/core/context-gc.js.map +1 -0
  9. package/dist/core/sdk.d.ts.map +1 -1
  10. package/dist/core/sdk.js +3 -3
  11. package/dist/core/sdk.js.map +1 -1
  12. package/dist/core/settings-defaults.d.ts +9 -1
  13. package/dist/core/settings-defaults.d.ts.map +1 -1
  14. package/dist/core/settings-defaults.js +9 -1
  15. package/dist/core/settings-defaults.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +6 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js +14 -0
  19. package/dist/core/settings-manager.js.map +1 -1
  20. package/dist/core/settings-types.d.ts +10 -0
  21. package/dist/core/settings-types.d.ts.map +1 -1
  22. package/dist/core/settings-types.js.map +1 -1
  23. package/dist/core/tools/bash.d.ts +4 -0
  24. package/dist/core/tools/bash.d.ts.map +1 -1
  25. package/dist/core/tools/bash.js +5 -3
  26. package/dist/core/tools/bash.js.map +1 -1
  27. package/dist/core/tools/plugins.d.ts.map +1 -1
  28. package/dist/core/tools/plugins.js +8 -0
  29. package/dist/core/tools/plugins.js.map +1 -1
  30. package/dist/core/tools/read.d.ts +4 -0
  31. package/dist/core/tools/read.d.ts.map +1 -1
  32. package/dist/core/tools/read.js +6 -4
  33. package/dist/core/tools/read.js.map +1 -1
  34. package/dist/core/tools/truncate.d.ts +9 -3
  35. package/dist/core/tools/truncate.d.ts.map +1 -1
  36. package/dist/core/tools/truncate.js +10 -4
  37. package/dist/core/tools/truncate.js.map +1 -1
  38. package/dist/extensions/core/hoo-core.d.ts +1 -0
  39. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  40. package/dist/extensions/core/hoo-core.js +3 -0
  41. package/dist/extensions/core/hoo-core.js.map +1 -1
  42. package/dist/extensions/core/prompt-reactive/nudges.d.ts +40 -0
  43. package/dist/extensions/core/prompt-reactive/nudges.d.ts.map +1 -0
  44. package/dist/extensions/core/prompt-reactive/nudges.js +163 -0
  45. package/dist/extensions/core/prompt-reactive/nudges.js.map +1 -0
  46. package/dist/extensions/core/prompt-reactive/policy.d.ts +69 -0
  47. package/dist/extensions/core/prompt-reactive/policy.d.ts.map +1 -0
  48. package/dist/extensions/core/prompt-reactive/policy.js +110 -0
  49. package/dist/extensions/core/prompt-reactive/policy.js.map +1 -0
  50. package/dist/main.d.ts.map +1 -1
  51. package/dist/main.js +3 -0
  52. package/dist/main.js.map +1 -1
  53. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  54. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  55. package/examples/extensions/sandbox/package.json +1 -1
  56. package/examples/extensions/with-deps/package.json +1 -1
  57. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../../src/core/tools/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../../src/core/tools/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 800 lines)\n * - Byte limit (default: 16KB)\n *\n * These caps bound how much a single tool result can inject into the transcript.\n * Because the whole transcript is re-sent every turn, one oversized result is\n * paid again on every subsequent turn until compaction, so the caps are kept\n * tight; read/bash page past them on demand (read via offset/limit, bash via the\n * temp-file spill), and both are overridable through the `toolOutput` setting.\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 800;\nexport const DEFAULT_MAX_BYTES = 16 * 1024; // 16KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
@@ -11,6 +11,7 @@
11
11
  * - thinking-escalation.ts — raise thinking after tool errors, then restore
12
12
  * - loop.ts — /loop cron scheduling + autonomous continuation
13
13
  * - marketplace.ts — /plugin marketplace + install/remove
14
+ * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)
14
15
  * - config.ts — hoo-config.json types, I/O, and merge rules
15
16
  *
16
17
  * `bin/hoocode.js` loads this module's default export as the built-in
@@ -1 +1 @@
1
- {"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAWnE,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAUvC;;;;AAGD,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
1
+ {"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAYnE,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAWvC;;;;AAGD,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupPromptReactiveNudges(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
@@ -11,6 +11,7 @@
11
11
  * - thinking-escalation.ts — raise thinking after tool errors, then restore
12
12
  * - loop.ts — /loop cron scheduling + autonomous continuation
13
13
  * - marketplace.ts — /plugin marketplace + install/remove
14
+ * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)
14
15
  * - config.ts — hoo-config.json types, I/O, and merge rules
15
16
  *
16
17
  * `bin/hoocode.js` loads this module's default export as the built-in
@@ -23,6 +24,7 @@ import { setupMarketplace } from "./marketplace.js";
23
24
  import { setupMcpLoader } from "./mcp-loader.js";
24
25
  import { setupMode } from "./modes.js";
25
26
  import { setupPermissionGate } from "./permission-gate.js";
27
+ import { setupPromptReactiveNudges } from "./prompt-reactive/nudges.js";
26
28
  import { setupScaffold } from "./scaffold.js";
27
29
  import { setupThinkingEscalation } from "./thinking-escalation.js";
28
30
  function hooCore(pi) {
@@ -35,6 +37,7 @@ function hooCore(pi) {
35
37
  setupThinkingEscalation(pi);
36
38
  setupLoop(pi);
37
39
  setupMarketplace(pi);
40
+ setupPromptReactiveNudges(pi);
38
41
  }
39
42
  hooCore.displayName = "hoo-core";
40
43
  export default hooCore;
@@ -1 +1 @@
1
- {"version":3,"file":"hoo-core.js","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,SAAS,OAAO,CAAC,EAAgB,EAAQ;IACxC,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,uBAAuB,CAAC,EAAE,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,gBAAgB,CAAC,EAAE,CAAC,CAAC;AAAA,CACrB;AAED,OAAO,CAAC,WAAW,GAAG,UAAU,CAAC;AACjC,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
1
+ {"version":3,"file":"hoo-core.js","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,SAAS,OAAO,CAAC,EAAgB,EAAQ;IACxC,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,uBAAuB,CAAC,EAAE,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,gBAAgB,CAAC,EAAE,CAAC,CAAC;IACrB,yBAAyB,CAAC,EAAE,CAAC,CAAC;AAAA,CAC9B;AAED,OAAO,CAAC,WAAW,GAAG,UAAU,CAAC;AACjC,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupPromptReactiveNudges(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Runtime "plugin reuse nudge" — the reactive half of the plugin-productivity
3
+ * guidance.
4
+ *
5
+ * The static `promptGuidelines` on the plugin tools are injected into the system
6
+ * prompt once at session build and never re-surface when the work actually shows
7
+ * a reusable-facing cue. This extension closes that gap: it watches tool output
8
+ * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it
9
+ * attaches a matching, plugin-facing note to the *next* turn's context — an
10
+ * ephemeral message merged into the outgoing request via the `context` hook, so
11
+ * nothing is written into persisted history.
12
+ *
13
+ * Policy (see ./policy.ts for the cue table — the single source of truth):
14
+ * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`
15
+ * (tool args, e.g. content being written) and `tool_execution_end` (tool
16
+ * output, e.g. content being read).
17
+ * - First-hit arming: a matching cue arms its nudge immediately.
18
+ * - One note per turn, and at most once per category per session — so a cue
19
+ * that keeps appearing never turns into a nag.
20
+ * - The whole thing is gated on the autonomous-plugin-system flag
21
+ * (`enablePluginTools`, default off) and never blocks normal flow.
22
+ *
23
+ * Wired once from hoo-core (the single default composition root); the guard below
24
+ * makes double-registration a no-op for downstreams that compose extensions
25
+ * differently, and the static import keeps it bundled in the compiled binary.
26
+ */
27
+ import type { ExtensionAPI } from "../../../core/extensions/types.js";
28
+ export interface PromptReactiveNudgesOptions {
29
+ /**
30
+ * Enablement gate. Defaults to the shared autonomous-plugin-system flag so the
31
+ * reactive nudge and the plugin tool surface flip together. Injectable for tests.
32
+ */
33
+ isEnabled?: (cwd: string) => boolean;
34
+ }
35
+ /**
36
+ * Install the runtime reuse-nudge extension. Idempotent — a second call on the
37
+ * same `pi` is a no-op, so composing default extensions twice is harmless.
38
+ */
39
+ export declare function setupPromptReactiveNudges(pi: ExtensionAPI, options?: PromptReactiveNudgesOptions): void;
40
+ //# sourceMappingURL=nudges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nudges.d.ts","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/nudges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAGX,YAAY,EAMZ,MAAM,mCAAmC,CAAC;AAe3C,MAAM,WAAW,2BAA2B;IAC3C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AA+DD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,GAAE,2BAAgC,GAAG,IAAI,CAoE3G","sourcesContent":["/**\n * Runtime \"plugin reuse nudge\" — the reactive half of the plugin-productivity\n * guidance.\n *\n * The static `promptGuidelines` on the plugin tools are injected into the system\n * prompt once at session build and never re-surface when the work actually shows\n * a reusable-facing cue. This extension closes that gap: it watches tool output\n * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it\n * attaches a matching, plugin-facing note to the *next* turn's context — an\n * ephemeral message merged into the outgoing request via the `context` hook, so\n * nothing is written into persisted history.\n *\n * Policy (see ./policy.ts for the cue table — the single source of truth):\n * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`\n * (tool args, e.g. content being written) and `tool_execution_end` (tool\n * output, e.g. content being read).\n * - First-hit arming: a matching cue arms its nudge immediately.\n * - One note per turn, and at most once per category per session — so a cue\n * that keeps appearing never turns into a nag.\n * - The whole thing is gated on the autonomous-plugin-system flag\n * (`enablePluginTools`, default off) and never blocks normal flow.\n *\n * Wired once from hoo-core (the single default composition root); the guard below\n * makes double-registration a no-op for downstreams that compose extensions\n * differently, and the static import keeps it bundled in the compiled binary.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type {\n\tBeforeAgentStartEvent,\n\tContextEvent,\n\tExtensionAPI,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolExecutionEndEvent,\n\tToolExecutionStartEvent,\n\tTurnStartEvent,\n} from \"../../../core/extensions/types.js\";\nimport {\n\tarmReuseNudge,\n\tclearArmedReuseNudges,\n\tisAutonomousPluginSystemEnabled,\n\tmatchReuseNudges,\n\ttype ReuseNudge,\n} from \"./policy.js\";\n\n/** Guards against double-registration when default extensions load more than once. */\nconst REGISTERED = Symbol.for(\"hoocode.promptReactiveNudges.registered\");\n\n/** Cap on how much tool text we scan per event — cues are short, files can be huge. */\nconst SCAN_CAP = 20_000;\n\nexport interface PromptReactiveNudgesOptions {\n\t/**\n\t * Enablement gate. Defaults to the shared autonomous-plugin-system flag so the\n\t * reactive nudge and the plugin tool surface flip together. Injectable for tests.\n\t */\n\tisEnabled?: (cwd: string) => boolean;\n}\n\n/**\n * Pull scannable text out of an arbitrary tool result or args object. Prefers\n * `content[].text` blocks (the tool-result convention) and falls back to a\n * bounded JSON stringify so cues in nested fields are still caught.\n */\nfunction extractText(value: unknown): string {\n\tif (value == null) return \"\";\n\tif (typeof value === \"string\") return value.slice(0, SCAN_CAP);\n\tif (typeof value === \"object\") {\n\t\tconst obj = value as { content?: unknown };\n\t\tif (Array.isArray(obj.content)) {\n\t\t\tconst text = obj.content\n\t\t\t\t.map((b) =>\n\t\t\t\t\tb && typeof b === \"object\" && typeof (b as { text?: unknown }).text === \"string\"\n\t\t\t\t\t\t? (b as { text: string }).text\n\t\t\t\t\t\t: \"\",\n\t\t\t\t)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) return text.slice(0, SCAN_CAP);\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.stringify(value).slice(0, SCAN_CAP);\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\treturn String(value).slice(0, SCAN_CAP);\n}\n\n/**\n * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text\n * block on the final user turn (valid alongside tool-result blocks) so the model\n * sees it right before it responds; falls back to a fresh user message when the\n * last message isn't a user turn. The input array is never mutated.\n */\nfunction injectNote(messages: AgentMessage[], note: string): AgentMessage[] {\n\tconst block: TextContent = { type: \"text\", text: note };\n\tconst out = messages.slice();\n\tconst last = out[out.length - 1] as { role?: string; content?: unknown } | undefined;\n\tif (last && last.role === \"user\") {\n\t\tconst content = last.content;\n\t\tconst merged =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: content } as TextContent, block]\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? [...content, block]\n\t\t\t\t\t: null;\n\t\tif (merged) {\n\t\t\tout[out.length - 1] = { ...(last as object), content: merged } as AgentMessage;\n\t\t\treturn out;\n\t\t}\n\t}\n\tout.push({ role: \"user\", content: [block], timestamp: Date.now() } as AgentMessage);\n\treturn out;\n}\n\n/** Wrap a nudge snippet so it reads as a system aside rather than user text. */\nfunction formatNote(nudge: ReuseNudge): string {\n\treturn `[reuse-nudge] ${nudge.snippet}`;\n}\n\n/**\n * Install the runtime reuse-nudge extension. Idempotent — a second call on the\n * same `pi` is a no-op, so composing default extensions twice is harmless.\n */\nexport function setupPromptReactiveNudges(pi: ExtensionAPI, options: PromptReactiveNudgesOptions = {}): void {\n\tconst guarded = pi as unknown as Record<symbol, boolean>;\n\tif (guarded[REGISTERED]) return;\n\tguarded[REGISTERED] = true;\n\n\tconst isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;\n\n\t// Session-scoped state. Categories fire once per session; the pending queue\n\t// holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.\n\tconst deliveredCategories = new Set<string>();\n\tconst pending: ReuseNudge[] = [];\n\tlet injectedThisTurn = false;\n\t// Cached enablement, recomputed lazily and reset on session start.\n\tlet enabledCache: boolean | undefined;\n\n\tconst enabled = (cwd: string): boolean => {\n\t\tif (enabledCache === undefined) enabledCache = isEnabled(cwd);\n\t\treturn enabledCache;\n\t};\n\n\tconst enqueue = (text: string, cwd: string): void => {\n\t\tif (!enabled(cwd)) return;\n\t\tfor (const nudge of matchReuseNudges(text)) {\n\t\t\tif (deliveredCategories.has(nudge.category)) continue;\n\t\t\tif (pending.some((p) => p.category === nudge.category)) continue;\n\t\t\tpending.push(nudge);\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent) => {\n\t\tdeliveredCategories.clear();\n\t\tpending.length = 0;\n\t\tinjectedThisTurn = false;\n\t\tenabledCache = undefined;\n\t\tclearArmedReuseNudges();\n\t});\n\n\tpi.on(\"turn_start\", (_event: TurnStartEvent) => {\n\t\tinjectedThisTurn = false;\n\t});\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(event.prompt, ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_start\", (event: ToolExecutionStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(extractText(event.args), ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_end\", (event: ToolExecutionEndEvent, ctx: ExtensionContext) => {\n\t\tif (event.isError) return;\n\t\tenqueue(extractText(event.result), ctx.cwd);\n\t});\n\n\t// The injection point: fires before each provider request. transformContext\n\t// output is request-scoped (never written back to agent state), so the note\n\t// is ephemeral by construction.\n\tpi.on(\"context\", (event: ContextEvent, ctx: ExtensionContext) => {\n\t\tif (!enabled(ctx.cwd) || injectedThisTurn) return undefined;\n\t\t// Drop any that raced to \"delivered\" via another path, then take the oldest.\n\t\twhile (pending.length > 0 && deliveredCategories.has(pending[0]!.category)) pending.shift();\n\t\tconst nudge = pending.shift();\n\t\tif (!nudge) return undefined;\n\t\tdeliveredCategories.add(nudge.category);\n\t\tarmReuseNudge(nudge);\n\t\tinjectedThisTurn = true;\n\t\treturn { messages: injectNote(event.messages, formatNote(nudge)) };\n\t});\n}\n"]}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Runtime "plugin reuse nudge" — the reactive half of the plugin-productivity
3
+ * guidance.
4
+ *
5
+ * The static `promptGuidelines` on the plugin tools are injected into the system
6
+ * prompt once at session build and never re-surface when the work actually shows
7
+ * a reusable-facing cue. This extension closes that gap: it watches tool output
8
+ * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it
9
+ * attaches a matching, plugin-facing note to the *next* turn's context — an
10
+ * ephemeral message merged into the outgoing request via the `context` hook, so
11
+ * nothing is written into persisted history.
12
+ *
13
+ * Policy (see ./policy.ts for the cue table — the single source of truth):
14
+ * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`
15
+ * (tool args, e.g. content being written) and `tool_execution_end` (tool
16
+ * output, e.g. content being read).
17
+ * - First-hit arming: a matching cue arms its nudge immediately.
18
+ * - One note per turn, and at most once per category per session — so a cue
19
+ * that keeps appearing never turns into a nag.
20
+ * - The whole thing is gated on the autonomous-plugin-system flag
21
+ * (`enablePluginTools`, default off) and never blocks normal flow.
22
+ *
23
+ * Wired once from hoo-core (the single default composition root); the guard below
24
+ * makes double-registration a no-op for downstreams that compose extensions
25
+ * differently, and the static import keeps it bundled in the compiled binary.
26
+ */
27
+ import { armReuseNudge, clearArmedReuseNudges, isAutonomousPluginSystemEnabled, matchReuseNudges, } from "./policy.js";
28
+ /** Guards against double-registration when default extensions load more than once. */
29
+ const REGISTERED = Symbol.for("hoocode.promptReactiveNudges.registered");
30
+ /** Cap on how much tool text we scan per event — cues are short, files can be huge. */
31
+ const SCAN_CAP = 20_000;
32
+ /**
33
+ * Pull scannable text out of an arbitrary tool result or args object. Prefers
34
+ * `content[].text` blocks (the tool-result convention) and falls back to a
35
+ * bounded JSON stringify so cues in nested fields are still caught.
36
+ */
37
+ function extractText(value) {
38
+ if (value == null)
39
+ return "";
40
+ if (typeof value === "string")
41
+ return value.slice(0, SCAN_CAP);
42
+ if (typeof value === "object") {
43
+ const obj = value;
44
+ if (Array.isArray(obj.content)) {
45
+ const text = obj.content
46
+ .map((b) => b && typeof b === "object" && typeof b.text === "string"
47
+ ? b.text
48
+ : "")
49
+ .join("\n");
50
+ if (text)
51
+ return text.slice(0, SCAN_CAP);
52
+ }
53
+ try {
54
+ return JSON.stringify(value).slice(0, SCAN_CAP);
55
+ }
56
+ catch {
57
+ return "";
58
+ }
59
+ }
60
+ return String(value).slice(0, SCAN_CAP);
61
+ }
62
+ /**
63
+ * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text
64
+ * block on the final user turn (valid alongside tool-result blocks) so the model
65
+ * sees it right before it responds; falls back to a fresh user message when the
66
+ * last message isn't a user turn. The input array is never mutated.
67
+ */
68
+ function injectNote(messages, note) {
69
+ const block = { type: "text", text: note };
70
+ const out = messages.slice();
71
+ const last = out[out.length - 1];
72
+ if (last && last.role === "user") {
73
+ const content = last.content;
74
+ const merged = typeof content === "string"
75
+ ? [{ type: "text", text: content }, block]
76
+ : Array.isArray(content)
77
+ ? [...content, block]
78
+ : null;
79
+ if (merged) {
80
+ out[out.length - 1] = { ...last, content: merged };
81
+ return out;
82
+ }
83
+ }
84
+ out.push({ role: "user", content: [block], timestamp: Date.now() });
85
+ return out;
86
+ }
87
+ /** Wrap a nudge snippet so it reads as a system aside rather than user text. */
88
+ function formatNote(nudge) {
89
+ return `[reuse-nudge] ${nudge.snippet}`;
90
+ }
91
+ /**
92
+ * Install the runtime reuse-nudge extension. Idempotent — a second call on the
93
+ * same `pi` is a no-op, so composing default extensions twice is harmless.
94
+ */
95
+ export function setupPromptReactiveNudges(pi, options = {}) {
96
+ const guarded = pi;
97
+ if (guarded[REGISTERED])
98
+ return;
99
+ guarded[REGISTERED] = true;
100
+ const isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;
101
+ // Session-scoped state. Categories fire once per session; the pending queue
102
+ // holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.
103
+ const deliveredCategories = new Set();
104
+ const pending = [];
105
+ let injectedThisTurn = false;
106
+ // Cached enablement, recomputed lazily and reset on session start.
107
+ let enabledCache;
108
+ const enabled = (cwd) => {
109
+ if (enabledCache === undefined)
110
+ enabledCache = isEnabled(cwd);
111
+ return enabledCache;
112
+ };
113
+ const enqueue = (text, cwd) => {
114
+ if (!enabled(cwd))
115
+ return;
116
+ for (const nudge of matchReuseNudges(text)) {
117
+ if (deliveredCategories.has(nudge.category))
118
+ continue;
119
+ if (pending.some((p) => p.category === nudge.category))
120
+ continue;
121
+ pending.push(nudge);
122
+ }
123
+ };
124
+ pi.on("session_start", (_event) => {
125
+ deliveredCategories.clear();
126
+ pending.length = 0;
127
+ injectedThisTurn = false;
128
+ enabledCache = undefined;
129
+ clearArmedReuseNudges();
130
+ });
131
+ pi.on("turn_start", (_event) => {
132
+ injectedThisTurn = false;
133
+ });
134
+ pi.on("before_agent_start", (event, ctx) => {
135
+ enqueue(event.prompt, ctx.cwd);
136
+ });
137
+ pi.on("tool_execution_start", (event, ctx) => {
138
+ enqueue(extractText(event.args), ctx.cwd);
139
+ });
140
+ pi.on("tool_execution_end", (event, ctx) => {
141
+ if (event.isError)
142
+ return;
143
+ enqueue(extractText(event.result), ctx.cwd);
144
+ });
145
+ // The injection point: fires before each provider request. transformContext
146
+ // output is request-scoped (never written back to agent state), so the note
147
+ // is ephemeral by construction.
148
+ pi.on("context", (event, ctx) => {
149
+ if (!enabled(ctx.cwd) || injectedThisTurn)
150
+ return undefined;
151
+ // Drop any that raced to "delivered" via another path, then take the oldest.
152
+ while (pending.length > 0 && deliveredCategories.has(pending[0].category))
153
+ pending.shift();
154
+ const nudge = pending.shift();
155
+ if (!nudge)
156
+ return undefined;
157
+ deliveredCategories.add(nudge.category);
158
+ armReuseNudge(nudge);
159
+ injectedThisTurn = true;
160
+ return { messages: injectNote(event.messages, formatNote(nudge)) };
161
+ });
162
+ }
163
+ //# sourceMappingURL=nudges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nudges.js","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/nudges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAcH,OAAO,EACN,aAAa,EACb,qBAAqB,EACrB,+BAA+B,EAC/B,gBAAgB,GAEhB,MAAM,aAAa,CAAC;AAErB,sFAAsF;AACtF,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;AAEzE,yFAAuF;AACvF,MAAM,QAAQ,GAAG,MAAM,CAAC;AAUxB;;;;GAIG;AACH,SAAS,WAAW,CAAC,KAAc,EAAU;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,KAA8B,CAAC;QAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO;iBACtB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACV,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAQ,CAAwB,CAAC,IAAI,KAAK,QAAQ;gBAC/E,CAAC,CAAE,CAAsB,CAAC,IAAI;gBAC9B,CAAC,CAAC,EAAE,CACL;iBACA,IAAI,CAAC,IAAI,CAAC,CAAC;YACb,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC;YACJ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,CACxC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,QAAwB,EAAE,IAAY,EAAkB;IAC3E,MAAM,KAAK,GAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAqD,CAAC;IACrF,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,MAAM,MAAM,GACX,OAAO,OAAO,KAAK,QAAQ;YAC1B,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAiB,EAAE,KAAK,CAAC;YACzD,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBACvB,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC;gBACrB,CAAC,CAAC,IAAI,CAAC;QACV,IAAI,MAAM,EAAE,CAAC;YACZ,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAI,IAAe,EAAE,OAAO,EAAE,MAAM,EAAkB,CAAC;YAC/E,OAAO,GAAG,CAAC;QACZ,CAAC;IACF,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAkB,CAAC,CAAC;IACpF,OAAO,GAAG,CAAC;AAAA,CACX;AAED,gFAAgF;AAChF,SAAS,UAAU,CAAC,KAAiB,EAAU;IAC9C,OAAO,iBAAiB,KAAK,CAAC,OAAO,EAAE,CAAC;AAAA,CACxC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,EAAgB,EAAE,OAAO,GAAgC,EAAE,EAAQ;IAC5G,MAAM,OAAO,GAAG,EAAwC,CAAC;IACzD,IAAI,OAAO,CAAC,UAAU,CAAC;QAAE,OAAO;IAChC,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IAE3B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,+BAA+B,CAAC;IAEvE,4EAA4E;IAC5E,+EAA+E;IAC/E,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,mEAAmE;IACnE,IAAI,YAAiC,CAAC;IAEtC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC;QACzC,IAAI,YAAY,KAAK,SAAS;YAAE,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC9D,OAAO,YAAY,CAAC;IAAA,CACpB,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,GAAW,EAAQ,EAAE,CAAC;QACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO;QAC1B,KAAK,MAAM,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACtD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACjE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IAAA,CACD,CAAC;IAEF,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,MAAyB,EAAE,EAAE,CAAC;QACrD,mBAAmB,CAAC,KAAK,EAAE,CAAC;QAC5B,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACnB,gBAAgB,GAAG,KAAK,CAAC;QACzB,YAAY,GAAG,SAAS,CAAC;QACzB,qBAAqB,EAAE,CAAC;IAAA,CACxB,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAsB,EAAE,EAAE,CAAC;QAC/C,gBAAgB,GAAG,KAAK,CAAC;IAAA,CACzB,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,KAA4B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACpF,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC/B,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,sBAAsB,EAAE,CAAC,KAA8B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACxF,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC1C,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,KAA4B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACpF,IAAI,KAAK,CAAC,OAAO;YAAE,OAAO;QAC1B,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC5C,CAAC,CAAC;IAEH,4EAA4E;IAC5E,4EAA4E;IAC5E,gCAAgC;IAChC,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,GAAqB,EAAE,EAAE,CAAC;QAChE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,gBAAgB;YAAE,OAAO,SAAS,CAAC;QAC5D,6EAA6E;QAC7E,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC;YAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5F,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,aAAa,CAAC,KAAK,CAAC,CAAC;QACrB,gBAAgB,GAAG,IAAI,CAAC;QACxB,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;IAAA,CACnE,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Runtime \"plugin reuse nudge\" — the reactive half of the plugin-productivity\n * guidance.\n *\n * The static `promptGuidelines` on the plugin tools are injected into the system\n * prompt once at session build and never re-surface when the work actually shows\n * a reusable-facing cue. This extension closes that gap: it watches tool output\n * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it\n * attaches a matching, plugin-facing note to the *next* turn's context — an\n * ephemeral message merged into the outgoing request via the `context` hook, so\n * nothing is written into persisted history.\n *\n * Policy (see ./policy.ts for the cue table — the single source of truth):\n * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`\n * (tool args, e.g. content being written) and `tool_execution_end` (tool\n * output, e.g. content being read).\n * - First-hit arming: a matching cue arms its nudge immediately.\n * - One note per turn, and at most once per category per session — so a cue\n * that keeps appearing never turns into a nag.\n * - The whole thing is gated on the autonomous-plugin-system flag\n * (`enablePluginTools`, default off) and never blocks normal flow.\n *\n * Wired once from hoo-core (the single default composition root); the guard below\n * makes double-registration a no-op for downstreams that compose extensions\n * differently, and the static import keeps it bundled in the compiled binary.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type {\n\tBeforeAgentStartEvent,\n\tContextEvent,\n\tExtensionAPI,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolExecutionEndEvent,\n\tToolExecutionStartEvent,\n\tTurnStartEvent,\n} from \"../../../core/extensions/types.js\";\nimport {\n\tarmReuseNudge,\n\tclearArmedReuseNudges,\n\tisAutonomousPluginSystemEnabled,\n\tmatchReuseNudges,\n\ttype ReuseNudge,\n} from \"./policy.js\";\n\n/** Guards against double-registration when default extensions load more than once. */\nconst REGISTERED = Symbol.for(\"hoocode.promptReactiveNudges.registered\");\n\n/** Cap on how much tool text we scan per event — cues are short, files can be huge. */\nconst SCAN_CAP = 20_000;\n\nexport interface PromptReactiveNudgesOptions {\n\t/**\n\t * Enablement gate. Defaults to the shared autonomous-plugin-system flag so the\n\t * reactive nudge and the plugin tool surface flip together. Injectable for tests.\n\t */\n\tisEnabled?: (cwd: string) => boolean;\n}\n\n/**\n * Pull scannable text out of an arbitrary tool result or args object. Prefers\n * `content[].text` blocks (the tool-result convention) and falls back to a\n * bounded JSON stringify so cues in nested fields are still caught.\n */\nfunction extractText(value: unknown): string {\n\tif (value == null) return \"\";\n\tif (typeof value === \"string\") return value.slice(0, SCAN_CAP);\n\tif (typeof value === \"object\") {\n\t\tconst obj = value as { content?: unknown };\n\t\tif (Array.isArray(obj.content)) {\n\t\t\tconst text = obj.content\n\t\t\t\t.map((b) =>\n\t\t\t\t\tb && typeof b === \"object\" && typeof (b as { text?: unknown }).text === \"string\"\n\t\t\t\t\t\t? (b as { text: string }).text\n\t\t\t\t\t\t: \"\",\n\t\t\t\t)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) return text.slice(0, SCAN_CAP);\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.stringify(value).slice(0, SCAN_CAP);\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\treturn String(value).slice(0, SCAN_CAP);\n}\n\n/**\n * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text\n * block on the final user turn (valid alongside tool-result blocks) so the model\n * sees it right before it responds; falls back to a fresh user message when the\n * last message isn't a user turn. The input array is never mutated.\n */\nfunction injectNote(messages: AgentMessage[], note: string): AgentMessage[] {\n\tconst block: TextContent = { type: \"text\", text: note };\n\tconst out = messages.slice();\n\tconst last = out[out.length - 1] as { role?: string; content?: unknown } | undefined;\n\tif (last && last.role === \"user\") {\n\t\tconst content = last.content;\n\t\tconst merged =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: content } as TextContent, block]\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? [...content, block]\n\t\t\t\t\t: null;\n\t\tif (merged) {\n\t\t\tout[out.length - 1] = { ...(last as object), content: merged } as AgentMessage;\n\t\t\treturn out;\n\t\t}\n\t}\n\tout.push({ role: \"user\", content: [block], timestamp: Date.now() } as AgentMessage);\n\treturn out;\n}\n\n/** Wrap a nudge snippet so it reads as a system aside rather than user text. */\nfunction formatNote(nudge: ReuseNudge): string {\n\treturn `[reuse-nudge] ${nudge.snippet}`;\n}\n\n/**\n * Install the runtime reuse-nudge extension. Idempotent — a second call on the\n * same `pi` is a no-op, so composing default extensions twice is harmless.\n */\nexport function setupPromptReactiveNudges(pi: ExtensionAPI, options: PromptReactiveNudgesOptions = {}): void {\n\tconst guarded = pi as unknown as Record<symbol, boolean>;\n\tif (guarded[REGISTERED]) return;\n\tguarded[REGISTERED] = true;\n\n\tconst isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;\n\n\t// Session-scoped state. Categories fire once per session; the pending queue\n\t// holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.\n\tconst deliveredCategories = new Set<string>();\n\tconst pending: ReuseNudge[] = [];\n\tlet injectedThisTurn = false;\n\t// Cached enablement, recomputed lazily and reset on session start.\n\tlet enabledCache: boolean | undefined;\n\n\tconst enabled = (cwd: string): boolean => {\n\t\tif (enabledCache === undefined) enabledCache = isEnabled(cwd);\n\t\treturn enabledCache;\n\t};\n\n\tconst enqueue = (text: string, cwd: string): void => {\n\t\tif (!enabled(cwd)) return;\n\t\tfor (const nudge of matchReuseNudges(text)) {\n\t\t\tif (deliveredCategories.has(nudge.category)) continue;\n\t\t\tif (pending.some((p) => p.category === nudge.category)) continue;\n\t\t\tpending.push(nudge);\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent) => {\n\t\tdeliveredCategories.clear();\n\t\tpending.length = 0;\n\t\tinjectedThisTurn = false;\n\t\tenabledCache = undefined;\n\t\tclearArmedReuseNudges();\n\t});\n\n\tpi.on(\"turn_start\", (_event: TurnStartEvent) => {\n\t\tinjectedThisTurn = false;\n\t});\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(event.prompt, ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_start\", (event: ToolExecutionStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(extractText(event.args), ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_end\", (event: ToolExecutionEndEvent, ctx: ExtensionContext) => {\n\t\tif (event.isError) return;\n\t\tenqueue(extractText(event.result), ctx.cwd);\n\t});\n\n\t// The injection point: fires before each provider request. transformContext\n\t// output is request-scoped (never written back to agent state), so the note\n\t// is ephemeral by construction.\n\tpi.on(\"context\", (event: ContextEvent, ctx: ExtensionContext) => {\n\t\tif (!enabled(ctx.cwd) || injectedThisTurn) return undefined;\n\t\t// Drop any that raced to \"delivered\" via another path, then take the oldest.\n\t\twhile (pending.length > 0 && deliveredCategories.has(pending[0]!.category)) pending.shift();\n\t\tconst nudge = pending.shift();\n\t\tif (!nudge) return undefined;\n\t\tdeliveredCategories.add(nudge.category);\n\t\tarmReuseNudge(nudge);\n\t\tinjectedThisTurn = true;\n\t\treturn { messages: injectNote(event.messages, formatNote(nudge)) };\n\t});\n}\n"]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Prompt-reactive reuse policy — the single source of truth for the runtime
3
+ * "plugin reuse nudge".
4
+ *
5
+ * The plugin-productivity guidance in `core/tools/plugins.ts` and
6
+ * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into
7
+ * the system prompt once at session build. That text is never re-surfaced when
8
+ * the work actually shows an actionable cue, so the model tends to repeat the
9
+ * same advice by hand instead of reaching for the plugin layer.
10
+ *
11
+ * This module holds the reactive policy that fixes that:
12
+ * - {@link REUSE_NUDGES} the curated cue → nudge table
13
+ * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues
14
+ * - the armed registry nudges that fired this process, so the plugin
15
+ * layer can surface them even when no tool asked
16
+ * - {@link isAutonomousPluginSystemEnabled}
17
+ * the one enablement gate shared with the
18
+ * tool-attach path in main.ts (settings.json
19
+ * `enablePluginTools`)
20
+ *
21
+ * The extension in `./nudges.ts` wires this policy to tool/turn events; the
22
+ * policy itself stays pure and independently testable.
23
+ *
24
+ * Conservative by construction: the patterns are specific (a false positive
25
+ * becoming a nag is the real risk), each nudge fires at most once per category
26
+ * per session, and every nudge points back at the plugin layer rather than
27
+ * re-stating advice.
28
+ */
29
+ /** A single reactive reuse cue and the plugin-facing nudge it arms. */
30
+ export interface ReuseNudge {
31
+ /** Stable id, also used as the de-dupe key so a nudge fires once per session. */
32
+ id: string;
33
+ /** Human-facing category label (grouping for the one-per-category cap). */
34
+ category: string;
35
+ /** Specific cue matched against runtime text. Kept tight to avoid false positives. */
36
+ pattern: RegExp;
37
+ /** The reusable-facing note injected into the turn; always points at the plugin layer. */
38
+ snippet: string;
39
+ }
40
+ /**
41
+ * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but
42
+ * keyed on concrete cues that show up in real work (style/policy directives,
43
+ * format standardization, explicit capability gaps). Patterns are deliberately
44
+ * specific — first-hit arming (see nudges.ts) only stays safe if the cue is
45
+ * unambiguous.
46
+ */
47
+ export declare const REUSE_NUDGES: readonly ReuseNudge[];
48
+ /** Record that a nudge fired this session so the plugin layer can surface it. */
49
+ export declare function armReuseNudge(nudge: ReuseNudge): void;
50
+ /** Reuse nudges that have fired this session, in insertion order. */
51
+ export declare function getArmedReuseNudges(): ReuseNudge[];
52
+ /** Reset the armed registry (called on session start). */
53
+ export declare function clearArmedReuseNudges(): void;
54
+ /**
55
+ * Scan a blob of runtime text (tool output, turn text, user prompt) and return
56
+ * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's
57
+ * job.
58
+ */
59
+ export declare function matchReuseNudges(text: string | undefined | null): ReuseNudge[];
60
+ /**
61
+ * The one enablement gate for the whole autonomous plugin system.
62
+ *
63
+ * Reads settings.json `enablePluginTools` (default false) — the same setting
64
+ * that gates whether the plugin lifecycle tools are attached to the top-level
65
+ * agent in main.ts. Keeping both the tool surface and the reactive nudge behind
66
+ * a single flag is the "whole autonomous plugin system, off by default" switch.
67
+ */
68
+ export declare function isAutonomousPluginSystemEnabled(cwd: string): boolean;
69
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,uEAAuE;AACvE,MAAM,WAAW,UAAU;IAC1B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EA6B7C,CAAC;AAWF,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAErD;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,IAAI,UAAU,EAAE,CAElD;AAED,0DAA0D;AAC1D,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,UAAU,EAAE,CAG9E;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOpE","sourcesContent":["/**\n * Prompt-reactive reuse policy — the single source of truth for the runtime\n * \"plugin reuse nudge\".\n *\n * The plugin-productivity guidance in `core/tools/plugins.ts` and\n * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into\n * the system prompt once at session build. That text is never re-surfaced when\n * the work actually shows an actionable cue, so the model tends to repeat the\n * same advice by hand instead of reaching for the plugin layer.\n *\n * This module holds the reactive policy that fixes that:\n * - {@link REUSE_NUDGES} the curated cue → nudge table\n * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues\n * - the armed registry nudges that fired this process, so the plugin\n * layer can surface them even when no tool asked\n * - {@link isAutonomousPluginSystemEnabled}\n * the one enablement gate shared with the\n * tool-attach path in main.ts (settings.json\n * `enablePluginTools`)\n *\n * The extension in `./nudges.ts` wires this policy to tool/turn events; the\n * policy itself stays pure and independently testable.\n *\n * Conservative by construction: the patterns are specific (a false positive\n * becoming a nag is the real risk), each nudge fires at most once per category\n * per session, and every nudge points back at the plugin layer rather than\n * re-stating advice.\n */\n\nimport { getAgentDir } from \"../../../config.js\";\nimport { SettingsManager } from \"../../../core/settings-manager.js\";\n\n/** A single reactive reuse cue and the plugin-facing nudge it arms. */\nexport interface ReuseNudge {\n\t/** Stable id, also used as the de-dupe key so a nudge fires once per session. */\n\tid: string;\n\t/** Human-facing category label (grouping for the one-per-category cap). */\n\tcategory: string;\n\t/** Specific cue matched against runtime text. Kept tight to avoid false positives. */\n\tpattern: RegExp;\n\t/** The reusable-facing note injected into the turn; always points at the plugin layer. */\n\tsnippet: string;\n}\n\n/**\n * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but\n * keyed on concrete cues that show up in real work (style/policy directives,\n * format standardization, explicit capability gaps). Patterns are deliberately\n * specific — first-hit arming (see nudges.ts) only stays safe if the cue is\n * unambiguous.\n */\nexport const REUSE_NUDGES: readonly ReuseNudge[] = [\n\t{\n\t\tid: \"style-active-voice\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\b(?:in\\s+)?active voice\\b|\\bavoid(?:ing)?\\s+passive voice\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"active voice\"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"style-avoid-repetition\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\bavoid(?:ing)?\\s+repetition\\b|\\bdon['’]?t\\s+repeat\\b|\\bavoid\\s+repeat(?:ing|s)?\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"avoid repetition\"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"format-prefer-json\",\n\t\tcategory: \"output-format\",\n\t\tpattern: /\\bprefer\\s+JSON\\b|\\buse\\s+JSON\\b|\\boutput\\s+(?:as\\s+)?JSON\\b|\\breturn\\s+(?:as\\s+)?JSON\\b/i,\n\t\tsnippet:\n\t\t\t\"An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.\",\n\t},\n\t{\n\t\tid: \"capability-gap\",\n\t\tcategory: \"capability-gap\",\n\t\tpattern: /\\bcapability gap\\b|\\bno tool for\\b|\\blacks? a tool\\b|\\bwish (?:I|we) had a tool\\b/i,\n\t\tsnippet:\n\t\t\t\"This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.\",\n\t},\n];\n\n// ── Armed registry (process-scoped) ──────────────────────────────────────────\n//\n// When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)\n// can surface \"there is already a reuse candidate for this work\" even if the\n// model never explicitly asked. The CLI is single-process per session, so a\n// module-level set is the natural home; nudges.ts clears it on session start.\n\nconst armed = new Map<string, ReuseNudge>();\n\n/** Record that a nudge fired this session so the plugin layer can surface it. */\nexport function armReuseNudge(nudge: ReuseNudge): void {\n\tarmed.set(nudge.id, nudge);\n}\n\n/** Reuse nudges that have fired this session, in insertion order. */\nexport function getArmedReuseNudges(): ReuseNudge[] {\n\treturn [...armed.values()];\n}\n\n/** Reset the armed registry (called on session start). */\nexport function clearArmedReuseNudges(): void {\n\tarmed.clear();\n}\n\n/**\n * Scan a blob of runtime text (tool output, turn text, user prompt) and return\n * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's\n * job.\n */\nexport function matchReuseNudges(text: string | undefined | null): ReuseNudge[] {\n\tif (!text) return [];\n\treturn REUSE_NUDGES.filter((n) => n.pattern.test(text));\n}\n\n/**\n * The one enablement gate for the whole autonomous plugin system.\n *\n * Reads settings.json `enablePluginTools` (default false) — the same setting\n * that gates whether the plugin lifecycle tools are attached to the top-level\n * agent in main.ts. Keeping both the tool surface and the reactive nudge behind\n * a single flag is the \"whole autonomous plugin system, off by default\" switch.\n */\nexport function isAutonomousPluginSystemEnabled(cwd: string): boolean {\n\ttry {\n\t\treturn SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();\n\t} catch {\n\t\t// Fail closed: if settings can't be read, treat the autonomous system as off.\n\t\treturn false;\n\t}\n}\n"]}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Prompt-reactive reuse policy — the single source of truth for the runtime
3
+ * "plugin reuse nudge".
4
+ *
5
+ * The plugin-productivity guidance in `core/tools/plugins.ts` and
6
+ * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into
7
+ * the system prompt once at session build. That text is never re-surfaced when
8
+ * the work actually shows an actionable cue, so the model tends to repeat the
9
+ * same advice by hand instead of reaching for the plugin layer.
10
+ *
11
+ * This module holds the reactive policy that fixes that:
12
+ * - {@link REUSE_NUDGES} the curated cue → nudge table
13
+ * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues
14
+ * - the armed registry nudges that fired this process, so the plugin
15
+ * layer can surface them even when no tool asked
16
+ * - {@link isAutonomousPluginSystemEnabled}
17
+ * the one enablement gate shared with the
18
+ * tool-attach path in main.ts (settings.json
19
+ * `enablePluginTools`)
20
+ *
21
+ * The extension in `./nudges.ts` wires this policy to tool/turn events; the
22
+ * policy itself stays pure and independently testable.
23
+ *
24
+ * Conservative by construction: the patterns are specific (a false positive
25
+ * becoming a nag is the real risk), each nudge fires at most once per category
26
+ * per session, and every nudge points back at the plugin layer rather than
27
+ * re-stating advice.
28
+ */
29
+ import { getAgentDir } from "../../../config.js";
30
+ import { SettingsManager } from "../../../core/settings-manager.js";
31
+ /**
32
+ * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but
33
+ * keyed on concrete cues that show up in real work (style/policy directives,
34
+ * format standardization, explicit capability gaps). Patterns are deliberately
35
+ * specific — first-hit arming (see nudges.ts) only stays safe if the cue is
36
+ * unambiguous.
37
+ */
38
+ export const REUSE_NUDGES = [
39
+ {
40
+ id: "style-active-voice",
41
+ category: "writing-style",
42
+ pattern: /\b(?:in\s+)?active voice\b|\bavoid(?:ing)?\s+passive voice\b/i,
43
+ snippet: 'A writing-style rule is in play ("active voice"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',
44
+ },
45
+ {
46
+ id: "style-avoid-repetition",
47
+ category: "writing-style",
48
+ pattern: /\bavoid(?:ing)?\s+repetition\b|\bdon['’]?t\s+repeat\b|\bavoid\s+repeat(?:ing|s)?\b/i,
49
+ snippet: 'A writing-style rule is in play ("avoid repetition"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',
50
+ },
51
+ {
52
+ id: "format-prefer-json",
53
+ category: "output-format",
54
+ pattern: /\bprefer\s+JSON\b|\buse\s+JSON\b|\boutput\s+(?:as\s+)?JSON\b|\breturn\s+(?:as\s+)?JSON\b/i,
55
+ snippet: "An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.",
56
+ },
57
+ {
58
+ id: "capability-gap",
59
+ category: "capability-gap",
60
+ pattern: /\bcapability gap\b|\bno tool for\b|\blacks? a tool\b|\bwish (?:I|we) had a tool\b/i,
61
+ snippet: "This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.",
62
+ },
63
+ ];
64
+ // ── Armed registry (process-scoped) ──────────────────────────────────────────
65
+ //
66
+ // When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)
67
+ // can surface "there is already a reuse candidate for this work" even if the
68
+ // model never explicitly asked. The CLI is single-process per session, so a
69
+ // module-level set is the natural home; nudges.ts clears it on session start.
70
+ const armed = new Map();
71
+ /** Record that a nudge fired this session so the plugin layer can surface it. */
72
+ export function armReuseNudge(nudge) {
73
+ armed.set(nudge.id, nudge);
74
+ }
75
+ /** Reuse nudges that have fired this session, in insertion order. */
76
+ export function getArmedReuseNudges() {
77
+ return [...armed.values()];
78
+ }
79
+ /** Reset the armed registry (called on session start). */
80
+ export function clearArmedReuseNudges() {
81
+ armed.clear();
82
+ }
83
+ /**
84
+ * Scan a blob of runtime text (tool output, turn text, user prompt) and return
85
+ * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's
86
+ * job.
87
+ */
88
+ export function matchReuseNudges(text) {
89
+ if (!text)
90
+ return [];
91
+ return REUSE_NUDGES.filter((n) => n.pattern.test(text));
92
+ }
93
+ /**
94
+ * The one enablement gate for the whole autonomous plugin system.
95
+ *
96
+ * Reads settings.json `enablePluginTools` (default false) — the same setting
97
+ * that gates whether the plugin lifecycle tools are attached to the top-level
98
+ * agent in main.ts. Keeping both the tool surface and the reactive nudge behind
99
+ * a single flag is the "whole autonomous plugin system, off by default" switch.
100
+ */
101
+ export function isAutonomousPluginSystemEnabled(cwd) {
102
+ try {
103
+ return SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();
104
+ }
105
+ catch {
106
+ // Fail closed: if settings can't be read, treat the autonomous system as off.
107
+ return false;
108
+ }
109
+ }
110
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAcpE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAA0B;IAClD;QACC,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,+DAA+D;QACxE,OAAO,EACN,8NAA4N;KAC7N;IACD;QACC,EAAE,EAAE,wBAAwB;QAC5B,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,uFAAqF;QAC9F,OAAO,EACN,0MAAwM;KACzM;IACD;QACC,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,2FAA2F;QACpG,OAAO,EACN,iMAA+L;KAChM;IACD;QACC,EAAE,EAAE,gBAAgB;QACpB,QAAQ,EAAE,gBAAgB;QAC1B,OAAO,EAAE,oFAAoF;QAC7F,OAAO,EACN,+KAA+K;KAChL;CACD,CAAC;AAEF,wKAAgF;AAChF,EAAE;AACF,kFAAkF;AAClF,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAE9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE5C,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,KAAiB,EAAQ;IACtD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAAA,CAC3B;AAED,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,GAAiB;IACnD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC3B;AAED,0DAA0D;AAC1D,MAAM,UAAU,qBAAqB,GAAS;IAC7C,KAAK,CAAC,KAAK,EAAE,CAAC;AAAA,CACd;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA+B,EAAgB;IAC/E,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CACxD;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,GAAW,EAAW;IACrE,IAAI,CAAC;QACJ,OAAO,eAAe,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC,oBAAoB,EAAE,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACR,8EAA8E;QAC9E,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD","sourcesContent":["/**\n * Prompt-reactive reuse policy — the single source of truth for the runtime\n * \"plugin reuse nudge\".\n *\n * The plugin-productivity guidance in `core/tools/plugins.ts` and\n * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into\n * the system prompt once at session build. That text is never re-surfaced when\n * the work actually shows an actionable cue, so the model tends to repeat the\n * same advice by hand instead of reaching for the plugin layer.\n *\n * This module holds the reactive policy that fixes that:\n * - {@link REUSE_NUDGES} the curated cue → nudge table\n * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues\n * - the armed registry nudges that fired this process, so the plugin\n * layer can surface them even when no tool asked\n * - {@link isAutonomousPluginSystemEnabled}\n * the one enablement gate shared with the\n * tool-attach path in main.ts (settings.json\n * `enablePluginTools`)\n *\n * The extension in `./nudges.ts` wires this policy to tool/turn events; the\n * policy itself stays pure and independently testable.\n *\n * Conservative by construction: the patterns are specific (a false positive\n * becoming a nag is the real risk), each nudge fires at most once per category\n * per session, and every nudge points back at the plugin layer rather than\n * re-stating advice.\n */\n\nimport { getAgentDir } from \"../../../config.js\";\nimport { SettingsManager } from \"../../../core/settings-manager.js\";\n\n/** A single reactive reuse cue and the plugin-facing nudge it arms. */\nexport interface ReuseNudge {\n\t/** Stable id, also used as the de-dupe key so a nudge fires once per session. */\n\tid: string;\n\t/** Human-facing category label (grouping for the one-per-category cap). */\n\tcategory: string;\n\t/** Specific cue matched against runtime text. Kept tight to avoid false positives. */\n\tpattern: RegExp;\n\t/** The reusable-facing note injected into the turn; always points at the plugin layer. */\n\tsnippet: string;\n}\n\n/**\n * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but\n * keyed on concrete cues that show up in real work (style/policy directives,\n * format standardization, explicit capability gaps). Patterns are deliberately\n * specific — first-hit arming (see nudges.ts) only stays safe if the cue is\n * unambiguous.\n */\nexport const REUSE_NUDGES: readonly ReuseNudge[] = [\n\t{\n\t\tid: \"style-active-voice\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\b(?:in\\s+)?active voice\\b|\\bavoid(?:ing)?\\s+passive voice\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"active voice\"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"style-avoid-repetition\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\bavoid(?:ing)?\\s+repetition\\b|\\bdon['’]?t\\s+repeat\\b|\\bavoid\\s+repeat(?:ing|s)?\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"avoid repetition\"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"format-prefer-json\",\n\t\tcategory: \"output-format\",\n\t\tpattern: /\\bprefer\\s+JSON\\b|\\buse\\s+JSON\\b|\\boutput\\s+(?:as\\s+)?JSON\\b|\\breturn\\s+(?:as\\s+)?JSON\\b/i,\n\t\tsnippet:\n\t\t\t\"An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.\",\n\t},\n\t{\n\t\tid: \"capability-gap\",\n\t\tcategory: \"capability-gap\",\n\t\tpattern: /\\bcapability gap\\b|\\bno tool for\\b|\\blacks? a tool\\b|\\bwish (?:I|we) had a tool\\b/i,\n\t\tsnippet:\n\t\t\t\"This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.\",\n\t},\n];\n\n// ── Armed registry (process-scoped) ──────────────────────────────────────────\n//\n// When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)\n// can surface \"there is already a reuse candidate for this work\" even if the\n// model never explicitly asked. The CLI is single-process per session, so a\n// module-level set is the natural home; nudges.ts clears it on session start.\n\nconst armed = new Map<string, ReuseNudge>();\n\n/** Record that a nudge fired this session so the plugin layer can surface it. */\nexport function armReuseNudge(nudge: ReuseNudge): void {\n\tarmed.set(nudge.id, nudge);\n}\n\n/** Reuse nudges that have fired this session, in insertion order. */\nexport function getArmedReuseNudges(): ReuseNudge[] {\n\treturn [...armed.values()];\n}\n\n/** Reset the armed registry (called on session start). */\nexport function clearArmedReuseNudges(): void {\n\tarmed.clear();\n}\n\n/**\n * Scan a blob of runtime text (tool output, turn text, user prompt) and return\n * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's\n * job.\n */\nexport function matchReuseNudges(text: string | undefined | null): ReuseNudge[] {\n\tif (!text) return [];\n\treturn REUSE_NUDGES.filter((n) => n.pattern.test(text));\n}\n\n/**\n * The one enablement gate for the whole autonomous plugin system.\n *\n * Reads settings.json `enablePluginTools` (default false) — the same setting\n * that gates whether the plugin lifecycle tools are attached to the top-level\n * agent in main.ts. Keeping both the tool surface and the reactive nudge behind\n * a single flag is the \"whole autonomous plugin system, off by default\" switch.\n */\nexport function isAutonomousPluginSystemEnabled(cwd: string): boolean {\n\ttry {\n\t\treturn SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();\n\t} catch {\n\t\t// Fail closed: if settings can't be read, treat the autonomous system as off.\n\t\treturn false;\n\t}\n}\n"]}