@tekyzinc/gsd-t 2.73.24 → 2.74.10
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.
- package/CHANGELOG.md +34 -0
- package/bin/archive-progress.js +335 -0
- package/bin/context-budget-audit.js +432 -0
- package/bin/gsd-t.js +79 -1
- package/bin/log-tail.js +81 -0
- package/bin/orchestrator.js +233 -47
- package/commands/gsd-t-design-decompose.md +26 -2
- package/docs/context-budget-recovery-plan.md +170 -0
- package/package.json +1 -1
- package/scripts/gsd-t-design-review-server.js +197 -1
- package/scripts/gsd-t-design-review.html +727 -37
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Context Budget Audit — measures the static context cost of a Claude Code session
|
|
3
|
+
// before any user work happens. Reports tokens consumed by CLAUDE.md files, command
|
|
4
|
+
// files, MCP server tool descriptions, and skills. Used to diagnose why long-running
|
|
5
|
+
// sessions hit the manual compaction prompt.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/context-budget-audit.js # current project + global
|
|
9
|
+
// node bin/context-budget-audit.js --json # JSON output for tooling
|
|
10
|
+
// node bin/context-budget-audit.js --top 20 # top N largest files
|
|
11
|
+
// node bin/context-budget-audit.js --threshold 5000 # flag files above N tokens
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
// Token estimation: GPT/Claude tokenizers average ~4 chars/token for English+code.
|
|
18
|
+
// This is a fast deterministic estimate, not a true tokenizer call. Within ~10%.
|
|
19
|
+
const CHARS_PER_TOKEN = 4;
|
|
20
|
+
const CONTEXT_WINDOW = 200_000; // claude-opus-4-6 default
|
|
21
|
+
|
|
22
|
+
function estimateTokens(bytes) {
|
|
23
|
+
return Math.round(bytes / CHARS_PER_TOKEN);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtPct(n) {
|
|
27
|
+
return `${n.toFixed(1)}%`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fmtNum(n) {
|
|
31
|
+
return n.toLocaleString('en-US');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeStat(p) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.statSync(p);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeRead(p) {
|
|
43
|
+
try {
|
|
44
|
+
return fs.readFileSync(p, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listFiles(dir, ext = null) {
|
|
51
|
+
if (!fs.existsSync(dir)) return [];
|
|
52
|
+
try {
|
|
53
|
+
return fs
|
|
54
|
+
.readdirSync(dir)
|
|
55
|
+
.filter((f) => !ext || f.endsWith(ext))
|
|
56
|
+
.map((f) => path.join(dir, f))
|
|
57
|
+
.filter((p) => {
|
|
58
|
+
const s = safeStat(p);
|
|
59
|
+
return s && s.isFile();
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function measureFile(filePath) {
|
|
67
|
+
const stat = safeStat(filePath);
|
|
68
|
+
if (!stat) return null;
|
|
69
|
+
return {
|
|
70
|
+
path: filePath,
|
|
71
|
+
bytes: stat.size,
|
|
72
|
+
tokens: estimateTokens(stat.size),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function measureCategory(name, files, source) {
|
|
77
|
+
const measured = files
|
|
78
|
+
.map(measureFile)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
81
|
+
const totalBytes = measured.reduce((s, f) => s + f.bytes, 0);
|
|
82
|
+
const totalTokens = measured.reduce((s, f) => s + f.tokens, 0);
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
source,
|
|
86
|
+
fileCount: measured.length,
|
|
87
|
+
totalBytes,
|
|
88
|
+
totalTokens,
|
|
89
|
+
pctOfWindow: (totalTokens / CONTEXT_WINDOW) * 100,
|
|
90
|
+
files: measured,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Estimate MCP tool descriptions cost — each deferred tool exposes a name + ~50-150
|
|
95
|
+
// chars of description in the system prompt. We can't read them directly, but we
|
|
96
|
+
// know the count from the list provided in conversation context (22 in this session).
|
|
97
|
+
function estimateMcpToolsCost(toolCount = 22, avgCharsPerTool = 120) {
|
|
98
|
+
const bytes = toolCount * avgCharsPerTool;
|
|
99
|
+
return {
|
|
100
|
+
name: 'MCP deferred tool manifest (Figma + Gmail + Calendar)',
|
|
101
|
+
source: '~/.claude/settings.json mcpServers (estimated)',
|
|
102
|
+
fileCount: toolCount,
|
|
103
|
+
totalBytes: bytes,
|
|
104
|
+
totalTokens: estimateTokens(bytes),
|
|
105
|
+
pctOfWindow: (estimateTokens(bytes) / CONTEXT_WINDOW) * 100,
|
|
106
|
+
files: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Estimate built-in tool schemas cost — Read, Edit, Write, Bash, Glob, Grep, etc.
|
|
111
|
+
// These are loaded into every session. Approx based on their JSONSchema sizes.
|
|
112
|
+
function estimateBuiltinToolsCost() {
|
|
113
|
+
// Top-of-prompt tools observed: Agent, Bash, Edit, Glob, Grep, Read, ScheduleWakeup,
|
|
114
|
+
// Skill, ToolSearch, Write. Each is ~200-2000 tokens in JSONSchema form.
|
|
115
|
+
const tools = {
|
|
116
|
+
Agent: 1800,
|
|
117
|
+
Bash: 2200,
|
|
118
|
+
Edit: 600,
|
|
119
|
+
Glob: 300,
|
|
120
|
+
Grep: 800,
|
|
121
|
+
Read: 700,
|
|
122
|
+
ScheduleWakeup: 900,
|
|
123
|
+
Skill: 400,
|
|
124
|
+
ToolSearch: 500,
|
|
125
|
+
Write: 400,
|
|
126
|
+
};
|
|
127
|
+
const totalTokens = Object.values(tools).reduce((s, t) => s + t, 0);
|
|
128
|
+
return {
|
|
129
|
+
name: 'Built-in tool schemas (top-of-prompt)',
|
|
130
|
+
source: 'Claude Code system prompt (estimated)',
|
|
131
|
+
fileCount: Object.keys(tools).length,
|
|
132
|
+
totalBytes: totalTokens * CHARS_PER_TOKEN,
|
|
133
|
+
totalTokens,
|
|
134
|
+
pctOfWindow: (totalTokens / CONTEXT_WINDOW) * 100,
|
|
135
|
+
files: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Estimate Claude Code system prompt overhead — the harness instructions, env block,
|
|
140
|
+
// system reminders, gitStatus, etc. Observed in the prompt header.
|
|
141
|
+
function estimateSystemPromptCost() {
|
|
142
|
+
// The system prompt header (everything before "You are an interactive agent") plus
|
|
143
|
+
// the doing-tasks/tone-style/session-guidance sections is ~6000-8000 tokens.
|
|
144
|
+
const tokens = 7000;
|
|
145
|
+
return {
|
|
146
|
+
name: 'Claude Code system prompt (instructions, env, gitStatus)',
|
|
147
|
+
source: 'Claude Code harness',
|
|
148
|
+
fileCount: 1,
|
|
149
|
+
totalBytes: tokens * CHARS_PER_TOKEN,
|
|
150
|
+
totalTokens: tokens,
|
|
151
|
+
pctOfWindow: (tokens / CONTEXT_WINDOW) * 100,
|
|
152
|
+
files: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function audit({ projectDir, globalDir, top = 10, threshold = 0 }) {
|
|
157
|
+
const categories = [];
|
|
158
|
+
|
|
159
|
+
// 1. System prompt overhead
|
|
160
|
+
categories.push(estimateSystemPromptCost());
|
|
161
|
+
|
|
162
|
+
// 2. Built-in tool schemas
|
|
163
|
+
categories.push(estimateBuiltinToolsCost());
|
|
164
|
+
|
|
165
|
+
// 3. MCP deferred tool manifest
|
|
166
|
+
categories.push(estimateMcpToolsCost());
|
|
167
|
+
|
|
168
|
+
// 4. Global CLAUDE.md
|
|
169
|
+
const globalClaude = path.join(globalDir, 'CLAUDE.md');
|
|
170
|
+
if (fs.existsSync(globalClaude)) {
|
|
171
|
+
categories.push(measureCategory('Global ~/.claude/CLAUDE.md', [globalClaude], globalClaude));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 5. Project CLAUDE.md
|
|
175
|
+
const projectClaude = path.join(projectDir, 'CLAUDE.md');
|
|
176
|
+
if (fs.existsSync(projectClaude)) {
|
|
177
|
+
categories.push(measureCategory('Project CLAUDE.md', [projectClaude], projectClaude));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 6. Auto-memory (MEMORY.md + entries)
|
|
181
|
+
const memoryDir = path.join(
|
|
182
|
+
globalDir,
|
|
183
|
+
'projects',
|
|
184
|
+
`-${projectDir.replace(/\//g, '-').replace(/^-/, '')}`,
|
|
185
|
+
'memory'
|
|
186
|
+
);
|
|
187
|
+
if (fs.existsSync(memoryDir)) {
|
|
188
|
+
const memFiles = listFiles(memoryDir, '.md');
|
|
189
|
+
categories.push(measureCategory('Auto-memory (MEMORY.md + entries)', memFiles, memoryDir));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 7. Installed user commands — IMPORTANT: Claude Code's skill system loads only
|
|
193
|
+
// the manifest (name + first-line description) into the system prompt for each
|
|
194
|
+
// command file. The full body loads only when the skill is invoked. So the
|
|
195
|
+
// static cost is much smaller than the file size suggests — roughly ~150 chars
|
|
196
|
+
// per command (name + description) regardless of body length.
|
|
197
|
+
const userCommandsDir = path.join(globalDir, 'commands');
|
|
198
|
+
if (fs.existsSync(userCommandsDir)) {
|
|
199
|
+
const cmdFiles = listFiles(userCommandsDir, '.md');
|
|
200
|
+
const manifestBytesPerCmd = 200; // typical "- name: <desc>" line
|
|
201
|
+
const manifestBytes = cmdFiles.length * manifestBytesPerCmd;
|
|
202
|
+
const fullBodyBytes = cmdFiles.reduce((s, f) => {
|
|
203
|
+
const st = safeStat(f);
|
|
204
|
+
return s + (st ? st.size : 0);
|
|
205
|
+
}, 0);
|
|
206
|
+
categories.push({
|
|
207
|
+
name: 'User commands MANIFEST (~/.claude/commands/ — names+descriptions only)',
|
|
208
|
+
source: userCommandsDir,
|
|
209
|
+
fileCount: cmdFiles.length,
|
|
210
|
+
totalBytes: manifestBytes,
|
|
211
|
+
totalTokens: estimateTokens(manifestBytes),
|
|
212
|
+
pctOfWindow: (estimateTokens(manifestBytes) / CONTEXT_WINDOW) * 100,
|
|
213
|
+
files: cmdFiles
|
|
214
|
+
.map((p) => {
|
|
215
|
+
const st = safeStat(p);
|
|
216
|
+
return st
|
|
217
|
+
? { path: p, bytes: manifestBytesPerCmd, tokens: estimateTokens(manifestBytesPerCmd) }
|
|
218
|
+
: null;
|
|
219
|
+
})
|
|
220
|
+
.filter(Boolean),
|
|
221
|
+
});
|
|
222
|
+
// Also report what the FULL bodies cost when invoked, so trimming targets are visible
|
|
223
|
+
categories.push({
|
|
224
|
+
name: 'User command FULL BODIES (loaded only when each skill is invoked)',
|
|
225
|
+
source: userCommandsDir + ' (per-invocation cost, not baseline)',
|
|
226
|
+
fileCount: cmdFiles.length,
|
|
227
|
+
totalBytes: fullBodyBytes,
|
|
228
|
+
totalTokens: estimateTokens(fullBodyBytes),
|
|
229
|
+
pctOfWindow: (estimateTokens(fullBodyBytes) / CONTEXT_WINDOW) * 100,
|
|
230
|
+
files: cmdFiles.map(measureFile).filter(Boolean).sort((a, b) => b.tokens - a.tokens),
|
|
231
|
+
lazyLoaded: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 8. Project commands — same lazy-load semantics as user commands. Only count if
|
|
236
|
+
// it's a different directory AND the user's project actually exposes them as
|
|
237
|
+
// skills (most projects don't).
|
|
238
|
+
const projectCommandsDir = path.join(projectDir, 'commands');
|
|
239
|
+
if (
|
|
240
|
+
fs.existsSync(projectCommandsDir) &&
|
|
241
|
+
path.resolve(projectCommandsDir) !== path.resolve(userCommandsDir)
|
|
242
|
+
) {
|
|
243
|
+
const cmdFiles = listFiles(projectCommandsDir, '.md');
|
|
244
|
+
const manifestBytesPerCmd = 200;
|
|
245
|
+
const manifestBytes = cmdFiles.length * manifestBytesPerCmd;
|
|
246
|
+
const fullBodyBytes = cmdFiles.reduce((s, f) => {
|
|
247
|
+
const st = safeStat(f);
|
|
248
|
+
return s + (st ? st.size : 0);
|
|
249
|
+
}, 0);
|
|
250
|
+
categories.push({
|
|
251
|
+
name: 'Project commands MANIFEST (commands/ — names+descriptions only)',
|
|
252
|
+
source: projectCommandsDir,
|
|
253
|
+
fileCount: cmdFiles.length,
|
|
254
|
+
totalBytes: manifestBytes,
|
|
255
|
+
totalTokens: estimateTokens(manifestBytes),
|
|
256
|
+
pctOfWindow: (estimateTokens(manifestBytes) / CONTEXT_WINDOW) * 100,
|
|
257
|
+
files: [],
|
|
258
|
+
});
|
|
259
|
+
categories.push({
|
|
260
|
+
name: 'Project command FULL BODIES (per-invocation, not baseline)',
|
|
261
|
+
source: projectCommandsDir,
|
|
262
|
+
fileCount: cmdFiles.length,
|
|
263
|
+
totalBytes: fullBodyBytes,
|
|
264
|
+
totalTokens: estimateTokens(fullBodyBytes),
|
|
265
|
+
pctOfWindow: (estimateTokens(fullBodyBytes) / CONTEXT_WINDOW) * 100,
|
|
266
|
+
files: cmdFiles.map(measureFile).filter(Boolean).sort((a, b) => b.tokens - a.tokens),
|
|
267
|
+
lazyLoaded: true,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 9. Project skills directory (if any)
|
|
272
|
+
const skillsDir = path.join(globalDir, 'skills');
|
|
273
|
+
if (fs.existsSync(skillsDir)) {
|
|
274
|
+
const skillFiles = listFiles(skillsDir, '.md');
|
|
275
|
+
if (skillFiles.length > 0) {
|
|
276
|
+
categories.push(measureCategory('Skills (~/.claude/skills/)', skillFiles, skillsDir));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Compute totals — exclude lazy-loaded categories from the baseline cost
|
|
281
|
+
const baselineCats = categories.filter((c) => !c.lazyLoaded);
|
|
282
|
+
const totalTokens = baselineCats.reduce((s, c) => s + c.totalTokens, 0);
|
|
283
|
+
const totalBytes = baselineCats.reduce((s, c) => s + c.totalBytes, 0);
|
|
284
|
+
const totalPct = (totalTokens / CONTEXT_WINDOW) * 100;
|
|
285
|
+
const remaining = CONTEXT_WINDOW - totalTokens;
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
contextWindow: CONTEXT_WINDOW,
|
|
289
|
+
totalBytes,
|
|
290
|
+
totalTokens,
|
|
291
|
+
totalPct,
|
|
292
|
+
remaining,
|
|
293
|
+
remainingPct: (remaining / CONTEXT_WINDOW) * 100,
|
|
294
|
+
categories,
|
|
295
|
+
top,
|
|
296
|
+
threshold,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderReport(result) {
|
|
301
|
+
const lines = [];
|
|
302
|
+
const bar = (pct, width = 40) => {
|
|
303
|
+
const filled = Math.round((pct / 100) * width);
|
|
304
|
+
return '█'.repeat(Math.min(filled, width)) + '░'.repeat(Math.max(width - filled, 0));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push('═══════════════════════════════════════════════════════════════════');
|
|
309
|
+
lines.push(' CONTEXT BUDGET AUDIT — what consumes context before you type');
|
|
310
|
+
lines.push('═══════════════════════════════════════════════════════════════════');
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push(` Context window: ${fmtNum(result.contextWindow)} tokens`);
|
|
313
|
+
lines.push(` Static preamble cost: ${fmtNum(result.totalTokens)} tokens (${fmtPct(result.totalPct)})`);
|
|
314
|
+
lines.push(` Remaining for work: ${fmtNum(result.remaining)} tokens (${fmtPct(result.remainingPct)})`);
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push(` [${bar(result.totalPct)}] ${fmtPct(result.totalPct)}`);
|
|
317
|
+
lines.push('');
|
|
318
|
+
|
|
319
|
+
if (result.totalPct >= 70) {
|
|
320
|
+
lines.push(' 🔴 CRITICAL: preamble already consumes >70% of context window.');
|
|
321
|
+
lines.push(' Long-running tasks will trigger manual /compact prompts.');
|
|
322
|
+
} else if (result.totalPct >= 50) {
|
|
323
|
+
lines.push(' ⚠️ WARNING: preamble consumes >50% of context window.');
|
|
324
|
+
lines.push(' Single-pass edits on large files may trigger compaction.');
|
|
325
|
+
} else if (result.totalPct >= 30) {
|
|
326
|
+
lines.push(' ⚡ ELEVATED: preamble consumes >30% of context window.');
|
|
327
|
+
lines.push(' Multi-step workflows may approach compaction threshold.');
|
|
328
|
+
} else {
|
|
329
|
+
lines.push(' ✅ HEALTHY: preamble consumes <30% of context window.');
|
|
330
|
+
}
|
|
331
|
+
lines.push('');
|
|
332
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
333
|
+
lines.push(' BREAKDOWN BY CATEGORY (largest first)');
|
|
334
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
335
|
+
lines.push('');
|
|
336
|
+
|
|
337
|
+
const sortedCats = [...result.categories].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
338
|
+
for (const cat of sortedCats) {
|
|
339
|
+
const tag = cat.lazyLoaded ? ' [LAZY — not in baseline]' : '';
|
|
340
|
+
lines.push(` ${cat.name}${tag}`);
|
|
341
|
+
lines.push(` source: ${cat.source}`);
|
|
342
|
+
lines.push(
|
|
343
|
+
` files: ${cat.fileCount} bytes: ${fmtNum(cat.totalBytes)} tokens: ${fmtNum(cat.totalTokens)} (${fmtPct(cat.pctOfWindow)})`
|
|
344
|
+
);
|
|
345
|
+
lines.push(` [${bar(cat.pctOfWindow, 30)}]`);
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Top N files across all categories
|
|
350
|
+
const allFiles = result.categories
|
|
351
|
+
.flatMap((c) => c.files)
|
|
352
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
353
|
+
if (allFiles.length > 0) {
|
|
354
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
355
|
+
lines.push(` TOP ${result.top} HEAVIEST FILES`);
|
|
356
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
357
|
+
lines.push('');
|
|
358
|
+
const topFiles = allFiles.slice(0, result.top);
|
|
359
|
+
for (const f of topFiles) {
|
|
360
|
+
const pct = (f.tokens / CONTEXT_WINDOW) * 100;
|
|
361
|
+
const flag = pct >= 5 ? ' 🔥' : pct >= 2 ? ' ⚠️' : '';
|
|
362
|
+
const rel = f.path.replace(os.homedir(), '~');
|
|
363
|
+
lines.push(` ${fmtNum(f.tokens).padStart(7)} tok ${fmtPct(pct).padStart(6)} ${rel}${flag}`);
|
|
364
|
+
}
|
|
365
|
+
lines.push('');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Files above threshold
|
|
369
|
+
if (result.threshold > 0) {
|
|
370
|
+
const above = allFiles.filter((f) => f.tokens >= result.threshold);
|
|
371
|
+
if (above.length > 0) {
|
|
372
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
373
|
+
lines.push(` FILES ABOVE THRESHOLD (>= ${fmtNum(result.threshold)} tokens)`);
|
|
374
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
375
|
+
lines.push('');
|
|
376
|
+
for (const f of above) {
|
|
377
|
+
const rel = f.path.replace(os.homedir(), '~');
|
|
378
|
+
lines.push(` ${fmtNum(f.tokens).padStart(7)} tok ${rel}`);
|
|
379
|
+
}
|
|
380
|
+
lines.push('');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
385
|
+
lines.push(' NOTES');
|
|
386
|
+
lines.push('───────────────────────────────────────────────────────────────────');
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push(' - Token estimate uses 4 chars/token (within ~10% of real tokenizer).');
|
|
389
|
+
lines.push(' - "Static preamble" = everything loaded BEFORE you type your first');
|
|
390
|
+
lines.push(' message. Add ~10-20K tokens of typical conversation overhead.');
|
|
391
|
+
lines.push(' - Skills auto-load into the prompt as a list of names+descriptions.');
|
|
392
|
+
lines.push(' Their full bodies are loaded only when invoked, but the manifest');
|
|
393
|
+
lines.push(' itself contributes to baseline.');
|
|
394
|
+
lines.push(' - Built-in and MCP tool costs are estimates; real values vary by');
|
|
395
|
+
lines.push(' Claude Code version. Check ENABLE_TELEMETRY for exact counts.');
|
|
396
|
+
lines.push('');
|
|
397
|
+
|
|
398
|
+
return lines.join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function main() {
|
|
402
|
+
const args = process.argv.slice(2);
|
|
403
|
+
const opts = {
|
|
404
|
+
projectDir: process.cwd(),
|
|
405
|
+
globalDir: path.join(os.homedir(), '.claude'),
|
|
406
|
+
top: 10,
|
|
407
|
+
threshold: 0,
|
|
408
|
+
json: false,
|
|
409
|
+
};
|
|
410
|
+
for (let i = 0; i < args.length; i++) {
|
|
411
|
+
const a = args[i];
|
|
412
|
+
if (a === '--json') opts.json = true;
|
|
413
|
+
else if (a === '--top') opts.top = parseInt(args[++i], 10);
|
|
414
|
+
else if (a === '--threshold') opts.threshold = parseInt(args[++i], 10);
|
|
415
|
+
else if (a === '--project') opts.projectDir = path.resolve(args[++i]);
|
|
416
|
+
else if (a === '--global') opts.globalDir = path.resolve(args[++i]);
|
|
417
|
+
else if (a === '--help' || a === '-h') {
|
|
418
|
+
console.log('Usage: node bin/context-budget-audit.js [--json] [--top N] [--threshold N]');
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const result = audit(opts);
|
|
424
|
+
|
|
425
|
+
if (opts.json) {
|
|
426
|
+
console.log(JSON.stringify(result, null, 2));
|
|
427
|
+
} else {
|
|
428
|
+
console.log(renderReport(result));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
main();
|
package/bin/gsd-t.js
CHANGED
|
@@ -1546,7 +1546,9 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1546
1546
|
}
|
|
1547
1547
|
const guardAdded = updateProjectClaudeMd(claudeMd, projectName);
|
|
1548
1548
|
const changelogCreated = createProjectChangelog(projectDir, projectName);
|
|
1549
|
-
|
|
1549
|
+
const binToolsCopied = copyBinToolsToProject(projectDir, projectName);
|
|
1550
|
+
const archiveRan = runProgressArchiveMigration(projectDir, projectName);
|
|
1551
|
+
if (guardAdded || changelogCreated || binToolsCopied || archiveRan) {
|
|
1550
1552
|
counts.updated++;
|
|
1551
1553
|
} else {
|
|
1552
1554
|
info(`${projectName} — already up to date`);
|
|
@@ -1554,6 +1556,82 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1554
1556
|
}
|
|
1555
1557
|
}
|
|
1556
1558
|
|
|
1559
|
+
// Bin tools that should ship with every registered project. Listed here so adding
|
|
1560
|
+
// a new tool only requires appending to this array.
|
|
1561
|
+
const PROJECT_BIN_TOOLS = ["archive-progress.js", "log-tail.js", "context-budget-audit.js"];
|
|
1562
|
+
|
|
1563
|
+
function copyBinToolsToProject(projectDir, projectName) {
|
|
1564
|
+
const projectBinDir = path.join(projectDir, "bin");
|
|
1565
|
+
if (!fs.existsSync(projectBinDir)) {
|
|
1566
|
+
try {
|
|
1567
|
+
fs.mkdirSync(projectBinDir, { recursive: true });
|
|
1568
|
+
} catch {
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
let copied = 0;
|
|
1573
|
+
for (const tool of PROJECT_BIN_TOOLS) {
|
|
1574
|
+
const src = path.join(PKG_ROOT, "bin", tool);
|
|
1575
|
+
const dest = path.join(projectBinDir, tool);
|
|
1576
|
+
if (!fs.existsSync(src)) continue;
|
|
1577
|
+
let needsCopy = true;
|
|
1578
|
+
if (fs.existsSync(dest)) {
|
|
1579
|
+
try {
|
|
1580
|
+
const srcContent = fs.readFileSync(src, "utf8");
|
|
1581
|
+
const destContent = fs.readFileSync(dest, "utf8");
|
|
1582
|
+
if (srcContent === destContent) needsCopy = false;
|
|
1583
|
+
} catch {
|
|
1584
|
+
// fall through, will copy
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (needsCopy) {
|
|
1588
|
+
try {
|
|
1589
|
+
fs.copyFileSync(src, dest);
|
|
1590
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
1591
|
+
copied++;
|
|
1592
|
+
} catch (e) {
|
|
1593
|
+
warn(`${projectName} — failed to copy ${tool}: ${e.message}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (copied > 0) {
|
|
1598
|
+
info(`${projectName} — copied ${copied} bin tool(s)`);
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// One-shot migration: roll the project's progress.md Decision Log into archive
|
|
1605
|
+
// files using bin/archive-progress.js. A marker file ensures we only do this once
|
|
1606
|
+
// per project — subsequent runs are no-ops.
|
|
1607
|
+
function runProgressArchiveMigration(projectDir, projectName) {
|
|
1608
|
+
const progressMd = path.join(projectDir, ".gsd-t", "progress.md");
|
|
1609
|
+
if (!fs.existsSync(progressMd)) return false;
|
|
1610
|
+
|
|
1611
|
+
const markerPath = path.join(projectDir, ".gsd-t", ".archive-migration-v1");
|
|
1612
|
+
if (fs.existsSync(markerPath)) return false;
|
|
1613
|
+
|
|
1614
|
+
const archiveScript = path.join(projectDir, "bin", "archive-progress.js");
|
|
1615
|
+
if (!fs.existsSync(archiveScript)) return false;
|
|
1616
|
+
|
|
1617
|
+
try {
|
|
1618
|
+
const output = execFileSync("node", [archiveScript, "--quiet"], {
|
|
1619
|
+
cwd: projectDir,
|
|
1620
|
+
encoding: "utf8",
|
|
1621
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1622
|
+
});
|
|
1623
|
+
fs.writeFileSync(
|
|
1624
|
+
markerPath,
|
|
1625
|
+
`# archive-migration-v1\nApplied: ${new Date().toISOString()}\nTool: bin/archive-progress.js\n`
|
|
1626
|
+
);
|
|
1627
|
+
info(`${projectName} — progress.md Decision Log archived (one-time migration)`);
|
|
1628
|
+
return true;
|
|
1629
|
+
} catch (e) {
|
|
1630
|
+
warn(`${projectName} — archive migration failed: ${e.message}`);
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1557
1635
|
function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing, syncCount) {
|
|
1558
1636
|
log("");
|
|
1559
1637
|
heading("Update All Complete");
|
package/bin/log-tail.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Log Tail — print the last N lines of a log file. Used by GSD-T command files to
|
|
3
|
+
// truncate test/build output before forwarding it into the conversation context.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node bin/log-tail.js <logfile> # print last 100 lines
|
|
7
|
+
// node bin/log-tail.js <logfile> 500 # print last 500 lines
|
|
8
|
+
// node bin/log-tail.js <logfile> --on-fail # print 500 lines if the log contains
|
|
9
|
+
// "FAIL", "ERROR", or non-zero exit;
|
|
10
|
+
// 100 lines otherwise
|
|
11
|
+
//
|
|
12
|
+
// Why: piping `npm test` or `playwright test` directly into a Bash tool result
|
|
13
|
+
// dumps the entire stdout (often 5K-50K tokens) into context. This helper writes
|
|
14
|
+
// the full log to disk and prints only the tail, with a header showing the path
|
|
15
|
+
// to the full log so the agent can read more if needed.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const opts = { logFile: null, lines: 100, onFail: false };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (a === '--on-fail') opts.onFail = true;
|
|
25
|
+
else if (a === '--help' || a === '-h') {
|
|
26
|
+
console.log('Usage: node bin/log-tail.js <logfile> [N=100] [--on-fail]');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
} else if (!opts.logFile) {
|
|
29
|
+
opts.logFile = a;
|
|
30
|
+
} else if (/^\d+$/.test(a)) {
|
|
31
|
+
opts.lines = parseInt(a, 10);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return opts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detectFailure(content) {
|
|
38
|
+
return /\b(FAIL|FAILED|ERROR|Exception|Traceback|Test Failed|✗|❌)\b/i.test(content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tail(content, n) {
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
if (lines.length <= n) return lines;
|
|
44
|
+
return lines.slice(-n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
49
|
+
if (!opts.logFile) {
|
|
50
|
+
console.error('Usage: node bin/log-tail.js <logfile> [N=100] [--on-fail]');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const abs = path.resolve(opts.logFile);
|
|
55
|
+
if (!fs.existsSync(abs)) {
|
|
56
|
+
console.error(`log-tail: file not found — ${abs}`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
61
|
+
let n = opts.lines;
|
|
62
|
+
if (opts.onFail) {
|
|
63
|
+
n = detectFailure(content) ? 500 : 100;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const total = content.split('\n').length;
|
|
67
|
+
const tailLines = tail(content, n);
|
|
68
|
+
const truncated = total > n;
|
|
69
|
+
|
|
70
|
+
console.log(`─── log tail: ${abs} ───`);
|
|
71
|
+
console.log(` total lines: ${total} showing: ${tailLines.length} truncated: ${truncated}`);
|
|
72
|
+
if (truncated) {
|
|
73
|
+
console.log(` full log: cat ${abs}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
76
|
+
console.log(tailLines.join('\n'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (require.main === module) main();
|
|
80
|
+
|
|
81
|
+
module.exports = { tail, detectFailure };
|