@nathapp/nax 0.37.0 → 0.38.1

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 (72) hide show
  1. package/dist/nax.js +3258 -2894
  2. package/package.json +4 -1
  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/prompts-export.ts +58 -0
  15. package/src/cli/prompts-init.ts +200 -0
  16. package/src/cli/prompts-main.ts +237 -0
  17. package/src/cli/prompts-tdd.ts +78 -0
  18. package/src/cli/prompts.ts +10 -541
  19. package/src/commands/logs-formatter.ts +201 -0
  20. package/src/commands/logs-reader.ts +171 -0
  21. package/src/commands/logs.ts +11 -362
  22. package/src/config/loader.ts +4 -15
  23. package/src/config/runtime-types.ts +448 -0
  24. package/src/config/schema-types.ts +53 -0
  25. package/src/config/types.ts +49 -486
  26. package/src/context/auto-detect.ts +2 -1
  27. package/src/context/builder.ts +3 -2
  28. package/src/execution/crash-heartbeat.ts +77 -0
  29. package/src/execution/crash-recovery.ts +23 -365
  30. package/src/execution/crash-signals.ts +149 -0
  31. package/src/execution/crash-writer.ts +154 -0
  32. package/src/execution/parallel-coordinator.ts +278 -0
  33. package/src/execution/parallel-executor-rectification-pass.ts +117 -0
  34. package/src/execution/parallel-executor-rectify.ts +135 -0
  35. package/src/execution/parallel-executor.ts +19 -211
  36. package/src/execution/parallel-worker.ts +148 -0
  37. package/src/execution/parallel.ts +5 -404
  38. package/src/execution/pid-registry.ts +3 -8
  39. package/src/execution/runner-completion.ts +160 -0
  40. package/src/execution/runner-execution.ts +221 -0
  41. package/src/execution/runner-setup.ts +82 -0
  42. package/src/execution/runner.ts +53 -202
  43. package/src/execution/timeout-handler.ts +100 -0
  44. package/src/hooks/runner.ts +11 -21
  45. package/src/metrics/tracker.ts +7 -30
  46. package/src/pipeline/runner.ts +2 -1
  47. package/src/pipeline/stages/completion.ts +0 -1
  48. package/src/pipeline/stages/context.ts +2 -1
  49. package/src/plugins/extensions.ts +225 -0
  50. package/src/plugins/loader.ts +2 -1
  51. package/src/plugins/types.ts +16 -221
  52. package/src/prd/index.ts +2 -1
  53. package/src/prd/validate.ts +41 -0
  54. package/src/precheck/checks-blockers.ts +15 -419
  55. package/src/precheck/checks-cli.ts +68 -0
  56. package/src/precheck/checks-config.ts +102 -0
  57. package/src/precheck/checks-git.ts +87 -0
  58. package/src/precheck/checks-system.ts +163 -0
  59. package/src/review/orchestrator.ts +19 -6
  60. package/src/review/runner.ts +17 -5
  61. package/src/routing/chain.ts +2 -1
  62. package/src/routing/loader.ts +2 -5
  63. package/src/tdd/orchestrator.ts +2 -1
  64. package/src/tdd/verdict-reader.ts +266 -0
  65. package/src/tdd/verdict.ts +6 -271
  66. package/src/utils/errors.ts +12 -0
  67. package/src/utils/git.ts +12 -5
  68. package/src/utils/json-file.ts +72 -0
  69. package/src/verification/executor.ts +2 -1
  70. package/src/verification/smart-runner.ts +23 -3
  71. package/src/worktree/manager.ts +9 -3
  72. package/src/worktree/merge.ts +3 -2
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Log formatting and display utilities
3
+ */
4
+
5
+ import { readdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import chalk from "chalk";
8
+ import type { LogEntry, LogLevel } from "../logger/types";
9
+ import { formatLogEntry, formatRunSummary } from "../logging/formatter";
10
+ import type { VerbosityMode } from "../logging/types";
11
+ import { extractRunSummary } from "./logs-reader";
12
+
13
+ /**
14
+ * Log level hierarchy for filtering
15
+ */
16
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
17
+ debug: 0,
18
+ info: 1,
19
+ warn: 2,
20
+ error: 3,
21
+ };
22
+
23
+ /**
24
+ * Display runs table
25
+ */
26
+ export async function displayRunsList(runsDir: string): Promise<void> {
27
+ const files = readdirSync(runsDir)
28
+ .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
29
+ .sort()
30
+ .reverse();
31
+
32
+ if (files.length === 0) {
33
+ console.log(chalk.dim("No runs found"));
34
+ return;
35
+ }
36
+
37
+ console.log(chalk.bold("\nRuns:\n"));
38
+ console.log(chalk.gray(" Timestamp Stories Duration Cost Status"));
39
+ console.log(chalk.gray(" ─────────────────────────────────────────────────────────"));
40
+
41
+ for (const file of files) {
42
+ const filePath = join(runsDir, file);
43
+ const summary = await extractRunSummary(filePath);
44
+
45
+ const timestamp = file.replace(".jsonl", "");
46
+ const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
47
+ const duration = summary ? formatDuration(summary.durationMs) : "?";
48
+ const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
49
+ const status = summary ? (summary.failed === 0 ? chalk.green("✓") : chalk.red("✗")) : "?";
50
+
51
+ console.log(` ${timestamp} ${stories.padEnd(7)} ${duration.padEnd(8)} ${cost.padEnd(8)} ${status}`);
52
+ }
53
+
54
+ console.log();
55
+ }
56
+
57
+ /**
58
+ * Display static logs
59
+ */
60
+ export async function displayLogs(
61
+ filePath: string,
62
+ options: { json?: boolean; story?: string; level?: LogLevel },
63
+ ): Promise<void> {
64
+ const file = Bun.file(filePath);
65
+ const content = await file.text();
66
+ const lines = content.trim().split("\n");
67
+
68
+ const mode: VerbosityMode = options.json ? "json" : "normal";
69
+
70
+ for (const line of lines) {
71
+ if (!line.trim()) continue;
72
+
73
+ try {
74
+ const entry: LogEntry = JSON.parse(line);
75
+
76
+ if (!shouldDisplayEntry(entry, options)) {
77
+ continue;
78
+ }
79
+
80
+ const formatted = formatLogEntry(entry, { mode, useColor: true });
81
+
82
+ if (formatted.shouldDisplay && formatted.output) {
83
+ console.log(formatted.output);
84
+ }
85
+ } catch {
86
+ // Skip invalid JSON lines
87
+ }
88
+ }
89
+
90
+ if (!options.json) {
91
+ const summary = await extractRunSummary(filePath);
92
+ if (summary) {
93
+ console.log(formatRunSummary(summary, { mode: "normal", useColor: true }));
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Follow logs in real-time (tail -f mode)
100
+ */
101
+ export async function followLogs(
102
+ filePath: string,
103
+ options: { json?: boolean; story?: string; level?: LogLevel },
104
+ ): Promise<void> {
105
+ const mode: VerbosityMode = options.json ? "json" : "normal";
106
+
107
+ const file = Bun.file(filePath);
108
+ const content = await file.text();
109
+ const lines = content.trim().split("\n");
110
+
111
+ for (const line of lines) {
112
+ if (!line.trim()) continue;
113
+
114
+ try {
115
+ const entry: LogEntry = JSON.parse(line);
116
+
117
+ if (!shouldDisplayEntry(entry, options)) {
118
+ continue;
119
+ }
120
+
121
+ const formatted = formatLogEntry(entry, { mode, useColor: true });
122
+
123
+ if (formatted.shouldDisplay && formatted.output) {
124
+ console.log(formatted.output);
125
+ }
126
+ } catch {
127
+ // Skip invalid JSON lines
128
+ }
129
+ }
130
+
131
+ let lastSize = (await Bun.file(filePath).stat()).size;
132
+
133
+ while (true) {
134
+ await Bun.sleep(500);
135
+
136
+ const currentSize = (await Bun.file(filePath).stat()).size;
137
+
138
+ if (currentSize > lastSize) {
139
+ const newFile = Bun.file(filePath);
140
+ const newContent = await newFile.text();
141
+ const newLines = newContent.slice(lastSize).trim().split("\n");
142
+
143
+ for (const line of newLines) {
144
+ if (!line.trim()) continue;
145
+
146
+ try {
147
+ const entry: LogEntry = JSON.parse(line);
148
+
149
+ if (!shouldDisplayEntry(entry, options)) {
150
+ continue;
151
+ }
152
+
153
+ const formatted = formatLogEntry(entry, { mode, useColor: true });
154
+
155
+ if (formatted.shouldDisplay && formatted.output) {
156
+ console.log(formatted.output);
157
+ }
158
+ } catch {
159
+ // Skip invalid JSON lines
160
+ }
161
+ }
162
+
163
+ lastSize = currentSize;
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Check if entry should be displayed based on filters
170
+ */
171
+ function shouldDisplayEntry(entry: LogEntry, options: { json?: boolean; story?: string; level?: LogLevel }): boolean {
172
+ if (options.story && entry.storyId !== options.story) {
173
+ return false;
174
+ }
175
+
176
+ if (options.level) {
177
+ const entryPriority = LOG_LEVEL_PRIORITY[entry.level];
178
+ const filterPriority = LOG_LEVEL_PRIORITY[options.level];
179
+
180
+ if (entryPriority < filterPriority) {
181
+ return false;
182
+ }
183
+ }
184
+
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Format duration in milliseconds
190
+ */
191
+ export function formatDuration(ms: number): string {
192
+ if (ms < 1000) {
193
+ return `${ms}ms`;
194
+ }
195
+ if (ms < 60000) {
196
+ return `${(ms / 1000).toFixed(1)}s`;
197
+ }
198
+ const minutes = Math.floor(ms / 60000);
199
+ const seconds = Math.floor((ms % 60000) / 1000);
200
+ return `${minutes}m${seconds}s`;
201
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Log reading and parsing utilities
3
+ */
4
+
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { readdir } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { LogEntry } from "../logger/types";
10
+ import type { MetaJson } from "../pipeline/subscribers/registry";
11
+
12
+ /**
13
+ * Swappable dependencies for testing
14
+ */
15
+ export const _deps = {
16
+ getRunsDir: () => process.env.NAX_RUNS_DIR ?? join(homedir(), ".nax", "runs"),
17
+ };
18
+
19
+ /**
20
+ * Resolve log file path for a runId from the central registry
21
+ */
22
+ export async function resolveRunFileFromRegistry(runId: string): Promise<string | null> {
23
+ const runsDir = _deps.getRunsDir();
24
+
25
+ let entries: string[];
26
+ try {
27
+ entries = await readdir(runsDir);
28
+ } catch {
29
+ throw new Error(`Run not found in registry: ${runId}`);
30
+ }
31
+
32
+ let matched: MetaJson | null = null;
33
+ for (const entry of entries) {
34
+ const metaPath = join(runsDir, entry, "meta.json");
35
+ try {
36
+ const meta: MetaJson = await Bun.file(metaPath).json();
37
+ if (meta.runId === runId || meta.runId.startsWith(runId)) {
38
+ matched = meta;
39
+ break;
40
+ }
41
+ } catch {
42
+ // skip unreadable meta.json entries
43
+ }
44
+ }
45
+
46
+ if (!matched) {
47
+ throw new Error(`Run not found in registry: ${runId}`);
48
+ }
49
+
50
+ if (!existsSync(matched.eventsDir)) {
51
+ console.log(`Log directory unavailable for run: ${runId}`);
52
+ return null;
53
+ }
54
+
55
+ const files = readdirSync(matched.eventsDir)
56
+ .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
57
+ .sort()
58
+ .reverse();
59
+
60
+ if (files.length === 0) {
61
+ console.log(`No log files found for run: ${runId}`);
62
+ return null;
63
+ }
64
+
65
+ const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
66
+ return join(matched.eventsDir, specificFile ?? files[0]);
67
+ }
68
+
69
+ /**
70
+ * Select latest run file from directory
71
+ */
72
+ export async function selectRunFile(runsDir: string): Promise<string | null> {
73
+ const files = readdirSync(runsDir)
74
+ .filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
75
+ .sort()
76
+ .reverse();
77
+
78
+ if (files.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ return join(runsDir, files[0]);
83
+ }
84
+
85
+ /**
86
+ * Extract run summary from log file
87
+ */
88
+ export async function extractRunSummary(filePath: string): Promise<{
89
+ total: number;
90
+ passed: number;
91
+ failed: number;
92
+ skipped: number;
93
+ durationMs: number;
94
+ totalCost: number;
95
+ startedAt: string;
96
+ completedAt: string | undefined;
97
+ } | null> {
98
+ const file = Bun.file(filePath);
99
+ const content = await file.text();
100
+ const lines = content.trim().split("\n");
101
+
102
+ let total = 0;
103
+ let passed = 0;
104
+ let failed = 0;
105
+ let skipped = 0;
106
+ let totalCost = 0;
107
+ let startedAt = "";
108
+ let completedAt: string | undefined;
109
+ let firstTimestamp = "";
110
+ let lastTimestamp = "";
111
+
112
+ for (const line of lines) {
113
+ if (!line.trim()) continue;
114
+
115
+ try {
116
+ const entry: LogEntry = JSON.parse(line);
117
+
118
+ if (!firstTimestamp) {
119
+ firstTimestamp = entry.timestamp;
120
+ }
121
+ lastTimestamp = entry.timestamp;
122
+
123
+ if (entry.stage === "run.start") {
124
+ startedAt = entry.timestamp;
125
+ const runData = entry.data as Record<string, unknown>;
126
+ total = typeof runData?.totalStories === "number" ? runData.totalStories : 0;
127
+ }
128
+
129
+ if (entry.stage === "story.complete" || entry.stage === "agent.complete") {
130
+ const data = entry.data as Record<string, unknown>;
131
+ const success = data?.success ?? true;
132
+ const action = data?.finalAction || data?.action;
133
+
134
+ if (success) {
135
+ passed++;
136
+ } else if (action === "skip") {
137
+ skipped++;
138
+ } else {
139
+ failed++;
140
+ }
141
+
142
+ if (data?.cost && typeof data.cost === "number") {
143
+ totalCost += data.cost;
144
+ }
145
+ }
146
+
147
+ if (entry.stage === "run.end") {
148
+ completedAt = entry.timestamp;
149
+ }
150
+ } catch {
151
+ // Skip invalid JSON lines
152
+ }
153
+ }
154
+
155
+ if (!startedAt) {
156
+ return null;
157
+ }
158
+
159
+ const durationMs = lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0;
160
+
161
+ return {
162
+ total,
163
+ passed,
164
+ failed,
165
+ skipped,
166
+ durationMs,
167
+ totalCost,
168
+ startedAt,
169
+ completedAt,
170
+ };
171
+ }