@nathapp/nax 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bin/nax.ts +20 -2
  2. package/docs/ROADMAP.md +33 -15
  3. package/docs/specs/trigger-completion.md +145 -0
  4. package/nax/features/central-run-registry/prd.json +105 -0
  5. package/nax/features/trigger-completion/prd.json +150 -0
  6. package/nax/features/trigger-completion/progress.txt +7 -0
  7. package/nax/status.json +14 -24
  8. package/package.json +2 -2
  9. package/src/commands/index.ts +1 -0
  10. package/src/commands/logs.ts +87 -17
  11. package/src/commands/runs.ts +220 -0
  12. package/src/config/types.ts +3 -1
  13. package/src/execution/crash-recovery.ts +11 -0
  14. package/src/execution/executor-types.ts +1 -1
  15. package/src/execution/lifecycle/run-setup.ts +4 -0
  16. package/src/execution/sequential-executor.ts +49 -7
  17. package/src/interaction/plugins/auto.ts +10 -1
  18. package/src/pipeline/event-bus.ts +14 -1
  19. package/src/pipeline/stages/completion.ts +20 -0
  20. package/src/pipeline/stages/execution.ts +62 -0
  21. package/src/pipeline/stages/review.ts +25 -1
  22. package/src/pipeline/subscribers/events-writer.ts +121 -0
  23. package/src/pipeline/subscribers/hooks.ts +32 -0
  24. package/src/pipeline/subscribers/interaction.ts +36 -1
  25. package/src/pipeline/subscribers/registry.ts +73 -0
  26. package/src/routing/router.ts +3 -2
  27. package/src/routing/strategies/keyword.ts +2 -1
  28. package/src/routing/strategies/llm-prompts.ts +29 -28
  29. package/src/utils/git.ts +21 -0
  30. package/test/integration/cli/cli-logs.test.ts +40 -17
  31. package/test/integration/routing/plugin-routing-core.test.ts +1 -1
  32. package/test/unit/commands/logs.test.ts +63 -22
  33. package/test/unit/commands/runs.test.ts +303 -0
  34. package/test/unit/execution/sequential-executor.test.ts +235 -0
  35. package/test/unit/interaction/auto-plugin.test.ts +162 -0
  36. package/test/unit/interaction-plugins.test.ts +308 -1
  37. package/test/unit/pipeline/stages/completion-review-gate.test.ts +218 -0
  38. package/test/unit/pipeline/stages/execution-ambiguity.test.ts +311 -0
  39. package/test/unit/pipeline/stages/execution-merge-conflict.test.ts +218 -0
  40. package/test/unit/pipeline/stages/review.test.ts +201 -0
  41. package/test/unit/pipeline/subscribers/events-writer.test.ts +227 -0
  42. package/test/unit/pipeline/subscribers/hooks.test.ts +43 -4
  43. package/test/unit/pipeline/subscribers/interaction.test.ts +284 -2
  44. package/test/unit/pipeline/subscribers/registry.test.ts +149 -0
  45. package/test/unit/prd-auto-default.test.ts +2 -2
  46. package/test/unit/routing/routing-stability.test.ts +1 -1
  47. package/test/unit/routing-core.test.ts +5 -5
  48. package/test/unit/routing-strategies.test.ts +1 -3
  49. package/test/unit/utils/git.test.ts +50 -0
package/bin/nax.ts CHANGED
@@ -62,6 +62,7 @@ import { generateCommand } from "../src/cli/generate";
62
62
  import { diagnose } from "../src/commands/diagnose";
63
63
  import { logsCommand } from "../src/commands/logs";
64
64
  import { precheckCommand } from "../src/commands/precheck";
65
+ import { runsCommand } from "../src/commands/runs";
65
66
  import { unlockCommand } from "../src/commands/unlock";
66
67
  import { DEFAULT_CONFIG, findProjectDir, loadConfig, validateDirectory } from "../src/config";
67
68
  import { run } from "../src/execution";
@@ -685,7 +686,7 @@ program
685
686
  .option("-s, --story <id>", "Filter to specific story")
686
687
  .option("--level <level>", "Filter by log level (debug|info|warn|error)")
687
688
  .option("-l, --list", "List all runs in table format", false)
688
- .option("-r, --run <timestamp>", "Select specific run by timestamp")
689
+ .option("-r, --run <runId>", "Select run by run ID from central registry (global)")
689
690
  .option("-j, --json", "Output raw JSONL", false)
690
691
  .action(async (options) => {
691
692
  let workdir: string;
@@ -773,7 +774,24 @@ program
773
774
  });
774
775
 
775
776
  // ── runs ─────────────────────────────────────────────
776
- const runs = program.command("runs").description("Manage and view run history");
777
+ const runs = program
778
+ .command("runs")
779
+ .description("Show all registered runs from the central registry (~/.nax/runs/)")
780
+ .option("--project <name>", "Filter by project name")
781
+ .option("--last <N>", "Limit to N most recent runs (default: 20)")
782
+ .option("--status <status>", "Filter by status (running|completed|failed|crashed)")
783
+ .action(async (options) => {
784
+ try {
785
+ await runsCommand({
786
+ project: options.project,
787
+ last: options.last !== undefined ? Number.parseInt(options.last, 10) : undefined,
788
+ status: options.status,
789
+ });
790
+ } catch (err) {
791
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
792
+ process.exit(1);
793
+ }
794
+ });
777
795
 
778
796
  runs
779
797
  .command("list")
package/docs/ROADMAP.md CHANGED
@@ -118,34 +118,50 @@
118
118
 
119
119
  ---
120
120
 
121
- ## v0.23.0 — Status File Consolidation
121
+ ## v0.23.0 — Status File Consolidation
122
122
 
