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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/cli.js +11 -0
  4. package/dist/cli/init.d.ts +2 -0
  5. package/dist/cli/init.js +154 -0
  6. package/dist/cli/init.js.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +122 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/config.d.ts +34 -0
  11. package/dist/lib/config.js +118 -0
  12. package/dist/lib/config.js.map +1 -0
  13. package/dist/lib/embeddings.d.ts +11 -0
  14. package/dist/lib/embeddings.js +88 -0
  15. package/dist/lib/embeddings.js.map +1 -0
  16. package/dist/lib/files.d.ts +15 -0
  17. package/dist/lib/files.js +60 -0
  18. package/dist/lib/files.js.map +1 -0
  19. package/dist/lib/git-extractor.d.ts +9 -0
  20. package/dist/lib/git-extractor.js +116 -0
  21. package/dist/lib/git-extractor.js.map +1 -0
  22. package/dist/lib/git.d.ts +29 -0
  23. package/dist/lib/git.js +86 -0
  24. package/dist/lib/git.js.map +1 -0
  25. package/dist/lib/session-parser.d.ts +45 -0
  26. package/dist/lib/session-parser.js +267 -0
  27. package/dist/lib/session-parser.js.map +1 -0
  28. package/dist/lib/state.d.ts +21 -0
  29. package/dist/lib/state.js +86 -0
  30. package/dist/lib/state.js.map +1 -0
  31. package/dist/lib/timeline-db.d.ts +67 -0
  32. package/dist/lib/timeline-db.js +380 -0
  33. package/dist/lib/timeline-db.js.map +1 -0
  34. package/dist/lib/triage.d.ts +29 -0
  35. package/dist/lib/triage.js +193 -0
  36. package/dist/lib/triage.js.map +1 -0
  37. package/dist/profiles.d.ts +3 -0
  38. package/dist/profiles.js +65 -0
  39. package/dist/profiles.js.map +1 -0
  40. package/dist/tools/audit-workspace.d.ts +2 -0
  41. package/dist/tools/audit-workspace.js +86 -0
  42. package/dist/tools/audit-workspace.js.map +1 -0
  43. package/dist/tools/checkpoint.d.ts +2 -0
  44. package/dist/tools/checkpoint.js +108 -0
  45. package/dist/tools/checkpoint.js.map +1 -0
  46. package/dist/tools/clarify-intent.d.ts +2 -0
  47. package/dist/tools/clarify-intent.js +180 -0
  48. package/dist/tools/clarify-intent.js.map +1 -0
  49. package/dist/tools/enrich-agent-task.d.ts +2 -0
  50. package/dist/tools/enrich-agent-task.js +97 -0
  51. package/dist/tools/enrich-agent-task.js.map +1 -0
  52. package/dist/tools/generate-scorecard.d.ts +2 -0
  53. package/dist/tools/generate-scorecard.js +617 -0
  54. package/dist/tools/generate-scorecard.js.map +1 -0
  55. package/dist/tools/log-correction.d.ts +2 -0
  56. package/dist/tools/log-correction.js +76 -0
  57. package/dist/tools/log-correction.js.map +1 -0
  58. package/dist/tools/onboard-project.d.ts +2 -0
  59. package/dist/tools/onboard-project.js +179 -0
  60. package/dist/tools/onboard-project.js.map +1 -0
  61. package/dist/tools/preflight-check.d.ts +2 -0
  62. package/dist/tools/preflight-check.js +229 -0
  63. package/dist/tools/preflight-check.js.map +1 -0
  64. package/dist/tools/prompt-score.d.ts +2 -0
  65. package/dist/tools/prompt-score.js +132 -0
  66. package/dist/tools/prompt-score.js.map +1 -0
  67. package/dist/tools/scan-sessions.d.ts +2 -0
  68. package/dist/tools/scan-sessions.js +182 -0
  69. package/dist/tools/scan-sessions.js.map +1 -0
  70. package/dist/tools/scope-work.d.ts +2 -0
  71. package/dist/tools/scope-work.js +214 -0
  72. package/dist/tools/scope-work.js.map +1 -0
  73. package/dist/tools/search-history.d.ts +2 -0
  74. package/dist/tools/search-history.js +130 -0
  75. package/dist/tools/search-history.js.map +1 -0
  76. package/dist/tools/sequence-tasks.d.ts +2 -0
  77. package/dist/tools/sequence-tasks.js +165 -0
  78. package/dist/tools/sequence-tasks.js.map +1 -0
  79. package/dist/tools/session-handoff.d.ts +2 -0
  80. package/dist/tools/session-handoff.js +113 -0
  81. package/dist/tools/session-handoff.js.map +1 -0
  82. package/dist/tools/session-health.d.ts +2 -0
  83. package/dist/tools/session-health.js +111 -0
  84. package/dist/tools/session-health.js.map +1 -0
  85. package/dist/tools/session-stats.d.ts +2 -0
  86. package/dist/tools/session-stats.js +112 -0
  87. package/dist/tools/session-stats.js.map +1 -0
  88. package/dist/tools/sharpen-followup.d.ts +2 -0
  89. package/dist/tools/sharpen-followup.js +192 -0
  90. package/dist/tools/sharpen-followup.js.map +1 -0
  91. package/dist/tools/timeline-view.d.ts +2 -0
  92. package/dist/tools/timeline-view.js +165 -0
  93. package/dist/tools/timeline-view.js.map +1 -0
  94. package/dist/tools/token-audit.d.ts +2 -0
  95. package/dist/tools/token-audit.js +227 -0
  96. package/dist/tools/token-audit.js.map +1 -0
  97. package/dist/tools/verify-completion.d.ts +2 -0
  98. package/dist/tools/verify-completion.js +154 -0
  99. package/dist/tools/verify-completion.js.map +1 -0
  100. package/dist/tools/what-changed.d.ts +2 -0
  101. package/dist/tools/what-changed.js +40 -0
  102. package/dist/tools/what-changed.js.map +1 -0
  103. package/dist/types.d.ts +78 -0
  104. package/dist/types.js +2 -0
  105. package/dist/types.js.map +1 -0
  106. package/package.json +52 -0
  107. package/src/cli/init.ts +133 -0
  108. package/src/index.ts +135 -0
  109. package/src/lib/config.ts +157 -0
  110. package/src/lib/embeddings.ts +118 -0
  111. package/src/lib/files.ts +59 -0
  112. package/src/lib/git-extractor.ts +137 -0
  113. package/src/lib/git.ts +89 -0
  114. package/src/lib/session-parser.ts +325 -0
  115. package/src/lib/state.ts +86 -0
  116. package/src/lib/timeline-db.ts +490 -0
  117. package/src/lib/triage.ts +255 -0
  118. package/src/profiles.ts +70 -0
  119. package/src/templates/config.yml +23 -0
  120. package/src/templates/triage.yml +27 -0
  121. package/src/tools/audit-workspace.ts +97 -0
  122. package/src/tools/checkpoint.ts +119 -0
  123. package/src/tools/clarify-intent.ts +191 -0
  124. package/src/tools/enrich-agent-task.ts +108 -0
  125. package/src/tools/generate-scorecard.ts +673 -0
  126. package/src/tools/log-correction.ts +89 -0
  127. package/src/tools/onboard-project.ts +214 -0
  128. package/src/tools/preflight-check.ts +263 -0
  129. package/src/tools/prompt-score.ts +150 -0
  130. package/src/tools/scan-sessions.ts +209 -0
  131. package/src/tools/scope-work.ts +238 -0
  132. package/src/tools/search-history.ts +145 -0
  133. package/src/tools/sequence-tasks.ts +182 -0
  134. package/src/tools/session-handoff.ts +125 -0
  135. package/src/tools/session-health.ts +107 -0
  136. package/src/tools/session-stats.ts +134 -0
  137. package/src/tools/sharpen-followup.ts +200 -0
  138. package/src/tools/timeline-view.ts +181 -0
  139. package/src/tools/token-audit.ts +259 -0
  140. package/src/tools/verify-completion.ts +159 -0
  141. package/src/tools/what-changed.ts +48 -0
  142. 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
+ }
@@ -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
+ }