@nathapp/nax 0.22.1 → 0.22.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/CLAUDE.md +17 -0
- package/bin/nax.ts +3 -4
- package/docs/ROADMAP.md +54 -43
- package/docs/specs/central-run-registry.md +104 -0
- package/docs/specs/status-file-consolidation.md +93 -0
- package/nax/features/status-file-consolidation/prd.json +61 -0
- package/package.json +2 -2
- package/src/execution/lifecycle/run-setup.ts +1 -1
- package/src/execution/runner.ts +2 -2
- package/src/execution/status-writer.ts +4 -4
- package/src/routing/strategies/llm.ts +5 -2
- package/test/integration/execution/status-file-integration.test.ts +20 -57
- package/test/integration/execution/status-writer.test.ts +1 -12
package/CLAUDE.md
CHANGED
|
@@ -75,6 +75,23 @@ Runner.run() [src/execution/runner.ts — thin orchestrator only]
|
|
|
75
75
|
|
|
76
76
|
Detailed coding standards, test architecture, and forbidden patterns are in `.claude/rules/`. Claude Code loads these automatically.
|
|
77
77
|
|
|
78
|
+
|
|
79
|
+
## Code Intelligence (Solograph MCP)
|
|
80
|
+
|
|
81
|
+
Use **solograph** MCP tools on-demand for code understanding. Do not use web_search, kb_search, or source_* tools.
|
|
82
|
+
|
|
83
|
+
| Tool | When to use |
|
|
84
|
+
|:-----|:------------|
|
|
85
|
+
| `project_code_search` | Find existing patterns, symbols, or implementations before writing new code |
|
|
86
|
+
| `codegraph_explain` | Get architecture overview of nax before tackling unfamiliar areas |
|
|
87
|
+
| `codegraph_query` | Cypher queries — dependency analysis, impact analysis, hub files |
|
|
88
|
+
| `codegraph_stats` | Quick graph stats (file/symbol counts) |
|
|
89
|
+
| `codegraph_shared` | Find packages shared across projects |
|
|
90
|
+
| `session_search` | Search prior Claude Code session history for relevant context |
|
|
91
|
+
| `project_info` | Project registry info |
|
|
92
|
+
| `project_code_reindex` | Reindex after creating or deleting source files, or major refactors |
|
|
93
|
+
|
|
94
|
+
Single source of truth: VPS solograph instance (Mac01 tunnels to VPS — same data either way).
|
|
78
95
|
## IMPORTANT
|
|
79
96
|
|
|
80
97
|
- Do NOT push to remote — let the human review and push.
|
package/bin/nax.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { existsSync, mkdirSync } from "node:fs";
|
|
41
41
|
import { homedir } from "node:os";
|
|
42
|
-
import { join
|
|
42
|
+
import { join } from "node:path";
|
|
43
43
|
import chalk from "chalk";
|
|
44
44
|
import { Command } from "commander";
|
|
45
45
|
|
|
@@ -217,7 +217,6 @@ program
|
|
|
217
217
|
.option("--silent", "Silent mode (errors only)", false)
|
|
218
218
|
.option("--json", "JSON mode (raw JSONL output to stdout)", false)
|
|
219
219
|
.option("-d, --dir <path>", "Working directory", process.cwd())
|
|
220
|
-
.option("--status-file <path>", "Write machine-readable JSON status file (updated during run)")
|
|
221
220
|
.option("--skip-precheck", "Skip precheck validations (advanced users only)", false)
|
|
222
221
|
.action(async (options) => {
|
|
223
222
|
// Validate directory path
|
|
@@ -331,8 +330,8 @@ program
|
|
|
331
330
|
console.log(chalk.dim(" [Headless mode — pipe output]"));
|
|
332
331
|
}
|
|
333
332
|
|
|
334
|
-
//
|
|
335
|
-
const statusFilePath =
|
|
333
|
+
// Compute status file path: <workdir>/nax/status.json
|
|
334
|
+
const statusFilePath = join(workdir, "nax", "status.json");
|
|
336
335
|
|
|
337
336
|
// Parse --parallel option
|
|
338
337
|
let parallel: number | undefined;
|
package/docs/ROADMAP.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## v0.18.0 — Orchestration Quality ✅
|
|
10
10
|
|
|
11
11
|
**Theme:** Fix execution bugs and improve orchestration reliability
|
|
12
|
-
**Status:**
|
|
12
|
+
**Status:** ✅ Shipped (2026-03-03)
|
|
13
13
|
|
|
14
14
|
### Bugfixes (Priority)
|
|
15
15
|
- [x] ~~**BUG-016:** Hardcoded 120s timeout in verify stage → read from config~~
|
|
@@ -64,19 +64,8 @@
|
|
|
64
64
|
- [x] ~~Result: verify drops from ~125s to ~10-20s for typical single-file fixes~~
|
|
65
65
|
|
|
66
66
|
### Bun PTY Migration (BUN-001)
|
|
67
|
-
- [
|
|
68
|
-
|
|
69
|
-
- [ ] Update `src/tui/hooks/usePty.ts` — replace `IPty` interface with Bun equivalent
|
|
70
|
-
- [ ] Remove `node-pty` from `dependencies` in `package.json`
|
|
71
|
-
- [ ] Remove `--ignore-scripts` workaround from `.gitlab-ci.yml`
|
|
72
|
-
- [ ] Benefit: no native build, no gyp/python/gcc in CI, cleaner alpine support
|
|
73
|
-
|
|
74
|
-
### CI Memory Optimization (CI-001)
|
|
75
|
-
- [ ] Investigate splitting test suite into parallel jobs (unit / integration / ui) to reduce per-job peak memory
|
|
76
|
-
- [ ] Evaluate `bun test --shard` when stable (currently experimental)
|
|
77
|
-
- [ ] Target: make test suite pass on 1GB runners (currently requires 8GB shared runner)
|
|
78
|
-
- [ ] Known constraints: 2008 tests across 125 files, ~75s on local VPS (3.8GB), OOMs even with `--smol --concurrency 1`
|
|
79
|
-
- [ ] Current workaround: use `saas-linux-small-amd64` (8GB) shared runner
|
|
67
|
+
- [x] ~~Replace `node-pty` with `Bun.spawn` (piped stdio) — shipped in v0.18.5~~
|
|
68
|
+
|
|
80
69
|
|
|
81
70
|
---
|
|
82
71
|
|
|
@@ -125,12 +114,35 @@
|
|
|
125
114
|
**Spec:** [docs/specs/bun-pty-migration.md](specs/bun-pty-migration.md)
|
|
126
115
|
|
|
127
116
|
### BUN-001: Replace node-pty with Bun.spawn
|
|
128
|
-
- [x]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
- [x] ~~All sub-items complete — `claude.ts` + `usePty.ts` migrated to `Bun.spawn`, `node-pty` removed from `package.json`, CI cleaned up~~
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## v0.22.2 — Status File Consolidation
|
|
122
|
+
|
|
123
|
+
**Theme:** Auto-write status.json to well-known paths, align readers, remove dead options
|
|
124
|
+
**Status:** 🔲 Planned
|
|
125
|
+
**Spec:** [docs/specs/status-file-consolidation.md](specs/status-file-consolidation.md)
|
|
126
|
+
**Pre-requisite for:** v0.23.0 (Central Run Registry)
|
|
127
|
+
|
|
128
|
+
### Stories
|
|
129
|
+
- [ ] **SFC-001:** Auto-write project-level status — remove `--status-file` flag, always write to `<workdir>/nax/status.json`
|
|
130
|
+
- [ ] **SFC-002:** Write feature-level status on run end — copy final snapshot to `<workdir>/nax/features/<feature>/status.json`
|
|
131
|
+
- [ ] **SFC-003:** Align status readers — `nax status` + `nax diagnose` read from correct paths
|
|
132
|
+
- [ ] **SFC-004:** Clean up dead code — remove `--status-file` option, `.nax-status.json` references
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## v0.23.0 — Central Run Registry
|
|
137
|
+
|
|
138
|
+
**Theme:** Global run index across all projects — single source of truth for all nax run history
|
|
139
|
+
**Status:** 🔲 Planned
|
|
140
|
+
**Spec:** [docs/specs/central-run-registry.md](specs/central-run-registry.md)
|
|
141
|
+
|
|
142
|
+
### Stories
|
|
143
|
+
- [ ] **CRR-001:** `src/pipeline/subscribers/registry.ts` — `wireRegistry()` subscriber, listens to `run:started`, writes `~/.nax/runs/<project>-<feature>-<runId>/meta.json` (path pointers only — no data duplication, no symlinks)
|
|
144
|
+
- [ ] **CRR-002:** `src/commands/runs.ts` — `nax runs` CLI, reads `meta.json` → resolves live `status.json` from `statusPath`, displays table (project, feature, status, stories, duration, date). Filters: `--project`, `--last`, `--status`
|
|
145
|
+
- [ ] **CRR-003:** `nax logs --run <runId>` — resolve run from global registry via `eventsDir`, stream logs from any directory
|
|
134
146
|
|
|
135
147
|
---
|
|
136
148
|
|
|
@@ -148,25 +160,25 @@
|
|
|
148
160
|
- [x] ~~**BUG-041:**~~ Won't fix — superseded by FEAT-010
|
|
149
161
|
- [x] ~~**FEAT-012:**~~ Won't fix — balanced tier sufficient for test-writer
|
|
150
162
|
|
|
151
|
-
### → v0.22.
|
|
163
|
+
### → v0.22.1 Pipeline Re-Architecture ✅ Shipped (2026-03-07)
|
|
152
164
|
**ADR:** [docs/adr/ADR-005-pipeline-re-architecture.md](adr/ADR-005-pipeline-re-architecture.md)
|
|
153
165
|
**Plan:** [docs/adr/ADR-005-implementation-plan.md](adr/ADR-005-implementation-plan.md)
|
|
154
|
-
**Branch:** `feat/re-architecture`
|
|
155
166
|
|
|
156
|
-
**Theme:** Eliminate ad-hoc orchestration, consolidate 4 scattered verification paths into single orchestrator, add event-bus-driven hooks/plugins/interaction, new stages (rectify, autofix, regression).
|
|
167
|
+
**Theme:** Eliminate ad-hoc orchestration, consolidate 4 scattered verification paths into single orchestrator, add event-bus-driven hooks/plugins/interaction, new stages (rectify, autofix, regression), post-run pipeline SSOT.
|
|
157
168
|
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [ ] **Phase 4:** Delete deprecated files, simplify sequential executor to ~80 lines
|
|
169
|
+
- [x] **Phase 1:** VerificationOrchestrator + Pipeline Event Bus (additive, no behavior change)
|
|
170
|
+
- [x] **Phase 2:** New stages — `rectify`, `autofix`, `regression` + `retry` stage action
|
|
171
|
+
- [x] **Phase 3:** Event-bus subscribers for hooks, reporters, interaction (replace 20+ scattered call sites)
|
|
172
|
+
- [x] **Phase 5:** Post-run pipeline SSOT — `deferred-regression` stage, tier escalation into `iteration-runner`, `runAcceptanceLoop` → `runPipeline(postRunPipeline)`
|
|
163
173
|
|
|
164
|
-
**
|
|
165
|
-
- [
|
|
166
|
-
- [
|
|
167
|
-
- [
|
|
168
|
-
- [
|
|
169
|
-
- [
|
|
174
|
+
**Resolved:**
|
|
175
|
+
- [x] **BUG-040:** Lint/typecheck auto-repair → `autofix` stage + `quality.commands.lintFix/formatFix`
|
|
176
|
+
- [x] **BUG-042:** Verifier failure capture → unified `VerifyResult` with `failures[]` always populated
|
|
177
|
+
- [x] **FEAT-014:** Heartbeat observability → Pipeline Event Bus with typed events
|
|
178
|
+
- [x] **BUG-026:** Regression gate triggers full retry → targeted `rectify` stage with `retry` action
|
|
179
|
+
- [x] **BUG-028:** Routing cache ignores escalation tier → cache key includes tier
|
|
180
|
+
|
|
181
|
+
**Test results:** 2264 pass, 12 skip, 1 fail (pre-existing disk space flaky)
|
|
170
182
|
|
|
171
183
|
---
|
|
172
184
|
|
|
@@ -202,9 +214,6 @@
|
|
|
202
214
|
- [x] `priorFailures` injected into escalated agent prompts via `context/builder.ts`
|
|
203
215
|
- [x] Reverse file mapping for regression attribution
|
|
204
216
|
|
|
205
|
-
### Central Run Registry (carried forward)
|
|
206
|
-
- [ ] `~/.nax/runs/<project>-<feature>-<runId>/` with status.json + events.jsonl symlink
|
|
207
|
-
|
|
208
217
|
---
|
|
209
218
|
|
|
210
219
|
## Shipped
|
|
@@ -212,6 +221,7 @@
|
|
|
212
221
|
| Version | Theme | Date | Details |
|
|
213
222
|
|:---|:---|:---|:---|
|
|
214
223
|
| v0.18.1 | Type Safety + CI Pipeline | 2026-03-03 | 60 TS errors + 12 lint errors fixed, GitLab CI green (1952/56/0) |
|
|
224
|
+
| v0.22.1 | Pipeline Re-Architecture | 2026-03-07 | VerificationOrchestrator, EventBus, new stages (rectify/autofix/regression/deferred-regression), post-run SSOT. 2264 pass |
|
|
215
225
|
| v0.20.0 | Verification Architecture v2 | 2026-03-06 | Deferred regression gate, remove duplicate tests, BUG-037 |
|
|
216
226
|
| v0.19.0 | Hardening & Compliance | 2026-03-04 | SEC-1 to SEC-5, BUG-1, Node.js API removal, _deps rollout |
|
|
217
227
|
| v0.18.5 | Bun PTY Migration | 2026-03-04 | BUN-001: node-pty → Bun.spawn, CI cleanup, flaky test fix |
|
|
@@ -271,16 +281,17 @@
|
|
|
271
281
|
- [x] **BUG-032:** Routing stage overrides escalated `modelTier` with complexity-derived tier. `src/pipeline/stages/routing.ts:43` always runs `complexityToModelTier(routing.complexity, config)` even when `story.routing.modelTier` was explicitly set by `handleTierEscalation()`. BUG-026 was escalated to `balanced` (logged in iteration header), but `Task classified` shows `modelTier=fast` because `complexityToModelTier("simple", config)` → `"fast"`. Related to BUG-013 (escalation routing not applied) which was marked fixed, but the fix in `applyCachedRouting()` in `pipeline-result-handler.ts:295-310` runs **after** the routing stage — too late. **Location:** `src/pipeline/stages/routing.ts:43`. **Fix:** When `story.routing.modelTier` is explicitly set (by escalation), skip `complexityToModelTier()` and use the cached tier directly. Only derive from complexity when `story.routing.modelTier` is absent.
|
|
272
282
|
- [x] **BUG-033:** LLM routing has no retry on timeout — single attempt with hardcoded 15s default. All 5 LLM routing attempts in the v0.18.3 run timed out at 15s, forcing keyword fallback every time. `src/routing/strategies/llm.ts:63` reads `llmConfig?.timeoutMs ?? 15000` but there's no retry logic — one timeout = immediate fallback. **Location:** `src/routing/strategies/llm.ts:callLlm()`. **Fix:** Add `routing.llm.retries` config (default: 1) with backoff. Also surface `routing.llm.timeoutMs` in `nax config --explain` and consider raising default to 30s for batch routing which processes multiple stories.
|
|
273
283
|
|
|
274
|
-
- [
|
|
275
|
-
- [
|
|
284
|
+
- [x] ~~**BUG-037:** Test output summary (verify stage) captures precheck boilerplate instead of actual `bun test` failure. Fixed: `.slice(-20)` tail — shipped in v0.22.1 (re-arch phase 2).~~
|
|
285
|
+
- [x] ~~**BUG-038:** `smart-runner` over-matching when global defaults change. Fixed by FEAT-010 (v0.21.0) — per-attempt `storyGitRef` baseRef tracking; `git diff <baseRef>..HEAD` prevents cross-story file pollution.~~
|
|
276
286
|
### Features
|
|
277
287
|
- [x] ~~`nax unlock` command~~
|
|
278
288
|
- [x] ~~Constitution file support~~
|
|
279
289
|
- [x] ~~Per-story testStrategy override — v0.18.1~~
|
|
280
290
|
- [x] ~~Smart Test Runner — v0.18.2~~
|
|
281
|
-
- [
|
|
282
|
-
- [
|
|
291
|
+
- [ ] **Central Run Registry** — moved to v0.23.0
|
|
292
|
+
- [x] ~~**BUN-001:** Bun PTY Migration — replace `node-pty` with `Bun.spawn` (piped stdio). Shipped in v0.18.5.~~
|
|
283
293
|
- [ ] **CI-001:** CI Memory Optimization — parallel test sharding for 1GB runners
|
|
294
|
+
- [ ] **CI-001:** CI Memory Optimization — parallel test sharding to pass on 1GB runners (currently requires 8GB). Evaluate `bun test --shard` when stable.
|
|
284
295
|
- [ ] Cost tracking dashboard
|
|
285
296
|
- [ ] npm publish setup
|
|
286
297
|
- [ ] `nax diagnose --ai` flag (LLM-assisted, future TBD)
|
|
@@ -296,4 +307,4 @@ Sequential canary → stable: `v0.12.0-canary.0` → `canary.N` → `v0.12.0`
|
|
|
296
307
|
Canary: `npm publish --tag canary`
|
|
297
308
|
Stable: `npm publish` (latest)
|
|
298
309
|
|
|
299
|
-
*Last updated: 2026-03-
|
|
310
|
+
*Last updated: 2026-03-07 (v0.22.1 shipped — Pipeline Re-Architecture: VerificationOrchestrator, EventBus, new stages, post-run SSOT)*
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Central Run Registry — Spec
|
|
2
|
+
|
|
3
|
+
**Version:** v0.23.0
|
|
4
|
+
**Status:** Planned
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
nax stores run state per-project at `<workdir>/nax/features/<feature>/status.json`. There is no global index — you must `cd` into each project to see its run history. There is no way to answer "what has nax run across all my projects recently?"
|
|
11
|
+
|
|
12
|
+
## Existing Layout (per-project)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
<workdir>/nax/
|
|
16
|
+
config.json
|
|
17
|
+
features/
|
|
18
|
+
<feature>/
|
|
19
|
+
prd.json
|
|
20
|
+
status.json ← live run state (NaxStatusFile, written continuously)
|
|
21
|
+
runs/
|
|
22
|
+
<timestamp>.jsonl ← event log
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`status.json` already contains: runId, feature, status (running/completed/failed/crashed), progress counts, cost, current story, startedAt, etc. — everything needed for a global view.
|
|
26
|
+
|
|
27
|
+
## Goal
|
|
28
|
+
|
|
29
|
+
A global `~/.nax/runs/` registry that indexes every nax run via path references — no data duplication, no symlinks.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Directory Structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
~/.nax/runs/
|
|
37
|
+
<project>-<feature>-<runId>/
|
|
38
|
+
meta.json ← pointer record only (paths + minimal identifiers)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### meta.json Schema
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"runId": "run-2026-03-07T05-30-00-000Z",
|
|
46
|
+
"project": "my-app",
|
|
47
|
+
"feature": "auth-system",
|
|
48
|
+
"workdir": "/Users/william/projects/my-app",
|
|
49
|
+
"statusPath": "/Users/william/projects/my-app/nax/features/auth-system/status.json",
|
|
50
|
+
"eventsDir": "/Users/william/projects/my-app/nax/features/auth-system/runs",
|
|
51
|
+
"registeredAt": "2026-03-07T05:30:00.000Z"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- Written **once** on run start — never updated (source of truth stays in `statusPath`)
|
|
56
|
+
- `nax runs` reads `meta.json` to locate `statusPath`, then reads live `status.json` for current state
|
|
57
|
+
- If `statusPath` doesn't exist (project deleted/moved) → show `[unavailable]` gracefully
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Implementation
|
|
62
|
+
|
|
63
|
+
### CRR-001: Registry Writer (new subscriber)
|
|
64
|
+
|
|
65
|
+
- New module: `src/execution/run-registry.ts` — `registerRun(meta)`, `getRunsDir()`
|
|
66
|
+
- On run start: create `~/.nax/runs/<project>-<feature>-<runId>/meta.json`
|
|
67
|
+
- Wire as **event bus subscriber** (`wireRegistry()` in `src/pipeline/subscribers/registry.ts`) — listens to `run:started`
|
|
68
|
+
- Best-effort: never throw/block the main run on registry failure (try/catch + warn log)
|
|
69
|
+
- `~/.nax/runs/` created on first call — no separate init step
|
|
70
|
+
|
|
71
|
+
### CRR-002: `nax runs` CLI Command
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
nax runs # All runs, newest first (default: last 20)
|
|
75
|
+
nax runs --project my-app # Filter by project name
|
|
76
|
+
nax runs --last 50 # Show last N runs
|
|
77
|
+
nax runs --status failed # Filter by status (running/completed/failed/crashed)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Output table:**
|
|
81
|
+
```
|
|
82
|
+
RUN ID PROJECT FEATURE STATUS STORIES DURATION DATE
|
|
83
|
+
run-2026-03-07T05-30-00-000Z my-app auth-system completed 5/5 45m 2026-03-07 13:30
|
|
84
|
+
run-2026-03-07T04-00-00-000Z nax re-arch failed 3/5 1h 2m 2026-03-07 12:00
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Reads all `~/.nax/runs/*/meta.json`, resolves live `status.json` from `statusPath`
|
|
88
|
+
- Sorts by `registeredAt` desc
|
|
89
|
+
- If `statusPath` missing → status shows `[unavailable]`
|
|
90
|
+
- New command: `src/commands/runs.ts`
|
|
91
|
+
|
|
92
|
+
### CRR-003: `nax logs` Enhancement
|
|
93
|
+
|
|
94
|
+
- `nax logs --run <runId>` — resolve run from global registry, locate `eventsDir`, stream logs
|
|
95
|
+
- No need to be in the project directory
|
|
96
|
+
- Falls back to current behaviour (local feature context) when `--run` not specified
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Out of Scope
|
|
101
|
+
|
|
102
|
+
- Registry cleanup/prune command (future)
|
|
103
|
+
- Remote sync (future)
|
|
104
|
+
- Search by story ID (future)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Status File Consolidation — Spec
|
|
2
|
+
|
|
3
|
+
**Version:** v0.22.2
|
|
4
|
+
**Status:** Planned
|
|
5
|
+
**Pre-requisite for:** v0.23.0 (Central Run Registry)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
StatusWriter only writes `status.json` when the `--status-file` CLI flag is explicitly passed. Without it, no status file is written. Additionally, `nax status` and `nax diagnose` read from different (non-existent) paths, creating a three-way disconnect.
|
|
12
|
+
|
|
13
|
+
### Current State
|
|
14
|
+
|
|
15
|
+
| Component | Path | Exists? |
|
|
16
|
+
|-----------|------|---------|
|
|
17
|
+
| StatusWriter (writer) | `--status-file <path>` (opt-in) | Only if flag passed |
|
|
18
|
+
| `nax status` (reader) | `nax/features/<feature>/status.json` | ❌ Never written |
|
|
19
|
+
| `nax diagnose` (reader) | `<workdir>/.nax-status.json` | ❌ Legacy path |
|
|
20
|
+
| Actual file on disk | `nax/status.json` | Only from manual flag usage |
|
|
21
|
+
|
|
22
|
+
## Goal
|
|
23
|
+
|
|
24
|
+
Auto-write status files to well-known paths. Zero config, zero flags. Both project-level and feature-level status always available.
|
|
25
|
+
|
|
26
|
+
### Target State
|
|
27
|
+
|
|
28
|
+
| File | Written | Purpose |
|
|
29
|
+
|------|---------|---------|
|
|
30
|
+
| `<workdir>/nax/status.json` | Continuously during run | Live monitoring: "is nax running? which feature? cost?" |
|
|
31
|
+
| `<workdir>/nax/features/<feature>/status.json` | Once at run end | Historical: "what was the last run result for this feature?" |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Stories
|
|
36
|
+
|
|
37
|
+
### SFC-001: Auto-write project-level status
|
|
38
|
+
|
|
39
|
+
**What:** Remove `--status-file` CLI option. StatusWriter always writes to `<workdir>/nax/status.json` automatically.
|
|
40
|
+
|
|
41
|
+
**Changes:**
|
|
42
|
+
- `bin/nax.ts` — remove `--status-file` option, compute `statusFile = join(workdir, "nax", "status.json")` automatically
|
|
43
|
+
- `src/execution/runner.ts` — `statusFile` no longer optional in `RunOptions`, always provided
|
|
44
|
+
- `src/execution/status-writer.ts` — remove the `if (!this.statusFile)` guard in `update()` (statusFile is always set)
|
|
45
|
+
- `src/execution/lifecycle/run-setup.ts` — statusFile always provided
|
|
46
|
+
|
|
47
|
+
**Test:** Run nax without `--status-file` flag → verify `nax/status.json` is written with correct schema.
|
|
48
|
+
|
|
49
|
+
### SFC-002: Write feature-level status on run end
|
|
50
|
+
|
|
51
|
+
**What:** On run complete/fail/crash, copy the final status snapshot to `<workdir>/nax/features/<feature>/status.json`.
|
|
52
|
+
|
|
53
|
+
**Changes:**
|
|
54
|
+
- `src/execution/status-writer.ts` — add `writeFeatureStatus(featureDir: string)` method that writes current snapshot to `<featureDir>/status.json`
|
|
55
|
+
- `src/execution/runner.ts` — call `statusWriter.writeFeatureStatus(featureDir)` in the finally block (after run completes, fails, or crashes)
|
|
56
|
+
- `src/execution/crash-recovery.ts` — also write feature status on crash
|
|
57
|
+
|
|
58
|
+
**Test:** After a completed run, verify `nax/features/<feature>/status.json` exists with `status: "completed"` or `"failed"`.
|
|
59
|
+
|
|
60
|
+
### SFC-003: Align status readers
|
|
61
|
+
|
|
62
|
+
**What:** Make `nax status` and `nax diagnose` read from the correct paths.
|
|
63
|
+
|
|
64
|
+
**Changes:**
|
|
65
|
+
- `src/cli/status-features.ts` — `loadStatusFile()` already reads from `<featureDir>/status.json` (correct after SFC-002 writes there). No change needed for feature-level.
|
|
66
|
+
- `src/cli/status-features.ts` — add project-level status display: read `nax/status.json` to show "currently running" info at the top of `nax status` output
|
|
67
|
+
- `src/cli/diagnose.ts` — change `.nax-status.json` → `nax/status.json`
|
|
68
|
+
|
|
69
|
+
**Test:** `nax status` shows current run info from project-level status + per-feature historical info. `nax diagnose` correctly detects running/stalled/crashed state.
|
|
70
|
+
|
|
71
|
+
### SFC-004: Clean up dead code
|
|
72
|
+
|
|
73
|
+
**What:** Remove deprecated paths and dead options.
|
|
74
|
+
|
|
75
|
+
**Changes:**
|
|
76
|
+
- `bin/nax.ts` — remove `--status-file` option definition and `statusFilePath` resolve logic
|
|
77
|
+
- `src/cli/diagnose.ts` — remove `.nax-status.json` path reference
|
|
78
|
+
- `src/execution/runner.ts` — remove `statusFile?` optional from `RunOptions` type (now required, auto-computed)
|
|
79
|
+
|
|
80
|
+
**Test:** Verify no references to `.nax-status.json` or `--status-file` remain in codebase.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Schema (unchanged)
|
|
85
|
+
|
|
86
|
+
The `NaxStatusFile` interface in `src/execution/status-file.ts` is already correct. No schema changes needed — both project-level and feature-level files use the same `NaxStatusFile` type.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Out of Scope
|
|
91
|
+
|
|
92
|
+
- Central Run Registry (`~/.nax/runs/`) — v0.23.0
|
|
93
|
+
- Status file cleanup/rotation — future
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "nax",
|
|
3
|
+
"branchName": "feat/status-file-consolidation",
|
|
4
|
+
"feature": "status-file-consolidation",
|
|
5
|
+
"version": "0.22.2",
|
|
6
|
+
"description": "Auto-write status.json to well-known paths (project-level + feature-level). Remove --status-file CLI flag. Align nax status and nax diagnose readers.",
|
|
7
|
+
"userStories": [
|
|
8
|
+
{
|
|
9
|
+
"id": "SFC-001",
|
|
10
|
+
"title": "Auto-write project-level status",
|
|
11
|
+
"description": "Remove --status-file CLI option. StatusWriter always writes to <workdir>/nax/status.json automatically. In bin/nax.ts, remove --status-file option and compute statusFile = join(workdir, 'nax', 'status.json'). In runner.ts, statusFile is no longer optional. In status-writer.ts, remove the if (!this.statusFile) guard in update().",
|
|
12
|
+
"complexity": "medium",
|
|
13
|
+
"status": "passed",
|
|
14
|
+
"acceptanceCriteria": [
|
|
15
|
+
"Running nax without --status-file flag writes nax/status.json automatically",
|
|
16
|
+
"nax/status.json contains valid NaxStatusFile schema with run.id, run.status, progress counts",
|
|
17
|
+
"--status-file CLI option no longer exists",
|
|
18
|
+
"StatusWriter.update() always writes (no no-op guard on missing statusFile)"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "SFC-002",
|
|
23
|
+
"title": "Write feature-level status on run end",
|
|
24
|
+
"description": "On run complete/fail/crash, write the final status snapshot to <workdir>/nax/features/<feature>/status.json. Add writeFeatureStatus(featureDir) method to StatusWriter. Call it in runner.ts finally block and in crash-recovery.ts.",
|
|
25
|
+
"complexity": "medium",
|
|
26
|
+
"status": "pending",
|
|
27
|
+
"acceptanceCriteria": [
|
|
28
|
+
"After a completed run, nax/features/<feature>/status.json exists with status 'completed'",
|
|
29
|
+
"After a failed run, nax/features/<feature>/status.json exists with status 'failed'",
|
|
30
|
+
"After a crash, nax/features/<feature>/status.json exists with status 'crashed'",
|
|
31
|
+
"Feature status.json uses the same NaxStatusFile schema as project-level"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "SFC-003",
|
|
36
|
+
"title": "Align status readers",
|
|
37
|
+
"description": "Make nax status read project-level status from nax/status.json for currently running info. Make nax diagnose read from nax/status.json instead of .nax-status.json. status-features.ts loadStatusFile() already reads <featureDir>/status.json which SFC-002 now writes \u2014 no change needed for feature-level reads.",
|
|
38
|
+
"complexity": "simple",
|
|
39
|
+
"status": "pending",
|
|
40
|
+
"acceptanceCriteria": [
|
|
41
|
+
"nax status shows current run info from nax/status.json at the top",
|
|
42
|
+
"nax status shows per-feature historical status from nax/features/<feature>/status.json",
|
|
43
|
+
"nax diagnose reads from nax/status.json (not .nax-status.json)",
|
|
44
|
+
"No references to .nax-status.json remain in codebase"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "SFC-004",
|
|
49
|
+
"title": "Clean up dead code",
|
|
50
|
+
"description": "Remove --status-file option definition from bin/nax.ts. Remove .nax-status.json path from diagnose.ts. Remove statusFile optional from RunOptions type (now required, auto-computed). Verify no stale references remain.",
|
|
51
|
+
"complexity": "simple",
|
|
52
|
+
"status": "pending",
|
|
53
|
+
"acceptanceCriteria": [
|
|
54
|
+
"No references to --status-file CLI option in codebase",
|
|
55
|
+
"No references to .nax-status.json in codebase",
|
|
56
|
+
"RunOptions.statusFile is required (not optional)",
|
|
57
|
+
"All existing tests pass"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
package/package.json
CHANGED
package/src/execution/runner.ts
CHANGED
|
@@ -47,8 +47,8 @@ export interface RunOptions {
|
|
|
47
47
|
parallel?: number;
|
|
48
48
|
/** Optional event emitter for TUI integration */
|
|
49
49
|
eventEmitter?: PipelineEventEmitter;
|
|
50
|
-
/** Path to write a machine-readable JSON status file
|
|
51
|
-
statusFile
|
|
50
|
+
/** Path to write a machine-readable JSON status file */
|
|
51
|
+
statusFile: string;
|
|
52
52
|
/** Path to JSONL log file (for crash recovery) */
|
|
53
53
|
logFilePath?: string;
|
|
54
54
|
/** Formatter verbosity mode for headless stdout (default: "normal") */
|
|
@@ -50,7 +50,7 @@ export interface StatusWriterContext {
|
|
|
50
50
|
* await sw.update(totalCost, iterations);
|
|
51
51
|
*/
|
|
52
52
|
export class StatusWriter {
|
|
53
|
-
private readonly statusFile: string
|
|
53
|
+
private readonly statusFile: string;
|
|
54
54
|
private readonly costLimit: number | null;
|
|
55
55
|
private readonly ctx: StatusWriterContext;
|
|
56
56
|
|
|
@@ -60,7 +60,7 @@ export class StatusWriter {
|
|
|
60
60
|
private _currentStory: RunStateSnapshot["currentStory"] = null;
|
|
61
61
|
private _consecutiveWriteFailures = 0; // BUG-2: Track consecutive write failures
|
|
62
62
|
|
|
63
|
-
constructor(statusFile: string
|
|
63
|
+
constructor(statusFile: string, config: NaxConfig, ctx: StatusWriterContext) {
|
|
64
64
|
this.statusFile = statusFile;
|
|
65
65
|
this.costLimit = config.execution.costLimit === Number.POSITIVE_INFINITY ? null : config.execution.costLimit;
|
|
66
66
|
this.ctx = ctx;
|
|
@@ -107,7 +107,7 @@ export class StatusWriter {
|
|
|
107
107
|
/**
|
|
108
108
|
* Write the current status to disk (atomic via .tmp + rename).
|
|
109
109
|
*
|
|
110
|
-
* No-ops if
|
|
110
|
+
* No-ops if _prd has not been set.
|
|
111
111
|
* On failure, logs a warning/error and increments the BUG-2 failure counter.
|
|
112
112
|
* Counter resets to 0 on next successful write.
|
|
113
113
|
*
|
|
@@ -116,7 +116,7 @@ export class StatusWriter {
|
|
|
116
116
|
* @param overrides - Optional partial snapshot overrides (spread last)
|
|
117
117
|
*/
|
|
118
118
|
async update(totalCost: number, iterations: number, overrides: Partial<RunStateSnapshot> = {}): Promise<void> {
|
|
119
|
-
if (!this.
|
|
119
|
+
if (!this._prd) return;
|
|
120
120
|
const safeLogger = getSafeLogger();
|
|
121
121
|
try {
|
|
122
122
|
const base = this.getSnapshot(totalCost, iterations);
|
|
@@ -116,15 +116,18 @@ async function callLlmOnce(modelTier: string, prompt: string, config: NaxConfig,
|
|
|
116
116
|
return result;
|
|
117
117
|
} catch (err) {
|
|
118
118
|
clearTimeout(timeoutId);
|
|
119
|
+
// Silence the floating outputPromise — after kill() the proc exits non-zero,
|
|
120
|
+
// causing outputPromise to throw. Without this, it becomes an unhandled rejection.
|
|
121
|
+
outputPromise.catch(() => {});
|
|
119
122
|
try {
|
|
120
123
|
proc.stdout.cancel();
|
|
121
124
|
} catch {
|
|
122
|
-
// ignore cancel errors
|
|
125
|
+
// ignore cancel errors — stream may already be locked by Response
|
|
123
126
|
}
|
|
124
127
|
try {
|
|
125
128
|
proc.stderr.cancel();
|
|
126
129
|
} catch {
|
|
127
|
-
// ignore cancel errors
|
|
130
|
+
// ignore cancel errors — stream may already be locked by Response
|
|
128
131
|
}
|
|
129
132
|
proc.kill();
|
|
130
133
|
throw err;
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* Integration Tests: Status File — runner + CLI (T2)
|
|
4
4
|
*
|
|
5
5
|
* Verifies:
|
|
6
|
-
* - RunOptions.statusFile
|
|
7
|
-
* - Status file written at all 4 write points (dry-run path)
|
|
8
|
-
* - Status file NOT written when statusFile omitted
|
|
6
|
+
* - RunOptions.statusFile: string is required
|
|
7
|
+
* - Status file always written at all 4 write points (dry-run path)
|
|
9
8
|
* - Valid JSON at each stage, NaxStatusFile schema correct
|
|
10
9
|
* - completed status, progress counts, null current at end
|
|
11
|
-
* -
|
|
10
|
+
* - CLI automatically computes statusFile to <workdir>/nax/status.json
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
@@ -53,13 +52,13 @@ class MockAgentAdapter implements AgentAdapter {
|
|
|
53
52
|
return [this.binary];
|
|
54
53
|
}
|
|
55
54
|
async run(_o: AgentRunOptions): Promise<AgentResult> {
|
|
56
|
-
return { success: true, exitCode: 0, output: "", durationMs: 10, estimatedCost: 0 };
|
|
55
|
+
return { success: true, exitCode: 0, output: "", durationMs: 10, estimatedCost: 0, rateLimited: false };
|
|
57
56
|
}
|
|
58
57
|
async plan(_o: PlanOptions): Promise<PlanResult> {
|
|
59
|
-
return { specContent: "# Feature\n"
|
|
58
|
+
return { specContent: "# Feature\n" };
|
|
60
59
|
}
|
|
61
60
|
async decompose(_o: DecomposeOptions): Promise<DecomposeResult> {
|
|
62
|
-
return { stories: []
|
|
61
|
+
return { stories: [] };
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -143,7 +142,7 @@ async function runWithStatus(feature: string, storyCount = 1, extraOpts: Partial
|
|
|
143
142
|
// RunOptions type-level checks
|
|
144
143
|
// ============================================================================
|
|
145
144
|
describe("RunOptions.statusFile", () => {
|
|
146
|
-
it("is
|
|
145
|
+
it("is required", () => {
|
|
147
146
|
const opts: RunOptions = {
|
|
148
147
|
prdPath: "/tmp/prd.json",
|
|
149
148
|
workdir: "/tmp",
|
|
@@ -151,52 +150,25 @@ describe("RunOptions.statusFile", () => {
|
|
|
151
150
|
hooks: { hooks: {} },
|
|
152
151
|
feature: "test",
|
|
153
152
|
dryRun: true,
|
|
153
|
+
statusFile: "/tmp/nax/status.json",
|
|
154
154
|
};
|
|
155
|
-
expect(opts.statusFile).
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("accepts a string value", () => {
|
|
159
|
-
const opts: RunOptions = {
|
|
160
|
-
prdPath: "/tmp/prd.json",
|
|
161
|
-
workdir: "/tmp",
|
|
162
|
-
config: createTestConfig(),
|
|
163
|
-
hooks: { hooks: {} },
|
|
164
|
-
feature: "test",
|
|
165
|
-
dryRun: true,
|
|
166
|
-
statusFile: "/tmp/nax-status.json",
|
|
167
|
-
};
|
|
168
|
-
expect(opts.statusFile).toBe("/tmp/nax-status.json");
|
|
155
|
+
expect(opts.statusFile).toBe("/tmp/nax/status.json");
|
|
169
156
|
});
|
|
170
157
|
});
|
|
171
158
|
|
|
172
159
|
// ============================================================================
|
|
173
|
-
//
|
|
160
|
+
// Status file is always written when provided
|
|
174
161
|
// ============================================================================
|
|
175
|
-
describe("status file
|
|
162
|
+
describe("status file always written when provided", () => {
|
|
176
163
|
let tmpDir: string;
|
|
177
164
|
afterEach(async () => {
|
|
178
165
|
if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true });
|
|
179
166
|
});
|
|
180
167
|
|
|
181
|
-
it("
|
|
182
|
-
const setup = await
|
|
168
|
+
it("writes status file to provided path during dry-run", async () => {
|
|
169
|
+
const { setup, statusFilePath } = await runWithStatus("sf-always-written", 1);
|
|
183
170
|
tmpDir = setup.tmpDir;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await run({
|
|
187
|
-
prdPath: setup.prdPath,
|
|
188
|
-
workdir: setup.tmpDir,
|
|
189
|
-
config: createTestConfig(),
|
|
190
|
-
hooks: { hooks: {} },
|
|
191
|
-
feature: "no-sf",
|
|
192
|
-
featureDir: setup.featureDir,
|
|
193
|
-
dryRun: true, // no statusFile
|
|
194
|
-
skipPrecheck: true,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const after = await fs.readdir(setup.tmpDir);
|
|
198
|
-
const newJson = after.filter((f) => f.endsWith(".json") && !before.includes(f));
|
|
199
|
-
expect(newJson).toHaveLength(0);
|
|
171
|
+
expect(nodeFs.existsSync(statusFilePath)).toBe(true);
|
|
200
172
|
});
|
|
201
173
|
});
|
|
202
174
|
|
|
@@ -299,28 +271,19 @@ describe("status file written during dry-run", () => {
|
|
|
299
271
|
});
|
|
300
272
|
|
|
301
273
|
// ============================================================================
|
|
302
|
-
// CLI
|
|
274
|
+
// CLI status file wiring (type check only)
|
|
303
275
|
// ============================================================================
|
|
304
|
-
describe("CLI
|
|
305
|
-
it("RunOptions.statusFile is
|
|
306
|
-
const
|
|
307
|
-
prdPath: "/tmp/prd.json",
|
|
308
|
-
workdir: "/tmp",
|
|
309
|
-
config: createTestConfig(),
|
|
310
|
-
hooks: { hooks: {} },
|
|
311
|
-
feature: "test",
|
|
312
|
-
dryRun: false,
|
|
313
|
-
statusFile: "/tmp/status.json",
|
|
314
|
-
};
|
|
315
|
-
const withoutFile: RunOptions = {
|
|
276
|
+
describe("CLI auto-computed status file", () => {
|
|
277
|
+
it("RunOptions.statusFile is required and always provided", () => {
|
|
278
|
+
const opts: RunOptions = {
|
|
316
279
|
prdPath: "/tmp/prd.json",
|
|
317
280
|
workdir: "/tmp",
|
|
318
281
|
config: createTestConfig(),
|
|
319
282
|
hooks: { hooks: {} },
|
|
320
283
|
feature: "test",
|
|
321
284
|
dryRun: false,
|
|
285
|
+
statusFile: "/tmp/nax/status.json",
|
|
322
286
|
};
|
|
323
|
-
expect(
|
|
324
|
-
expect(withoutFile.statusFile).toBeUndefined();
|
|
287
|
+
expect(opts.statusFile).toBe("/tmp/nax/status.json");
|
|
325
288
|
});
|
|
326
289
|
});
|
|
@@ -77,14 +77,10 @@ function makeCtx(overrides: Partial<StatusWriterContext> = {}): StatusWriterCont
|
|
|
77
77
|
// ============================================================================
|
|
78
78
|
|
|
79
79
|
describe("StatusWriter construction", () => {
|
|
80
|
-
test("constructs without error
|
|
80
|
+
test("constructs without error with statusFile path", () => {
|
|
81
81
|
expect(() => new StatusWriter("/tmp/status.json", makeConfig(), makeCtx())).not.toThrow();
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test("constructs without error when statusFile is undefined (no-op mode)", () => {
|
|
85
|
-
expect(() => new StatusWriter(undefined, makeConfig(), makeCtx())).not.toThrow();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
84
|
test("costLimit Infinity → stored as null in snapshot", async () => {
|
|
89
85
|
const dir = await mkdtemp(join(tmpdir(), "sw-test-"));
|
|
90
86
|
const path = join(dir, "status.json");
|
|
@@ -214,13 +210,6 @@ describe("StatusWriter.getSnapshot", () => {
|
|
|
214
210
|
// ============================================================================
|
|
215
211
|
|
|
216
212
|
describe("StatusWriter.update no-op guards", () => {
|
|
217
|
-
test("no-op when statusFile is undefined (even with prd set)", async () => {
|
|
218
|
-
const sw = new StatusWriter(undefined, makeConfig(), makeCtx());
|
|
219
|
-
sw.setPrd(makePrd());
|
|
220
|
-
// Should not throw
|
|
221
|
-
await expect(sw.update(0, 0)).resolves.toBeUndefined();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
213
|
test("no-op when prd not yet set", async () => {
|
|
225
214
|
const dir = await mkdtemp(join(tmpdir(), "sw-test-"));
|
|
226
215
|
const path = join(dir, "status.json");
|