@nathapp/nax 0.38.0 → 0.38.2
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/dist/nax.js +3294 -2907
- package/package.json +2 -2
- package/src/agents/claude-complete.ts +72 -0
- package/src/agents/claude-execution.ts +189 -0
- package/src/agents/claude-interactive.ts +77 -0
- package/src/agents/claude-plan.ts +23 -8
- package/src/agents/claude.ts +64 -349
- package/src/analyze/classifier.ts +2 -1
- package/src/cli/config-descriptions.ts +206 -0
- package/src/cli/config-diff.ts +103 -0
- package/src/cli/config-display.ts +285 -0
- package/src/cli/config-get.ts +55 -0
- package/src/cli/config.ts +7 -618
- package/src/cli/plugins.ts +15 -4
- package/src/cli/prompts-export.ts +58 -0
- package/src/cli/prompts-init.ts +200 -0
- package/src/cli/prompts-main.ts +237 -0
- package/src/cli/prompts-tdd.ts +78 -0
- package/src/cli/prompts.ts +10 -541
- package/src/commands/logs-formatter.ts +201 -0
- package/src/commands/logs-reader.ts +171 -0
- package/src/commands/logs.ts +11 -362
- package/src/config/loader.ts +4 -15
- package/src/config/runtime-types.ts +451 -0
- package/src/config/schema-types.ts +53 -0
- package/src/config/schemas.ts +2 -0
- package/src/config/types.ts +49 -486
- package/src/context/auto-detect.ts +2 -1
- package/src/context/builder.ts +3 -2
- package/src/execution/crash-heartbeat.ts +77 -0
- package/src/execution/crash-recovery.ts +23 -365
- package/src/execution/crash-signals.ts +149 -0
- package/src/execution/crash-writer.ts +154 -0
- package/src/execution/lifecycle/run-setup.ts +7 -1
- package/src/execution/parallel-coordinator.ts +278 -0
- package/src/execution/parallel-executor-rectification-pass.ts +117 -0
- package/src/execution/parallel-executor-rectify.ts +135 -0
- package/src/execution/parallel-executor.ts +19 -211
- package/src/execution/parallel-worker.ts +148 -0
- package/src/execution/parallel.ts +5 -404
- package/src/execution/pid-registry.ts +3 -8
- package/src/execution/runner-completion.ts +160 -0
- package/src/execution/runner-execution.ts +221 -0
- package/src/execution/runner-setup.ts +82 -0
- package/src/execution/runner.ts +53 -202
- package/src/execution/timeout-handler.ts +100 -0
- package/src/hooks/runner.ts +11 -21
- package/src/metrics/tracker.ts +7 -30
- package/src/pipeline/runner.ts +2 -1
- package/src/pipeline/stages/completion.ts +0 -1
- package/src/pipeline/stages/context.ts +2 -1
- package/src/plugins/extensions.ts +225 -0
- package/src/plugins/loader.ts +40 -4
- package/src/plugins/types.ts +18 -221
- package/src/prd/index.ts +2 -1
- package/src/prd/validate.ts +41 -0
- package/src/precheck/checks-blockers.ts +15 -419
- package/src/precheck/checks-cli.ts +68 -0
- package/src/precheck/checks-config.ts +102 -0
- package/src/precheck/checks-git.ts +87 -0
- package/src/precheck/checks-system.ts +163 -0
- package/src/review/orchestrator.ts +19 -6
- package/src/review/runner.ts +17 -5
- package/src/routing/chain.ts +2 -1
- package/src/routing/loader.ts +2 -5
- package/src/tdd/orchestrator.ts +2 -1
- package/src/tdd/verdict-reader.ts +266 -0
- package/src/tdd/verdict.ts +6 -271
- package/src/utils/errors.ts +12 -0
- package/src/utils/git.ts +12 -5
- package/src/utils/json-file.ts +72 -0
- package/src/verification/executor.ts +2 -1
- package/src/verification/smart-runner.ts +23 -3
- package/src/worktree/manager.ts +9 -3
- package/src/worktree/merge.ts +3 -2
|
@@ -1,427 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Precheck Tier 1 Blockers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports check implementations from specialized modules.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, statSync } from "node:fs";
|
|
7
|
-
import type { NaxConfig } from "../config";
|
|
8
|
-
import type { PRD } from "../prd/types";
|
|
9
|
-
import type { Check } from "./types";
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stderr: "pipe",
|
|
18
|
-
});
|
|
7
|
+
// Re-export for backward compatibility
|
|
8
|
+
export {
|
|
9
|
+
checkGitRepoExists,
|
|
10
|
+
checkWorkingTreeClean,
|
|
11
|
+
checkGitUserConfigured,
|
|
12
|
+
} from "./checks-git";
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
let passed = exitCode === 0;
|
|
14
|
+
export { checkStaleLock, checkPRDValid } from "./checks-config";
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
// This handles test scenarios where .git exists but isn't fully initialized
|
|
25
|
-
if (!passed) {
|
|
26
|
-
const gitDir = `${workdir}/.git`;
|
|
27
|
-
if (existsSync(gitDir)) {
|
|
28
|
-
const stats = statSync(gitDir);
|
|
29
|
-
passed = stats.isDirectory();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
16
|
+
export { checkClaudeCLI, checkAgentCLI, _deps } from "./checks-cli";
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Check if working tree is clean. Uses: git status --porcelain */
|
|
42
|
-
export async function checkWorkingTreeClean(workdir: string): Promise<Check> {
|
|
43
|
-
const proc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
44
|
-
cwd: workdir,
|
|
45
|
-
stdout: "pipe",
|
|
46
|
-
stderr: "pipe",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const output = await new Response(proc.stdout).text();
|
|
50
|
-
const exitCode = await proc.exited;
|
|
51
|
-
const passed = exitCode === 0 && output.trim() === "";
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
name: "working-tree-clean",
|
|
55
|
-
tier: "blocker",
|
|
56
|
-
passed,
|
|
57
|
-
message: passed ? "Working tree is clean" : "Uncommitted changes detected",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Check if nax.lock is older than 2 hours. */
|
|
62
|
-
export async function checkStaleLock(workdir: string): Promise<Check> {
|
|
63
|
-
const lockPath = `${workdir}/nax.lock`;
|
|
64
|
-
const exists = existsSync(lockPath);
|
|
65
|
-
|
|
66
|
-
if (!exists) {
|
|
67
|
-
return {
|
|
68
|
-
name: "no-stale-lock",
|
|
69
|
-
tier: "blocker",
|
|
70
|
-
passed: true,
|
|
71
|
-
message: "No lock file present",
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const file = Bun.file(lockPath);
|
|
77
|
-
const content = await file.text();
|
|
78
|
-
const lockData = JSON.parse(content);
|
|
79
|
-
|
|
80
|
-
// Support both timestamp (ms) and startedAt (ISO string) formats
|
|
81
|
-
let lockTimeMs: number;
|
|
82
|
-
if (lockData.timestamp) {
|
|
83
|
-
lockTimeMs = lockData.timestamp;
|
|
84
|
-
} else if (lockData.startedAt) {
|
|
85
|
-
lockTimeMs = new Date(lockData.startedAt).getTime();
|
|
86
|
-
} else {
|
|
87
|
-
// Fallback to file mtime if no timestamp in JSON
|
|
88
|
-
const stat = statSync(lockPath);
|
|
89
|
-
lockTimeMs = stat.mtimeMs;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const ageMs = Date.now() - lockTimeMs;
|
|
93
|
-
const twoHoursMs = 2 * 60 * 60 * 1000;
|
|
94
|
-
const passed = ageMs < twoHoursMs;
|
|
95
|
-
|
|
96
|
-
const ageMinutes = Math.floor(ageMs / 60000);
|
|
97
|
-
const ageHours = Math.floor(ageMinutes / 60);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
name: "no-stale-lock",
|
|
101
|
-
tier: "blocker",
|
|
102
|
-
passed,
|
|
103
|
-
message: passed ? "Lock file is fresh" : "stale lock detected (over 2 hours old)",
|
|
104
|
-
};
|
|
105
|
-
} catch (error) {
|
|
106
|
-
return {
|
|
107
|
-
name: "no-stale-lock",
|
|
108
|
-
tier: "blocker",
|
|
109
|
-
passed: false,
|
|
110
|
-
message: "Failed to read lock file",
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Validate PRD structure and required fields. Auto-defaults: tags=[], status=pending, storyPoints=1 */
|
|
116
|
-
export async function checkPRDValid(prd: PRD): Promise<Check> {
|
|
117
|
-
const errors: string[] = [];
|
|
118
|
-
|
|
119
|
-
// Validate required PRD fields
|
|
120
|
-
if (!prd.project || prd.project.trim() === "") {
|
|
121
|
-
errors.push("Missing project field");
|
|
122
|
-
}
|
|
123
|
-
if (!prd.feature || prd.feature.trim() === "") {
|
|
124
|
-
errors.push("Missing feature field");
|
|
125
|
-
}
|
|
126
|
-
if (!prd.branchName || prd.branchName.trim() === "") {
|
|
127
|
-
errors.push("Missing branchName field");
|
|
128
|
-
}
|
|
129
|
-
if (!Array.isArray(prd.userStories)) {
|
|
130
|
-
errors.push("userStories must be an array");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Validate each story
|
|
134
|
-
if (Array.isArray(prd.userStories)) {
|
|
135
|
-
for (const story of prd.userStories) {
|
|
136
|
-
// Auto-default optional fields in-memory (don't modify the PRD)
|
|
137
|
-
story.tags = story.tags ?? [];
|
|
138
|
-
story.status = story.status ?? "pending";
|
|
139
|
-
story.storyPoints = story.storyPoints ?? 1;
|
|
140
|
-
story.acceptanceCriteria = story.acceptanceCriteria ?? [];
|
|
141
|
-
|
|
142
|
-
// Validate required fields
|
|
143
|
-
if (!story.id || story.id.trim() === "") {
|
|
144
|
-
errors.push(`Story missing id: ${JSON.stringify(story).slice(0, 50)}`);
|
|
145
|
-
}
|
|
146
|
-
if (!story.title || story.title.trim() === "") {
|
|
147
|
-
errors.push(`Story ${story.id} missing title`);
|
|
148
|
-
}
|
|
149
|
-
if (!story.description || story.description.trim() === "") {
|
|
150
|
-
errors.push(`Story ${story.id} missing description`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const passed = errors.length === 0;
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
name: "prd-valid",
|
|
159
|
-
tier: "blocker",
|
|
160
|
-
passed,
|
|
161
|
-
message: passed ? "PRD structure is valid" : errors.join("; "),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Dependency injection for testability */
|
|
166
|
-
export const _deps = {
|
|
167
|
-
spawn: Bun.spawn,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/** Check if Claude CLI is available. Uses: claude --version */
|
|
171
|
-
export async function checkClaudeCLI(): Promise<Check> {
|
|
172
|
-
try {
|
|
173
|
-
const proc = _deps.spawn(["claude", "--version"], {
|
|
174
|
-
stdout: "pipe",
|
|
175
|
-
stderr: "pipe",
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const exitCode = await proc.exited;
|
|
179
|
-
const passed = exitCode === 0;
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
name: "claude-cli-available",
|
|
183
|
-
tier: "blocker",
|
|
184
|
-
passed,
|
|
185
|
-
message: passed ? "Claude CLI is available" : "Claude CLI not found. Install from https://claude.ai/download",
|
|
186
|
-
};
|
|
187
|
-
} catch {
|
|
188
|
-
// Bun.spawn throws ENOENT when the binary is not found in PATH.
|
|
189
|
-
// Treat this as a failed check rather than an unhandled exception so the
|
|
190
|
-
// rest of the precheck pipeline can continue and report all issues at once.
|
|
191
|
-
return {
|
|
192
|
-
name: "claude-cli-available",
|
|
193
|
-
tier: "blocker",
|
|
194
|
-
passed: false,
|
|
195
|
-
message: "Claude CLI not found in PATH. Install from https://claude.ai/download",
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Check if configured agent binary is available. Reads agent from config, defaults to 'claude'.
|
|
201
|
-
* Supports: claude, codex, opencode, gemini, aider */
|
|
202
|
-
export async function checkAgentCLI(config: NaxConfig): Promise<Check> {
|
|
203
|
-
const agent = config.execution?.agent || "claude";
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const proc = _deps.spawn([agent, "--version"], {
|
|
207
|
-
stdout: "pipe",
|
|
208
|
-
stderr: "pipe",
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const exitCode = await proc.exited;
|
|
212
|
-
const passed = exitCode === 0;
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
name: "agent-cli-available",
|
|
216
|
-
tier: "blocker",
|
|
217
|
-
passed,
|
|
218
|
-
message: passed ? `${agent} CLI is available` : `${agent} CLI not found. Install the ${agent} binary.`,
|
|
219
|
-
};
|
|
220
|
-
} catch {
|
|
221
|
-
// Bun.spawn throws ENOENT when the binary is not found in PATH.
|
|
222
|
-
return {
|
|
223
|
-
name: "agent-cli-available",
|
|
224
|
-
tier: "blocker",
|
|
225
|
-
passed: false,
|
|
226
|
-
message: `${agent} CLI not found in PATH. Install the ${agent} binary.`,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Check if dependencies are installed (language-aware). Detects: node_modules, target, venv, vendor */
|
|
232
|
-
export async function checkDependenciesInstalled(workdir: string): Promise<Check> {
|
|
233
|
-
const depPaths = [
|
|
234
|
-
{ path: "node_modules" },
|
|
235
|
-
{ path: "target" },
|
|
236
|
-
{ path: "venv" },
|
|
237
|
-
{ path: ".venv" },
|
|
238
|
-
{ path: "vendor" },
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
const found: string[] = [];
|
|
242
|
-
for (const { path } of depPaths) {
|
|
243
|
-
const fullPath = `${workdir}/${path}`;
|
|
244
|
-
// Check if it exists and is a directory
|
|
245
|
-
if (existsSync(fullPath)) {
|
|
246
|
-
const stats = statSync(fullPath);
|
|
247
|
-
if (stats.isDirectory()) {
|
|
248
|
-
found.push(path);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const passed = found.length > 0;
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
name: "dependencies-installed",
|
|
257
|
-
tier: "blocker",
|
|
258
|
-
passed,
|
|
259
|
-
message: passed ? `Dependencies found: ${found.join(", ")}` : "No dependency directories detected",
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Check if test command works. Skips silently if command is null/false. */
|
|
264
|
-
export async function checkTestCommand(config: NaxConfig): Promise<Check> {
|
|
265
|
-
// Try multiple possible locations for testCommand
|
|
266
|
-
const testCommand = config.execution.testCommand || (config.quality?.commands?.test as string | undefined);
|
|
267
|
-
|
|
268
|
-
// Skip if explicitly disabled or not configured
|
|
269
|
-
if (!testCommand || testCommand === null || testCommand === null) {
|
|
270
|
-
return {
|
|
271
|
-
name: "test-command-works",
|
|
272
|
-
tier: "blocker",
|
|
273
|
-
passed: true,
|
|
274
|
-
message: "Test command not configured (skipped)",
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Parse command and args
|
|
279
|
-
const parts = testCommand.split(" ");
|
|
280
|
-
const [cmd, ...args] = parts;
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const proc = Bun.spawn([cmd, ...args, "--help"], {
|
|
284
|
-
stdout: "pipe",
|
|
285
|
-
stderr: "pipe",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const exitCode = await proc.exited;
|
|
289
|
-
const passed = exitCode === 0;
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
name: "test-command-works",
|
|
293
|
-
tier: "blocker",
|
|
294
|
-
passed,
|
|
295
|
-
message: passed ? "Test command is available" : `Test command failed: ${testCommand}`,
|
|
296
|
-
};
|
|
297
|
-
} catch (error) {
|
|
298
|
-
return {
|
|
299
|
-
name: "test-command-works",
|
|
300
|
-
tier: "blocker",
|
|
301
|
-
passed: false,
|
|
302
|
-
message: `Test command failed: ${testCommand}`,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Check if lint command works. Skips silently if command is null/false. */
|
|
308
|
-
export async function checkLintCommand(config: NaxConfig): Promise<Check> {
|
|
309
|
-
const lintCommand = config.execution.lintCommand;
|
|
310
|
-
|
|
311
|
-
// Skip if explicitly disabled or not configured
|
|
312
|
-
if (!lintCommand || lintCommand === null || lintCommand === null) {
|
|
313
|
-
return {
|
|
314
|
-
name: "lint-command-works",
|
|
315
|
-
tier: "blocker",
|
|
316
|
-
passed: true,
|
|
317
|
-
message: "Lint command not configured (skipped)",
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Parse command and args
|
|
322
|
-
const parts = lintCommand.split(" ");
|
|
323
|
-
const [cmd, ...args] = parts;
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
const proc = Bun.spawn([cmd, ...args, "--help"], {
|
|
327
|
-
stdout: "pipe",
|
|
328
|
-
stderr: "pipe",
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const exitCode = await proc.exited;
|
|
332
|
-
const passed = exitCode === 0;
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
name: "lint-command-works",
|
|
336
|
-
tier: "blocker",
|
|
337
|
-
passed,
|
|
338
|
-
message: passed ? "Lint command is available" : `Lint command failed: ${lintCommand}`,
|
|
339
|
-
};
|
|
340
|
-
} catch (error) {
|
|
341
|
-
return {
|
|
342
|
-
name: "lint-command-works",
|
|
343
|
-
tier: "blocker",
|
|
344
|
-
passed: false,
|
|
345
|
-
message: `Lint command failed: ${lintCommand}`,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** Check if typecheck command works. Skips silently if command is null/false. */
|
|
351
|
-
export async function checkTypecheckCommand(config: NaxConfig): Promise<Check> {
|
|
352
|
-
const typecheckCommand = config.execution.typecheckCommand;
|
|
353
|
-
|
|
354
|
-
// Skip if explicitly disabled or not configured
|
|
355
|
-
if (!typecheckCommand || typecheckCommand === null || typecheckCommand === null) {
|
|
356
|
-
return {
|
|
357
|
-
name: "typecheck-command-works",
|
|
358
|
-
tier: "blocker",
|
|
359
|
-
passed: true,
|
|
360
|
-
message: "Typecheck command not configured (skipped)",
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Parse command and args
|
|
365
|
-
const parts = typecheckCommand.split(" ");
|
|
366
|
-
const [cmd, ...args] = parts;
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const proc = Bun.spawn([cmd, ...args, "--help"], {
|
|
370
|
-
stdout: "pipe",
|
|
371
|
-
stderr: "pipe",
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
const exitCode = await proc.exited;
|
|
375
|
-
const passed = exitCode === 0;
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
name: "typecheck-command-works",
|
|
379
|
-
tier: "blocker",
|
|
380
|
-
passed,
|
|
381
|
-
message: passed
|
|
382
|
-
? `Typecheck command is available: ${typecheckCommand}`
|
|
383
|
-
: `Typecheck command failed: ${typecheckCommand}`,
|
|
384
|
-
};
|
|
385
|
-
} catch (error) {
|
|
386
|
-
return {
|
|
387
|
-
name: "typecheck-command-works",
|
|
388
|
-
tier: "blocker",
|
|
389
|
-
passed: false,
|
|
390
|
-
message: `Typecheck command failed: ${typecheckCommand}`,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Check if git user is configured. */
|
|
396
|
-
export async function checkGitUserConfigured(workdir?: string): Promise<Check> {
|
|
397
|
-
const spawnOptions = {
|
|
398
|
-
stdout: "pipe" as const,
|
|
399
|
-
stderr: "pipe" as const,
|
|
400
|
-
...(workdir && { cwd: workdir }),
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const nameProc = Bun.spawn(["git", "config", "user.name"], spawnOptions);
|
|
404
|
-
const emailProc = Bun.spawn(["git", "config", "user.email"], spawnOptions);
|
|
405
|
-
|
|
406
|
-
const nameOutput = await new Response(nameProc.stdout).text();
|
|
407
|
-
const emailOutput = await new Response(emailProc.stdout).text();
|
|
408
|
-
const nameExitCode = await nameProc.exited;
|
|
409
|
-
const emailExitCode = await emailProc.exited;
|
|
410
|
-
|
|
411
|
-
const hasName = nameExitCode === 0 && nameOutput.trim() !== "";
|
|
412
|
-
const hasEmail = emailExitCode === 0 && emailOutput.trim() !== "";
|
|
413
|
-
const passed = hasName && hasEmail;
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
name: "git-user-configured",
|
|
417
|
-
tier: "blocker",
|
|
418
|
-
passed,
|
|
419
|
-
message: passed
|
|
420
|
-
? "Git user is configured"
|
|
421
|
-
: !hasName && !hasEmail
|
|
422
|
-
? "Git user.name and user.email not configured"
|
|
423
|
-
: !hasName
|
|
424
|
-
? "Git user.name not configured"
|
|
425
|
-
: "Git user.email not configured",
|
|
426
|
-
};
|
|
427
|
-
}
|
|
18
|
+
export {
|
|
19
|
+
checkDependenciesInstalled,
|
|
20
|
+
checkTestCommand,
|
|
21
|
+
checkLintCommand,
|
|
22
|
+
checkTypecheckCommand,
|
|
23
|
+
} from "./checks-system";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI availability precheck implementations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NaxConfig } from "../config";
|
|
6
|
+
import type { Check } from "./types";
|
|
7
|
+
|
|
8
|
+
/** Dependency injection for testability */
|
|
9
|
+
export const _deps = {
|
|
10
|
+
spawn: Bun.spawn,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Check if Claude CLI is available. Uses: claude --version */
|
|
14
|
+
export async function checkClaudeCLI(): Promise<Check> {
|
|
15
|
+
try {
|
|
16
|
+
const proc = _deps.spawn(["claude", "--version"], {
|
|
17
|
+
stdout: "pipe",
|
|
18
|
+
stderr: "pipe",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const exitCode = await proc.exited;
|
|
22
|
+
const passed = exitCode === 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: "claude-cli-available",
|
|
26
|
+
tier: "blocker",
|
|
27
|
+
passed,
|
|
28
|
+
message: passed ? "Claude CLI is available" : "Claude CLI not found. Install from https://claude.ai/download",
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
name: "claude-cli-available",
|
|
33
|
+
tier: "blocker",
|
|
34
|
+
passed: false,
|
|
35
|
+
message: "Claude CLI not found in PATH. Install from https://claude.ai/download",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Check if configured agent binary is available. Reads agent from config, defaults to 'claude'.
|
|
41
|
+
* Supports: claude, codex, opencode, gemini, aider */
|
|
42
|
+
export async function checkAgentCLI(config: NaxConfig): Promise<Check> {
|
|
43
|
+
const agent = config.execution?.agent || "claude";
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const proc = _deps.spawn([agent, "--version"], {
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const exitCode = await proc.exited;
|
|
52
|
+
const passed = exitCode === 0;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: "agent-cli-available",
|
|
56
|
+
tier: "blocker",
|
|
57
|
+
passed,
|
|
58
|
+
message: passed ? `${agent} CLI is available` : `${agent} CLI not found. Install the ${agent} binary.`,
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return {
|
|
62
|
+
name: "agent-cli-available",
|
|
63
|
+
tier: "blocker",
|
|
64
|
+
passed: false,
|
|
65
|
+
message: `${agent} CLI not found in PATH. Install the ${agent} binary.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration-related precheck implementations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import type { PRD } from "../prd/types";
|
|
7
|
+
import type { Check } from "./types";
|
|
8
|
+
|
|
9
|
+
/** Check if nax.lock is older than 2 hours. */
|
|
10
|
+
export async function checkStaleLock(workdir: string): Promise<Check> {
|
|
11
|
+
const lockPath = `${workdir}/nax.lock`;
|
|
12
|
+
const exists = existsSync(lockPath);
|
|
13
|
+
|
|
14
|
+
if (!exists) {
|
|
15
|
+
return {
|
|
16
|
+
name: "no-stale-lock",
|
|
17
|
+
tier: "blocker",
|
|
18
|
+
passed: true,
|
|
19
|
+
message: "No lock file present",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const file = Bun.file(lockPath);
|
|
25
|
+
const content = await file.text();
|
|
26
|
+
const lockData = JSON.parse(content);
|
|
27
|
+
|
|
28
|
+
let lockTimeMs: number;
|
|
29
|
+
if (lockData.timestamp) {
|
|
30
|
+
lockTimeMs = lockData.timestamp;
|
|
31
|
+
} else if (lockData.startedAt) {
|
|
32
|
+
lockTimeMs = new Date(lockData.startedAt).getTime();
|
|
33
|
+
} else {
|
|
34
|
+
const stat = statSync(lockPath);
|
|
35
|
+
lockTimeMs = stat.mtimeMs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ageMs = Date.now() - lockTimeMs;
|
|
39
|
+
const twoHoursMs = 2 * 60 * 60 * 1000;
|
|
40
|
+
const passed = ageMs < twoHoursMs;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: "no-stale-lock",
|
|
44
|
+
tier: "blocker",
|
|
45
|
+
passed,
|
|
46
|
+
message: passed ? "Lock file is fresh" : "stale lock detected (over 2 hours old)",
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return {
|
|
50
|
+
name: "no-stale-lock",
|
|
51
|
+
tier: "blocker",
|
|
52
|
+
passed: false,
|
|
53
|
+
message: "Failed to read lock file",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Validate PRD structure and required fields. Auto-defaults: tags=[], status=pending, storyPoints=1 */
|
|
59
|
+
export async function checkPRDValid(prd: PRD): Promise<Check> {
|
|
60
|
+
const errors: string[] = [];
|
|
61
|
+
|
|
62
|
+
if (!prd.project || prd.project.trim() === "") {
|
|
63
|
+
errors.push("Missing project field");
|
|
64
|
+
}
|
|
65
|
+
if (!prd.feature || prd.feature.trim() === "") {
|
|
66
|
+
errors.push("Missing feature field");
|
|
67
|
+
}
|
|
68
|
+
if (!prd.branchName || prd.branchName.trim() === "") {
|
|
69
|
+
errors.push("Missing branchName field");
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(prd.userStories)) {
|
|
72
|
+
errors.push("userStories must be an array");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(prd.userStories)) {
|
|
76
|
+
for (const story of prd.userStories) {
|
|
77
|
+
story.tags = story.tags ?? [];
|
|
78
|
+
story.status = story.status ?? "pending";
|
|
79
|
+
story.storyPoints = story.storyPoints ?? 1;
|
|
80
|
+
story.acceptanceCriteria = story.acceptanceCriteria ?? [];
|
|
81
|
+
|
|
82
|
+
if (!story.id || story.id.trim() === "") {
|
|
83
|
+
errors.push(`Story missing id: ${JSON.stringify(story).slice(0, 50)}`);
|
|
84
|
+
}
|
|
85
|
+
if (!story.title || story.title.trim() === "") {
|
|
86
|
+
errors.push(`Story ${story.id} missing title`);
|
|
87
|
+
}
|
|
88
|
+
if (!story.description || story.description.trim() === "") {
|
|
89
|
+
errors.push(`Story ${story.id} missing description`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const passed = errors.length === 0;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: "prd-valid",
|
|
98
|
+
tier: "blocker",
|
|
99
|
+
passed,
|
|
100
|
+
message: passed ? "PRD structure is valid" : errors.join("; "),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-related precheck implementations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import type { Check } from "./types";
|
|
7
|
+
|
|
8
|
+
/** Check if directory is a git repository. Uses: git rev-parse --git-dir */
|
|
9
|
+
export async function checkGitRepoExists(workdir: string): Promise<Check> {
|
|
10
|
+
const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
|
|
11
|
+
cwd: workdir,
|
|
12
|
+
stdout: "pipe",
|
|
13
|
+
stderr: "pipe",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const exitCode = await proc.exited;
|
|
17
|
+
let passed = exitCode === 0;
|
|
18
|
+
|
|
19
|
+
if (!passed) {
|
|
20
|
+
const gitDir = `${workdir}/.git`;
|
|
21
|
+
if (existsSync(gitDir)) {
|
|
22
|
+
const stats = statSync(gitDir);
|
|
23
|
+
passed = stats.isDirectory();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: "git-repo-exists",
|
|
29
|
+
tier: "blocker",
|
|
30
|
+
passed,
|
|
31
|
+
message: passed ? "git repository detected" : "not a git repository",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Check if working tree is clean. Uses: git status --porcelain */
|
|
36
|
+
export async function checkWorkingTreeClean(workdir: string): Promise<Check> {
|
|
37
|
+
const proc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
38
|
+
cwd: workdir,
|
|
39
|
+
stdout: "pipe",
|
|
40
|
+
stderr: "pipe",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const output = await new Response(proc.stdout).text();
|
|
44
|
+
const exitCode = await proc.exited;
|
|
45
|
+
const passed = exitCode === 0 && output.trim() === "";
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name: "working-tree-clean",
|
|
49
|
+
tier: "blocker",
|
|
50
|
+
passed,
|
|
51
|
+
message: passed ? "Working tree is clean" : "Uncommitted changes detected",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Check if git user is configured. */
|
|
56
|
+
export async function checkGitUserConfigured(workdir?: string): Promise<Check> {
|
|
57
|
+
const spawnOptions = {
|
|
58
|
+
stdout: "pipe" as const,
|
|
59
|
+
stderr: "pipe" as const,
|
|
60
|
+
...(workdir && { cwd: workdir }),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const nameProc = Bun.spawn(["git", "config", "user.name"], spawnOptions);
|
|
64
|
+
const emailProc = Bun.spawn(["git", "config", "user.email"], spawnOptions);
|
|
65
|
+
|
|
66
|
+
const nameOutput = await new Response(nameProc.stdout).text();
|
|
67
|
+
const emailOutput = await new Response(emailProc.stdout).text();
|
|
68
|
+
const nameExitCode = await nameProc.exited;
|
|
69
|
+
const emailExitCode = await emailProc.exited;
|
|
70
|
+
|
|
71
|
+
const hasName = nameExitCode === 0 && nameOutput.trim() !== "";
|
|
72
|
+
const hasEmail = emailExitCode === 0 && emailOutput.trim() !== "";
|
|
73
|
+
const passed = hasName && hasEmail;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: "git-user-configured",
|
|
77
|
+
tier: "blocker",
|
|
78
|
+
passed,
|
|
79
|
+
message: passed
|
|
80
|
+
? "Git user is configured"
|
|
81
|
+
: !hasName && !hasEmail
|
|
82
|
+
? "Git user.name and user.email not configured"
|
|
83
|
+
: !hasName
|
|
84
|
+
? "Git user.name not configured"
|
|
85
|
+
: "Git user.email not configured",
|
|
86
|
+
};
|
|
87
|
+
}
|