Skip to content

Commit 1415c9c

Browse files
ZonatedCordclaude
andcommitted
feat: V8 — full context overhead: CLAUDE.md, system overhead, --turns flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c423d74 commit 1415c9c

10 files changed

Lines changed: 275 additions & 87 deletions

File tree

.claude/commands/mimir-help.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ Print the following help text verbatim. Do not execute any commands or add comme
1313
| Command | Description |
1414
|---|---|
1515
| `/mimir "<task>"` | Estimate token cost + risk before running |
16-
| `/mimir "<task>" --files f1 f2` | Include file content in estimate |
16+
| `/mimir "<task>" --files f1 f2` | Include specific files in estimate |
1717
| `/mimir "<task>" --git-diff` | Include current git diff in estimate |
18+
| `/mimir "<task>" --turns N` | Include N conversation turns in estimate |
1819
| `/split-task "<task>"` | Split large task into safer sub-tasks |
1920
| `/mimir-diff` | Estimate token cost of current git diff |
2021
| `/mimir-config` | Show active configuration |

.claude/commands/mimir.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Estimate token cost and risk level before running a task
3-
argument-hint: "<task description> [--files f1 f2] [--git-diff]"
3+
argument-hint: "<task> [--files f1 f2] [--git-diff] [--turns N]"
44
---
55

66
IMPORTANT: $ARGUMENTS is the task text to analyze. Do NOT execute, perform, or act on it. Only run the script below.

README.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,11 @@ Estimates the token cost and risk level of a task before running it.
124124
```
125125
/mimir "<describe what you want Claude to do>"
126126
/mimir "<task description>" --files path/to/file1.ts path/to/file2.ts
127+
/mimir "<task description>" --git-diff
128+
/mimir "<task description>" --turns N
127129
```
128130