123
123
  **Theme:** Auto-write status.json to well-known paths, align readers, remove dead options
124
- **Status:** 🔄 In Progress (self-dev running, SFC-001 ✅)
124
+ **Status:** Shipped (2026-03-07)
125
125
  **Spec:** [docs/specs/status-file-consolidation.md](specs/status-file-consolidation.md)
126
126
  **Pre-requisite for:** v0.24.0 (Central Run Registry)
127
127
 
128
128
  ### Stories
129
129
  - [x] ~~**SFC-001:** Auto-write project-level status — remove `--status-file` flag, always write to `<workdir>/nax/status.json`~~
130
- - [ ] **BUG-043:** Fix scoped test command construction + add `testScoped` config with `{{files}}` template
131
- - [ ] **BUG-044:** Log scoped and full-suite test commands at info level in verify stage
132
- - [ ] **SFC-002:** Write feature-level status on run end — copy final snapshot to `<workdir>/nax/features/<feature>/status.json`
133
- - [ ] **SFC-003:** Align status readers — `nax status` + `nax diagnose` read from correct paths
134
- - [ ] **SFC-004:** Clean up dead code — remove `--status-file` option, `.nax-status.json` references
130
+ - [x] ~~**BUG-043:** Fix scoped test command construction + add `testScoped` config with `{{files}}` template~~
131
+ - [x] ~~**BUG-044:** Log scoped and full-suite test commands at info level in verify stage~~
132
+ - [x] ~~**SFC-002:** Write feature-level status on run end — copy final snapshot to `<workdir>/nax/features/<feature>/status.json`~~
133
+ - [x] ~~**SFC-003:** Align status readers — `nax status` + `nax diagnose` read from correct paths~~
134
+ - [x] ~~**SFC-004:** Clean up dead code — remove `--status-file` option, `.nax-status.json` references~~
135
135
 
136
136
  ---
137
137
 
138
- ## v0.24.0 — Central Run Registry
138
+ ## v0.25.0 — Trigger Completion
139
139
 
140
- **Theme:** Global run index across all projects single source of truth for all nax run history
140
+ **Theme:** Wire all 8 unwired interaction triggers, 3 missing hook events, and add plugin integration tests
141
141
  **Status:** 🔲 Planned
142
+ **Spec:** [docs/specs/trigger-completion.md](specs/trigger-completion.md)
143
+
144
+ ### Stories
145
+ - [ ] **TC-001:** Wire `cost-exceeded` + `cost-warning` triggers — fire at 80%/100% of cost limit in sequential-executor.ts
146
+ - [ ] **TC-002:** Wire `max-retries` trigger — fire on permanent story failure via `story:failed` event in wireInteraction
147
+ - [ ] **TC-003:** Wire `security-review`, `merge-conflict`, `pre-merge` triggers — review rejection, git conflict detection, pre-completion gate
148
+ - [ ] **TC-004:** Wire `story-ambiguity` + `review-gate` triggers — ambiguity keyword detection, per-story human checkpoint
149
+ - [ ] **TC-005:** Wire missing hook events — `on-resume`, `on-session-end`, `on-error` to pipeline events
150
+ - [ ] **TC-006:** Auto plugin + Telegram + Webhook integration tests — mock LLM/network, cover approve/reject/HMAC flows
151
+
152
+ ---
153
+
154
+ ## v0.24.0 — Central Run Registry ✅
155
+
156
+ **Theme:** Global run index across all projects — single source of truth for all nax run history
157
+ **Status:** ✅ Shipped (2026-03-07)
142
158
  **Spec:** [docs/specs/central-run-registry.md](specs/central-run-registry.md)
143
159
 
144
160
  ### Stories
145
- - [ ] **CRR-000:** `src/pipeline/subscribers/events-writer.ts` — `wireEventsWriter()`, writes lifecycle events to `~/.nax/events/<project>/events.jsonl` (machine-readable completion signal for watchdog/CI)
146
- - [ ] **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)
147
- - [ ] **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`
148
- - [ ] **CRR-003:** `nax logs --run <runId>` — resolve run from global registry via `eventsDir`, stream logs from any directory
161
+ - [x] ~~**CRR-000:** `src/pipeline/subscribers/events-writer.ts` — `wireEventsWriter()`, writes lifecycle events to `~/.nax/events/<project>/events.jsonl` (machine-readable completion signal for watchdog/CI)~~
162
+ - [x] ~~**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)~~
163
+ - [x] ~~**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`~~
164
+ - [x] ~~**CRR-003:** `nax logs --run <runId>` — resolve run from global registry via `eventsDir`, stream logs from any directory~~
149
165
 
150
166
  ---
151
167
 
@@ -223,6 +239,8 @@
223
239
 
224
240
  | Version | Theme | Date | Details |
225
241
  |:---|:---|:---|:---|
242
+ | v0.24.0 | Central Run Registry | 2026-03-07 | CRR-000–003: events writer, registry, nax runs CLI, nax logs --run global resolution |
243
+ | v0.23.0 | Status File Consolidation | 2026-03-07 | SFC-001–004: auto-write status.json, feature-level status, align readers, remove dead code; BUG-043/044: testScoped config + command logging |
226
244
  | v0.18.1 | Type Safety + CI Pipeline | 2026-03-03 | 60 TS errors + 12 lint errors fixed, GitLab CI green (1952/56/0) |
227
245
  | v0.22.2 | Routing Stability + SFC-001 | 2026-03-07 | BUG-040 floating outputPromise crash on LLM timeout retry; SFC-001 auto-write status.json |
228
246
  | v0.22.1 | Pipeline Re-Architecture | 2026-03-07 | VerificationOrchestrator, EventBus, new stages (rectify/autofix/regression/deferred-regression), post-run SSOT. 2264 pass |
@@ -287,8 +305,8 @@
287
305
 
