@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.
Files changed (75) hide show
  1. package/dist/nax.js +3294 -2907
  2. package/package.json +2 -2
  3. package/src/agents/claude-complete.ts +72 -0
  4. package/src/agents/claude-execution.ts +189 -0
  5. package/src/agents/claude-interactive.ts +77 -0
  6. package/src/agents/claude-plan.ts +23 -8
  7. package/src/agents/claude.ts +64 -349
  8. package/src/analyze/classifier.ts +2 -1
  9. package/src/cli/config-descriptions.ts +206 -0
  10. package/src/cli/config-diff.ts +103 -0
  11. package/src/cli/config-display.ts +285 -0
  12. package/src/cli/config-get.ts +55 -0
  13. package/src/cli/config.ts +7 -618
  14. package/src/cli/plugins.ts +15 -4
  15. package/src/cli/prompts-export.ts +58 -0
  16. package/src/cli/prompts-init.ts +200 -0
  17. package/src/cli/prompts-main.ts +237 -0
  18. package/src/cli/prompts-tdd.ts +78 -0
  19. package/src/cli/prompts.ts +10 -541
  20. package/src/commands/logs-formatter.ts +201 -0
  21. package/src/commands/logs-reader.ts +171 -0
  22. package/src/commands/logs.ts +11 -362
  23. package/src/config/loader.ts +4 -15
  24. package/src/config/runtime-types.ts +451 -0
  25. package/src/config/schema-types.ts +53 -0
  26. package/src/config/schemas.ts +2 -0
  27. package/src/config/types.ts +49 -486
  28. package/src/context/auto-detect.ts +2 -1
  29. package/src/context/builder.ts +3 -2
  30. package/src/execution/crash-heartbeat.ts +77 -0
  31. package/src/execution/crash-recovery.ts +23 -365
  32. package/src/execution/crash-signals.ts +149 -0
  33. package/src/execution/crash-writer.ts +154 -0
  34. package/src/execution/lifecycle/run-setup.ts +7 -1
  35. package/src/execution/parallel-coordinator.ts +278 -0
  36. package/src/execution/parallel-executor-rectification-pass.ts +117 -0
  37. package/src/execution/parallel-executor-rectify.ts +135 -0
  38. package/src/execution/parallel-executor.ts +19 -211
  39. package/src/execution/parallel-worker.ts +148 -0
  40. package/src/execution/parallel.ts +5 -404
  41. package/src/execution/pid-registry.ts +3 -8
  42. package/src/execution/runner-completion.ts +160 -0
  43. package/src/execution/runner-execution.ts +221 -0
  44. package/src/execution/runner-setup.ts +82 -0
  45. package/src/execution/runner.ts +53 -202
  46. package/src/execution/timeout-handler.ts +100 -0
  47. package/src/hooks/runner.ts +11 -21
  48. package/src/metrics/tracker.ts +7 -30
  49. package/src/pipeline/runner.ts +2 -1
  50. package/src/pipeline/stages/completion.ts +0 -1
  51. package/src/pipeline/stages/context.ts +2 -1
  52. package/src/plugins/extensions.ts +225 -0
  53. package/src/plugins/loader.ts +40 -4
  54. package/src/plugins/types.ts +18 -221
  55. package/src/prd/index.ts +2 -1
  56. package/src/prd/validate.ts +41 -0
  57. package/src/precheck/checks-blockers.ts +15 -419
  58. package/src/precheck/checks-cli.ts +68 -0
  59. package/src/precheck/checks-config.ts +102 -0
  60. package/src/precheck/checks-git.ts +87 -0
  61. package/src/precheck/checks-system.ts +163 -0
  62. package/src/review/orchestrator.ts +19 -6
  63. package/src/review/runner.ts +17 -5
  64. package/src/routing/chain.ts +2 -1
  65. package/src/routing/loader.ts +2 -5
  66. package/src/tdd/orchestrator.ts +2 -1
  67. package/src/tdd/verdict-reader.ts +266 -0
  68. package/src/tdd/verdict.ts +6 -271
  69. package/src/utils/errors.ts +12 -0
  70. package/src/utils/git.ts +12 -5
  71. package/src/utils/json-file.ts +72 -0
  72. package/src/verification/executor.ts +2 -1
  73. package/src/verification/smart-runner.ts +23 -3
  74. package/src/worktree/manager.ts +9 -3
  75. package/src/worktree/merge.ts +3 -2
