@kolisachint/hoocode-agent 0.5.17 → 0.5.19

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 (83) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/dist/core/learn/audit.d.ts +136 -0
  3. package/dist/core/learn/audit.d.ts.map +1 -0
  4. package/dist/core/learn/audit.js +316 -0
  5. package/dist/core/learn/audit.js.map +1 -0
  6. package/dist/core/learn/cache.d.ts +58 -0
  7. package/dist/core/learn/cache.d.ts.map +1 -0
  8. package/dist/core/learn/cache.js +132 -0
  9. package/dist/core/learn/cache.js.map +1 -0
  10. package/dist/core/learn/cluster.d.ts +78 -0
  11. package/dist/core/learn/cluster.d.ts.map +1 -0
  12. package/dist/core/learn/cluster.js +184 -0
  13. package/dist/core/learn/cluster.js.map +1 -0
  14. package/dist/core/learn/coverage.d.ts +58 -0
  15. package/dist/core/learn/coverage.d.ts.map +1 -0
  16. package/dist/core/learn/coverage.js +144 -0
  17. package/dist/core/learn/coverage.js.map +1 -0
  18. package/dist/core/learn/digest.d.ts +13 -0
  19. package/dist/core/learn/digest.d.ts.map +1 -1
  20. package/dist/core/learn/digest.js +113 -14
  21. package/dist/core/learn/digest.js.map +1 -1
  22. package/dist/core/learn/extract.d.ts +108 -105
  23. package/dist/core/learn/extract.d.ts.map +1 -1
  24. package/dist/core/learn/extract.js +308 -447
  25. package/dist/core/learn/extract.js.map +1 -1
  26. package/dist/core/learn/mine.d.ts +178 -0
  27. package/dist/core/learn/mine.d.ts.map +1 -0
  28. package/dist/core/learn/mine.js +390 -0
  29. package/dist/core/learn/mine.js.map +1 -0
  30. package/dist/core/learn/reduce.d.ts +89 -0
  31. package/dist/core/learn/reduce.d.ts.map +1 -0
  32. package/dist/core/learn/reduce.js +179 -0
  33. package/dist/core/learn/reduce.js.map +1 -0
  34. package/dist/core/learn/state.d.ts +19 -18
  35. package/dist/core/learn/state.d.ts.map +1 -1
  36. package/dist/core/learn/state.js +35 -31
  37. package/dist/core/learn/state.js.map +1 -1
  38. package/dist/core/settings-defaults.d.ts +1 -1
  39. package/dist/core/settings-defaults.d.ts.map +1 -1
  40. package/dist/core/settings-defaults.js +1 -1
  41. package/dist/core/settings-defaults.js.map +1 -1
  42. package/dist/core/settings-manager.d.ts +4 -2
  43. package/dist/core/settings-manager.d.ts.map +1 -1
  44. package/dist/core/settings-manager.js +5 -1
  45. package/dist/core/settings-manager.js.map +1 -1
  46. package/dist/core/settings-types.d.ts +1 -1
  47. package/dist/core/settings-types.d.ts.map +1 -1
  48. package/dist/core/settings-types.js.map +1 -1
  49. package/dist/core/startup-progress.d.ts +12 -7
  50. package/dist/core/startup-progress.d.ts.map +1 -1
  51. package/dist/core/startup-progress.js +12 -7
  52. package/dist/core/startup-progress.js.map +1 -1
  53. package/dist/extensions/core/learn.d.ts +8 -4
  54. package/dist/extensions/core/learn.d.ts.map +1 -1
  55. package/dist/extensions/core/learn.js +292 -56
  56. package/dist/extensions/core/learn.js.map +1 -1
  57. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/footer.js +7 -25
  59. package/dist/modes/interactive/components/footer.js.map +1 -1
  60. package/dist/modes/interactive/components/progress-bar.d.ts +50 -0
  61. package/dist/modes/interactive/components/progress-bar.d.ts.map +1 -0
  62. package/dist/modes/interactive/components/progress-bar.js +77 -0
  63. package/dist/modes/interactive/components/progress-bar.js.map +1 -0
  64. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/settings-selector.js +1 -1
  66. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +1 -1
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/modes/interactive/voice/voice-panel.d.ts +6 -1
  71. package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
  72. package/dist/modes/interactive/voice/voice-panel.js +18 -14
  73. package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
  74. package/docs/settings.md +9 -6
  75. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  76. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  77. package/examples/extensions/sandbox/package.json +1 -1
  78. package/examples/extensions/with-deps/package.json +1 -1
  79. package/package.json +4 -4
  80. package/dist/core/learn/normalize.d.ts +0 -65
  81. package/dist/core/learn/normalize.d.ts.map +0 -1
  82. package/dist/core/learn/normalize.js +0 -245
  83. package/dist/core/learn/normalize.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../../src/core/learn/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAehD,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA2IjG","sourcesContent":["/**\n * Renders the extractor's output into the message `/learn` injects.\n *\n * The digest is evidence plus instructions, and the split matters: the numbers\n * come from {@link extractLearnDigest} and are not negotiable, while everything\n * the model does with them — phrasing, routing, deciding a pattern is not worth\n * a rule — is judgement it has to exercise. Counts are printed on every item\n * because \"said in 5 of your last 12 sessions\" is a decision the reader can\n * make in one keystroke, where \"extracted from your session\" is not.\n */\n\nimport type { LearnDigest } from \"./extract.js\";\nimport { LEARN_DIGEST_MARKER } from \"./extract.js\";\n\nfunction shortDate(iso: string | undefined): string {\n\tif (!iso) return \"unknown\";\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? \"unknown\" : date.toISOString().slice(0, 10);\n}\n\nfunction evidence(count: number, sessions: number, lastSeen: string): string {\n\tconst times = count === 1 ? \"once\" : `${count}x`;\n\tconst where = sessions === 1 ? \"1 session\" : `${sessions} sessions`;\n\treturn `${times} across ${where}, last ${shortDate(lastSeen)}`;\n}\n\n/** True when there is nothing worth asking the model to look at. */\nexport function isEmptyDigest(digest: LearnDigest): boolean {\n\treturn digest.directives.length === 0 && digest.fixes.length === 0 && digest.workflows.length === 0;\n}\n\nexport function renderLearnDigest(digest: LearnDigest, options: { userScopePath: string }): string {\n\tconst lines: string[] = [];\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Mined ${digest.scannedSessions} session(s) in this directory` +\n\t\t\t(digest.skippedSessions > 0 ? ` (${digest.skippedSessions} skipped: out of window or unreadable)` : \"\") +\n\t\t\t(digest.oldestSession ? `, ${shortDate(digest.oldestSession)} to ${shortDate(digest.newestSession)}` : \"\") +\n\t\t\t(digest.suppressed > 0 ? `. ${digest.suppressed} item(s) held back — already shown and unchanged since` : \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"The counts below are computed from session transcripts on disk, not from this conversation. \" +\n\t\t\t\"Treat them as evidence, not conclusions — your job is to decide what deserves to be written down, \" +\n\t\t\t\"phrase it, and put it in the right place.\",\n\t);\n\tlines.push(\"\");\n\n\t// ── Directives ───────────────────────────────────────────────────────────\n\tif (digest.directives.length > 0) {\n\t\tlines.push(\"## Directives you have repeated\");\n\t\tlines.push(\"\");\n\t\tfor (const cluster of digest.directives) {\n\t\t\tlines.push(`- **${cluster.status}** — \"${cluster.text.replace(/\\s+/g, \" \").trim()}\"`);\n\t\t\tlines.push(` - ${evidence(cluster.count, cluster.sessions, cluster.lastSeen)}`);\n\t\t\tif (cluster.existingRule) {\n\t\t\t\tlines.push(` - already covered by: \"${cluster.existingRule.slice(0, 160)}\"`);\n\t\t\t}\n\t\t\tif (cluster.existingSkill) {\n\t\t\t\tlines.push(` - already covered by the \\`${cluster.existingSkill}\\` skill`);\n\t\t\t}\n\t\t\tif (cluster.previouslyDeclined) {\n\t\t\t\tlines.push(\" - proposed before and not written down — you have already passed on this once\");\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Fixes ────────────────────────────────────────────────────────────────\n\tif (digest.fixes.length > 0) {\n\t\tlines.push(\"## Failures you resolved\");\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\t\"Each is a command that failed, then later succeeded unchanged after intervening work — \" +\n\t\t\t\t\"so something in between was the fix.\",\n\t\t);\n\t\tlines.push(\"\");\n\t\tfor (const fix of digest.fixes) {\n\t\t\tlines.push(`- \\`${fix.command}\\` — ${evidence(fix.count, fix.sessions, fix.lastSeen)}`);\n\t\t\tlines.push(` - error: ${fix.errorExcerpt}`);\n\t\t\tif (fix.interveningCommands.length > 0) {\n\t\t\t\tlines.push(` - commands in between: ${fix.interveningCommands.map((c) => `\\`${c}\\``).join(\", \")}`);\n\t\t\t}\n\t\t\tif (fix.editedFiles.length > 0) {\n\t\t\t\tlines.push(` - files edited: ${fix.editedFiles.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Workflows ────────────────────────────────────────────────────────────\n\tif (digest.workflows.length > 0) {\n\t\tlines.push(\"## Repeated tool sequences\");\n\t\tlines.push(\"\");\n\t\tfor (const workflow of digest.workflows) {\n\t\t\tlines.push(\n\t\t\t\t`- \\`${workflow.steps.join(\" → \")}\\` — ${evidence(workflow.count, workflow.sessions, workflow.lastSeen)}`,\n\t\t\t);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Instructions ─────────────────────────────────────────────────────────\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\"Work through the items above and propose concrete edits. For each one, decide:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"1. **Is it durable?** A rule that will still be true next month belongs somewhere. A one-off preference \" +\n\t\t\t\"about the task you happened to be doing does not. When in doubt, drop it — a wrong rule costs more than \" +\n\t\t\t\"a missing one, because it is paid on every request forever.\",\n\t);\n\tlines.push(\n\t\t\"2. **Rule or skill?** This is the most important call. A context file is loaded on **every** turn; a skill \" +\n\t\t\t\"is loaded **on demand**. So: short, always-true, unconditional → a one-line rule. Long, procedural, \" +\n\t\t\t'or conditional (a sequence of steps, a runbook, anything starting \"when X, do Y\") → a skill, not a rule. ' +\n\t\t\t\"Repeated tool sequences are almost always skills.\",\n\t);\n\tlines.push(\n\t\t`3. **Which scope?** Project-specific (this repo's tests, build, architecture, conventions) → the repo ` +\n\t\t\t`\\`AGENTS.md\\`. Personal habits that travel with you across every repo (style preferences, how you like ` +\n\t\t\t`commits written) → \\`${options.userScopePath}\\`. If it names this repo's files or commands, it is not a ` +\n\t\t\t`user-scope rule.`,\n\t);\n\tlines.push(\n\t\t\"4. **Restated items are rewrites, not additions.** An item marked `restated` is already covered by a rule \" +\n\t\t\t\"that is not working — too vague, buried, or contradicted elsewhere. Rewrite the existing line or delete \" +\n\t\t\t\"it in favour of a sharper one. Do not add a second rule saying the same thing.\",\n\t);\n\tlines.push(\n\t\t\"5. **`has-skill` items are a triggering problem, not a missing rule.** A skill already covers it and you \" +\n\t\t\t\"asked by hand anyway, which usually means the skill's `description` frontmatter does not describe the \" +\n\t\t\t\"situation you were in. Sharpen that description so it matches, rather than adding a rule that duplicates \" +\n\t\t\t\"what the skill already does.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"Then, while you have the file open, audit it:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"- **Delete rules that no longer match the code.** Check a sample against the repo before trusting them.\",\n\t);\n\tlines.push(\"- **Delete rules that restate default behaviour.** Guidance the agent already follows is pure cost.\");\n\tlines.push(\n\t\t\"- **Collapse duplicates**, including any rule stated at both repo and user scope — that one is paid twice.\",\n\t);\n\tlines.push(\n\t\t\"- **One line per rule.** No rationale, no examples, no preamble, unless the example *is* the rule. Prose is \" +\n\t\t\t\"the single biggest source of context-file bloat.\",\n\t);\n\tlines.push(\"\");\n\n\tif (digest.agentsFilePath) {\n\t\tlines.push(\n\t\t\t`The repo context file is \\`${digest.agentsFilePath}\\`` +\n\t\t\t\t(digest.agentsFileTokens ? ` (~${digest.agentsFileTokens} tokens, re-sent every request)` : \"\") +\n\t\t\t\t\". Report the token delta of your proposed changes before applying them; a net reduction is a good outcome.\",\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"No repo context file exists yet. Create one only if at least one durable project rule survives step 1.\",\n\t\t);\n\t}\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Show what you propose, then apply it with edits — do not ask a separate approval question first, the edit \" +\n\t\t\t\"prompt is the approval. If nothing here is worth writing down, say so plainly and change nothing.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../../src/core/learn/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA4BhD,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CA8C7D;AAED,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,aAAa,GAAG,KAAK,CAAA;CAAE,GAC9D,MAAM,CAmMR","sourcesContent":["/**\n * Renders the extractor's output into the message `/learn` injects.\n *\n * The digest is evidence plus instructions, and the split matters: the numbers\n * come from {@link extractLearnDigest} and are not negotiable, while everything\n * the model does with them — phrasing, routing, deciding a pattern is not worth\n * a rule — is judgement it has to exercise. Counts are printed on every item\n * because \"said in 5 of your last 12 sessions\" is a decision the reader can\n * make in one keystroke, where \"extracted from your session\" is not.\n */\n\nimport type { AuditReport } from \"./audit.js\";\nimport { staleTokens } from \"./audit.js\";\nimport type { LearnDigest } from \"./extract.js\";\nimport { LEARN_DIGEST_MARKER } from \"./extract.js\";\n\n/**\n * Quote lengths. A directive is a sentence; a request is a whole task message,\n * and can be a slash-command body running to thousands of characters.\n */\nconst DIRECTIVE_QUOTE_CHARS = 400;\nconst REQUEST_QUOTE_CHARS = 200;\n\n/** One line, bounded — a quote has to survive being rendered inside a list item. */\nfunction quote(text: string, limit: number): string {\n\tconst flat = text.replace(/\\s+/g, \" \").trim();\n\treturn flat.length > limit ? `${flat.slice(0, limit)}…` : flat;\n}\n\nfunction shortDate(iso: string | undefined): string {\n\tif (!iso) return \"unknown\";\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? \"unknown\" : date.toISOString().slice(0, 10);\n}\n\nfunction evidence(count: number, sessions: number, lastSeen: string): string {\n\tconst times = count === 1 ? \"once\" : `${count}x`;\n\tconst where = sessions === 1 ? \"1 session\" : `${sessions} sessions`;\n\treturn `${times} across ${where}, last ${shortDate(lastSeen)}`;\n}\n\n/** True when there is nothing worth asking the model to look at. */\nexport function isEmptyDigest(digest: LearnDigest): boolean {\n\treturn digest.directives.length === 0 && digest.fixes.length === 0 && digest.requests.length === 0;\n}\n\n/**\n * Render the audit findings as a message the model can act on.\n *\n * Deliberately framed as questions rather than verdicts. The checker is\n * deterministic and therefore confident, but \"this path does not resolve\" is\n * not the same claim as \"this line is wrong\" — a context file may name a\n * location the tool reads at runtime, or one that belongs to another checkout.\n * Roughly a third of findings on a real file are of that kind, so the message\n * that carries them has to ask for verification, not authorise a sweep.\n */\nexport function renderAuditReport(report: AuditReport): string {\n\tconst lines: string[] = [];\n\tconst cost = staleTokens(report);\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Audited ${report.files.length} context file(s) — ${report.checked} referent(s) checked ` +\n\t\t\t`against the filesystem, ${report.stale.length} did not resolve (~${cost} tokens of always-loaded context).`,\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"This check is deterministic: it resolved every backticked path and `run` script named by the context files \" +\n\t\t\t\"below against this working tree and every package root in it. It costs no model calls and knows nothing \" +\n\t\t\t\"about intent.\",\n\t);\n\tlines.push(\"\");\n\n\tfor (const item of report.stale) {\n\t\tlines.push(`- \\`${item.referent}\\` — ${item.file}:${item.line}, ~${item.tokens} tokens`);\n\t\tlines.push(` - line: ${item.lineText.slice(0, 200)}`);\n\t}\n\tlines.push(\"\");\n\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Each entry is a candidate, not a verdict. For each one, check the repo before touching the line — \" +\n\t\t\t\"`git log` for a file that moved or was deleted is usually enough to tell which case you are in:\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"1. **The referent moved.** Fix the path in place. Do not delete the rule; it is still true.\");\n\tlines.push(\n\t\t\"2. **The referent is gone and the rule went with it.** Delete the line, and the surrounding section if \" +\n\t\t\t\"nothing in it survives. This is the case worth the most — it is always-loaded context describing \" +\n\t\t\t\"something that cannot happen.\",\n\t);\n\tlines.push(\n\t\t\"3. **The path is a runtime or optional location** the tool reads if it happens to exist, or a file in \" +\n\t\t\t\"another checkout. Nothing is wrong; leave it alone and say so.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Report the token delta of what you remove. Do not add anything — this pass is subtractive, and it is the \" +\n\t\t\t\"only one that moves the per-request cost down.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function renderLearnDigest(\n\tdigest: LearnDigest,\n\toptions: { userScopePath: string; mode?: \"incremental\" | \"all\" },\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Mined ${digest.scannedSessions} session(s) in this directory` +\n\t\t\t(digest.skippedSessions > 0 ? ` (${digest.skippedSessions} skipped: out of window or unreadable)` : \"\") +\n\t\t\t(digest.oldestSession ? `, ${shortDate(digest.oldestSession)} to ${shortDate(digest.newestSession)}` : \"\") +\n\t\t\t(digest.suppressed > 0 ? `. ${digest.suppressed} item(s) held back — already shown and unchanged since` : \"\") +\n\t\t\t// A cut item cleared every bar and lost on rank. Saying so is the\n\t\t\t// difference between \"this is everything\" and \"this is the top of a list\".\n\t\t\t(digest.cut > 0 ? `. ${digest.cut} more cleared the bar but were cut to fit the per-run cap` : \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\n\t\t`Read ${digest.funnel.candidates} occurrence(s), which named ${digest.funnel.points} distinct point(s); ` +\n\t\t\t`${digest.funnel.belowThreshold} did not recur in enough separate sessions to be proposed.`,\n\t);\n\t// Naming the mode keeps two very different empty results from reading alike:\n\t// \"nothing new since last time\" and \"nothing here at all\" are not the same\n\t// answer, and the reader cannot tell them apart from the counts.\n\tif (options.mode === \"all\") {\n\t\tlines.push(\"Mode: all — suppression is off, so items you have already seen and decided on are included.\");\n\t}\n\t// The model reads every transcript in full, which costs real tokens. Saying\n\t// what was re-read versus reused keeps that price visible rather than hidden.\n\tlines.push(\n\t\t`Read by the model this run: ${digest.mining.mined}; reused from cache: ${digest.mining.cached}` +\n\t\t\t(digest.mining.failed > 0\n\t\t\t\t? `; failed: ${digest.mining.failed} (their signals are missing from the counts below)`\n\t\t\t\t: \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"The counts below are computed from session transcripts on disk, not from this conversation. \" +\n\t\t\t\"Treat them as evidence, not conclusions — your job is to decide what deserves to be written down, \" +\n\t\t\t\"phrase it, and put it in the right place.\",\n\t);\n\tlines.push(\"\");\n\n\t// ── Directives ───────────────────────────────────────────────────────────\n\tif (digest.directives.length > 0) {\n\t\tlines.push(\"## Directives you have repeated\");\n\t\tlines.push(\"\");\n\t\tfor (const cluster of digest.directives) {\n\t\t\tlines.push(`- **${cluster.status}** — \"${quote(cluster.text, DIRECTIVE_QUOTE_CHARS)}\"`);\n\t\t\tlines.push(` - ${evidence(cluster.count, cluster.sessions, cluster.lastSeen)}`);\n\t\t\t// Occurrences were grouped by meaning, not by wording, so the quote above\n\t\t\t// is one phrasing of several. Naming the shared point keeps a count of 5\n\t\t\t// from looking like five copies of one sentence.\n\t\t\tlines.push(` - grouped as: ${cluster.label}`);\n\t\t\tif (cluster.rationale) {\n\t\t\t\tlines.push(` - why it may be durable: ${cluster.rationale}`);\n\t\t\t}\n\t\t\tif (cluster.existingRule) {\n\t\t\t\tlines.push(` - already covered by: \"${cluster.existingRule.slice(0, 160)}\"`);\n\t\t\t}\n\t\t\tif (cluster.existingSkill) {\n\t\t\t\tlines.push(` - already covered by the \\`${cluster.existingSkill}\\` skill`);\n\t\t\t}\n\t\t\tif (cluster.previouslyDeclined) {\n\t\t\t\tlines.push(\" - proposed before and not written down — you have already passed on this once\");\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Fixes ────────────────────────────────────────────────────────────────\n\tif (digest.fixes.length > 0) {\n\t\tlines.push(\"## Failures you resolved\");\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\t\"Each is a command that failed and later succeeded, where something done in between was the fix. \" +\n\t\t\t\t\"Recurring ones are worth writing down; a one-off is not.\",\n\t\t);\n\t\tlines.push(\"\");\n\t\tfor (const fix of digest.fixes) {\n\t\t\tlines.push(`- \\`${fix.command}\\` — ${evidence(fix.count, fix.sessions, fix.lastSeen)}`);\n\t\t\tlines.push(` - grouped as: ${fix.label}`);\n\t\t\t// The excerpt comes from the model now, which may not have quoted one.\n\t\t\tif (fix.errorExcerpt) {\n\t\t\t\tlines.push(` - error: ${fix.errorExcerpt}`);\n\t\t\t}\n\t\t\tif (fix.interveningCommands.length > 0) {\n\t\t\t\tlines.push(` - commands in between: ${fix.interveningCommands.map((c) => `\\`${c}\\``).join(\", \")}`);\n\t\t\t}\n\t\t\tif (fix.editedFiles.length > 0) {\n\t\t\t\tlines.push(` - files edited: ${fix.editedFiles.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Requests ────────────────────────────────────────────────────────────\n\tif (digest.requests.length > 0) {\n\t\tlines.push(\"## Work you keep asking for by name\");\n\t\tlines.push(\"\");\n\t\tfor (const request of digest.requests) {\n\t\t\t// Flattened and capped, unlike a directive quote. A request *is* a whole\n\t\t\t// task message — a slash-command body runs to thousands of characters — so\n\t\t\t// eight of them rendered raw would swamp the digest and a multi-line one\n\t\t\t// would break the list it sits in.\n\t\t\tlines.push(`- **${request.label}** — \"${quote(request.text, REQUEST_QUOTE_CHARS)}\"`);\n\t\t\tlines.push(` - ${evidence(request.count, request.sessions, request.lastSeen)}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Instructions ─────────────────────────────────────────────────────────\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\"Work through the items above and propose concrete edits. For each one, decide:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"1. **Is it durable?** A rule that will still be true next month belongs somewhere. A one-off preference \" +\n\t\t\t\"about the task you happened to be doing does not. When in doubt, drop it — a wrong rule costs more than \" +\n\t\t\t\"a missing one, because it is paid on every request forever.\",\n\t);\n\tlines.push(\n\t\t\"2. **Rule, skill, or slash command?** This is the most important call, and it is a cost question. A \" +\n\t\t\t\"context file is loaded on **every** turn; a skill's description is always loaded but its body only on \" +\n\t\t\t\"demand; a slash command costs nothing until it is invoked.\",\n\t);\n\tlines.push(\n\t\t\" - **Rule** — short, always true, unconditional. One line in a context file. Highest bar, because it is \" +\n\t\t\t\"paid on every request forever whether or not it is relevant.\",\n\t);\n\tlines.push(\n\t\t' - **Skill** — long, procedural, or conditional; anything shaped \"when X, do Y\"; a runbook or a sequence ' +\n\t\t\t\"of steps. Write `.agents/skills/<name>/SKILL.md`, and spend the effort on the `description` \" +\n\t\t\t\"frontmatter: it is the only part always in context, and it decides whether the skill ever fires.\",\n\t);\n\tlines.push(\n\t\t\" - **Slash command** — a *job you keep asking for*, not a rule about how work is done. The items under \" +\n\t\t\t'\"Work you keep asking for by name\" are these. Write `.agents/commands/<name>.md`, with `$1`/`$ARGUMENTS` ' +\n\t\t\t\"where the request varies. The cheapest artifact there is: nothing is loaded until you type it.\",\n\t);\n\tlines.push(\n\t\t`3. **Which scope?** Project-specific (this repo's tests, build, architecture, conventions) → the repo ` +\n\t\t\t`\\`AGENTS.md\\`. Personal habits that travel with you across every repo (style preferences, how you like ` +\n\t\t\t`commits written) → \\`${options.userScopePath}\\`. If it names this repo's files or commands, it is not a ` +\n\t\t\t`user-scope rule.`,\n\t);\n\tlines.push(\n\t\t\"4. **Restated items are rewrites, not additions.** An item marked `restated` is already covered by a rule \" +\n\t\t\t\"that is not working — too vague, buried, or contradicted elsewhere. Rewrite the existing line or delete \" +\n\t\t\t\"it in favour of a sharper one. Do not add a second rule saying the same thing.\",\n\t);\n\tlines.push(\n\t\t\"5. **`has-skill` items are a triggering problem, not a missing rule.** A skill already covers it and you \" +\n\t\t\t\"asked by hand anyway, which usually means the skill's `description` frontmatter does not describe the \" +\n\t\t\t\"situation you were in. Sharpen that description so it matches, rather than adding a rule that duplicates \" +\n\t\t\t\"what the skill already does.\",\n\t);\n\tlines.push(\n\t\t\"6. **Write local files, not a plugin.** Skills and commands proposed from this evidence are local habits: \" +\n\t\t\t\"write them under `.agents/`. `ProposePlugin` packages something already proven useful into a portable, \" +\n\t\t\t\"publishable artifact — a later step for a skill that has earned it, not the way to create one. Never \" +\n\t\t\t\"propose a hook or an MCP server from this evidence: it records what was said and what failed, which is \" +\n\t\t\t\"far too weak a warrant for anything that executes.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"Then, while you have the file open, audit it:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"- **Delete rules that no longer match the code.** Check a sample against the repo before trusting them.\",\n\t);\n\tlines.push(\"- **Delete rules that restate default behaviour.** Guidance the agent already follows is pure cost.\");\n\tlines.push(\n\t\t\"- **Collapse duplicates**, including any rule stated at both repo and user scope — that one is paid twice.\",\n\t);\n\tlines.push(\n\t\t\"- **One line per rule.** No rationale, no examples, no preamble, unless the example *is* the rule. Prose is \" +\n\t\t\t\"the single biggest source of context-file bloat.\",\n\t);\n\tlines.push(\"\");\n\n\tif (digest.agentsFilePath) {\n\t\tlines.push(\n\t\t\t`The repo context file is \\`${digest.agentsFilePath}\\`` +\n\t\t\t\t(digest.agentsFileTokens ? ` (~${digest.agentsFileTokens} tokens, re-sent every request)` : \"\") +\n\t\t\t\t\". Report the token delta of your proposed changes before applying them; a net reduction is a good outcome.\",\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"No repo context file exists yet. Create one only if at least one durable project rule survives step 1.\",\n\t\t);\n\t}\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Show what you propose, then apply it with edits — do not ask a separate approval question first, the edit \" +\n\t\t\t\"prompt is the approval. If nothing here is worth writing down, say so plainly and change nothing.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -8,7 +8,19 @@
8
8
  * because "said in 5 of your last 12 sessions" is a decision the reader can
9
9
  * make in one keystroke, where "extracted from your session" is not.
10
10
  */
11
+ import { staleTokens } from "./audit.js";
11
12
  import { LEARN_DIGEST_MARKER } from "./extract.js";
13
+ /**
14
+ * Quote lengths. A directive is a sentence; a request is a whole task message,
15
+ * and can be a slash-command body running to thousands of characters.
16
+ */
17
+ const DIRECTIVE_QUOTE_CHARS = 400;
18
+ const REQUEST_QUOTE_CHARS = 200;
19
+ /** One line, bounded — a quote has to survive being rendered inside a list item. */
20
+ function quote(text, limit) {
21
+ const flat = text.replace(/\s+/g, " ").trim();
22
+ return flat.length > limit ? `${flat.slice(0, limit)}…` : flat;
23
+ }
12
24
  function shortDate(iso) {
13
25
  if (!iso)
14
26
  return "unknown";
@@ -22,7 +34,48 @@ function evidence(count, sessions, lastSeen) {
22
34
  }
23
35
  /** True when there is nothing worth asking the model to look at. */
24
36
  export function isEmptyDigest(digest) {
25
- return digest.directives.length === 0 && digest.fixes.length === 0 && digest.workflows.length === 0;
37
+ return digest.directives.length === 0 && digest.fixes.length === 0 && digest.requests.length === 0;
38
+ }
39
+ /**
40
+ * Render the audit findings as a message the model can act on.
41
+ *
42
+ * Deliberately framed as questions rather than verdicts. The checker is
43
+ * deterministic and therefore confident, but "this path does not resolve" is
44
+ * not the same claim as "this line is wrong" — a context file may name a
45
+ * location the tool reads at runtime, or one that belongs to another checkout.
46
+ * Roughly a third of findings on a real file are of that kind, so the message
47
+ * that carries them has to ask for verification, not authorise a sweep.
48
+ */
49
+ export function renderAuditReport(report) {
50
+ const lines = [];
51
+ const cost = staleTokens(report);
52
+ lines.push(`${LEARN_DIGEST_MARKER} Audited ${report.files.length} context file(s) — ${report.checked} referent(s) checked ` +
53
+ `against the filesystem, ${report.stale.length} did not resolve (~${cost} tokens of always-loaded context).`);
54
+ lines.push("");
55
+ lines.push("This check is deterministic: it resolved every backticked path and `run` script named by the context files " +
56
+ "below against this working tree and every package root in it. It costs no model calls and knows nothing " +
57
+ "about intent.");
58
+ lines.push("");
59
+ for (const item of report.stale) {
60
+ lines.push(`- \`${item.referent}\` — ${item.file}:${item.line}, ~${item.tokens} tokens`);
61
+ lines.push(` - line: ${item.lineText.slice(0, 200)}`);
62
+ }
63
+ lines.push("");
64
+ lines.push("## What to do");
65
+ lines.push("");
66
+ lines.push("Each entry is a candidate, not a verdict. For each one, check the repo before touching the line — " +
67
+ "`git log` for a file that moved or was deleted is usually enough to tell which case you are in:");
68
+ lines.push("");
69
+ lines.push("1. **The referent moved.** Fix the path in place. Do not delete the rule; it is still true.");
70
+ lines.push("2. **The referent is gone and the rule went with it.** Delete the line, and the surrounding section if " +
71
+ "nothing in it survives. This is the case worth the most — it is always-loaded context describing " +
72
+ "something that cannot happen.");
73
+ lines.push("3. **The path is a runtime or optional location** the tool reads if it happens to exist, or a file in " +
74
+ "another checkout. Nothing is wrong; leave it alone and say so.");
75
+ lines.push("");
76
+ lines.push("Report the token delta of what you remove. Do not add anything — this pass is subtractive, and it is the " +
77
+ "only one that moves the per-request cost down.");
78
+ return lines.join("\n");
26
79
  }
27
80
  export function renderLearnDigest(digest, options) {
28
81
  const lines = [];
@@ -30,6 +83,24 @@ export function renderLearnDigest(digest, options) {
30
83
  (digest.skippedSessions > 0 ? ` (${digest.skippedSessions} skipped: out of window or unreadable)` : "") +
31
84
  (digest.oldestSession ? `, ${shortDate(digest.oldestSession)} to ${shortDate(digest.newestSession)}` : "") +
32
85
  (digest.suppressed > 0 ? `. ${digest.suppressed} item(s) held back — already shown and unchanged since` : "") +
86
+ // A cut item cleared every bar and lost on rank. Saying so is the
87
+ // difference between "this is everything" and "this is the top of a list".
88
+ (digest.cut > 0 ? `. ${digest.cut} more cleared the bar but were cut to fit the per-run cap` : "") +
89
+ ".");
90
+ lines.push(`Read ${digest.funnel.candidates} occurrence(s), which named ${digest.funnel.points} distinct point(s); ` +
91
+ `${digest.funnel.belowThreshold} did not recur in enough separate sessions to be proposed.`);
92
+ // Naming the mode keeps two very different empty results from reading alike:
93
+ // "nothing new since last time" and "nothing here at all" are not the same
94
+ // answer, and the reader cannot tell them apart from the counts.
95
+ if (options.mode === "all") {
96
+ lines.push("Mode: all — suppression is off, so items you have already seen and decided on are included.");
97
+ }
98
+ // The model reads every transcript in full, which costs real tokens. Saying
99
+ // what was re-read versus reused keeps that price visible rather than hidden.
100
+ lines.push(`Read by the model this run: ${digest.mining.mined}; reused from cache: ${digest.mining.cached}` +
101
+ (digest.mining.failed > 0
102
+ ? `; failed: ${digest.mining.failed} (their signals are missing from the counts below)`
103
+ : "") +
33
104
  ".");
34
105
  lines.push("");
35
106
  lines.push("The counts below are computed from session transcripts on disk, not from this conversation. " +
@@ -41,8 +112,15 @@ export function renderLearnDigest(digest, options) {
41
112
  lines.push("## Directives you have repeated");
42
113
  lines.push("");
43
114
  for (const cluster of digest.directives) {
44
- lines.push(`- **${cluster.status}** — "${cluster.text.replace(/\s+/g, " ").trim()}"`);
115
+ lines.push(`- **${cluster.status}** — "${quote(cluster.text, DIRECTIVE_QUOTE_CHARS)}"`);
45
116
  lines.push(` - ${evidence(cluster.count, cluster.sessions, cluster.lastSeen)}`);
117
+ // Occurrences were grouped by meaning, not by wording, so the quote above
118
+ // is one phrasing of several. Naming the shared point keeps a count of 5
119
+ // from looking like five copies of one sentence.
120
+ lines.push(` - grouped as: ${cluster.label}`);
121
+ if (cluster.rationale) {
122
+ lines.push(` - why it may be durable: ${cluster.rationale}`);
123
+ }
46
124
  if (cluster.existingRule) {
47
125
  lines.push(` - already covered by: "${cluster.existingRule.slice(0, 160)}"`);
48
126
  }
@@ -59,12 +137,16 @@ export function renderLearnDigest(digest, options) {
59
137
  if (digest.fixes.length > 0) {
60
138
  lines.push("## Failures you resolved");
61
139
  lines.push("");
62
- lines.push("Each is a command that failed, then later succeeded unchanged after intervening work " +
63
- "so something in between was the fix.");
140
+ lines.push("Each is a command that failed and later succeeded, where something done in between was the fix. " +
141
+ "Recurring ones are worth writing down; a one-off is not.");
64
142
  lines.push("");
65
143
  for (const fix of digest.fixes) {
66
144
  lines.push(`- \`${fix.command}\` — ${evidence(fix.count, fix.sessions, fix.lastSeen)}`);
67
- lines.push(` - error: ${fix.errorExcerpt}`);
145
+ lines.push(` - grouped as: ${fix.label}`);
146
+ // The excerpt comes from the model now, which may not have quoted one.
147
+ if (fix.errorExcerpt) {
148
+ lines.push(` - error: ${fix.errorExcerpt}`);
149
+ }
68
150
  if (fix.interveningCommands.length > 0) {
69
151
  lines.push(` - commands in between: ${fix.interveningCommands.map((c) => `\`${c}\``).join(", ")}`);
70
152
  }
@@ -74,12 +156,17 @@ export function renderLearnDigest(digest, options) {
74
156
  }
75
157
  lines.push("");
76
158
  }
77
- // ── Workflows ────────────────────────────────────────────────────────────
78
- if (digest.workflows.length > 0) {
79
- lines.push("## Repeated tool sequences");
159
+ // ── Requests ────────────────────────────────────────────────────────────
160
+ if (digest.requests.length > 0) {
161
+ lines.push("## Work you keep asking for by name");
80
162
  lines.push("");
81
- for (const workflow of digest.workflows) {
82
- lines.push(`- \`${workflow.steps.join(" ")}\` ${evidence(workflow.count, workflow.sessions, workflow.lastSeen)}`);
163
+ for (const request of digest.requests) {
164
+ // Flattened and capped, unlike a directive quote. A request *is* a whole
165
+ // task message — a slash-command body runs to thousands of characters — so
166
+ // eight of them rendered raw would swamp the digest and a multi-line one
167
+ // would break the list it sits in.
168
+ lines.push(`- **${request.label}** — "${quote(request.text, REQUEST_QUOTE_CHARS)}"`);
169
+ lines.push(` - ${evidence(request.count, request.sessions, request.lastSeen)}`);
83
170
  }
84
171
  lines.push("");
85
172
  }
@@ -91,10 +178,17 @@ export function renderLearnDigest(digest, options) {
91
178
  lines.push("1. **Is it durable?** A rule that will still be true next month belongs somewhere. A one-off preference " +
92
179
  "about the task you happened to be doing does not. When in doubt, drop it — a wrong rule costs more than " +
93
180
  "a missing one, because it is paid on every request forever.");
94
- lines.push("2. **Rule or skill?** This is the most important call. A context file is loaded on **every** turn; a skill " +
95
- "is loaded **on demand**. So: short, always-true, unconditional a one-line rule. Long, procedural, " +
96
- 'or conditional (a sequence of steps, a runbook, anything starting "when X, do Y") → a skill, not a rule. ' +
97
- "Repeated tool sequences are almost always skills.");
181
+ lines.push("2. **Rule, skill, or slash command?** This is the most important call, and it is a cost question. A " +
182
+ "context file is loaded on **every** turn; a skill's description is always loaded but its body only on " +
183
+ "demand; a slash command costs nothing until it is invoked.");
184
+ lines.push(" - **Rule** short, always true, unconditional. One line in a context file. Highest bar, because it is " +
185
+ "paid on every request forever whether or not it is relevant.");
186
+ lines.push(' - **Skill** — long, procedural, or conditional; anything shaped "when X, do Y"; a runbook or a sequence ' +
187
+ "of steps. Write `.agents/skills/<name>/SKILL.md`, and spend the effort on the `description` " +
188
+ "frontmatter: it is the only part always in context, and it decides whether the skill ever fires.");
189
+ lines.push(" - **Slash command** — a *job you keep asking for*, not a rule about how work is done. The items under " +
190
+ '"Work you keep asking for by name" are these. Write `.agents/commands/<name>.md`, with `$1`/`$ARGUMENTS` ' +
191
+ "where the request varies. The cheapest artifact there is: nothing is loaded until you type it.");
98
192
  lines.push(`3. **Which scope?** Project-specific (this repo's tests, build, architecture, conventions) → the repo ` +
99
193
  `\`AGENTS.md\`. Personal habits that travel with you across every repo (style preferences, how you like ` +
100
194
  `commits written) → \`${options.userScopePath}\`. If it names this repo's files or commands, it is not a ` +
@@ -106,6 +200,11 @@ export function renderLearnDigest(digest, options) {
106
200
  "asked by hand anyway, which usually means the skill's `description` frontmatter does not describe the " +
107
201
  "situation you were in. Sharpen that description so it matches, rather than adding a rule that duplicates " +
108
202
  "what the skill already does.");
203
+ lines.push("6. **Write local files, not a plugin.** Skills and commands proposed from this evidence are local habits: " +
204
+ "write them under `.agents/`. `ProposePlugin` packages something already proven useful into a portable, " +
205
+ "publishable artifact — a later step for a skill that has earned it, not the way to create one. Never " +
206
+ "propose a hook or an MCP server from this evidence: it records what was said and what failed, which is " +
207
+ "far too weak a warrant for anything that executes.");
109
208
  lines.push("");
110
209
  lines.push("Then, while you have the file open, audit it:");
111
210
  lines.push("");
@@ -1 +1 @@
1
- {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../../src/core/learn/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,SAAS,SAAS,CAAC,GAAuB,EAAU;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CAClF;AAED,SAAS,QAAQ,CAAC,KAAa,EAAE,QAAgB,EAAE,QAAgB,EAAU;IAC5E,MAAM,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,QAAQ,WAAW,CAAC;IACpE,OAAO,GAAG,KAAK,WAAW,KAAK,UAAU,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;AAAA,CAC/D;AAED,oEAAoE;AACpE,MAAM,UAAU,aAAa,CAAC,MAAmB,EAAW;IAC3D,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC;AAAA,CACpG;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAmB,EAAE,OAAkC,EAAU;IAClG,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CACT,GAAG,mBAAmB,UAAU,MAAM,CAAC,eAAe,+BAA+B;QACpF,CAAC,MAAM,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,eAAe,wCAAwC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1G,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,UAAU,0DAAwD,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7G,GAAG,CACJ,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACT,8FAA8F;QAC7F,sGAAoG;QACpG,2CAA2C,CAC5C,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,sMAA4E;IAC5E,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACzC,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,MAAM,WAAS,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACtF,KAAK,CAAC,IAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACjF,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;gBAC1B,KAAK,CAAC,IAAI,CAAC,4BAA4B,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/E,CAAC;YACD,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,gCAAgC,OAAO,CAAC,aAAa,UAAU,CAAC,CAAC;YAC7E,CAAC;YACD,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;gBAChC,KAAK,CAAC,IAAI,CAAC,mFAAiF,CAAC,CAAC;YAC/F,CAAC;QACF,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IAED,gNAA4E;IAC5E,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACT,2FAAyF;YACxF,sCAAsC,CACvC,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,UAAQ,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACxF,KAAK,CAAC,IAAI,CAAC,cAAc,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;YAC7C,IAAI,GAAG,CAAC,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,KAAK,CAAC,IAAI,CAAC,4BAA4B,GAAG,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACrG,CAAC;YACD,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,KAAK,CAAC,IAAI,CAAC,qBAAqB,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC/D,CAAC;QACF,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IAED,wMAA4E;IAC5E,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACzC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACzC,KAAK,CAAC,IAAI,CACT,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,OAAK,CAAC,UAAQ,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CACzG,CAAC;QACH,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IAED,kMAA4E;IAC5E,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAC;IAC7F,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACT,0GAA0G;QACzG,4GAA0G;QAC1G,6DAA6D,CAC9D,CAAC;IACF,KAAK,CAAC,IAAI,CACT,6GAA6G;QAC5G,wGAAsG;QACtG,6GAA2G;QAC3G,mDAAmD,CACpD,CAAC;IACF,KAAK,CAAC,IAAI,CACT,0GAAwG;QACvG,yGAAyG;QACzG,0BAAwB,OAAO,CAAC,aAAa,6DAA6D;QAC1G,kBAAkB,CACnB,CAAC;IACF,KAAK,CAAC,IAAI,CACT,4GAA4G;QAC3G,4GAA0G;QAC1G,gFAAgF,CACjF,CAAC;IACF,KAAK,CAAC,IAAI,CACT,2GAA2G;QAC1G,wGAAwG;QACxG,2GAA2G;QAC3G,8BAA8B,CAC/B,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;IAC5D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACT,yGAAyG,CACzG,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,qGAAqG,CAAC,CAAC;IAClH,KAAK,CAAC,IAAI,CACT,8GAA4G,CAC5G,CAAC;IACF,KAAK,CAAC,IAAI,CACT,8GAA8G;QAC7G,kDAAkD,CACnD,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CACT,8BAA8B,MAAM,CAAC,cAAc,IAAI;YACtD,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,gBAAgB,iCAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/F,4GAA4G,CAC7G,CAAC;IACH,CAAC;SAAM,CAAC;QACP,KAAK,CAAC,IAAI,CACT,wGAAwG,CACxG,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACT,8GAA4G;QAC3G,mGAAmG,CACpG,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB","sourcesContent":["/**\n * Renders the extractor's output into the message `/learn` injects.\n *\n * The digest is evidence plus instructions, and the split matters: the numbers\n * come from {@link extractLearnDigest} and are not negotiable, while everything\n * the model does with them — phrasing, routing, deciding a pattern is not worth\n * a rule — is judgement it has to exercise. Counts are printed on every item\n * because \"said in 5 of your last 12 sessions\" is a decision the reader can\n * make in one keystroke, where \"extracted from your session\" is not.\n */\n\nimport type { LearnDigest } from \"./extract.js\";\nimport { LEARN_DIGEST_MARKER } from \"./extract.js\";\n\nfunction shortDate(iso: string | undefined): string {\n\tif (!iso) return \"unknown\";\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? \"unknown\" : date.toISOString().slice(0, 10);\n}\n\nfunction evidence(count: number, sessions: number, lastSeen: string): string {\n\tconst times = count === 1 ? \"once\" : `${count}x`;\n\tconst where = sessions === 1 ? \"1 session\" : `${sessions} sessions`;\n\treturn `${times} across ${where}, last ${shortDate(lastSeen)}`;\n}\n\n/** True when there is nothing worth asking the model to look at. */\nexport function isEmptyDigest(digest: LearnDigest): boolean {\n\treturn digest.directives.length === 0 && digest.fixes.length === 0 && digest.workflows.length === 0;\n}\n\nexport function renderLearnDigest(digest: LearnDigest, options: { userScopePath: string }): string {\n\tconst lines: string[] = [];\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Mined ${digest.scannedSessions} session(s) in this directory` +\n\t\t\t(digest.skippedSessions > 0 ? ` (${digest.skippedSessions} skipped: out of window or unreadable)` : \"\") +\n\t\t\t(digest.oldestSession ? `, ${shortDate(digest.oldestSession)} to ${shortDate(digest.newestSession)}` : \"\") +\n\t\t\t(digest.suppressed > 0 ? `. ${digest.suppressed} item(s) held back — already shown and unchanged since` : \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"The counts below are computed from session transcripts on disk, not from this conversation. \" +\n\t\t\t\"Treat them as evidence, not conclusions — your job is to decide what deserves to be written down, \" +\n\t\t\t\"phrase it, and put it in the right place.\",\n\t);\n\tlines.push(\"\");\n\n\t// ── Directives ───────────────────────────────────────────────────────────\n\tif (digest.directives.length > 0) {\n\t\tlines.push(\"## Directives you have repeated\");\n\t\tlines.push(\"\");\n\t\tfor (const cluster of digest.directives) {\n\t\t\tlines.push(`- **${cluster.status}** — \"${cluster.text.replace(/\\s+/g, \" \").trim()}\"`);\n\t\t\tlines.push(` - ${evidence(cluster.count, cluster.sessions, cluster.lastSeen)}`);\n\t\t\tif (cluster.existingRule) {\n\t\t\t\tlines.push(` - already covered by: \"${cluster.existingRule.slice(0, 160)}\"`);\n\t\t\t}\n\t\t\tif (cluster.existingSkill) {\n\t\t\t\tlines.push(` - already covered by the \\`${cluster.existingSkill}\\` skill`);\n\t\t\t}\n\t\t\tif (cluster.previouslyDeclined) {\n\t\t\t\tlines.push(\" - proposed before and not written down — you have already passed on this once\");\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Fixes ────────────────────────────────────────────────────────────────\n\tif (digest.fixes.length > 0) {\n\t\tlines.push(\"## Failures you resolved\");\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\t\"Each is a command that failed, then later succeeded unchanged after intervening work — \" +\n\t\t\t\t\"so something in between was the fix.\",\n\t\t);\n\t\tlines.push(\"\");\n\t\tfor (const fix of digest.fixes) {\n\t\t\tlines.push(`- \\`${fix.command}\\` — ${evidence(fix.count, fix.sessions, fix.lastSeen)}`);\n\t\t\tlines.push(` - error: ${fix.errorExcerpt}`);\n\t\t\tif (fix.interveningCommands.length > 0) {\n\t\t\t\tlines.push(` - commands in between: ${fix.interveningCommands.map((c) => `\\`${c}\\``).join(\", \")}`);\n\t\t\t}\n\t\t\tif (fix.editedFiles.length > 0) {\n\t\t\t\tlines.push(` - files edited: ${fix.editedFiles.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Workflows ────────────────────────────────────────────────────────────\n\tif (digest.workflows.length > 0) {\n\t\tlines.push(\"## Repeated tool sequences\");\n\t\tlines.push(\"\");\n\t\tfor (const workflow of digest.workflows) {\n\t\t\tlines.push(\n\t\t\t\t`- \\`${workflow.steps.join(\" → \")}\\` — ${evidence(workflow.count, workflow.sessions, workflow.lastSeen)}`,\n\t\t\t);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Instructions ─────────────────────────────────────────────────────────\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\"Work through the items above and propose concrete edits. For each one, decide:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"1. **Is it durable?** A rule that will still be true next month belongs somewhere. A one-off preference \" +\n\t\t\t\"about the task you happened to be doing does not. When in doubt, drop it — a wrong rule costs more than \" +\n\t\t\t\"a missing one, because it is paid on every request forever.\",\n\t);\n\tlines.push(\n\t\t\"2. **Rule or skill?** This is the most important call. A context file is loaded on **every** turn; a skill \" +\n\t\t\t\"is loaded **on demand**. So: short, always-true, unconditional → a one-line rule. Long, procedural, \" +\n\t\t\t'or conditional (a sequence of steps, a runbook, anything starting \"when X, do Y\") → a skill, not a rule. ' +\n\t\t\t\"Repeated tool sequences are almost always skills.\",\n\t);\n\tlines.push(\n\t\t`3. **Which scope?** Project-specific (this repo's tests, build, architecture, conventions) → the repo ` +\n\t\t\t`\\`AGENTS.md\\`. Personal habits that travel with you across every repo (style preferences, how you like ` +\n\t\t\t`commits written) → \\`${options.userScopePath}\\`. If it names this repo's files or commands, it is not a ` +\n\t\t\t`user-scope rule.`,\n\t);\n\tlines.push(\n\t\t\"4. **Restated items are rewrites, not additions.** An item marked `restated` is already covered by a rule \" +\n\t\t\t\"that is not working — too vague, buried, or contradicted elsewhere. Rewrite the existing line or delete \" +\n\t\t\t\"it in favour of a sharper one. Do not add a second rule saying the same thing.\",\n\t);\n\tlines.push(\n\t\t\"5. **`has-skill` items are a triggering problem, not a missing rule.** A skill already covers it and you \" +\n\t\t\t\"asked by hand anyway, which usually means the skill's `description` frontmatter does not describe the \" +\n\t\t\t\"situation you were in. Sharpen that description so it matches, rather than adding a rule that duplicates \" +\n\t\t\t\"what the skill already does.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"Then, while you have the file open, audit it:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"- **Delete rules that no longer match the code.** Check a sample against the repo before trusting them.\",\n\t);\n\tlines.push(\"- **Delete rules that restate default behaviour.** Guidance the agent already follows is pure cost.\");\n\tlines.push(\n\t\t\"- **Collapse duplicates**, including any rule stated at both repo and user scope — that one is paid twice.\",\n\t);\n\tlines.push(\n\t\t\"- **One line per rule.** No rationale, no examples, no preamble, unless the example *is* the rule. Prose is \" +\n\t\t\t\"the single biggest source of context-file bloat.\",\n\t);\n\tlines.push(\"\");\n\n\tif (digest.agentsFilePath) {\n\t\tlines.push(\n\t\t\t`The repo context file is \\`${digest.agentsFilePath}\\`` +\n\t\t\t\t(digest.agentsFileTokens ? ` (~${digest.agentsFileTokens} tokens, re-sent every request)` : \"\") +\n\t\t\t\t\". Report the token delta of your proposed changes before applying them; a net reduction is a good outcome.\",\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"No repo context file exists yet. Create one only if at least one durable project rule survives step 1.\",\n\t\t);\n\t}\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Show what you propose, then apply it with edits — do not ask a separate approval question first, the edit \" +\n\t\t\t\"prompt is the approval. If nothing here is worth writing down, say so plainly and change nothing.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../../src/core/learn/digest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Renders the extractor's output into the message `/learn` injects.\n *\n * The digest is evidence plus instructions, and the split matters: the numbers\n * come from {@link extractLearnDigest} and are not negotiable, while everything\n * the model does with them — phrasing, routing, deciding a pattern is not worth\n * a rule — is judgement it has to exercise. Counts are printed on every item\n * because \"said in 5 of your last 12 sessions\" is a decision the reader can\n * make in one keystroke, where \"extracted from your session\" is not.\n */\n\nimport type { AuditReport } from \"./audit.js\";\nimport { staleTokens } from \"./audit.js\";\nimport type { LearnDigest } from \"./extract.js\";\nimport { LEARN_DIGEST_MARKER } from \"./extract.js\";\n\n/**\n * Quote lengths. A directive is a sentence; a request is a whole task message,\n * and can be a slash-command body running to thousands of characters.\n */\nconst DIRECTIVE_QUOTE_CHARS = 400;\nconst REQUEST_QUOTE_CHARS = 200;\n\n/** One line, bounded — a quote has to survive being rendered inside a list item. */\nfunction quote(text: string, limit: number): string {\n\tconst flat = text.replace(/\\s+/g, \" \").trim();\n\treturn flat.length > limit ? `${flat.slice(0, limit)}…` : flat;\n}\n\nfunction shortDate(iso: string | undefined): string {\n\tif (!iso) return \"unknown\";\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? \"unknown\" : date.toISOString().slice(0, 10);\n}\n\nfunction evidence(count: number, sessions: number, lastSeen: string): string {\n\tconst times = count === 1 ? \"once\" : `${count}x`;\n\tconst where = sessions === 1 ? \"1 session\" : `${sessions} sessions`;\n\treturn `${times} across ${where}, last ${shortDate(lastSeen)}`;\n}\n\n/** True when there is nothing worth asking the model to look at. */\nexport function isEmptyDigest(digest: LearnDigest): boolean {\n\treturn digest.directives.length === 0 && digest.fixes.length === 0 && digest.requests.length === 0;\n}\n\n/**\n * Render the audit findings as a message the model can act on.\n *\n * Deliberately framed as questions rather than verdicts. The checker is\n * deterministic and therefore confident, but \"this path does not resolve\" is\n * not the same claim as \"this line is wrong\" — a context file may name a\n * location the tool reads at runtime, or one that belongs to another checkout.\n * Roughly a third of findings on a real file are of that kind, so the message\n * that carries them has to ask for verification, not authorise a sweep.\n */\nexport function renderAuditReport(report: AuditReport): string {\n\tconst lines: string[] = [];\n\tconst cost = staleTokens(report);\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Audited ${report.files.length} context file(s) — ${report.checked} referent(s) checked ` +\n\t\t\t`against the filesystem, ${report.stale.length} did not resolve (~${cost} tokens of always-loaded context).`,\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"This check is deterministic: it resolved every backticked path and `run` script named by the context files \" +\n\t\t\t\"below against this working tree and every package root in it. It costs no model calls and knows nothing \" +\n\t\t\t\"about intent.\",\n\t);\n\tlines.push(\"\");\n\n\tfor (const item of report.stale) {\n\t\tlines.push(`- \\`${item.referent}\\` — ${item.file}:${item.line}, ~${item.tokens} tokens`);\n\t\tlines.push(` - line: ${item.lineText.slice(0, 200)}`);\n\t}\n\tlines.push(\"\");\n\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Each entry is a candidate, not a verdict. For each one, check the repo before touching the line — \" +\n\t\t\t\"`git log` for a file that moved or was deleted is usually enough to tell which case you are in:\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"1. **The referent moved.** Fix the path in place. Do not delete the rule; it is still true.\");\n\tlines.push(\n\t\t\"2. **The referent is gone and the rule went with it.** Delete the line, and the surrounding section if \" +\n\t\t\t\"nothing in it survives. This is the case worth the most — it is always-loaded context describing \" +\n\t\t\t\"something that cannot happen.\",\n\t);\n\tlines.push(\n\t\t\"3. **The path is a runtime or optional location** the tool reads if it happens to exist, or a file in \" +\n\t\t\t\"another checkout. Nothing is wrong; leave it alone and say so.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Report the token delta of what you remove. Do not add anything — this pass is subtractive, and it is the \" +\n\t\t\t\"only one that moves the per-request cost down.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function renderLearnDigest(\n\tdigest: LearnDigest,\n\toptions: { userScopePath: string; mode?: \"incremental\" | \"all\" },\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(\n\t\t`${LEARN_DIGEST_MARKER} Mined ${digest.scannedSessions} session(s) in this directory` +\n\t\t\t(digest.skippedSessions > 0 ? ` (${digest.skippedSessions} skipped: out of window or unreadable)` : \"\") +\n\t\t\t(digest.oldestSession ? `, ${shortDate(digest.oldestSession)} to ${shortDate(digest.newestSession)}` : \"\") +\n\t\t\t(digest.suppressed > 0 ? `. ${digest.suppressed} item(s) held back — already shown and unchanged since` : \"\") +\n\t\t\t// A cut item cleared every bar and lost on rank. Saying so is the\n\t\t\t// difference between \"this is everything\" and \"this is the top of a list\".\n\t\t\t(digest.cut > 0 ? `. ${digest.cut} more cleared the bar but were cut to fit the per-run cap` : \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\n\t\t`Read ${digest.funnel.candidates} occurrence(s), which named ${digest.funnel.points} distinct point(s); ` +\n\t\t\t`${digest.funnel.belowThreshold} did not recur in enough separate sessions to be proposed.`,\n\t);\n\t// Naming the mode keeps two very different empty results from reading alike:\n\t// \"nothing new since last time\" and \"nothing here at all\" are not the same\n\t// answer, and the reader cannot tell them apart from the counts.\n\tif (options.mode === \"all\") {\n\t\tlines.push(\"Mode: all — suppression is off, so items you have already seen and decided on are included.\");\n\t}\n\t// The model reads every transcript in full, which costs real tokens. Saying\n\t// what was re-read versus reused keeps that price visible rather than hidden.\n\tlines.push(\n\t\t`Read by the model this run: ${digest.mining.mined}; reused from cache: ${digest.mining.cached}` +\n\t\t\t(digest.mining.failed > 0\n\t\t\t\t? `; failed: ${digest.mining.failed} (their signals are missing from the counts below)`\n\t\t\t\t: \"\") +\n\t\t\t\".\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"The counts below are computed from session transcripts on disk, not from this conversation. \" +\n\t\t\t\"Treat them as evidence, not conclusions — your job is to decide what deserves to be written down, \" +\n\t\t\t\"phrase it, and put it in the right place.\",\n\t);\n\tlines.push(\"\");\n\n\t// ── Directives ───────────────────────────────────────────────────────────\n\tif (digest.directives.length > 0) {\n\t\tlines.push(\"## Directives you have repeated\");\n\t\tlines.push(\"\");\n\t\tfor (const cluster of digest.directives) {\n\t\t\tlines.push(`- **${cluster.status}** — \"${quote(cluster.text, DIRECTIVE_QUOTE_CHARS)}\"`);\n\t\t\tlines.push(` - ${evidence(cluster.count, cluster.sessions, cluster.lastSeen)}`);\n\t\t\t// Occurrences were grouped by meaning, not by wording, so the quote above\n\t\t\t// is one phrasing of several. Naming the shared point keeps a count of 5\n\t\t\t// from looking like five copies of one sentence.\n\t\t\tlines.push(` - grouped as: ${cluster.label}`);\n\t\t\tif (cluster.rationale) {\n\t\t\t\tlines.push(` - why it may be durable: ${cluster.rationale}`);\n\t\t\t}\n\t\t\tif (cluster.existingRule) {\n\t\t\t\tlines.push(` - already covered by: \"${cluster.existingRule.slice(0, 160)}\"`);\n\t\t\t}\n\t\t\tif (cluster.existingSkill) {\n\t\t\t\tlines.push(` - already covered by the \\`${cluster.existingSkill}\\` skill`);\n\t\t\t}\n\t\t\tif (cluster.previouslyDeclined) {\n\t\t\t\tlines.push(\" - proposed before and not written down — you have already passed on this once\");\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Fixes ────────────────────────────────────────────────────────────────\n\tif (digest.fixes.length > 0) {\n\t\tlines.push(\"## Failures you resolved\");\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\t\"Each is a command that failed and later succeeded, where something done in between was the fix. \" +\n\t\t\t\t\"Recurring ones are worth writing down; a one-off is not.\",\n\t\t);\n\t\tlines.push(\"\");\n\t\tfor (const fix of digest.fixes) {\n\t\t\tlines.push(`- \\`${fix.command}\\` — ${evidence(fix.count, fix.sessions, fix.lastSeen)}`);\n\t\t\tlines.push(` - grouped as: ${fix.label}`);\n\t\t\t// The excerpt comes from the model now, which may not have quoted one.\n\t\t\tif (fix.errorExcerpt) {\n\t\t\t\tlines.push(` - error: ${fix.errorExcerpt}`);\n\t\t\t}\n\t\t\tif (fix.interveningCommands.length > 0) {\n\t\t\t\tlines.push(` - commands in between: ${fix.interveningCommands.map((c) => `\\`${c}\\``).join(\", \")}`);\n\t\t\t}\n\t\t\tif (fix.editedFiles.length > 0) {\n\t\t\t\tlines.push(` - files edited: ${fix.editedFiles.join(\", \")}`);\n\t\t\t}\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Requests ────────────────────────────────────────────────────────────\n\tif (digest.requests.length > 0) {\n\t\tlines.push(\"## Work you keep asking for by name\");\n\t\tlines.push(\"\");\n\t\tfor (const request of digest.requests) {\n\t\t\t// Flattened and capped, unlike a directive quote. A request *is* a whole\n\t\t\t// task message — a slash-command body runs to thousands of characters — so\n\t\t\t// eight of them rendered raw would swamp the digest and a multi-line one\n\t\t\t// would break the list it sits in.\n\t\t\tlines.push(`- **${request.label}** — \"${quote(request.text, REQUEST_QUOTE_CHARS)}\"`);\n\t\t\tlines.push(` - ${evidence(request.count, request.sessions, request.lastSeen)}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\n\t// ── Instructions ─────────────────────────────────────────────────────────\n\tlines.push(\"## What to do\");\n\tlines.push(\"\");\n\tlines.push(\"Work through the items above and propose concrete edits. For each one, decide:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"1. **Is it durable?** A rule that will still be true next month belongs somewhere. A one-off preference \" +\n\t\t\t\"about the task you happened to be doing does not. When in doubt, drop it — a wrong rule costs more than \" +\n\t\t\t\"a missing one, because it is paid on every request forever.\",\n\t);\n\tlines.push(\n\t\t\"2. **Rule, skill, or slash command?** This is the most important call, and it is a cost question. A \" +\n\t\t\t\"context file is loaded on **every** turn; a skill's description is always loaded but its body only on \" +\n\t\t\t\"demand; a slash command costs nothing until it is invoked.\",\n\t);\n\tlines.push(\n\t\t\" - **Rule** — short, always true, unconditional. One line in a context file. Highest bar, because it is \" +\n\t\t\t\"paid on every request forever whether or not it is relevant.\",\n\t);\n\tlines.push(\n\t\t' - **Skill** — long, procedural, or conditional; anything shaped \"when X, do Y\"; a runbook or a sequence ' +\n\t\t\t\"of steps. Write `.agents/skills/<name>/SKILL.md`, and spend the effort on the `description` \" +\n\t\t\t\"frontmatter: it is the only part always in context, and it decides whether the skill ever fires.\",\n\t);\n\tlines.push(\n\t\t\" - **Slash command** — a *job you keep asking for*, not a rule about how work is done. The items under \" +\n\t\t\t'\"Work you keep asking for by name\" are these. Write `.agents/commands/<name>.md`, with `$1`/`$ARGUMENTS` ' +\n\t\t\t\"where the request varies. The cheapest artifact there is: nothing is loaded until you type it.\",\n\t);\n\tlines.push(\n\t\t`3. **Which scope?** Project-specific (this repo's tests, build, architecture, conventions) → the repo ` +\n\t\t\t`\\`AGENTS.md\\`. Personal habits that travel with you across every repo (style preferences, how you like ` +\n\t\t\t`commits written) → \\`${options.userScopePath}\\`. If it names this repo's files or commands, it is not a ` +\n\t\t\t`user-scope rule.`,\n\t);\n\tlines.push(\n\t\t\"4. **Restated items are rewrites, not additions.** An item marked `restated` is already covered by a rule \" +\n\t\t\t\"that is not working — too vague, buried, or contradicted elsewhere. Rewrite the existing line or delete \" +\n\t\t\t\"it in favour of a sharper one. Do not add a second rule saying the same thing.\",\n\t);\n\tlines.push(\n\t\t\"5. **`has-skill` items are a triggering problem, not a missing rule.** A skill already covers it and you \" +\n\t\t\t\"asked by hand anyway, which usually means the skill's `description` frontmatter does not describe the \" +\n\t\t\t\"situation you were in. Sharpen that description so it matches, rather than adding a rule that duplicates \" +\n\t\t\t\"what the skill already does.\",\n\t);\n\tlines.push(\n\t\t\"6. **Write local files, not a plugin.** Skills and commands proposed from this evidence are local habits: \" +\n\t\t\t\"write them under `.agents/`. `ProposePlugin` packages something already proven useful into a portable, \" +\n\t\t\t\"publishable artifact — a later step for a skill that has earned it, not the way to create one. Never \" +\n\t\t\t\"propose a hook or an MCP server from this evidence: it records what was said and what failed, which is \" +\n\t\t\t\"far too weak a warrant for anything that executes.\",\n\t);\n\tlines.push(\"\");\n\tlines.push(\"Then, while you have the file open, audit it:\");\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"- **Delete rules that no longer match the code.** Check a sample against the repo before trusting them.\",\n\t);\n\tlines.push(\"- **Delete rules that restate default behaviour.** Guidance the agent already follows is pure cost.\");\n\tlines.push(\n\t\t\"- **Collapse duplicates**, including any rule stated at both repo and user scope — that one is paid twice.\",\n\t);\n\tlines.push(\n\t\t\"- **One line per rule.** No rationale, no examples, no preamble, unless the example *is* the rule. Prose is \" +\n\t\t\t\"the single biggest source of context-file bloat.\",\n\t);\n\tlines.push(\"\");\n\n\tif (digest.agentsFilePath) {\n\t\tlines.push(\n\t\t\t`The repo context file is \\`${digest.agentsFilePath}\\`` +\n\t\t\t\t(digest.agentsFileTokens ? ` (~${digest.agentsFileTokens} tokens, re-sent every request)` : \"\") +\n\t\t\t\t\". Report the token delta of your proposed changes before applying them; a net reduction is a good outcome.\",\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"No repo context file exists yet. Create one only if at least one durable project rule survives step 1.\",\n\t\t);\n\t}\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Show what you propose, then apply it with edits — do not ask a separate approval question first, the edit \" +\n\t\t\t\"prompt is the approval. If nothing here is worth writing down, say so plainly and change nothing.\",\n\t);\n\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -8,82 +8,32 @@
8
8
  * whether something is a durable rule or a one-off, and it is the one thing a
9
9
  * prompt reading its own context cannot see.
10
10
  *
11
- * The split of labour is deliberate. This module is entirely deterministic: it
12
- * parses, filters, normalizes, counts and ranks. Judgement — is this a rule, how
13
- * should it be phrased, which scope owns it — belongs to the model reading the
14
- * digest, which is why the output carries evidence (counts, sessions, dates)
15
- * rather than conclusions.
16
- */
17
- import { type LearnState } from "./state.js";
18
- /**
19
- * Prefix on the message `/learn` injects. The digest is persisted like any user
20
- * turn, so without this marker the next `/learn` would mine its own output and
21
- * every proposal would compound its own count.
22
- */
23
- export declare const LEARN_DIGEST_MARKER = "[learn-digest]";
24
- /**
25
- * Where a repeated directive already lives, if anywhere.
11
+ * This module is the orchestrator, and the split of labour inside it is
12
+ * deliberate:
26
13
  *
27
- * A directive covered by a rule and said only once is simply dropped — the rule
28
- * exists and is working. What survives is one of three cases, and they want
29
- * different responses:
14
+ * - **Gathering** is deterministic. Finding session files, resolving which cwd
15
+ * they belong to, walking the active branch of a forked session all exact,
16
+ * all cheap, all here.
17
+ * - **Judgement** is the model's, in `mine.ts` and `coverage.ts`. What counts as
18
+ * a directive, what two phrasings have in common, whether a rule already
19
+ * covers something — none of that survives contact with a regex, and it used
20
+ * to be decided by one.
21
+ * - **Counting** is deterministic again, in `reduce.ts`. The number is the
22
+ * product, and a model asked to count over a long context will be
23
+ * approximately right.
30
24
  *
31
- * - `new` not written down anywhere. Propose it.
32
- * - `restated` a context-file rule covers it and you said it anyway, so the
33
- * rule is not working. Rewrite it; do not add a second one.
34
- * - `has-skill` — a *skill* covers it and you asked by hand anyway, which
35
- * usually means the skill's `description` is not triggering. Sharpen the
36
- * description rather than writing a rule that duplicates the skill.
25
+ * The expensive step is memoized per session file (`cache.ts`), so a session is
26
+ * read by the model exactly once in its life and the counts are still computed
27
+ * over every session in the window on every run.
37
28
  */
38
- export type DirectiveStatus = "new" | "restated" | "has-skill";
39
- /** Fields every proposable item shares, so suppression can be applied uniformly. */
40
- interface Proposable {
41
- /** Stable identity across runs what the state file remembers. */
42
- key: string;
43
- /** Newest occurrence in the window, ISO. */
44
- lastSeen: string;
45
- }
46
- export interface DirectiveCluster extends Proposable {
47
- /** Representative raw text, the longest seen in the cluster. */
48
- text: string;
49
- normalized: string;
50
- /** Total times said. */
51
- count: number;
52
- /** Distinct sessions it was said in — the stronger of the two counts. */
53
- sessions: number;
54
- status: DirectiveStatus;
55
- /** The existing rule line matched, when status is `restated`. */
56
- existingRule?: string;
57
- /** The skill that already covers this, when status is `has-skill`. */
58
- existingSkill?: string;
59
- /**
60
- * Shown before and still not written down anywhere — neither as a rule nor as
61
- * a skill — so you saw this proposal and passed on it. Only meaningful for
62
- * directives, which are the only items with a real coverage signal.
63
- */
64
- previouslyDeclined: boolean;
65
- }
66
- export interface FixCandidate extends Proposable {
67
- /** Normalized failing command. */
68
- command: string;
69
- /** Normalized error signature, the dedupe key. */
70
- signature: string;
71
- /** Short raw excerpt, so the model sees the real error text. */
72
- errorExcerpt: string;
73
- /** Commands run between the failure and the pass. */
74
- interveningCommands: string[];
75
- /** Files edited between the failure and the pass. */
76
- editedFiles: string[];
77
- /** Times this signature failed and was resolved across the window. */
78
- count: number;
79
- sessions: number;
80
- }
81
- export interface WorkflowCandidate extends Proposable {
82
- /** Tool-call signatures in order. */
83
- steps: string[];
84
- count: number;
85
- sessions: number;
86
- }
29
+ import type { Clusterer } from "./cluster.js";
30
+ import type { CoverageIndex, CoverageJudge } from "./coverage.js";
31
+ import type { Miner } from "./mine.js";
32
+ import type { DirectiveCluster, FixCandidate, RequestCandidate } from "./reduce.js";
33
+ import { type LearnState } from "./state.js";
34
+ export type { CoverageIndex, CoverageMatch } from "./coverage.js";
35
+ export { LEARN_DIGEST_MARKER } from "./mine.js";
36
+ export type { DirectiveCluster, DirectiveStatus, FixCandidate, RequestCandidate } from "./reduce.js";
87
37
  /**
88
38
  * Why a session file on disk did not make it into the digest.
89
39
  *
@@ -108,25 +58,72 @@ export interface SessionScanReport {
108
58
  /** Skipped for being unreadable, unparseable, or empty. */
109
59
  unreadable: number;
110
60
  }
61
+ /** What the run cost, so the price of an LLM-read pipeline is visible rather than hidden. */
62
+ export interface MiningReport {
63
+ /** Sessions whose candidates came from cache, free. */
64
+ cached: number;
65
+ /** Sessions sent to the model this run. */
66
+ mined: number;
67
+ /** Sessions the model failed on. Their signals are missing from the counts. */
68
+ failed: number;
69
+ }
111
70
  export interface LearnDigest {
112
71
  scannedSessions: number;
113
72
  skippedSessions: number;
114
73
  /** Where the sessions came from, and what was passed over. */
115
74
  scan: SessionScanReport;
75
+ /** What was read by the model versus reused. */
76
+ mining: MiningReport;
77
+ /**
78
+ * The run stopped before reading the whole window, so the counts below are
79
+ * computed from part of it. Callers must not record these as surfaced: a
80
+ * partial count can fall under the repeat threshold, and bookmarking it would
81
+ * hide the item on the next run, when the evidence is complete.
82
+ */
83
+ aborted: boolean;
84
+ /**
85
+ * The coverage judge failed, so every directive reads `new` whether or not it
86
+ * is written down. Callers must not record these as surfaced either: the
87
+ * bookmark stores whether an item was covered when shown, and a wrong `false`
88
+ * there tells a later run you passed over a proposal you were never given.
89
+ */
90
+ coverageFailed: boolean;
116
91
  oldestSession?: string;
117
92
  newestSession?: string;
118
93
  agentsFilePath?: string;
119
94
  agentsFileTokens?: number;
120
95
  directives: DirectiveCluster[];
121
96
  fixes: FixCandidate[];
122
- workflows: WorkflowCandidate[];
97
+ requests: RequestCandidate[];
123
98
  /** Items held back because nothing new has happened since they were last shown. */
124
99
  suppressed: number;
100
+ /** Items that cleared every threshold but lost the ranking to `maxProposals`. */
101
+ cut: number;
102
+ /**
103
+ * What the window contained before the thresholds, so an empty digest can be
104
+ * read.
105
+ *
106
+ * The pipeline filters hard — replayed slash-command bodies, tool output,
107
+ * quotes that cannot be found in the transcript, then a distinct-session bar
108
+ * — and every one of those is silent. Without these numbers "nothing to
109
+ * propose" is unreadable: it could mean the sessions taught nothing, or that
110
+ * the bar is one session too high, and the reader has no way to tell which
111
+ * knob to reach for.
112
+ */
113
+ funnel: {
114
+ /** Occurrences the miner reported and the quote check accepted. */
115
+ candidates: number;
116
+ /** Distinct points after naming — how much the clustering pass actually merged. */
117
+ points: number;
118
+ /** Points that were named and counted but did not clear the repeat threshold. */
119
+ belowThreshold: number;
120
+ };
125
121
  /** Everything this run put on screen, for the caller to persist. */
126
122
  surfaced: Array<{
127
123
  key: string;
128
124
  lastSeen: string;
129
125
  covered: boolean;
126
+ text?: string;
130
127
  }>;
131
128
  }
132
129
  export interface ExtractOptions {
@@ -142,8 +139,8 @@ export interface ExtractOptions {
142
139
  maxAgeDays?: number;
143
140
  /** Occurrences a directive needs before it is proposed. The signal/noise dial. */
144
141
  minRepeats?: number;
145
- /** Non-overlapping repeats a tool sequence needs before it is proposed as a skill. */
146
- minWorkflowRepeats?: number;
142
+ /** Repeats a tool sequence needs before it is proposed as a skill. */
143
+ minRequestRepeats?: number;
147
144
  /** Cap on each list in the digest. */
148
145
  maxProposals?: number;
149
146
  /**
@@ -164,6 +161,26 @@ export interface ExtractOptions {
164
161
  /** Injectable clock, for tests. */
165
162
  now?: Date;
166
163
  }
164
+ /** Everything the async pipeline needs beyond the window settings. */
165
+ export interface MineOptions extends ExtractOptions {
166
+ /** Reads one session and reports what it saw. */
167
+ miner: Miner;
168
+ /**
169
+ * Names the whole window at once, deciding which occurrences are the same
170
+ * point. Without one, each candidate is named after its own wording, which
171
+ * groups identical sentences and nothing else.
172
+ */
173
+ clusterer?: Clusterer;
174
+ /** Decides which proposals are already written down. Defaults to "none are". */
175
+ coverageJudge?: CoverageJudge;
176
+ /** Progress callback, so a cold-cache run is not a silent wait. */
177
+ onProgress?: (progress: {
178
+ done: number;
179
+ total: number;
180
+ cached: number;
181
+ }) => void;
182
+ signal?: AbortSignal;
183
+ }
167
184
  /**
168
185
  * Every directory this cwd's sessions could be sitting in.
169
186
  *
@@ -178,38 +195,25 @@ export interface ExtractOptions {
178
195
  export declare function candidateSessionDirs(options: Pick<ExtractOptions, "cwd" | "agentDir" | "sessionDir">): string[];
179
196
  /**
180
197
  * Where this cwd's sessions were found and what was passed over, without
181
- * ranking anything. `/learn stats` reports on the window without re-mining it.
198
+ * mining anything. `/learn settings` and `/learn stats` report on the window
199
+ * without paying for a model call.
182
200
  */
183
201
  export declare function scanSessions(options: ExtractOptions): SessionScanReport;
184
202
  /**
185
- * Everything a proposal could already have been written into.
186
- *
187
- * Built once and shared, because the same question — is this already written
188
- * down? — is asked while ranking a run *and* afterwards by `/learn stats`,
189
- * which reconstructs adoption by comparing coverage now against coverage when
190
- * the item was shown.
191
- */
192
- export interface CoverageIndex {
193
- /** Candidate rule lines from the repo context file and both user scopes. */
194
- ruleLines: string[];
195
- skills: Array<{
196
- name: string;
197
- description: string;
198
- }>;
199
- }
200
- export interface CoverageMatch {
201
- /** The context-file line that covers this, if any. */
202
- rule?: string;
203
- /** The skill that covers this, if any. Only set when no rule matched. */
204
- skill?: string;
205
- }
206
- /**
207
- * Where a piece of text is already written down, if anywhere.
203
+ * What a run would read, without reading it.
208
204
  *
209
- * A rule wins over a skill when both match: it is the more specific answer, and
210
- * "rewrite this line" is more actionable than "sharpen a description".
205
+ * Runs the real selection the same age, cwd, cap and de-duplication rules
206
+ * `mineLearnDigest` applies and then asks the cache about each survivor. It
207
+ * has to be the same selection: this number is what the confirmation prompt
208
+ * quotes, and a prompt that says twelve before reading three is worse than no
209
+ * prompt at all. Hashing the chosen files is cheap next to sending them to a
210
+ * model.
211
211
  */
212
- export declare function matchCoverage(text: string, index: CoverageIndex): CoverageMatch;
212
+ export declare function planMining(options: ExtractOptions): {
213
+ total: number;
214
+ cached: number;
215
+ pending: number;
216
+ };
213
217
  /** Assemble the coverage index for a directory. */
214
218
  export declare function buildCoverageIndex(options: {
215
219
  cwd: string;
@@ -220,6 +224,5 @@ export declare function buildCoverageIndex(options: {
220
224
  }>;
221
225
  }): CoverageIndex;
222
226
  /** Mine the recent sessions for this cwd and return the ranked digest. */
223
- export declare function extractLearnDigest(options: ExtractOptions): LearnDigest;
224
- export {};
227
+ export declare function mineLearnDigest(options: MineOptions): Promise<LearnDigest>;
225
228
  //# sourceMappingURL=extract.d.ts.map