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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart triage classification system for preflight MCP server.
|
|
3
|
+
* Classifies incoming prompts into categories and returns recommended action.
|
|
4
|
+
*
|
|
5
|
+
* Pure function, no side effects, no external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Example classifications:
|
|
8
|
+
* "commit" → trivial
|
|
9
|
+
* "fix the null check in src/auth/jwt.ts line 42" → clear
|
|
10
|
+
* "fix the auth bug" → ambiguous
|
|
11
|
+
* "add tiered rewards" (with rewards-api related) → cross-service
|
|
12
|
+
* "refactor auth to OAuth2 and update all API consumers" → multi-step
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export type TriageLevel =
|
|
18
|
+
| 'trivial'
|
|
19
|
+
| 'clear'
|
|
20
|
+
| 'ambiguous'
|
|
21
|
+
| 'cross-service'
|
|
22
|
+
| 'multi-step';
|
|
23
|
+
|
|
24
|
+
export interface TriageResult {
|
|
25
|
+
level: TriageLevel;
|
|
26
|
+
confidence: number; // 0–1
|
|
27
|
+
reasons: string[];
|
|
28
|
+
recommended_tools: string[];
|
|
29
|
+
cross_service_hits?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TriageConfig {
|
|
33
|
+
alwaysCheck?: string[];
|
|
34
|
+
skip?: string[];
|
|
35
|
+
crossServiceKeywords?: string[];
|
|
36
|
+
strictness?: string; // 'relaxed' | 'standard' | 'strict'
|
|
37
|
+
relatedAliases?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const TRIVIAL_COMMANDS = [
|
|
43
|
+
'commit', 'format', 'lint', 'run tests', 'push', 'pull',
|
|
44
|
+
'status', 'build', 'test', 'deploy', 'start', 'stop', 'restart',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const VAGUE_PRONOUNS = /\b(it|them|the thing|those|these)\b/i;
|
|
48
|
+
|
|
49
|
+
const VAGUE_VERBS = ['fix', 'update', 'change'];
|
|
50
|
+
|
|
51
|
+
const CROSS_SERVICE_TERMS = [
|
|
52
|
+
'schema', 'contract', 'interface', 'event',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const FILE_PATH_RE = /(?:^|[\s,:(])([.\w\-/\\]+\.\w{1,6})\b/;
|
|
56
|
+
const LINE_NUMBER_RE = /\bline\s+\d+|:\d+\b/;
|
|
57
|
+
|
|
58
|
+
const MULTI_STEP_SEQUENTIAL = /\b(then|after that|first\b.*\bthen|finally)\b/i;
|
|
59
|
+
|
|
60
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function lower(s: string): string {
|
|
63
|
+
return s.toLowerCase().trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isTrivialCommand(prompt: string): boolean {
|
|
67
|
+
const p = lower(prompt);
|
|
68
|
+
return TRIVIAL_COMMANDS.some(
|
|
69
|
+
(cmd) => p === cmd || p.startsWith(cmd + ' '),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasFileRefs(prompt: string): boolean {
|
|
74
|
+
return FILE_PATH_RE.test(prompt);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasLineNumbers(prompt: string): boolean {
|
|
78
|
+
return LINE_NUMBER_RE.test(prompt);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasVaguePronouns(prompt: string): boolean {
|
|
82
|
+
return VAGUE_PRONOUNS.test(prompt);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Returns true when a vague verb appears without a concrete target after it. */
|
|
86
|
+
function hasVagueVerbs(prompt: string): boolean {
|
|
87
|
+
const words = lower(prompt).split(/\s+/);
|
|
88
|
+
return VAGUE_VERBS.some((verb) => {
|
|
89
|
+
const idx = words.indexOf(verb);
|
|
90
|
+
if (idx === -1) return false;
|
|
91
|
+
// Look at the next few words for something concrete
|
|
92
|
+
const tail = words.slice(idx + 1, idx + 4);
|
|
93
|
+
const hasTarget = tail.some(
|
|
94
|
+
(w) => /\.\w+/.test(w) || w.length > 6 || /[A-Z]/.test(w),
|
|
95
|
+
);
|
|
96
|
+
return !hasTarget;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function detectCrossService(
|
|
101
|
+
prompt: string,
|
|
102
|
+
config: TriageConfig,
|
|
103
|
+
): string[] {
|
|
104
|
+
const p = lower(prompt);
|
|
105
|
+
const hits: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const kw of config.crossServiceKeywords ?? []) {
|
|
108
|
+
if (p.includes(lower(kw))) hits.push(`keyword: ${kw}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const alias of config.relatedAliases ?? []) {
|
|
112
|
+
if (p.includes(lower(alias))) hits.push(`project: ${alias}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const term of CROSS_SERVICE_TERMS) {
|
|
116
|
+
if (p.includes(term)) hits.push(`term: ${term}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return hits;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isMultiStep(prompt: string): boolean {
|
|
123
|
+
const p = lower(prompt);
|
|
124
|
+
|
|
125
|
+
// "and" connecting distinct clauses (heuristic: split and check length)
|
|
126
|
+
if (p.includes(' and ') && p.split(' and ').length > 1) {
|
|
127
|
+
const parts = p.split(' and ');
|
|
128
|
+
// Both sides should be non-trivial (> 2 words each)
|
|
129
|
+
if (parts.every((part) => part.trim().split(/\s+/).length >= 2)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sequential language
|
|
135
|
+
if (MULTI_STEP_SEQUENTIAL.test(prompt)) return true;
|
|
136
|
+
|
|
137
|
+
// Numbered / bulleted lists
|
|
138
|
+
if (/\n\s*[1-9][.)]\s/.test(prompt) || /\n\s*[-*]\s/.test(prompt)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Multiple file refs in different directories
|
|
143
|
+
const files = prompt.match(/[\w\-./\\]+\.\w{1,6}/g) ?? [];
|
|
144
|
+
if (files.length > 1) {
|
|
145
|
+
const dirs = new Set(files.map((f) => f.split('/')[0]));
|
|
146
|
+
if (dirs.size > 1) return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export function triagePrompt(
|
|
155
|
+
prompt: string,
|
|
156
|
+
config?: TriageConfig,
|
|
157
|
+
): TriageResult {
|
|
158
|
+
const cfg: TriageConfig = config ?? {};
|
|
159
|
+
const len = prompt.trim().length;
|
|
160
|
+
const reasons: string[] = [];
|
|
161
|
+
const tools: string[] = [];
|
|
162
|
+
|
|
163
|
+
// 1. Skip keywords → trivial immediately
|
|
164
|
+
for (const kw of cfg.skip ?? []) {
|
|
165
|
+
if (lower(prompt).includes(lower(kw))) {
|
|
166
|
+
return {
|
|
167
|
+
level: 'trivial',
|
|
168
|
+
confidence: 0.95,
|
|
169
|
+
reasons: [`matches skip keyword: "${kw}"`],
|
|
170
|
+
recommended_tools: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Multi-step (check early — highest complexity)
|
|
176
|
+
if (isMultiStep(prompt)) {
|
|
177
|
+
reasons.push('contains multi-step indicators');
|
|
178
|
+
tools.push('clarify-intent', 'scope-work', 'sequence-tasks');
|
|
179
|
+
return { level: 'multi-step', confidence: 0.85, reasons, recommended_tools: tools };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. Cross-service
|
|
183
|
+
const csHits = detectCrossService(prompt, cfg);
|
|
184
|
+
if (csHits.length > 0) {
|
|
185
|
+
reasons.push(`cross-service indicators: ${csHits.join(', ')}`);
|
|
186
|
+
tools.push('clarify-intent', 'scope-work', 'search-related-projects');
|
|
187
|
+
return {
|
|
188
|
+
level: 'cross-service',
|
|
189
|
+
confidence: 0.8,
|
|
190
|
+
reasons,
|
|
191
|
+
recommended_tools: tools,
|
|
192
|
+
cross_service_hits: csHits,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. always_check keywords → at least ambiguous
|
|
197
|
+
for (const kw of cfg.alwaysCheck ?? []) {
|
|
198
|
+
if (lower(prompt).includes(lower(kw))) {
|
|
199
|
+
reasons.push(`matches always_check keyword: "${kw}"`);
|
|
200
|
+
tools.push('clarify-intent', 'scope-work');
|
|
201
|
+
return { level: 'ambiguous', confidence: 0.8, reasons, recommended_tools: tools };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 5. Trivial: short common commands
|
|
206
|
+
if (len < 20 && isTrivialCommand(prompt)) {
|
|
207
|
+
return {
|
|
208
|
+
level: 'trivial',
|
|
209
|
+
confidence: 0.9,
|
|
210
|
+
reasons: ['short common command'],
|
|
211
|
+
recommended_tools: [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 6. Ambiguous signals
|
|
216
|
+
const ambiguousReasons: string[] = [];
|
|
217
|
+
if (len < 50 && !hasFileRefs(prompt)) {
|
|
218
|
+
ambiguousReasons.push('short prompt without file references');
|
|
219
|
+
}
|
|
220
|
+
if (hasVaguePronouns(prompt)) {
|
|
221
|
+
ambiguousReasons.push('contains vague pronouns');
|
|
222
|
+
}
|
|
223
|
+
if (hasVagueVerbs(prompt)) {
|
|
224
|
+
ambiguousReasons.push('contains vague verbs without specific targets');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (ambiguousReasons.length > 0) {
|
|
228
|
+
return {
|
|
229
|
+
level: 'ambiguous',
|
|
230
|
+
confidence: 0.7,
|
|
231
|
+
reasons: ambiguousReasons,
|
|
232
|
+
recommended_tools: ['clarify-intent', 'scope-work'],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 7. Clear — specific, well-formed prompt
|
|
237
|
+
if (hasFileRefs(prompt)) reasons.push('references specific file paths');
|
|
238
|
+
if (hasLineNumbers(prompt)) reasons.push('references specific line numbers');
|
|
239
|
+
if (len > 50) reasons.push('detailed prompt with concrete nouns');
|
|
240
|
+
if (reasons.length === 0) reasons.push('well-formed prompt with clear intent');
|
|
241
|
+
|
|
242
|
+
const clearTools: string[] = hasFileRefs(prompt) ? ['verify-files-exist'] : [];
|
|
243
|
+
|
|
244
|
+
// Strictness adjustment
|
|
245
|
+
if (cfg.strictness === 'strict' && clearTools.length === 0) {
|
|
246
|
+
clearTools.push('verify-files-exist');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
level: 'clear',
|
|
251
|
+
confidence: cfg.strictness === 'strict' ? 0.8 : 0.85,
|
|
252
|
+
reasons,
|
|
253
|
+
recommended_tools: clearTools,
|
|
254
|
+
};
|
|
255
|
+
}
|
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Profile system — controls which tools are registered
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// minimal: No vectors. Pure JSONL parsing + git state. ~5MB install.
|
|
5
|
+
// standard: Local embeddings (Xenova) + LanceDB. Auto-downloads model on
|
|
6
|
+
// first use. Zero config. ~200MB after model download. DEFAULT.
|
|
7
|
+
// full: Everything in standard + OpenAI option for higher quality embeddings.
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
import { getConfig, type Profile } from "./lib/config.js";
|
|
11
|
+
|
|
12
|
+
const MINIMAL_TOOLS = new Set([
|
|
13
|
+
"preflight_check",
|
|
14
|
+
"clarify_intent",
|
|
15
|
+
"check_session_health",
|
|
16
|
+
"session_stats",
|
|
17
|
+
"prompt_score",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// Standard IS the default — includes embeddings + timeline.
|
|
21
|
+
// LanceDB is embedded (no server), Xenova downloads model silently on first use.
|
|
22
|
+
const STANDARD_TOOLS = new Set([
|
|
23
|
+
// Main entry point
|
|
24
|
+
"preflight_check",
|
|
25
|
+
// All 14 prompt discipline tools
|
|
26
|
+
"scope_work",
|
|
27
|
+
"clarify_intent",
|
|
28
|
+
"enrich_agent_task",
|
|
29
|
+
"sharpen_followup",
|
|
30
|
+
"token_audit",
|
|
31
|
+
"sequence_tasks",
|
|
32
|
+
"checkpoint",
|
|
33
|
+
"check_session_health",
|
|
34
|
+
"log_correction",
|
|
35
|
+
"audit_workspace",
|
|
36
|
+
"session_handoff",
|
|
37
|
+
"what_changed",
|
|
38
|
+
"verify_completion",
|
|
39
|
+
// Lightweight tools
|
|
40
|
+
"session_stats",
|
|
41
|
+
"prompt_score",
|
|
42
|
+
"generate_scorecard",
|
|
43
|
+
// Timeline tools — local embeddings, zero config
|
|
44
|
+
"onboard_project",
|
|
45
|
+
"search_history",
|
|
46
|
+
"timeline_view",
|
|
47
|
+
"scan_sessions",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Full = standard + OpenAI embedding option (needs API key)
|
|
51
|
+
// Identical tool set — the difference is config, not features.
|
|
52
|
+
const FULL_TOOLS = new Set([
|
|
53
|
+
...STANDARD_TOOLS,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export function getProfile(): Profile {
|
|
57
|
+
return getConfig().profile;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isToolEnabled(toolName: string): boolean {
|
|
61
|
+
const profile = getProfile();
|
|
62
|
+
switch (profile) {
|
|
63
|
+
case "minimal":
|
|
64
|
+
return MINIMAL_TOOLS.has(toolName);
|
|
65
|
+
case "standard":
|
|
66
|
+
return STANDARD_TOOLS.has(toolName);
|
|
67
|
+
case "full":
|
|
68
|
+
return FULL_TOOLS.has(toolName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Preflight Configuration
|
|
2
|
+
# =============================================================================
|
|
3
|
+
|
|
4
|
+
# Profile: minimal | standard | full
|
|
5
|
+
profile: standard
|
|
6
|
+
|
|
7
|
+
# Related projects for cross-service context
|
|
8
|
+
related_projects:
|
|
9
|
+
# - path: /Users/jack/Developer/auth-service
|
|
10
|
+
# alias: auth-service
|
|
11
|
+
# - path: /Users/jack/Developer/notifications-service
|
|
12
|
+
# alias: notifications-service
|
|
13
|
+
|
|
14
|
+
# Thresholds
|
|
15
|
+
thresholds:
|
|
16
|
+
session_stale_minutes: 30
|
|
17
|
+
max_tool_calls_before_checkpoint: 100
|
|
18
|
+
correction_pattern_threshold: 3 # corrections before pattern triggers
|
|
19
|
+
|
|
20
|
+
# Embedding provider: local | openai
|
|
21
|
+
embeddings:
|
|
22
|
+
provider: local # or openai
|
|
23
|
+
# openai_api_key: from env var OPENAI_API_KEY
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Preflight Triage Rules
|
|
2
|
+
# =============================================================================
|
|
3
|
+
|
|
4
|
+
# Custom triage classification rules
|
|
5
|
+
rules:
|
|
6
|
+
# Always full-check prompts mentioning these keywords
|
|
7
|
+
always_check:
|
|
8
|
+
- rewards
|
|
9
|
+
- permissions
|
|
10
|
+
- migration
|
|
11
|
+
- schema
|
|
12
|
+
|
|
13
|
+
# Never check these (pass-through)
|
|
14
|
+
skip:
|
|
15
|
+
- commit
|
|
16
|
+
- format
|
|
17
|
+
- lint
|
|
18
|
+
|
|
19
|
+
# Cross-service keywords (trigger related project search)
|
|
20
|
+
cross_service_keywords:
|
|
21
|
+
- auth
|
|
22
|
+
- notification
|
|
23
|
+
- event
|
|
24
|
+
- webhook
|
|
25
|
+
|
|
26
|
+
# Default strictness: relaxed | standard | strict
|
|
27
|
+
strictness: standard
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { run } from "../lib/git.js";
|
|
3
|
+
import { readIfExists, findWorkspaceDocs } from "../lib/files.js";
|
|
4
|
+
|
|
5
|
+
/** Extract top-level work areas from file paths generically */
|
|
6
|
+
function detectWorkAreas(files: string[]): Set<string> {
|
|
7
|
+
const areas = new Set<string>();
|
|
8
|
+
for (const f of files) {
|
|
9
|
+
if (!f || f.startsWith(".")) continue;
|
|
10
|
+
|
|
11
|
+
// Use first 1-2 path segments as the area
|
|
12
|
+
const parts = f.split("/");
|
|
13
|
+
if (parts.length >= 2) {
|
|
14
|
+
// For test-like directories, just use "tests"
|
|
15
|
+
if (/^(tests?|__tests__|spec)$/i.test(parts[0])) {
|
|
16
|
+
areas.add("tests");
|
|
17
|
+
} else if (parts.length >= 3) {
|
|
18
|
+
// e.g. app/api/foo → "app/api", src/components/Bar → "src/components"
|
|
19
|
+
areas.add(`${parts[0]}/${parts[1]}`);
|
|
20
|
+
} else {
|
|
21
|
+
areas.add(parts[0]);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
// Root-level files: group by extension category
|
|
25
|
+
if (/\.(json|ya?ml|toml|lock)$/.test(f)) areas.add("config");
|
|
26
|
+
else areas.add("root");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return areas;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerAuditWorkspace(server: McpServer): void {
|
|
33
|
+
server.tool(
|
|
34
|
+
"audit_workspace",
|
|
35
|
+
`Audit workspace documentation freshness vs actual project state. Compares .claude/ workspace docs against recent git commits to find stale or missing documentation. Call after completing a batch of work or at session end.`,
|
|
36
|
+
{},
|
|
37
|
+
async () => {
|
|
38
|
+
const docs = findWorkspaceDocs();
|
|
39
|
+
const recentFiles = run("git diff --name-only HEAD~10 2>/dev/null || echo ''").split("\n").filter(Boolean);
|
|
40
|
+
const sections: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Doc freshness
|
|
43
|
+
const docStatus: { name: string; ageHours: number; stale: boolean; size: number }[] = [];
|
|
44
|
+
const currentTime = Date.now();
|
|
45
|
+
for (const [name, info] of Object.entries(docs)) {
|
|
46
|
+
const ageHours = Math.round((currentTime - info.mtime.getTime()) / 3600000);
|
|
47
|
+
const stale = ageHours > 4;
|
|
48
|
+
docStatus.push({ name, ageHours, stale, size: info.size });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Use bullet list format (renders everywhere)
|
|
52
|
+
sections.push(`## Workspace Doc Freshness\n${docStatus.length > 0
|
|
53
|
+
? docStatus.map(d =>
|
|
54
|
+
`- .claude/${d.name} — ${d.ageHours}h old ${d.stale ? "🔴 STALE" : "🟢 Fresh"}`
|
|
55
|
+
).join("\n")
|
|
56
|
+
: "No workspace docs found."
|
|
57
|
+
}`);
|
|
58
|
+
|
|
59
|
+
// Detect work areas generically from git diffs
|
|
60
|
+
const workAreas = detectWorkAreas(recentFiles);
|
|
61
|
+
|
|
62
|
+
// Check which areas lack docs
|
|
63
|
+
const docNames = Object.keys(docs).join(" ").toLowerCase();
|
|
64
|
+
const undocumented = [...workAreas].filter(area => {
|
|
65
|
+
const areaLower = area.toLowerCase();
|
|
66
|
+
// Check if any doc name contains the area name (or key parts)
|
|
67
|
+
const keywords = areaLower.split("/").filter(Boolean);
|
|
68
|
+
return !keywords.some(kw => docNames.includes(kw));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (undocumented.length > 0) {
|
|
72
|
+
sections.push(`## Undocumented Work Areas\nRecent commits touched these areas but no workspace docs cover them:\n${undocumented.map(a => `- ❌ **${a}**`).join("\n")}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for gap trackers or similar tracking docs
|
|
76
|
+
const trackingDocs = Object.entries(docs).filter(([n]) => /gap|track|progress/i.test(n));
|
|
77
|
+
if (trackingDocs.length > 0) {
|
|
78
|
+
const testFilesCount = parseInt(run("find tests -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | wc -l").trim()) || 0;
|
|
79
|
+
sections.push(`## Tracking Docs\n${trackingDocs.map(([n]) => {
|
|
80
|
+
const age = docStatus.find(d => d.name === n)?.ageHours ?? "?";
|
|
81
|
+
return `- .claude/${n} — last updated ${age}h ago`;
|
|
82
|
+
}).join("\n")}\nTest files on disk: ${testFilesCount}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Summary
|
|
86
|
+
const staleCount = docStatus.filter(d => d.stale).length;
|
|
87
|
+
const recs: string[] = [];
|
|
88
|
+
if (staleCount > 0) recs.push(`⚠️ ${staleCount} docs are stale. Update them before ending this session.`);
|
|
89
|
+
else recs.push("✅ Workspace docs are fresh.");
|
|
90
|
+
if (undocumented.length > 0) recs.push(`⚠️ ${undocumented.length} work areas have no docs. Consider creating docs for: ${undocumented.join(", ")}`);
|
|
91
|
+
|
|
92
|
+
sections.push(`## Recommendation\n${recs.join("\n")}`);
|
|
93
|
+
|
|
94
|
+
return { content: [{ type: "text" as const, text: sections.join("\n\n") }] };
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { run, getBranch, getStatus, getLastCommit, getStagedFiles } from "../lib/git.js";
|
|
6
|
+
import { PROJECT_DIR } from "../lib/files.js";
|
|
7
|
+
import { appendLog, now } from "../lib/state.js";
|
|
8
|
+
|
|
9
|
+
export function registerCheckpoint(server: McpServer): void {
|
|
10
|
+
server.tool(
|
|
11
|
+
"checkpoint",
|
|
12
|
+
`Save a session checkpoint before context compaction hits. Commits current work, writes session state to workspace docs, and creates a resumption note. Call this proactively when session is getting long, or when the session-health hook warns about turn count. This is your "save game" before compaction wipes context.`,
|
|
13
|
+
{
|
|
14
|
+
summary: z.string().describe("What was accomplished so far in this session"),
|
|
15
|
+
next_steps: z.string().describe("What still needs to be done"),
|
|
16
|
+
current_blockers: z.string().optional().describe("Any issues or blockers encountered"),
|
|
17
|
+
commit_mode: z.enum(["staged", "tracked", "all"]).optional().describe("What to commit: 'staged' (only staged files), 'tracked' (modified tracked files), 'all' (git add -A). Default: 'tracked'"),
|
|
18
|
+
},
|
|
19
|
+
async ({ summary, next_steps, current_blockers, commit_mode }) => {
|
|
20
|
+
const mode = commit_mode || "tracked";
|
|
21
|
+
const branch = getBranch();
|
|
22
|
+
const dirty = getStatus();
|
|
23
|
+
const lastCommit = getLastCommit();
|
|
24
|
+
const timestamp = now();
|
|
25
|
+
|
|
26
|
+
// Write checkpoint file
|
|
27
|
+
const checkpointDir = join(PROJECT_DIR, ".claude");
|
|
28
|
+
if (!existsSync(checkpointDir)) mkdirSync(checkpointDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const checkpointFile = join(checkpointDir, "last-checkpoint.md");
|
|
31
|
+
const checkpointContent = `# Session Checkpoint
|
|
32
|
+
**Time**: ${timestamp}
|
|
33
|
+
**Branch**: ${branch}
|
|
34
|
+
**Last Commit**: ${lastCommit}
|
|
35
|
+
|
|
36
|
+
## Accomplished
|
|
37
|
+
${summary}
|
|
38
|
+
|
|
39
|
+
## Next Steps
|
|
40
|
+
${next_steps}
|
|
41
|
+
|
|
42
|
+
${current_blockers ? `## Blockers\n${current_blockers}\n` : ""}
|
|
43
|
+
## Uncommitted Work (at checkpoint time)
|
|
44
|
+
\`\`\`
|
|
45
|
+
${dirty || "clean"}
|
|
46
|
+
\`\`\`
|
|
47
|
+
`;
|
|
48
|
+
writeFileSync(checkpointFile, checkpointContent);
|
|
49
|
+
|
|
50
|
+
appendLog("checkpoint-log.jsonl", {
|
|
51
|
+
timestamp,
|
|
52
|
+
branch,
|
|
53
|
+
summary,
|
|
54
|
+
next_steps,
|
|
55
|
+
blockers: current_blockers || null,
|
|
56
|
+
dirty_files: dirty ? dirty.split("\n").filter(Boolean).length : 0,
|
|
57
|
+
commit_mode: mode,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Commit based on mode
|
|
61
|
+
let commitResult = "no uncommitted changes";
|
|
62
|
+
if (dirty) {
|
|
63
|
+
const shortSummary = summary.split("\n")[0].slice(0, 72);
|
|
64
|
+
const commitMsg = `checkpoint: ${shortSummary}`;
|
|
65
|
+
|
|
66
|
+
let addCmd: string = "git add -u";
|
|
67
|
+
switch (mode) {
|
|
68
|
+
case "staged": {
|
|
69
|
+
const staged = getStagedFiles();
|
|
70
|
+
if (!staged) {
|
|
71
|
+
commitResult = "nothing staged — skipped commit (use 'tracked' or 'all' mode, or stage files first)";
|
|
72
|
+
}
|
|
73
|
+
addCmd = "true"; // noop, already staged
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "all":
|
|
77
|
+
addCmd = "git add -A";
|
|
78
|
+
break;
|
|
79
|
+
case "tracked":
|
|
80
|
+
default:
|
|
81
|
+
addCmd = "git add -u";
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (commitResult === "no uncommitted changes") {
|
|
86
|
+
// Stage the checkpoint file too
|
|
87
|
+
run(`git add "${checkpointFile}"`);
|
|
88
|
+
const result = run(`${addCmd} && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>&1`);
|
|
89
|
+
if (result.includes("commit failed") || result.includes("nothing to commit")) {
|
|
90
|
+
// Rollback: unstage if commit failed
|
|
91
|
+
run("git reset HEAD 2>/dev/null");
|
|
92
|
+
commitResult = `commit failed: ${result}`;
|
|
93
|
+
} else {
|
|
94
|
+
commitResult = result;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text" as const,
|
|
102
|
+
text: `## Checkpoint Saved ✅
|
|
103
|
+
**File**: .claude/last-checkpoint.md
|
|
104
|
+
**Branch**: ${branch}
|
|
105
|
+
**Commit mode**: ${mode}
|
|
106
|
+
**Commit**: ${commitResult}
|
|
107
|
+
|
|
108
|
+
### What's saved:
|
|
109
|
+
- Summary of work done
|
|
110
|
+
- Next steps for continuation
|
|
111
|
+
${current_blockers ? "- Current blockers\n" : ""}- Working tree state at checkpoint time
|
|
112
|
+
|
|
113
|
+
### To resume after compaction:
|
|
114
|
+
Tell the next session/continuation: "Read .claude/last-checkpoint.md for where I left off"`,
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
}
|