@@ -3,25 +3,21 @@
3
3
  *
4
4
  * Displays run logs with filtering, follow mode, and multiple output formats.
5
5
  * Uses resolveProject() for directory resolution and formatter for output.
6
+ *
7
+ * Re-exports reader and formatter modules for backward compatibility.
6
8
  */
7
9
 
8
- import { existsSync, readdirSync } from "node:fs";
9
- import { readdir } from "node:fs/promises";
10
- import { homedir } from "node:os";
10
+ import { existsSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import chalk from "chalk";
13
- import type { LogEntry, LogLevel } from "../logger/types";
14
- import { type FormattedEntry, formatLogEntry, formatRunSummary } from "../logging/formatter";
15
- import type { RunSummary, VerbosityMode } from "../logging/types";
16
- import type { MetaJson } from "../pipeline/subscribers/registry";
17
- import { type ResolveProjectOptions, resolveProject } from "./common";
12
+ import type { LogLevel } from "../logger/types";
13
+ import { resolveProject } from "./common";
14
+ import { displayLogs, displayRunsList, followLogs } from "./logs-formatter";
15
+ import { resolveRunFileFromRegistry, selectRunFile } from "./logs-reader";
18
16
 
19
- /**
20
- * Swappable dependencies for testing (project convention: _deps over mock.module).
21
- */
22
- export const _deps = {
23
- getRunsDir: () => process.env.NAX_RUNS_DIR ?? join(homedir(), ".nax", "runs"),
24
- };
17
+ // Re-exports for backward compatibility
18
+ export { _deps } from "./logs-reader";
19
+ export { extractRunSummary, resolveRunFileFromRegistry, selectRunFile } from "./logs-reader";
20
+ export { displayLogs, displayRunsList, followLogs, formatDuration } from "./logs-formatter";
25
21
 
26
22
  /**
27
23
  * Options for logs command
@@ -43,78 +39,8 @@ export interface LogsOptions {
43
39
  json?: boolean;
44
40
  }
45
41
 
46
- /**
47
- * Log level hierarchy for filtering
48
- */
49
- const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
50
- debug: 0,
51
- info: 1,
52
- warn: 2,
53
- error: 3,
54
- };
55
-
56
- /**
57
- * Resolve log file path for a runId from the central registry (~/.nax/runs/).
58
- *
59
- * Scans all ~/.nax/runs/*\/meta.json entries for an exact or prefix match on runId.
60
- * Returns the path to the matching run's JSONL file, or null if eventsDir/file is unavailable.
61
- * Throws if the runId is not found in the registry at all.
62
- *
63
- * @param runId - Full or prefix run ID to look up
64
- * @returns Absolute path to the JSONL log file, or null if unavailable
65
- */
66
- async function resolveRunFileFromRegistry(runId: string): Promise<string | null> {
67
- const runsDir = _deps.getRunsDir();
68
-
69
- let entries: string[];
70
- try {
71
- entries = await readdir(runsDir);
72
- } catch {
73
- throw new Error(`Run not found in registry: ${runId}`);
74
- }
75
-
76
- let matched: MetaJson | null = null;
77
- for (const entry of entries) {
78
- const metaPath = join(runsDir, entry, "meta.json");
79
- try {
80
- const meta: MetaJson = await Bun.file(metaPath).json();
81
- if (meta.runId === runId || meta.runId.startsWith(runId)) {
82
- matched = meta;
83
- break;
84
- }
85
- } catch {
86
- // skip unreadable meta.json entries
87
- }
88
- }
89
-
90
- if (!matched) {
91
- throw new Error(`Run not found in registry: ${runId}`);
92
- }
93
-
94
- if (!existsSync(matched.eventsDir)) {
95
- console.log(`Log directory unavailable for run: ${runId}`);
96
- return null;
97
- }
98
-
99
- const files = readdirSync(matched.eventsDir)
100
- .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
101
- .sort()
102
- .reverse();
103
-
104
- if (files.length === 0) {
105
- console.log(`No log files found for run: ${runId}`);
106
- return null;
107
- }
108
-
109
- // Look for the specific run file by runId, fall back to newest
110
- const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
111
- return join(matched.eventsDir, specificFile ?? files[0]);
112
- }
113
-
114
42
  /**
115
43
  * Display logs with filtering and formatting
116
- *
117
- * @param options - Command options
118
44
  */
