@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.
- package/CHANGELOG.md +247 -0
- package/dist/core/learn/audit.d.ts +136 -0
- package/dist/core/learn/audit.d.ts.map +1 -0
- package/dist/core/learn/audit.js +316 -0
- package/dist/core/learn/audit.js.map +1 -0
- package/dist/core/learn/cache.d.ts +58 -0
- package/dist/core/learn/cache.d.ts.map +1 -0
- package/dist/core/learn/cache.js +132 -0
- package/dist/core/learn/cache.js.map +1 -0
- package/dist/core/learn/cluster.d.ts +78 -0
- package/dist/core/learn/cluster.d.ts.map +1 -0
- package/dist/core/learn/cluster.js +184 -0
- package/dist/core/learn/cluster.js.map +1 -0
- package/dist/core/learn/coverage.d.ts +58 -0
- package/dist/core/learn/coverage.d.ts.map +1 -0
- package/dist/core/learn/coverage.js +144 -0
- package/dist/core/learn/coverage.js.map +1 -0
- package/dist/core/learn/digest.d.ts +13 -0
- package/dist/core/learn/digest.d.ts.map +1 -1
- package/dist/core/learn/digest.js +113 -14
- package/dist/core/learn/digest.js.map +1 -1
- package/dist/core/learn/extract.d.ts +108 -105
- package/dist/core/learn/extract.d.ts.map +1 -1
- package/dist/core/learn/extract.js +308 -447
- package/dist/core/learn/extract.js.map +1 -1
- package/dist/core/learn/mine.d.ts +178 -0
- package/dist/core/learn/mine.d.ts.map +1 -0
- package/dist/core/learn/mine.js +390 -0
- package/dist/core/learn/mine.js.map +1 -0
- package/dist/core/learn/reduce.d.ts +89 -0
- package/dist/core/learn/reduce.d.ts.map +1 -0
- package/dist/core/learn/reduce.js +179 -0
- package/dist/core/learn/reduce.js.map +1 -0
- package/dist/core/learn/state.d.ts +19 -18
- package/dist/core/learn/state.d.ts.map +1 -1
- package/dist/core/learn/state.js +35 -31
- package/dist/core/learn/state.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/startup-progress.d.ts +12 -7
- package/dist/core/startup-progress.d.ts.map +1 -1
- package/dist/core/startup-progress.js +12 -7
- package/dist/core/startup-progress.js.map +1 -1
- package/dist/extensions/core/learn.d.ts +8 -4
- package/dist/extensions/core/learn.d.ts.map +1 -1
- package/dist/extensions/core/learn.js +292 -56
- package/dist/extensions/core/learn.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/progress-bar.d.ts +50 -0
- package/dist/modes/interactive/components/progress-bar.d.ts.map +1 -0
- package/dist/modes/interactive/components/progress-bar.js +77 -0
- package/dist/modes/interactive/components/progress-bar.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts +6 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +18 -14
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/docs/settings.md +9 -6
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/learn/normalize.d.ts +0 -65
- package/dist/core/learn/normalize.d.ts.map +0 -1
- package/dist/core/learn/normalize.js +0 -245
- 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.
|
|
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
|
|
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
|
|
63
|
-
"
|
|
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(` -
|
|
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
|
-
// ──
|
|
78
|
-
if (digest.
|
|
79
|
-
lines.push("##
|
|
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
|
|
82
|
-
|
|
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
|
|
95
|
-
"is loaded
|
|
96
|
-
|
|
97
|
-
|
|
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":"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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 { 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
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
146
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
210
|
-
*
|
|
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
|
|
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
|
|
224
|
-
export {};
|
|
227
|
+
export declare function mineLearnDigest(options: MineOptions): Promise<LearnDigest>;
|
|
225
228
|
//# sourceMappingURL=extract.d.ts.map
|