@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":"extract.js","sourceRoot":"","sources":["../../../src/core/learn/extract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session mining for `/learn`.\n *\n * Reads session `.jsonl` files straight off disk rather than the live context.\n * That is the whole point: the on-disk transcript is complete even when the\n * in-context one has been compacted away, and it spans every past session\n * instead of only this one. Cross-session repetition is the signal that decides\n * whether something is a durable rule or a one-off, and it is the one thing a\n * prompt reading its own context cannot see.\n *\n * The split of labour is deliberate. This module is entirely deterministic: it\n * parses, filters, normalizes, counts and ranks. Judgement — is this a rule, how\n * should it be phrased, which scope owns it — belongs to the model reading the\n * digest, which is why the output carries evidence (counts, sessions, dates)\n * rather than conclusions.\n */\n\nimport { existsSync, readdirSync, readFileSync, realpathSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve, sep } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent, ToolCall } from \"@kolisachint/hoocode-ai\";\nimport { getUserAgentsDir } from \"../../config.js\";\nimport { getSessionDirPath } from \"../session-manager.js\";\nimport { loadSkills } from \"../skills.js\";\nimport {\n\tcommandHead,\n\tcontentWords,\n\textractErrorRegion,\n\tisBenignFailure,\n\tisRuleShapedDirective,\n\tisUninformativeFailure,\n\tnormalizeCommand,\n\tnormalizeDirective,\n\tnormalizeErrorSignature,\n\twordOverlap,\n} from \"./normalize.js\";\nimport { judge, type LearnState } from \"./state.js\";\n\n/**\n * Prefix on the message `/learn` injects. The digest is persisted like any user\n * turn, so without this marker the next `/learn` would mine its own output and\n * every proposal would compound its own count.\n */\nexport const LEARN_DIGEST_MARKER = \"[learn-digest]\";\n\n/** Sessions considered, newest first. */\nconst DEFAULT_MAX_SESSIONS = 20;\n/** Sessions older than this are ignored — a pattern that stopped is not a rule. */\nconst DEFAULT_MAX_AGE_DAYS = 30;\n/** Entries parsed per session file, as a guard against pathological transcripts. */\nconst MAX_ENTRIES_PER_SESSION = 8000;\n/** Tool calls per session fed to the workflow detector. */\nconst MAX_TOOL_CALLS_PER_SESSION = 400;\n/** How far forward the fix extractor looks for the same command succeeding. */\nconst FIX_LOOKAHEAD = 40;\n/** Word overlap against an existing rule above which a directive counts as covered. */\nconst COVERED_OVERLAP = 0.6;\n/**\n * The same bar for skills, set higher on purpose.\n *\n * A rule is one line, so overlap against it is a sharp signal. A skill is a name\n * plus a description written to attract matches, which is a far larger haystack\n * — a short directive's words turn up in it by chance much more readily. The\n * higher bar and the truncation below keep \"you already have a skill for this\"\n * from being said on a coincidence.\n */\nconst SKILL_COVERED_OVERLAP = 0.75;\n/** Description characters considered. The opening says what a skill does; the rest is trigger bait. */\nconst SKILL_DESCRIPTION_CHARS = 300;\n/** Directives must reach this many occurrences to be reported at all. */\nconst DEFAULT_MIN_DIRECTIVE_COUNT = 2;\n/** Tool sequence lengths considered as workflow candidates. */\nconst WORKFLOW_MIN_LEN = 3;\nconst WORKFLOW_MAX_LEN = 5;\n/** Repeats before a tool sequence is worth proposing as a skill. */\nconst DEFAULT_MIN_WORKFLOW_COUNT = 3;\n/** Cap on each list in the digest, so the model's budget goes to the top signals. */\nconst DEFAULT_MAX_PER_CATEGORY = 8;\n\n/**\n * Where a repeated directive already lives, if anywhere.\n *\n * A directive covered by a rule and said only once is simply dropped — the rule\n * exists and is working. What survives is one of three cases, and they want\n * different responses:\n *\n * - `new` — not written down anywhere. Propose it.\n * - `restated` — a context-file rule covers it and you said it anyway, so the\n * rule is not working. Rewrite it; do not add a second one.\n * - `has-skill` — a *skill* covers it and you asked by hand anyway, which\n * usually means the skill's `description` is not triggering. Sharpen the\n * description rather than writing a rule that duplicates the skill.\n */\nexport type DirectiveStatus = \"new\" | \"restated\" | \"has-skill\";\n\n/** Fields every proposable item shares, so suppression can be applied uniformly. */\ninterface Proposable {\n\t/** Stable identity across runs — what the state file remembers. */\n\tkey: string;\n\t/** Newest occurrence in the window, ISO. */\n\tlastSeen: string;\n}\n\nexport interface DirectiveCluster extends Proposable {\n\t/** Representative raw text, the longest seen in the cluster. */\n\ttext: string;\n\tnormalized: string;\n\t/** Total times said. */\n\tcount: number;\n\t/** Distinct sessions it was said in — the stronger of the two counts. */\n\tsessions: number;\n\tstatus: DirectiveStatus;\n\t/** The existing rule line matched, when status is `restated`. */\n\texistingRule?: string;\n\t/** The skill that already covers this, when status is `has-skill`. */\n\texistingSkill?: string;\n\t/**\n\t * Shown before and still not written down anywhere — neither as a rule nor as\n\t * a skill — so you saw this proposal and passed on it. Only meaningful for\n\t * directives, which are the only items with a real coverage signal.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\nexport interface FixCandidate extends Proposable {\n\t/** Normalized failing command. */\n\tcommand: string;\n\t/** Normalized error signature, the dedupe key. */\n\tsignature: string;\n\t/** Short raw excerpt, so the model sees the real error text. */\n\terrorExcerpt: string;\n\t/** Commands run between the failure and the pass. */\n\tinterveningCommands: string[];\n\t/** Files edited between the failure and the pass. */\n\teditedFiles: string[];\n\t/** Times this signature failed and was resolved across the window. */\n\tcount: number;\n\tsessions: number;\n}\n\nexport interface WorkflowCandidate extends Proposable {\n\t/** Tool-call signatures in order. */\n\tsteps: string[];\n\tcount: number;\n\tsessions: number;\n}\n\n/**\n * Why a session file on disk did not make it into the digest.\n *\n * \"No recent sessions\" is the one outcome a user cannot act on without this:\n * an empty session directory, a directory full of month-old sessions, and a\n * directory full of sessions belonging to another checkout all produce the same\n * sentence, and the fix differs in each case.\n */\nexport interface SessionScanReport {\n\t/** Directories actually searched, in order. */\n\tdirs: string[];\n\t/** Directories that do not exist on disk. */\n\tmissingDirs: string[];\n\t/** `.jsonl` files found across all searched directories. */\n\tfiles: number;\n\t/** Skipped for being older than the age window. */\n\ttooOld: number;\n\t/** Skipped because the session header records a different working directory. */\n\totherCwd: number;\n\t/** Skipped for being beyond `maxSessions`. */\n\toverLimit: number;\n\t/** Skipped for being unreadable, unparseable, or empty. */\n\tunreadable: number;\n}\n\nexport interface LearnDigest {\n\tscannedSessions: number;\n\tskippedSessions: number;\n\t/** Where the sessions came from, and what was passed over. */\n\tscan: SessionScanReport;\n\toldestSession?: string;\n\tnewestSession?: string;\n\tagentsFilePath?: string;\n\tagentsFileTokens?: number;\n\tdirectives: DirectiveCluster[];\n\tfixes: FixCandidate[];\n\tworkflows: WorkflowCandidate[];\n\t/** Items held back because nothing new has happened since they were last shown. */\n\tsuppressed: number;\n\t/** Everything this run put on screen, for the caller to persist. */\n\tsurfaced: Array<{ key: string; lastSeen: string; covered: boolean }>;\n}\n\nexport interface ExtractOptions {\n\tcwd: string;\n\tagentDir: string;\n\t/**\n\t * An extra directory to scan, normally the live session manager's. The\n\t * per-cwd default directory is always scanned as well, so a session manager\n\t * pointing somewhere unusual cannot hide this directory's history.\n\t */\n\tsessionDir?: string;\n\tmaxSessions?: number;\n\tmaxAgeDays?: number;\n\t/** Occurrences a directive needs before it is proposed. The signal/noise dial. */\n\tminRepeats?: number;\n\t/** Non-overlapping repeats a tool sequence needs before it is proposed as a skill. */\n\tminWorkflowRepeats?: number;\n\t/** Cap on each list in the digest. */\n\tmaxProposals?: number;\n\t/**\n\t * What previous runs already showed. Items with no new occurrences since are\n\t * held back. Omit (or pass `ignoreState`) to propose everything in the window.\n\t */\n\tstate?: LearnState;\n\t/** Re-propose everything, ignoring what previous runs surfaced (`/learn all`). */\n\tignoreState?: boolean;\n\t/**\n\t * Skills a directive can already be covered by. Defaults to the ones loaded\n\t * from disk; injectable so tests do not read the developer's real skills.\n\t */\n\tskills?: Array<{ name: string; description: string }>;\n\t/** Injectable clock, for tests. */\n\tnow?: Date;\n}\n\ninterface SessionHeaderLike {\n\ttype: \"session\";\n\tid?: string;\n\ttimestamp?: string;\n\tcwd?: string;\n}\n\ninterface EntryLike {\n\ttype: string;\n\tid?: string;\n\tparentId?: string | null;\n\ttimestamp?: string;\n\tmessage?: AgentMessage;\n}\n\n/** One session, reduced to the branch that was actually taken. */\ninterface ParsedSession {\n\tfile: string;\n\tid: string;\n\ttimestamp: string;\n\tentries: EntryLike[];\n}\n\nfunction textOf(content: unknown): string {\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.map((block) =>\n\t\t\tblock && typeof block === \"object\" && (block as TextContent).type === \"text\"\n\t\t\t\t? ((block as TextContent).text ?? \"\")\n\t\t\t\t: \"\",\n\t\t)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isToolCall(block: unknown): block is ToolCall {\n\treturn !!block && typeof block === \"object\" && (block as ToolCall).type === \"toolCall\";\n}\n\n/**\n * Reduce a session's raw entries to the branch that was actually taken.\n *\n * Session files are trees — forks and clones append entries that were never\n * part of the same conversation. Walking parent links back from the last entry\n * keeps the extractor from stitching a \"fix\" out of two turns that never\n * happened in sequence. Sessions written before entry ids existed are flat, and\n * for those file order *is* the branch.\n */\nfunction activeBranch(entries: EntryLike[]): EntryLike[] {\n\tconst withIds = entries.filter((e) => typeof e.id === \"string\");\n\tif (withIds.length === 0) return entries;\n\n\tconst byId = new Map<string, EntryLike>();\n\tfor (const entry of withIds) byId.set(entry.id as string, entry);\n\n\tconst branch: EntryLike[] = [];\n\tconst seen = new Set<string>();\n\tlet cursor: EntryLike | undefined = withIds[withIds.length - 1];\n\twhile (cursor?.id && !seen.has(cursor.id)) {\n\t\tseen.add(cursor.id);\n\t\tbranch.push(cursor);\n\t\tcursor = cursor.parentId ? byId.get(cursor.parentId) : undefined;\n\t}\n\treturn branch.reverse();\n}\n\n/**\n * Compare two directory paths the way the filesystem does.\n *\n * A session header stores the cwd as it was typed, and the same directory can\n * be spelled several ways: through a symlink (`/tmp` is `/private/tmp` on\n * macOS), with a trailing separator, or in different case on the\n * case-insensitive filesystems that macOS and Windows ship by default. String\n * equality on `resolve()` alone rejects every one of those, and rejecting them\n * here means silently discarding the whole history the command exists to read.\n */\nfunction normalizeDirPath(path: string): string {\n\tlet resolved = resolve(path);\n\ttry {\n\t\tresolved = realpathSync.native(resolved);\n\t} catch {\n\t\t// Deleted or never-created directory: the textual form is all we have.\n\t}\n\t// `resolve` already drops a trailing separator except at a filesystem root,\n\t// where dropping it would turn \"/\" into \"\".\n\tif (resolved.length > 1 && resolved.endsWith(sep)) resolved = resolved.slice(0, -1);\n\treturn process.platform === \"win32\" || process.platform === \"darwin\" ? resolved.toLowerCase() : resolved;\n}\n\nfunction sameDirectory(a: string, b: string): boolean {\n\treturn normalizeDirPath(a) === normalizeDirPath(b);\n}\n\n/** Reason a candidate file produced no session, for the scan report. */\ntype SkipReason = \"otherCwd\" | \"unreadable\";\n\nfunction parseSessionFile(file: string, cwd: string, onSkip: (reason: SkipReason) => void): ParsedSession | undefined {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(file, \"utf-8\");\n\t} catch {\n\t\tonSkip(\"unreadable\");\n\t\treturn undefined;\n\t}\n\n\tconst lines = raw.split(\"\\n\");\n\tlet header: SessionHeaderLike | undefined;\n\tconst entries: EntryLike[] = [];\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\tif (entries.length >= MAX_ENTRIES_PER_SESSION) break;\n\t\tlet parsed: EntryLike | SessionHeaderLike;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch {\n\t\t\t// A partially-flushed final line is normal for a live session.\n\t\t\tcontinue;\n\t\t}\n\t\tif (parsed.type === \"session\") {\n\t\t\theader ??= parsed as SessionHeaderLike;\n\t\t\tcontinue;\n\t\t}\n\t\tentries.push(parsed as EntryLike);\n\t}\n\n\t// An explicit `--session` path can put a session for another directory in\n\t// this directory, so trust the header over the file's location.\n\tif (header?.cwd && !sameDirectory(header.cwd, cwd)) {\n\t\tonSkip(\"otherCwd\");\n\t\treturn undefined;\n\t}\n\tif (entries.length === 0) {\n\t\tonSkip(\"unreadable\");\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tfile,\n\t\tid: header?.id ?? file,\n\t\ttimestamp: header?.timestamp ?? statSync(file).mtime.toISOString(),\n\t\tentries: activeBranch(entries),\n\t};\n}\n\n/**\n * Every directory this cwd's sessions could be sitting in.\n *\n * The caller passes the live session manager's directory, which is the right\n * answer almost always — but not quite always, and each exception silently\n * emptied the digest. An in-memory session (`--no-session`) reports `\"\"`; an\n * explicit `--session <path>` reports wherever that file lives; a custom\n * `sessionDir` setting points at one shared directory. In every one of those\n * cases the per-cwd default directory still holds the history worth mining, so\n * search both and let the header check sort out what belongs to this cwd.\n */\nexport function candidateSessionDirs(options: Pick<ExtractOptions, \"cwd\" | \"agentDir\" | \"sessionDir\">): string[] {\n\tconst dirs: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const dir of [options.sessionDir, getSessionDirPath(options.cwd, options.agentDir)]) {\n\t\tif (!dir) continue;\n\t\tconst key = normalizeDirPath(dir);\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tdirs.push(dir);\n\t}\n\treturn dirs;\n}\n\nfunction listSessions(options: ExtractOptions): {\n\tsessions: ParsedSession[];\n\tskipped: number;\n\tscan: SessionScanReport;\n} {\n\tconst dirs = candidateSessionDirs(options);\n\tconst scan: SessionScanReport = {\n\t\tdirs,\n\t\tmissingDirs: [],\n\t\tfiles: 0,\n\t\ttooOld: 0,\n\t\totherCwd: 0,\n\t\toverLimit: 0,\n\t\tunreadable: 0,\n\t};\n\n\tconst maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;\n\tconst maxAgeDays = options.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;\n\tconst now = options.now ?? new Date();\n\tconst cutoff = now.getTime() - maxAgeDays * 24 * 60 * 60 * 1000;\n\n\tconst files: string[] = [];\n\tfor (const dir of dirs) {\n\t\tif (!existsSync(dir)) {\n\t\t\tscan.missingDirs.push(dir);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tfor (const name of readdirSync(dir)) {\n\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(dir, name));\n\t\t\t}\n\t\t} catch {\n\t\t\tscan.missingDirs.push(dir);\n\t\t}\n\t}\n\tscan.files = files.length;\n\n\t// Newest first across all directories, so `maxSessions` keeps the most recent\n\t// history rather than whichever directory happened to be searched first.\n\tconst dated = files\n\t\t.map((file) => {\n\t\t\ttry {\n\t\t\t\treturn { file, mtime: statSync(file).mtime.getTime() };\n\t\t\t} catch {\n\t\t\t\tscan.unreadable++;\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t})\n\t\t.filter((f): f is { file: string; mtime: number } => !!f)\n\t\t.sort((a, b) => b.mtime - a.mtime);\n\n\tconst sessions: ParsedSession[] = [];\n\tconst seenIds = new Set<string>();\n\tlet skipped = 0;\n\tfor (const { file, mtime } of dated) {\n\t\tif (sessions.length >= maxSessions) {\n\t\t\tscan.overLimit++;\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (mtime < cutoff) {\n\t\t\tscan.tooOld++;\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst parsed = parseSessionFile(file, options.cwd, (reason) => {\n\t\t\tscan[reason]++;\n\t\t});\n\t\tif (!parsed) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Searching two directories can turn up the same session twice (an explicit\n\t\t// `--session` path inside the default directory). Counting it twice would\n\t\t// inflate the cross-session repetition that decides what gets proposed.\n\t\tif (seenIds.has(parsed.id)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tseenIds.add(parsed.id);\n\t\tsessions.push(parsed);\n\t}\n\treturn { sessions, skipped, scan };\n}\n\n/**\n * Hold back items already shown that have not recurred since, then cap the rest.\n *\n * Order matters: suppression runs *before* the cap, or an item you already\n * decided on would occupy one of the few slots the digest has and push a live\n * signal off the list.\n */\nfunction applySuppression<T extends Proposable>(\n\titems: T[],\n\tstate: LearnState | undefined,\n\tmaxProposals: number,\n\tcovered: (item: T) => boolean,\n\tonDeclined?: (item: T) => void,\n): { kept: T[]; suppressed: number } {\n\tif (!state) return { kept: items.slice(0, maxProposals), suppressed: 0 };\n\n\tconst kept: T[] = [];\n\tlet suppressed = 0;\n\tfor (const item of items) {\n\t\tconst verdict = judge(state, { key: item.key, lastSeen: item.lastSeen, covered: covered(item) });\n\t\tif (verdict.suppressed) {\n\t\t\tsuppressed++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (verdict.previouslyDeclined) onDeclined?.(item);\n\t\tkept.push(item);\n\t}\n\treturn { kept: kept.slice(0, maxProposals), suppressed };\n}\n\n/**\n * Where this cwd's sessions were found and what was passed over, without\n * ranking anything. `/learn stats` reports on the window without re-mining it.\n */\nexport function scanSessions(options: ExtractOptions): SessionScanReport {\n\treturn listSessions(options).scan;\n}\n\n/** Nearest AGENTS.md walking up from cwd, so proposals can be checked against it. */\nfunction findAgentsFile(cwd: string): string | undefined {\n\tlet dir = resolve(cwd);\n\twhile (true) {\n\t\tfor (const name of [\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]) {\n\t\t\tconst candidate = join(dir, name);\n\t\t\tif (existsSync(candidate)) return candidate;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\ninterface ToolEvent {\n\tname: string;\n\targs: Record<string, any>;\n\t/** Set once the matching result is seen. */\n\tisError?: boolean;\n\toutput?: string;\n}\n\n/** Pair tool calls with their results along one branch, in call order. */\nfunction toolEvents(entries: EntryLike[]): ToolEvent[] {\n\tconst byCallId = new Map<string, ToolEvent>();\n\tconst ordered: ToolEvent[] = [];\n\n\tfor (const entry of entries) {\n\t\tconst message = entry.type === \"message\" ? entry.message : undefined;\n\t\tif (!message) continue;\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const block of (message.content ?? []) as unknown[]) {\n\t\t\t\tif (!isToolCall(block)) continue;\n\t\t\t\tconst event: ToolEvent = { name: block.name, args: block.arguments ?? {} };\n\t\t\t\tbyCallId.set(block.id, event);\n\t\t\t\tordered.push(event);\n\t\t\t}\n\t\t} else if (message.role === \"toolResult\") {\n\t\t\tconst event = byCallId.get(message.toolCallId);\n\t\t\tif (!event) continue;\n\t\t\tevent.isError = message.isError;\n\t\t\tevent.output = textOf(message.content);\n\t\t}\n\t}\n\treturn ordered;\n}\n\n/** User turns worth mining, in order, with the digest's own output excluded. */\nfunction userDirectives(entries: EntryLike[]): string[] {\n\tconst out: string[] = [];\n\tfor (const entry of entries) {\n\t\tconst message = entry.type === \"message\" ? entry.message : undefined;\n\t\tif (!message || message.role !== \"user\") continue;\n\t\tconst text = textOf(message.content);\n\t\tif (!text || text.startsWith(LEARN_DIGEST_MARKER)) continue;\n\t\tif (!isRuleShapedDirective(text)) continue;\n\t\tout.push(text.trim());\n\t}\n\treturn out;\n}\n\nfunction clusterDirectives(\n\tperSession: Array<{ session: ParsedSession; directives: string[] }>,\n\tcoverage: CoverageIndex,\n\tminRepeats: number,\n): DirectiveCluster[] {\n\tinterface Acc {\n\t\ttext: string;\n\t\tnormalized: string;\n\t\tcount: number;\n\t\tsessions: Set<string>;\n\t\tlastSeen: string;\n\t}\n\tconst acc = new Map<string, Acc>();\n\n\tfor (const { session, directives } of perSession) {\n\t\tfor (const text of directives) {\n\t\t\tconst normalized = normalizeDirective(text);\n\t\t\tif (!normalized) continue;\n\t\t\tconst existing = acc.get(normalized);\n\t\t\tif (existing) {\n\t\t\t\texisting.count++;\n\t\t\t\texisting.sessions.add(session.id);\n\t\t\t\tif (session.timestamp > existing.lastSeen) existing.lastSeen = session.timestamp;\n\t\t\t\tif (text.length > existing.text.length) existing.text = text;\n\t\t\t} else {\n\t\t\t\tacc.set(normalized, {\n\t\t\t\t\ttext,\n\t\t\t\t\tnormalized,\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tsessions: new Set([session.id]),\n\t\t\t\t\tlastSeen: session.timestamp,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tconst clusters: DirectiveCluster[] = [];\n\tfor (const entry of acc.values()) {\n\t\tif (entry.count < minRepeats) continue;\n\n\t\t// Everything reaching here cleared the repeat threshold. Suppression handles\n\t\t// the case that used to make these labels lie — a proposal accepted from a\n\t\t// previous run coming back as \"not working\" when nothing had happened\n\t\t// since. By the time an item survives that filter, a match genuinely means\n\t\t// you repeated yourself after the rule or skill already existed.\n\t\tconst match = matchCoverage(entry.text, coverage);\n\t\tclusters.push({\n\t\t\tkey: `directive:${entry.normalized}`,\n\t\t\ttext: entry.text,\n\t\t\tnormalized: entry.normalized,\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t\tstatus: match.rule ? \"restated\" : match.skill ? \"has-skill\" : \"new\",\n\t\t\texistingRule: match.rule,\n\t\t\texistingSkill: match.skill,\n\t\t\tpreviouslyDeclined: false,\n\t\t});\n\t}\n\n\treturn clusters.sort((a, b) => b.sessions - a.sessions || b.count - a.count || a.text.localeCompare(b.text));\n}\n\n/** Files a mutating tool touched, for the resolution summary. */\nfunction editedFile(event: ToolEvent): string | undefined {\n\tif (![\"edit\", \"write\", \"multi_edit\", \"apply_patch\"].includes(event.name)) return undefined;\n\tconst path = event.args?.path ?? event.args?.file_path ?? event.args?.filePath;\n\treturn typeof path === \"string\" ? path : undefined;\n}\n\nfunction extractFixes(perSession: Array<{ session: ParsedSession; events: ToolEvent[] }>): FixCandidate[] {\n\tinterface Acc {\n\t\tcandidate: FixCandidate;\n\t\tsessions: Set<string>;\n\t}\n\tconst acc = new Map<string, Acc>();\n\n\tfor (const { session, events } of perSession) {\n\t\tfor (let i = 0; i < events.length; i++) {\n\t\t\tconst failure = events[i]!;\n\t\t\tif (failure.name !== \"bash\" || !failure.isError) continue;\n\t\t\tconst command = typeof failure.args?.command === \"string\" ? failure.args.command : \"\";\n\t\t\tif (!command || isBenignFailure(command)) continue;\n\n\t\t\tconst normalized = normalizeCommand(command);\n\t\t\tconst interveningCommands: string[] = [];\n\t\t\tconst editedFiles: string[] = [];\n\t\t\tlet resolved = false;\n\n\t\t\tfor (let j = i + 1; j < Math.min(events.length, i + 1 + FIX_LOOKAHEAD); j++) {\n\t\t\t\tconst next = events[j]!;\n\t\t\t\tconst file = editedFile(next);\n\t\t\t\tif (file) editedFiles.push(file);\n\n\t\t\t\tif (next.name !== \"bash\") continue;\n\t\t\t\tconst nextCommand = typeof next.args?.command === \"string\" ? next.args.command : \"\";\n\t\t\t\tif (!nextCommand) continue;\n\n\t\t\t\t// The same command later succeeding is the only evidence that the\n\t\t\t\t// problem was actually fixed. A *different* command passing says\n\t\t\t\t// nothing, and neither does the model moving on.\n\t\t\t\tif (normalizeCommand(nextCommand) === normalized && !next.isError) {\n\t\t\t\t\tresolved = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tinterveningCommands.push(nextCommand.trim());\n\t\t\t}\n\n\t\t\tif (!resolved) continue;\n\n\t\t\tconst output = failure.output ?? \"\";\n\t\t\t// An abort is the user changing their mind, not a problem that was\n\t\t\t// solved, and empty output carries nothing to sign or show.\n\t\t\tif (isUninformativeFailure(output)) continue;\n\n\t\t\t// Sign the error region, not the whole output: build tools lead with an\n\t\t\t// identical banner, so signing everything makes unrelated failures of\n\t\t\t// the same command collide on their shared preamble.\n\t\t\tconst errorRegion = extractErrorRegion(output);\n\t\t\tconst signature = normalizeErrorSignature(errorRegion);\n\t\t\tif (!signature) continue;\n\n\t\t\tconst key = `${normalized}\u0000${signature}`;\n\t\t\tconst existing = acc.get(key);\n\t\t\tif (existing) {\n\t\t\t\texisting.candidate.count++;\n\t\t\t\texisting.sessions.add(session.id);\n\t\t\t\tif (session.timestamp > existing.candidate.lastSeen) existing.candidate.lastSeen = session.timestamp;\n\t\t\t} else {\n\t\t\t\tacc.set(key, {\n\t\t\t\t\tsessions: new Set([session.id]),\n\t\t\t\t\tcandidate: {\n\t\t\t\t\t\tkey: `fix:${key}`,\n\t\t\t\t\t\tcommand: normalized,\n\t\t\t\t\t\tsignature,\n\t\t\t\t\t\terrorExcerpt: errorRegion.replace(/\\s+/g, \" \").trim().slice(0, 240),\n\t\t\t\t\t\tinterveningCommands: [...new Set(interveningCommands)].slice(0, 5),\n\t\t\t\t\t\teditedFiles: [...new Set(editedFiles)].slice(0, 5),\n\t\t\t\t\t\tcount: 1,\n\t\t\t\t\t\tsessions: 1,\n\t\t\t\t\t\tlastSeen: session.timestamp,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tconst out: FixCandidate[] = [];\n\tfor (const { candidate, sessions } of acc.values()) {\n\t\tcandidate.sessions = sessions.size;\n\t\tout.push(candidate);\n\t}\n\treturn out.sort((a, b) => b.count - a.count || b.sessions - a.sessions || a.signature.localeCompare(b.signature));\n}\n\n/** A tool call reduced to a comparable step: the tool, plus what a bash call runs. */\nfunction stepSignature(event: ToolEvent): string {\n\tif (event.name === \"bash\") {\n\t\tconst command = typeof event.args?.command === \"string\" ? event.args.command : \"\";\n\t\tconst head = commandHead(command);\n\t\treturn head ? `bash:${head}` : \"bash\";\n\t}\n\treturn event.name;\n}\n\n/**\n * Commands that are how an agent looks around rather than what the user was\n * doing. A sequence built only from these plus file edits describes \"coding\",\n * not a workflow, and no useful skill has ever come out of one.\n */\nconst PLUMBING_COMMANDS = new Set([\n\t\"cd\",\n\t\"ls\",\n\t\"pwd\",\n\t\"cat\",\n\t\"head\",\n\t\"tail\",\n\t\"wc\",\n\t\"echo\",\n\t\"which\",\n\t\"find\",\n\t\"fd\",\n\t\"grep\",\n\t\"rg\",\n\t\"sed\",\n\t\"awk\",\n\t\"git status\",\n\t\"git diff\",\n\t\"git log\",\n\t\"git show\",\n]);\n\n/**\n * Whether a sequence is a procedure rather than the rhythm of editing code.\n *\n * Two distinct doing-commands is the bar, and it was set by looking at real\n * transcripts. One command is not enough: the edit/test loop\n * (`edit → edit → bash:npm run`) satisfies it, and because a sliding window\n * over a long alternating run produces every rotation of that cycle, it alone\n * filled all eight slots with `edit → npm run → edit`, `npm run → edit → edit`\n * and so on — one habit described eight ways.\n *\n * A procedure worth a skill chains *different* actions: test then commit then\n * push, build then tag then publish. Requiring two distinct ones keeps those and\n * drops the rhythm. The cost is real — a genuine one-command routine with setup\n * is missed — and that is the intended trade, since a missed skill costs nothing\n * while a digest full of noise costs the reader's attention every run.\n */\nfunction isProcedure(steps: string[]): boolean {\n\tconst commands = new Set<string>();\n\tfor (const step of steps) {\n\t\tif (!step.startsWith(\"bash:\")) continue;\n\t\tconst head = step.slice(\"bash:\".length);\n\t\tif (PLUMBING_COMMANDS.has(head) || PLUMBING_COMMANDS.has(head.split(\" \")[0] ?? \"\")) continue;\n\t\tcommands.add(head);\n\t}\n\treturn commands.size >= 2;\n}\n\n/** True when `needle` appears as a contiguous run inside `haystack`. */\nfunction containsSequence(haystack: string[], needle: string[]): boolean {\n\tif (needle.length > haystack.length) return false;\n\tfor (let i = 0; i + needle.length <= haystack.length; i++) {\n\t\tif (needle.every((step, offset) => haystack[i + offset] === step)) return true;\n\t}\n\treturn false;\n}\n\nfunction extractWorkflows(\n\tperSession: Array<{ session: ParsedSession; events: ToolEvent[] }>,\n\tminRepeats: number,\n): WorkflowCandidate[] {\n\tinterface Acc {\n\t\tsteps: string[];\n\t\tcount: number;\n\t\tsessions: Set<string>;\n\t\tlastSeen: string;\n\t}\n\tconst acc = new Map<string, Acc>();\n\n\tfor (const { session, events } of perSession) {\n\t\tconst steps = events.slice(0, MAX_TOOL_CALLS_PER_SESSION).map(stepSignature);\n\n\t\tfor (let len = WORKFLOW_MIN_LEN; len <= WORKFLOW_MAX_LEN; len++) {\n\t\t\t// Collect every position first, then count greedily without overlap.\n\t\t\t// Counting each sliding position separately treats one long stretch of\n\t\t\t// edit/read churn as dozens of repeats: an `edit > read > edit` run of\n\t\t\t// length 12 scores 10 occurrences when it is really one stretch of work.\n\t\t\tconst positions = new Map<string, number[]>();\n\t\t\tfor (let i = 0; i + len <= steps.length; i++) {\n\t\t\t\tconst window = steps.slice(i, i + len);\n\t\t\t\t// A run of one repeated tool is a loop, not a workflow.\n\t\t\t\tif (new Set(window).size < 2) continue;\n\t\t\t\tif (!isProcedure(window)) continue;\n\t\t\t\tconst key = window.join(\" > \");\n\t\t\t\tconst list = positions.get(key);\n\t\t\t\tif (list) list.push(i);\n\t\t\t\telse positions.set(key, [i]);\n\t\t\t}\n\n\t\t\tfor (const [key, occurrences] of positions) {\n\t\t\t\tlet count = 0;\n\t\t\t\tlet nextFree = -1;\n\t\t\t\tfor (const start of occurrences) {\n\t\t\t\t\tif (start < nextFree) continue;\n\t\t\t\t\tcount++;\n\t\t\t\t\tnextFree = start + len;\n\t\t\t\t}\n\n\t\t\t\tconst existing = acc.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\texisting.count += count;\n\t\t\t\t\texisting.sessions.add(session.id);\n\t\t\t\t\tif (session.timestamp > existing.lastSeen) existing.lastSeen = session.timestamp;\n\t\t\t\t} else {\n\t\t\t\t\tacc.set(key, {\n\t\t\t\t\t\tsteps: key.split(\" > \"),\n\t\t\t\t\t\tcount,\n\t\t\t\t\t\tsessions: new Set([session.id]),\n\t\t\t\t\t\tlastSeen: session.timestamp,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst ranked = [...acc.values()]\n\t\t.filter((entry) => entry.count >= minRepeats)\n\t\t.map((entry) => ({\n\t\t\tkey: `workflow:${entry.steps.join(\" > \")}`,\n\t\t\tsteps: entry.steps,\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t}))\n\t\t// Sessions first, matching directives: a sequence seen in three sessions is\n\t\t// a workflow, while one repeated ten times in a single session is usually\n\t\t// just the shape of that one task.\n\t\t.sort(\n\t\t\t(a, b) =>\n\t\t\t\tb.sessions - a.sessions ||\n\t\t\t\tb.count - a.count ||\n\t\t\t\tb.steps.length - a.steps.length ||\n\t\t\t\ta.steps.join().localeCompare(b.steps.join()),\n\t\t);\n\n\t// Every n-gram overlaps its own extensions and prefixes, so without this the\n\t// list is one workflow described five slightly different ways. The test runs\n\t// both directions on purpose: a shorter sequence always outranks the longer\n\t// one containing it (it occurs at least as often), so checking only\n\t// shorter-inside-kept would never fire. Keep the best-ranked member of each\n\t// family and drop the rest.\n\tconst distinct: typeof ranked = [];\n\tfor (const candidate of ranked) {\n\t\tconst overlapsKept = distinct.some(\n\t\t\t(kept) => containsSequence(kept.steps, candidate.steps) || containsSequence(candidate.steps, kept.steps),\n\t\t);\n\t\tif (overlapsKept) continue;\n\t\tdistinct.push(candidate);\n\t}\n\treturn distinct;\n}\n\n/**\n * Everything a proposal could already have been written into.\n *\n * Built once and shared, because the same question — is this already written\n * down? — is asked while ranking a run *and* afterwards by `/learn stats`,\n * which reconstructs adoption by comparing coverage now against coverage when\n * the item was shown.\n */\nexport interface CoverageIndex {\n\t/** Candidate rule lines from the repo context file and both user scopes. */\n\truleLines: string[];\n\tskills: Array<{ name: string; description: string }>;\n}\n\nexport interface CoverageMatch {\n\t/** The context-file line that covers this, if any. */\n\trule?: string;\n\t/** The skill that covers this, if any. Only set when no rule matched. */\n\tskill?: string;\n}\n\n/**\n * Where a piece of text is already written down, if anywhere.\n *\n * A rule wins over a skill when both match: it is the more specific answer, and\n * \"rewrite this line\" is more actionable than \"sharpen a description\".\n */\nexport function matchCoverage(text: string, index: CoverageIndex): CoverageMatch {\n\tconst words = contentWords(text);\n\n\tlet bestLine: string | undefined;\n\tlet bestOverlap = 0;\n\tfor (const line of index.ruleLines) {\n\t\tconst overlap = wordOverlap(words, line);\n\t\tif (overlap > bestOverlap) {\n\t\t\tbestOverlap = overlap;\n\t\t\tbestLine = line;\n\t\t}\n\t}\n\tif (bestOverlap >= COVERED_OVERLAP) return { rule: bestLine };\n\n\tlet bestSkill: string | undefined;\n\tlet bestSkillOverlap = 0;\n\tfor (const skill of index.skills) {\n\t\tconst haystack = `${skill.name} ${skill.description.slice(0, SKILL_DESCRIPTION_CHARS)}`;\n\t\tconst overlap = wordOverlap(words, haystack);\n\t\tif (overlap > bestSkillOverlap) {\n\t\t\tbestSkillOverlap = overlap;\n\t\t\tbestSkill = skill.name;\n\t\t}\n\t}\n\tif (bestSkillOverlap >= SKILL_COVERED_OVERLAP) return { skill: bestSkill };\n\n\treturn {};\n}\n\n/** Assemble the coverage index for a directory. */\nexport function buildCoverageIndex(options: {\n\tcwd: string;\n\tagentDir: string;\n\tskills?: Array<{ name: string; description: string }>;\n}): CoverageIndex {\n\tconst corpus = coverageCorpus(options.agentDir, findAgentsFile(options.cwd));\n\treturn {\n\t\truleLines: corpus\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.filter((line) => line.length > 0 && !line.startsWith(\"#\")),\n\t\tskills: options.skills ?? loadSkillIndex(options.cwd, options.agentDir),\n\t};\n}\n\n/**\n * Text a proposal is checked against to decide whether it is already written\n * down — the nearest repo context file plus both user scopes.\n *\n * All three matter for suppression, because `/learn` can route a rule to the\n * user scope. Checking only the repo file would report a rule you accepted into\n * `~/.agents/AGENTS.md` as declined.\n */\nfunction coverageCorpus(agentDir: string, repoFile: string | undefined): string {\n\tconst parts: string[] = [];\n\tfor (const candidate of [repoFile, join(getUserAgentsDir(), \"AGENTS.md\"), join(agentDir, \"AGENTS.md\")]) {\n\t\tif (!candidate || !existsSync(candidate)) continue;\n\t\ttry {\n\t\t\tparts.push(readFileSync(candidate, \"utf-8\"));\n\t\t} catch {\n\t\t\t// Unreadable context file: treat as absent rather than failing the run.\n\t\t}\n\t}\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * Skills a proposal could already have become.\n *\n * `/learn` routes long or conditional guidance to a skill rather than a rule, so\n * without this a proposal you adopted *as a skill* would read as declined —\n * looking only at context files sees an unchanged `AGENTS.md` and concludes you\n * passed. Reuses the real loader rather than a second SKILL.md scanner so the\n * set of locations cannot drift from what the session actually loads.\n */\nfunction loadSkillIndex(cwd: string, agentDir: string): Array<{ name: string; description: string }> {\n\ttry {\n\t\treturn loadSkills({ cwd, agentDir, skillPaths: [], includeDefaults: true }).skills.map((skill) => ({\n\t\t\tname: skill.name,\n\t\t\tdescription: skill.description ?? \"\",\n\t\t}));\n\t} catch {\n\t\t// Skills are an enrichment here, not the point of the command.\n\t\treturn [];\n\t}\n}\n\n/** Mine the recent sessions for this cwd and return the ranked digest. */\nexport function extractLearnDigest(options: ExtractOptions): LearnDigest {\n\tconst { sessions, skipped, scan } = listSessions(options);\n\n\tconst agentsFilePath = findAgentsFile(options.cwd);\n\tlet agentsContent: string | undefined;\n\tif (agentsFilePath) {\n\t\ttry {\n\t\t\tagentsContent = readFileSync(agentsFilePath, \"utf-8\");\n\t\t} catch {\n\t\t\tagentsContent = undefined;\n\t\t}\n\t}\n\tconst coverage = buildCoverageIndex({ cwd: options.cwd, agentDir: options.agentDir, skills: options.skills });\n\n\tconst withDirectives = sessions.map((session) => ({ session, directives: userDirectives(session.entries) }));\n\tconst withEvents = sessions.map((session) => ({ session, events: toolEvents(session.entries) }));\n\n\tconst timestamps = sessions.map((s) => s.timestamp).sort();\n\tconst state = options.ignoreState ? undefined : options.state;\n\n\t// Directives carry a real coverage signal — is this written down as a rule or\n\t// a skill right now? — which is what separates an adopted proposal from a\n\t// declined one. Fixes and workflows do not: a fix may have become a rule, a\n\t// skill, or a habit, and which one is not recoverable here, so they get\n\t// suppression only and are never labelled declined.\n\tconst maxProposals = options.maxProposals ?? DEFAULT_MAX_PER_CATEGORY;\n\tconst directives = applySuppression(\n\t\tclusterDirectives(withDirectives, coverage, options.minRepeats ?? DEFAULT_MIN_DIRECTIVE_COUNT),\n\t\tstate,\n\t\tmaxProposals,\n\t\t(item) => item.status !== \"new\",\n\t\t(item) => {\n\t\t\titem.previouslyDeclined = true;\n\t\t},\n\t);\n\tconst fixes = applySuppression(extractFixes(withEvents), state, maxProposals, () => false);\n\tconst workflows = applySuppression(\n\t\textractWorkflows(withEvents, options.minWorkflowRepeats ?? DEFAULT_MIN_WORKFLOW_COUNT),\n\t\tstate,\n\t\tmaxProposals,\n\t\t() => false,\n\t);\n\n\tconst surfaced = [\n\t\t...directives.kept.map((d) => ({ key: d.key, lastSeen: d.lastSeen, covered: d.status !== \"new\" })),\n\t\t...fixes.kept.map((f) => ({ key: f.key, lastSeen: f.lastSeen, covered: false })),\n\t\t...workflows.kept.map((w) => ({ key: w.key, lastSeen: w.lastSeen, covered: false })),\n\t];\n\n\treturn {\n\t\tscannedSessions: sessions.length,\n\t\tskippedSessions: skipped,\n\t\tscan,\n\t\toldestSession: timestamps[0],\n\t\tnewestSession: timestamps[timestamps.length - 1],\n\t\tagentsFilePath,\n\t\tagentsFileTokens:\n\t\t\tagentsContent === undefined ? undefined : Math.round(Buffer.byteLength(agentsContent, \"utf-8\") / 4),\n\t\tdirectives: directives.kept,\n\t\tfixes: fixes.kept,\n\t\tworkflows: workflows.kept,\n\t\tsuppressed: directives.suppressed + fixes.suppressed + workflows.suppressed,\n\t\tsurfaced,\n\t};\n}\n"]}
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../../src/core/learn/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAmB,MAAM,YAAY,CAAC;AAGpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGhD,yCAAyC;AACzC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAChC,qFAAmF;AACnF,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAChC,oFAAoF;AACpF,MAAM,uBAAuB,GAAG,IAAI,CAAC;AACrC,2DAA2D;AAC3D,MAAM,2BAA2B,GAAG,CAAC,CAAC;AACtC;;;;;;GAMG;AACH,MAAM,yBAAyB,GAAG,CAAC,CAAC;AACpC,qFAAqF;AACrF,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAmLnC,qFAAqF;AACrF,SAAS,cAAc,CAAC,OAAoB,EAAE,QAAgB,EAAU;IACvE,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,GAAG,MAAM;YAAE,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IAC/F,CAAC;IACD,OAAO,MAAM,IAAI,QAAQ,CAAC;AAAA,CAC1B;AAED;;;;;;;;GAQG;AACH,SAAS,YAAY,CAAC,OAAoB,EAAe;IACxD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAEzC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAY,EAAE,KAAK,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,MAAM,GAA0B,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChE,OAAO,MAAM,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpB,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;AAAA,CACxB;AAED;;;;;;;;;GASG;AACH,SAAS,gBAAgB,CAAC,IAAY,EAAU;IAC/C,IAAI,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC;QACJ,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACR,uEAAuE;IACxE,CAAC;IACD,4EAA4E;IAC5E,4CAA4C;IAC5C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACpF,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AAAA,CACzG;AAED,SAAS,aAAa,CAAC,CAAS,EAAE,CAAS,EAAW;IACrD,OAAO,gBAAgB,CAAC,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAAA,CACnD;AAKD,SAAS,gBAAgB,CAAC,IAAY,EAAE,GAAW,EAAE,MAAoC,EAA6B;IACrH,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACJ,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,CAAC,YAAY,CAAC,CAAC;QACrB,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,MAAqC,CAAC;IAC1C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,OAAO,CAAC,MAAM,IAAI,uBAAuB;YAAE,MAAM;QACrD,IAAI,MAAqC,CAAC;QAC1C,IAAI,CAAC;YACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACR,+DAA+D;YAC/D,SAAS;QACV,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,KAAK,MAA2B,CAAC;YACvC,SAAS;QACV,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,MAAmB,CAAC,CAAC;IACnC,CAAC;IAED,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,CAAC,UAAU,CAAC,CAAC;QACnB,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,CAAC,YAAY,CAAC,CAAC;QACrB,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,EAAE,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;IACvE,OAAO;QACN,IAAI;QACJ,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,IAAI;QACtB,SAAS,EAAE,MAAM;QACjB,YAAY,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC;QAC5C,OAAO,EAAE,MAAM;KACf,CAAC;AAAA,CACF;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgE,EAAY;IAChH,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC1F,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,YAAY,CAAC,OAAuB,EAI3C;IACD,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAsB;QAC/B,IAAI;QACJ,WAAW,EAAE,EAAE;QACf,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;KACb,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAChE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,oBAAoB,CAAC;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAEhE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3B,SAAS;QACV,CAAC;QACD,IAAI,CAAC;YACJ,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YAC1D,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACF,CAAC;IACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAE1B,8EAA8E;IAC9E,yEAAyE;IACzE,MAAM,KAAK,GAAG,KAAK;SACjB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACd,IAAI,CAAC;YACJ,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACR,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,OAAO,SAAS,CAAC;QAClB,CAAC;IAAA,CACD,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAwC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;SACxD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAEpC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,EAAE,CAAC;QACrC,IAAI,QAAQ,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC;YACpC,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,OAAO,EAAE,CAAC;YACV,SAAS;QACV,CAAC;QACD,IAAI,KAAK,GAAG,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,EAAE,CAAC;YACV,SAAS;QACV,CAAC;QACD,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC;YAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAAA,CACf,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,OAAO,EAAE,CAAC;YACV,SAAS;QACV,CAAC;QACD,4EAA4E;QAC5E,0EAA0E;QAC1E,wEAAwE;QACxE,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;YACV,SAAS;QACV,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAAA,CACnC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CACxB,KAAU,EACV,KAA6B,EAC7B,YAAoB,EACpB,OAA6B,EAC7B,UAA8B,EACmB;IACjD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,EAAE,CAAC;IAC7G,CAAC;IAED,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjG,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACxB,UAAU,EAAE,CAAC;YACb,SAAS;QACV,CAAC;QACD,IAAI,OAAO,CAAC,kBAAkB;YAAE,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,4EAA4E;IAC5E,6EAA2E;IAC3E,2EAA2E;IAC3E,6CAA6C;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,EAAE,CAAC;AAAA,CACvG;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAuB,EAAqB;IACxE,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;AAAA,CAClC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,OAAuB,EAAsD;IACvG,MAAM,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,IAAI,IAAI,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;YAAE,MAAM,EAAE,CAAC;;YAC1D,OAAO,EAAE,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAAA,CACnD;AASD;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,aAAa,CAC3B,GAAsB,EACtB,SAAgC,EAChC,WAAqB,EACrB,MAAoB,EACM;IAC1B,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,MAAM,MAAM,GAAkD,EAAE,CAAC;IACjE,KAAK,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACrD,KAAK,MAAM,CAAC,cAAc,EAAE,SAAS,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;YACxE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/E,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;QACnE,CAAC;IACF,CAAC;IAED,IAAI,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,IAAI,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACJ,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACR,yCAAyC;QAC1C,CAAC;IACF,CAAC;IAED,MAAM,QAAQ,GAAmB,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACtD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,UAAU,EAAE,EAAE;KACd,CAAC,CAAC,CAAC;IACJ,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACtF,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS;YAAE,SAAS;QACnC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC;YACxC,GAAG,SAAS;YACZ,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC;SACxD,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,wFAAwF;AACxF,SAAS,eAAe,CAAC,KAA6B,EAAY;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAAA,CACjF;AAED,qFAAqF;AACrF,SAAS,cAAc,CAAC,GAAW,EAAsB;IACxD,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACvB,OAAO,IAAI,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC;YACzE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAClC,IAAI,UAAU,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QAC7C,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,SAAS,CAAC;QACrC,GAAG,GAAG,MAAM,CAAC;IACd,CAAC;AAAA,CACD;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,WAAW,CAAC,OAAe,EAAE,KAAa,EAAY;IAC9D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO,GAAG,CAAC,OAAO,CAAC;YACnB,SAAS;QACV,CAAC;QACD,IAAI,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAE3C,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,OAAO,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;YACtC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YACvD,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACvC,SAAS;QACV,CAAC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,OAIlC,EAAiB;IACjB,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,OAAO,CAAC,QAAQ,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACjF,SAAS,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;AAAA,CAC9F;AAED;;;;;;;GAOG;AACH,SAAS,aAAa,CAAC,QAAgB,EAAE,QAA4B,EAA6C;IACjH,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,UAAU,GAAuD;QACtE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;QACjC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,EAAE,WAAW,CAAC,EAAE;QAC9D,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE;KACpD,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7D,IAAI,CAAC;YACJ,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;QAAC,MAAM,CAAC;YACR,wEAAwE;QACzE,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,GAAW,EAAE,QAAgB,EAAgD;IACpG,IAAI,CAAC;QACJ,OAAO,UAAU,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAClG,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE;SACpC,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACR,+DAA+D;QAC/D,OAAO,EAAE,CAAC;IACX,CAAC;AAAA,CACD;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,YAAY,CAC1B,QAAyB,EACzB,OAAoB,EAC4D;IAChF,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,MAAM,MAAM,GAAiB,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAEhE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAErE,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,IAAI,MAAM,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;YACzG,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,EAAE,CAAC;YACP,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YAC9E,SAAS;QACV,CAAC;QAED,MAAM,OAAO,GAAmB,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3G,IAAI,CAAC;YACJ,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAChE,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,UAAU,EAAE,CAAC,CAAC;YACtF,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,IAAI,EAAE,CAAC;gBACV,iBAAiB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE;oBACzC,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,UAAU;oBACV,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACjC,CAAC,CAAC;YACJ,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,oEAAoE;YACpE,yEAAyE;YACzE,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;gBAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACrE,MAAM,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;QACD,IAAI,EAAE,CAAC;QACP,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,CACzC;AAED,yEAAyE;AACzE,SAAS,aAAa,CAAC,UAA8B,EAAE,QAAwD,EAAQ;IACtH,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC;YAC5B,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;QACrC,CAAC;aAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC;YAC7B,OAAO,CAAC,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC;QACvC,CAAC;IACF,CAAC;AAAA,CACD;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAoB,EAAwB;IACjF,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAE1D,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnD,IAAI,aAAiC,CAAC;IACtC,IAAI,cAAc,EAAE,CAAC;QACpB,IAAI,CAAC;YACJ,aAAa,GAAG,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACR,aAAa,GAAG,SAAS,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzE,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAE/C,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC9D,gFAA8E;IAC9E,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAE/G,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,2BAA2B,CAAC;IACrE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,wBAAwB,CAAC;IACtE,MAAM,UAAU,GAAG,gBAAgB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,iBAAiB,IAAI,yBAAyB,CAAC,CAAC;IAElG,0EAA0E;IAC1E,iFAA+E;IAC/E,kBAAkB;IAClB,MAAM,UAAU,GACf,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7G,MAAM,MAAM,GAAG;QACd,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACjF,MAAM,EAAE,UAAU;QAClB,cAAc,EAAE,UAAU,GAAG,UAAU,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;KAC/E,CAAC;IAEF,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9G,MAAM,OAAO,GAAoB,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC3F,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,IAAI,eAAe,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACrG,aAAa,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACR,sEAAsE;QACtE,mEAAmE;QACnE,2EAA2E;QAC3E,sEAAsE;QACtE,cAAc,GAAG,IAAI,CAAC;IACvB,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3D,gFAA8E;IAC9E,4EAA0E;IAC1E,2EAA2E;IAC3E,wEAAwE;IACxE,oDAAoD;IACpD,MAAM,cAAc,GAAG,gBAAgB,CACtC,UAAU,EACV,KAAK,EACL,YAAY,EACZ,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,EAC/B,CAAC,IAAI,EAAE,EAAE,CAAC;QACT,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAAA,CAC/B,CACD,CAAC;IACF,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAC5E,MAAM,YAAY,GAAG,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAElF,MAAM,QAAQ,GAAG;QAChB,2EAA2E;QAC3E,wEAAwE;QACxE,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClC,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,OAAO,EAAE,CAAC,CAAC,MAAM,KAAK,KAAK;YAC3B,IAAI,EAAE,CAAC,CAAC,IAAI;SACZ,CAAC,CAAC;QACH,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;KACvF,CAAC;IAEF,OAAO;QACN,eAAe,EAAE,QAAQ,CAAC,MAAM;QAChC,eAAe,EAAE,OAAO;QACxB,IAAI;QACJ,MAAM,EAAE,MAAM;QACd,OAAO;QACP,cAAc;QACd,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;QAC5B,aAAa,EAAE,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;QAChD,cAAc;QACd,gBAAgB,EACf,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpG,UAAU,EAAE,cAAc,CAAC,IAAI;QAC/B,KAAK,EAAE,SAAS,CAAC,IAAI;QACrB,QAAQ,EAAE,YAAY,CAAC,IAAI;QAC3B,UAAU,EAAE,cAAc,CAAC,UAAU,GAAG,SAAS,CAAC,UAAU,GAAG,YAAY,CAAC,UAAU;QACtF,GAAG,EAAE,cAAc,CAAC,GAAG,GAAG,SAAS,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG;QAC1D,MAAM;QACN,QAAQ;KACR,CAAC;AAAA,CACF","sourcesContent":["/**\n * Session mining for `/learn`.\n *\n * Reads session `.jsonl` files straight off disk rather than the live context.\n * That is the whole point: the on-disk transcript is complete even when the\n * in-context one has been compacted away, and it spans every past session\n * instead of only this one. Cross-session repetition is the signal that decides\n * whether something is a durable rule or a one-off, and it is the one thing a\n * prompt reading its own context cannot see.\n *\n * This module is the orchestrator, and the split of labour inside it is\n * deliberate:\n *\n * - **Gathering** is deterministic. Finding session files, resolving which cwd\n * they belong to, walking the active branch of a forked session — all exact,\n * all cheap, all here.\n * - **Judgement** is the model's, in `mine.ts` and `coverage.ts`. What counts as\n * a directive, what two phrasings have in common, whether a rule already\n * covers something — none of that survives contact with a regex, and it used\n * to be decided by one.\n * - **Counting** is deterministic again, in `reduce.ts`. The number is the\n * product, and a model asked to count over a long context will be\n * approximately right.\n *\n * The expensive step is memoized per session file (`cache.ts`), so a session is\n * read by the model exactly once in its life and the counts are still computed\n * over every session in the window on every run.\n */\n\nimport { existsSync, readdirSync, readFileSync, realpathSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve, sep } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { getUserAgentsDir } from \"../../config.js\";\nimport { getSessionDirPath } from \"../session-manager.js\";\nimport { loadSkills } from \"../skills.js\";\nimport { hashSessionFile, pruneLearnCache, readCachedMining, writeCachedMining } from \"./cache.js\";\nimport type { Clusterer, ClusterInput } from \"./cluster.js\";\nimport { fallbackLabel } from \"./cluster.js\";\nimport type { CoverageIndex, CoverageJudge, CoverageQuery } from \"./coverage.js\";\nimport { noCoverageJudge } from \"./coverage.js\";\nimport type { MinableSession, MinedCandidate, Miner } from \"./mine.js\";\nimport type { DirectiveCluster, FixCandidate, MinedSession, Proposable, RequestCandidate } from \"./reduce.js\";\nimport { reduceDirectives, reduceFixes, reduceRequests } from \"./reduce.js\";\nimport { judge, type LearnState } from \"./state.js\";\n\nexport type { CoverageIndex, CoverageMatch } from \"./coverage.js\";\nexport { LEARN_DIGEST_MARKER } from \"./mine.js\";\nexport type { DirectiveCluster, DirectiveStatus, FixCandidate, RequestCandidate } from \"./reduce.js\";\n\n/** Sessions considered, newest first. */\nconst DEFAULT_MAX_SESSIONS = 20;\n/** Sessions older than this are ignored — a pattern that stopped is not a rule. */\nconst DEFAULT_MAX_AGE_DAYS = 30;\n/** Entries parsed per session file, as a guard against pathological transcripts. */\nconst MAX_ENTRIES_PER_SESSION = 8000;\n/** Occurrences a directive needs before it is proposed. */\nconst DEFAULT_MIN_DIRECTIVE_COUNT = 2;\n/**\n * Sessions a request needs before it is worth proposing as a slash command.\n *\n * Higher than the directive bar. A rule you stated twice is a rule; a job you\n * asked for twice may just be a job that came up twice. Three separate sessions\n * is the point at which typing it again is the expensive option.\n */\nconst DEFAULT_MIN_REQUEST_COUNT = 3;\n/** Cap on each list in the digest, so the model's budget goes to the top signals. */\nconst DEFAULT_MAX_PER_CATEGORY = 8;\n\n/**\n * Why a session file on disk did not make it into the digest.\n *\n * \"No recent sessions\" is the one outcome a user cannot act on without this:\n * an empty session directory, a directory full of month-old sessions, and a\n * directory full of sessions belonging to another checkout all produce the same\n * sentence, and the fix differs in each case.\n */\nexport interface SessionScanReport {\n\t/** Directories actually searched, in order. */\n\tdirs: string[];\n\t/** Directories that do not exist on disk. */\n\tmissingDirs: string[];\n\t/** `.jsonl` files found across all searched directories. */\n\tfiles: number;\n\t/** Skipped for being older than the age window. */\n\ttooOld: number;\n\t/** Skipped because the session header records a different working directory. */\n\totherCwd: number;\n\t/** Skipped for being beyond `maxSessions`. */\n\toverLimit: number;\n\t/** Skipped for being unreadable, unparseable, or empty. */\n\tunreadable: number;\n}\n\n/** What the run cost, so the price of an LLM-read pipeline is visible rather than hidden. */\nexport interface MiningReport {\n\t/** Sessions whose candidates came from cache, free. */\n\tcached: number;\n\t/** Sessions sent to the model this run. */\n\tmined: number;\n\t/** Sessions the model failed on. Their signals are missing from the counts. */\n\tfailed: number;\n}\n\nexport interface LearnDigest {\n\tscannedSessions: number;\n\tskippedSessions: number;\n\t/** Where the sessions came from, and what was passed over. */\n\tscan: SessionScanReport;\n\t/** What was read by the model versus reused. */\n\tmining: MiningReport;\n\t/**\n\t * The run stopped before reading the whole window, so the counts below are\n\t * computed from part of it. Callers must not record these as surfaced: a\n\t * partial count can fall under the repeat threshold, and bookmarking it would\n\t * hide the item on the next run, when the evidence is complete.\n\t */\n\taborted: boolean;\n\t/**\n\t * The coverage judge failed, so every directive reads `new` whether or not it\n\t * is written down. Callers must not record these as surfaced either: the\n\t * bookmark stores whether an item was covered when shown, and a wrong `false`\n\t * there tells a later run you passed over a proposal you were never given.\n\t */\n\tcoverageFailed: boolean;\n\toldestSession?: string;\n\tnewestSession?: string;\n\tagentsFilePath?: string;\n\tagentsFileTokens?: number;\n\tdirectives: DirectiveCluster[];\n\tfixes: FixCandidate[];\n\trequests: RequestCandidate[];\n\t/** Items held back because nothing new has happened since they were last shown. */\n\tsuppressed: number;\n\t/** Items that cleared every threshold but lost the ranking to `maxProposals`. */\n\tcut: number;\n\t/**\n\t * What the window contained before the thresholds, so an empty digest can be\n\t * read.\n\t *\n\t * The pipeline filters hard — replayed slash-command bodies, tool output,\n\t * quotes that cannot be found in the transcript, then a distinct-session bar\n\t * — and every one of those is silent. Without these numbers \"nothing to\n\t * propose\" is unreadable: it could mean the sessions taught nothing, or that\n\t * the bar is one session too high, and the reader has no way to tell which\n\t * knob to reach for.\n\t */\n\tfunnel: {\n\t\t/** Occurrences the miner reported and the quote check accepted. */\n\t\tcandidates: number;\n\t\t/** Distinct points after naming — how much the clustering pass actually merged. */\n\t\tpoints: number;\n\t\t/** Points that were named and counted but did not clear the repeat threshold. */\n\t\tbelowThreshold: number;\n\t};\n\t/** Everything this run put on screen, for the caller to persist. */\n\tsurfaced: Array<{ key: string; lastSeen: string; covered: boolean; text?: string }>;\n}\n\nexport interface ExtractOptions {\n\tcwd: string;\n\tagentDir: string;\n\t/**\n\t * An extra directory to scan, normally the live session manager's. The\n\t * per-cwd default directory is always scanned as well, so a session manager\n\t * pointing somewhere unusual cannot hide this directory's history.\n\t */\n\tsessionDir?: string;\n\tmaxSessions?: number;\n\tmaxAgeDays?: number;\n\t/** Occurrences a directive needs before it is proposed. The signal/noise dial. */\n\tminRepeats?: number;\n\t/** Repeats a tool sequence needs before it is proposed as a skill. */\n\tminRequestRepeats?: number;\n\t/** Cap on each list in the digest. */\n\tmaxProposals?: number;\n\t/**\n\t * What previous runs already showed. Items with no new occurrences since are\n\t * held back. Omit (or pass `ignoreState`) to propose everything in the window.\n\t */\n\tstate?: LearnState;\n\t/** Re-propose everything, ignoring what previous runs surfaced (`/learn all`). */\n\tignoreState?: boolean;\n\t/**\n\t * Skills a directive can already be covered by. Defaults to the ones loaded\n\t * from disk; injectable so tests do not read the developer's real skills.\n\t */\n\tskills?: Array<{ name: string; description: string }>;\n\t/** Injectable clock, for tests. */\n\tnow?: Date;\n}\n\n/** Everything the async pipeline needs beyond the window settings. */\nexport interface MineOptions extends ExtractOptions {\n\t/** Reads one session and reports what it saw. */\n\tminer: Miner;\n\t/**\n\t * Names the whole window at once, deciding which occurrences are the same\n\t * point. Without one, each candidate is named after its own wording, which\n\t * groups identical sentences and nothing else.\n\t */\n\tclusterer?: Clusterer;\n\t/** Decides which proposals are already written down. Defaults to \"none are\". */\n\tcoverageJudge?: CoverageJudge;\n\t/** Progress callback, so a cold-cache run is not a silent wait. */\n\tonProgress?: (progress: { done: number; total: number; cached: number }) => void;\n\tsignal?: AbortSignal;\n}\n\ninterface SessionHeaderLike {\n\ttype: \"session\";\n\tid?: string;\n\ttimestamp?: string;\n\tcwd?: string;\n}\n\ninterface EntryLike {\n\ttype: string;\n\tid?: string;\n\tparentId?: string | null;\n\ttimestamp?: string;\n\tmessage?: AgentMessage;\n}\n\n/** One session, reduced to the branch that was actually taken. */\ninterface ParsedSession {\n\tfile: string;\n\tid: string;\n\t/** When the session was opened. Describes the window, not what is in it. */\n\ttimestamp: string;\n\t/**\n\t * When the session was last written to.\n\t *\n\t * This is the clock suppression runs on, and it must not be the session's\n\t * start. A session opened yesterday and worked in today would date everything\n\t * said in it to yesterday, which can be older than the last `/learn` run — so\n\t * something said minutes ago reads as \"nothing new since you were last shown\n\t * this\" and is held back. Per-candidate timestamps would be finer, but the\n\t * miner sees an untimestamped blob and would have to invent them; the\n\t * session's last activity is deterministic, free, and errs toward showing an\n\t * item again rather than hiding it.\n\t */\n\tlastActivity: string;\n\tentries: EntryLike[];\n}\n\n/** Newest entry timestamp on the branch, falling back to when the session opened. */\nfunction lastActivityOf(entries: EntryLike[], fallback: string): string {\n\tlet latest = \"\";\n\tfor (const entry of entries) {\n\t\tif (typeof entry.timestamp === \"string\" && entry.timestamp > latest) latest = entry.timestamp;\n\t}\n\treturn latest || fallback;\n}\n\n/**\n * Reduce a session's raw entries to the branch that was actually taken.\n *\n * Session files are trees — forks and clones append entries that were never\n * part of the same conversation. Walking parent links back from the last entry\n * keeps the miner from reading two turns that never happened in sequence as if\n * they did. Sessions written before entry ids existed are flat, and for those\n * file order *is* the branch.\n */\nfunction activeBranch(entries: EntryLike[]): EntryLike[] {\n\tconst withIds = entries.filter((e) => typeof e.id === \"string\");\n\tif (withIds.length === 0) return entries;\n\n\tconst byId = new Map<string, EntryLike>();\n\tfor (const entry of withIds) byId.set(entry.id as string, entry);\n\n\tconst branch: EntryLike[] = [];\n\tconst seen = new Set<string>();\n\tlet cursor: EntryLike | undefined = withIds[withIds.length - 1];\n\twhile (cursor?.id && !seen.has(cursor.id)) {\n\t\tseen.add(cursor.id);\n\t\tbranch.push(cursor);\n\t\tcursor = cursor.parentId ? byId.get(cursor.parentId) : undefined;\n\t}\n\treturn branch.reverse();\n}\n\n/**\n * Compare two directory paths the way the filesystem does.\n *\n * A session header stores the cwd as it was typed, and the same directory can\n * be spelled several ways: through a symlink (`/tmp` is `/private/tmp` on\n * macOS), with a trailing separator, or in different case on the\n * case-insensitive filesystems that macOS and Windows ship by default. String\n * equality on `resolve()` alone rejects every one of those, and rejecting them\n * here means silently discarding the whole history the command exists to read.\n */\nfunction normalizeDirPath(path: string): string {\n\tlet resolved = resolve(path);\n\ttry {\n\t\tresolved = realpathSync.native(resolved);\n\t} catch {\n\t\t// Deleted or never-created directory: the textual form is all we have.\n\t}\n\t// `resolve` already drops a trailing separator except at a filesystem root,\n\t// where dropping it would turn \"/\" into \"\".\n\tif (resolved.length > 1 && resolved.endsWith(sep)) resolved = resolved.slice(0, -1);\n\treturn process.platform === \"win32\" || process.platform === \"darwin\" ? resolved.toLowerCase() : resolved;\n}\n\nfunction sameDirectory(a: string, b: string): boolean {\n\treturn normalizeDirPath(a) === normalizeDirPath(b);\n}\n\n/** Reason a candidate file produced no session, for the scan report. */\ntype SkipReason = \"otherCwd\" | \"unreadable\";\n\nfunction parseSessionFile(file: string, cwd: string, onSkip: (reason: SkipReason) => void): ParsedSession | undefined {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(file, \"utf-8\");\n\t} catch {\n\t\tonSkip(\"unreadable\");\n\t\treturn undefined;\n\t}\n\n\tconst lines = raw.split(\"\\n\");\n\tlet header: SessionHeaderLike | undefined;\n\tconst entries: EntryLike[] = [];\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\tif (entries.length >= MAX_ENTRIES_PER_SESSION) break;\n\t\tlet parsed: EntryLike | SessionHeaderLike;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch {\n\t\t\t// A partially-flushed final line is normal for a live session.\n\t\t\tcontinue;\n\t\t}\n\t\tif (parsed.type === \"session\") {\n\t\t\theader ??= parsed as SessionHeaderLike;\n\t\t\tcontinue;\n\t\t}\n\t\tentries.push(parsed as EntryLike);\n\t}\n\n\t// An explicit `--session` path can put a session for another directory in\n\t// this directory, so trust the header over the file's location.\n\tif (header?.cwd && !sameDirectory(header.cwd, cwd)) {\n\t\tonSkip(\"otherCwd\");\n\t\treturn undefined;\n\t}\n\tif (entries.length === 0) {\n\t\tonSkip(\"unreadable\");\n\t\treturn undefined;\n\t}\n\n\tconst branch = activeBranch(entries);\n\tconst opened = header?.timestamp ?? statSync(file).mtime.toISOString();\n\treturn {\n\t\tfile,\n\t\tid: header?.id ?? file,\n\t\ttimestamp: opened,\n\t\tlastActivity: lastActivityOf(branch, opened),\n\t\tentries: branch,\n\t};\n}\n\n/**\n * Every directory this cwd's sessions could be sitting in.\n *\n * The caller passes the live session manager's directory, which is the right\n * answer almost always — but not quite always, and each exception silently\n * emptied the digest. An in-memory session (`--no-session`) reports `\"\"`; an\n * explicit `--session <path>` reports wherever that file lives; a custom\n * `sessionDir` setting points at one shared directory. In every one of those\n * cases the per-cwd default directory still holds the history worth mining, so\n * search both and let the header check sort out what belongs to this cwd.\n */\nexport function candidateSessionDirs(options: Pick<ExtractOptions, \"cwd\" | \"agentDir\" | \"sessionDir\">): string[] {\n\tconst dirs: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const dir of [options.sessionDir, getSessionDirPath(options.cwd, options.agentDir)]) {\n\t\tif (!dir) continue;\n\t\tconst key = normalizeDirPath(dir);\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tdirs.push(dir);\n\t}\n\treturn dirs;\n}\n\nfunction listSessions(options: ExtractOptions): {\n\tsessions: ParsedSession[];\n\tskipped: number;\n\tscan: SessionScanReport;\n} {\n\tconst dirs = candidateSessionDirs(options);\n\tconst scan: SessionScanReport = {\n\t\tdirs,\n\t\tmissingDirs: [],\n\t\tfiles: 0,\n\t\ttooOld: 0,\n\t\totherCwd: 0,\n\t\toverLimit: 0,\n\t\tunreadable: 0,\n\t};\n\n\tconst maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;\n\tconst maxAgeDays = options.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;\n\tconst now = options.now ?? new Date();\n\tconst cutoff = now.getTime() - maxAgeDays * 24 * 60 * 60 * 1000;\n\n\tconst files: string[] = [];\n\tfor (const dir of dirs) {\n\t\tif (!existsSync(dir)) {\n\t\t\tscan.missingDirs.push(dir);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tfor (const name of readdirSync(dir)) {\n\t\t\t\tif (name.endsWith(\".jsonl\")) files.push(join(dir, name));\n\t\t\t}\n\t\t} catch {\n\t\t\tscan.missingDirs.push(dir);\n\t\t}\n\t}\n\tscan.files = files.length;\n\n\t// Newest first across all directories, so `maxSessions` keeps the most recent\n\t// history rather than whichever directory happened to be searched first.\n\tconst dated = files\n\t\t.map((file) => {\n\t\t\ttry {\n\t\t\t\treturn { file, mtime: statSync(file).mtime.getTime() };\n\t\t\t} catch {\n\t\t\t\tscan.unreadable++;\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t})\n\t\t.filter((f): f is { file: string; mtime: number } => !!f)\n\t\t.sort((a, b) => b.mtime - a.mtime);\n\n\tconst sessions: ParsedSession[] = [];\n\tconst seenIds = new Set<string>();\n\tlet skipped = 0;\n\tfor (const { file, mtime } of dated) {\n\t\tif (sessions.length >= maxSessions) {\n\t\t\tscan.overLimit++;\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (mtime < cutoff) {\n\t\t\tscan.tooOld++;\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst parsed = parseSessionFile(file, options.cwd, (reason) => {\n\t\t\tscan[reason]++;\n\t\t});\n\t\tif (!parsed) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Searching two directories can turn up the same session twice (an explicit\n\t\t// `--session` path inside the default directory). Counting it twice would\n\t\t// inflate the cross-session repetition that decides what gets proposed.\n\t\tif (seenIds.has(parsed.id)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tseenIds.add(parsed.id);\n\t\tsessions.push(parsed);\n\t}\n\treturn { sessions, skipped, scan };\n}\n\n/**\n * Hold back items already shown that have not recurred since, then cap the rest.\n *\n * Order matters: suppression runs *before* the cap, or an item you already\n * decided on would occupy one of the few slots the digest has and push a live\n * signal off the list.\n */\nfunction applySuppression<T extends Proposable>(\n\titems: T[],\n\tstate: LearnState | undefined,\n\tmaxProposals: number,\n\tcovered: (item: T) => boolean,\n\tonDeclined?: (item: T) => void,\n): { kept: T[]; suppressed: number; cut: number } {\n\tif (!state) {\n\t\treturn { kept: items.slice(0, maxProposals), suppressed: 0, cut: Math.max(0, items.length - maxProposals) };\n\t}\n\n\tconst kept: T[] = [];\n\tlet suppressed = 0;\n\tfor (const item of items) {\n\t\tconst verdict = judge(state, { key: item.key, lastSeen: item.lastSeen, covered: covered(item) });\n\t\tif (verdict.suppressed) {\n\t\t\tsuppressed++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (verdict.previouslyDeclined) onDeclined?.(item);\n\t\tkept.push(item);\n\t}\n\t// Anything past the cap cleared every bar and lost on rank alone. It is not\n\t// suppressed and it is not bookmarked, so it will be back next run — but a\n\t// digest that silently shows eight of twenty reads as \"twenty is all there\n\t// was\", and the reader tunes the wrong knob.\n\treturn { kept: kept.slice(0, maxProposals), suppressed, cut: Math.max(0, kept.length - maxProposals) };\n}\n\n/**\n * Where this cwd's sessions were found and what was passed over, without\n * mining anything. `/learn settings` and `/learn stats` report on the window\n * without paying for a model call.\n */\nexport function scanSessions(options: ExtractOptions): SessionScanReport {\n\treturn listSessions(options).scan;\n}\n\n/**\n * What a run would read, without reading it.\n *\n * Runs the real selection — the same age, cwd, cap and de-duplication rules\n * `mineLearnDigest` applies — and then asks the cache about each survivor. It\n * has to be the same selection: this number is what the confirmation prompt\n * quotes, and a prompt that says twelve before reading three is worse than no\n * prompt at all. Hashing the chosen files is cheap next to sending them to a\n * model.\n */\nexport function planMining(options: ExtractOptions): { total: number; cached: number; pending: number } {\n\tconst { sessions } = listSessions(options);\n\tlet cached = 0;\n\tlet pending = 0;\n\tfor (const session of sessions) {\n\t\tconst hash = hashSessionFile(session.file);\n\t\tif (hash && readCachedMining(options.agentDir, hash)) cached++;\n\t\telse pending++;\n\t}\n\treturn { total: sessions.length, cached, pending };\n}\n\n/** One session's candidates before the naming pass has seen them. */\ninterface RawMinedSession {\n\tsessionId: string;\n\tlastActivity: string;\n\tcandidates: MinedCandidate[];\n}\n\n/**\n * Name every candidate in the window in one place.\n *\n * The pass runs over the whole window at once rather than per session, which is\n * the entire point: \"is this the same point as that\" is unanswerable from\n * inside one transcript. Labels already on record are offered as vocabulary so\n * an item you decided on keeps the key it was bookmarked under — without that,\n * a renamed cluster reads as brand new and suppression quietly stops working.\n *\n * A failed call falls back to naming each candidate after its own wording,\n * which groups identical sentences and nothing else. That is the behaviour the\n * pipeline had before this stage existed, so a clustering outage costs recall,\n * not the run.\n */\nasync function labelSessions(\n\traw: RawMinedSession[],\n\tclusterer: Clusterer | undefined,\n\tknownLabels: string[],\n\tsignal?: AbortSignal,\n): Promise<MinedSession[]> {\n\tconst inputs: ClusterInput[] = [];\n\tconst origin: Array<{ session: number; candidate: number }> = [];\n\tfor (const [sessionIndex, session] of raw.entries()) {\n\t\tfor (const [candidateIndex, candidate] of session.candidates.entries()) {\n\t\t\tinputs.push({ id: inputs.length, kind: candidate.kind, text: candidate.text });\n\t\t\torigin.push({ session: sessionIndex, candidate: candidateIndex });\n\t\t}\n\t}\n\n\tlet labels = new Map<number, string>();\n\tif (clusterer && inputs.length > 0) {\n\t\ttry {\n\t\t\tlabels = await clusterer(inputs, knownLabels, signal);\n\t\t} catch {\n\t\t\t// Fall through to per-text labels below.\n\t\t}\n\t}\n\n\tconst labelled: MinedSession[] = raw.map((session) => ({\n\t\tsessionId: session.sessionId,\n\t\tlastActivity: session.lastActivity,\n\t\tcandidates: [],\n\t}));\n\tfor (const [index, input] of inputs.entries()) {\n\t\tconst where = origin[index];\n\t\tconst candidate = where ? raw[where.session]?.candidates[where.candidate] : undefined;\n\t\tif (!where || !candidate) continue;\n\t\tlabelled[where.session]?.candidates.push({\n\t\t\t...candidate,\n\t\t\tlabel: labels.get(input.id) ?? fallbackLabel(input.text),\n\t\t});\n\t}\n\treturn labelled;\n}\n\n/** Labels already on record, so the naming pass can reuse rather than reinvent them. */\nfunction knownLabelsFrom(state: LearnState | undefined): string[] {\n\tif (!state) return [];\n\treturn Object.keys(state.surfaced).map((key) => key.slice(key.indexOf(\":\") + 1));\n}\n\n/** Nearest AGENTS.md walking up from cwd, so proposals can be checked against it. */\nfunction findAgentsFile(cwd: string): string | undefined {\n\tlet dir = resolve(cwd);\n\twhile (true) {\n\t\tfor (const name of [\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]) {\n\t\t\tconst candidate = join(dir, name);\n\t\t\tif (existsSync(candidate)) return candidate;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Turn one context file into rule lines the coverage judge can reason about.\n *\n * Headings were dropped and the lines under them sent bare, which asks the\n * model to decide whether a proposal is in scope using text with the scope\n * removed — \"stage only your own files\" reads very differently under \"Git Rules\n * for Parallel Agents\" than on its own. So each line carries its heading path,\n * and the scope it came from, since the corpus spans a repo file and two user\n * ones and a rule's home decides who it binds.\n *\n * Fenced blocks go: a code sample illustrates a rule, it is not one, and on a\n * real file it is a large share of the non-bullet text.\n */\nfunction ruleLinesOf(content: string, scope: string): string[] {\n\tconst lines: string[] = [];\n\tconst headings: string[] = [];\n\tlet inFence = false;\n\n\tfor (const raw of content.split(\"\\n\")) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line.length === 0) continue;\n\n\t\tconst heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n\t\tif (heading) {\n\t\t\tconst depth = heading[1]?.length ?? 1;\n\t\t\theadings.length = Math.min(headings.length, depth - 1);\n\t\t\theadings[depth - 1] = heading[2] ?? \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst path = headings.filter(Boolean).join(\" > \");\n\t\tlines.push(path ? `[${scope}] ${path} > ${line}` : `[${scope}] ${line}`);\n\t}\n\treturn lines;\n}\n\n/** Assemble the coverage index for a directory. */\nexport function buildCoverageIndex(options: {\n\tcwd: string;\n\tagentDir: string;\n\tskills?: Array<{ name: string; description: string }>;\n}): CoverageIndex {\n\tconst ruleLines: string[] = [];\n\tfor (const file of coverageFiles(options.agentDir, findAgentsFile(options.cwd))) {\n\t\truleLines.push(...ruleLinesOf(file.content, file.scope));\n\t}\n\treturn { ruleLines, skills: options.skills ?? loadSkillIndex(options.cwd, options.agentDir) };\n}\n\n/**\n * Text a proposal is checked against to decide whether it is already written\n * down — the nearest repo context file plus both user scopes.\n *\n * All three matter for suppression, because `/learn` can route a rule to the\n * user scope. Checking only the repo file would report a rule you accepted into\n * `~/.agents/AGENTS.md` as declined.\n */\nfunction coverageFiles(agentDir: string, repoFile: string | undefined): Array<{ scope: string; content: string }> {\n\tconst files: Array<{ scope: string; content: string }> = [];\n\tconst candidates: Array<{ scope: string; path: string | undefined }> = [\n\t\t{ scope: \"repo\", path: repoFile },\n\t\t{ scope: \"user\", path: join(getUserAgentsDir(), \"AGENTS.md\") },\n\t\t{ scope: \"user\", path: join(agentDir, \"AGENTS.md\") },\n\t];\n\tfor (const candidate of candidates) {\n\t\tif (!candidate.path || !existsSync(candidate.path)) continue;\n\t\ttry {\n\t\t\tfiles.push({ scope: candidate.scope, content: readFileSync(candidate.path, \"utf-8\") });\n\t\t} catch {\n\t\t\t// Unreadable context file: treat as absent rather than failing the run.\n\t\t}\n\t}\n\treturn files;\n}\n\n/**\n * Skills a proposal could already have become.\n *\n * `/learn` routes long or conditional guidance to a skill rather than a rule, so\n * without this a proposal you adopted *as a skill* would read as declined —\n * looking only at context files sees an unchanged `AGENTS.md` and concludes you\n * passed. Reuses the real loader rather than a second SKILL.md scanner so the\n * set of locations cannot drift from what the session actually loads.\n */\nfunction loadSkillIndex(cwd: string, agentDir: string): Array<{ name: string; description: string }> {\n\ttry {\n\t\treturn loadSkills({ cwd, agentDir, skillPaths: [], includeDefaults: true }).skills.map((skill) => ({\n\t\t\tname: skill.name,\n\t\t\tdescription: skill.description ?? \"\",\n\t\t}));\n\t} catch {\n\t\t// Skills are an enrichment here, not the point of the command.\n\t\treturn [];\n\t}\n}\n\n/**\n * Run the miner over the window, reusing cached results wherever the file has\n * not changed.\n *\n * A session that fails to mine is counted and skipped rather than aborting the\n * run: one provider hiccup on one transcript should cost that transcript's\n * signals, not the whole digest. The failure count is reported so the reader\n * knows the numbers are short.\n *\n * Cancellation is different from failure and is reported separately. A run\n * stopped half way has counted only some of the window, so its numbers are not\n * merely short — they are wrong in a way that would poison the bookmark if the\n * digest were treated as a completed run.\n */\nasync function mineSessions(\n\tsessions: ParsedSession[],\n\toptions: MineOptions,\n): Promise<{ mined: RawMinedSession[]; report: MiningReport; aborted: boolean }> {\n\tconst mined: RawMinedSession[] = [];\n\tconst report: MiningReport = { cached: 0, mined: 0, failed: 0 };\n\n\tlet done = 0;\n\tfor (const session of sessions) {\n\t\tif (options.signal?.aborted) return { mined, report, aborted: true };\n\n\t\tconst hash = hashSessionFile(session.file);\n\t\tconst cached = hash ? readCachedMining(options.agentDir, hash) : undefined;\n\t\tif (cached) {\n\t\t\tmined.push({ sessionId: session.id, lastActivity: session.lastActivity, candidates: cached.candidates });\n\t\t\treport.cached++;\n\t\t\tdone++;\n\t\t\toptions.onProgress?.({ done, total: sessions.length, cached: report.cached });\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst minable: MinableSession = { id: session.id, timestamp: session.timestamp, entries: session.entries };\n\t\ttry {\n\t\t\tconst candidates = await options.miner(minable, options.signal);\n\t\t\tmined.push({ sessionId: session.id, lastActivity: session.lastActivity, candidates });\n\t\t\treport.mined++;\n\t\t\tif (hash) {\n\t\t\t\twriteCachedMining(options.agentDir, hash, {\n\t\t\t\t\tsessionId: session.id,\n\t\t\t\t\ttimestamp: session.timestamp,\n\t\t\t\t\tcandidates,\n\t\t\t\t\tminedAt: new Date().toISOString(),\n\t\t\t\t});\n\t\t\t}\n\t\t} catch {\n\t\t\t// A cancelled request surfaces here as a rejection. That is not the\n\t\t\t// provider failing on this transcript, so it must not be counted as one.\n\t\t\tif (options.signal?.aborted) return { mined, report, aborted: true };\n\t\t\treport.failed++;\n\t\t}\n\t\tdone++;\n\t\toptions.onProgress?.({ done, total: sessions.length, cached: report.cached });\n\t}\n\n\treturn { mined, report, aborted: false };\n}\n\n/** Apply the coverage verdicts to the clusters they were asked about. */\nfunction applyCoverage(directives: DirectiveCluster[], verdicts: Map<string, { rule?: string; skill?: string }>): void {\n\tfor (const cluster of directives) {\n\t\tconst verdict = verdicts.get(cluster.label);\n\t\tif (!verdict) continue;\n\t\tif (verdict.rule) {\n\t\t\tcluster.status = \"restated\";\n\t\t\tcluster.existingRule = verdict.rule;\n\t\t} else if (verdict.skill) {\n\t\t\tcluster.status = \"has-skill\";\n\t\t\tcluster.existingSkill = verdict.skill;\n\t\t}\n\t}\n}\n\n/** Mine the recent sessions for this cwd and return the ranked digest. */\nexport async function mineLearnDigest(options: MineOptions): Promise<LearnDigest> {\n\tconst { sessions, skipped, scan } = listSessions(options);\n\n\tconst agentsFilePath = findAgentsFile(options.cwd);\n\tlet agentsContent: string | undefined;\n\tif (agentsFilePath) {\n\t\ttry {\n\t\t\tagentsContent = readFileSync(agentsFilePath, \"utf-8\");\n\t\t} catch {\n\t\t\tagentsContent = undefined;\n\t\t}\n\t}\n\n\tconst { mined, report, aborted } = await mineSessions(sessions, options);\n\tpruneLearnCache(options.agentDir, options.now);\n\n\tconst state = options.ignoreState ? undefined : options.state;\n\t// Named against the labels already on record — including in `all` mode, where\n\t// suppression is off but the bookmark still has to line up next run.\n\tconst labelled = await labelSessions(mined, options.clusterer, knownLabelsFrom(options.state), options.signal);\n\n\tconst minRepeats = options.minRepeats ?? DEFAULT_MIN_DIRECTIVE_COUNT;\n\tconst maxProposals = options.maxProposals ?? DEFAULT_MAX_PER_CATEGORY;\n\tconst directives = reduceDirectives(labelled, minRepeats);\n\tconst fixes = reduceFixes(labelled, minRepeats);\n\tconst requests = reduceRequests(labelled, options.minRequestRepeats ?? DEFAULT_MIN_REQUEST_COUNT);\n\n\t// Counted with the threshold at 1, which is the same reduce over the same\n\t// input — so the difference is exactly what the threshold cost, rather than an\n\t// estimate of it.\n\tconst everyPoint =\n\t\treduceDirectives(labelled, 1).length + reduceFixes(labelled, 1).length + reduceRequests(labelled, 1).length;\n\tconst funnel = {\n\t\tcandidates: labelled.reduce((sum, session) => sum + session.candidates.length, 0),\n\t\tpoints: everyPoint,\n\t\tbelowThreshold: everyPoint - directives.length - fixes.length - requests.length,\n\t};\n\n\t// Coverage is asked only about what survived the repeat threshold. Judging\n\t// everything would mean sending the context file alongside a long tail of\n\t// one-off observations that are never going to be proposed.\n\tconst coverage = buildCoverageIndex({ cwd: options.cwd, agentDir: options.agentDir, skills: options.skills });\n\tconst queries: CoverageQuery[] = directives.map((d) => ({ label: d.label, text: d.text }));\n\tlet coverageFailed = false;\n\ttry {\n\t\tconst verdicts = await (options.coverageJudge ?? noCoverageJudge)(queries, coverage, options.signal);\n\t\tapplyCoverage(directives, verdicts);\n\t} catch {\n\t\t// A failed coverage call leaves everything `new`, which over-proposes\n\t\t// slightly. That is the right way to fail: the reader can reject a\n\t\t// duplicate, but cannot recover a proposal that was wrongly withheld. What\n\t\t// must not happen is writing that guess down as if it were a reading.\n\t\tcoverageFailed = true;\n\t}\n\n\tconst timestamps = sessions.map((s) => s.timestamp).sort();\n\t// Directives carry a real coverage signal — is this written down as a rule or\n\t// a skill right now? — which is what separates an adopted proposal from a\n\t// declined one. Fixes and requests do not: a fix may have become a rule, a\n\t// skill, or a habit, and which one is not recoverable here, so they get\n\t// suppression only and are never labelled declined.\n\tconst keptDirectives = applySuppression(\n\t\tdirectives,\n\t\tstate,\n\t\tmaxProposals,\n\t\t(item) => item.status !== \"new\",\n\t\t(item) => {\n\t\t\titem.previouslyDeclined = true;\n\t\t},\n\t);\n\tconst keptFixes = applySuppression(fixes, state, maxProposals, () => false);\n\tconst keptRequests = applySuppression(requests, state, maxProposals, () => false);\n\n\tconst surfaced = [\n\t\t// Directives carry their wording forward so a later `/learn stats` can ask\n\t\t// about coverage using the sentence rather than the slug that names it.\n\t\t...keptDirectives.kept.map((d) => ({\n\t\t\tkey: d.key,\n\t\t\tlastSeen: d.lastSeen,\n\t\t\tcovered: d.status !== \"new\",\n\t\t\ttext: d.text,\n\t\t})),\n\t\t...keptFixes.kept.map((f) => ({ key: f.key, lastSeen: f.lastSeen, covered: false })),\n\t\t...keptRequests.kept.map((r) => ({ key: r.key, lastSeen: r.lastSeen, covered: false })),\n\t];\n\n\treturn {\n\t\tscannedSessions: sessions.length,\n\t\tskippedSessions: skipped,\n\t\tscan,\n\t\tmining: report,\n\t\taborted,\n\t\tcoverageFailed,\n\t\toldestSession: timestamps[0],\n\t\tnewestSession: timestamps[timestamps.length - 1],\n\t\tagentsFilePath,\n\t\tagentsFileTokens:\n\t\t\tagentsContent === undefined ? undefined : Math.round(Buffer.byteLength(agentsContent, \"utf-8\") / 4),\n\t\tdirectives: keptDirectives.kept,\n\t\tfixes: keptFixes.kept,\n\t\trequests: keptRequests.kept,\n\t\tsuppressed: keptDirectives.suppressed + keptFixes.suppressed + keptRequests.suppressed,\n\t\tcut: keptDirectives.cut + keptFixes.cut + keptRequests.cut,\n\t\tfunnel,\n\t\tsurfaced,\n\t};\n}\n"]}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The map half of `/learn`: a model reads one session transcript and says what
3
+ * it saw.
4
+ *
5
+ * This replaces the regex gate that used to decide which user turns were worth
6
+ * looking at. That gate was a whitelist of imperative words, so a directive
7
+ * phrased any other way — "we're on bun now", "that's not how our error
8
+ * handling works" — was not ranked low, it was invisible. Recall was traded for
9
+ * a token budget, silently and unrecoverably.
10
+ *
11
+ * The trade here is explicit instead. Every user turn goes to the model
12
+ * verbatim; the budget is enforced by chunking and by a session cap the reader
13
+ * can see, not by a filter they cannot.
14
+ *
15
+ * What the model does *not* do here is name or count. It used to emit a label
16
+ * per occurrence — its own canonical name for what was meant — and the reduce
17
+ * step grouped on exact label equality. That cannot work from inside one
18
+ * session: the model is asked to hit a shared vocabulary it has never seen, and
19
+ * on a real corpus it agreed with itself 3 times out of 188. Naming now happens
20
+ * once, globally, in `cluster.ts`, where every candidate is visible at the same
21
+ * time. Counting stays in `reduce.ts`, where it always belonged.
22
+ *
23
+ * Leaving labels out also makes the cache model-independent. A cached candidate
24
+ * used to carry a label frozen at mining time, so changing the `fast` tier
25
+ * forked the vocabulary permanently: old sessions and new ones named the same
26
+ * thing differently, and neither side reached the repeat threshold.
27
+ */
28
+ import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
29
+ import type { Model } from "@kolisachint/hoocode-ai";
30
+ /** What kind of thing the model noticed. */
31
+ export type CandidateKind = "directive" | "fix" | "request";
32
+ /**
33
+ * One occurrence, as reported by the model reading a single session.
34
+ *
35
+ * Deliberately unnamed. What was *meant* is only decidable against everything
36
+ * else that was said, and this stage sees one session, so it reports what it
37
+ * saw and leaves grouping to `cluster.ts`. This is also the shape that goes in
38
+ * the cache, which is why nothing model-specific may live on it.
39
+ */
40
+ export interface MinedCandidate {
41
+ kind: CandidateKind;
42
+ /** Verbatim text from the transcript, so the digest can quote rather than paraphrase. */
43
+ text: string;
44
+ /** Why this is durable, in the model's words. Shown when a proposal is borderline. */
45
+ rationale?: string;
46
+ /** The failing command, for `fix` candidates. */
47
+ command?: string;
48
+ /** Short error excerpt, for `fix` candidates. */
49
+ errorExcerpt?: string;
50
+ /** What was done in between, for `fix` candidates. */
51
+ interveningCommands?: string[];
52
+ /** Files changed as part of the fix. */
53
+ editedFiles?: string[];
54
+ }
55
+ /** A candidate once the global naming pass has decided what to call it. */
56
+ export interface LabelledCandidate extends MinedCandidate {
57
+ /** Canonical slug for what was meant. The clustering key. */
58
+ label: string;
59
+ }
60
+ /** A session reduced to what the miner needs: identity, time, and rendered text. */
61
+ export interface MinableSession {
62
+ id: string;
63
+ timestamp: string;
64
+ entries: Array<{
65
+ type: string;
66
+ message?: AgentMessage;
67
+ }>;
68
+ }
69
+ /**
70
+ * Mines one session. Injectable so the reduce path can be tested without a
71
+ * model, and so a cached result can stand in for a live call.
72
+ */
73
+ export type Miner = (session: MinableSession, signal?: AbortSignal) => Promise<MinedCandidate[]>;
74
+ /**
75
+ * How much rendered transcript to send per call, given the reading model.
76
+ *
77
+ * Only a fraction of the window is used: the instructions, the response, and
78
+ * tokenizer variance all have to fit alongside, and overshooting costs a
79
+ * context-overflow error rather than a slightly worse answer.
80
+ */
81
+ export declare function chunkCharsForModel(model: Pick<Model<any>, "contextWindow">): number;
82
+ /**
83
+ * Prefix on the message `/learn` injects. Its own digest is persisted like any
84
+ * other user turn, so without this the next run would mine its own output and
85
+ * every proposal would compound its own count.
86
+ */
87
+ export declare const LEARN_DIGEST_MARKER = "[learn-digest]";
88
+ /**
89
+ * Literal runs from slash-command bodies, used to recognise a replayed expansion.
90
+ *
91
+ * A `user`-type slash command is persisted as an ordinary user message holding
92
+ * the whole template body, with nothing to mark it as machinery. Read back off
93
+ * disk it is indistinguishable from something the user typed — and it is the
94
+ * most repeated text in a real corpus, because running `/pr` thirty times
95
+ * writes the same two thousand characters thirty times. Mining it produces
96
+ * directives the user never stated, at counts that look exactly like organic
97
+ * repetition.
98
+ *
99
+ * Detection is retroactive on purpose. A provenance flag written at turn time
100
+ * would be exact, but it would only help sessions recorded after it shipped,
101
+ * leaving the existing corpus contaminated for months. Matching against the
102
+ * command bodies still on disk fixes the history that already exists. The gap
103
+ * is a template that has since been deleted; that case wants the flag, and is
104
+ * the reason to add one later.
105
+ */
106
+ export declare function replayFingerprints(templates: Array<{
107
+ content: string;
108
+ }>): string[];
109
+ /** True when a user turn is the body of a slash command rather than something typed. */
110
+ export declare function isReplayedTurn(text: string, fingerprints: string[]): boolean;
111
+ /**
112
+ * Render a session as plain text for the model.
113
+ *
114
+ * User turns the user actually typed go in whole and unfiltered — any
115
+ * truncation there would quietly reintroduce the recall problem the old regex
116
+ * gate had. What does not go in is text the user's tooling replayed: its own
117
+ * past digests, and slash-command bodies.
118
+ *
119
+ * Assistant prose is dropped: it is the bulk of a transcript and almost none of
120
+ * it is evidence about what the *user* wants. Tool calls are kept, because a
121
+ * failure-then-pass is a fix. Successful tool output is dropped: it is a file
122
+ * or a command's stdout, not a statement by anyone, and feeding it to a miner
123
+ * looking for directives yields lines lifted out of plan files and configs
124
+ * attributed to the user.
125
+ */
126
+ export declare function renderTranscript(session: MinableSession, fingerprints?: string[]): string;
127
+ /** Everything the user actually said in a session, normalized, for checking quotes against. */
128
+ export declare function spokenText(session: MinableSession, fingerprints?: string[]): string;
129
+ /**
130
+ * Drop candidates whose quote cannot be found in what the user said.
131
+ *
132
+ * The miner is told to quote verbatim and the digest renders every quote inside
133
+ * quotation marks, but on a real corpus a third of them appear nowhere in the
134
+ * session: paraphrases, merged sentences, and lines lifted out of tool output.
135
+ * A quote that cannot be located is evidence that cannot be shown, and a
136
+ * proposal the reader cannot check is worse than one that was never made.
137
+ *
138
+ * Whitespace is normalized before comparing, because a directive written in a
139
+ * markdown file arrives wrapped across lines and the model unwraps it.
140
+ */
141
+ export declare function verifyCandidates(candidates: MinedCandidate[], spoken: string): MinedCandidate[];
142
+ /**
143
+ * Split rendered text on line boundaries, so a chunk never cuts a user turn in
144
+ * half. A single turn longer than the budget gets its own oversized chunk
145
+ * rather than being split — losing the second half of a long directive is
146
+ * exactly the failure this rewrite exists to remove.
147
+ */
148
+ export declare function chunkTranscript(text: string, chunkChars?: number): string[];
149
+ /**
150
+ * Pull the JSON object out of a model response.
151
+ *
152
+ * Models fence JSON even when told not to, and occasionally prepend a sentence.
153
+ * Scanning for the outermost braces is more forgiving than trusting the format
154
+ * and cheaper than a repair pass — and a chunk whose response cannot be parsed
155
+ * is skipped, never fatal, because one bad chunk should not lose a whole run.
156
+ */
157
+ export declare function parseCandidates(response: string): MinedCandidate[];
158
+ export interface MinerDeps {
159
+ model: Model<any>;
160
+ apiKey?: string;
161
+ headers?: Record<string, string>;
162
+ /**
163
+ * Literal runs from the slash-command bodies in force, from
164
+ * `replayFingerprints`. User turns matching one are machinery replaying
165
+ * itself, not the user speaking.
166
+ */
167
+ replayFingerprints?: string[];
168
+ }
169
+ /**
170
+ * Build the real miner: one model call per chunk of one session.
171
+ *
172
+ * Chunks are mined sequentially rather than in parallel. A cold-cache run is
173
+ * already the expensive path, and firing every chunk of every session at once
174
+ * is how you trip a provider rate limit on exactly the run that has the most to
175
+ * do.
176
+ */
177
+ export declare function createLlmMiner(deps: MinerDeps): Miner;
178
+ //# sourceMappingURL=mine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mine.d.ts","sourceRoot":"","sources":["../../../src/core/learn/mine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The map half of `/learn`: a model reads one session transcript and says what\n * it saw.\n *\n * This replaces the regex gate that used to decide which user turns were worth\n * looking at. That gate was a whitelist of imperative words, so a directive\n * phrased any other way — \"we're on bun now\", \"that's not how our error\n * handling works\" — was not ranked low, it was invisible. Recall was traded for\n * a token budget, silently and unrecoverably.\n *\n * The trade here is explicit instead. Every user turn goes to the model\n * verbatim; the budget is enforced by chunking and by a session cap the reader\n * can see, not by a filter they cannot.\n *\n * What the model does *not* do here is name or count. It used to emit a label\n * per occurrence — its own canonical name for what was meant — and the reduce\n * step grouped on exact label equality. That cannot work from inside one\n * session: the model is asked to hit a shared vocabulary it has never seen, and\n * on a real corpus it agreed with itself 3 times out of 188. Naming now happens\n * once, globally, in `cluster.ts`, where every candidate is visible at the same\n * time. Counting stays in `reduce.ts`, where it always belonged.\n *\n * Leaving labels out also makes the cache model-independent. A cached candidate\n * used to carry a label frozen at mining time, so changing the `fast` tier\n * forked the vocabulary permanently: old sessions and new ones named the same\n * thing differently, and neither side reached the repeat threshold.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { Model, TextContent, ToolCall } from \"@kolisachint/hoocode-ai\";\nimport { completeSimple } from \"@kolisachint/hoocode-ai\";\n\n/** What kind of thing the model noticed. */\nexport type CandidateKind = \"directive\" | \"fix\" | \"request\";\n\n/**\n * One occurrence, as reported by the model reading a single session.\n *\n * Deliberately unnamed. What was *meant* is only decidable against everything\n * else that was said, and this stage sees one session, so it reports what it\n * saw and leaves grouping to `cluster.ts`. This is also the shape that goes in\n * the cache, which is why nothing model-specific may live on it.\n */\nexport interface MinedCandidate {\n\tkind: CandidateKind;\n\t/** Verbatim text from the transcript, so the digest can quote rather than paraphrase. */\n\ttext: string;\n\t/** Why this is durable, in the model's words. Shown when a proposal is borderline. */\n\trationale?: string;\n\t/** The failing command, for `fix` candidates. */\n\tcommand?: string;\n\t/** Short error excerpt, for `fix` candidates. */\n\terrorExcerpt?: string;\n\t/** What was done in between, for `fix` candidates. */\n\tinterveningCommands?: string[];\n\t/** Files changed as part of the fix. */\n\teditedFiles?: string[];\n}\n\n/** A candidate once the global naming pass has decided what to call it. */\nexport interface LabelledCandidate extends MinedCandidate {\n\t/** Canonical slug for what was meant. The clustering key. */\n\tlabel: string;\n}\n\n/** A session reduced to what the miner needs: identity, time, and rendered text. */\nexport interface MinableSession {\n\tid: string;\n\ttimestamp: string;\n\tentries: Array<{ type: string; message?: AgentMessage }>;\n}\n\n/**\n * Mines one session. Injectable so the reduce path can be tested without a\n * model, and so a cached result can stand in for a live call.\n */\nexport type Miner = (session: MinableSession, signal?: AbortSignal) => Promise<MinedCandidate[]>;\n\n/**\n * Chunking exists to fit a session into a context window, so it is sized from\n * the window rather than from a fixed guess.\n *\n * The guess was costing calls. Rendering already strips assistant prose and\n * truncates tool output, which compresses the two real transcripts in this repo\n * from 0.93 MB and 2.26 MB down to 183 KB and 266 KB — about 47k and 68k\n * tokens. A fixed 120k-character chunk cut those into two and three pieces for\n * no reason: on any model with a 200k window each is comfortably one call.\n *\n * One call per session is also better than a cheaper-looking alternative. A\n * chunk boundary is a blind spot — a failure and the fix that resolved it can\n * land on opposite sides of one — so the fewer boundaries inside a session, the\n * more the model can actually see.\n */\nconst CHUNK_CONTEXT_FRACTION = 0.6;\n\n/** Rough bytes per token. Deliberately conservative; a wrong guess here costs a wasted call. */\nconst CHARS_PER_TOKEN = 4;\n\n/** Used when a model does not report a usable window. */\nconst FALLBACK_CHUNK_CHARS = 120_000;\n\n/** Never chunk below this, or a small window would shred a transcript into noise. */\nconst MIN_CHUNK_CHARS = 40_000;\n\n/**\n * How much rendered transcript to send per call, given the reading model.\n *\n * Only a fraction of the window is used: the instructions, the response, and\n * tokenizer variance all have to fit alongside, and overshooting costs a\n * context-overflow error rather than a slightly worse answer.\n */\nexport function chunkCharsForModel(model: Pick<Model<any>, \"contextWindow\">): number {\n\tconst window = model.contextWindow;\n\tif (!Number.isFinite(window) || window <= 0) return FALLBACK_CHUNK_CHARS;\n\tconst budgetTokens = window * CHUNK_CONTEXT_FRACTION - MAX_RESPONSE_TOKENS;\n\treturn Math.max(MIN_CHUNK_CHARS, Math.floor(budgetTokens * CHARS_PER_TOKEN));\n}\n\n/** Error output kept per call. Errors carry the signal; success output is dropped entirely. */\nconst TOOL_ERROR_CHARS = 1_500;\n\n/**\n * Shortest literal run of a slash-command body that identifies a replay.\n *\n * Long enough that a user cannot type it by accident, short enough to survive a\n * template whose placeholders are densely packed.\n */\nconst REPLAY_FINGERPRINT_CHARS = 40;\n\n/** Response ceiling per chunk. A chunk yielding more than this is noise, not signal. */\nconst MAX_RESPONSE_TOKENS = 4_000;\n\n/** Candidates accepted from a single chunk, as a guard against a runaway response. */\nconst MAX_CANDIDATES_PER_CHUNK = 40;\n\n/**\n * Prefix on the message `/learn` injects. Its own digest is persisted like any\n * other user turn, so without this the next run would mine its own output and\n * every proposal would compound its own count.\n */\nexport const LEARN_DIGEST_MARKER = \"[learn-digest]\";\n\n/** Collapse whitespace so a quote survives the wrapping a markdown source imposes on it. */\nfunction normalizeForMatch(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().toLowerCase();\n}\n\n/**\n * Literal runs from slash-command bodies, used to recognise a replayed expansion.\n *\n * A `user`-type slash command is persisted as an ordinary user message holding\n * the whole template body, with nothing to mark it as machinery. Read back off\n * disk it is indistinguishable from something the user typed — and it is the\n * most repeated text in a real corpus, because running `/pr` thirty times\n * writes the same two thousand characters thirty times. Mining it produces\n * directives the user never stated, at counts that look exactly like organic\n * repetition.\n *\n * Detection is retroactive on purpose. A provenance flag written at turn time\n * would be exact, but it would only help sessions recorded after it shipped,\n * leaving the existing corpus contaminated for months. Matching against the\n * command bodies still on disk fixes the history that already exists. The gap\n * is a template that has since been deleted; that case wants the flag, and is\n * the reason to add one later.\n */\nexport function replayFingerprints(templates: Array<{ content: string }>): string[] {\n\tconst out: string[] = [];\n\tfor (const template of templates) {\n\t\t// Split on the placeholders that argument substitution rewrites, leaving the\n\t\t// literal text that survives every expansion.\n\t\tconst segments = template.content.split(/\\$(?:\\d+|ARGUMENTS|\\*)/);\n\t\tlet longest = \"\";\n\t\tfor (const segment of segments) {\n\t\t\tconst normalized = normalizeForMatch(segment);\n\t\t\tif (normalized.length > longest.length) longest = normalized;\n\t\t}\n\t\tif (longest.length >= REPLAY_FINGERPRINT_CHARS) out.push(longest);\n\t}\n\treturn out;\n}\n\n/** True when a user turn is the body of a slash command rather than something typed. */\nexport function isReplayedTurn(text: string, fingerprints: string[]): boolean {\n\tif (fingerprints.length === 0) return false;\n\tconst normalized = normalizeForMatch(text);\n\tif (normalized.length < REPLAY_FINGERPRINT_CHARS) return false;\n\treturn fingerprints.some((fingerprint) => normalized.includes(fingerprint));\n}\n\nfunction textOf(content: unknown): string {\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.map((block) =>\n\t\t\tblock && typeof block === \"object\" && (block as TextContent).type === \"text\"\n\t\t\t\t? ((block as TextContent).text ?? \"\")\n\t\t\t\t: \"\",\n\t\t)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isToolCall(block: unknown): block is ToolCall {\n\treturn !!block && typeof block === \"object\" && (block as ToolCall).type === \"toolCall\";\n}\n\n/** Compact one tool call's arguments — enough to recognise it, not enough to flood the window. */\nfunction renderArgs(args: Record<string, unknown> | undefined): string {\n\tif (!args) return \"\";\n\tconst parts: string[] = [];\n\tfor (const [key, value] of Object.entries(args)) {\n\t\tif (typeof value === \"string\") {\n\t\t\tparts.push(`${key}=${value.length > 200 ? `${value.slice(0, 200)}…` : value}`);\n\t\t} else if (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\t\tparts.push(`${key}=${value}`);\n\t\t}\n\t\t// Objects and arrays are structural detail the miner does not need.\n\t}\n\treturn parts.join(\" \");\n}\n\n/**\n * Render a session as plain text for the model.\n *\n * User turns the user actually typed go in whole and unfiltered — any\n * truncation there would quietly reintroduce the recall problem the old regex\n * gate had. What does not go in is text the user's tooling replayed: its own\n * past digests, and slash-command bodies.\n *\n * Assistant prose is dropped: it is the bulk of a transcript and almost none of\n * it is evidence about what the *user* wants. Tool calls are kept, because a\n * failure-then-pass is a fix. Successful tool output is dropped: it is a file\n * or a command's stdout, not a statement by anyone, and feeding it to a miner\n * looking for directives yields lines lifted out of plan files and configs\n * attributed to the user.\n */\nexport function renderTranscript(session: MinableSession, fingerprints: string[] = []): string {\n\tconst lines: string[] = [];\n\n\tfor (const entry of session.entries) {\n\t\tconst message = entry.type === \"message\" ? entry.message : undefined;\n\t\tif (!message) continue;\n\n\t\tif (message.role === \"user\") {\n\t\t\tconst text = textOf(message.content);\n\t\t\t// Skip the command's own past output, or proposals compound their counts.\n\t\t\tif (!text || text.startsWith(LEARN_DIGEST_MARKER)) continue;\n\t\t\tif (isReplayedTurn(text, fingerprints)) continue;\n\t\t\tlines.push(`USER: ${text}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const block of (message.content ?? []) as unknown[]) {\n\t\t\t\tif (!isToolCall(block)) continue;\n\t\t\t\tlines.push(`TOOL: ${block.name}(${renderArgs(block.arguments as Record<string, unknown>)})`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (message.role === \"toolResult\" && message.isError) {\n\t\t\tconst output = textOf(message.content);\n\t\t\tif (!output) continue;\n\t\t\tlines.push(`ERROR: ${output.length > TOOL_ERROR_CHARS ? `${output.slice(0, TOOL_ERROR_CHARS)}…` : output}`);\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n/** Everything the user actually said in a session, normalized, for checking quotes against. */\nexport function spokenText(session: MinableSession, fingerprints: string[] = []): string {\n\tconst parts: string[] = [];\n\tfor (const entry of session.entries) {\n\t\tconst message = entry.type === \"message\" ? entry.message : undefined;\n\t\tif (!message || message.role !== \"user\") continue;\n\t\tconst text = textOf(message.content);\n\t\tif (!text || text.startsWith(LEARN_DIGEST_MARKER)) continue;\n\t\tif (isReplayedTurn(text, fingerprints)) continue;\n\t\tparts.push(text);\n\t}\n\treturn normalizeForMatch(parts.join(\"\\n\"));\n}\n\n/**\n * Drop candidates whose quote cannot be found in what the user said.\n *\n * The miner is told to quote verbatim and the digest renders every quote inside\n * quotation marks, but on a real corpus a third of them appear nowhere in the\n * session: paraphrases, merged sentences, and lines lifted out of tool output.\n * A quote that cannot be located is evidence that cannot be shown, and a\n * proposal the reader cannot check is worse than one that was never made.\n *\n * Whitespace is normalized before comparing, because a directive written in a\n * markdown file arrives wrapped across lines and the model unwraps it.\n */\nexport function verifyCandidates(candidates: MinedCandidate[], spoken: string): MinedCandidate[] {\n\t// Normalized again rather than trusting the caller: the check is a substring\n\t// test, and one un-normalized argument would silently reject everything.\n\tconst haystack = normalizeForMatch(spoken);\n\tif (!haystack) return [];\n\treturn candidates.filter((candidate) => {\n\t\t// A fix is evidenced by commands and errors, not by something the user said.\n\t\tif (candidate.kind === \"fix\") return true;\n\t\treturn haystack.includes(normalizeForMatch(candidate.text));\n\t});\n}\n\n/**\n * Split rendered text on line boundaries, so a chunk never cuts a user turn in\n * half. A single turn longer than the budget gets its own oversized chunk\n * rather than being split — losing the second half of a long directive is\n * exactly the failure this rewrite exists to remove.\n */\nexport function chunkTranscript(text: string, chunkChars = FALLBACK_CHUNK_CHARS): string[] {\n\tif (text.length <= chunkChars) return text.length > 0 ? [text] : [];\n\n\tconst chunks: string[] = [];\n\tlet current: string[] = [];\n\tlet size = 0;\n\tfor (const line of text.split(\"\\n\")) {\n\t\tif (size > 0 && size + line.length + 1 > chunkChars) {\n\t\t\tchunks.push(current.join(\"\\n\"));\n\t\t\tcurrent = [];\n\t\t\tsize = 0;\n\t\t}\n\t\tcurrent.push(line);\n\t\tsize += line.length + 1;\n\t}\n\tif (current.length > 0) chunks.push(current.join(\"\\n\"));\n\treturn chunks;\n}\n\nconst MINER_SYSTEM_PROMPT = `You read one coding-session transcript and report durable signals in it.\n\nYou are the recall stage of a two-stage pipeline. A later stage counts how often each signal recurs ACROSS sessions and decides what is worth writing down. Your job is to notice and name, not to judge importance and not to count — you are seeing one session and cannot know what repeats.\n\nReport three kinds of thing.\n\n**directive** — the user stating a preference, correction, constraint, or fact about how they want work done. Include these regardless of phrasing. All of these are directives:\n- imperative: \"always run the tests before pushing\"\n- corrective: \"no, that's not how our error handling works\"\n- declarative: \"we're on bun now\", \"the API returns snake_case\"\n- preference stated once, in passing: \"I'd rather see this as a table\"\nA directive is how things should be done in general. What to do right now is a **request** — see below — not a directive. When a message contains both, report the directive part here.\n\n**fix** — a command that failed and later succeeded, where something in between was the cause. Report the failing command, a short error excerpt, and what changed in between.\n\n**request** — the user asking for a piece of work by name: \"open a release PR\", \"run the full check and fix what it finds\", \"give me a demo of X\". Report the request as they phrased it. A request repeated across sessions is a slash command waiting to be written, which is why it is worth reporting even though it is not a rule.\n\nA message can contain both a request and a directive — \"open a release PR, and remember to stage only your own files\" is one of each. Report both, separately.\n\nQuote \"text\" VERBATIM from the transcript. Do not paraphrase, merge two sentences, or tidy the wording: a quote that cannot be found in the session is discarded, because the reader is shown it in quotation marks and has to be able to check it.\n\nDo not name or group anything. A later stage sees every session at once and decides what counts as the same point; from inside one session you cannot know.\n\nOutput STRICT JSON, no markdown fence, no prose:\n{\"candidates\":[{\"kind\":\"directive\",\"text\":\"<verbatim quote>\",\"rationale\":\"<one clause on why it is durable>\"}]}\n\nFor fix items add: \"command\", \"errorExcerpt\", \"interveningCommands\" (array), \"editedFiles\" (array).\n\nReport nothing rather than padding. An empty list is a correct answer for a session that taught nothing: {\"candidates\":[]}`;\n\n/**\n * Pull the JSON object out of a model response.\n *\n * Models fence JSON even when told not to, and occasionally prepend a sentence.\n * Scanning for the outermost braces is more forgiving than trusting the format\n * and cheaper than a repair pass — and a chunk whose response cannot be parsed\n * is skipped, never fatal, because one bad chunk should not lose a whole run.\n */\nexport function parseCandidates(response: string): MinedCandidate[] {\n\tconst start = response.indexOf(\"{\");\n\tconst end = response.lastIndexOf(\"}\");\n\tif (start < 0 || end <= start) return [];\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(response.slice(start, end + 1));\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst raw = (parsed as { candidates?: unknown })?.candidates;\n\tif (!Array.isArray(raw)) return [];\n\n\tconst out: MinedCandidate[] = [];\n\tfor (const item of raw.slice(0, MAX_CANDIDATES_PER_CHUNK)) {\n\t\tif (!item || typeof item !== \"object\") continue;\n\t\tconst candidate = item as Record<string, unknown>;\n\t\tconst kind = candidate.kind;\n\t\tif (kind !== \"directive\" && kind !== \"fix\" && kind !== \"request\") continue;\n\n\t\tconst text = typeof candidate.text === \"string\" ? candidate.text.trim() : \"\";\n\t\t// Nothing to quote back means nothing to show the reader.\n\t\tif (!text) continue;\n\n\t\tconst strings = (value: unknown): string[] | undefined =>\n\t\t\tArray.isArray(value) ? value.filter((v): v is string => typeof v === \"string\").slice(0, 12) : undefined;\n\n\t\tout.push({\n\t\t\tkind,\n\t\t\ttext,\n\t\t\trationale: typeof candidate.rationale === \"string\" ? candidate.rationale.trim() : undefined,\n\t\t\tcommand: typeof candidate.command === \"string\" ? candidate.command : undefined,\n\t\t\terrorExcerpt: typeof candidate.errorExcerpt === \"string\" ? candidate.errorExcerpt.slice(0, 400) : undefined,\n\t\t\tinterveningCommands: strings(candidate.interveningCommands),\n\t\t\teditedFiles: strings(candidate.editedFiles),\n\t\t});\n\t}\n\treturn out;\n}\n\nexport interface MinerDeps {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\t/**\n\t * Literal runs from the slash-command bodies in force, from\n\t * `replayFingerprints`. User turns matching one are machinery replaying\n\t * itself, not the user speaking.\n\t */\n\treplayFingerprints?: string[];\n}\n\n/**\n * Build the real miner: one model call per chunk of one session.\n *\n * Chunks are mined sequentially rather than in parallel. A cold-cache run is\n * already the expensive path, and firing every chunk of every session at once\n * is how you trip a provider rate limit on exactly the run that has the most to\n * do.\n */\nexport function createLlmMiner(deps: MinerDeps): Miner {\n\tconst chunkChars = chunkCharsForModel(deps.model);\n\tconst fingerprints = deps.replayFingerprints ?? [];\n\treturn async (session, signal) => {\n\t\tconst chunks = chunkTranscript(renderTranscript(session, fingerprints), chunkChars);\n\t\tconst candidates: MinedCandidate[] = [];\n\n\t\tfor (const chunk of chunks) {\n\t\t\tif (signal?.aborted) break;\n\n\t\t\tconst response = await completeSimple(\n\t\t\t\tdeps.model,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MINER_SYSTEM_PROMPT,\n\t\t\t\t\tmessages: [{ role: \"user\", content: [{ type: \"text\", text: chunk }], timestamp: Date.now() }],\n\t\t\t\t},\n\t\t\t\t{ maxTokens: MAX_RESPONSE_TOKENS, signal, apiKey: deps.apiKey, headers: deps.headers },\n\t\t\t);\n\n\t\t\tif (response.stopReason === \"error\") {\n\t\t\t\tthrow new Error(response.errorMessage || \"miner call failed\");\n\t\t\t}\n\n\t\t\tconst text = response.content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t\tcandidates.push(...parseCandidates(text));\n\t\t}\n\n\t\t// Verify against the whole session rather than the chunk that produced the\n\t\t// candidate: a quote can legitimately straddle a chunk boundary, and a\n\t\t// dropped-for-being-unfindable verdict has to mean unfindable anywhere.\n\t\treturn verifyCandidates(candidates, spokenText(session, fingerprints));\n\t};\n}\n"]}