@kolisachint/hoocode-agent 0.5.18 → 0.5.20

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 (61) hide show
  1. package/CHANGELOG.md +179 -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.map +1 -1
  7. package/dist/core/learn/cache.js +14 -2
  8. package/dist/core/learn/cache.js.map +1 -1
  9. package/dist/core/learn/cluster.d.ts +78 -0
  10. package/dist/core/learn/cluster.d.ts.map +1 -0
  11. package/dist/core/learn/cluster.js +184 -0
  12. package/dist/core/learn/cluster.js.map +1 -0
  13. package/dist/core/learn/coverage.d.ts.map +1 -1
  14. package/dist/core/learn/coverage.js +2 -0
  15. package/dist/core/learn/coverage.js.map +1 -1
  16. package/dist/core/learn/digest.d.ts +12 -0
  17. package/dist/core/learn/digest.d.ts.map +1 -1
  18. package/dist/core/learn/digest.js +86 -14
  19. package/dist/core/learn/digest.js.map +1 -1
  20. package/dist/core/learn/extract.d.ts +39 -4
  21. package/dist/core/learn/extract.d.ts.map +1 -1
  22. package/dist/core/learn/extract.js +170 -34
  23. package/dist/core/learn/extract.js.map +1 -1
  24. package/dist/core/learn/mine.d.ts +78 -23
  25. package/dist/core/learn/mine.d.ts.map +1 -1
  26. package/dist/core/learn/mine.js +142 -37
  27. package/dist/core/learn/mine.js.map +1 -1
  28. package/dist/core/learn/reduce.d.ts +19 -8
  29. package/dist/core/learn/reduce.d.ts.map +1 -1
  30. package/dist/core/learn/reduce.js +69 -13
  31. package/dist/core/learn/reduce.js.map +1 -1
  32. package/dist/core/learn/state.d.ts +11 -18
  33. package/dist/core/learn/state.d.ts.map +1 -1
  34. package/dist/core/learn/state.js +23 -34
  35. package/dist/core/learn/state.js.map +1 -1
  36. package/dist/core/settings-defaults.d.ts +1 -1
  37. package/dist/core/settings-defaults.d.ts.map +1 -1
  38. package/dist/core/settings-defaults.js +1 -1
  39. package/dist/core/settings-defaults.js.map +1 -1
  40. package/dist/core/settings-manager.d.ts +2 -2
  41. package/dist/core/settings-manager.d.ts.map +1 -1
  42. package/dist/core/settings-manager.js +1 -1
  43. package/dist/core/settings-manager.js.map +1 -1
  44. package/dist/core/settings-types.d.ts +1 -1
  45. package/dist/core/settings-types.d.ts.map +1 -1
  46. package/dist/core/settings-types.js.map +1 -1
  47. package/dist/extensions/core/learn.d.ts.map +1 -1
  48. package/dist/extensions/core/learn.js +128 -73
  49. package/dist/extensions/core/learn.js.map +1 -1
  50. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  51. package/dist/modes/interactive/components/settings-selector.js +1 -1
  52. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  53. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/dist/modes/interactive/interactive-mode.js +1 -1
  55. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/docs/settings.md +9 -6
  57. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  58. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  59. package/examples/extensions/sandbox/package.json +1 -1
  60. package/examples/extensions/with-deps/package.json +1 -1
  61. package/package.json +4 -4
@@ -32,8 +32,9 @@ import { getUserAgentsDir } from "../../config.js";
32
32
  import { getSessionDirPath } from "../session-manager.js";
33
33
  import { loadSkills } from "../skills.js";
34
34
  import { hashSessionFile, pruneLearnCache, readCachedMining, writeCachedMining } from "./cache.js";
35
+ import { fallbackLabel } from "./cluster.js";
35
36
  import { noCoverageJudge } from "./coverage.js";
36
- import { reduceDirectives, reduceFixes, reduceWorkflows } from "./reduce.js";
37
+ import { reduceDirectives, reduceFixes, reduceRequests } from "./reduce.js";
37
38
  import { judge } from "./state.js";
