eklavya 1.91.1 → 1.92.0

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 (113) hide show
  1. package/README.md +6 -4
  2. package/dist/assets/dashboard.html +151 -3
  3. package/dist/cli-economy.js +434 -0
  4. package/dist/cli-economy.js.map +1 -0
  5. package/dist/cli-memory.js +4 -1
  6. package/dist/cli-memory.js.map +1 -1
  7. package/dist/cli.js +46 -5
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config-path.js +23 -1
  10. package/dist/config-path.js.map +1 -1
  11. package/dist/config.js +63 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/cost-report.js +67 -0
  14. package/dist/cost-report.js.map +1 -0
  15. package/dist/dashboard.js +79 -1
  16. package/dist/dashboard.js.map +1 -1
  17. package/dist/economy/LICENSE +202 -0
  18. package/dist/economy/envfacts.js +229 -0
  19. package/dist/economy/envfacts.js.map +1 -0
  20. package/dist/economy/loopguard.js +169 -0
  21. package/dist/economy/loopguard.js.map +1 -0
  22. package/dist/economy/pricing.js +54 -0
  23. package/dist/economy/pricing.js.map +1 -0
  24. package/dist/economy/shell-edits.js +238 -0
  25. package/dist/economy/shell-edits.js.map +1 -0
  26. package/dist/economy/signals.js +164 -0
  27. package/dist/economy/signals.js.map +1 -0
  28. package/dist/economy/toolprune.js +209 -0
  29. package/dist/economy/toolprune.js.map +1 -0
  30. package/dist/economy/transcript.js +290 -0
  31. package/dist/economy/transcript.js.map +1 -0
  32. package/dist/feedback-review.js +1 -1
  33. package/dist/feedback-review.js.map +1 -1
  34. package/dist/feedback.js +3 -0
  35. package/dist/feedback.js.map +1 -1
  36. package/dist/guard-note.js +21 -0
  37. package/dist/guard-note.js.map +1 -0
  38. package/dist/hooks/capture-tool.js +61 -11
  39. package/dist/hooks/capture-tool.js.map +1 -1
  40. package/dist/hooks/economy-lib.js +20 -0
  41. package/dist/hooks/economy-lib.js.map +1 -0
  42. package/dist/hooks/file-context.js +1 -1
  43. package/dist/hooks/file-context.js.map +1 -1
  44. package/dist/hooks/lib.js.map +1 -1
  45. package/dist/hooks/memory-lib.js +17 -1
  46. package/dist/hooks/memory-lib.js.map +1 -1
  47. package/dist/hooks/prompt-submit-nudge.js +15 -1
  48. package/dist/hooks/prompt-submit-nudge.js.map +1 -1
  49. package/dist/hooks/session-end.js +32 -0
  50. package/dist/hooks/session-end.js.map +1 -0
  51. package/dist/hooks/session-start.js +34 -0
  52. package/dist/hooks/session-start.js.map +1 -1
  53. package/dist/hooks/stop-quiz-check.js +20 -0
  54. package/dist/hooks/stop-quiz-check.js.map +1 -1
  55. package/dist/hooks/subagent-start.js +17 -2
  56. package/dist/hooks/subagent-start.js.map +1 -1
  57. package/dist/install.js +5 -1
  58. package/dist/install.js.map +1 -1
  59. package/dist/ledger-pending.js +85 -0
  60. package/dist/ledger-pending.js.map +1 -0
  61. package/dist/ledger.js +144 -0
  62. package/dist/ledger.js.map +1 -0
  63. package/dist/loop-guard.js +190 -0
  64. package/dist/loop-guard.js.map +1 -0
  65. package/dist/memory/capture.js +2 -2
  66. package/dist/memory/capture.js.map +1 -1
  67. package/dist/memory/embed.js +37 -0
  68. package/dist/memory/embed.js.map +1 -1
  69. package/dist/memory/pinned.js +175 -0
  70. package/dist/memory/pinned.js.map +1 -0
  71. package/dist/memory/provider.js +66 -0
  72. package/dist/memory/provider.js.map +1 -1
  73. package/dist/memory/recall.js +111 -33
  74. package/dist/memory/recall.js.map +1 -1
  75. package/dist/memory/seam.js +88 -0
  76. package/dist/memory/seam.js.map +1 -0
  77. package/dist/memory/search.js +65 -11
  78. package/dist/memory/search.js.map +1 -1
  79. package/dist/memory/store.js +74 -15
  80. package/dist/memory/store.js.map +1 -1
  81. package/dist/memory/summarize.js.map +1 -1
  82. package/dist/memory/terms.js +100 -0
  83. package/dist/memory/terms.js.map +1 -0
  84. package/dist/memory/tokens.js +11 -7
  85. package/dist/memory/tokens.js.map +1 -1
  86. package/dist/memory/use.js +123 -0
  87. package/dist/memory/use.js.map +1 -0
  88. package/dist/memory/worker.js +16 -1
  89. package/dist/memory/worker.js.map +1 -1
  90. package/dist/migrations/034_memory_keywords.sql +59 -0
  91. package/dist/migrations/035_econ_ledger.sql +69 -0
  92. package/dist/migrations/036_econ_turns.sql +52 -0
  93. package/dist/migrations/037_econ_toolprune.sql +38 -0
  94. package/dist/migrations/038_pinned_facts.sql +48 -0
  95. package/dist/migrations/039_memory_use.sql +34 -0
  96. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  97. package/dist/plugin/hooks/hooks.json +30 -0
  98. package/dist/plugin/skills/CLAUDE.md +1 -1
  99. package/dist/plugin/skills/memory/SKILL.md +2 -1
  100. package/dist/plugin/skills/toolprune/SKILL.md +31 -0
  101. package/dist/statusline.js +2 -2
  102. package/dist/statusline.js.map +1 -1
  103. package/dist/toolprune-store.js +136 -0
  104. package/dist/toolprune-store.js.map +1 -0
  105. package/dist/tools/config_tools.js +30 -0
  106. package/dist/tools/config_tools.js.map +1 -1
  107. package/dist/tools/index.js +2 -1
  108. package/dist/tools/index.js.map +1 -1
  109. package/dist/tools/memory_read_tools.js +8 -0
  110. package/dist/tools/memory_read_tools.js.map +1 -1
  111. package/dist/tools/memory_write_tools.js +21 -0
  112. package/dist/tools/memory_write_tools.js.map +1 -1
  113. package/package.json +1 -1