119
45
  export async function logsCommand(options: LogsOptions): Promise<void> {
120
46
  // When --run <runId> is provided, resolve via central registry
@@ -175,280 +101,3 @@ export async function logsCommand(options: LogsOptions): Promise<void> {
175
101
  // Display static logs
176
102
  await displayLogs(runFile, options);
177
103
  }
178
-
179
- /**
180
- * Select which run file to display (always returns the latest run)
181
- */
182
- async function selectRunFile(runsDir: string): Promise<string | null> {
183
- const files = readdirSync(runsDir)
184
- .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
185
- .sort()
186
- .reverse();
187
-
188
- if (files.length === 0) {
189
- return null;
190
- }
191
-
192
- return join(runsDir, files[0]);
193
- }
194
-
195
- /**
196
- * Display runs table
197
- */
198
- async function displayRunsList(runsDir: string): Promise<void> {
199
- const files = readdirSync(runsDir)
200
- .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
201
- .sort()
202
- .reverse();
203
-
204
- if (files.length === 0) {
205
- console.log(chalk.dim("No runs found"));
206
- return;
207
- }
208
-
209
- console.log(chalk.bold("\nRuns:\n"));
210
- console.log(chalk.gray(" Timestamp Stories Duration Cost Status"));
211
- console.log(chalk.gray(" ─────────────────────────────────────────────────────────"));
212
-
213
- for (const file of files) {
214
- const filePath = join(runsDir, file);
215
- const summary = await extractRunSummary(filePath);
216
-
217
- const timestamp = file.replace(".jsonl", "");
218
- const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
219
- const duration = summary ? formatDuration(summary.durationMs) : "?";
220
- const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
221
- const status = summary ? (summary.failed === 0 ? chalk.green("✓") : chalk.red("✗")) : "?";
222
-
223
- console.log(` ${timestamp} ${stories.padEnd(7)} ${duration.padEnd(8)} ${cost.padEnd(8)} ${status}`);
224
- }
225
-
226
- console.log();
227
- }
228
-
229
- /**
230
- * Extract run summary from log file
231
- */
232
- async function extractRunSummary(filePath: string): Promise<RunSummary | null> {
233
- const file = Bun.file(filePath);
234
- const content = await file.text();
235
- const lines = content.trim().split("\n");
236
-
237
- let total = 0;
238
- let passed = 0;
239
- let failed = 0;
240
- let skipped = 0;
241
- let totalCost = 0;
242
- let startedAt = "";
243
- let completedAt: string | undefined;
244
- let firstTimestamp = "";
245
- let lastTimestamp = "";
246
-
247
- for (const line of lines) {
248
- if (!line.trim()) continue;
249
-
250
- try {
251
- const entry: LogEntry = JSON.parse(line);
252
-
253
- if (!firstTimestamp) {
254
- firstTimestamp = entry.timestamp;
255
- }
256
- lastTimestamp = entry.timestamp;
257
-
258
- if (entry.stage === "run.start") {
259
- startedAt = entry.timestamp;
260
- const runData = entry.data as Record<string, unknown>;
261
- total = typeof runData?.totalStories === "number" ? runData.totalStories : 0;
262
- }
263
-
264
- if (entry.stage === "story.complete" || entry.stage === "agent.complete") {
265
- const data = entry.data as Record<string, unknown>;
266
- const success = data?.success ?? true;
267
- const action = data?.finalAction || data?.action;
268
-
269
- if (success) {
270
- passed++;
271
- } else if (action === "skip") {
272
- skipped++;
273
- } else {
274
- failed++;
275
- }
276
-
277
- if (data?.cost && typeof data.cost === "number") {
278
- totalCost += data.cost;
279
- }
280
- }
281
-
282
- if (entry.stage === "run.end") {
283
- completedAt = entry.timestamp;
284
- }
285
- } catch {
286
- // Skip invalid JSON lines
287
- }
288
- }
289
-
290
- if (!startedAt) {
291
- return null;
292
- }
293
-
294
- const durationMs = lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0;
295
-
296
- return {
297
- total,
298
- passed,
299
- failed,
300
- skipped,
301
- durationMs,
302
- totalCost,
303
- startedAt,
304
- completedAt,
305
- };
306
- }
307
-
308
- /**
309
- * Display static logs
310
- */
311
- async function displayLogs(filePath: string, options: LogsOptions): Promise<void> {
312
- const file = Bun.file(filePath);
313
- const content = await file.text();
314
- const lines = content.trim().split("\n");
315
-
316
- const mode: VerbosityMode = options.json ? "json" : "normal";
317
-
318
- for (const line of lines) {
319
- if (!line.trim()) continue;
320
-
321
- try {
322
- const entry: LogEntry = JSON.parse(line);
323
-
324
- // Apply filters
325
- if (!shouldDisplayEntry(entry, options)) {
326
- continue;
327
- }
328
-
329
- // Format and display
330
- const formatted = formatLogEntry(entry, { mode, useColor: true });
331
-
332
- if (formatted.shouldDisplay && formatted.output) {
333
- console.log(formatted.output);
334
- }
335
- } catch {
336
- // Skip invalid JSON lines
337
- }
338
- }
339
-
340
- // Display summary footer (unless in json mode)
341
- if (!options.json) {
342
- const summary = await extractRunSummary(filePath);
343
- if (summary) {
344
- console.log(formatRunSummary(summary, { mode: "normal", useColor: true }));
345
- }
346
- }
347
- }
348
-
349
- /**
350
- * Follow logs in real-time (tail -f mode)
351
- */
352
- async function followLogs(filePath: string, options: LogsOptions): Promise<void> {
353
- const mode: VerbosityMode = options.json ? "json" : "normal";
354
-
355
- // Display existing logs first
356
- const file = Bun.file(filePath);
357
- const content = await file.text();
358
- const lines = content.trim().split("\n");
359
-
360
- for (const line of lines) {
361
- if (!line.trim()) continue;
362
-
363
- try {
364
- const entry: LogEntry = JSON.parse(line);
365
-
366
- if (!shouldDisplayEntry(entry, options)) {
367
- continue;
368
- }
369
-
370
- const formatted = formatLogEntry(entry, { mode, useColor: true });
371
-
372
- if (formatted.shouldDisplay && formatted.output) {
373
- console.log(formatted.output);
374
- }
375
- } catch {
376
- // Skip invalid JSON lines
377
- }
378
- }
379
-
380
- // Now watch for new lines
381
- let lastSize = (await Bun.file(filePath).stat()).size;
382
-
383
- while (true) {
384
- await Bun.sleep(500);
385
-
386
- const currentSize = (await Bun.file(filePath).stat()).size;
387
-
388
- if (currentSize > lastSize) {
389
- // File has grown, read new content
390
- const newFile = Bun.file(filePath);
391
- const newContent = await newFile.text();
392
- const newLines = newContent.slice(lastSize).trim().split("\n");
393
-
394
- for (const line of newLines) {
395
- if (!line.trim()) continue;
396
-
397
- try {
398
- const entry: LogEntry = JSON.parse(line);
399
-
400
- if (!shouldDisplayEntry(entry, options)) {
401
- continue;
402
- }
403
-
404
- const formatted = formatLogEntry(entry, { mode, useColor: true });
405
-
406
- if (formatted.shouldDisplay && formatted.output) {
407
- console.log(formatted.output);
408
- }
409
- } catch {
410
- // Skip invalid JSON lines
411
- }
412
- }
413
-
414
- lastSize = currentSize;
415
- }
416
- }
417
- }
418
-
419
- /**
420
- * Check if entry should be displayed based on filters
421
- */
422
- function shouldDisplayEntry(entry: LogEntry, options: LogsOptions): boolean {
423
- // Story filter
424
- if (options.story && entry.storyId !== options.story) {
425
- return false;
426
- }
427
-
428
- // Level filter
429
- if (options.level) {
430
- const entryPriority = LOG_LEVEL_PRIORITY[entry.level];
431
- const filterPriority = LOG_LEVEL_PRIORITY[options.level];
432
-
433
- if (entryPriority < filterPriority) {
434
- return false;
435
- }
436
- }
437
-
438
- return true;
439
- }
440
-
441
- /**
442
- * Format duration in milliseconds
443
- */
444
- function formatDuration(ms: number): string {
445
- if (ms < 1000) {
446
- return `${ms}ms`;
447
- }
448
- if (ms < 60000) {
449
- return `${(ms / 1000).toFixed(1)}s`;
450
- }
451
- const minutes = Math.floor(ms / 60000);
452
- const seconds = Math.floor((ms % 60000) / 1000);
453
- return `${minutes}m${seconds}s`;
454
- }
@@ -7,9 +7,10 @@
7
7
  import { existsSync } from "node:fs";
8
8
  import { join, resolve } from "node:path";
9
9
  import { getLogger } from "../logger";
10
+ import { loadJsonFile } from "../utils/json-file";
10
11
  import { deepMergeConfig } from "./merger";
11
12
  import { MAX_DIRECTORY_DEPTH } from "./path-security";
12
- import { globalConfigDir, projectConfigDir } from "./paths";
13
+ import { globalConfigDir } from "./paths";
13
14
  import { DEFAULT_CONFIG, type NaxConfig, NaxConfigSchema } from "./schema";
14
15
 
15
16
  /** Global config path */
@@ -36,18 +37,6 @@ export function findProjectDir(startDir: string = process.cwd()): string | null
36
37
  return null;
37
38
  }
