preflight-dev 3.1.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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +11 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +154 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +118 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/embeddings.d.ts +11 -0
- package/dist/lib/embeddings.js +88 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/files.d.ts +15 -0
- package/dist/lib/files.js +60 -0
- package/dist/lib/files.js.map +1 -0
- package/dist/lib/git-extractor.d.ts +9 -0
- package/dist/lib/git-extractor.js +116 -0
- package/dist/lib/git-extractor.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.js +86 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/session-parser.d.ts +45 -0
- package/dist/lib/session-parser.js +267 -0
- package/dist/lib/session-parser.js.map +1 -0
- package/dist/lib/state.d.ts +21 -0
- package/dist/lib/state.js +86 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/timeline-db.d.ts +67 -0
- package/dist/lib/timeline-db.js +380 -0
- package/dist/lib/timeline-db.js.map +1 -0
- package/dist/lib/triage.d.ts +29 -0
- package/dist/lib/triage.js +193 -0
- package/dist/lib/triage.js.map +1 -0
- package/dist/profiles.d.ts +3 -0
- package/dist/profiles.js +65 -0
- package/dist/profiles.js.map +1 -0
- package/dist/tools/audit-workspace.d.ts +2 -0
- package/dist/tools/audit-workspace.js +86 -0
- package/dist/tools/audit-workspace.js.map +1 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +108 -0
- package/dist/tools/checkpoint.js.map +1 -0
- package/dist/tools/clarify-intent.d.ts +2 -0
- package/dist/tools/clarify-intent.js +180 -0
- package/dist/tools/clarify-intent.js.map +1 -0
- package/dist/tools/enrich-agent-task.d.ts +2 -0
- package/dist/tools/enrich-agent-task.js +97 -0
- package/dist/tools/enrich-agent-task.js.map +1 -0
- package/dist/tools/generate-scorecard.d.ts +2 -0
- package/dist/tools/generate-scorecard.js +617 -0
- package/dist/tools/generate-scorecard.js.map +1 -0
- package/dist/tools/log-correction.d.ts +2 -0
- package/dist/tools/log-correction.js +76 -0
- package/dist/tools/log-correction.js.map +1 -0
- package/dist/tools/onboard-project.d.ts +2 -0
- package/dist/tools/onboard-project.js +179 -0
- package/dist/tools/onboard-project.js.map +1 -0
- package/dist/tools/preflight-check.d.ts +2 -0
- package/dist/tools/preflight-check.js +229 -0
- package/dist/tools/preflight-check.js.map +1 -0
- package/dist/tools/prompt-score.d.ts +2 -0
- package/dist/tools/prompt-score.js +132 -0
- package/dist/tools/prompt-score.js.map +1 -0
- package/dist/tools/scan-sessions.d.ts +2 -0
- package/dist/tools/scan-sessions.js +182 -0
- package/dist/tools/scan-sessions.js.map +1 -0
- package/dist/tools/scope-work.d.ts +2 -0
- package/dist/tools/scope-work.js +214 -0
- package/dist/tools/scope-work.js.map +1 -0
- package/dist/tools/search-history.d.ts +2 -0
- package/dist/tools/search-history.js +130 -0
- package/dist/tools/search-history.js.map +1 -0
- package/dist/tools/sequence-tasks.d.ts +2 -0
- package/dist/tools/sequence-tasks.js +165 -0
- package/dist/tools/sequence-tasks.js.map +1 -0
- package/dist/tools/session-handoff.d.ts +2 -0
- package/dist/tools/session-handoff.js +113 -0
- package/dist/tools/session-handoff.js.map +1 -0
- package/dist/tools/session-health.d.ts +2 -0
- package/dist/tools/session-health.js +111 -0
- package/dist/tools/session-health.js.map +1 -0
- package/dist/tools/session-stats.d.ts +2 -0
- package/dist/tools/session-stats.js +112 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/sharpen-followup.d.ts +2 -0
- package/dist/tools/sharpen-followup.js +192 -0
- package/dist/tools/sharpen-followup.js.map +1 -0
- package/dist/tools/timeline-view.d.ts +2 -0
- package/dist/tools/timeline-view.js +165 -0
- package/dist/tools/timeline-view.js.map +1 -0
- package/dist/tools/token-audit.d.ts +2 -0
- package/dist/tools/token-audit.js +227 -0
- package/dist/tools/token-audit.js.map +1 -0
- package/dist/tools/verify-completion.d.ts +2 -0
- package/dist/tools/verify-completion.js +154 -0
- package/dist/tools/verify-completion.js.map +1 -0
- package/dist/tools/what-changed.d.ts +2 -0
- package/dist/tools/what-changed.js +40 -0
- package/dist/tools/what-changed.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/cli/init.ts +133 -0
- package/src/index.ts +135 -0
- package/src/lib/config.ts +157 -0
- package/src/lib/embeddings.ts +118 -0
- package/src/lib/files.ts +59 -0
- package/src/lib/git-extractor.ts +137 -0
- package/src/lib/git.ts +89 -0
- package/src/lib/session-parser.ts +325 -0
- package/src/lib/state.ts +86 -0
- package/src/lib/timeline-db.ts +490 -0
- package/src/lib/triage.ts +255 -0
- package/src/profiles.ts +70 -0
- package/src/templates/config.yml +23 -0
- package/src/templates/triage.yml +27 -0
- package/src/tools/audit-workspace.ts +97 -0
- package/src/tools/checkpoint.ts +119 -0
- package/src/tools/clarify-intent.ts +191 -0
- package/src/tools/enrich-agent-task.ts +108 -0
- package/src/tools/generate-scorecard.ts +673 -0
- package/src/tools/log-correction.ts +89 -0
- package/src/tools/onboard-project.ts +214 -0
- package/src/tools/preflight-check.ts +263 -0
- package/src/tools/prompt-score.ts +150 -0
- package/src/tools/scan-sessions.ts +209 -0
- package/src/tools/scope-work.ts +238 -0
- package/src/tools/search-history.ts +145 -0
- package/src/tools/sequence-tasks.ts +182 -0
- package/src/tools/session-handoff.ts +125 -0
- package/src/tools/session-health.ts +107 -0
- package/src/tools/session-stats.ts +134 -0
- package/src/tools/sharpen-followup.ts +200 -0
- package/src/tools/timeline-view.ts +181 -0
- package/src/tools/token-audit.ts +259 -0
- package/src/tools/verify-completion.ts +159 -0
- package/src/tools/what-changed.ts +48 -0
- package/src/types.ts +87 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// CATEGORY 5: token_audit — Token Efficiency
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { run } from "../lib/git.js";
|
|
5
|
+
import { readIfExists, findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js";
|
|
6
|
+
import { loadState, saveState, now, STATE_DIR } from "../lib/state.js";
|
|
7
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
/** Shell-escape a filename for safe interpolation */
|
|
11
|
+
function shellEscape(s: string): string {
|
|
12
|
+
return s.replace(/'/g, "'\\''");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Grade thresholds rationale:
|
|
17
|
+
* - A (0-10): Minimal waste — small diffs, targeted reads, lean context
|
|
18
|
+
* - B (11-25): Minor waste — a few large files or slightly bloated docs
|
|
19
|
+
* - C (26-45): Moderate waste — repeated reads, large diffs, or context bloat
|
|
20
|
+
* - D (46-65): Significant waste — multiple anti-patterns compounding
|
|
21
|
+
* - F (66+): Severe waste — session is burning tokens on avoidable overhead
|
|
22
|
+
*
|
|
23
|
+
* Each pattern contributes a weighted score reflecting its relative token cost.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const MAX_TOOL_LOG_BYTES = 5 * 1024 * 1024; // 5MB limit for tool log parsing
|
|
27
|
+
|
|
28
|
+
export function registerTokenAudit(server: McpServer): void {
|
|
29
|
+
server.tool(
|
|
30
|
+
"token_audit",
|
|
31
|
+
`Detect token waste patterns in Claude Code sessions — repeated reads, large file dumps, context bloat, skill paste overhead. Call periodically to check efficiency, especially in long sessions.`,
|
|
32
|
+
{
|
|
33
|
+
session_actions: z.string().optional().describe("Description of recent actions if available"),
|
|
34
|
+
check_mode: z.enum(["quick", "deep"]).default("quick").describe("Quick checks git/files; deep also analyzes tool call patterns"),
|
|
35
|
+
},
|
|
36
|
+
async ({ session_actions, check_mode }) => {
|
|
37
|
+
const patterns: string[] = [];
|
|
38
|
+
const recommendations: string[] = [];
|
|
39
|
+
let wasteScore = 0;
|
|
40
|
+
|
|
41
|
+
// 1. Git diff size & dirty file count
|
|
42
|
+
const diffStat = run("git diff --stat --no-color 2>/dev/null");
|
|
43
|
+
const dirtyFiles = run("git diff --name-only 2>/dev/null");
|
|
44
|
+
const dirtyList = dirtyFiles.split("\n").filter(Boolean);
|
|
45
|
+
const dirtyCount = dirtyList.length;
|
|
46
|
+
|
|
47
|
+
const summaryLine = diffStat.split("\n").pop() || "";
|
|
48
|
+
const insertionsMatch = summaryLine.match(/(\d+) insertion/);
|
|
49
|
+
const deletionsMatch = summaryLine.match(/(\d+) deletion/);
|
|
50
|
+
const totalChanges = (parseInt(insertionsMatch?.[1] || "0") || 0) + (parseInt(deletionsMatch?.[1] || "0") || 0);
|
|
51
|
+
|
|
52
|
+
if (totalChanges > 2000) {
|
|
53
|
+
patterns.push(`Large uncommitted diff: ~${totalChanges} changed lines across ${dirtyCount} files`);
|
|
54
|
+
recommendations.push("Commit more frequently — large diffs bloat context when re-read");
|
|
55
|
+
wasteScore += 15;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Estimated context size from dirty files (safe line counting)
|
|
59
|
+
const AVG_LINE_BYTES = 45;
|
|
60
|
+
const AVG_TOKENS_PER_BYTE = 0.25;
|
|
61
|
+
let estimatedContextTokens = 0;
|
|
62
|
+
const largeFiles: string[] = [];
|
|
63
|
+
|
|
64
|
+
for (const f of dirtyList.slice(0, 30)) {
|
|
65
|
+
// Use shell-safe quoting instead of interpolation
|
|
66
|
+
const wc = run(`wc -l < '${shellEscape(f)}' 2>/dev/null`);
|
|
67
|
+
const lines = parseInt(wc) || 0;
|
|
68
|
+
estimatedContextTokens += lines * AVG_LINE_BYTES * AVG_TOKENS_PER_BYTE;
|
|
69
|
+
if (lines > 500) {
|
|
70
|
+
largeFiles.push(`${f} (${lines} lines)`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (largeFiles.length > 0) {
|
|
75
|
+
patterns.push(`Large files in working set (>500 lines): ${largeFiles.join(", ")}`);
|
|
76
|
+
recommendations.push("Use offset/limit when reading large files — avoid dumping entire contents");
|
|
77
|
+
wasteScore += largeFiles.length * 8;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. CLAUDE.md bloat check
|
|
81
|
+
const claudeMd = readIfExists("CLAUDE.md", 1);
|
|
82
|
+
if (claudeMd !== null) {
|
|
83
|
+
const stat = run(`wc -c < '${shellEscape("CLAUDE.md")}' 2>/dev/null`);
|
|
84
|
+
const bytes = parseInt(stat) || 0;
|
|
85
|
+
if (bytes > 5120) {
|
|
86
|
+
patterns.push(`CLAUDE.md is ${(bytes / 1024).toFixed(1)}KB — injected every session, burns tokens on paste`);
|
|
87
|
+
recommendations.push("Trim CLAUDE.md to essentials (<5KB). Move reference docs to files read on-demand");
|
|
88
|
+
wasteScore += 12;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. Workspace doc bloat
|
|
93
|
+
const docs = findWorkspaceDocs();
|
|
94
|
+
const docValues = Object.values(docs);
|
|
95
|
+
const totalDocSize = docValues.length > 0
|
|
96
|
+
? docValues.reduce((sum, d) => sum + (d.size || 0), 0)
|
|
97
|
+
: 0;
|
|
98
|
+
if (totalDocSize > 20000) {
|
|
99
|
+
patterns.push(`Workspace context docs total ~${(totalDocSize / 1024).toFixed(1)}KB`);
|
|
100
|
+
recommendations.push("Consolidate or slim workspace docs — every byte is injected each turn");
|
|
101
|
+
wasteScore += 8;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Sub-agent count — check multiple possible state locations
|
|
105
|
+
let subAgentCount = 0;
|
|
106
|
+
const stateLocations = [
|
|
107
|
+
join(STATE_DIR, "session.json"),
|
|
108
|
+
join(PROJECT_DIR, ".claude", "state.json"),
|
|
109
|
+
];
|
|
110
|
+
for (const loc of stateLocations) {
|
|
111
|
+
if (!existsSync(loc)) continue;
|
|
112
|
+
try {
|
|
113
|
+
const content = readFileSync(loc, "utf-8");
|
|
114
|
+
const state = JSON.parse(content);
|
|
115
|
+
subAgentCount = state.subAgents?.length || state.sub_agents?.length || 0;
|
|
116
|
+
if (subAgentCount > 0) break;
|
|
117
|
+
} catch { /* ignore malformed state */ }
|
|
118
|
+
}
|
|
119
|
+
if (subAgentCount > 5) {
|
|
120
|
+
patterns.push(`${subAgentCount} sub-agents spawned this session`);
|
|
121
|
+
recommendations.push("Batch related work to reduce sub-agent overhead — each carries full context");
|
|
122
|
+
wasteScore += subAgentCount * 3;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 6. Deep mode: tool call pattern analysis (with size limit)
|
|
126
|
+
let repeatedReads: Record<string, number> = {};
|
|
127
|
+
let totalToolCalls = 0;
|
|
128
|
+
|
|
129
|
+
if (check_mode === "deep") {
|
|
130
|
+
const toolLogPath = join(STATE_DIR, "tool-calls.jsonl");
|
|
131
|
+
if (existsSync(toolLogPath)) {
|
|
132
|
+
try {
|
|
133
|
+
const stat = statSync(toolLogPath);
|
|
134
|
+
if (stat.size > MAX_TOOL_LOG_BYTES) {
|
|
135
|
+
patterns.push(`Tool call log is ${(stat.size / 1024 / 1024).toFixed(1)}MB — too large for full analysis, sampling tail`);
|
|
136
|
+
wasteScore += 5;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Read with size cap: take the tail if too large
|
|
140
|
+
const raw = stat.size <= MAX_TOOL_LOG_BYTES
|
|
141
|
+
? readFileSync(toolLogPath, "utf-8")
|
|
142
|
+
: run(`tail -c ${MAX_TOOL_LOG_BYTES} '${shellEscape(toolLogPath)}'`);
|
|
143
|
+
|
|
144
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
145
|
+
totalToolCalls = lines.length;
|
|
146
|
+
const readCounts: Record<string, number> = {};
|
|
147
|
+
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
try {
|
|
150
|
+
const entry = JSON.parse(line);
|
|
151
|
+
const tool = entry.tool || entry.name || "";
|
|
152
|
+
const filePath = entry.params?.path || entry.params?.file_path || "";
|
|
153
|
+
if ((tool === "Read" || tool === "read") && filePath) {
|
|
154
|
+
readCounts[filePath] = (readCounts[filePath] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
} catch { /* skip malformed lines */ }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
repeatedReads = Object.fromEntries(
|
|
160
|
+
Object.entries(readCounts).filter(([, count]) => count >= 3)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (Object.keys(repeatedReads).length > 0) {
|
|
164
|
+
const top = Object.entries(repeatedReads)
|
|
165
|
+
.sort((a, b) => b[1] - a[1])
|
|
166
|
+
.slice(0, 5)
|
|
167
|
+
.map(([f, c]) => `${f} (${c}x)`)
|
|
168
|
+
.join(", ");
|
|
169
|
+
patterns.push(`Repeated file reads: ${top}`);
|
|
170
|
+
recommendations.push("Cache file contents or use targeted reads (offset/limit) to avoid re-reading");
|
|
171
|
+
wasteScore += Object.keys(repeatedReads).length * 6;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (totalToolCalls > 100) {
|
|
175
|
+
patterns.push(`High tool call volume: ${totalToolCalls} calls in session`);
|
|
176
|
+
recommendations.push("Plan multi-step work before executing — fewer, more precise tool calls save tokens");
|
|
177
|
+
wasteScore += 10;
|
|
178
|
+
}
|
|
179
|
+
} catch { /* ignore */ }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 7. Session action hints
|
|
184
|
+
if (session_actions) {
|
|
185
|
+
const lower = session_actions.toLowerCase();
|
|
186
|
+
if (lower.includes("read") && lower.includes("same file")) {
|
|
187
|
+
patterns.push("User reports repeated reads of same file");
|
|
188
|
+
wasteScore += 10;
|
|
189
|
+
}
|
|
190
|
+
if (lower.includes("full file") || lower.includes("entire file")) {
|
|
191
|
+
patterns.push("User reports full-file reads where partial would suffice");
|
|
192
|
+
wasteScore += 8;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Score calculation
|
|
197
|
+
const clampedWaste = Math.min(wasteScore, 100);
|
|
198
|
+
const grade =
|
|
199
|
+
clampedWaste <= 10 ? "A" :
|
|
200
|
+
clampedWaste <= 25 ? "B" :
|
|
201
|
+
clampedWaste <= 45 ? "C" :
|
|
202
|
+
clampedWaste <= 65 ? "D" : "F";
|
|
203
|
+
|
|
204
|
+
const savingsEstimate =
|
|
205
|
+
clampedWaste <= 10 ? "< 5%" :
|
|
206
|
+
clampedWaste <= 25 ? "5–15%" :
|
|
207
|
+
clampedWaste <= 45 ? "15–30%" :
|
|
208
|
+
clampedWaste <= 65 ? "30–50%" : "50%+";
|
|
209
|
+
|
|
210
|
+
if (patterns.length === 0) {
|
|
211
|
+
patterns.push("No significant token waste patterns detected");
|
|
212
|
+
}
|
|
213
|
+
if (recommendations.length === 0) {
|
|
214
|
+
recommendations.push("Session looks efficient — keep using targeted reads and incremental commits");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Persist audit state
|
|
218
|
+
saveState("token_audit", {
|
|
219
|
+
ts: now(),
|
|
220
|
+
grade,
|
|
221
|
+
wasteScore: clampedWaste,
|
|
222
|
+
savingsEstimate,
|
|
223
|
+
patternsFound: patterns.length,
|
|
224
|
+
mode: check_mode,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const report = [
|
|
228
|
+
`## Token Efficiency Audit (${check_mode} mode)`,
|
|
229
|
+
"",
|
|
230
|
+
`**Score: ${grade}** (waste index: ${clampedWaste}/100)`,
|
|
231
|
+
`**Estimated savings if addressed: ${savingsEstimate}**`,
|
|
232
|
+
"",
|
|
233
|
+
"### Grading Scale",
|
|
234
|
+
"| Grade | Waste | Meaning |",
|
|
235
|
+
"|-------|-------|---------|",
|
|
236
|
+
"| A | 0–10 | Minimal waste |",
|
|
237
|
+
"| B | 11–25 | Minor inefficiencies |",
|
|
238
|
+
"| C | 26–45 | Moderate — worth addressing |",
|
|
239
|
+
"| D | 46–65 | Significant token burn |",
|
|
240
|
+
"| F | 66+ | Severe — immediate action needed |",
|
|
241
|
+
"",
|
|
242
|
+
"### Working Set",
|
|
243
|
+
`- Dirty files: ${dirtyCount}`,
|
|
244
|
+
`- Uncommitted changes: ~${totalChanges} lines`,
|
|
245
|
+
`- Est. context from dirty files: ~${Math.round(estimatedContextTokens).toLocaleString()} tokens`,
|
|
246
|
+
`- Sub-agents spawned: ${subAgentCount}`,
|
|
247
|
+
...(check_mode === "deep" ? [`- Total tool calls analyzed: ${totalToolCalls}`, `- Files read 3+ times: ${Object.keys(repeatedReads).length}`] : []),
|
|
248
|
+
"",
|
|
249
|
+
"### Waste Patterns",
|
|
250
|
+
...patterns.map((p) => `- ⚠️ ${p}`),
|
|
251
|
+
"",
|
|
252
|
+
"### Recommendations",
|
|
253
|
+
...recommendations.map((r) => `- 💡 ${r}`),
|
|
254
|
+
].join("\n");
|
|
255
|
+
|
|
256
|
+
return { content: [{ type: "text" as const, text: report }] };
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { run, getStatus } from "../lib/git.js";
|
|
4
|
+
import { PROJECT_DIR } from "../lib/files.js";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
/** Detect package manager from lockfiles */
|
|
9
|
+
function detectPM(): string {
|
|
10
|
+
if (existsSync(join(PROJECT_DIR, "pnpm-lock.yaml"))) return "pnpm";
|
|
11
|
+
if (existsSync(join(PROJECT_DIR, "yarn.lock"))) return "yarn";
|
|
12
|
+
if (existsSync(join(PROJECT_DIR, "bun.lockb"))) return "bun";
|
|
13
|
+
return "npx";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Detect test runner from config/dependencies */
|
|
17
|
+
function detectTestRunner(): string | null {
|
|
18
|
+
// Check for common test configs
|
|
19
|
+
const configs = [
|
|
20
|
+
"playwright.config.ts", "playwright.config.js",
|
|
21
|
+
"vitest.config.ts", "vitest.config.js",
|
|
22
|
+
"jest.config.ts", "jest.config.js", "jest.config.mjs",
|
|
23
|
+
];
|
|
24
|
+
for (const c of configs) {
|
|
25
|
+
if (existsSync(join(PROJECT_DIR, c))) {
|
|
26
|
+
if (c.startsWith("playwright")) return "playwright";
|
|
27
|
+
if (c.startsWith("vitest")) return "vitest";
|
|
28
|
+
if (c.startsWith("jest")) return "jest";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Check if a build script exists in package.json */
|
|
35
|
+
function hasBuildScript(): boolean {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(run("cat package.json 2>/dev/null"));
|
|
38
|
+
return !!pkg?.scripts?.build;
|
|
39
|
+
} catch { return false; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerVerifyCompletion(server: McpServer): void {
|
|
43
|
+
server.tool(
|
|
44
|
+
"verify_completion",
|
|
45
|
+
`Verify that work is actually complete before declaring done. Runs type check, relevant tests, checks for uncommitted files, and validates against the original task criteria. Call this BEFORE saying "done" or committing final work.`,
|
|
46
|
+
{
|
|
47
|
+
task_description: z.string().describe("What was the task? Used to check if success criteria are met."),
|
|
48
|
+
test_scope: z.string().optional().describe("Which tests to run: 'all', a directory/keyword, or a specific spec file path. Default: auto-detect from changed files."),
|
|
49
|
+
skip_tests: z.boolean().optional().describe("Skip running tests (only check types + git state). Default: false."),
|
|
50
|
+
skip_build: z.boolean().optional().describe("Skip build check. Default: false."),
|
|
51
|
+
},
|
|
52
|
+
async ({ task_description, test_scope, skip_tests, skip_build }) => {
|
|
53
|
+
const pm = detectPM();
|
|
54
|
+
const sections: string[] = [];
|
|
55
|
+
const checks: { name: string; passed: boolean; detail: string }[] = [];
|
|
56
|
+
|
|
57
|
+
// 1. Type check (single invocation, extract both result and count)
|
|
58
|
+
const tscOutput = run(`${pm === "npx" ? "npx" : pm} tsc --noEmit 2>&1 | tail -20`);
|
|
59
|
+
const errorLines = tscOutput.split("\n").filter(l => /error TS\d+/.test(l));
|
|
60
|
+
const typePassed = errorLines.length === 0;
|
|
61
|
+
checks.push({
|
|
62
|
+
name: "Type Check",
|
|
63
|
+
passed: typePassed,
|
|
64
|
+
detail: typePassed
|
|
65
|
+
? "✅ Clean"
|
|
66
|
+
: `❌ ${errorLines.length} errors\n${errorLines.slice(0, 10).join("\n")}${errorLines.length > 10 ? `\n... and ${errorLines.length - 10} more` : ""}`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 2. Git state
|
|
70
|
+
const dirty = getStatus();
|
|
71
|
+
const dirtyCount = dirty ? dirty.split("\n").filter(Boolean).length : 0;
|
|
72
|
+
checks.push({
|
|
73
|
+
name: "Git State",
|
|
74
|
+
passed: true, // informational, not a blocker
|
|
75
|
+
detail: dirtyCount > 0
|
|
76
|
+
? `${dirtyCount} uncommitted files:\n\`\`\`\n${dirty}\n\`\`\``
|
|
77
|
+
: "✅ Clean working tree",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 3. Tests
|
|
81
|
+
if (!skip_tests) {
|
|
82
|
+
const runner = detectTestRunner();
|
|
83
|
+
const changedFiles = run("git diff --name-only HEAD~1 2>/dev/null").split("\n").filter(Boolean);
|
|
84
|
+
let testCmd = "";
|
|
85
|
+
|
|
86
|
+
if (runner === "playwright") {
|
|
87
|
+
const runnerCmd = `${pm === "npx" ? "npx" : `${pm} exec`} playwright test`;
|
|
88
|
+
if (test_scope && test_scope !== "all") {
|
|
89
|
+
testCmd = test_scope.endsWith(".spec.ts") || test_scope.endsWith(".test.ts")
|
|
90
|
+
? `${runnerCmd} ${test_scope} --reporter=line 2>&1 | tail -20`
|
|
91
|
+
: `${runnerCmd} --grep "${test_scope}" --reporter=line 2>&1 | tail -20`;
|
|
92
|
+
} else {
|
|
93
|
+
// Auto-detect from changed files
|
|
94
|
+
const changedTests = changedFiles.filter(f => /\.(spec|test)\.(ts|tsx|js)$/.test(f)).slice(0, 5);
|
|
95
|
+
if (changedTests.length > 0) {
|
|
96
|
+
testCmd = `${runnerCmd} ${changedTests.join(" ")} --reporter=line 2>&1 | tail -20`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else if (runner === "vitest" || runner === "jest") {
|
|
100
|
+
const runnerCmd = `${pm === "npx" ? "npx" : `${pm} exec`} ${runner}`;
|
|
101
|
+
if (test_scope && test_scope !== "all") {
|
|
102
|
+
testCmd = `${runnerCmd} --run ${test_scope} 2>&1 | tail -20`;
|
|
103
|
+
} else {
|
|
104
|
+
const changedTests = changedFiles.filter(f => /\.(spec|test)\.(ts|tsx|js)$/.test(f)).slice(0, 5);
|
|
105
|
+
if (changedTests.length > 0) {
|
|
106
|
+
testCmd = `${runnerCmd} --run ${changedTests.join(" ")} 2>&1 | tail -20`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else if (test_scope) {
|
|
110
|
+
// No recognized runner but scope given — try npm test
|
|
111
|
+
testCmd = `${pm} test 2>&1 | tail -20`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (testCmd) {
|
|
115
|
+
const testResult = run(testCmd, { timeout: 120000 });
|
|
116
|
+
const testPassed = /pass/i.test(testResult) && !/fail/i.test(testResult);
|
|
117
|
+
checks.push({
|
|
118
|
+
name: "Tests",
|
|
119
|
+
passed: testPassed,
|
|
120
|
+
detail: testPassed ? `✅ Tests passed\n${testResult}` : `❌ Tests failed\n${testResult}`,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: "Tests",
|
|
125
|
+
passed: true,
|
|
126
|
+
detail: `⚠️ No relevant tests identified${runner ? ` (runner: ${runner})` : ""}. Consider running full suite.`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. Build check (only if build script exists and not skipped)
|
|
132
|
+
if (!skip_build && hasBuildScript()) {
|
|
133
|
+
const buildCheck = run(`${pm === "npx" ? "npm run" : pm} build 2>&1 | tail -10`, { timeout: 60000 });
|
|
134
|
+
const buildPassed = !/\b[Ee]rror\b/.test(buildCheck) || /Successfully compiled/.test(buildCheck);
|
|
135
|
+
checks.push({
|
|
136
|
+
name: "Build",
|
|
137
|
+
passed: buildPassed,
|
|
138
|
+
detail: buildPassed ? "✅ Build succeeds" : `❌ Build failed\n${buildCheck}`,
|
|
139
|
+
});
|
|
140
|
+
} else if (!skip_build) {
|
|
141
|
+
checks.push({ name: "Build", passed: true, detail: "⚠️ No build script found — skipped" });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const allPassed = checks.every(c => c.passed);
|
|
145
|
+
sections.push(`## Verification Report\n**Task**: ${task_description}\n\n${checks.map(c => `### ${c.name}\n${c.detail}`).join("\n\n")}`);
|
|
146
|
+
|
|
147
|
+
sections.push(`## Verdict\n${allPassed
|
|
148
|
+
? "✅ **ALL CHECKS PASSED.** Safe to commit and declare done."
|
|
149
|
+
: "❌ **CHECKS FAILED.** Fix the issues above before committing."
|
|
150
|
+
}`);
|
|
151
|
+
|
|
152
|
+
if (!allPassed) {
|
|
153
|
+
sections.push(`## Do NOT:\n- Commit with failing checks\n- Say "done" without green tests\n- Push broken code to remote\n\n## DO:\n- Fix each failing check\n- Re-run \`verify_completion\` after fixes\n- Then commit`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { content: [{ type: "text" as const, text: sections.join("\n\n") }] };
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { run, getBranch, getDiffStat } from "../lib/git.js";
|
|
4
|
+
|
|
5
|
+
export function registerWhatChanged(server: McpServer): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
"what_changed",
|
|
8
|
+
`Summarize what changed recently. Useful after sub-agents finish, after a break, when context was compacted, or at the start of a new session. Returns diff summary with commit messages.`,
|
|
9
|
+
{
|
|
10
|
+
since: z.string().optional().describe("Git ref: 'HEAD~5', 'HEAD~3', etc. Default: HEAD~5"),
|
|
11
|
+
},
|
|
12
|
+
async ({ since }) => {
|
|
13
|
+
const ref = since || "HEAD~5";
|
|
14
|
+
const diffStat = getDiffStat(ref);
|
|
15
|
+
const diffFiles = run(`git diff ${ref} --name-only 2>/dev/null || git diff HEAD~3 --name-only`);
|
|
16
|
+
const log = run(`git log ${ref}..HEAD --oneline 2>/dev/null || git log -5 --oneline`);
|
|
17
|
+
const branch = getBranch();
|
|
18
|
+
|
|
19
|
+
const fileList = diffFiles.split("\n").filter(Boolean);
|
|
20
|
+
const fileCount = fileList.length;
|
|
21
|
+
const commitCount = log.split("\n").filter(Boolean).length;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: "text" as const,
|
|
26
|
+
text: `## What Changed (since ${ref})
|
|
27
|
+
Branch: ${branch}
|
|
28
|
+
**${commitCount} commits**, **${fileCount} files** changed
|
|
29
|
+
|
|
30
|
+
### Commits
|
|
31
|
+
\`\`\`
|
|
32
|
+
${log || "no commits in range"}
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
### Files Changed (${fileCount})
|
|
36
|
+
\`\`\`
|
|
37
|
+
${diffFiles || "none"}
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
### Stats
|
|
41
|
+
\`\`\`
|
|
42
|
+
${diffStat || "no changes"}
|
|
43
|
+
\`\`\``,
|
|
44
|
+
}],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
|
|
3
|
+
export type RegisterToolFn = (server: McpServer) => void;
|
|
4
|
+
|
|
5
|
+
/** Standard MCP tool return format. */
|
|
6
|
+
export type ToolResult = { content: Array<{ type: "text"; text: string }> };
|
|
7
|
+
|
|
8
|
+
export interface DocInfo {
|
|
9
|
+
content: string;
|
|
10
|
+
mtime: Date;
|
|
11
|
+
size: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Metadata-only doc info (no content read). */
|
|
15
|
+
export interface DocMeta {
|
|
16
|
+
mtime: Date;
|
|
17
|
+
size: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CorrectionEntry {
|
|
21
|
+
timestamp: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
user_said: string;
|
|
24
|
+
wrong_action: string;
|
|
25
|
+
root_cause: string;
|
|
26
|
+
category: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CheckpointLogEntry {
|
|
30
|
+
timestamp: string;
|
|
31
|
+
branch: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
next_steps: string;
|
|
34
|
+
blockers: string | null;
|
|
35
|
+
dirty_files: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Error details from a failed shell command. */
|
|
39
|
+
export interface RunError {
|
|
40
|
+
exitCode: number | null;
|
|
41
|
+
timedOut: boolean;
|
|
42
|
+
stderr: string;
|
|
43
|
+
stdout: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Per-project metadata stored alongside the LanceDB */
|
|
47
|
+
export interface ProjectMeta {
|
|
48
|
+
project_dir: string;
|
|
49
|
+
onboarded_at: string;
|
|
50
|
+
event_count: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Project registry entry in index.json */
|
|
54
|
+
export interface ProjectRegistryEntry {
|
|
55
|
+
hash: string;
|
|
56
|
+
onboarded_at: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Project registry mapping absolute paths to metadata */
|
|
60
|
+
export type ProjectRegistry = Record<string, ProjectRegistryEntry>;
|
|
61
|
+
|
|
62
|
+
/** Search scope for timeline queries */
|
|
63
|
+
export type SearchScope = "current" | "related" | "all";
|
|
64
|
+
|
|
65
|
+
/** Triage classification levels for prompt analysis */
|
|
66
|
+
export type TriageLevel = 'trivial' | 'clear' | 'ambiguous' | 'cross-service' | 'multi-step';
|
|
67
|
+
|
|
68
|
+
/** Result from triage classification with recommendations */
|
|
69
|
+
export interface TriageResult {
|
|
70
|
+
level: TriageLevel;
|
|
71
|
+
confidence: number; // 0-1
|
|
72
|
+
reasons: string[]; // why this classification
|
|
73
|
+
recommended_tools: string[]; // which tools should fire
|
|
74
|
+
cross_service_hits?: string[]; // which related projects matched
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Configuration for preflight checks and triage system */
|
|
78
|
+
export interface PreflightConfig {
|
|
79
|
+
related_projects?: Record<string, string>; // alias -> path mapping
|
|
80
|
+
triage?: {
|
|
81
|
+
always_check?: string[];
|
|
82
|
+
skip?: string[];
|
|
83
|
+
cross_service_keywords?: string[];
|
|
84
|
+
strictness?: 'relaxed' | 'standard' | 'strict';
|
|
85
|
+
};
|
|
86
|
+
correction_pattern_count?: number;
|
|
87
|
+
}
|