@@ -0,0 +1,238 @@
1
+ // Translated and adapted from microsoft/TokenEconomizer,
2
+ // tokeneconomizer/loopguard/shell_edits.py at d3e193b8734ec76230da1e08d7929ba9d14640d8.
3
+ // Licensed under the Apache License, Version 2.0 (see ./LICENSE in this folder).
4
+ // Changes: ported from Python to TypeScript; the Codex `apply_patch` form is kept,
5
+ // `shlex` is replaced by a small tokenizer with the same punctuation rules.
6
+ /**
7
+ * Edits made through the shell, which an edit-tool counter does not see.
8
+ *
9
+ * An agent that edits with `sed -i`, `perl -pi`, a redirect or `tee` into a
10
+ * source file, an inline Python script that writes the file back, `patch` or
11
+ * `git apply` looks like one that edited nothing. `shellEditTargets` reads those
12
+ * writes off the command so the loop guard's turn state counts them.
13
+ */
14
+ const HEREDOC_START = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1/;
15
+ const OPS = new Set(['&&', '||', ';', ';;', '|', '|&', '&', '\n', '(', ')']);
16
+ const OUT = new Set(['>', '>>', '>|', '&>', '&>>']);
17
+ const SKIP_OPERAND = new Set(['<', '<<', '<<<', '<&', '>&', '<>']);
18
+ const PUNCT = '();<>|&\n';
19
+ /** Redirect and `tee` targets count only when they look like source, so `> out.log` is not an edit. */
20
+ export const SOURCE_FILE = /\.(?:py|pyi|pyx|pxd|js|jsx|mjs|cjs|ts|tsx|rs|go|java|kt|kts|scala|c|cc|cpp|cxx|h|hh|hpp|rb|php|cs|swift|m|mm|sh|bash|zsh|lua|jl|ex|exs|erl|hs|ml|clj|sql|toml|ya?ml|json|cfg|ini|conf|md|rst|html?|css|scss|vue|svelte|xml|gradle|properties|in)$/i;
21
+ const PYTHON = /(?:^|[\s/;&|(])python\d?(?:\.\d+)?\b/;
22
+ const PY_OPEN_LIT = /open\(\s*(['"])([^'"\n]+)\1\s*,\s*(['"])([^'"]*)\3/g;
23
+ const PY_OPEN_VAR = /open\(\s*([A-Za-z_]\w*)\s*,\s*(['"])([^'"]*)\2/g;
24
+ const PY_WRITE_PATH_LIT = /(?:pathlib\.)?Path\(\s*(['"])([^'"\n]+)\1\s*\)\.write_(?:text|bytes)\(/g;
25
+ const PY_WRITE_PATH_VAR = /\b([A-Za-z_]\w*)\.write_(?:text|bytes)\(/g;
26
+ const PATCH_FILE = /^\*\*\* (?:Update|Add|Delete) File:\s*(.+?)\s*$/gm;
27
+ export const basename = (p) => p.replace(/\/+$/, '').split('/').pop();
28
+ const notScratch = (p) => Boolean(p) && !/^\/(dev|proc|tmp)\//.test(p);
29
+ const unquote = (t) => (t.length >= 2 && t[0] === t[t.length - 1] && (t[0] === "'" || t[0] === '"') ? t.slice(1, -1) : t);
30
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ /** The command without heredoc bodies, and the bodies. */
32
+ function splitHeredocs(cmd) {
33
+ const shell = [];
34
+ const bodies = [];
35
+ let tag = null;
36
+ let body = [];
37
+ for (const line of cmd.split('\n')) {
38
+ if (tag !== null) {
39
+ if (line.trim() === tag) {
40
+ bodies.push(body.join('\n'));
41
+ tag = null;
42
+ body = [];
43
+ }
44
+ else
45
+ body.push(line);
46
+ continue;
47
+ }
48
+ shell.push(line);
49
+ const m = HEREDOC_START.exec(line);
50
+ if (m)
51
+ tag = m[2];
52
+ }
53
+ if (tag !== null)
54
+ bodies.push(body.join('\n'));
55
+ return [shell.join('\n'), bodies];
56
+ }
57
+ /**
58
+ * Shell words and operators. Quotes stay on quoted words, so a quoted `'>'` is
59
+ * never an operator; a run of punctuation is one token. Throws on an
60
+ * unbalanced quote, as `shlex` does.
61
+ */
62
+ export function shellTokens(text) {
63
+ const out = [];
64
+ let word = '';
65
+ let i = 0;
66
+ const flush = () => {
67
+ if (word)
68
+ out.push(word);
69
+ word = '';
70
+ };
71
+ while (i < text.length) {
72
+ const c = text[i];
73
+ if (c === "'" || c === '"') {
74
+ const end = text.indexOf(c, i + 1);
75
+ if (end === -1)
76
+ throw new Error('unbalanced quote');
77
+ word += text.slice(i, end + 1);
78
+ i = end + 1;
79
+ }
80
+ else if (c === ' ' || c === '\t' || c === '\r') {
81
+ flush();
82
+ i++;
83
+ }
84
+ else if (PUNCT.includes(c)) {
85
+ flush();
86
+ let j = i;
87
+ while (j < text.length && PUNCT.includes(text[j]))
88
+ j++;
89
+ out.push(text.slice(i, j));
90
+ i = j;
91
+ }
92
+ else {
93
+ word += c;
94
+ i++;
95
+ }
96
+ }
97
+ flush();
98
+ return out;
99
+ }
100
+ /** Simple commands as unquoted word lists, and the files their redirects write. */
101
+ function commands(shell) {
102
+ let streams;
103
+ try {
104
+ streams = [shellTokens(shell)];
105
+ }
106
+ catch {
107
+ // One bad line must not hide the others.
108
+ streams = [];
109
+ for (const line of shell.split('\n')) {
110
+ try {
111
+ streams.push([...shellTokens(line), '\n']);
112
+ }
113
+ catch {
114
+ /* skip the line */
115
+ }
116
+ }
117
+ }
118
+ const cmds = [];
119
+ const writes = [];
120
+ for (const toks of streams) {
121
+ let cur = [];
122
+ for (let i = 0; i < toks.length; i++) {
123
+ const t = toks[i];
124
+ if (OPS.has(t)) {
125
+ if (cur.length)
126
+ cmds.push(cur);
127
+ cur = [];
128
+ }
129
+ else if (OUT.has(t) || SKIP_OPERAND.has(t)) {
130
+ // `2>`: the fd number is not an operand.
131
+ if (cur.length && /^\d+$/.test(cur[cur.length - 1]))
132
+ cur.pop();
133
+ if (OUT.has(t) && i + 1 < toks.length && !OPS.has(toks[i + 1]))
134
+ writes.push(unquote(toks[i + 1]));
135
+ i++;
136
+ }
137
+ else
138
+ cur.push(unquote(t));
139
+ }
140
+ if (cur.length)
141
+ cmds.push(cur);
142
+ }
143
+ return [cmds, writes];
144
+ }
145
+ /** File operands of an in-place editor (`sed -i`, `perl -pi`), or [] if not in place. */
146
+ function inplaceFiles(tokens, inplace, scriptFlag) {
147
+ if (!tokens.slice(1).some(inplace))
148
+ return [];
149
+ const files = [];
150
+ let haveScript = false;
151
+ let skip = false;
152
+ for (const t of tokens.slice(1)) {
153
+ if (skip) {
154
+ skip = false;
155
+ haveScript = true;
156
+ continue;
157
+ }
158
+ if (!t || t.startsWith('-')) {
159
+ if (t && scriptFlag(t))
160
+ skip = true;
161
+ continue;
162
+ }
163
+ if (!haveScript) {
164
+ haveScript = true;
165
+ continue;
166
+ }
167
+ files.push(t);
168
+ }
169
+ return files;
170
+ }
171
+ /** Paths an inline Python script writes; "(script)" for a write whose path this cannot resolve. */
172
+ function pyWrites(text) {
173
+ const out = [];
174
+ for (const m of text.matchAll(PY_OPEN_LIT))
175
+ if (/[wax+]/.test(m[4]))
176
+ out.push(m[2]);
177
+ const variables = [];
178
+ for (const m of text.matchAll(PY_OPEN_VAR))
179
+ if (/[wax+]/.test(m[3]))
180
+ variables.push(m[1]);
181
+ for (const m of text.matchAll(PY_WRITE_PATH_LIT))
182
+ out.push(m[2]);
183
+ for (const m of text.matchAll(PY_WRITE_PATH_VAR))
184
+ variables.push(m[1]);
185
+ for (const v of new Set(variables)) {
186
+ const name = escapeRe(v);
187
+ const found = [...text.matchAll(new RegExp(`\\b${name}\\s*=\\s*(?:(?:pathlib\\.)?Path\\(\\s*)?(['"])([^'"\\n]+)\\1`, 'g'))].map((m) => m[2]);
188
+ const loop = new RegExp(`\\bfor\\s+${name}\\s+in\\s+[\\[(]([^\\])]*)[\\])]`).exec(text);
189
+ const paths = [...found, ...(loop ? [...loop[1].matchAll(/['"]([^'"\n]+)['"]/g)].map((m) => m[1]) : [])];
190
+ out.push(...(paths.length ? paths : ['(script)']));
191
+ }
192
+ return out;
193
+ }
194
+ /** Basenames of the files a shell command writes, in order, without repeats. */
195
+ export function shellEditTargets(cmd) {
196
+ if (typeof cmd !== 'string' || !cmd.trim())
197
+ return [];
198
+ const [shell, bodies] = splitHeredocs(cmd);
199
+ const [cmds, writes] = commands(shell);
200
+ const found = writes.filter((w) => SOURCE_FILE.test(w));
201
+ for (let tokens of cmds) {
202
+ while (tokens.length && (['sudo', 'command', 'env', 'time', 'nohup'].includes(tokens[0]) || /^\w+=/.test(tokens[0]))) {
203
+ tokens = tokens.slice(1);
204
+ }
205
+ if (!tokens.length)
206
+ continue;
207
+ const head = tokens[0].split('/').pop();
208
+ if (head === 'sed') {
209
+ found.push(...inplaceFiles(tokens, (t) => /^(?:-[a-zA-Z]*i|--in-place)/.test(t), (t) => ['-e', '-f', '--expression', '--file'].includes(t)));
210
+ }
211
+ else if (head === 'perl') {
212
+ found.push(...inplaceFiles(tokens, (t) => /^-[a-zA-Z]*i/.test(t), (t) => /^-[a-zA-Z]*e$/.test(t)));
213
+ }
214
+ else if (head === 'tee') {
215
+ found.push(...tokens.slice(1).filter((t) => !t.startsWith('-') && SOURCE_FILE.test(t)));
216
+ }
217
+ else if (head === 'apply_patch') {
218
+ const named = [...[...bodies, tokens.slice(1).join(' ')].join('\n').matchAll(PATCH_FILE)].map((m) => m[1]);
219
+ found.push(...(named.length ? named : ['(patch)']));
220
+ }
221
+ else if (head === 'patch' || (head === 'git' && tokens[1] === 'apply')) {
222
+ found.push('(patch)');
223
+ }
224
+ else if (head === 'git' && tokens.length > 2 && (tokens[1] === 'checkout' || tokens[1] === 'restore')) {
225
+ const dash = tokens.indexOf('--');
226
+ const rest = dash !== -1 ? tokens.slice(dash + 1) : tokens[1] === 'restore' ? tokens.slice(2) : [];
227
+ found.push(...rest.filter((t) => !t.startsWith('-')));
228
+ }
229
+ }
230
+ if (PYTHON.test(shell))
231
+ found.push(...pyWrites([shell, ...bodies].join('\n')));
232
+ const names = found
233
+ .filter((p) => p.startsWith('(') || notScratch(p))
234
+ .map((p) => (p.startsWith('(') ? p : basename(p)))
235
+ .filter(Boolean);
236
+ return [...new Set(names)];
237
+ }
238
+ //# sourceMappingURL=shell-edits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-edits.js","sourceRoot":"","sources":["../../src/economy/shell-edits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ // Translated and adapted from microsoft/TokenEconomizer,
2
+ // tokeneconomizer/loopguard/state.py at d3e193b8734ec76230da1e08d7929ba9d14640d8.
3
+ // Licensed under the Apache License, Version 2.0 (see ./LICENSE in this folder).
4
+ // Changes: ported from Python to TypeScript; Claude Code tool names only; the
5
+ // Go `FAIL` lines use a second, case-sensitive pattern (JavaScript has no
6
+ // inline flag groups); a host-reported failure counts as an error output even
7
+ // without an error marker; more test runners are recognised, and `python3 -m unittest` as well as
8
+ // `python -m unittest` (found by a live run of the pinned-facts gate); a verification verdict is kept so a turn that edited
9
+ // after its last check reads as unknown, not green; shell edits are folded in
10
+ // `update` rather than by the caller.
11
+ import crypto from 'node:crypto';
12
+ import { basename, shellEditTargets } from './shell-edits.js';
13
+ /** An output that ran code to verify behaviour: the test step of the edit→test loop. */
14
+ export const TEST_RUN_RE = /(runtests(\.py)?|\bpytest\b|py\.test|manage\.py\s+test|python\d?(?:\.\d+)?\s+-m\s+unittest|\btox\b|\bnox\b|\btrial\b|python\d?\s+-c\b|\bnpm (?:run )?test\b|\bnpx (?:vitest|jest|mocha)\b|\bvitest\b|\bjest\b|\bgo test\b|\bcargo test\b|\bmvn (?:-\S+ )*test\b|\bgradlew? test\b|\brspec\b|node --test\b)/i;
15
+ const ERR_RE = /(Traceback \(most recent call last\)|\b\w*Error\b|\bFAILED\b|\bassert\b|Exception|errors during collection|^# fail [1-9]|^not ok \d|\b[1-9]\d* (?:failing|failures?)\b|Failures: [1-9])/im;
16
+ /** Go's `--- FAIL: T` and `FAIL<tab>pkg`, case-sensitive so unittest's `FAIL: test_x` keeps its own line. */
17
+ const GO_FAIL_RE = /^(?:--- FAIL: |FAIL\t|FAIL$)/m;
18
+ export const isErrorText = (text) => ERR_RE.test(text) || GO_FAIL_RE.test(text);
19
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
20
+ /** Tools whose output is file content, not the outcome of running anything. */
21
+ const CONTENT_TOOLS = new Set(['Read', 'Grep', 'Glob', 'NotebookRead']);
22
+ /**
23
+ * A normalised signature of a failing output: numbers, addresses and paths
24
+ * dropped, so two runs that fail the same way hash the same. A hash, never
25
+ * the content.
26
+ */
27
+ export function failureSignature(text) {
28
+ if (!text)
29
+ return null;
30
+ const first = text.split('\n').find((ln) => isErrorText(ln));
31
+ if (first === undefined)
32
+ return null;
33
+ const norm = first.replace(/0x[0-9a-f]+|\d+|\/\S+/g, '').trim();
34
+ return crypto.createHash('md5').update(norm).digest('hex').slice(0, 10);
35
+ }
36
+ export const emptyTurn = () => ({
37
+ calls: 0,
38
+ editCounts: {},
39
+ errorOutputs: 0,
40
+ testRuns: 0,
41
+ failSigs: {},
42
+ repeatedFailSigs: 0,
43
+ lastCheck: null,
44
+ edited: false,
45
+ recent: [],
46
+ });
47
+ /** Numbers and quoted strings replaced, so a digest carries a command's shape, not its data. */
48
+ export const commandTemplate = (cmd) => cmd
49
+ .replace(/\s+/g, ' ')
50
+ .trim()
51
+ .replace(/(['"])(?:(?!\1).)*\1/g, '$1…$1')
52
+ .replace(/\b\d+\b/g, 'N')
53
+ .slice(0, 90);
54
+ /**
55
+ * One turn's failure-loop signals, folded one completed tool call at a time:
56
+ * edits per file, files edited, error outputs, verification runs and how often
57
+ * a failure repeats one seen before (`stagnation`).
58
+ */
59
+ export class TurnState {
60
+ d;
61
+ constructor(d = emptyTurn()) {
62
+ this.d = d;
63
+ }
64
+ get editChurn() {
65
+ const v = Object.values(this.d.editCounts);
66
+ return v.length ? Math.max(...v) : 0;
67
+ }
68
+ get distinctEditFiles() {
69
+ return Object.keys(this.d.editCounts).length;
70
+ }
71
+ /** Share of failing outputs that repeat an earlier identical failure. */
72
+ get stagnation() {
73
+ const total = Object.values(this.d.failSigs).reduce((a, b) => a + b, 0);
74
+ return total ? this.d.repeatedFailSigs / total : 0;
75
+ }
76
+ /**
77
+ * Green only when a verification ran after the last edit and passed; a turn
78
+ * that edited and never checked is unknown, and so is one with no edits.
79
+ */
80
+ get verdict() {
81
+ if (this.d.lastCheck === 'red')
82
+ return 'red';
83
+ if (this.d.edited && this.d.lastCheck === 'green')
84
+ return 'green';
85
+ return 'unknown';
86
+ }
87
+ edit(name) {
88
+ this.d.editCounts[name] = (this.d.editCounts[name] ?? 0) + 1;
89
+ this.d.edited = true;
90
+ this.d.lastCheck = null;
91
+ }
92
+ /**
93
+ * Fold one completed call. `failed` is what the host said: true for a tool
94
+ * error, false for success, null when it did not say. A call known to have
95
+ * succeeded is never a failing output; a content tool only when the tool
96
+ * itself failed; otherwise the output's own markers decide.
97
+ */
98
+ update(tool, args, result, failed) {
99
+ this.d.calls++;
100
+ let action = tool || '?';
101
+ if (EDIT_TOOLS.has(tool)) {
102
+ const b = basename(String(args.file_path ?? args.notebook_path ?? args.path ?? ''));
103
+ if (b) {
104
+ this.edit(b);
105
+ action = `edit ${b}`;
106
+ }
107
+ }
108
+ const cmd = tool === 'Bash' && typeof args.command === 'string' ? args.command : '';
109
+ let isTest = false;
110
+ if (cmd) {
111
+ action = `$ ${commandTemplate(cmd)}`;
112
+ for (const b of shellEditTargets(cmd))
113
+ this.edit(b);
114
+ if (TEST_RUN_RE.test(cmd)) {
115
+ isTest = true;
116
+ this.d.testRuns++;
117
+ }
118
+ }
119
+ const canFail = CONTENT_TOOLS.has(tool) ? failed === true : failed !== false;
120
+ let sig = null;
121
+ // A failure the host reported counts whatever its text says (`ls: cannot
122
+ // access` names no Error); otherwise the output's markers decide.
123
+ const errored = canFail && (failed === true || (Boolean(result) && isErrorText(result)));
124
+ if (errored) {
125
+ this.d.errorOutputs++;
126
+ sig = failureSignature(result);
127
+ if (sig) {
128
+ if (this.d.failSigs[sig])
129
+ this.d.repeatedFailSigs++;
130
+ this.d.failSigs[sig] = (this.d.failSigs[sig] ?? 0) + 1;
131
+ }
132
+ }
133
+ if (isTest)
134
+ this.d.lastCheck = errored ? 'red' : 'green';
135
+ this.d.recent.push(`${this.d.calls}: ${action}${sig ? ` [err#${sig}]` : ''}`);
136
+ if (this.d.recent.length > 40)
137
+ this.d.recent = this.d.recent.slice(-40);
138
+ }
139
+ snapshot() {
140
+ return {
141
+ calls: this.d.calls,
142
+ edit_churn: this.editChurn,
143
+ distinct_edit_files: this.distinctEditFiles,
144
+ error_outputs: this.d.errorOutputs,
145
+ test_runs: this.d.testRuns,
146
+ stagnation: Math.round(this.stagnation * 1000) / 1000,
147
+ repeated_fail_sigs: this.d.repeatedFailSigs,
148
+ };
149
+ }
150
+ /** The judge's digest: counts and the recent action log, never file content or output. */
151
+ digest(envelope, n = 24) {
152
+ const s = this.snapshot();
153
+ let head = `calls=${s.calls} edit_churn=${s.edit_churn} distinct_edit_files=${s.distinct_edit_files} ` +
154
+ `error_outputs=${s.error_outputs} test_runs=${s.test_runs} repeated_identical_failures=${s.repeated_fail_sigs} ` +
155
+ `stagnation=${this.stagnation.toFixed(2)}`;
156
+ if (envelope) {
157
+ head +=
158
+ `\nrepo_context: turns here that end green usually finish within ~${Math.round(envelope)} calls; ` +
159
+ `this turn is at ${s.calls} calls = ${(s.calls / envelope).toFixed(1)}x that. Checks are not green.`;
160
+ }
161
+ return `${head}\nRecent actions (oldest->newest):\n${this.d.recent.slice(-n).join('\n')}`;
162
+ }
163
+ }
164
+ //# sourceMappingURL=signals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.js","sourceRoot":"","sources":["../../src/economy/signals.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,209 @@
1
+ // Translated and adapted from microsoft/TokenEconomizer,
2
+ // tokeneconomizer/plugin/denylist.py and toolprune/tools.py at
3
+ // d3e193b8734ec76230da1e08d7929ba9d14640d8.
4
+ // Licensed under the Apache License, Version 2.0 (see ./LICENSE in this folder).
5
+ // Changes: ported from Python to TypeScript; propose mode first (nothing is
6
+ // written until the developer applies); the protected list includes
7
+ // Eklavya's own tools and the tools its quiz and delegation depend on; the
8
+ // file's original bytes are kept so a restore that undoes every change puts
9
+ // them back exactly; `.claude` reached through a symlink out of the checkout
10
+ // is refused.
11
+ import { execFileSync } from 'node:child_process';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { isEklavyaTool } from './transcript.js';
15
+ /**
16
+ * Tool pruning: tools a repository's sessions load and never use cost context
17
+ * on every request. Eklavya proposes denying them in this checkout's
18
+ * `.claude/settings.local.json`, and writes only when the developer runs
19
+ * `eklavya toolprune apply`.
20
+ */
21
+ /** Never proposed, whatever the evidence. */
22
+ export const PROTECTED = new Set([
23
+ 'Bash', 'Read', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Grep', 'Glob',
24
+ 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode', 'TodoWrite',
25
+ 'WebFetch', 'WebSearch',
26
+ 'Agent', 'Task', 'Skill', 'ToolSearch',
27
+ ]);
28
+ export function isProtected(tool, extra = []) {
29
+ return PROTECTED.has(tool) || isEklavyaTool(tool) || extra.includes(tool);
30
+ }
31
+ /** Unused for `evidence` prompts across `min_sessions` sessions, or for `sessions` sessions. */
32
+ export function propose(rows, rule) {
33
+ return rows
34
+ .filter((r) => !isProtected(r.tool, rule.protect))
35
+ .filter((r) => (r.prompts_since >= rule.evidence && r.sessions_since >= rule.min_sessions) || r.sessions_since >= rule.sessions)
36
+ .sort((a, b) => b.sessions_since - a.sessions_since || b.prompts_since - a.prompts_since || a.tool.localeCompare(b.tool));
37
+ }
38
+ /** Is `target` (followed through symlinks) inside `root`? */
39
+ function inside(root, target) {
40
+ const rel = path.relative(fs.realpathSync(root), target);
41
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
42
+ }
43
+ /** Two spaces unless the file's first indented line says otherwise. */
44
+ const indentOf = (text) => /\n([ \t]+)\S/.exec(text)?.[1] ?? ' ';
45
+ /** The settings file for a checkout, refusing a `.claude` that leads out of it. */
46
+ export function settingsPath(repoRoot) {
47
+ const dir = path.join(repoRoot, '.claude');
48
+ if (fs.existsSync(dir) && !inside(repoRoot, fs.realpathSync(dir))) {
49
+ return { reason: '.claude is a symlink that leads out of the repository' };
50
+ }
51
+ const file = path.join(dir, 'settings.local.json');
52
+ if (fs.existsSync(file) && !inside(repoRoot, fs.realpathSync(file))) {
53
+ return { reason: 'settings.local.json is a symlink that leads out of the repository' };
54
+ }
55
+ return { file };
56
+ }
57
+ function readSettings(file) {
58
+ if (!fs.existsSync(file))
59
+ return { text: null, json: {} };
60
+ const text = fs.readFileSync(file, 'utf8');
61
+ try {
62
+ const json = JSON.parse(text);
63
+ if (!json || typeof json !== 'object' || Array.isArray(json))
64
+ return { reason: 'settings.local.json is not a JSON object' };
65
+ const perms = json.permissions;
66
+ if (perms !== undefined && (!perms || typeof perms !== 'object' || Array.isArray(perms))) {
67
+ return { reason: 'settings.local.json has a permissions value that is not an object' };
68
+ }
69
+ const deny = perms?.deny;
70
+ if (deny !== undefined && !Array.isArray(deny))
71
+ return { reason: 'settings.local.json has a permissions.deny that is not a list' };
72
+ return { text, json: json };
73
+ }
74
+ catch {
75
+ return { reason: 'settings.local.json is not valid JSON' };
76
+ }
77
+ }
78
+ function writeJson(file, json, indent, trailingNewline) {
79
+ fs.mkdirSync(path.dirname(file), { recursive: true });
80
+ const tmp = `${file}.eklavya-tmp`;
81
+ fs.writeFileSync(tmp, JSON.stringify(json, null, indent) + (trailingNewline ? '\n' : ''));
82
+ fs.renameSync(tmp, file);
83
+ }
84
+ /**
85
+ * Adds `tools` to `permissions.deny` as bare names, leaving every existing
86
+ * rule alone. `previous` is the record of an earlier apply to this file, so a
87
+ * second apply keeps the first one's original bytes.
88
+ */
89
+ export function applyDeny(repoRoot, tools, previous = null) {
90
+ const where = settingsPath(repoRoot);
91
+ if ('reason' in where)
92
+ return { ok: false, reason: where.reason };
93
+ const read = readSettings(where.file);
94
+ if ('reason' in read)
95
+ return { ok: false, reason: read.reason };
96
+ const json = read.json;
97
+ const perms = (json.permissions ??= {});
98
+ const deny = (perms.deny ??= []);
99
+ const added = tools.filter((t) => !deny.includes(t));
100
+ const record = previous
101
+ ? { ...previous, added: [...new Set([...previous.added, ...added])] }
102
+ : { file: where.file, original: read.text, added, excludeLine: null };
103
+ if (!added.length)
104
+ return { ok: true, record };
105
+ deny.push(...added);
106
+ writeJson(where.file, json, read.text ? indentOf(read.text) : ' ', read.text ? read.text.endsWith('\n') : true);
107
+ if (read.text === null && !previous)
108
+ record.excludeLine = excludeFile(repoRoot, where.file);
109
+ return { ok: true, record };
110
+ }
111
+ /** Adds the file to `.git/info/exclude` when Eklavya created it; returns the line, or null. */
112
+ function excludeFile(repoRoot, file) {
113
+ const exclude = excludePath(repoRoot);
114
+ if (!exclude)
115
+ return null;
116
+ const line = `/${path.relative(repoRoot, file).split(path.sep).join('/')}`;
117
+ const current = fs.existsSync(exclude) ? fs.readFileSync(exclude, 'utf8') : '';
118
+ if (current.split('\n').includes(line))
119
+ return null;
120
+ fs.mkdirSync(path.dirname(exclude), { recursive: true });
121
+ fs.writeFileSync(exclude, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${line}\n`);
122
+ return line;
123
+ }
124
+ /**
125
+ * Removes exactly the entries Eklavya added. When nothing else in the file
126
+ * changed since, the original bytes come back (or the file goes, if Eklavya
127
+ * created it); otherwise only those entries leave and the rest is kept.
128
+ */
129
+ export function restoreDeny(repoRoot, record, only) {
130
+ const read = readSettings(record.file);
131
+ if ('reason' in read)
132
+ return { ok: false, reason: read.reason };
133
+ const removing = only ? record.added.filter((t) => only.includes(t)) : record.added;
134
+ const json = read.json;
135
+ const perms = json.permissions;
136
+ const deny = perms?.deny ?? [];
137
+ const kept = deny.filter((t) => !(typeof t === 'string' && removing.includes(t)));
138
+ const removed = removing.filter((t) => deny.includes(t));
139
+ const remaining = record.added.filter((t) => !removing.includes(t));
140
+ // Untouched: the file is still exactly what the apply made of the original,
141
+ // so the original bytes (or no file) can come back.
142
+ const original = record.original === null ? {} : JSON.parse(record.original);
143
+ const expected = structuredClone(original);
144
+ const ep = (expected.permissions ??= {});
145
+ ep.deny = [...(ep.deny ?? []), ...record.added.filter((t) => !(original.permissions?.deny ?? []).includes(t))];
146
+ const untouched = !remaining.length && JSON.stringify(read.json) === JSON.stringify(expected);
147
+ if (perms)
148
+ perms.deny = kept;
149
+ if (untouched) {
150
+ if (record.original === null) {
151
+ fs.rmSync(record.file, { force: true });
152
+ if (record.excludeLine)
153
+ unexclude(repoRoot, record.excludeLine);
154
+ }
155
+ else
156
+ fs.writeFileSync(record.file, record.original);
157
+ }
158
+ else if (removed.length) {
159
+ // Something was removed, so the file exists and has text.
160
+ writeJson(record.file, json, indentOf(read.text), read.text.endsWith('\n'));
161
+ }
162
+ return { ok: true, removed };
163
+ }
164
+ /**
165
+ * The checkout's `info/exclude`, asked of git: in a linked worktree `.git` is a
166
+ * file and the exclude file lives in the shared git dir. Null outside a
167
+ * checkout, or when git cannot say.
168
+ */
169
+ export function excludePath(repoRoot) {
170
+ if (!fs.existsSync(path.join(repoRoot, '.git')))
171
+ return null;
172
+ try {
173
+ const out = execFileSync('git', ['rev-parse', '--git-path', 'info/exclude'], {
174
+ cwd: repoRoot,
175
+ encoding: 'utf8',
176
+ timeout: 2_000,
177
+ stdio: ['ignore', 'pipe', 'ignore'],
178
+ }).trim();
179
+ return path.resolve(repoRoot, out);
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ function unexclude(repoRoot, line) {
186
+ const exclude = excludePath(repoRoot);
187
+ if (!exclude || !fs.existsSync(exclude))
188
+ return;
189
+ const lines = fs.readFileSync(exclude, 'utf8').split('\n');
190
+ const at = lines.lastIndexOf(line);
191
+ if (at === -1)
192
+ return;
193
+ lines.splice(at, 1);
194
+ fs.writeFileSync(exclude, lines.join('\n'));
195
+ }
196
+ /**
197
+ * The size of tool definitions before and after denying `tools`, from a
198
+ * catalog of definition sizes in estimated tokens. A deferred MCP tool costs
199
+ * one listing line, not its schema, so it is counted as such.
200
+ */
201
+ export function measure(catalog, deferred, denied) {
202
+ const LISTING = 12;
203
+ const cost = (t) => (deferred.includes(t) ? LISTING : catalog[t]);
204
+ const all = new Set([...Object.keys(catalog), ...deferred]);
205
+ const before = [...all].reduce((s, t) => s + cost(t), 0);
206
+ const after = [...all].filter((t) => !denied.includes(t)).reduce((s, t) => s + cost(t), 0);
207
+ return { before, after, saved: before - after };
208
+ }
209
+ //# sourceMappingURL=toolprune.js.map