288
306
  - [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).~~
289
307
  - [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.~~
290
- - [ ] **BUG-043:** Scoped test command appends files instead of replacing path — `runners.ts:scoped()` concatenates `scopedTestPaths` to full-suite command, resulting in `bun test test/ --timeout=60000 /path/to/file.ts` (runs everything). Fix: use `testScoped` config with `{{files}}` template, fall back to `buildSmartTestCommand()` heuristic. **Location:** `src/verification/runners.ts:scoped()`
291
- - [ ] **BUG-044:** Scoped/full-suite test commands not logged — no visibility into what command was actually executed during verify stage. Fix: log at info level before execution.
308
+ - [x] ~~**BUG-043:** Scoped test command appends files instead of replacing path — `runners.ts:scoped()` concatenates `scopedTestPaths` to full-suite command, resulting in `bun test test/ --timeout=60000 /path/to/file.ts` (runs everything). Fix: use `testScoped` config with `{{files}}` template, fall back to `buildSmartTestCommand()` heuristic. **Location:** `src/verification/runners.ts:scoped()`
309
+ - [x] ~~**BUG-044:** Scoped/full-suite test commands not logged — no visibility into what command was actually executed during verify stage. Fix: log at info level before execution.
292
310
 
293
311
  ### Features
294
312
  - [x] ~~`nax unlock` command~~
@@ -0,0 +1,145 @@
1
+ # Trigger Completion — Spec
2
+
3
+ **Version:** v0.25.0
4
+ **Status:** Planned
5
+
6
+ ---
7
+
8
+ ## Problem
9
+
10
+ 8 of 9 interaction trigger helpers (`checkCostExceeded`, `checkCostWarning`, `checkMaxRetries`, `checkSecurityReview`, `checkMergeConflict`, `checkPreMerge`, `checkStoryAmbiguity`, `checkReviewGate`) are implemented in `src/interaction/triggers.ts` and exported but **never called** from the pipeline.
11
+
12
+ Only `human-review` is wired (via `wireInteraction` subscriber on `human-review:requested` event).
13
+
14
+ Additionally, 3 hook events (`on-resume`, `on-session-end`, `on-error`) are defined in `HookEvent` but not wired to any pipeline event.
15
+
16
+ ---
17
+
18
+ ## Goal
19
+
20
+ Wire all 8 remaining triggers to the correct pipeline decision points. Add 3 missing hook events. Add E2E/integration test coverage for the Telegram and auto plugins.
21
+
22
+ ---
23
+
24
+ ## Stories
25
+
26
+ ### TC-001: Wire `cost-exceeded` and `cost-warning` triggers
27
+
28
+ **Location:** `src/execution/sequential-executor.ts`
29
+
30
+ Currently at line 93, when `totalCost >= config.execution.costLimit`, the run exits with `"cost-limit"` — no interaction trigger is fired.
31
+
32
+ **Fix:**
33
+ - Before exiting on cost limit: call `checkCostExceeded({featureName, cost, limit}, config, interactionChain)`. If trigger returns `abort` or chain not available → exit as today. Pass `interactionChain` into `executeSequential` ctx (already present in `SequentialExecutionContext`).
34
+ - Add a `cost-warning` threshold check: when `totalCost >= costLimit * 0.8` (configurable via `interaction.triggers.cost-warning.threshold`, default 0.8), fire `checkCostWarning`. Fire only once per run (track with a boolean flag). Fallback: `continue`.
35
+ - Emit new `run:paused` event if trigger response is `escalate` (pause for human decision).
36
+ - Add `CostExceededEvent` and `CostWarningEvent` to `PipelineEventBus` (or reuse `run:paused` with a `reason` field — preferred, avoids new event types).
37
+
38
+ **Acceptance criteria:**
39
+ - When cost hits 80% of limit, `cost-warning` trigger fires once and run continues (default fallback)
40
+ - When cost hits 100% of limit, `cost-exceeded` trigger fires; abort kills the run, skip/continue allows proceeding past limit
41
+ - When no interaction plugin is configured, behavior is identical to today (no-op)
42
+ - Tests: unit test both thresholds with mock chain
43
+
44
+ ---
45
+
46
+ ### TC-002: Wire `max-retries` trigger
47
+
48
+ **Location:** `src/execution/sequential-executor.ts` or `src/pipeline/pipeline-result-handler.ts`
49
+
50
+ Currently when a story exhausts all tier escalations and is marked failed permanently (`markStoryFailed`), no trigger fires (except `human-review` which fires on `human-review:requested` event for a different condition).
51
+
52
+ **Fix:**
53
+ - In the story failure path (after all escalations exhausted), call `checkMaxRetries({featureName, storyId, iteration}, config, interactionChain)`.
54
+ - Response `skip` = proceed (today's behavior), `abort` = halt entire run, `escalate` = retry story from scratch at top tier.
55
+ - Wire via `story:failed` event in `wireInteraction` subscriber (add alongside `human-review:requested`).
56
+
57
+ **Acceptance criteria:**
58
+ - `max-retries` trigger fires when a story is permanently failed
59
+ - `abort` response halts the run with exit reason `"interaction-abort"`
60
+ - `skip` response is silent (today's behavior)
61
+ - Tests: unit test with mock chain for all three fallbacks
62
+
63
+ ---
64
+
65
+ ### TC-003: Wire `security-review`, `merge-conflict`, `pre-merge` triggers
66
+
67
+ **Location:** `src/pipeline/stages/review.ts` and `src/pipeline/stages/completion.ts` (post-story)
68
+
69
+ - **`security-review`**: Fire after plugin reviewer (e.g. semgrep) rejects a story in `review.ts`. Currently returns `{ action: "fail" }`. Before failing permanently, call `checkSecurityReview`. Response `abort` = fail (today), `escalate` = retry with security context injected.
70
+ - **`merge-conflict`**: Fire when git operations detect a merge conflict during story commit. Currently no merge-conflict detection exists — add detection in `src/execution/git.ts` (catch `CONFLICT` in git merge/rebase output) and call `checkMergeConflict`.
71
+ - **`pre-merge`**: Fire after all stories pass but before the run is marked complete. Call `checkPreMerge({featureName, totalStories, cost}, config, interactionChain)` in `sequential-executor.ts` final block. Response `abort` = halt, `continue` = complete normally.
72
+
73
+ **Acceptance criteria:**
74
+ - `security-review` trigger fires when plugin reviewer rejects (not when lint/typecheck fails)
75
+ - `merge-conflict` trigger fires when git detects CONFLICT markers
76
+ - `pre-merge` trigger fires once after all stories pass, before run:completed
77
+ - Tests: unit tests for each trigger point with mock chain
78
+
79
+ ---
80
+
81
+ ### TC-004: Wire `story-ambiguity` and `review-gate` triggers
82
+
83
+ **Location:** `src/pipeline/stages/execution.ts`
84
+
85
+ - **`story-ambiguity`**: Fire when agent session returns ambiguous/clarification-needed signal. Currently the agent exit codes and output are parsed in `execution.ts` — add a detection heuristic (e.g. agent output contains "unclear" / "ambiguous" / "need clarification" keywords, or a new `needsClarification` flag in agent result). Call `checkStoryAmbiguity` before escalating.
86
+ - **`review-gate`**: Fire after `story:completed` as a human checkpoint gate (configurable, disabled by default). Wire via new `review-gate:requested` event emitted in completion stage when `interaction.triggers.review-gate.enabled = true`.
87
+
88
+ **Acceptance criteria:**
89
+ - `story-ambiguity` trigger fires when agent signals ambiguity (keyword detection)
90
+ - `review-gate` trigger fires after each story passes when enabled
91
+ - Both default to disabled in config (opt-in)
92
+ - Tests: unit tests for ambiguity detection heuristic + trigger dispatch
93
+
94
+ ---
95
+
96
+ ### TC-005: Wire missing hook events (`on-resume`, `on-session-end`, `on-error`)
97
+
98
+ **Location:** `src/pipeline/subscribers/hooks.ts`
99
+
100
+ Three hook events are defined in `HookEvent` but never wired to pipeline events:
101
+
102
+ - **`on-resume`**: Fire when a paused run resumes. Add `run:resumed` event to `PipelineEventBus`, emit it in `sequential-executor.ts` when resuming from pause state. Wire in `wireHooks`.
103
+ - **`on-session-end`**: Fire when an individual agent session ends (pass or fail). Map to `story:completed` + `story:failed`. Wire in `wireHooks` on both events.
104
+ - **`on-error`**: Fire on unhandled errors / crash. Emit in `crash-recovery.ts` crash handler. Wire in `wireHooks`.
105
+
106
+ **Acceptance criteria:**
107
+ - `on-resume` hook fires when a paused run is continued
108
+ - `on-session-end` hook fires after every agent session (pass or fail)
109
+ - `on-error` hook fires in crash handler before exit
110
+ - Tests: extend existing `hooks.test.ts` with the three new events
111
+
112
+ ---
113
+
114
+ ### TC-006: Auto plugin integration tests
115
+
116
+ **Location:** `test/integration/interaction/`
117
+
118
+ The `AutoInteractionPlugin` (LLM-based) has zero test coverage. The Telegram and Webhook plugins have init/config tests but no send/receive flow tests.
119
+
120
+ **Fix:**
121
+ - `auto.test.ts` — mock the LLM call (`_deps` pattern), test: approve decision, reject decision, confidence below threshold falls back, `security-review` is never auto-approved.
122
+ - Extend `interaction-plugins.test.ts` with Telegram send flow (mock `fetch`, verify message format + inline keyboard structure).
123
+ - Extend with Webhook send flow (mock HTTP server, verify HMAC signature validation).
124
+
125
+ **Acceptance criteria:**
126
+ - Auto plugin: LLM approve/reject/confidence-fallback/security-review-block all covered
127
+ - Telegram: message send format and inline keyboard structure verified
128
+ - Webhook: HMAC verification tested (valid + tampered signatures)
129
+ - All tests are unit/mock — no real network calls
130
+
131
+ ---
132
+
133
+ ## Out of Scope
134
+
135
+ - Full E2E test with real Telegram bot (requires live credentials)
136
+ - New trigger types beyond the 9 already defined
137
+ - Interaction state persistence (pause/resume full flow) — separate feature
138
+
139
+ ---
140
+
141
+ ## Notes
142
+
143
+ - All trigger calls must be best-effort guarded: if `interactionChain` is null/undefined, skip silently (today's behavior)
144
+ - `interactionChain` is already threaded through `SequentialExecutionContext` — no new context changes needed for most stories
145
+ - Config `interaction.triggers.<name>.enabled` must be `true` for any trigger to fire (`isTriggerEnabled` handles this)
@@ -0,0 +1,105 @@
1
+ {
2
+ "project": "nax",
3
+ "branchName": "feat/central-run-registry",
4
+ "feature": "central-run-registry",
5
+ "version": "0.24.0",
6
+ "description": "Global run index across all projects. Events file writer for machine-readable lifecycle events, registry subscriber that indexes every run to ~/.nax/runs/, nax runs CLI for cross-project run history, and nax logs --run <runId> for global log resolution.",
7
+ "userStories": [
8
+ {
9
+ "id": "CRR-000",
10
+ "title": "Events file writer subscriber",
11
+ "description": "New subscriber: src/pipeline/subscribers/events-writer.ts \u2014 wireEventsWriter(bus, project, feature, runId, workdir). Listens to run:started, story:started, story:completed, story:failed, run:completed, run:paused. Writes one JSON line per event to ~/.nax/events/<project>/events.jsonl (append mode). Each line: {\"ts\": ISO8601, \"event\": string, \"runId\": string, \"feature\": string, \"project\": string, \"storyId\"?: string, \"data\"?: object}. The run:completed event writes an 'on-complete' entry \u2014 the machine-readable signal that nax exited gracefully (fixes watchdog false crash reports). Best-effort: wrap all writes in try/catch, log warnings on failure, never throw or block the pipeline. Create ~/.nax/events/<project>/ on first write via mkdir recursive. Derive project name from path.basename(workdir). Wire in sequential-executor.ts alongside existing wireHooks/wireReporters/wireInteraction calls. Return UnsubscribeFn matching existing subscriber pattern.",
12
+ "complexity": "medium",
13
+ "status": "pending",
14
+ "acceptanceCriteria": [
15
+ "After a run, ~/.nax/events/<project>/events.jsonl exists with JSONL entries",
16
+ "Each line is valid JSON with ts, event, runId, feature, project fields",
17
+ "run:completed produces an entry with event=on-complete",
18
+ "story:started/completed/failed events include storyId",
19
+ "Write failure does not crash or block the nax run",
20
+ "Directory is created automatically on first write",
21
+ "wireEventsWriter is called in sequential-executor.ts",
22
+ "Returns UnsubscribeFn consistent with wireHooks/wireReporters pattern"
23
+ ],
24
+ "attempts": 0,
25
+ "priorErrors": [],
26
+ "priorFailures": [],
27
+ "escalations": [],
28
+ "dependencies": [],
29
+ "tags": [],
30
+ "storyPoints": 2
31
+ },
32
+ {
33
+ "id": "CRR-001",
34
+ "title": "Registry writer subscriber",
35
+ "description": "New subscriber: src/pipeline/subscribers/registry.ts \u2014 wireRegistry(bus, project, feature, runId, workdir). Listens to run:started. On event, creates ~/.nax/runs/<project>-<feature>-<runId>/meta.json with fields: {runId, project, feature, workdir, statusPath: join(workdir, 'nax/features', feature, 'status.json'), eventsDir: join(workdir, 'nax/features', feature, 'runs'), registeredAt: ISO8601}. meta.json schema exported as MetaJson interface. Written once, never updated. Best-effort: try/catch + warn log, never throw/block. Create ~/.nax/runs/ directory on first call via mkdir recursive. Derive project from path.basename(workdir). Wire in sequential-executor.ts alongside wireEventsWriter. Return UnsubscribeFn.",
36
+ "complexity": "medium",
37
+ "status": "pending",
38
+ "acceptanceCriteria": [
39
+ "After run start, ~/.nax/runs/<project>-<feature>-<runId>/meta.json exists",
40
+ "meta.json contains runId, project, feature, workdir, statusPath, eventsDir, registeredAt",
41
+ "statusPath points to <workdir>/nax/features/<feature>/status.json",
42
+ "eventsDir points to <workdir>/nax/features/<feature>/runs",
43
+ "MetaJson interface is exported from the module",
44
+ "Write failure does not crash or block the nax run",
45
+ "wireRegistry is called in sequential-executor.ts"
46
+ ],
47
+ "attempts": 0,
48
+ "priorErrors": [],
49
+ "priorFailures": [],
50
+ "escalations": [],
51
+ "dependencies": [],
52
+ "tags": [],
53
+ "storyPoints": 2
54
+ },
55
+ {
56
+ "id": "CRR-002",
57
+ "title": "nax runs CLI command",
58
+ "description": "New command: src/commands/runs.ts \u2014 runsCommand(options). Reads all ~/.nax/runs/*/meta.json, resolves each statusPath to read the live NaxStatusFile for current state. Displays a table sorted by registeredAt desc. Columns: RUN ID, PROJECT, FEATURE, STATUS, STORIES (passed/total), DURATION, DATE. Default limit: 20. Options: --project <name> (filter), --last <N> (limit), --status <running|completed|failed|crashed> (filter). If statusPath missing, show status as '[unavailable]'. Register in src/commands/index.ts and wire in bin/nax.ts CLI arg parser (match pattern of existing diagnose/logs/status commands). Use chalk for colored output. Green for completed, red for failed, yellow for running, dim for unavailable.",
59
+ "complexity": "medium",
60
+ "status": "pending",
61
+ "acceptanceCriteria": [
62
+ "nax runs displays a table of all registered runs sorted newest-first",
63
+ "nax runs --project <name> filters by project name",
64
+ "nax runs --last <N> limits output to N runs",
65
+ "nax runs --status <status> filters by run status",
66
+ "Runs with missing statusPath show '[unavailable]' status gracefully",
67
+ "Empty registry shows 'No runs found' message",
68
+ "Command is registered in CLI help output and bin/nax.ts"
69
+ ],
70
+ "attempts": 0,
71
+ "priorErrors": [],
72
+ "priorFailures": [],
73
+ "escalations": [],
74
+ "dependencies": [
75
+ "CRR-001"
76
+ ],
77
+ "tags": [],
78
+ "storyPoints": 3
79
+ },
80
+ {
81
+ "id": "CRR-003",
82
+ "title": "nax logs --run <runId> global resolution",
83
+ "description": "Enhance src/commands/logs.ts logsCommand: when --run <runId> is provided, scan ~/.nax/runs/*/meta.json for matching runId (exact or prefix match). Read eventsDir from meta.json, locate the JSONL log file in that directory (newest .jsonl file), then display using existing displayLogs/followLogs functions. Falls back to current behavior (local feature context) when --run is not specified. If runId not found in registry, throw error Run not found in registry: <runId>. If eventsDir or log file missing, show unavailable message. Update LogsOptions interface to accept run?: string. Update bin/nax.ts to pass --run option through.",
84
+ "complexity": "medium",
85
+ "status": "pending",
86
+ "acceptanceCriteria": [
87
+ "nax logs --run <runId> displays logs from the matching run regardless of cwd",
88
+ "Resolution uses ~/.nax/runs/*/meta.json to find eventsDir path",
89
+ "Unknown runId shows clear error message",
90
+ "Missing log files show unavailable message",
91
+ "Without --run flag, existing local behavior is unchanged",
92
+ "Partial runId matching works (prefix match on runId field)"
93
+ ],
94
+ "attempts": 0,
95
+ "priorErrors": [],
96
+ "priorFailures": [],
97
+ "escalations": [],
98
+ "dependencies": [
99
+ "CRR-001"
100
+ ],
101
+ "tags": [],
102
+ "storyPoints": 2
103
+ }
104
+ ]
105
+ }
@@ -0,0 +1,150 @@
1
+ {
2
+ "project": "nax",
3
+ "branchName": "feat/trigger-completion",
4
+ "feature": "trigger-completion",
5
+ "version": "0.25.0",
6
+ "description": "Wire all 8 unwired interaction triggers to correct pipeline decision points, add 3 missing hook events, and add integration tests for auto/telegram/webhook plugins.",
7
+ "userStories": [
8
+ {
9
+ "id": "TC-001",
10
+ "title": "Wire cost-exceeded and cost-warning triggers",
11
+ "description": "In src/execution/sequential-executor.ts: (1) Before exiting on cost limit (line ~93), call checkCostExceeded({featureName: ctx.feature, cost: totalCost, limit: ctx.config.execution.costLimit}, ctx.config, ctx.interactionChain). Import checkCostExceeded from src/interaction/triggers.ts. If isTriggerEnabled(\"cost-exceeded\", config) is false or chain is null, keep today behavior. Trigger abort = exit \"cost-limit\". Trigger skip/continue = allow run to proceed past limit. (2) Add cost-warning: track a boolean warningSent=false. In the iteration loop, when totalCost >= costLimit * (interaction.triggers[\"cost-warning\"]?.threshold ?? 0.8) and !warningSent, call checkCostWarning({featureName, cost, limit}, config, interactionChain), set warningSent=true. isTriggerEnabled guards the call. Default fallback continue = proceed silently. Both calls must be best-effort: guard with if(interactionChain) check.",
12
+ "complexity": "medium",
13
+ "status": "passed",
14
+ "acceptanceCriteria": [
15
+ "When cost hits 80% of limit and cost-warning trigger is enabled, checkCostWarning fires once",
16
+ "Warning fires only once per run even if cost stays above threshold for multiple iterations",
17
+ "When cost hits 100% of limit and cost-exceeded is enabled, checkCostExceeded fires before exit",
18
+ "abort response exits with cost-limit reason; skip/continue allows run to proceed",
19
+ "When interaction plugin not configured, behavior is identical to today",
20
+ "Unit tests cover 80% threshold, 100% threshold, abort, skip, continue responses"
21
+ ],
22
+ "attempts": 0,
23
+ "priorErrors": [],
24
+ "priorFailures": [],
25
+ "escalations": [],
26
+ "dependencies": [],
27
+ "tags": [],
28
+ "storyPoints": 2,
29
+ "passes": true
30
+ },
31
+ {
32
+ "id": "TC-002",
33
+ "title": "Wire max-retries trigger",
34
+ "description": "In src/pipeline/subscribers/interaction.ts, extend wireInteraction to also subscribe to story:failed event. When story:failed fires with countsTowardEscalation=true (permanent failure, all tiers exhausted), call executeTrigger(\"max-retries\", {featureName: ev.feature ?? \"\", storyId: ev.storyId, iteration: ev.attempts ?? 0}, config, interactionChain). Import StoryFailedEvent from event-bus. Guard with isTriggerEnabled(\"max-retries\", config) and interactionChain check. Response handling: abort = emit a new run:paused event with reason \"max-retries-abort\" (the executor checks this to halt); skip = default, proceed; escalate = not supported for this trigger, treat as skip. Note: the actual run halt on abort requires reading from a shared flag or emitting run:paused — simplest: log a warning and let the run continue (abort behavior can be enhanced later). For now, abort = warn log only.",
35
+ "complexity": "medium",
36
+ "status": "passed",
37
+ "acceptanceCriteria": [
38
+ "max-retries trigger fires when story:failed event has countsTowardEscalation=true",
39
+ "max-retries trigger does NOT fire when countsTowardEscalation=false",
40
+ "When trigger disabled or no chain, no-op",
41
+ "abort response logs a warning (full halt is future work)",
42
+ "Unit tests cover enabled/disabled, countsTowardEscalation true/false, all fallback responses"
43
+ ],
44
+ "attempts": 0,
45
+ "priorErrors": [],
46
+ "priorFailures": [],
47
+ "escalations": [],
48
+ "dependencies": [],
49
+ "tags": [],
50
+ "storyPoints": 2,
51
+ "passes": true
52
+ },
53
+ {
54
+ "id": "TC-003",
55
+ "title": "Wire security-review, merge-conflict, and pre-merge triggers",
56
+ "description": "Three trigger wiring points: (1) security-review in src/pipeline/stages/review.ts: when plugin reviewer (semgrep etc) returns failure (the existing check at ~line 50 that returns action:fail for plugin reviewer rejection), before permanently failing, call checkSecurityReview({featureName, storyId: ctx.story.id}, ctx.config, ctx.interactionChain) if isTriggerEnabled and chain present. abort=fail (today), escalate=return {action:\"escalate\"}. Import from interaction. (2) merge-conflict: add conflict detection in src/execution/git.ts — after any git merge/rebase/commit operation, check if stdout/stderr contains \"CONFLICT\" or \"conflict\". If detected and isTriggerEnabled(\"merge-conflict\") and chain, call checkMergeConflict. Export a detectMergeConflict(output: string): boolean helper. (3) pre-merge in sequential-executor.ts: after all stories complete (isComplete(prd)=true) and before emitting run:completed, call checkPreMerge({featureName: ctx.feature, totalStories: prd.userStories.length, cost: totalCost}, ctx.config, ctx.interactionChain) if enabled. abort = exit without completing.",
57
+ "complexity": "medium",
58
+ "status": "passed",
59
+ "acceptanceCriteria": [
60
+ "security-review trigger fires when plugin reviewer rejects (not lint/typecheck)",
61
+ "security-review abort = story permanently fails; escalate = story retried",
62
+ "detectMergeConflict(output) returns true when CONFLICT present in git output",
63
+ "merge-conflict trigger fires when git conflict detected and trigger enabled",
64
+ "pre-merge trigger fires once after all stories pass, before run:completed",
65
+ "pre-merge abort exits run; continue = complete normally",
66
+ "Unit tests for each trigger point with mock chain"
67
+ ],
68
+ "attempts": 0,
69
+ "priorErrors": [],
70
+ "priorFailures": [],
71
+ "escalations": [],
72
+ "dependencies": [],
73
+ "tags": [],
74
+ "storyPoints": 3,
75
+ "passes": true
76
+ },
77
+ {
78
+ "id": "TC-004",
79
+ "title": "Wire story-ambiguity and review-gate triggers",
80
+ "description": "Two opt-in triggers (disabled by default): (1) story-ambiguity in src/pipeline/stages/execution.ts: after agent session result is parsed, check if agent output contains ambiguity signals. Add helper isAmbiguousOutput(output: string): boolean that returns true if output contains any of: [\"unclear\", \"ambiguous\", \"need clarification\", \"please clarify\", \"which one\", \"not sure which\"]. If detected and isTriggerEnabled(\"story-ambiguity\", config) and interactionChain, call checkStoryAmbiguity({featureName, storyId: ctx.story.id, reason: \"Agent output suggests ambiguity\"}, config, ctx.interactionChain). abort = escalate story; continue = proceed as normal. (2) review-gate in src/pipeline/stages/completion.ts (or wherever story:completed is emitted): if isTriggerEnabled(\"review-gate\", config) and interactionChain, call checkReviewGate({featureName, storyId: ctx.story.id}, config, ctx.interactionChain) after story passes. abort = mark story as needing re-review (log warning, do not fail); continue = proceed. Both triggers default to disabled in config.",
81
+ "complexity": "medium",
82
+ "status": "passed",
83
+ "acceptanceCriteria": [
84
+ "isAmbiguousOutput() detects all 6 keyword phrases (case-insensitive)",
85
+ "story-ambiguity trigger fires when isAmbiguousOutput=true and trigger enabled",
86
+ "story-ambiguity is disabled by default (isTriggerEnabled returns false)",
87
+ "review-gate trigger fires after each story passes when enabled",
88
+ "review-gate is disabled by default",
89
+ "Unit tests for isAmbiguousOutput and both trigger dispatch paths"
90
+ ],
91
+ "attempts": 0,
92
+ "priorErrors": [],
93
+ "priorFailures": [],
94
+ "escalations": [],
95
+ "dependencies": [],
96
+ "tags": [],
97
+ "storyPoints": 2,
98
+ "passes": true
99
+ },
100
+ {
101
+ "id": "TC-005",
102
+ "title": "Wire missing hook events: on-resume, on-session-end, on-error",
103
+ "description": "Three missing hook events to wire in src/pipeline/subscribers/hooks.ts: (1) on-resume: add RunResumedEvent {type:\"run:resumed\"; feature: string} to PipelineEventBus. Emit it in sequential-executor.ts when resuming from pause state (detect via interaction state or run:paused→run:resumed cycle). Wire bus.on(\"run:resumed\") → fireHook(hooks, \"on-resume\", ...) in wireHooks. (2) on-session-end: fire after every agent session ends (pass OR fail). Wire bus.on(\"story:completed\") AND bus.on(\"story:failed\") → fireHook(hooks, \"on-session-end\", hookCtx(feature, {storyId, status: passed?\"passed\":\"failed\"})). (3) on-error: emit a run:errored event in src/execution/crash-recovery.ts crash handler (unhandledRejection / SIGTERM / SIGINT handlers). Wire bus.on(\"run:errored\") → fireHook(hooks, \"on-error\", hookCtx(feature, {reason: signal/error})). Add RunErroredEvent type to event-bus. All three follow existing best-effort fire-and-forget pattern.",
104
+ "complexity": "medium",
105
+ "status": "passed",
106
+ "acceptanceCriteria": [
107
+ "RunResumedEvent type added to PipelineEventBus",
108
+ "on-resume hook fires when run:resumed event emitted",
109
+ "on-session-end hook fires after story:completed AND story:failed events",
110
+ "RunErroredEvent type added to PipelineEventBus",
111
+ "on-error hook fires in crash-recovery handlers (SIGTERM, SIGINT, unhandledRejection)",
112
+ "All three follow fire-and-forget pattern (no await, errors logged)",
113
+ "Extend hooks.test.ts with tests for all three new events"
114
+ ],
115
+ "attempts": 0,
116
+ "priorErrors": [],
117
+ "priorFailures": [],
118
+ "escalations": [],
119
+ "dependencies": [],
120
+ "tags": [],
121
+ "storyPoints": 2,
122
+ "passes": true
123
+ },
124
+ {
125
+ "id": "TC-006",
126
+ "title": "Auto plugin and Telegram/Webhook plugin integration tests",
127
+ "description": "Add mock-based integration tests for the three untested plugins. File locations: test/unit/interaction/auto-plugin.test.ts, extend test/unit/interaction-plugins.test.ts. (1) AutoInteractionPlugin (_deps pattern): mock the LLM call via _deps.callLlm. Test: LLM returns approve → response.action=\"continue\"; LLM returns reject → response.action=\"abort\"; confidence < threshold → fallback to chain default; trigger=security-review → always rejects auto-approval (hardcoded block), returns chain default. Add _deps.callLlm to auto.ts if not present. (2) Telegram send flow: mock fetch globally in test. Verify send() POSTs to correct API URL with message text and inline keyboard buttons (approve/reject). Verify poll() parses callback_query correctly. (3) Webhook: mock an HTTP server using Bun.serve in test. Verify send() POSTs payload with correct Content-Type. Verify HMAC signature validation rejects tampered payload. All tests are pure unit/mock — no real network calls.",
128
+ "complexity": "medium",
129
+ "status": "passed",
130
+ "acceptanceCriteria": [
131
+ "AutoInteractionPlugin: approve, reject, low-confidence, security-review-block all tested",
132
+ "Auto plugin uses _deps pattern for LLM call (testable without real API)",
133
+ "Telegram send() verified to POST correct message structure with inline keyboard",
134
+ "Telegram poll() parses callback_query response correctly",
135
+ "Webhook send() verified with correct Content-Type and payload structure",
136
+ "Webhook HMAC validation: valid signature passes, tampered payload rejected",
137
+ "Zero real network calls in any test"
138
+ ],
139
+ "attempts": 0,
140
+ "priorErrors": [],
141
+ "priorFailures": [],
142
+ "escalations": [],
143
+ "dependencies": [],
144
+ "tags": [],
145
+ "storyPoints": 2,
146
+ "passes": true
147
+ }
148
+ ],
149
+ "updatedAt": "2026-03-07T14:53:47.398Z"
150
+ }
@@ -0,0 +1,7 @@
1
+ [2026-03-07T14:04:48.521Z] TC-001 — PASSED — Wire cost-exceeded and cost-warning triggers — Cost: $1.1231
2
+ [2026-03-07T14:11:47.185Z] TC-002 — PASSED — Wire max-retries trigger — Cost: $0.1029
3
+ [2026-03-07T14:30:04.761Z] TC-003 — PASSED — Wire security-review, merge-conflict, and pre-merge triggers — Cost: $1.3628
4
+ [2026-03-07T14:36:14.823Z] TC-004 — PASSED — Wire story-ambiguity and review-gate triggers — Cost: $0.0000
5
+ [2026-03-07T14:38:01.345Z] TC-004 — PASSED — Wire story-ambiguity and review-gate triggers — Cost: $0.1019
6
+ [2026-03-07T14:43:51.353Z] TC-005 — PASSED — Wire missing hook events: on-resume, on-session-end, on-error — Cost: $0.4284
7
+ [2026-03-07T14:53:47.397Z] TC-006 — PASSED — Auto plugin and Telegram/Webhook plugin integration tests — Cost: $0.7347
package/nax/status.json CHANGED
@@ -1,37 +1,27 @@
1
1
  {
2
2
  "version": 1,
3
3
  "run": {
4
- "id": "run-2026-03-07T06-14-21-018Z",
5
- "feature": "status-file-consolidation",
6
- "startedAt": "2026-03-07T06:14:21.018Z",
7
- "status": "crashed",
4
+ "id": "run-2026-03-07T13-49-17-400Z",
5
+ "feature": "trigger-completion",
6
+ "startedAt": "2026-03-07T13:49:17.400Z",
7
+ "status": "completed",
8
8
  "dryRun": false,
9
- "pid": 217461,
10
- "crashedAt": "2026-03-07T06:22:36.300Z",
11
- "crashSignal": "SIGTERM"
9
+ "pid": 97007
12
10
  },
13
11
  "progress": {
14
- "total": 4,
15
- "passed": 0,
12
+ "total": 6,
13
+ "passed": 6,
16
14
  "failed": 0,
17
15
  "paused": 0,
18
16
  "blocked": 0,
19
- "pending": 4
17
+ "pending": 0
20
18
  },
21
19
  "cost": {
22
- "spent": 0,
23
- "limit": 3
20
+ "spent": 3.85387425,
21
+ "limit": 8
24
22
  },
25
- "current": {
26
- "storyId": "SFC-002",
27
- "title": "Write feature-level status on run end",
28
- "complexity": "medium",
29
- "tddStrategy": "test-after",
30
- "model": "balanced",
31
- "attempt": 1,
32
- "phase": "routing"
33
- },
34
- "iterations": 0,
35
- "updatedAt": "2026-03-07T06:22:36.300Z",
36
- "durationMs": 495282
23
+ "current": null,
24
+ "iterations": 7,
25
+ "updatedAt": "2026-03-07T14:58:57.404Z",
26
+ "durationMs": 4180004
37
27
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.23.0",
4
- "description": "AI Coding Agent Orchestrator loops until done",
3
+ "version": "0.25.0",
4
+ "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nax": "./bin/nax.ts"
@@ -5,4 +5,5 @@
5
5
  export { resolveProject, type ResolveProjectOptions, type ResolvedProject } from "./common";
6
6
  export { logsCommand, type LogsOptions } from "./logs";
7
7
  export { precheckCommand, type PrecheckOptions } from "./precheck";
8
+ export { runsCommand, type RunsOptions } from "./runs";
8
9
  export { unlockCommand, type UnlockOptions } from "./unlock";