38
39
  export { LEARN_DIGEST_MARKER } from "./mine.js";
39
40
  /** Sessions considered, newest first. */
@@ -44,10 +45,25 @@ const DEFAULT_MAX_AGE_DAYS = 30;
44
45
  const MAX_ENTRIES_PER_SESSION = 8000;
45
46
  /** Occurrences a directive needs before it is proposed. */
46
47
  const DEFAULT_MIN_DIRECTIVE_COUNT = 2;
47
- /** Repeats before a tool sequence is worth proposing as a skill. */
48
- const DEFAULT_MIN_WORKFLOW_COUNT = 3;
48
+ /**
49
+ * Sessions a request needs before it is worth proposing as a slash command.
50
+ *
51
+ * Higher than the directive bar. A rule you stated twice is a rule; a job you
52
+ * asked for twice may just be a job that came up twice. Three separate sessions
53
+ * is the point at which typing it again is the expensive option.
54
+ */
55
+ const DEFAULT_MIN_REQUEST_COUNT = 3;
49
56
  /** Cap on each list in the digest, so the model's budget goes to the top signals. */
50
57
  const DEFAULT_MAX_PER_CATEGORY = 8;
58
+ /** Newest entry timestamp on the branch, falling back to when the session opened. */
59
+ function lastActivityOf(entries, fallback) {
60
+ let latest = "";
61
+ for (const entry of entries) {
62
+ if (typeof entry.timestamp === "string" && entry.timestamp > latest)
63
+ latest = entry.timestamp;
64
+ }
65
+ return latest || fallback;
66
+ }
51
67
  /**
52
68
  * Reduce a session's raw entries to the branch that was actually taken.
53
69
  *
@@ -142,11 +158,14 @@ function parseSessionFile(file, cwd, onSkip) {
142
158
  onSkip("unreadable");
143
159
  return undefined;
144
160
  }
161
+ const branch = activeBranch(entries);
162
+ const opened = header?.timestamp ?? statSync(file).mtime.toISOString();
145
163
  return {
146
164
  file,
147
165
  id: header?.id ?? file,
148
- timestamp: header?.timestamp ?? statSync(file).mtime.toISOString(),
149
- entries: activeBranch(entries),
166
+ timestamp: opened,
167
+ lastActivity: lastActivityOf(branch, opened),
168
+ entries: branch,
150
169
  };
151
170
  }
152
171
  /**
@@ -261,8 +280,9 @@ function listSessions(options) {
261
280
  * signal off the list.
262
281
  */