38
39
 
39
- /** Load and parse a JSON config file */
40
- async function loadJsonFile<T>(path: string): Promise<T | null> {
41
- if (!existsSync(path)) return null;
42
- try {
43
- return await Bun.file(path).json();
44
- } catch (err) {
45
- const logger = getLogger();
46
- logger.warn("config", "Failed to parse config file", { path, error: String(err) });
47
- return null;
48
- }
49
- }
50
-
51
40
  /** @internal Backward compat: map deprecated routing.llm.batchMode to routing.llm.mode.
52
41
  * Returns a new object (immutable -- does not mutate the input). */
53
42
  function applyBatchModeCompat(conf: Record<string, unknown>): Record<string, unknown> {
@@ -83,7 +72,7 @@ export async function loadConfig(projectDir?: string, cliOverrides?: Record<stri
83
72
  let rawConfig: Record<string, unknown> = structuredClone(DEFAULT_CONFIG as unknown as Record<string, unknown>);
84
73
 
85
74
  // Layer 1: Global config (~/.nax/config.json)
86
- const globalConfRaw = await loadJsonFile<Record<string, unknown>>(globalConfigPath());
75
+ const globalConfRaw = await loadJsonFile<Record<string, unknown>>(globalConfigPath(), "config");
87
76
  if (globalConfRaw) {
88
77
  // Backward compatibility: apply batchMode->mode shim before merge so defaults don't shadow it
89
78
  const globalConf = applyBatchModeCompat(globalConfRaw);
@@ -93,7 +82,7 @@ export async function loadConfig(projectDir?: string, cliOverrides?: Record<stri
93
82
  // Layer 2: Project config (nax/config.json)
94
83
  const projDir = projectDir ?? findProjectDir();
95
84
  if (projDir) {
96
- const projConf = await loadJsonFile<Record<string, unknown>>(join(projDir, "config.json"));
85
+ const projConf = await loadJsonFile<Record<string, unknown>>(join(projDir, "config.json"), "config");
97
86
  if (projConf) {
98
87
  // Backward compatibility: map deprecated batchMode -> mode on raw user config
99
88
  // MUST run before deepMergeConfig so defaults don't shadow the check.