@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.
- package/dist/nax.js +3258 -2894
- package/package.json +4 -1
- 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/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 +448 -0
- package/src/config/schema-types.ts +53 -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/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 +2 -1
- package/src/plugins/types.ts +16 -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
|
@@ -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
|
+
}
|