263
282
  function applySuppression(items, state, maxProposals, covered, onDeclined) {
264
- if (!state)
265
- return { kept: items.slice(0, maxProposals), suppressed: 0 };
283
+ if (!state) {
284
+ return { kept: items.slice(0, maxProposals), suppressed: 0, cut: Math.max(0, items.length - maxProposals) };
285
+ }
266
286
  const kept = [];
267
287
  let suppressed = 0;
268
288
  for (const item of items) {
@@ -275,7 +295,11 @@ function applySuppression(items, state, maxProposals, covered, onDeclined) {
275
295
  onDeclined?.(item);
276
296
  kept.push(item);
277
297
  }
278
- return { kept: kept.slice(0, maxProposals), suppressed };
298
+ // Anything past the cap cleared every bar and lost on rank alone. It is not
299
+ // suppressed and it is not bookmarked, so it will be back next run — but a
300
+ // digest that silently shows eight of twenty reads as "twenty is all there
301
+ // was", and the reader tunes the wrong knob.
302
+ return { kept: kept.slice(0, maxProposals), suppressed, cut: Math.max(0, kept.length - maxProposals) };
279
303
  }
280
304
  /**
281
305
  * Where this cwd's sessions were found and what was passed over, without
@@ -308,6 +332,61 @@ export function planMining(options) {
308
332
  }
309
333
  return { total: sessions.length, cached, pending };
310
334
  }
335
+ /**
336
+ * Name every candidate in the window in one place.
337
+ *
338
+ * The pass runs over the whole window at once rather than per session, which is
339
+ * the entire point: "is this the same point as that" is unanswerable from
340
+ * inside one transcript. Labels already on record are offered as vocabulary so
341
+ * an item you decided on keeps the key it was bookmarked under — without that,
342
+ * a renamed cluster reads as brand new and suppression quietly stops working.
343
+ *
344
+ * A failed call falls back to naming each candidate after its own wording,
345
+ * which groups identical sentences and nothing else. That is the behaviour the
346
+ * pipeline had before this stage existed, so a clustering outage costs recall,
347
+ * not the run.
348
+ */
349
+ async function labelSessions(raw, clusterer, knownLabels, signal) {
350
+ const inputs = [];
351
+ const origin = [];
352
+ for (const [sessionIndex, session] of raw.entries()) {
353
+ for (const [candidateIndex, candidate] of session.candidates.entries()) {
354
+ inputs.push({ id: inputs.length, kind: candidate.kind, text: candidate.text });
355
+ origin.push({ session: sessionIndex, candidate: candidateIndex });
356
+ }
357
+ }
358
+ let labels = new Map();
359
+ if (clusterer && inputs.length > 0) {
360
+ try {
361
+ labels = await clusterer(inputs, knownLabels, signal);
362
+ }
363
+ catch {
364
+ // Fall through to per-text labels below.
365
+ }
366
+ }
367
+ const labelled = raw.map((session) => ({
368
+ sessionId: session.sessionId,
369
+ lastActivity: session.lastActivity,
370
+ candidates: [],
371
+ }));
372
+ for (const [index, input] of inputs.entries()) {
373
+ const where = origin[index];
374
+ const candidate = where ? raw[where.session]?.candidates[where.candidate] : undefined;
375
+ if (!where || !candidate)
376
+ continue;
377
+ labelled[where.session]?.candidates.push({
378
+ ...candidate,
379
+ label: labels.get(input.id) ?? fallbackLabel(input.text),
380
+ });
381
+ }
382
+ return labelled;
383
+ }
384
+ /** Labels already on record, so the naming pass can reuse rather than reinvent them. */
385
+ function knownLabelsFrom(state) {
386
+ if (!state)
387
+ return [];
388
+ return Object.keys(state.surfaced).map((key) => key.slice(key.indexOf(":") + 1));
389
+ }
311
390
  /** Nearest AGENTS.md walking up from cwd, so proposals can be checked against it. */
312
391
  function findAgentsFile(cwd) {
313
392
  let dir = resolve(cwd);
@@ -323,16 +402,50 @@ function findAgentsFile(cwd) {
323
402
  dir = parent;
324
403
  }
325
404
  }
405
+ /**
406
+ * Turn one context file into rule lines the coverage judge can reason about.
407
+ *
408
+ * Headings were dropped and the lines under them sent bare, which asks the
409
+ * model to decide whether a proposal is in scope using text with the scope
410
+ * removed — "stage only your own files" reads very differently under "Git Rules
411
+ * for Parallel Agents" than on its own. So each line carries its heading path,
412
+ * and the scope it came from, since the corpus spans a repo file and two user
413
+ * ones and a rule's home decides who it binds.
414
+ *
415
+ * Fenced blocks go: a code sample illustrates a rule, it is not one, and on a
416
+ * real file it is a large share of the non-bullet text.
417
+ */
418
+ function ruleLinesOf(content, scope) {
419
+ const lines = [];
420
+ const headings = [];
421
+ let inFence = false;
422
+ for (const raw of content.split("\n")) {
423
+ const line = raw.trim();
424
+ if (line.startsWith("```")) {
425
+ inFence = !inFence;
426
+ continue;
427
+ }
428
+ if (inFence || line.length === 0)
429
+ continue;
430
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
431
+ if (heading) {
432
+ const depth = heading[1]?.length ?? 1;
433
+ headings.length = Math.min(headings.length, depth - 1);
434
+ headings[depth - 1] = heading[2] ?? "";
435
+ continue;
436
+ }
437
+ const path = headings.filter(Boolean).join(" > ");
438
+ lines.push(path ? `[${scope}] ${path} > ${line}` : `[${scope}] ${line}`);
439
+ }
440
+ return lines;
441
+ }
326
442
  /** Assemble the coverage index for a directory. */
327
443
  export function buildCoverageIndex(options) {
328
- const corpus = coverageCorpus(options.agentDir, findAgentsFile(options.cwd));
329
- return {
330
- ruleLines: corpus
331
- .split("\n")
332
- .map((line) => line.trim())
333
- .filter((line) => line.length > 0 && !line.startsWith("#")),
334
- skills: options.skills ?? loadSkillIndex(options.cwd, options.agentDir),
335
- };
444
+ const ruleLines = [];
445
+ for (const file of coverageFiles(options.agentDir, findAgentsFile(options.cwd))) {
446
+ ruleLines.push(...ruleLinesOf(file.content, file.scope));
447
+ }
448
+ return { ruleLines, skills: options.skills ?? loadSkillIndex(options.cwd, options.agentDir) };
336
449
  }
337
450
  /**
338
451
  * Text a proposal is checked against to decide whether it is already written
@@ -342,19 +455,24 @@ export function buildCoverageIndex(options) {
342
455
  * user scope. Checking only the repo file would report a rule you accepted into
343
456
  * `~/.agents/AGENTS.md` as declined.
344
457
  */
345
- function coverageCorpus(agentDir, repoFile) {
346
- const parts = [];
347
- for (const candidate of [repoFile, join(getUserAgentsDir(), "AGENTS.md"), join(agentDir, "AGENTS.md")]) {
348
- if (!candidate || !existsSync(candidate))
458
+ function coverageFiles(agentDir, repoFile) {
459
+ const files = [];
460
+ const candidates = [
461
+ { scope: "repo", path: repoFile },
462
+ { scope: "user", path: join(getUserAgentsDir(), "AGENTS.md") },
463
+ { scope: "user", path: join(agentDir, "AGENTS.md") },
464
+ ];
465
+ for (const candidate of candidates) {
466
+ if (!candidate.path || !existsSync(candidate.path))
349
467
  continue;
350
468
  try {
351
- parts.push(readFileSync(candidate, "utf-8"));
469
+ files.push({ scope: candidate.scope, content: readFileSync(candidate.path, "utf-8") });
352
470
  }
353
471
  catch {
354
472
  // Unreadable context file: treat as absent rather than failing the run.
355
473
  }
356
474
  }
357
- return parts.join("\n");
475
+ return files;
358
476
  }
359
477
  /**
360
478
  * Skills a proposal could already have become.
@@ -401,7 +519,7 @@ async function mineSessions(sessions, options) {
401
519
  const hash = hashSessionFile(session.file);
402
520
  const cached = hash ? readCachedMining(options.agentDir, hash) : undefined;
403
521
  if (cached) {
404
- mined.push({ sessionId: session.id, timestamp: session.timestamp, candidates: cached.candidates });
522
+ mined.push({ sessionId: session.id, lastActivity: session.lastActivity, candidates: cached.candidates });
405
523
  report.cached++;
406
524
  done++;
407
525
  options.onProgress?.({ done, total: sessions.length, cached: report.cached });
@@ -410,7 +528,7 @@ async function mineSessions(sessions, options) {
410
528
  const minable = { id: session.id, timestamp: session.timestamp, entries: session.entries };
411
529
  try {
412
530
  const candidates = await options.miner(minable, options.signal);
413
- mined.push({ sessionId: session.id, timestamp: session.timestamp, candidates });
531
+ mined.push({ sessionId: session.id, lastActivity: session.lastActivity, candidates });
414
532
  report.mined++;
415
533
  if (hash) {
416
534
  writeCachedMining(options.agentDir, hash, {
@@ -464,16 +582,30 @@ export async function mineLearnDigest(options) {
464
582
  }
465
583
  const { mined, report, aborted } = await mineSessions(sessions, options);
466
584
  pruneLearnCache(options.agentDir, options.now);
585
+ const state = options.ignoreState ? undefined : options.state;
586
+ // Named against the labels already on record — including in `all` mode, where
587
+ // suppression is off but the bookmark still has to line up next run.
588
+ const labelled = await labelSessions(mined, options.clusterer, knownLabelsFrom(options.state), options.signal);
467
589
  const minRepeats = options.minRepeats ?? DEFAULT_MIN_DIRECTIVE_COUNT;
468
590
  const maxProposals = options.maxProposals ?? DEFAULT_MAX_PER_CATEGORY;
469
- const directives = reduceDirectives(mined, minRepeats);
470
- const fixes = reduceFixes(mined, minRepeats);
471
- const workflows = reduceWorkflows(mined, options.minWorkflowRepeats ?? DEFAULT_MIN_WORKFLOW_COUNT);
591
+ const directives = reduceDirectives(labelled, minRepeats);
592
+ const fixes = reduceFixes(labelled, minRepeats);
593
+ const requests = reduceRequests(labelled, options.minRequestRepeats ?? DEFAULT_MIN_REQUEST_COUNT);
594
+ // Counted with the threshold at 1, which is the same reduce over the same
595
+ // input — so the difference is exactly what the threshold cost, rather than an
596
+ // estimate of it.
597
+ const everyPoint = reduceDirectives(labelled, 1).length + reduceFixes(labelled, 1).length + reduceRequests(labelled, 1).length;
598
+ const funnel = {
599
+ candidates: labelled.reduce((sum, session) => sum + session.candidates.length, 0),
600
+ points: everyPoint,
601
+ belowThreshold: everyPoint - directives.length - fixes.length - requests.length,
602
+ };
472
603
  // Coverage is asked only about what survived the repeat threshold. Judging
473
604
  // everything would mean sending the context file alongside a long tail of
474
605
  // one-off observations that are never going to be proposed.
475
606
  const coverage = buildCoverageIndex({ cwd: options.cwd, agentDir: options.agentDir, skills: options.skills });
476
607
  const queries = directives.map((d) => ({ label: d.label, text: d.text }));
608
+ let coverageFailed = false;
477
609
  try {
478
610
  const verdicts = await (options.coverageJudge ?? noCoverageJudge)(queries, coverage, options.signal);
479
611
  applyCoverage(directives, verdicts);
@@ -481,20 +613,21 @@ export async function mineLearnDigest(options) {
481
613
  catch {
482
614
  // A failed coverage call leaves everything `new`, which over-proposes
483
615
  // slightly. That is the right way to fail: the reader can reject a
484
- // duplicate, but cannot recover a proposal that was wrongly withheld.
616
+ // duplicate, but cannot recover a proposal that was wrongly withheld. What
617
+ // must not happen is writing that guess down as if it were a reading.
618
+ coverageFailed = true;
485
619
  }
486
620
  const timestamps = sessions.map((s) => s.timestamp).sort();
487
- const state = options.ignoreState ? undefined : options.state;
488
621
  // Directives carry a real coverage signal — is this written down as a rule or
489
622
  // a skill right now? — which is what separates an adopted proposal from a
490
- // declined one. Fixes and workflows do not: a fix may have become a rule, a
623
+ // declined one. Fixes and requests do not: a fix may have become a rule, a
491
624
  // skill, or a habit, and which one is not recoverable here, so they get
492
625
  // suppression only and are never labelled declined.
493
626
  const keptDirectives = applySuppression(directives, state, maxProposals, (item) => item.status !== "new", (item) => {
494
627
  item.previouslyDeclined = true;
495
628
  });
496
629
  const keptFixes = applySuppression(fixes, state, maxProposals, () => false);
497
- const keptWorkflows = applySuppression(workflows, state, maxProposals, () => false);
630
+ const keptRequests = applySuppression(requests, state, maxProposals, () => false);
498
631
  const surfaced = [
499
632
  // Directives carry their wording forward so a later `/learn stats` can ask
500
633
  // about coverage using the sentence rather than the slug that names it.
@@ -505,7 +638,7 @@ export async function mineLearnDigest(options) {
505
638
  text: d.text,
506
639
  })),
507
640
  ...keptFixes.kept.map((f) => ({ key: f.key, lastSeen: f.lastSeen, covered: false })),
508
- ...keptWorkflows.kept.map((w) => ({ key: w.key, lastSeen: w.lastSeen, covered: false })),
641
+ ...keptRequests.kept.map((r) => ({ key: r.key, lastSeen: r.lastSeen, covered: false })),
509
642
  ];
510
643
  return {
511
644
  scannedSessions: sessions.length,
@@ -513,14 +646,17 @@ export async function mineLearnDigest(options) {
513
646
  scan,
514
647
  mining: report,
515
648
  aborted,
649
+ coverageFailed,
516
650
  oldestSession: timestamps[0],
517
651
  newestSession: timestamps[timestamps.length - 1],
518
652
  agentsFilePath,
519
653
  agentsFileTokens: agentsContent === undefined ? undefined : Math.round(Buffer.byteLength(agentsContent, "utf-8") / 4),
520
654
  directives: keptDirectives.kept,
521
655
  fixes: keptFixes.kept,
522
- workflows: keptWorkflows.kept,
523
- suppressed: keptDirectives.suppressed + keptFixes.suppressed + keptWorkflows.suppressed,
656
+ requests: keptRequests.kept,
657
+ suppressed: keptDirectives.suppressed + keptFixes.suppressed + keptRequests.suppressed,
658
+ cut: keptDirectives.cut + keptFixes.cut + keptRequests.cut,
659
+ funnel,
524
660
  surfaced,
525
661
  };
526
662
  }
@@ -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 * 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 { CoverageIndex, CoverageJudge, CoverageQuery } from \"./coverage.js\";\nimport { noCoverageJudge } from \"./coverage.js\";\nimport type { MinableSession, Miner } from \"./mine.js\";\nimport type { DirectiveCluster, FixCandidate, MinedSession, Proposable, WorkflowCandidate } from \"./reduce.js\";\nimport { reduceDirectives, reduceFixes, reduceWorkflows } 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, WorkflowCandidate } 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/** 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 * 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\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; 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\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\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/** 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\ttimestamp: string;\n\tentries: EntryLike[];\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\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 * 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/** 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/** 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/**\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: MinedSession[]; report: MiningReport; aborted: boolean }> {\n\tconst mined: MinedSession[] = [];\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, timestamp: session.timestamp, 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, timestamp: session.timestamp, 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 minRepeats = options.minRepeats ?? DEFAULT_MIN_DIRECTIVE_COUNT;\n\tconst maxProposals = options.maxProposals ?? DEFAULT_MAX_PER_CATEGORY;\n\tconst directives = reduceDirectives(mined, minRepeats);\n\tconst fixes = reduceFixes(mined, minRepeats);\n\tconst workflows = reduceWorkflows(mined, options.minWorkflowRepeats ?? DEFAULT_MIN_WORKFLOW_COUNT);\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\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.\n\t}\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 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 keptWorkflows = applySuppression(workflows, 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...keptWorkflows.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\tmining: report,\n\t\taborted,\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\tworkflows: keptWorkflows.kept,\n\t\tsuppressed: keptDirectives.suppressed + keptFixes.suppressed + keptWorkflows.suppressed,\n\t\tsurfaced,\n\t};\n}\n"]}
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 * 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"]}