@nathapp/nax 0.49.0 → 0.49.3
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/CHANGELOG.md +22 -0
- package/README.md +280 -10
- package/dist/nax.js +85 -24
- package/package.json +1 -1
- package/src/acceptance/generator.ts +4 -1
- package/src/agents/acp/adapter.ts +7 -2
- package/src/agents/acp/parser.ts +1 -1
- package/src/agents/acp/spawn-client.ts +2 -0
- package/src/agents/types.ts +6 -0
- package/src/cli/plan.ts +11 -1
- package/src/config/test-strategy.ts +4 -4
- package/src/execution/iteration-runner.ts +1 -1
- package/src/execution/pipeline-result-handler.ts +4 -1
- package/src/execution/story-selector.ts +2 -1
- package/src/pipeline/stages/autofix.ts +26 -7
- package/src/pipeline/stages/routing.ts +21 -2
- package/src/review/runner.ts +15 -0
- package/src/utils/git.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.49.3] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Autofix `recheckReview` bug:** `reviewStage.execute()` returns `action:"continue"` for both pass AND built-in-check-failure (to hand off to autofix). Using `result.action === "continue"` always returned `true`, causing "Mechanical autofix succeeded" to log every cycle and looping until `MAX_STAGE_RETRIES` with no real fix. Fix: check `ctx.reviewResult?.success` directly after execute.
|
|
12
|
+
- **Autofix selective mechanical fix:** `lintFix`/`formatFix` cannot fix typecheck errors. Phase 1 now only runs when the `lint` check actually failed. Typecheck-only failures skip straight to agent rectification (Phase 2).
|
|
13
|
+
- **Review command logging:** `runner.ts` now logs the resolved command and workdir for every check at info level, and full output on failure at warn level — eliminates phantom failure mystery.
|
|
14
|
+
- **Re-decompose on second run:** Batch-mode story selector was missing `"decomposed"` in its status skip list (single-story path already excluded it). Stories with `status: "decomposed"` were being picked up again, triggering unnecessary LLM decompose calls. Added `"decomposed"` to batch filter and a guard in routing SD-004 block.
|
|
15
|
+
- **totalCost always 0:** `handlePipelineFailure` returned no `costDelta`; `iteration-runner` hardcoded `costDelta: 0` for failures. Agent cost for failed stories was silently dropped. Fix: extract `agentResult?.estimatedCost` in failure path same as success path.
|
|
16
|
+
|
|
17
|
+
## [0.49.2] - 2026-03-18
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Test strategy descriptions:** `TEST_STRATEGY_GUIDE` (used in plan and decompose prompts) had incorrect descriptions for `three-session-tdd` and `three-session-tdd-lite`. Both strategies use 3 sessions. Key distinction: `three-session-tdd` (strict) — test-writer makes no src/ changes, implementer makes no test changes; `three-session-tdd-lite` (lite) — test-writer may add minimal src/ stubs, implementer may expand coverage and replace stubs. Updated in `src/config/test-strategy.ts`, `docs/specs/test-strategy-ssot.md`, and `docs/architecture/ARCHITECTURE.md`.
|
|
21
|
+
|
|
22
|
+
## [0.49.1] - 2026-03-18
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **ACP zero cost:** `acpx prompt` was called without `--format json`, causing it to output plain text instead of JSON-RPC NDJSON. Cost and token usage were always 0. Fix: pass `--format json` as a global flag so the parser receives `usage_update` (exact cost in USD) and `result.usage` (token breakdown).
|
|
26
|
+
- **Decompose session name / model:** Decompose one-shots used an auto-generated timestamp session name and passed the tier string (`"balanced"`) as the model instead of the resolved model ID. Fix: session name is now `nax-decompose-<story-id>` and model tier is resolved via `resolveModel()` before the `complete()` call.
|
|
27
|
+
- **`autoCommitIfDirty` skipping monorepo subdirs:** The working-directory guard rejected any workdir that wasn't exactly the git root, silently skipping commits for monorepo package subdirs. Fix: allow subdirs (`startsWith(gitRoot + '/')`); use `git add .` for subdirs vs `git add -A` at root.
|
|
28
|
+
- **`complete()` missing model in `generateFromPRD()` and `plan` auto mode:** `generator.ts` ignored `options.modelDef.model`; `plan.ts` auto path didn't call `resolveModel()`. Both now pass the correct resolved model to `adapter.complete()`.
|
|
29
|
+
|
|
8
30
|
## [0.46.2] - 2026-03-17
|
|
9
31
|
|
|
10
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -18,8 +18,16 @@ bun install -g @nathapp/nax
|
|
|
18
18
|
cd your-project
|
|
19
19
|
nax init
|
|
20
20
|
nax features create my-feature
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
# Option A: write prd.json manually, then run
|
|
23
|
+
nax run -f my-feature
|
|
24
|
+
|
|
25
|
+
# Option B: generate prd.json from a spec file, then run
|
|
26
|
+
nax plan -f my-feature --from spec.md
|
|
22
27
|
nax run -f my-feature
|
|
28
|
+
|
|
29
|
+
# Option C: plan + run in one command
|
|
30
|
+
nax run -f my-feature --plan --from spec.md
|
|
23
31
|
```
|
|
24
32
|
|
|
25
33
|
## How It Works
|
|
@@ -54,6 +62,14 @@ nax/
|
|
|
54
62
|
└── features/ # One folder per feature
|
|
55
63
|
```
|
|
56
64
|
|
|
65
|
+
**Monorepo — scaffold a package:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nax init --package packages/api
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Creates `packages/api/nax/context.md` for per-package agent context.
|
|
72
|
+
|
|
57
73
|
---
|
|
58
74
|
|
|
59
75
|
### `nax features create <name>`
|
|
@@ -76,20 +92,33 @@ nax features list
|
|
|
76
92
|
|
|
77
93
|
---
|
|
78
94
|
|
|
79
|
-
### `nax
|
|
95
|
+
### `nax plan -f <name> --from <spec>`
|
|
80
96
|
|
|
81
|
-
|
|
97
|
+
Generate a `prd.json` from a spec file using an LLM. Replaces the deprecated `nax analyze`.
|
|
82
98
|
|
|
83
99
|
```bash
|
|
84
|
-
nax
|
|
100
|
+
nax plan -f my-feature --from spec.md
|
|
85
101
|
```
|
|
86
102
|
|
|
87
103
|
**Flags:**
|
|
88
104
|
|
|
89
105
|
| Flag | Description |
|
|
90
106
|
|:-----|:------------|
|
|
91
|
-
|
|
|
92
|
-
| `--
|
|
107
|
+
| `-f, --feature <name>` | Feature name (required) |
|
|
108
|
+
| `--from <spec-path>` | Path to spec file (required) |
|
|
109
|
+
| `--auto` / `--one-shot` | Skip interactive Q&A — single LLM call, no back-and-forth |
|
|
110
|
+
| `-b, --branch <branch>` | Override default branch name |
|
|
111
|
+
| `-d, --dir <path>` | Project directory |
|
|
112
|
+
|
|
113
|
+
**Interactive vs one-shot:**
|
|
114
|
+
- Default (no flag): interactive planning session — nax asks clarifying questions, refines the plan iteratively
|
|
115
|
+
- `--auto` / `--one-shot`: single LLM call, faster but less precise
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### `nax analyze` *(deprecated)*
|
|
120
|
+
|
|
121
|
+
> ⚠️ **Deprecated.** Use `nax plan` instead. `nax analyze` remains available for backward compatibility but will be removed in a future version.
|
|
93
122
|
|
|
94
123
|
---
|
|
95
124
|
|
|
@@ -105,10 +134,23 @@ nax run -f my-feature
|
|
|
105
134
|
|
|
106
135
|
| Flag | Description |
|
|
107
136
|
|:-----|:------------|
|
|
108
|
-
| `-f, --feature <name>` | Feature name
|
|
137
|
+
| `-f, --feature <name>` | Feature name |
|
|
138
|
+
| `-a, --agent <name>` | Force a specific agent (`claude`, `opencode`, `codex`, etc.) |
|
|
139
|
+
| `--plan` | Run plan phase first (requires `--from`) |
|
|
140
|
+
| `--from <spec-path>` | Spec file for `--plan` |
|
|
141
|
+
| `--one-shot` | Skip interactive Q&A during planning (ACP only) |
|
|
142
|
+
| `--force` | Overwrite existing `prd.json` when using `--plan` |
|
|
143
|
+
| `--parallel <n>` | Max parallel sessions (`0` = auto based on CPU cores; omit = sequential) |
|
|
109
144
|
| `--dry-run` | Preview story routing without running agents |
|
|
110
145
|
| `--headless` | Non-interactive output (structured logs, no TUI) |
|
|
111
|
-
|
|
|
146
|
+
| `--verbose` | Debug-level logging |
|
|
147
|
+
| `--quiet` | Warnings and errors only |
|
|
148
|
+
| `--silent` | Errors only |
|
|
149
|
+
| `--json` | Raw JSONL output to stdout (for scripting) |
|
|
150
|
+
| `--skip-precheck` | Skip precheck validations (advanced users only) |
|
|
151
|
+
| `--no-context` | Disable context builder (skip file context in prompts) |
|
|
152
|
+
| `--no-batch` | Execute all stories individually (disable batching) |
|
|
153
|
+
| `-d, --dir <path>` | Working directory |
|
|
112
154
|
|
|
113
155
|
**Examples:**
|
|
114
156
|
|
|
@@ -116,11 +158,23 @@ nax run -f my-feature
|
|
|
116
158
|
# Preview what would run (no agents spawned)
|
|
117
159
|
nax run -f user-auth --dry-run
|
|
118
160
|
|
|
119
|
-
#
|
|
120
|
-
nax run -f user-auth
|
|
161
|
+
# Plan from spec then run — one command
|
|
162
|
+
nax run -f user-auth --plan --from spec.md
|
|
163
|
+
|
|
164
|
+
# Run with parallel execution (auto concurrency)
|
|
165
|
+
nax run -f user-auth --parallel 0
|
|
166
|
+
|
|
167
|
+
# Run with up to 3 parallel worktree sessions
|
|
168
|
+
nax run -f user-auth --parallel 3
|
|
169
|
+
|
|
170
|
+
# Force a specific agent
|
|
171
|
+
nax run -f user-auth --agent opencode
|
|
121
172
|
|
|
122
173
|
# Run in CI/CD (structured output)
|
|
123
174
|
nax run -f user-auth --headless
|
|
175
|
+
|
|
176
|
+
# Raw JSONL for scripting
|
|
177
|
+
nax run -f user-auth --json
|
|
124
178
|
```
|
|
125
179
|
|
|
126
180
|
---
|
|
@@ -199,6 +253,58 @@ Output sections:
|
|
|
199
253
|
|
|
200
254
|
---
|
|
201
255
|
|
|
256
|
+
### `nax generate`
|
|
257
|
+
|
|
258
|
+
Generate agent config files from `nax/context.md`. Supports Claude Code, OpenCode, Codex, Cursor, Windsurf, Aider, and Gemini.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
nax generate
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Flags:**
|
|
265
|
+
|
|
266
|
+
| Flag | Description |
|
|
267
|
+
|:-----|:------------|
|
|
268
|
+
| `-c, --context <path>` | Context file path (default: `nax/context.md`) |
|
|
269
|
+
| `-o, --output <dir>` | Output directory (default: project root) |
|
|
270
|
+
| `-a, --agent <name>` | Generate for a specific agent only (`claude`, `opencode`, `cursor`, `windsurf`, `aider`, `codex`, `gemini`) |
|
|
271
|
+
| `--dry-run` | Preview without writing files |
|
|
272
|
+
| `--no-auto-inject` | Disable auto-injection of project metadata |
|
|
273
|
+
| `--package <dir>` | Generate for a specific monorepo package (e.g. `packages/api`) |
|
|
274
|
+
| `--all-packages` | Generate for all discovered packages |
|
|
275
|
+
|
|
276
|
+
**What it generates:**
|
|
277
|
+
|
|
278
|
+
| Agent | File |
|
|
279
|
+
|:------|:-----|
|
|
280
|
+
| Claude Code | `CLAUDE.md` |
|
|
281
|
+
| OpenCode | `AGENTS.md` |
|
|
282
|
+
| Codex | `AGENTS.md` |
|
|
283
|
+
| Cursor | `.cursorrules` |
|
|
284
|
+
| Windsurf | `.windsurfrules` |
|
|
285
|
+
| Aider | `.aider.md` |
|
|
286
|
+
| Gemini | `GEMINI.md` |
|
|
287
|
+
|
|
288
|
+
**Workflow:**
|
|
289
|
+
|
|
290
|
+
1. Create `nax/context.md` — describe your project's architecture, conventions, and coding standards
|
|
291
|
+
2. Run `nax generate` — writes agent config files to the project root (and per-package if configured)
|
|
292
|
+
3. Commit the generated files — your agents will automatically pick them up
|
|
293
|
+
|
|
294
|
+
**Monorepo (per-package):**
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# Generate CLAUDE.md for a single package
|
|
298
|
+
nax generate --package packages/api
|
|
299
|
+
|
|
300
|
+
# Generate for all packages (auto-discovers workspace packages)
|
|
301
|
+
nax generate --all-packages
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Each package can have its own `nax/context.md` at `<package>/nax/context.md` for package-specific agent instructions.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
202
308
|
### `nax prompts -f <name>`
|
|
203
309
|
|
|
204
310
|
Assemble and display the prompt that would be sent to the agent for each story role.
|
|
@@ -439,6 +545,170 @@ If the regression gate detects failures, nax maps them to the responsible story
|
|
|
439
545
|
|
|
440
546
|
---
|
|
441
547
|
|
|
548
|
+
## Parallel Execution
|
|
549
|
+
|
|
550
|
+
nax can run multiple stories concurrently using git worktrees — each story gets an isolated worktree so agents don't step on each other.
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
# Auto concurrency (based on CPU cores)
|
|
554
|
+
nax run -f my-feature --parallel 0
|
|
555
|
+
|
|
556
|
+
# Fixed concurrency
|
|
557
|
+
nax run -f my-feature --parallel 3
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**How it works:**
|
|
561
|
+
|
|
562
|
+
1. Stories are grouped by dependency order (dependent stories wait for their prerequisites)
|
|
563
|
+
2. Each batch of independent stories gets its own git worktree
|
|
564
|
+
3. Agent sessions run concurrently inside those worktrees
|
|
565
|
+
4. Once a batch completes, changes are merged back in dependency order
|
|
566
|
+
5. Merge conflicts are automatically rectified by re-running the conflicted story on the updated base
|
|
567
|
+
|
|
568
|
+
**Config:**
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"execution": {
|
|
573
|
+
"maxParallelSessions": 4
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
> Sequential mode (no `--parallel`) is the safe default. Use parallel for large feature sets with independent stories.
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Agents
|
|
583
|
+
|
|
584
|
+
nax supports multiple coding agents. By default it uses Claude Code via the ACP protocol.
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
# List installed agents and their capabilities
|
|
588
|
+
nax agents
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Supported agents:**
|
|
592
|
+
|
|
593
|
+
| Agent | Protocol | Notes |
|
|
594
|
+
|:------|:---------|:------|
|
|
595
|
+
| `claude` | ACP (default) | Claude Code via acpx |
|
|
596
|
+
| `opencode` | ACP | OpenCode via acpx |
|
|
597
|
+
| `codex` | ACP | Codex via acpx |
|
|
598
|
+
| `cursor` | ACP | Cursor via acpx |
|
|
599
|
+
| `windsurf` | ACP | Windsurf via acpx |
|
|
600
|
+
| `aider` | ACP | Aider via acpx |
|
|
601
|
+
| `gemini` | ACP | Gemini CLI via acpx |
|
|
602
|
+
|
|
603
|
+
**ACP protocol (default):**
|
|
604
|
+
|
|
605
|
+
nax uses [acpx](https://github.com/nathapp/acpx) as the ACP transport. All agents run as persistent sessions — nax sends prompts and receives structured JSON-RPC responses including token counts and exact USD cost per session.
|
|
606
|
+
|
|
607
|
+
**Configuring agents:**
|
|
608
|
+
|
|
609
|
+
```json
|
|
610
|
+
{
|
|
611
|
+
"execution": {
|
|
612
|
+
"defaultAgent": "claude",
|
|
613
|
+
"protocol": "acp",
|
|
614
|
+
"fallbackOrder": ["claude", "codex", "opencode", "gemini"]
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Force a specific agent at runtime:**
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
nax run -f my-feature --agent opencode
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Monorepo Support
|
|
628
|
+
|
|
629
|
+
nax supports monorepos with workspace-level and per-package configuration.
|
|
630
|
+
|
|
631
|
+
### Setup
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
# Initialize nax at the repo root
|
|
635
|
+
nax init
|
|
636
|
+
|
|
637
|
+
# Scaffold per-package context for a specific package
|
|
638
|
+
nax init --package packages/api
|
|
639
|
+
nax init --package packages/web
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Per-Package Config
|
|
643
|
+
|
|
644
|
+
Each package can override specific config fields by placing a `nax/config.json` inside the package directory:
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
repo-root/
|
|
648
|
+
├── nax/
|
|
649
|
+
│ └── config.json # root config
|
|
650
|
+
├── packages/
|
|
651
|
+
│ ├── api/
|
|
652
|
+
│ │ └── nax/
|
|
653
|
+
│ │ ├── config.json # overrides for api package
|
|
654
|
+
│ │ └── context.md # agent context for api
|
|
655
|
+
│ └── web/
|
|
656
|
+
│ └── nax/
|
|
657
|
+
│ ├── config.json # overrides for web package
|
|
658
|
+
│ └── context.md # agent context for web
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Overridable fields per package:** `execution`, `review`, `acceptance`, `quality`, `context`
|
|
662
|
+
|
|
663
|
+
```json
|
|
664
|
+
// packages/api/nax/config.json
|
|
665
|
+
{
|
|
666
|
+
"quality": {
|
|
667
|
+
"commands": {
|
|
668
|
+
"test": "turbo test --filter=@myapp/api",
|
|
669
|
+
"lint": "turbo lint --filter=@myapp/api"
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Per-Package Stories
|
|
676
|
+
|
|
677
|
+
In your `prd.json`, set `workdir` on each story to point to the package:
|
|
678
|
+
|
|
679
|
+
```json
|
|
680
|
+
{
|
|
681
|
+
"userStories": [
|
|
682
|
+
{
|
|
683
|
+
"id": "US-001",
|
|
684
|
+
"title": "Add auth endpoint",
|
|
685
|
+
"workdir": "packages/api",
|
|
686
|
+
"status": "pending"
|
|
687
|
+
}
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
nax will run the agent inside that package's directory and apply its config overrides automatically.
|
|
693
|
+
|
|
694
|
+
### Workspace Detection
|
|
695
|
+
|
|
696
|
+
When `nax plan` generates stories for a monorepo, it auto-discovers packages from:
|
|
697
|
+
- `turbo.json` → `packages` field
|
|
698
|
+
- `package.json` → `workspaces`
|
|
699
|
+
- `pnpm-workspace.yaml` → `packages`
|
|
700
|
+
- Existing `*/nax/context.md` files
|
|
701
|
+
|
|
702
|
+
### Generate Agent Files for All Packages
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
nax generate --all-packages
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Generates a `CLAUDE.md` (or agent-specific file) in each discovered package directory, using the package's own `nax/context.md` if present.
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
442
712
|
## Hooks
|
|
443
713
|
|
|
444
714
|
Integrate notifications, CI triggers, or custom scripts via lifecycle hooks.
|
package/dist/nax.js
CHANGED
|
@@ -3267,10 +3267,10 @@ Security-critical functions (authentication, cryptography, tokens, sessions, cre
|
|
|
3267
3267
|
password hashing, access control) must be classified at MINIMUM "medium" complexity
|
|
3268
3268
|
regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
3269
3269
|
|
|
3270
|
-
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
3271
|
-
- tdd-simple: Medium complexity. Write
|
|
3272
|
-
- three-session-tdd: Complex stories.
|
|
3273
|
-
- three-session-tdd-lite: Expert/high-risk stories.
|
|
3270
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation in a single session.
|
|
3271
|
+
- tdd-simple: Medium complexity. Write failing tests first, then implement to pass them \u2014 all in one session.
|
|
3272
|
+
- three-session-tdd: Complex stories. 3 sessions: (1) test-writer writes failing tests \u2014 no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
|
|
3273
|
+
- three-session-tdd-lite: Expert/high-risk stories. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may add missing coverage or replace stubs, (3) verifier confirms correctness.`, GROUPING_RULES = `## Grouping Rules
|
|
3274
3274
|
|
|
3275
3275
|
- Combine small, related tasks into a single "simple" or "medium" story.
|
|
3276
3276
|
- Do NOT create separate stories for every single file or function unless complex.
|
|
@@ -18729,7 +18729,10 @@ describe("${options.featureName} - Acceptance Tests", () => {
|
|
|
18729
18729
|
|
|
18730
18730
|
IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
|
|
18731
18731
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
18732
|
-
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
18732
|
+
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
18733
|
+
model: options.modelDef.model,
|
|
18734
|
+
config: options.config
|
|
18735
|
+
});
|
|
18733
18736
|
const testCode = extractTestCode(rawOutput);
|
|
18734
18737
|
const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
|
|
18735
18738
|
acId: `AC-${i + 1}`,
|
|
@@ -19170,6 +19173,8 @@ class SpawnAcpSession {
|
|
|
19170
19173
|
"acpx",
|
|
19171
19174
|
"--cwd",
|
|
19172
19175
|
this.cwd,
|
|
19176
|
+
"--format",
|
|
19177
|
+
"json",
|
|
19173
19178
|
...this.permissionMode === "approve-all" ? ["--approve-all"] : [],
|
|
19174
19179
|
"--model",
|
|
19175
19180
|
this.model,
|
|
@@ -19742,7 +19747,11 @@ class AcpAgentAdapter {
|
|
|
19742
19747
|
await client.start();
|
|
19743
19748
|
let session = null;
|
|
19744
19749
|
try {
|
|
19745
|
-
session = await client.createSession({
|
|
19750
|
+
session = await client.createSession({
|
|
19751
|
+
agentName: this.name,
|
|
19752
|
+
permissionMode,
|
|
19753
|
+
sessionName: _options?.sessionName
|
|
19754
|
+
});
|
|
19746
19755
|
let timeoutId;
|
|
19747
19756
|
const timeoutPromise = new Promise((_, reject) => {
|
|
19748
19757
|
timeoutId = setTimeout(() => reject(new Error(`complete() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
@@ -22241,7 +22250,7 @@ var package_default;
|
|
|
22241
22250
|
var init_package = __esm(() => {
|
|
22242
22251
|
package_default = {
|
|
22243
22252
|
name: "@nathapp/nax",
|
|
22244
|
-
version: "0.49.
|
|
22253
|
+
version: "0.49.3",
|
|
22245
22254
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22246
22255
|
type: "module",
|
|
22247
22256
|
bin: {
|
|
@@ -22314,8 +22323,8 @@ var init_version = __esm(() => {
|
|
|
22314
22323
|
NAX_VERSION = package_default.version;
|
|
22315
22324
|
NAX_COMMIT = (() => {
|
|
22316
22325
|
try {
|
|
22317
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22318
|
-
return "
|
|
22326
|
+
if (/^[0-9a-f]{6,10}$/.test("30ff375"))
|
|
22327
|
+
return "30ff375";
|
|
22319
22328
|
} catch {}
|
|
22320
22329
|
try {
|
|
22321
22330
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -24348,6 +24357,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir) {
|
|
|
24348
24357
|
}
|
|
24349
24358
|
async function runCheck(check2, command, workdir) {
|
|
24350
24359
|
const startTime = Date.now();
|
|
24360
|
+
const logger = getSafeLogger();
|
|
24361
|
+
logger?.info("review", `Running ${check2} check`, { check: check2, command, workdir });
|
|
24351
24362
|
try {
|
|
24352
24363
|
const parts = command.split(/\s+/);
|
|
24353
24364
|
const executable = parts[0];
|
|
@@ -24386,6 +24397,17 @@ async function runCheck(check2, command, workdir) {
|
|
|
24386
24397
|
const stderr = await new Response(proc.stderr).text();
|
|
24387
24398
|
const output = [stdout, stderr].filter(Boolean).join(`
|
|
24388
24399
|
`);
|
|
24400
|
+
if (exitCode !== 0) {
|
|
24401
|
+
logger?.warn("review", `${check2} check failed`, {
|
|
24402
|
+
check: check2,
|
|
24403
|
+
command,
|
|
24404
|
+
workdir,
|
|
24405
|
+
exitCode,
|
|
24406
|
+
output: output.slice(0, 2000)
|
|
24407
|
+
});
|
|
24408
|
+
} else {
|
|
24409
|
+
logger?.debug("review", `${check2} check passed`, { check: check2, command, durationMs: Date.now() - startTime });
|
|
24410
|
+
}
|
|
24389
24411
|
return {
|
|
24390
24412
|
check: check2,
|
|
24391
24413
|
command,
|
|
@@ -24671,8 +24693,8 @@ async function recheckReview(ctx) {
|
|
|
24671
24693
|
const { reviewStage: reviewStage2 } = await Promise.resolve().then(() => (init_review(), exports_review));
|
|
24672
24694
|
if (!reviewStage2.enabled(ctx))
|
|
24673
24695
|
return true;
|
|
24674
|
-
|
|
24675
|
-
return
|
|
24696
|
+
await reviewStage2.execute(ctx);
|
|
24697
|
+
return ctx.reviewResult?.success === true;
|
|
24676
24698
|
}
|
|
24677
24699
|
function collectFailedChecks(ctx) {
|
|
24678
24700
|
return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
|
|
@@ -24784,11 +24806,18 @@ var init_autofix = __esm(() => {
|
|
|
24784
24806
|
const lintFixCmd = effectiveConfig.quality.commands.lintFix;
|
|
24785
24807
|
const formatFixCmd = effectiveConfig.quality.commands.formatFix;
|
|
24786
24808
|
const effectiveWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
24787
|
-
|
|
24809
|
+
const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
|
|
24810
|
+
const hasLintFailure = failedCheckNames.has("lint");
|
|
24811
|
+
logger.info("autofix", "Starting autofix", {
|
|
24812
|
+
storyId: ctx.story.id,
|
|
24813
|
+
failedChecks: [...failedCheckNames],
|
|
24814
|
+
workdir: effectiveWorkdir
|
|
24815
|
+
});
|
|
24816
|
+
if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
|
|
24788
24817
|
if (lintFixCmd) {
|
|
24789
24818
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
|
|
24790
24819
|
const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
|
|
24791
|
-
logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id });
|
|
24820
|
+
logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
|
|
24792
24821
|
if (lintResult.exitCode !== 0) {
|
|
24793
24822
|
logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
|
|
24794
24823
|
storyId: ctx.story.id,
|
|
@@ -24799,7 +24828,10 @@ var init_autofix = __esm(() => {
|
|
|
24799
24828
|
if (formatFixCmd) {
|
|
24800
24829
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
|
|
24801
24830
|
const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
|
|
24802
|
-
logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
|
|
24831
|
+
logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
|
|
24832
|
+
storyId: ctx.story.id,
|
|
24833
|
+
command: formatFixCmd
|
|
24834
|
+
});
|
|
24803
24835
|
if (fmtResult.exitCode !== 0) {
|
|
24804
24836
|
logger.warn("autofix", "formatFix command failed \u2014 may not have fixed all issues", {
|
|
24805
24837
|
storyId: ctx.story.id,
|
|
@@ -24810,11 +24842,12 @@ var init_autofix = __esm(() => {
|
|
|
24810
24842
|
const recheckPassed = await _autofixDeps.recheckReview(ctx);
|
|
24811
24843
|
pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
|
|
24812
24844
|
if (recheckPassed) {
|
|
24813
|
-
if (ctx.reviewResult)
|
|
24814
|
-
ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
24815
24845
|
logger.info("autofix", "Mechanical autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
|
|
24816
24846
|
return { action: "retry", fromStage: "review" };
|
|
24817
24847
|
}
|
|
24848
|
+
logger.info("autofix", "Mechanical autofix did not resolve all failures \u2014 proceeding to agent rectification", {
|
|
24849
|
+
storyId: ctx.story.id
|
|
24850
|
+
});
|
|
24818
24851
|
}
|
|
24819
24852
|
const agentFixed = await _autofixDeps.runAgentRectification(ctx);
|
|
24820
24853
|
if (agentFixed) {
|
|
@@ -26178,7 +26211,9 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
|
26178
26211
|
return gitRoot;
|
|
26179
26212
|
}
|
|
26180
26213
|
})();
|
|
26181
|
-
|
|
26214
|
+
const isAtRoot = realWorkdir === realGitRoot;
|
|
26215
|
+
const isSubdir = realGitRoot && realWorkdir.startsWith(`${realGitRoot}/`);
|
|
26216
|
+
if (!isAtRoot && !isSubdir)
|
|
26182
26217
|
return;
|
|
26183
26218
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
26184
26219
|
cwd: workdir,
|
|
@@ -26195,7 +26230,8 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
|
26195
26230
|
dirtyFiles: statusOutput.trim().split(`
|
|
26196
26231
|
`).length
|
|
26197
26232
|
});
|
|
26198
|
-
const
|
|
26233
|
+
const addArgs = isSubdir ? ["git", "add", "."] : ["git", "add", "-A"];
|
|
26234
|
+
const addProc = _gitDeps.spawn(addArgs, { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
26199
26235
|
await addProc.exited;
|
|
26200
26236
|
const commitProc = _gitDeps.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
26201
26237
|
cwd: workdir,
|
|
@@ -29440,9 +29476,24 @@ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
|
|
|
29440
29476
|
if (!agent) {
|
|
29441
29477
|
throw new Error(`[decompose] Agent "${config2.autoMode.defaultAgent}" not found \u2014 cannot decompose`);
|
|
29442
29478
|
}
|
|
29479
|
+
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
29480
|
+
let decomposeModel;
|
|
29481
|
+
try {
|
|
29482
|
+
const { resolveModel: resolveModel2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
29483
|
+
const models = config2.models;
|
|
29484
|
+
const entry = models[decomposeTier] ?? models.balanced;
|
|
29485
|
+
if (entry)
|
|
29486
|
+
decomposeModel = resolveModel2(entry).model;
|
|
29487
|
+
} catch {}
|
|
29488
|
+
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
29443
29489
|
const adapter = {
|
|
29444
29490
|
async decompose(prompt) {
|
|
29445
|
-
return agent.complete(prompt, {
|
|
29491
|
+
return agent.complete(prompt, {
|
|
29492
|
+
model: decomposeModel,
|
|
29493
|
+
jsonMode: true,
|
|
29494
|
+
config: config2,
|
|
29495
|
+
sessionName: storySessionName
|
|
29496
|
+
});
|
|
29446
29497
|
}
|
|
29447
29498
|
};
|
|
29448
29499
|
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
|
@@ -29526,7 +29577,7 @@ var init_routing2 = __esm(() => {
|
|
|
29526
29577
|
logger.debug("routing", ctx.routing.reasoning);
|
|
29527
29578
|
}
|
|
29528
29579
|
const decomposeConfig = ctx.config.decompose;
|
|
29529
|
-
if (decomposeConfig) {
|
|
29580
|
+
if (decomposeConfig && ctx.story.status !== "decomposed") {
|
|
29530
29581
|
const acCount = ctx.story.acceptanceCriteria.length;
|
|
29531
29582
|
const complexity = ctx.routing.complexity;
|
|
29532
29583
|
const isOversized = acCount > decomposeConfig.maxAcceptanceCriteria && (complexity === "complex" || complexity === "expert");
|
|
@@ -34229,6 +34280,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
|
|
|
34229
34280
|
const logger = getSafeLogger();
|
|
34230
34281
|
let prd = ctx.prd;
|
|
34231
34282
|
let prdDirty = false;
|
|
34283
|
+
const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
|
|
34232
34284
|
switch (pipelineResult.finalAction) {
|
|
34233
34285
|
case "pause":
|
|
34234
34286
|
markStoryPaused(prd, ctx.story.id);
|
|
@@ -34295,7 +34347,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
|
|
|
34295
34347
|
break;
|
|
34296
34348
|
}
|
|
34297
34349
|
}
|
|
34298
|
-
return { prd, prdDirty };
|
|
34350
|
+
return { prd, prdDirty, costDelta };
|
|
34299
34351
|
}
|
|
34300
34352
|
var init_pipeline_result_handler = __esm(() => {
|
|
34301
34353
|
init_logger2();
|
|
@@ -34400,7 +34452,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
34400
34452
|
return {
|
|
34401
34453
|
prd: r.prd,
|
|
34402
34454
|
storiesCompletedDelta: 0,
|
|
34403
|
-
costDelta:
|
|
34455
|
+
costDelta: r.costDelta,
|
|
34404
34456
|
prdDirty: r.prdDirty,
|
|
34405
34457
|
finalAction: pipelineResult.finalAction,
|
|
34406
34458
|
reason: pipelineResult.reason
|
|
@@ -34438,7 +34490,7 @@ function buildPreviewRouting(story, config2) {
|
|
|
34438
34490
|
function selectNextStories(prd, config2, batchPlan, currentBatchIndex, lastStoryId, useBatch) {
|
|
34439
34491
|
if (useBatch && currentBatchIndex < batchPlan.length) {
|
|
34440
34492
|
const batch = batchPlan[currentBatchIndex];
|
|
34441
|
-
const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused");
|
|
34493
|
+
const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed");
|
|
34442
34494
|
if (storiesToExecute.length === 0) {
|
|
34443
34495
|
return { selection: null, nextBatchIndex: currentBatchIndex + 1 };
|
|
34444
34496
|
}
|
|
@@ -67305,7 +67357,16 @@ async function planCommand(workdir, config2, options) {
|
|
|
67305
67357
|
const cliAdapter = _deps2.getAgent(agentName);
|
|
67306
67358
|
if (!cliAdapter)
|
|
67307
67359
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
67308
|
-
|
|
67360
|
+
let autoModel;
|
|
67361
|
+
try {
|
|
67362
|
+
const planTier = config2?.plan?.model ?? "balanced";
|
|
67363
|
+
const { resolveModel: resolveModel2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
67364
|
+
const models = config2?.models;
|
|
67365
|
+
const entry = models?.[planTier] ?? models?.balanced;
|
|
67366
|
+
if (entry)
|
|
67367
|
+
autoModel = resolveModel2(entry).model;
|
|
67368
|
+
} catch {}
|
|
67369
|
+
rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config: config2 });
|
|
67309
67370
|
try {
|
|
67310
67371
|
const envelope = JSON.parse(rawResponse);
|
|
67311
67372
|
if (envelope?.type === "result" && typeof envelope?.result === "string") {
|
package/package.json
CHANGED
|
@@ -108,7 +108,10 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
|
|
|
108
108
|
|
|
109
109
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
110
110
|
|
|
111
|
-
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
111
|
+
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
112
|
+
model: options.modelDef.model,
|
|
113
|
+
config: options.config,
|
|
114
|
+
});
|
|
112
115
|
const testCode = extractTestCode(rawOutput);
|
|
113
116
|
|
|
114
117
|
const refinedJsonContent = JSON.stringify(
|
|
@@ -694,8 +694,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
694
694
|
|
|
695
695
|
let session: AcpSession | null = null;
|
|
696
696
|
try {
|
|
697
|
-
// complete() is one-shot — ephemeral session, no
|
|
698
|
-
|
|
697
|
+
// complete() is one-shot — ephemeral session, no sidecar
|
|
698
|
+
// Use caller-provided sessionName if available (aids tracing), otherwise timestamp-based
|
|
699
|
+
session = await client.createSession({
|
|
700
|
+
agentName: this.name,
|
|
701
|
+
permissionMode,
|
|
702
|
+
sessionName: _options?.sessionName,
|
|
703
|
+
});
|
|
699
704
|
|
|
700
705
|
// Enforce timeout via Promise.race — session.prompt() can hang indefinitely
|
|
701
706
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
package/src/agents/acp/parser.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function parseAcpxJsonOutput(rawOutput: string): {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Final result with token breakdown
|
|
73
|
+
// Final result with token breakdown
|
|
74
74
|
if (event.id !== undefined && event.result && typeof event.result === "object") {
|
|
75
75
|
const result = event.result as Record<string, unknown>;
|
|
76
76
|
|
package/src/agents/types.ts
CHANGED
|
@@ -127,6 +127,12 @@ export interface CompleteOptions {
|
|
|
127
127
|
* Pass when available so complete() honours permissionProfile / dangerouslySkipPermissions.
|
|
128
128
|
*/
|
|
129
129
|
config?: NaxConfig;
|
|
130
|
+
/**
|
|
131
|
+
* Named session to use for this completion call.
|
|
132
|
+
* If omitted, a timestamp-based ephemeral session name is generated.
|
|
133
|
+
* Pass a meaningful name (e.g. "nax-decompose-us-001") to aid debugging.
|
|
134
|
+
*/
|
|
135
|
+
sessionName?: string;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
package/src/cli/plan.ts
CHANGED
|
@@ -138,7 +138,17 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
138
138
|
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails);
|
|
139
139
|
const cliAdapter = _deps.getAgent(agentName);
|
|
140
140
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
141
|
-
|
|
141
|
+
let autoModel: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const planTier = config?.plan?.model ?? "balanced";
|
|
144
|
+
const { resolveModel } = await import("../config/schema");
|
|
145
|
+
const models = config?.models as Record<string, unknown> | undefined;
|
|
146
|
+
const entry = models?.[planTier] ?? models?.balanced;
|
|
147
|
+
if (entry) autoModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
148
|
+
} catch {
|
|
149
|
+
// fall through — complete() will use its own fallback
|
|
150
|
+
}
|
|
151
|
+
rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config });
|
|
142
152
|
// CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
|
|
143
153
|
try {
|
|
144
154
|
const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
|
|
@@ -53,10 +53,10 @@ regardless of LOC count. These require at minimum "tdd-simple" test strategy.`;
|
|
|
53
53
|
|
|
54
54
|
export const TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
55
55
|
|
|
56
|
-
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
57
|
-
- tdd-simple: Medium complexity. Write
|
|
58
|
-
- three-session-tdd: Complex stories.
|
|
59
|
-
- three-session-tdd-lite: Expert/high-risk stories.
|
|
56
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation in a single session.
|
|
57
|
+
- tdd-simple: Medium complexity. Write failing tests first, then implement to pass them — all in one session.
|
|
58
|
+
- three-session-tdd: Complex stories. 3 sessions: (1) test-writer writes failing tests — no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
|
|
59
|
+
- three-session-tdd-lite: Expert/high-risk stories. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may add missing coverage or replace stubs, (3) verifier confirms correctness.`;
|
|
60
60
|
|
|
61
61
|
export const GROUPING_RULES = `## Grouping Rules
|
|
62
62
|
|
|
@@ -102,6 +102,7 @@ export async function handlePipelineSuccess(
|
|
|
102
102
|
export interface PipelineFailureResult {
|
|
103
103
|
prd: PRD;
|
|
104
104
|
prdDirty: boolean;
|
|
105
|
+
costDelta: number;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export async function handlePipelineFailure(
|
|
@@ -111,6 +112,8 @@ export async function handlePipelineFailure(
|
|
|
111
112
|
const logger = getSafeLogger();
|
|
112
113
|
let prd = ctx.prd;
|
|
113
114
|
let prdDirty = false;
|
|
115
|
+
// Always capture cost even for failed stories — agent ran and spent tokens
|
|
116
|
+
const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
|
|
114
117
|
|
|
115
118
|
switch (pipelineResult.finalAction) {
|
|
116
119
|
case "pause":
|
|
@@ -185,5 +188,5 @@ export async function handlePipelineFailure(
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
return { prd, prdDirty };
|
|
191
|
+
return { prd, prdDirty, costDelta };
|
|
189
192
|
}
|
|
@@ -61,12 +61,22 @@ export const autofixStage: PipelineStage = {
|
|
|
61
61
|
// Effective workdir for running commands (scoped to package if monorepo)
|
|
62
62
|
const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
64
|
+
// Identify which checks failed
|
|
65
|
+
const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
|
|
66
|
+
const hasLintFailure = failedCheckNames.has("lint");
|
|
67
|
+
|
|
68
|
+
logger.info("autofix", "Starting autofix", {
|
|
69
|
+
storyId: ctx.story.id,
|
|
70
|
+
failedChecks: [...failedCheckNames],
|
|
71
|
+
workdir: effectiveWorkdir,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Phase 1: Mechanical fix — only for lint failures (lintFix/formatFix cannot fix typecheck errors)
|
|
75
|
+
if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
|
|
66
76
|
if (lintFixCmd) {
|
|
67
77
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
|
|
68
78
|
const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
|
|
69
|
-
logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id });
|
|
79
|
+
logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
|
|
70
80
|
if (lintResult.exitCode !== 0) {
|
|
71
81
|
logger.warn("autofix", "lintFix command failed — may not have fixed all issues", {
|
|
72
82
|
storyId: ctx.story.id,
|
|
@@ -78,7 +88,10 @@ export const autofixStage: PipelineStage = {
|
|
|
78
88
|
if (formatFixCmd) {
|
|
79
89
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
|
|
80
90
|
const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
|
|
81
|
-
logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
|
|
91
|
+
logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
|
|
92
|
+
storyId: ctx.story.id,
|
|
93
|
+
command: formatFixCmd,
|
|
94
|
+
});
|
|
82
95
|
if (fmtResult.exitCode !== 0) {
|
|
83
96
|
logger.warn("autofix", "formatFix command failed — may not have fixed all issues", {
|
|
84
97
|
storyId: ctx.story.id,
|
|
@@ -91,10 +104,13 @@ export const autofixStage: PipelineStage = {
|
|
|
91
104
|
pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
|
|
92
105
|
|
|
93
106
|
if (recheckPassed) {
|
|
94
|
-
if (ctx.reviewResult) ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
95
107
|
logger.info("autofix", "Mechanical autofix succeeded — retrying review", { storyId: ctx.story.id });
|
|
96
108
|
return { action: "retry", fromStage: "review" };
|
|
97
109
|
}
|
|
110
|
+
|
|
111
|
+
logger.info("autofix", "Mechanical autofix did not resolve all failures — proceeding to agent rectification", {
|
|
112
|
+
storyId: ctx.story.id,
|
|
113
|
+
});
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
// Phase 2: Agent rectification — spawn agent with review error context
|
|
@@ -134,8 +150,11 @@ async function recheckReview(ctx: PipelineContext): Promise<boolean> {
|
|
|
134
150
|
// Import reviewStage lazily to avoid circular deps
|
|
135
151
|
const { reviewStage } = await import("./review");
|
|
136
152
|
if (!reviewStage.enabled(ctx)) return true;
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
// reviewStage.execute updates ctx.reviewResult in place.
|
|
154
|
+
// We cannot use result.action here because review returns "continue" for BOTH
|
|
155
|
+
// pass and built-in-check-failure (to hand off to autofix). Check success directly.
|
|
156
|
+
await reviewStage.execute(ctx);
|
|
157
|
+
return ctx.reviewResult?.success === true;
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
function collectFailedChecks(ctx: PipelineContext): ReviewCheckResult[] {
|
|
@@ -65,9 +65,28 @@ async function runDecompose(
|
|
|
65
65
|
if (!agent) {
|
|
66
66
|
throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// Resolve decompose model: config.decompose.model tier → actual model string
|
|
70
|
+
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
71
|
+
let decomposeModel: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
const { resolveModel } = await import("../../config/schema");
|
|
74
|
+
const models = config.models as Record<string, unknown>;
|
|
75
|
+
const entry = models[decomposeTier] ?? models.balanced;
|
|
76
|
+
if (entry) decomposeModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
77
|
+
} catch {
|
|
78
|
+
// resolveModel can throw on malformed entries — fall through to let complete() handle it
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
68
82
|
const adapter = {
|
|
69
83
|
async decompose(prompt: string): Promise<string> {
|
|
70
|
-
return agent.complete(prompt, {
|
|
84
|
+
return agent.complete(prompt, {
|
|
85
|
+
model: decomposeModel,
|
|
86
|
+
jsonMode: true,
|
|
87
|
+
config,
|
|
88
|
+
sessionName: storySessionName,
|
|
89
|
+
});
|
|
71
90
|
},
|
|
72
91
|
};
|
|
73
92
|
|
|
@@ -177,7 +196,7 @@ export const routingStage: PipelineStage = {
|
|
|
177
196
|
|
|
178
197
|
// SD-004: Oversized story detection and decomposition
|
|
179
198
|
const decomposeConfig = ctx.config.decompose;
|
|
180
|
-
if (decomposeConfig) {
|
|
199
|
+
if (decomposeConfig && ctx.story.status !== "decomposed") {
|
|
181
200
|
const acCount = ctx.story.acceptanceCriteria.length;
|
|
182
201
|
const complexity = ctx.routing.complexity;
|
|
183
202
|
const isOversized =
|
package/src/review/runner.ts
CHANGED
|
@@ -99,6 +99,9 @@ const SIGKILL_GRACE_PERIOD_MS = 5_000;
|
|
|
99
99
|
*/
|
|
100
100
|
async function runCheck(check: ReviewCheckName, command: string, workdir: string): Promise<ReviewCheckResult> {
|
|
101
101
|
const startTime = Date.now();
|
|
102
|
+
const logger = getSafeLogger();
|
|
103
|
+
|
|
104
|
+
logger?.info("review", `Running ${check} check`, { check, command, workdir });
|
|
102
105
|
|
|
103
106
|
try {
|
|
104
107
|
// Parse command into executable and args
|
|
@@ -152,6 +155,18 @@ async function runCheck(check: ReviewCheckName, command: string, workdir: string
|
|
|
152
155
|
const stderr = await new Response(proc.stderr).text();
|
|
153
156
|
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
154
157
|
|
|
158
|
+
if (exitCode !== 0) {
|
|
159
|
+
logger?.warn("review", `${check} check failed`, {
|
|
160
|
+
check,
|
|
161
|
+
command,
|
|
162
|
+
workdir,
|
|
163
|
+
exitCode,
|
|
164
|
+
output: output.slice(0, 2000),
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
logger?.debug("review", `${check} check passed`, { check, command, durationMs: Date.now() - startTime });
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
return {
|
|
156
171
|
check,
|
|
157
172
|
command,
|
package/src/utils/git.ts
CHANGED
|
@@ -181,7 +181,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
|
|
|
181
181
|
return gitRoot;
|
|
182
182
|
}
|
|
183
183
|
})();
|
|
184
|
-
|
|
184
|
+
// Allow: workdir IS the git root, or workdir is a subdirectory (monorepo package)
|
|
185
|
+
// Reject: workdir has no git repo at all (realGitRoot would be empty/error)
|
|
186
|
+
const isAtRoot = realWorkdir === realGitRoot;
|
|
187
|
+
const isSubdir = realGitRoot && realWorkdir.startsWith(`${realGitRoot}/`);
|
|
188
|
+
if (!isAtRoot && !isSubdir) return;
|
|
185
189
|
|
|
186
190
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
187
191
|
cwd: workdir,
|
|
@@ -199,7 +203,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
|
|
|
199
203
|
dirtyFiles: statusOutput.trim().split("\n").length,
|
|
200
204
|
});
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
// Use "git add ." when workdir is a monorepo package subdir — only stages files under
|
|
207
|
+
// that package, preventing accidental cross-package commits.
|
|
208
|
+
// Use "git add -A" at repo root to capture renames/deletions across the full tree.
|
|
209
|
+
const addArgs = isSubdir ? ["git", "add", "."] : ["git", "add", "-A"];
|
|
210
|
+
const addProc = _gitDeps.spawn(addArgs, { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
203
211
|
await addProc.exited;
|
|
204
212
|
|
|
205
213
|
const commitProc = _gitDeps.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|