@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
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The reduce half of `/learn`: turn per-session observations into counts.
3
+ *
4
+ * The split with `mine.ts` is the whole design. Deciding that "we're on bun
5
+ * now" and "stop using npm" mean the same thing is semantics, and the model is
6
+ * better at it than any normalizer — so the model does it, by emitting a shared
7
+ * `label`. Deciding that the shared label occurred nine times across five
8
+ * sessions is arithmetic, and arithmetic stays here, because a model asked to
9
+ * count over a long context will be approximately right, and an approximately
10
+ * right number is worse than none when the number is the thing the reader acts
11
+ * on.
12
+ *
13
+ * Nothing in this file filters on content. The only gate is the repeat
14
+ * threshold, which is a dial the user sets and the digest reports, not a
15
+ * whitelist they cannot see.
16
+ */
17
+ /**
18
+ * Group every candidate of one kind by its label.
19
+ *
20
+ * `lastSeen` takes the session's last activity rather than anything the model
21
+ * reports: the model is reading a transcript and has no reliable clock, and
22
+ * `lastSeen` drives suppression, where a wrong value silently hides a live
23
+ * signal or resurfaces a dead one. It is deliberately not the session's start
24
+ * either — a long-running session would date today's words to the day it was
25
+ * opened, and suppression would call them old news.
26
+ */
27
+ function groupByLabel(sessions, kind) {
28
+ const acc = new Map();
29
+ for (const session of sessions) {
30
+ for (const candidate of session.candidates) {
31
+ if (candidate.kind !== kind)
32
+ continue;
33
+ const existing = acc.get(candidate.label);
34
+ if (existing) {
35
+ existing.count++;
36
+ existing.sessions.add(session.sessionId);
37
+ if (session.lastActivity > existing.lastSeen)
38
+ existing.lastSeen = session.lastActivity;
39
+ // Keep the fullest quote: a longer one carries more of the reasoning.
40
+ if (candidate.text.length > existing.text.length)
41
+ existing.text = candidate.text;
42
+ existing.rationale ??= candidate.rationale;
43
+ existing.samples.push(candidate);
44
+ }
45
+ else {
46
+ acc.set(candidate.label, {
47
+ label: candidate.label,
48
+ text: candidate.text,
49
+ rationale: candidate.rationale,
50
+ count: 1,
51
+ sessions: new Set([session.sessionId]),
52
+ lastSeen: session.lastActivity,
53
+ samples: [candidate],
54
+ });
55
+ }
56
+ }
57
+ }
58
+ return [...acc.values()];
59
+ }
60
+ /**
61
+ * Merge accumulators whose representative quote is the same sentence.
62
+ *
63
+ * The model labels each session independently and has no way to see what it
64
+ * called the same thing last time, so identical text routinely arrives under
65
+ * two labels — and then reads as two separate proposals, the same words twice.
66
+ * Matching on the quote costs nothing and is the one case where sameness is not
67
+ * a judgement call.
68
+ *
69
+ * This runs before the repeat threshold on purpose: two occurrences split
70
+ * across two labels are each below the bar, and merging them afterwards would
71
+ * mean neither was ever considered.
72
+ *
73
+ * The surviving label is the lexicographically smallest of the merged set, not
74
+ * the first one seen. First-seen follows session order, which changes whenever
75
+ * a session is added — so the merged cluster would silently change its name
76
+ * between runs, the state key with it, and every item the reader had already
77
+ * decided on would come back as new. A merge has to be a pure function of what
78
+ * was merged.
79
+ */
80
+ function mergeIdenticalText(entries) {
81
+ const byText = new Map();
82
+ const order = [];
83
+ for (const entry of entries) {
84
+ const key = entry.text.replace(/\s+/g, " ").trim().toLowerCase();
85
+ const existing = byText.get(key);
86
+ if (!existing) {
87
+ byText.set(key, entry);
88
+ order.push(entry);
89
+ continue;
90
+ }
91
+ existing.count += entry.count;
92
+ for (const session of entry.sessions)
93
+ existing.sessions.add(session);
94
+ if (entry.lastSeen > existing.lastSeen)
95
+ existing.lastSeen = entry.lastSeen;
96
+ existing.rationale ??= entry.rationale;
97
+ existing.samples.push(...entry.samples);
98
+ if (entry.label < existing.label)
99
+ existing.label = entry.label;
100
+ }
101
+ return order;
102
+ }
103
+ /** Distinct sessions first, then raw count: five sessions beats nine times in one. */
104
+ function byEvidence(a, b) {
105
+ return b.sessions - a.sessions || b.count - a.count || a.label.localeCompare(b.label);
106
+ }
107
+ /** Merge repeated string fields across a cluster's samples, preserving order and dropping dupes. */
108
+ function mergeStrings(samples, pick) {
109
+ const out = [];
110
+ const seen = new Set();
111
+ for (const sample of samples) {
112
+ for (const value of pick(sample) ?? []) {
113
+ if (seen.has(value))
114
+ continue;
115
+ seen.add(value);
116
+ out.push(value);
117
+ }
118
+ }
119
+ return out.slice(0, 12);
120
+ }
121
+ /**
122
+ * The repeat threshold counts distinct sessions, not occurrences.
123
+ *
124
+ * Saying a thing twice inside one session is the commonest thing in a
125
+ * transcript and usually means the opposite of durable: the agent ignored it
126
+ * the first time, so it was restated. Two *sessions* is a claim about how you
127
+ * work; two lines in one session is a claim about one afternoon.
128
+ */
129
+ function repeatedEnough(entry, minRepeats) {
130
+ return entry.sessions.size >= minRepeats;
131
+ }
132
+ export function reduceDirectives(sessions, minRepeats) {
133
+ return mergeIdenticalText(groupByLabel(sessions, "directive"))
134
+ .filter((entry) => repeatedEnough(entry, minRepeats))
135
+ .map((entry) => ({
136
+ key: `directive:${entry.label}`,
137
+ label: entry.label,
138
+ text: entry.text,
139
+ rationale: entry.rationale,
140
+ count: entry.count,
141
+ sessions: entry.sessions.size,
142
+ lastSeen: entry.lastSeen,
143
+ // Coverage is decided later, by a model that can tell a paraphrase from a
144
+ // coincidence. Everything starts `new` and is corrected in place.
145
+ status: "new",
146
+ previouslyDeclined: false,
147
+ }))
148
+ .sort(byEvidence);
149
+ }
150
+ export function reduceFixes(sessions, minRepeats) {
151
+ return groupByLabel(sessions, "fix")
152
+ .filter((entry) => repeatedEnough(entry, minRepeats))
153
+ .map((entry) => ({
154
+ key: `fix:${entry.label}`,
155
+ label: entry.label,
156
+ command: entry.samples.find((s) => s.command)?.command ?? entry.text,
157
+ errorExcerpt: entry.samples.find((s) => s.errorExcerpt)?.errorExcerpt ?? "",
158
+ interveningCommands: mergeStrings(entry.samples, (s) => s.interveningCommands),
159
+ editedFiles: mergeStrings(entry.samples, (s) => s.editedFiles),
160
+ count: entry.count,
161
+ sessions: entry.sessions.size,
162
+ lastSeen: entry.lastSeen,
163
+ }))
164
+ .sort(byEvidence);
165
+ }
166
+ export function reduceRequests(sessions, minRepeats) {
167
+ return mergeIdenticalText(groupByLabel(sessions, "request"))
168
+ .filter((entry) => repeatedEnough(entry, minRepeats))
169
+ .map((entry) => ({
170
+ key: `request:${entry.label}`,
171
+ label: entry.label,
172
+ text: entry.text,
173
+ count: entry.count,
174
+ sessions: entry.sessions.size,
175
+ lastSeen: entry.lastSeen,
176
+ }))
177
+ .sort(byEvidence);
178
+ }
179
+ //# sourceMappingURL=reduce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reduce.js","sourceRoot":"","sources":["../../../src/core/learn/reduce.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The reduce half of `/learn`: turn per-session observations into counts.\n *\n * The split with `mine.ts` is the whole design. Deciding that \"we're on bun\n * now\" and \"stop using npm\" mean the same thing is semantics, and the model is\n * better at it than any normalizer — so the model does it, by emitting a shared\n * `label`. Deciding that the shared label occurred nine times across five\n * sessions is arithmetic, and arithmetic stays here, because a model asked to\n * count over a long context will be approximately right, and an approximately\n * right number is worse than none when the number is the thing the reader acts\n * on.\n *\n * Nothing in this file filters on content. The only gate is the repeat\n * threshold, which is a dial the user sets and the digest reports, not a\n * whitelist they cannot see.\n */\n\nimport type { LabelledCandidate, MinedCandidate } from \"./mine.js\";\n\n/** Where a repeated directive already lives, if anywhere. */\nexport type DirectiveStatus = \"new\" | \"restated\" | \"has-skill\";\n\n/** Fields every proposable item shares, so suppression can be applied uniformly. */\nexport interface 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/** The model's canonical name for what was meant. The clustering key. */\n\tlabel: string;\n\t/** Representative verbatim quote, the longest seen in the cluster. */\n\ttext: string;\n\t/** Why it is durable, in the model's words. */\n\trationale?: 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 the reader saw this proposal and passed on it.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\nexport interface FixCandidate extends Proposable {\n\tlabel: string;\n\t/** The failing command. */\n\tcommand: string;\n\t/** Short excerpt of 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\tcount: number;\n\tsessions: number;\n}\n\n/**\n * A piece of work the user keeps asking for by name.\n *\n * This is the slash-command signal, and it was being thrown away: the miner\n * used to be told not to report task requests at all. But a request repeated\n * across sessions is exactly what a slash command is for, and the evidence for\n * it is stronger than for most rules — the same job, asked for again, in\n * someone's own words.\n */\nexport interface RequestCandidate extends Proposable {\n\tlabel: string;\n\t/** How the user phrased it, so the proposal can quote rather than invent. */\n\ttext: string;\n\tcount: number;\n\tsessions: number;\n}\n\n/** One session's mining output, named and tagged with the identity the counts need. */\nexport interface MinedSession {\n\tsessionId: string;\n\t/** When the session was last written to — the clock suppression runs on. */\n\tlastActivity: string;\n\t/** Named by the global clustering pass; grouping here is exact-label arithmetic. */\n\tcandidates: LabelledCandidate[];\n}\n\ninterface Acc {\n\tlabel: string;\n\ttext: string;\n\trationale?: string;\n\tcount: number;\n\tsessions: Set<string>;\n\tlastSeen: string;\n\tsamples: MinedCandidate[];\n}\n\n/**\n * Group every candidate of one kind by its label.\n *\n * `lastSeen` takes the session's last activity rather than anything the model\n * reports: the model is reading a transcript and has no reliable clock, and\n * `lastSeen` drives suppression, where a wrong value silently hides a live\n * signal or resurfaces a dead one. It is deliberately not the session's start\n * either — a long-running session would date today's words to the day it was\n * opened, and suppression would call them old news.\n */\nfunction groupByLabel(sessions: MinedSession[], kind: MinedCandidate[\"kind\"]): Acc[] {\n\tconst acc = new Map<string, Acc>();\n\n\tfor (const session of sessions) {\n\t\tfor (const candidate of session.candidates) {\n\t\t\tif (candidate.kind !== kind) continue;\n\t\t\tconst existing = acc.get(candidate.label);\n\t\t\tif (existing) {\n\t\t\t\texisting.count++;\n\t\t\t\texisting.sessions.add(session.sessionId);\n\t\t\t\tif (session.lastActivity > existing.lastSeen) existing.lastSeen = session.lastActivity;\n\t\t\t\t// Keep the fullest quote: a longer one carries more of the reasoning.\n\t\t\t\tif (candidate.text.length > existing.text.length) existing.text = candidate.text;\n\t\t\t\texisting.rationale ??= candidate.rationale;\n\t\t\t\texisting.samples.push(candidate);\n\t\t\t} else {\n\t\t\t\tacc.set(candidate.label, {\n\t\t\t\t\tlabel: candidate.label,\n\t\t\t\t\ttext: candidate.text,\n\t\t\t\t\trationale: candidate.rationale,\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tsessions: new Set([session.sessionId]),\n\t\t\t\t\tlastSeen: session.lastActivity,\n\t\t\t\t\tsamples: [candidate],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()];\n}\n\n/**\n * Merge accumulators whose representative quote is the same sentence.\n *\n * The model labels each session independently and has no way to see what it\n * called the same thing last time, so identical text routinely arrives under\n * two labels — and then reads as two separate proposals, the same words twice.\n * Matching on the quote costs nothing and is the one case where sameness is not\n * a judgement call.\n *\n * This runs before the repeat threshold on purpose: two occurrences split\n * across two labels are each below the bar, and merging them afterwards would\n * mean neither was ever considered.\n *\n * The surviving label is the lexicographically smallest of the merged set, not\n * the first one seen. First-seen follows session order, which changes whenever\n * a session is added — so the merged cluster would silently change its name\n * between runs, the state key with it, and every item the reader had already\n * decided on would come back as new. A merge has to be a pure function of what\n * was merged.\n */\nfunction mergeIdenticalText(entries: Acc[]): Acc[] {\n\tconst byText = new Map<string, Acc>();\n\tconst order: Acc[] = [];\n\n\tfor (const entry of entries) {\n\t\tconst key = entry.text.replace(/\\s+/g, \" \").trim().toLowerCase();\n\t\tconst existing = byText.get(key);\n\t\tif (!existing) {\n\t\t\tbyText.set(key, entry);\n\t\t\torder.push(entry);\n\t\t\tcontinue;\n\t\t}\n\t\texisting.count += entry.count;\n\t\tfor (const session of entry.sessions) existing.sessions.add(session);\n\t\tif (entry.lastSeen > existing.lastSeen) existing.lastSeen = entry.lastSeen;\n\t\texisting.rationale ??= entry.rationale;\n\t\texisting.samples.push(...entry.samples);\n\t\tif (entry.label < existing.label) existing.label = entry.label;\n\t}\n\n\treturn order;\n}\n\n/** Distinct sessions first, then raw count: five sessions beats nine times in one. */\nfunction byEvidence(a: { sessions: number; count: number; label: string }, b: typeof a): number {\n\treturn b.sessions - a.sessions || b.count - a.count || a.label.localeCompare(b.label);\n}\n\n/** Merge repeated string fields across a cluster's samples, preserving order and dropping dupes. */\nfunction mergeStrings(samples: MinedCandidate[], pick: (c: MinedCandidate) => string[] | undefined): string[] {\n\tconst out: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const sample of samples) {\n\t\tfor (const value of pick(sample) ?? []) {\n\t\t\tif (seen.has(value)) continue;\n\t\t\tseen.add(value);\n\t\t\tout.push(value);\n\t\t}\n\t}\n\treturn out.slice(0, 12);\n}\n\n/**\n * The repeat threshold counts distinct sessions, not occurrences.\n *\n * Saying a thing twice inside one session is the commonest thing in a\n * transcript and usually means the opposite of durable: the agent ignored it\n * the first time, so it was restated. Two *sessions* is a claim about how you\n * work; two lines in one session is a claim about one afternoon.\n */\nfunction repeatedEnough(entry: Acc, minRepeats: number): boolean {\n\treturn entry.sessions.size >= minRepeats;\n}\n\nexport function reduceDirectives(sessions: MinedSession[], minRepeats: number): DirectiveCluster[] {\n\treturn mergeIdenticalText(groupByLabel(sessions, \"directive\"))\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `directive:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\ttext: entry.text,\n\t\t\trationale: entry.rationale,\n\t\t\tcount: entry.count,\n\t\t\tsessions: entry.sessions.size,\n\t\t\tlastSeen: entry.lastSeen,\n\t\t\t// Coverage is decided later, by a model that can tell a paraphrase from a\n\t\t\t// coincidence. Everything starts `new` and is corrected in place.\n\t\t\tstatus: \"new\" as DirectiveStatus,\n\t\t\tpreviouslyDeclined: false,\n\t\t}))\n\t\t.sort(byEvidence);\n}\n\nexport function reduceFixes(sessions: MinedSession[], minRepeats: number): FixCandidate[] {\n\treturn groupByLabel(sessions, \"fix\")\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `fix:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\tcommand: entry.samples.find((s) => s.command)?.command ?? entry.text,\n\t\t\terrorExcerpt: entry.samples.find((s) => s.errorExcerpt)?.errorExcerpt ?? \"\",\n\t\t\tinterveningCommands: mergeStrings(entry.samples, (s) => s.interveningCommands),\n\t\t\teditedFiles: mergeStrings(entry.samples, (s) => s.editedFiles),\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.sort(byEvidence);\n}\n\nexport function reduceRequests(sessions: MinedSession[], minRepeats: number): RequestCandidate[] {\n\treturn mergeIdenticalText(groupByLabel(sessions, \"request\"))\n\t\t.filter((entry) => repeatedEnough(entry, minRepeats))\n\t\t.map((entry) => ({\n\t\t\tkey: `request:${entry.label}`,\n\t\t\tlabel: entry.label,\n\t\t\ttext: entry.text,\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.sort(byEvidence);\n}\n"]}
@@ -31,6 +31,13 @@ export interface SurfacedItem {
31
31
  * told from a declined one, without asking.
32
32
  */
33
33
  coveredWhenSurfaced: boolean;
34
+ /**
35
+ * Representative wording, kept so `/learn stats` can ask about coverage using
36
+ * what was actually said. The key alone is a slug, and judging "is this
37
+ * written down?" from a slug is a much weaker question than judging it from
38
+ * the sentence the slug stands for.
39
+ */
40
+ text?: string;
34
41
  }
