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,191 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { run, getBranch, getStatus, getRecentCommits, getDiffFiles, getStagedFiles } from "../lib/git.js";
4
+ import { findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js";
5
+ import { searchSemantic } from "../lib/timeline-db.js";
6
+ import { getRelatedProjects } from "../lib/config.js";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join, basename } from "path";
9
+
10
+ /** Parse test failures from common report formats without fragile shell pipelines */
11
+ function getTestFailures(): string {
12
+ // Try playwright JSON report
13
+ const reportPath = join(PROJECT_DIR, "playwright-report", "results.json");
14
+ if (existsSync(reportPath)) {
15
+ try {
16
+ const data = JSON.parse(readFileSync(reportPath, "utf-8"));
17
+ const failures: string[] = [];
18
+ const walk = (suites: any[]) => {
19
+ for (const suite of suites || []) {
20
+ for (const spec of suite.specs || []) {
21
+ if (spec.ok === false) failures.push(spec.title);
22
+ }
23
+ walk(suite.suites);
24
+ }
25
+ };
26
+ walk(data.suites);
27
+ return failures.length ? failures.join("\n") : "all passing";
28
+ } catch {
29
+ return "report exists but could not parse";
30
+ }
31
+ }
32
+
33
+ // Try jest/vitest JSON output
34
+ for (const p of ["test-results.json", "jest-results.json"]) {
35
+ const fp = join(PROJECT_DIR, p);
36
+ if (existsSync(fp)) {
37
+ try {
38
+ const data = JSON.parse(readFileSync(fp, "utf-8"));
39
+ const failed = data.testResults
40
+ ?.filter((t: any) => t.status === "failed")
41
+ ?.map((t: any) => t.name) || [];
42
+ return failed.length ? failed.join("\n") : "all passing";
43
+ } catch { continue; }
44
+ }
45
+ }
46
+
47
+ return "no test report found";
48
+ }
49
+
50
+ /** Extract intent signals using weighted pattern matching */
51
+ function extractSignals(msg: string, context: { hasTypeErrors: boolean; hasTestFailures: boolean; hasDirtyFiles: boolean }): string[] {
52
+ const signals: string[] = [];
53
+ const lower = msg.toLowerCase();
54
+
55
+ const patterns: [RegExp, string, number][] = [
56
+ [/\b(fix|repair|broken|failing|error|bug|crash|issue)\b/, "FIX", 2],
57
+ [/\b(test|spec|suite|playwright|jest|vitest|e2e)\b/, "TESTS", 2],
58
+ [/\b(commit|push|pr|merge|rebase|cherry.?pick)\b/, "GIT", 2],
59
+ [/\b(add|create|new|build|implement|feature)\b/, "CREATE", 1],
60
+ [/\b(remove|delete|clean|strip|drop|deprecate)\b/, "REMOVE", 1],
61
+ [/\b(check|verify|confirm|status|review|audit)\b/, "VERIFY", 1],
62
+ [/\b(refactor|rename|move|reorganize|extract)\b/, "REFACTOR", 1],
63
+ [/\b(deploy|release|ship|publish)\b/, "DEPLOY", 1],
64
+ [/\b(everything|all|entire|whole)\b/, "⚠️ UNBOUNDED", 3],
65
+ ];
66
+
67
+ const matched: { label: string; weight: number; hint: string }[] = [];
68
+ for (const [re, label, weight] of patterns) {
69
+ if (re.test(lower)) matched.push({ label, weight, hint: "" });
70
+ }
71
+
72
+ // Add contextual hints
73
+ if (matched.some(m => m.label === "FIX")) {
74
+ if (context.hasTypeErrors) signals.push("FIX: Type errors detected — likely the target.");
75
+ if (context.hasTestFailures) signals.push("FIX: Test failures detected — check test output.");
76
+ if (!context.hasTypeErrors && !context.hasTestFailures) signals.push("FIX: No obvious errors — ask what's broken.");
77
+ }
78
+ if (matched.some(m => m.label === "TESTS")) signals.push("TESTS: Check failing tests and test files below.");
79
+ if (matched.some(m => m.label === "GIT")) signals.push("GIT: Check dirty files and branch state.");
80
+ if (matched.some(m => m.label === "CREATE")) signals.push("CREATE: Check workspace priorities for planned work.");
81
+ if (matched.some(m => m.label === "REMOVE")) signals.push("REMOVE: Clarify what 'them/it' refers to before deleting.");
82
+ if (matched.some(m => m.label === "VERIFY")) signals.push("VERIFY: Use git/test state to answer.");
83
+ if (matched.some(m => m.label === "REFACTOR")) signals.push("REFACTOR: Identify scope boundaries before starting.");
84
+ if (matched.some(m => m.label === "DEPLOY")) signals.push("DEPLOY: Verify all checks pass first.");
85
+ if (matched.some(m => m.label === "⚠️ UNBOUNDED")) signals.push("⚠️ UNBOUNDED: Narrow down using workspace priorities.");
86
+
87
+ if (!signals.length) signals.push("UNCLEAR: Ask ONE clarifying question before proceeding.");
88
+
89
+ return signals;
90
+ }
91
+
92
+ /** Search for relevant cross-project context */
93
+ async function searchCrossProjectContext(userMessage: string): Promise<string[]> {
94
+ const relatedProjects = getRelatedProjects();
95
+ if (relatedProjects.length === 0) return [];
96
+
97
+ // Generate search queries for schemas, types, contracts, APIs
98
+ const queries = [
99
+ `${userMessage} type interface schema`,
100
+ `${userMessage} API endpoint contract`,
101
+ `${userMessage} enum constant definition`,
102
+ `${userMessage} class function method`,
103
+ ];
104
+
105
+ const contextItems: string[] = [];
106
+
107
+ for (const query of queries) {
108
+ try {
109
+ const results = await searchSemantic(query, {
110
+ project_dirs: relatedProjects,
111
+ type: "commit", // Focus on code changes
112
+ limit: 3,
113
+ });
114
+
115
+ for (const result of results) {
116
+ const projectName = basename(result.project);
117
+ const content = result.content.slice(0, 200);
118
+ const sourceInfo = result.source_file ? ` at ${result.source_file}` : "";
119
+ contextItems.push(`**From ${projectName}:** ${content}${sourceInfo}`);
120
+ }
121
+ } catch {
122
+ // Skip if search fails
123
+ }
124
+ }
125
+
126
+ return contextItems.slice(0, 5); // Limit to top 5 context items
127
+ }
128
+
129
+ export function registerClarifyIntent(server: McpServer): void {
130
+ server.tool(
131
+ "clarify_intent",
132
+ `Clarify a vague user instruction by gathering project context. Call BEFORE executing when the user's prompt is missing specific files, actions, scope, or done conditions. Returns git state, test failures, recent changes, and workspace priorities.`,
133
+ {
134
+ user_message: z.string().describe("The user's raw message/instruction to clarify"),
135
+ suspected_area: z.string().optional().describe("Best guess area: 'tests', 'git', 'ui', 'api', 'schema'"),
136
+ },
137
+ async ({ user_message, suspected_area }) => {
138
+ const sections: string[] = [];
139
+ const branch = getBranch();
140
+ const status = getStatus();
141
+ const recentCommits = getRecentCommits(5);
142
+ const recentFiles = getDiffFiles("HEAD~3");
143
+ const staged = getStagedFiles();
144
+ const dirtyCount = status ? status.split("\n").filter(Boolean).length : 0;
145
+
146
+ sections.push(`## Git State\nBranch: ${branch}\nDirty files: ${dirtyCount}\n${status ? `\`\`\`\n${status}\n\`\`\`` : "Working tree clean"}\nStaged: ${staged || "nothing"}\n\nRecent commits:\n\`\`\`\n${recentCommits}\n\`\`\`\n\nRecently changed files:\n\`\`\`\n${recentFiles}\n\`\`\``);
147
+
148
+ // Gather test/type state when relevant
149
+ const area = (suspected_area || "").toLowerCase();
150
+ let hasTypeErrors = false;
151
+ let hasTestFailures = false;
152
+
153
+ if (!area || area.includes("test") || area.includes("fix") || area.includes("ui") || area.includes("api")) {
154
+ const typeErrors = run("pnpm tsc --noEmit 2>&1 | grep -c 'error TS' || echo '0'");
155
+ hasTypeErrors = parseInt(typeErrors, 10) > 0;
156
+
157
+ const testFiles = run("find tests -name '*.spec.ts' -maxdepth 4 2>/dev/null | head -20");
158
+ const failingTests = getTestFailures();
159
+ hasTestFailures = failingTests !== "all passing" && failingTests !== "no test report found";
160
+
161
+ sections.push(`## Test State\nType errors: ${typeErrors}\nFailing tests: ${failingTests}\nTest files:\n\`\`\`\n${testFiles || "none found"}\n\`\`\``);
162
+ }
163
+
164
+ // Workspace priorities
165
+ const workspaceDocs = findWorkspaceDocs();
166
+ const priorityDocs = Object.entries(workspaceDocs)
167
+ .filter(([n]) => /gap|roadmap|current|todo|changelog/i.test(n))
168
+ .slice(0, 3);
169
+ if (priorityDocs.length > 0) {
170
+ sections.push(`## Workspace Priorities\n${priorityDocs.map(([n, d]) => `### .claude/${n}\n\`\`\`\n${d.content}\n\`\`\``).join("\n\n")}`);
171
+ }
172
+
173
+ const signals = extractSignals(user_message, {
174
+ hasTypeErrors,
175
+ hasTestFailures,
176
+ hasDirtyFiles: dirtyCount > 0,
177
+ });
178
+
179
+ // Search for cross-project context
180
+ const crossProjectContext = await searchCrossProjectContext(user_message);
181
+ if (crossProjectContext.length > 0) {
182
+ sections.push(`## Related Project Context\n${crossProjectContext.map(c => `- ${c}`).join("\n")}`);
183
+ }
184
+
185
+ sections.push(`## Intent Signals\n${signals.map(s => `- ${s}`).join("\n")}`);
186
+ sections.push(`## Recommendation\n1. **Proceed with specifics** — state what you'll do and why\n2. **Ask ONE question** — if context doesn't disambiguate`);
187
+
188
+ return { content: [{ type: "text" as const, text: sections.join("\n\n") }] };
189
+ }
190
+ );
191
+ }
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { run, getDiffFiles } from "../lib/git.js";
4
+ import { PROJECT_DIR } from "../lib/files.js";
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+
8
+ /** Sanitize user input for safe use in shell commands */
9
+ function shellEscape(s: string): string {
10
+ return s.replace(/[^a-zA-Z0-9_\-./]/g, "");
11
+ }
12
+
13
+ /** Detect package manager from lockfiles */
14
+ function detectPackageManager(): string {
15
+ if (existsSync(join(PROJECT_DIR, "pnpm-lock.yaml"))) return "pnpm";
16
+ if (existsSync(join(PROJECT_DIR, "yarn.lock"))) return "yarn";
17
+ if (existsSync(join(PROJECT_DIR, "bun.lockb"))) return "bun";
18
+ return "npm";
19
+ }
20
+
21
+ /** Find files in a target area using git-tracked files (project-agnostic) */
22
+ function findAreaFiles(area: string): string {
23
+ if (!area) return getDiffFiles("HEAD~3");
24
+
25
+ const safeArea = shellEscape(area);
26
+
27
+ // If area looks like a path, search directly
28
+ if (area.includes("/")) {
29
+ return run(`git ls-files -- '${safeArea}*' 2>/dev/null | head -20`);
30
+ }
31
+
32
+ // Search for area keyword in git-tracked file paths
33
+ const files = run(`git ls-files 2>/dev/null | grep -i '${safeArea}' | head -20`);
34
+ if (files && !files.startsWith("[command failed")) return files;
35
+
36
+ // Fallback to recently changed files
37
+ return getDiffFiles("HEAD~3");
38
+ }
39
+
40
+ /** Find related test files for an area */
41
+ function findRelatedTests(area: string): string {
42
+ if (!area) return run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");
43
+
44
+ const safeArea = shellEscape(area.split(/\s+/)[0]);
45
+ const tests = run(`git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | grep -i '${safeArea}' | head -10`);
46
+ return tests || run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");
47
+ }
48
+
49
+ /** Get an example pattern from the first matching file */
50
+ function getExamplePattern(files: string): string {
51
+ const firstFile = files.split("\n").filter(Boolean)[0];
52
+ if (!firstFile) return "no pattern available";
53
+ return run(`head -30 '${shellEscape(firstFile)}' 2>/dev/null || echo 'could not read file'`);
54
+ }
55
+
56
+ export function registerEnrichAgentTask(server: McpServer): void {
57
+ server.tool(
58
+ "enrich_agent_task",
59
+ `Enrich a vague sub-agent task with project context. Call before spawning a Task/sub-agent to add file paths, patterns, scope boundaries, and done conditions.`,
60
+ {
61
+ task_description: z.string().describe("The raw task for the sub-agent"),
62
+ target_area: z.string().optional().describe("Codebase area: directory path, keyword, or description like 'auth tests', 'api routes'"),
63
+ },
64
+ async ({ task_description, target_area }) => {
65
+ const area = target_area || "";
66
+ const pm = detectPackageManager();
67
+ const fileList = findAreaFiles(area);
68
+ const testFiles = findRelatedTests(area);
69
+ const pattern = getExamplePattern(area.includes("test") ? testFiles : fileList);
70
+
71
+ const fileSummary = fileList
72
+ ? fileList.split("\n").filter(Boolean).slice(0, 5).join(", ")
73
+ : "Specify exact files";
74
+ const testSummary = testFiles
75
+ ? testFiles.split("\n").filter(Boolean).slice(0, 3).join(", ")
76
+ : "Run relevant tests";
77
+
78
+ return {
79
+ content: [{
80
+ type: "text" as const,
81
+ text: `## Files in Target Area
82
+ \`\`\`
83
+ ${fileList || "none found — specify a more precise area"}
84
+ \`\`\`
85
+
86
+ ## Related Tests
87
+ \`\`\`
88
+ ${testFiles || "none"}
89
+ \`\`\`
90
+
91
+ ## Existing Pattern
92
+ \`\`\`typescript
93
+ ${pattern}
94
+ \`\`\`
95
+
96
+ ## Enriched Task
97
+ Original: "${task_description}"
98
+
99
+ - **Files**: ${fileSummary}
100
+ - **Pattern**: Follow existing pattern above
101
+ - **Tests**: ${testSummary}
102
+ - **Scope**: Do NOT modify files outside target area
103
+ - **Done when**: All relevant tests pass + \`${pm} tsc --noEmit\` clean`,
104
+ }],
105
+ };
106
+ }
107
+ );
108
+ }