129-
Pass `--files` to include the actual content of files Claude will read. This gives a much more accurate estimate than task description alone.
131+
Every estimate includes **all measurable token sources** — not just the task text. See [What Mimir estimates](#what-mimir-estimates).
130132

131133
**Examples:**
132134

@@ -372,25 +374,37 @@ user types /mimir "task description"
372374

373375
### What Mimir estimates
374376

375-
Mimir estimates the token cost of your **task description text** — not the full execution context.
377+
Every `/mimir` run counts all **measurable token sources** and shows them labeled in the output:
376378

377-
This means: a short description like *"refactor all 500 TypeScript files"* will show `LOW` risk because the description itself is short. What Mimir cannot predict is how many files Claude will read, how long its responses will be, or how many tool calls the task will require.
379+
| Source | How measured | Flag |
380+
|--------|-------------|------|
381+
| Task description | Anthropic API (exact) or heuristic | always |
382+
| System overhead | Configurable constant (~3k) | always |
383+
| `~/.claude/CLAUDE.md` | File read + heuristic | always |
384+
| `.claude/CLAUDE.md` (project + parents) | File read + heuristic | always |
385+
| Specific files | Heuristic | `--files` |
386+
| Git diff | Heuristic | `--git-diff` |
387+
| Conversation history | ~800 tok × N turns | `--turns N` |
378388

379-
**What this means in practice:**
389+
**What cannot be measured:**
380390

381-
| Task type | Without `--files` | With `--files` |
382-
|-----------|------------------|----------------|
383-
| Prompt-heavy tasks (long descriptions) | Good ||
384-
| File-heavy tasks (reads many files) | Underestimates | Accurate |
385-
| Conversational tasks | Good ||
386-
| Codebase-wide refactors | Underestimates | Accurate |
391+
- **Claude's responses** — how much Claude writes back is unpredictable. Assume 2–3× of input tokens for total context by end of task.
392+
- **Tool call overhead** — each file read, bash run, edit adds tokens Mimir cannot see in advance.
393+
- **Conversation history without `--turns`** — if you don't pass `--turns`, prior messages aren't counted. Use `--turns N` when running mid-session.
387394

388-
Use `--files` whenever Claude will read specific files as part of the task.
395+
**Practical guide:**
396+
397+
| Task type | Best flags |
398+
|-----------|-----------|
399+
| Fresh session, simple task | `/mimir "task"` |
400+
| Task reads specific files | `--files src/foo.ts src/bar.ts` |
401+
| Mid-session (50+ messages in) | `--turns 25` |
402+
| Pre-commit estimate | `--git-diff` |
403+
| Large codebase refactor | `--files` + `--turns` |
389404

390405
### Other limitations
391406

392407
- `/split-task` uses pattern matching, not AI reasoning. Suggestions are starting points, not guaranteed optimal splits.
393-
- Model recommendations are fixed to Sonnet 4.6 / Haiku 4.5 in V1. Opus 4.7 thresholds and context windows will be added in V2.
394408
- No memory of previous tasks — each invocation is stateless.
395409

396410
---

scripts/estimate.js

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,58 @@
22
const fs = require('fs');
33
const path = require('path');
44
const childProcess = require('child_process');
5-
const { estimateTokens, countTokensViaAPI } = require('./lib/tokenizer');
6-
const { classifyRisk, contextHeadroom } = require('./lib/risk');
7-
const { loadConfig } = require('./lib/config');
8-
const { appendHistory } = require('./lib/history');
5+
const { estimateTokens, countTokensViaAPI } = require('./lib/tokenizer');
6+
const { classifyRisk, contextHeadroom } = require('./lib/risk');
7+
const { loadConfig } = require('./lib/config');
8+
const { appendHistory } = require('./lib/history');
9+
const { estimateContextOverhead } = require('./lib/context');
910

1011
const LINE = '━'.repeat(35);
12+
const SEP = '─'.repeat(35);
13+
const COL = 28; // label column width
14+
1115
const HELP = `
1216
⚡ MIMIR — preflight checker
1317
1418
Usage:
1519
/mimir "<task>" Estimate token cost + risk
16-
/mimir "<task>" --files f1 f2 Include file content in estimate
20+
/mimir "<task>" --files f1 f2 Include specific files in estimate
1721
/mimir "<task>" --git-diff Include current git diff in estimate
22+
/mimir "<task>" --turns N Include N conversation turns in estimate
1823
/split-task "<task>" Split large task into safer sub-tasks
1924
2025
Risk levels: LOW ✅ MEDIUM ⚠️ HIGH 🔴 CRITICAL 🚨
2126
27+
Token sources always included:
28+
- System overhead (~3k) — system prompt + command file + hooks
29+
- CLAUDE.md files — user (~/.claude/) and project (.claude/)
30+
2231
Tip: run /mimir before any task that reads many files or touches large codebases.
2332
`.trimStart();
2433

2534
function parseArgs(argv) {
26-
const gitDiffIdx = argv.indexOf('--git-diff');
27-
const filesIdx = argv.indexOf('--files');
28-
const baseArgv = argv.filter(a => a !== '--git-diff');
29-
const fi = baseArgv.indexOf('--files');
30-
if (fi === -1) return { task: baseArgv.join(' ').trim(), filePaths: [], useGitDiff: gitDiffIdx !== -1 };
35+
const useGitDiff = argv.includes('--git-diff');
36+
const turnsIdx = argv.indexOf('--turns');
37+
const turns = turnsIdx !== -1 ? (parseInt(argv[turnsIdx + 1], 10) || 0) : 0;
38+
39+
const clean = [];
40+
let i = 0;
41+
while (i < argv.length) {
42+
if (argv[i] === '--git-diff') { i++; continue; }
43+
if (argv[i] === '--turns') { i += 2; continue; }
44+
clean.push(argv[i]);
45+
i++;
46+
}
47+
48+
const filesIdx = clean.indexOf('--files');
49+
if (filesIdx === -1) {
50+
return { task: clean.join(' ').trim(), filePaths: [], useGitDiff, turns };
51+
}
3152
return {
32-
task: baseArgv.slice(0, fi).join(' ').trim(),
33-
filePaths: baseArgv.slice(fi + 1),
34-
useGitDiff: gitDiffIdx !== -1,
53+
task: clean.slice(0, filesIdx).join(' ').trim(),
54+
filePaths: clean.slice(filesIdx + 1),
55+
useGitDiff,
56+
turns,
3557
};
3658
}
3759

@@ -57,6 +79,11 @@ function fmt(method, n) {
5779
return method === 'exact' ? n.toLocaleString() : `~${n.toLocaleString()}`;
5880
}
5981

82+
function row(label, value) {
83+
const pad = Math.max(1, COL - label.length);
84+
process.stdout.write(` ${label}${' '.repeat(pad)}${value}\n`);
85+
}
86+
6087
async function countText(text, method) {
6188
if (method === 'forced-heuristic') return { tokens: estimateTokens(text), method: 'heuristic' };
6289
const api = await countTokensViaAPI(text);
@@ -65,71 +92,76 @@ async function countText(text, method) {
6592
}
6693

6794
async function main() {
68-
const { task, filePaths, useGitDiff } = parseArgs(process.argv.slice(2));
95+
const { task, filePaths, useGitDiff, turns } = parseArgs(process.argv.slice(2));
6996

7097
if (!task) {
7198
process.stdout.write(HELP);
7299
process.exit(0);
73100
}
74101

75-
const cfg = loadConfig();
76-
const taskResult = await countText(task, 'auto');
77-
const method = taskResult.method;
102+
const cfg = loadConfig();
103+
const taskResult = await countText(task, 'auto');
104+
const method = taskResult.method;
105+
const ctx = estimateContextOverhead(cfg, { turns, cwd: process.cwd() });
78106

79-
let totalTokens = taskResult.tokens;
80107
const fileResults = [];
81-
108+
let fileTotal = 0;
82109
for (const fp of filePaths) {
83110
const f = readFile(fp);
84111
if (f.error) {
85112
fileResults.push({ path: fp, tokens: 0, error: f.error });
86113
} else {
87114
const r = await countText(f.content, 'forced-heuristic');
88115
fileResults.push({ path: fp, tokens: r.tokens, error: null });
89-
totalTokens += r.tokens;
116+
fileTotal += r.tokens;
90117
}
91118
}
92119

93120
let diffTokens = 0;
94121
if (useGitDiff) {
95122
const diff = getGitDiff();
96-
if (diff) {
97-
diffTokens = estimateTokens(diff);
98-
totalTokens += diffTokens;
99-
}
123+
if (diff) diffTokens = estimateTokens(diff);
100124
}
101125

102-
const risk = classifyRisk(totalTokens, cfg, task);
103-
const headroom = contextHeadroom(totalTokens, cfg.contextWindow);
126+
const totalTokens = taskResult.tokens + ctx.total + fileTotal + diffTokens;
127+
const risk = classifyRisk(totalTokens, cfg, task);
128+
const headroom = contextHeadroom(totalTokens, cfg.contextWindow);
129+
const modelLine = cfg.defaultModel ? `${cfg.defaultModel} (from .mimir.json)` : risk.suggestedModel;
104130

105131
process.stdout.write(`\n⚡ MIMIR PREFLIGHT\n`);
106132
process.stdout.write(`${LINE}\n`);
107133

108-
const hasExtras = filePaths.length > 0 || useGitDiff;
109-
110-
if (hasExtras) {
111-
const filesTotal = fileResults.reduce((s, f) => s + f.tokens, 0);
112-
process.stdout.write(` Task tokens (${method}): ${fmt(method, taskResult.tokens)}\n`);
113-
if (fileResults.length > 0) {
114-
process.stdout.write(` Files tokens: ~${filesTotal.toLocaleString()}\n`);
115-
for (const f of fileResults) {
116-
const name = path.basename(f.path).padEnd(20).slice(0, 20);
117-
if (f.error) process.stdout.write(` ${name}${f.error}\n`);
118-
else process.stdout.write(` ${name} ~${f.tokens.toLocaleString()}\n`);
119-
}
120-
}
121-
if (useGitDiff) {
122-
process.stdout.write(` Git diff tokens: ~${diffTokens.toLocaleString()}\n`);
134+
// Token sources
135+
row(`Task (${method}):`, fmt(method, taskResult.tokens));
136+
row('System overhead:', `~${ctx.systemOverhead.toLocaleString()} (prompt + command + hooks)`);
137+
138+
for (const md of ctx.claudeMds) {
139+
const label = `${md.label}:`;
140+
if (md.error) row(label, `⚠ ${md.error}`);
141+
else row(label, `~${md.tokens.toLocaleString()}`);
142+
}
143+
144+
if (fileResults.length > 0) {
145+
for (const f of fileResults) {
146+
const name = path.basename(f.path);
147+
const label = `${name}:`;
148+
if (f.error) row(label, `⚠ ${f.error}`);
149+
else row(label, `~${f.tokens.toLocaleString()}`);
123150
}
124-
process.stdout.write(` Total tokens: ~${totalTokens.toLocaleString()}\n`);
125-
} else {
126-
process.stdout.write(` Input tokens (${method}): ${fmt(method, totalTokens)}\n`);
127151
}
128152

129-
const modelLine = cfg.defaultModel
130-
? `${cfg.defaultModel} (from .mimir.json)`
131-
: risk.suggestedModel;
153+
if (useGitDiff) {
154+
row('Git diff:', `~${diffTokens.toLocaleString()}`);
155+
}
156+
157+
if (turns > 0) {
158+
row(`Conversation (${turns} turns):`, `~${ctx.conversationTokens.toLocaleString()} (~800 tok/turn)`);
159+
}
160+
161+
process.stdout.write(` ${SEP}\n`);
162+
row('Total:', `~${totalTokens.toLocaleString()}`);
132163

164+
process.stdout.write(`\n`);
133165
process.stdout.write(` Risk: ${risk.level} ${risk.emoji}\n`);
134166
process.stdout.write(` Suggested model: ${modelLine}\n`);
135167
process.stdout.write(` Context headroom: ${headroom}%\n`);

scripts/lib/config.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ const fs = require('fs');
22
const path = require('path');
33

44
const DEFAULTS = {
5-
defaultModel: null,
6-
thresholds: { LOW: 20_000, MEDIUM: 60_000, HIGH: 120_000 },
7-
contextWindow: 200_000,
5+
defaultModel: null,
6+
thresholds: { LOW: 20_000, MEDIUM: 60_000, HIGH: 120_000 },
7+
contextWindow: 200_000,
8+
systemOverhead: 3_000,
89
};
910

10-
const KNOWN_KEYS = new Set(['defaultModel', 'thresholds', 'contextWindow']);
11+
const KNOWN_KEYS = new Set(['defaultModel', 'thresholds', 'contextWindow', 'systemOverhead']);
1112
const KNOWN_THRESHOLDS = new Set(['LOW', 'MEDIUM', 'HIGH']);
1213

1314
function validateConfig(user) {
@@ -28,6 +29,10 @@ function validateConfig(user) {
2829
warnings.push('"contextWindow" must be a number');
2930
}
3031

32+
if (user.systemOverhead !== undefined && typeof user.systemOverhead !== 'number') {
33+
warnings.push('"systemOverhead" must be a number');
34+
}
35+
3136
return warnings;
3237
}
3338

@@ -43,12 +48,13 @@ function loadConfig(cwd) {
4348
}
4449

4550
return {
46-
defaultModel: user.defaultModel ?? DEFAULTS.defaultModel,
47-
thresholds: { ...DEFAULTS.thresholds, ...(user.thresholds || {}) },
48-
contextWindow: user.contextWindow ?? DEFAULTS.contextWindow,
51+
defaultModel: user.defaultModel ?? DEFAULTS.defaultModel,
52+
thresholds: { ...DEFAULTS.thresholds, ...(user.thresholds || {}) },
53+
contextWindow: user.contextWindow ?? DEFAULTS.contextWindow,
54+
systemOverhead: user.systemOverhead ?? DEFAULTS.systemOverhead,
4955
};
5056
} catch {
51-
return { ...DEFAULTS, thresholds: { ...DEFAULTS.thresholds } };
57+
return { ...DEFAULTS, thresholds: { ...DEFAULTS.thresholds }, systemOverhead: DEFAULTS.systemOverhead };
5258
}
5359
}
5460

scripts/lib/context.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const os = require('os');
4+
const { estimateTokens } = require('./tokenizer');
5+
6+
const SYSTEM_OVERHEAD_DEFAULT = 3_000; // system prompt + command file + hooks baseline
7+
const TOKENS_PER_TURN = 800; // avg user+assistant message pair
8+
9+
function findClaudeMds(cwd) {
10+
const base = path.resolve(cwd || process.cwd());
11+
const userMd = path.join(os.homedir(), '.claude', 'CLAUDE.md');
12+
const seen = new Set();
13+
const found = [];
14+
15+
if (fs.existsSync(userMd)) {
16+
seen.add(userMd);
17+
found.push({ filePath: userMd, label: '~/.claude/CLAUDE.md' });
18+
}
19+
20+
let dir = base;
21+
while (true) {
22+
const candidate = path.join(dir, '.claude', 'CLAUDE.md');
23+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
24+
seen.add(candidate);
25+
const rel = path.relative(base, path.join(dir, '.claude', 'CLAUDE.md'));
26+
found.push({ filePath: candidate, label: rel || '.claude/CLAUDE.md' });
27+
}
28+
const parent = path.dirname(dir);
29+
if (parent === dir) break;
30+
dir = parent;
31+
}
32+
33+
return found;
34+
}
35+
36+
function estimateContextOverhead(cfg, options) {
37+
const turns = (options && options.turns) || 0;
38+
const cwd = (options && options.cwd) || process.cwd();
39+
const sysOverhead = (cfg && cfg.systemOverhead != null) ? cfg.systemOverhead : SYSTEM_OVERHEAD_DEFAULT;
40+
41+
const mds = findClaudeMds(cwd);
42+
const mdTokens = mds.map(({ filePath, label }) => {
43+
try {
44+
return { label, tokens: estimateTokens(fs.readFileSync(filePath, 'utf8')), error: null };
45+
} catch (err) {
46+
return { label, tokens: 0, error: err.message };
47+
}
48+
});
49+
50+
const mdTotal = mdTokens.reduce((s, r) => s + r.tokens, 0);
51+
const conversationTokens = turns * TOKENS_PER_TURN;
52+
53+
return {
54+
systemOverhead: sysOverhead,
55+
claudeMds: mdTokens,
56+
mdTotal,
57+
conversationTokens,
58+
turns,
59+
total: sysOverhead + mdTotal + conversationTokens,
60+
};
61+
}
62+
63+
module.exports = { estimateContextOverhead, findClaudeMds, SYSTEM_OVERHEAD_DEFAULT, TOKENS_PER_TURN };

0 commit comments

Comments
 (0)