35
42
  export interface LearnState {
36
43
  version: number;
@@ -84,35 +91,29 @@ export interface LearnStats {
84
91
  total: number;
85
92
  directives: number;
86
93
  fixes: number;
87
- workflows: number;
88
- /**
89
- * Directive proposals that were genuinely open questions — not already
90
- * written down when they were shown. Only these can be adopted or passed
91
- * over, so they are the denominator of the rate.
92
- */
93
- open: number;
94
- adopted: number;
95
- declined: number;
94
+ requests: number;
96
95
  earliest?: string;
97
96
  latest?: string;
98
97
  lastRun?: string;
99
98
  }
100
99
  /**
101
- * Reconstruct what happened to past proposals.
102
- *
103
- * Nothing extra is recorded to make this work: each entry already stores whether
104
- * it was covered when it was shown, and coverage now is recomputable, so the
105
- * pair is enough to tell an adopted proposal from one that was passed over.
100
+ * Count what has been proposed, and when.
106
101
  *
107
- * Directives only. Fixes and workflows have no coverage signal a fix may have
108
- * become a rule, a skill, or a habit, and which one is not recoverable — so
109
- * counting them here would mean inventing an outcome.
102
+ * Deliberately no adoption rate. There used to be one, derived by re-judging
103
+ * coverage and calling the delta "adopted", and it was wrong in both
104
+ * directions: a failed judge at either end moved the number, and a proposal
105
+ * correctly rejected as junk was indistinguishable from one ignored. It shipped
106
+ * with two disclaimers explaining how not to misread it, which is the clearest
107
+ * possible sign that it should not have shipped. What the reader actually wants
108
+ * — is the always-loaded surface growing or shrinking — is measurable exactly,
109
+ * from the context files themselves, and is reported instead.
110
110
  */
111
- export declare function summarizeLearnState(state: LearnState, isCoveredNow: (normalized: string) => boolean): LearnStats;
111
+ export declare function summarizeLearnState(state: LearnState): LearnStats;
112
112
  /** Record everything this run put on screen, so the next run can suppress it. */
113
113
  export declare function recordSurfaced(state: LearnState, items: Array<{
114
114
  key: string;
115
115
  lastSeen: string;
116
116
  covered: boolean;
117
+ text?: string;
117
118
  }>, now?: Date): LearnState;
118
119
  //# sourceMappingURL=state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,MAAM,WAAW,YAAY;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,mBAAmB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACvC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAavD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,GAAE,IAAiB,GAAG,IAAI,CAe7F;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,GAAG,kBAAkB,CAcpF;AAED,MAAM,WAAW,UAAU;IAC1B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GAAG,UAAU,CA6BhH;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,EACjE,GAAG,GAAE,IAAiB,GACpB,UAAU,CAOZ","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/** Bump when the on-disk shape changes; an unknown version is discarded, not migrated. */\nconst STATE_VERSION = 1;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\tworkflows: number;\n\t/**\n\t * Directive proposals that were genuinely open questions — not already\n\t * written down when they were shown. Only these can be adopted or passed\n\t * over, so they are the denominator of the rate.\n\t */\n\topen: number;\n\tadopted: number;\n\tdeclined: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Reconstruct what happened to past proposals.\n *\n * Nothing extra is recorded to make this work: each entry already stores whether\n * it was covered when it was shown, and coverage now is recomputable, so the\n * pair is enough to tell an adopted proposal from one that was passed over.\n *\n * Directives only. Fixes and workflows have no coverage signal — a fix may have\n * become a rule, a skill, or a habit, and which one is not recoverable — so\n * counting them here would mean inventing an outcome.\n */\nexport function summarizeLearnState(state: LearnState, isCoveredNow: (normalized: string) => boolean): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, workflows: 0, open: 0, adopted: 0, declined: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) {\n\t\t\tstats.fixes++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (key.startsWith(\"workflow:\")) {\n\t\t\tstats.workflows++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!key.startsWith(\"directive:\")) continue;\n\n\t\tstats.directives++;\n\t\t// Already written down when shown, so there was no decision to make.\n\t\tif (item.coveredWhenSurfaced) continue;\n\n\t\tstats.open++;\n\t\tif (isCoveredNow(key.slice(\"directive:\".length))) stats.adopted++;\n\t\telse stats.declined++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = { surfacedAt: at, lastOccurrence: item.lastSeen, coveredWhenSurfaced: item.covered };\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuBH,MAAM,WAAW,YAAY;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,mBAAmB,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACvC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAavD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,GAAE,IAAiB,GAAG,IAAI,CAe7F;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IAClC,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,GAAG,kBAAkB,CAcpF;AAED,MAAM,WAAW,UAAU;IAC1B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAejE;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAChF,GAAG,GAAE,IAAiB,GACpB,UAAU,CAYZ","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/**\n * Bump when the on-disk shape changes; an unknown version is discarded, not\n * migrated.\n *\n * v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where\n * v1 held normalized directive text (`directive:always use bun not npm`).\n *\n * v3: `workflow:` keys are gone — the category was removed — and labels now come\n * from a global naming pass rather than per-session guesses, so the keys a v2\n * file holds were drawn from a vocabulary that no longer exists. Keeping them\n * would suppress nothing and count as proposals that were never revisited.\n */\nconst STATE_VERSION = 3;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n\t/**\n\t * Representative wording, kept so `/learn stats` can ask about coverage using\n\t * what was actually said. The key alone is a slug, and judging \"is this\n\t * written down?\" from a slug is a much weaker question than judging it from\n\t * the sentence the slug stands for.\n\t */\n\ttext?: string;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\trequests: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Count what has been proposed, and when.\n *\n * Deliberately no adoption rate. There used to be one, derived by re-judging\n * coverage and calling the delta \"adopted\", and it was wrong in both\n * directions: a failed judge at either end moved the number, and a proposal\n * correctly rejected as junk was indistinguishable from one ignored. It shipped\n * with two disclaimers explaining how not to misread it, which is the clearest\n * possible sign that it should not have shipped. What the reader actually wants\n * — is the always-loaded surface growing or shrinking — is measurable exactly,\n * from the context files themselves, and is reported instead.\n */\nexport function summarizeLearnState(state: LearnState): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, requests: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) stats.fixes++;\n\t\telse if (key.startsWith(\"request:\")) stats.requests++;\n\t\telse if (key.startsWith(\"directive:\")) stats.directives++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean; text?: string }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = {\n\t\t\tsurfacedAt: at,\n\t\t\tlastOccurrence: item.lastSeen,\n\t\t\tcoveredWhenSurfaced: item.covered,\n\t\t\ttext: item.text,\n\t\t};\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
@@ -23,8 +23,19 @@
23
23
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
24
24
  import { basename, join } from "node:path";
25
25
  import { writeFileAtomicSync } from "../../utils/atomic-file.js";
26
- /** Bump when the on-disk shape changes; an unknown version is discarded, not migrated. */
27
- const STATE_VERSION = 1;
26
+ /**
27
+ * Bump when the on-disk shape changes; an unknown version is discarded, not
28
+ * migrated.
29
+ *
30
+ * v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where
31
+ * v1 held normalized directive text (`directive:always use bun not npm`).
32
+ *
33
+ * v3: `workflow:` keys are gone — the category was removed — and labels now come
34
+ * from a global naming pass rather than per-session guesses, so the keys a v2
35
+ * file holds were drawn from a vocabulary that no longer exists. Keeping them
36
+ * would suppress nothing and count as proposals that were never revisited.
37
+ */
38
+ const STATE_VERSION = 3;
28
39
  /** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */
29
40
  const STATE_RETENTION_DAYS = 180;
30
41
  function emptyState() {
@@ -101,43 +112,31 @@ export function judge(state, input) {
101
112
  };
102
113
  }
103
114
  /**
104
- * Reconstruct what happened to past proposals.
115
+ * Count what has been proposed, and when.
105
116
  *
106
- * Nothing extra is recorded to make this work: each entry already stores whether
107
- * it was covered when it was shown, and coverage now is recomputable, so the
108
- * pair is enough to tell an adopted proposal from one that was passed over.
109
- *
110
- * Directives only. Fixes and workflows have no coverage signal a fix may have
111
- * become a rule, a skill, or a habit, and which one is not recoverable — so
112
- * counting them here would mean inventing an outcome.
117
+ * Deliberately no adoption rate. There used to be one, derived by re-judging
118
+ * coverage and calling the delta "adopted", and it was wrong in both
119
+ * directions: a failed judge at either end moved the number, and a proposal
120
+ * correctly rejected as junk was indistinguishable from one ignored. It shipped
121
+ * with two disclaimers explaining how not to misread it, which is the clearest
122
+ * possible sign that it should not have shipped. What the reader actually wants
123
+ * is the always-loaded surface growing or shrinking — is measurable exactly,
124
+ * from the context files themselves, and is reported instead.
113
125
  */
114
- export function summarizeLearnState(state, isCoveredNow) {
115
- const stats = { total: 0, directives: 0, fixes: 0, workflows: 0, open: 0, adopted: 0, declined: 0 };
126
+ export function summarizeLearnState(state) {
127
+ const stats = { total: 0, directives: 0, fixes: 0, requests: 0 };
116
128
  for (const [key, item] of Object.entries(state.surfaced)) {
117
129
  stats.total++;
118
130
  if (!stats.earliest || item.surfacedAt < stats.earliest)
119
131
  stats.earliest = item.surfacedAt;
120
132
  if (!stats.latest || item.surfacedAt > stats.latest)
121
133
  stats.latest = item.surfacedAt;
122
- if (key.startsWith("fix:")) {
134
+ if (key.startsWith("fix:"))
123
135
  stats.fixes++;
124
- continue;
125
- }
126
- if (key.startsWith("workflow:")) {
127
- stats.workflows++;
128
- continue;
129
- }
130
- if (!key.startsWith("directive:"))
131
- continue;
132
- stats.directives++;
133
- // Already written down when shown, so there was no decision to make.
134
- if (item.coveredWhenSurfaced)
135
- continue;
136
- stats.open++;
137
- if (isCoveredNow(key.slice("directive:".length)))
138
- stats.adopted++;
139
- else
140
- stats.declined++;
136
+ else if (key.startsWith("request:"))
137
+ stats.requests++;
138
+ else if (key.startsWith("directive:"))
139
+ stats.directives++;
141
140
  }
142
141
  stats.lastRun = state.lastRun;
143
142
  return stats;
@@ -147,7 +146,12 @@ export function recordSurfaced(state, items, now = new Date()) {
147
146
  const surfaced = { ...state.surfaced };
148
147
  const at = now.toISOString();
149
148
  for (const item of items) {
150
- surfaced[item.key] = { surfacedAt: at, lastOccurrence: item.lastSeen, coveredWhenSurfaced: item.covered };
149
+ surfaced[item.key] = {
150
+ surfacedAt: at,
151
+ lastOccurrence: item.lastSeen,
152
+ coveredWhenSurfaced: item.covered,
153
+ text: item.text,
154
+ };
151
155
  }
152
156
  return { version: STATE_VERSION, lastRun: at, surfaced };
153
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/** Bump when the on-disk shape changes; an unknown version is discarded, not migrated. */\nconst STATE_VERSION = 1;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\tworkflows: number;\n\t/**\n\t * Directive proposals that were genuinely open questions — not already\n\t * written down when they were shown. Only these can be adopted or passed\n\t * over, so they are the denominator of the rate.\n\t */\n\topen: number;\n\tadopted: number;\n\tdeclined: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Reconstruct what happened to past proposals.\n *\n * Nothing extra is recorded to make this work: each entry already stores whether\n * it was covered when it was shown, and coverage now is recomputable, so the\n * pair is enough to tell an adopted proposal from one that was passed over.\n *\n * Directives only. Fixes and workflows have no coverage signal — a fix may have\n * become a rule, a skill, or a habit, and which one is not recoverable — so\n * counting them here would mean inventing an outcome.\n */\nexport function summarizeLearnState(state: LearnState, isCoveredNow: (normalized: string) => boolean): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, workflows: 0, open: 0, adopted: 0, declined: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) {\n\t\t\tstats.fixes++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (key.startsWith(\"workflow:\")) {\n\t\t\tstats.workflows++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!key.startsWith(\"directive:\")) continue;\n\n\t\tstats.directives++;\n\t\t// Already written down when shown, so there was no decision to make.\n\t\tif (item.coveredWhenSurfaced) continue;\n\n\t\tstats.open++;\n\t\tif (isCoveredNow(key.slice(\"directive:\".length))) stats.adopted++;\n\t\telse stats.declined++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = { surfacedAt: at, lastOccurrence: item.lastSeen, coveredWhenSurfaced: item.covered };\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/learn/state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-directory memory of what `/learn` has already shown you.\n *\n * The extractor computes a sliding-window snapshot — \"in your last N sessions\n * you said this M times\" — which is stateless by design and correct to\n * recompute from scratch every run. What it cannot know on its own is whether\n * you have already *seen* a given item and made a call on it. Without that, a\n * rule you accepted last week comes back forever (its occurrences are still\n * inside the window), and worse, it comes back flagged `restated` — accusing a\n * rule that is working of not working. An item you declined comes back too,\n * unchanged, every single run.\n *\n * So one bookmark per directory: what was surfaced, and when. An item is shown\n * again only once something new has happened — you said it again *after* it was\n * last put in front of you. That single rule fixes both cases, and it keeps the\n * `restated` label honest, because the only way to earn it is to repeat yourself\n * after the rule already existed.\n *\n * This is a bookmark, not an index. No cards, no retrieval, no embeddings — the\n * transcripts remain the source of truth and this file can be deleted at any\n * time with no loss beyond re-proposing things once.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { writeFileAtomicSync } from \"../../utils/atomic-file.js\";\n\n/**\n * Bump when the on-disk shape changes; an unknown version is discarded, not\n * migrated.\n *\n * v2: keys hold the miner's semantic label (`directive:use-bun-not-npm`) where\n * v1 held normalized directive text (`directive:always use bun not npm`).\n *\n * v3: `workflow:` keys are gone — the category was removed — and labels now come\n * from a global naming pass rather than per-session guesses, so the keys a v2\n * file holds were drawn from a vocabulary that no longer exists. Keeping them\n * would suppress nothing and count as proposals that were never revisited.\n */\nconst STATE_VERSION = 3;\n\n/** Surfaced keys older than this are forgotten, so the file cannot grow without bound. */\nconst STATE_RETENTION_DAYS = 180;\n\nexport interface SurfacedItem {\n\t/** When this item was last put in front of the user, ISO. */\n\tsurfacedAt: string;\n\t/** Newest occurrence the run knew about at that point, ISO. */\n\tlastOccurrence: string;\n\t/**\n\t * Whether the item was covered by a context file the last time it was\n\t * surfaced. Comparing that against coverage now is how an adopted proposal is\n\t * told from a declined one, without asking.\n\t */\n\tcoveredWhenSurfaced: boolean;\n\t/**\n\t * Representative wording, kept so `/learn stats` can ask about coverage using\n\t * what was actually said. The key alone is a slug, and judging \"is this\n\t * written down?\" from a slug is a much weaker question than judging it from\n\t * the sentence the slug stands for.\n\t */\n\ttext?: string;\n}\n\nexport interface LearnState {\n\tversion: number;\n\tlastRun?: string;\n\tsurfaced: Record<string, SurfacedItem>;\n}\n\nfunction emptyState(): LearnState {\n\treturn { version: STATE_VERSION, surfaced: {} };\n}\n\n/**\n * State file for a directory.\n *\n * Keyed off the *name* of the cwd's session directory, which encodes the cwd —\n * so the bookmark is per working directory, matching what the digest describes.\n * Callers pass the cwd-derived path rather than whatever directory the live\n * session happens to be reading: a session with no directory of its own would\n * otherwise land every project on one nameless state file, and projects sharing\n * a custom `sessionDir` would share one bookmark between them.\n */\nexport function getLearnStatePath(agentDir: string, sessionDir: string): string {\n\treturn join(agentDir, \"learn\", `${basename(sessionDir)}.json`);\n}\n\n/** Read state, treating any unreadable or unknown-version file as empty. */\nexport function readLearnState(path: string): LearnState {\n\ttry {\n\t\tif (!existsSync(path)) return emptyState();\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as Partial<LearnState>;\n\t\tif (parsed.version !== STATE_VERSION || typeof parsed.surfaced !== \"object\" || parsed.surfaced === null) {\n\t\t\treturn emptyState();\n\t\t}\n\t\treturn { version: STATE_VERSION, lastRun: parsed.lastRun, surfaced: parsed.surfaced };\n\t} catch {\n\t\t// A corrupt bookmark is not worth failing a command over; the cost of\n\t\t// starting fresh is proposing a few things a second time.\n\t\treturn emptyState();\n\t}\n}\n\n/** Persist state, pruning entries nothing has referenced in a long time. */\nexport function writeLearnState(path: string, state: LearnState, now: Date = new Date()): void {\n\tconst cutoff = now.getTime() - STATE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n\tconst surfaced: Record<string, SurfacedItem> = {};\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tconst at = Date.parse(item.surfacedAt);\n\t\tif (Number.isNaN(at) || at >= cutoff) surfaced[key] = item;\n\t}\n\n\ttry {\n\t\tmkdirSync(join(path, \"..\"), { recursive: true });\n\t\twriteFileAtomicSync(path, `${JSON.stringify({ ...state, surfaced }, null, 2)}\\n`);\n\t} catch {\n\t\t// Losing the bookmark degrades /learn to its previous behaviour (proposing\n\t\t// already-decided items again). Never worth failing the command.\n\t}\n}\n\n/** What the caller needs to decide whether an item is worth showing again. */\nexport interface SuppressionInput {\n\tkey: string;\n\t/** Newest occurrence of this item in the current window, ISO. */\n\tlastSeen: string;\n\t/** Whether a context file covers it right now. */\n\tcovered: boolean;\n}\n\nexport interface SuppressionVerdict {\n\t/** Drop it: already surfaced, and nothing new has happened since. */\n\tsuppressed: boolean;\n\t/**\n\t * Surfaced before, still not covered by any context file — you saw it and\n\t * chose not to write it down. Worth telling the model so it proposes again\n\t * tentatively instead of pressing the same case twice.\n\t */\n\tpreviouslyDeclined: boolean;\n}\n\n/**\n * Decide whether an item should be shown again.\n *\n * An item is suppressed when it was surfaced before and has not recurred since:\n * `lastSeen <= surfacedAt` means every occurrence backing it was already on\n * screen when you made your call. Say it again and `lastSeen` moves past\n * `surfacedAt`, and it returns — which is exactly when it is worth returning.\n */\nexport function judge(state: LearnState, input: SuppressionInput): SuppressionVerdict {\n\tconst previous = state.surfaced[input.key];\n\tif (!previous) return { suppressed: false, previouslyDeclined: false };\n\n\tconst surfacedAt = Date.parse(previous.surfacedAt);\n\tconst lastSeen = Date.parse(input.lastSeen);\n\tconst recurredSince = !Number.isNaN(surfacedAt) && !Number.isNaN(lastSeen) && lastSeen > surfacedAt;\n\n\treturn {\n\t\tsuppressed: !recurredSince,\n\t\t// Covered now but not when it was surfaced means the proposal was taken up.\n\t\t// Still uncovered means it was passed over.\n\t\tpreviouslyDeclined: !input.covered && !previous.coveredWhenSurfaced,\n\t};\n}\n\nexport interface LearnStats {\n\t/** Proposals on record, across all categories. */\n\ttotal: number;\n\tdirectives: number;\n\tfixes: number;\n\trequests: number;\n\tearliest?: string;\n\tlatest?: string;\n\tlastRun?: string;\n}\n\n/**\n * Count what has been proposed, and when.\n *\n * Deliberately no adoption rate. There used to be one, derived by re-judging\n * coverage and calling the delta \"adopted\", and it was wrong in both\n * directions: a failed judge at either end moved the number, and a proposal\n * correctly rejected as junk was indistinguishable from one ignored. It shipped\n * with two disclaimers explaining how not to misread it, which is the clearest\n * possible sign that it should not have shipped. What the reader actually wants\n * — is the always-loaded surface growing or shrinking — is measurable exactly,\n * from the context files themselves, and is reported instead.\n */\nexport function summarizeLearnState(state: LearnState): LearnStats {\n\tconst stats: LearnStats = { total: 0, directives: 0, fixes: 0, requests: 0 };\n\n\tfor (const [key, item] of Object.entries(state.surfaced)) {\n\t\tstats.total++;\n\t\tif (!stats.earliest || item.surfacedAt < stats.earliest) stats.earliest = item.surfacedAt;\n\t\tif (!stats.latest || item.surfacedAt > stats.latest) stats.latest = item.surfacedAt;\n\n\t\tif (key.startsWith(\"fix:\")) stats.fixes++;\n\t\telse if (key.startsWith(\"request:\")) stats.requests++;\n\t\telse if (key.startsWith(\"directive:\")) stats.directives++;\n\t}\n\n\tstats.lastRun = state.lastRun;\n\treturn stats;\n}\n\n/** Record everything this run put on screen, so the next run can suppress it. */\nexport function recordSurfaced(\n\tstate: LearnState,\n\titems: Array<{ key: string; lastSeen: string; covered: boolean; text?: string }>,\n\tnow: Date = new Date(),\n): LearnState {\n\tconst surfaced = { ...state.surfaced };\n\tconst at = now.toISOString();\n\tfor (const item of items) {\n\t\tsurfaced[item.key] = {\n\t\t\tsurfacedAt: at,\n\t\t\tlastOccurrence: item.lastSeen,\n\t\t\tcoveredWhenSurfaced: item.covered,\n\t\t\ttext: item.text,\n\t\t};\n\t}\n\treturn { version: STATE_VERSION, lastRun: at, surfaced };\n}\n"]}
@@ -54,7 +54,7 @@ export declare const DEFAULT_SETTINGS: {
54
54
  learnMaxSessions: number;
55
55
  learnMaxAgeDays: number;
56
56
  learnMinRepeats: number;
57
- learnMinWorkflowRepeats: number;
57
+ learnMinRequestRepeats: number;
58
58
  learnMaxProposals: number;
59
59
  light: false;
60
60
  terminal: {
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinWorkflowRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinRequestRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -54,7 +54,7 @@ export const DEFAULT_SETTINGS = {
54
54
  learnMaxSessions: 20,
55
55
  learnMaxAgeDays: 30,
56
56
  learnMinRepeats: 2,
57
- learnMinWorkflowRepeats: 3,
57
+ learnMinRequestRepeats: 3,
58
58
  learnMaxProposals: 8,
59
59
  light: false,
60
60
  terminal: {
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,kBAAkB,EAAE,MAAM;IAC1B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,gBAAgB,EAAE,EAAE;IACpB,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,CAAC;IAClB,uBAAuB,EAAE,CAAC;IAC1B,iBAAiB,EAAE,CAAC;IACpB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinWorkflowRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,kBAAkB,EAAE,MAAM;IAC1B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,gBAAgB,EAAE,EAAE;IACpB,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,CAAC;IAClB,sBAAsB,EAAE,CAAC;IACzB,iBAAiB,EAAE,CAAC;IACpB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tpluginInstallScope: \"user\",\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlearnMaxSessions: 20,\n\tlearnMaxAgeDays: 30,\n\tlearnMinRepeats: 2,\n\tlearnMinRequestRepeats: 3,\n\tlearnMaxProposals: 8,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -1,7 +1,7 @@
1
1
  import { type SettingsError, type SettingsScope, type SettingsStorage } from "./settings-storage.js";
2
2
  import type { BranchSummarySettings, CompactionSettings, PackageSource, Settings, ThinkingBudgetsSettings, TransportSetting, WarningSettings } from "./settings-types.js";
3
3
  /** The `/learn` thresholds, which are flat top-level numeric settings. */
4
- export type LearnSettingKey = "learnMaxSessions" | "learnMaxAgeDays" | "learnMinRepeats" | "learnMinWorkflowRepeats" | "learnMaxProposals";
4
+ export type LearnSettingKey = "learnMaxSessions" | "learnMaxAgeDays" | "learnMinRepeats" | "learnMinRequestRepeats" | "learnMaxProposals";
5
5
  export type { SettingsError, SettingsScope, SettingsStorage };
6
6
  export type { BranchSummarySettings, CompactionSettings, ImageSettings, PackageSource, RetrySettings, Settings, ThinkingBudgetsSettings, TransportSetting, WarningSettings, } from "./settings-types.js";
7
7
  export declare class SettingsManager {
@@ -81,9 +81,11 @@ export declare class SettingsManager {
81
81
  maxSessions: number;
82
82
  maxAgeDays: number;
83
83
  minRepeats: number;
84
- minWorkflowRepeats: number;
84
+ minRequestRepeats: number;
85
85
  maxProposals: number;
86
86
  };
87
+ /** Configured `fast`/`standard`/`capable` tiers, for callers resolving a category. */
88
+ getModelCategories(): Settings["modelCategories"];
87
89
  /**
88
90
  * Persist one `/learn` threshold to the user scope.
89
91
  *