@nathapp/nax 0.18.5 → 0.19.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.
- package/.gitlab-ci.yml +3 -3
- package/CHANGELOG.md +7 -0
- package/docs/ROADMAP.md +2 -1
- package/nax/features/nax-compliance/prd.json +52 -0
- package/nax/features/nax-compliance/progress.txt +1 -0
- package/nax/features/v0.19.0-hardening/plan.md +7 -0
- package/nax/features/v0.19.0-hardening/prd.json +84 -0
- package/nax/features/v0.19.0-hardening/progress.txt +7 -0
- package/nax/features/v0.19.0-hardening/spec.md +18 -0
- package/nax/features/v0.19.0-hardening/tasks.md +8 -0
- package/nax/status.json +27 -0
- package/package.json +2 -2
- package/src/acceptance/fix-generator.ts +6 -2
- package/src/acceptance/generator.ts +3 -1
- package/src/acceptance/types.ts +3 -1
- package/src/agents/claude-plan.ts +6 -5
- package/src/agents/claude.ts +19 -9
- package/src/cli/analyze.ts +1 -0
- package/src/cli/init.ts +7 -6
- package/src/config/defaults.ts +1 -0
- package/src/config/types.ts +2 -0
- package/src/context/injector.ts +18 -18
- package/src/execution/crash-recovery.ts +7 -10
- package/src/execution/lifecycle/acceptance-loop.ts +1 -0
- package/src/execution/lifecycle/index.ts +0 -1
- package/src/execution/lifecycle/precheck-runner.ts +1 -1
- package/src/execution/lifecycle/run-setup.ts +14 -14
- package/src/execution/parallel.ts +1 -1
- package/src/execution/runner.ts +1 -19
- package/src/execution/sequential-executor.ts +1 -1
- package/src/hooks/runner.ts +2 -2
- package/src/interaction/plugins/auto.ts +2 -2
- package/src/logger/logger.ts +3 -5
- package/src/plugins/loader.ts +36 -9
- package/src/routing/batch-route.ts +32 -0
- package/src/routing/index.ts +1 -0
- package/src/routing/loader.ts +7 -0
- package/src/tui/hooks/usePty.ts +20 -9
- package/src/utils/path-security.ts +56 -0
- package/src/verification/executor.ts +6 -13
- package/test/integration/plugins/config-resolution.test.ts +3 -3
- package/test/integration/plugins/loader.test.ts +3 -1
- package/test/integration/precheck-integration.test.ts +18 -11
- package/test/integration/security-loader.test.ts +83 -0
- package/test/unit/formatters.test.ts +2 -3
- package/test/unit/hooks/shell-security.test.ts +40 -0
- package/test/unit/utils/path-security.test.ts +47 -0
- package/src/execution/lifecycle/run-lifecycle.ts +0 -312
- package/test/unit/run-lifecycle.test.ts +0 -140
package/.gitlab-ci.yml
CHANGED
|
@@ -17,7 +17,7 @@ test:
|
|
|
17
17
|
stage: test
|
|
18
18
|
image:
|
|
19
19
|
name: nathapp/node-bun:22.21.0-1.3.9-alpine
|
|
20
|
-
pull_policy:
|
|
20
|
+
pull_policy: always
|
|
21
21
|
before_script:
|
|
22
22
|
- apk add --no-cache git
|
|
23
23
|
- git config --global safe.directory '*'
|
|
@@ -47,7 +47,7 @@ release:
|
|
|
47
47
|
stage: release
|
|
48
48
|
image:
|
|
49
49
|
name: nathapp/node-bun:22.21.0-1.3.9-alpine
|
|
50
|
-
pull_policy:
|
|
50
|
+
pull_policy: always
|
|
51
51
|
cache:
|
|
52
52
|
key:
|
|
53
53
|
files:
|
|
@@ -86,7 +86,7 @@ notify:
|
|
|
86
86
|
stage: notify
|
|
87
87
|
image:
|
|
88
88
|
name: registry-intl.cn-hongkong.aliyuncs.com/gkci/node:22.14.0-alpine-ci
|
|
89
|
-
pull_policy:
|
|
89
|
+
pull_policy: always
|
|
90
90
|
needs: [release]
|
|
91
91
|
script:
|
|
92
92
|
- VERSION=$(node -e "console.log(require('./package.json').version)")
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.18.6] - 2026-03-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **BUG-2:** Infinite PTY respawn loop in `usePty` hook by destructuring object-identity dependencies.
|
|
12
|
+
- **MEM-1 & MEM-3:** Prevented child process hangs on full `stderr` pipes by switching to `stderr: "inherit"`.
|
|
13
|
+
- **BUG-21 & BUG-22:** Added missing error handling and `.catch()` chains to process `stdout` streaming and exit handlers.
|
|
14
|
+
|
|
8
15
|
## [0.18.5] - 2026-03-04
|
|
9
16
|
|
|
10
17
|
### Changed
|
package/docs/ROADMAP.md
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
|
|
135
135
|
---
|
|
136
136
|
|
|
137
|
-
## v0.19.0 —
|
|
137
|
+
## v0.19.0 — Hardening & Compliance
|
|
138
138
|
|
|
139
139
|
**Theme:** Eliminate duplicate test runs, deferred regression gate, structured escalation context
|
|
140
140
|
**Status:** 🔲 Planned
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
| Version | Theme | Date | Details |
|
|
167
167
|
|:---|:---|:---|:---|
|
|
168
168
|
| v0.18.1 | Type Safety + CI Pipeline | 2026-03-03 | 60 TS errors + 12 lint errors fixed, GitLab CI green (1952/56/0) |
|
|
169
|
+
| v0.19.0 | Hardening & Compliance | TBD | SEC-1 to SEC-5, BUG-1, Node.js API removal, _deps rollout |
|
|
169
170
|
| v0.18.5 | Bun PTY Migration | 2026-03-04 | BUN-001: node-pty → Bun.spawn, CI cleanup, flaky test fix |
|
|
170
171
|
| v0.18.4 | Routing Stability | 2026-03-04 | BUG-031 keyword drift, BUG-033 LLM retry, pre-commit hook |
|
|
171
172
|
| v0.18.3 | Execution Reliability + Smart Runner | 2026-03-04 | BUG-026/028/029/030/032 + SFC-001/002 + STR-007, all items complete |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "@nathapp/nax",
|
|
3
|
+
"feature": "nax-compliance",
|
|
4
|
+
"branchName": "feat/v0.19.0-sec",
|
|
5
|
+
"createdAt": "2026-03-05T02:37:00Z",
|
|
6
|
+
"updatedAt": "2026-03-05T02:38:46.450Z",
|
|
7
|
+
"userStories": [
|
|
8
|
+
{
|
|
9
|
+
"id": "US-001",
|
|
10
|
+
"title": "Node.js API Removal",
|
|
11
|
+
"description": "Replace all forbidden Node.js APIs (readFileSync, appendFileSync, existsSync, setTimeout) with Bun-native equivalents (Bun.file().text(), Bun.write, Bun.file().exists(), Bun.sleep) across the codebase as per the v0.19.0 roadmap. Refer to .claude/rules/04-forbidden-patterns.md.",
|
|
12
|
+
"acceptanceCriteria": [
|
|
13
|
+
"No occurrences of readFileSync or appendFileSync remain in src/",
|
|
14
|
+
"No occurrences of existsSync remain in src/ (unless performance critical)",
|
|
15
|
+
"No occurrences of setTimeout remain in src/",
|
|
16
|
+
"Codebase passes typecheck and lint after changes"
|
|
17
|
+
],
|
|
18
|
+
"status": "failed",
|
|
19
|
+
"passes": false,
|
|
20
|
+
"attempts": 1,
|
|
21
|
+
"routing": {
|
|
22
|
+
"complexity": "simple",
|
|
23
|
+
"modelTier": "powerful",
|
|
24
|
+
"testStrategy": "test-after"
|
|
25
|
+
},
|
|
26
|
+
"priorErrors": [
|
|
27
|
+
"Attempt 1 failed with model tier: fast: Stage requested escalation to higher tier",
|
|
28
|
+
"Attempt 1 failed with model tier: balanced: Stage requested escalation to higher tier"
|
|
29
|
+
],
|
|
30
|
+
"priorFailures": [
|
|
31
|
+
{
|
|
32
|
+
"attempt": 1,
|
|
33
|
+
"modelTier": "fast",
|
|
34
|
+
"stage": "escalation",
|
|
35
|
+
"summary": "Failed with tier fast, escalating to next tier",
|
|
36
|
+
"timestamp": "2026-03-05T02:37:41.586Z"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"attempt": 1,
|
|
40
|
+
"modelTier": "balanced",
|
|
41
|
+
"stage": "escalation",
|
|
42
|
+
"summary": "Failed with tier balanced, escalating to next tier",
|
|
43
|
+
"timestamp": "2026-03-05T02:38:14.713Z"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"escalations": [],
|
|
47
|
+
"dependencies": [],
|
|
48
|
+
"tags": [],
|
|
49
|
+
"storyPoints": 1
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[2026-03-05T02:38:46.450Z] US-001 — FAILED — Node.js API Removal — Execution failed
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "@nathapp/nax",
|
|
3
|
+
"feature": "v0.19.0-hardening",
|
|
4
|
+
"branchName": "feat/v0.19.0-sec",
|
|
5
|
+
"createdAt": "2026-03-05T02:58:00Z",
|
|
6
|
+
"updatedAt": "2026-03-05T03:13:29.408Z",
|
|
7
|
+
"userStories": [
|
|
8
|
+
{
|
|
9
|
+
"id": "US-001",
|
|
10
|
+
"title": "Security P0 Hardening",
|
|
11
|
+
"description": "Implement path validation for loaders (SEC-1, SEC-2), fix shell injection vectors (SEC-3, SEC-4), and respect dangerouslySkipPermissions config (SEC-5).",
|
|
12
|
+
"status": "passed",
|
|
13
|
+
"passes": true,
|
|
14
|
+
"attempts": 1,
|
|
15
|
+
"priorErrors": [],
|
|
16
|
+
"priorFailures": [],
|
|
17
|
+
"escalations": [],
|
|
18
|
+
"dependencies": [],
|
|
19
|
+
"tags": [],
|
|
20
|
+
"acceptanceCriteria": [],
|
|
21
|
+
"storyPoints": 1
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "US-002",
|
|
25
|
+
"title": "BUG-1 Parallel Race Condition",
|
|
26
|
+
"description": "Replace Promise.race loop with proper concurrency control in parallel executor.",
|
|
27
|
+
"status": "passed",
|
|
28
|
+
"passes": true,
|
|
29
|
+
"attempts": 1,
|
|
30
|
+
"priorErrors": [],
|
|
31
|
+
"priorFailures": [],
|
|
32
|
+
"escalations": [],
|
|
33
|
+
"dependencies": [],
|
|
34
|
+
"tags": [],
|
|
35
|
+
"acceptanceCriteria": [],
|
|
36
|
+
"storyPoints": 1
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "US-003",
|
|
40
|
+
"title": "Node.js API Removal",
|
|
41
|
+
"description": "Replace readFileSync, appendFileSync, existsSync, and setTimeout with Bun-native equivalents.",
|
|
42
|
+
"status": "passed",
|
|
43
|
+
"passes": true,
|
|
44
|
+
"attempts": 1,
|
|
45
|
+
"priorErrors": [],
|
|
46
|
+
"priorFailures": [],
|
|
47
|
+
"escalations": [],
|
|
48
|
+
"dependencies": [],
|
|
49
|
+
"tags": [],
|
|
50
|
+
"acceptanceCriteria": [],
|
|
51
|
+
"storyPoints": 1
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "US-004",
|
|
55
|
+
"title": "Type Fixes (v0.19.0)",
|
|
56
|
+
"description": "Fix the 18 type errors introduced during the Node.js API removal and security hardening in the src/ directory. Ensure all Bun-native APIs (Bun.file, Bun.write) are used correctly with valid types. Fix variable scopes and missing imports.",
|
|
57
|
+
"status": "failed",
|
|
58
|
+
"passes": false,
|
|
59
|
+
"attempts": 1,
|
|
60
|
+
"routing": {
|
|
61
|
+
"complexity": "medium",
|
|
62
|
+
"modelTier": "powerful",
|
|
63
|
+
"testStrategy": "test-after"
|
|
64
|
+
},
|
|
65
|
+
"priorErrors": [
|
|
66
|
+
"Attempt 1 failed with model tier: balanced: Stage requested escalation to higher tier"
|
|
67
|
+
],
|
|
68
|
+
"priorFailures": [
|
|
69
|
+
{
|
|
70
|
+
"attempt": 1,
|
|
71
|
+
"modelTier": "balanced",
|
|
72
|
+
"stage": "escalation",
|
|
73
|
+
"summary": "Failed with tier balanced, escalating to next tier",
|
|
74
|
+
"timestamp": "2026-03-05T03:12:59.254Z"
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"escalations": [],
|
|
78
|
+
"dependencies": [],
|
|
79
|
+
"tags": [],
|
|
80
|
+
"acceptanceCriteria": [],
|
|
81
|
+
"storyPoints": 1
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# v0.19.0 Hardening & Compliance
|
|
2
|
+
|
|
3
|
+
Address Security, Reliability, and Technical Debt findings from the 2026-03-04 audit.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
- SEC-1 to SEC-5: Complete security hardening (RCE, Shell, Permissions).
|
|
7
|
+
- BUG-1: Fix parallel concurrency race condition.
|
|
8
|
+
- BUG-3, BUG-5, MEM-2: Reliability fixes (metrics, mutation, memory leak).
|
|
9
|
+
- Technical Debt: Replace forbidden Node.js APIs (readFileSync, etc.) with Bun-native equivalents.
|
|
10
|
+
- Architecture: Split 400-line files and cleanup dead code.
|
|
11
|
+
|
|
12
|
+
## Acceptance Criteria
|
|
13
|
+
- SEC-1/2: Dynamic imports restricted to allowed roots.
|
|
14
|
+
- SEC-3/4: Shell injection via backticks/dollar-signs blocked.
|
|
15
|
+
- SEC-5: --dangerously-skip-permissions respects config.
|
|
16
|
+
- BUG-1: Parallel execution respects maxConcurrency exactly.
|
|
17
|
+
- Node.js APIs: All readFileSync/appendFileSync/setTimeout replaced with Bun equivalents.
|
|
18
|
+
- Files: cli/config.ts split below 400 lines.
|
package/nax/status.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"run": {
|
|
4
|
+
"id": "run-2026-03-05T02-37-04-540Z",
|
|
5
|
+
"feature": "nax-compliance",
|
|
6
|
+
"startedAt": "2026-03-05T02:37:04.540Z",
|
|
7
|
+
"status": "stalled",
|
|
8
|
+
"dryRun": false,
|
|
9
|
+
"pid": 814245
|
|
10
|
+
},
|
|
11
|
+
"progress": {
|
|
12
|
+
"total": 1,
|
|
13
|
+
"passed": 0,
|
|
14
|
+
"failed": 1,
|
|
15
|
+
"paused": 0,
|
|
16
|
+
"blocked": 0,
|
|
17
|
+
"pending": 0
|
|
18
|
+
},
|
|
19
|
+
"cost": {
|
|
20
|
+
"spent": 0,
|
|
21
|
+
"limit": 8
|
|
22
|
+
},
|
|
23
|
+
"current": null,
|
|
24
|
+
"iterations": 3,
|
|
25
|
+
"updatedAt": "2026-03-05T02:38:46.469Z",
|
|
26
|
+
"durationMs": 101929
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AgentAdapter } from "../agents/types";
|
|
9
|
-
import type { ModelDef } from "../config/schema";
|
|
9
|
+
import type { ModelDef, NaxConfig } from "../config/schema";
|
|
10
10
|
import { getLogger } from "../logger";
|
|
11
11
|
import type { PRD, UserStory } from "../prd/types";
|
|
12
12
|
|
|
@@ -70,6 +70,8 @@ export interface GenerateFixStoriesOptions {
|
|
|
70
70
|
workdir: string;
|
|
71
71
|
/** Model definition for LLM call */
|
|
72
72
|
modelDef: ModelDef;
|
|
73
|
+
/** Global config */
|
|
74
|
+
config: NaxConfig;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -224,7 +226,9 @@ export async function generateFixStories(
|
|
|
224
226
|
|
|
225
227
|
try {
|
|
226
228
|
// Call agent to generate fix description
|
|
227
|
-
const
|
|
229
|
+
const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
|
|
230
|
+
const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
|
|
231
|
+
const cmd = [adapter.binary, "--model", modelDef.model, ...permArgs, "-p", prompt];
|
|
228
232
|
|
|
229
233
|
const proc = Bun.spawn(cmd, {
|
|
230
234
|
cwd: workdir,
|
|
@@ -178,7 +178,9 @@ export async function generateAcceptanceTests(
|
|
|
178
178
|
|
|
179
179
|
try {
|
|
180
180
|
// Call agent to generate tests (using decompose as pattern)
|
|
181
|
-
const
|
|
181
|
+
const skipPerms = options.config.quality?.dangerouslySkipPermissions ?? true;
|
|
182
|
+
const permArgs = skipPerms ? ["--dangerously-skip-permissions"] : [];
|
|
183
|
+
const cmd = [adapter.binary, "--model", options.modelDef.model, ...permArgs, "-p", prompt];
|
|
182
184
|
|
|
183
185
|
const proc = Bun.spawn(cmd, {
|
|
184
186
|
cwd: options.workdir,
|
package/src/acceptance/types.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Types for generating acceptance tests from spec.md acceptance criteria.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ModelDef, ModelTier } from "../config/schema";
|
|
7
|
+
import type { ModelDef, ModelTier, NaxConfig } from "../config/schema";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A single acceptance criterion extracted from spec.md.
|
|
@@ -55,6 +55,8 @@ export interface GenerateAcceptanceTestsOptions {
|
|
|
55
55
|
modelTier: ModelTier;
|
|
56
56
|
/** Resolved model definition */
|
|
57
57
|
modelDef: ModelDef;
|
|
58
|
+
/** Global config for quality settings */
|
|
59
|
+
config: NaxConfig;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
/**
|
|
2
5
|
* Claude Code Plan Logic
|
|
3
6
|
*
|
|
@@ -96,9 +99,7 @@ export async function runPlan(
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
// Non-interactive: redirect stdout to temp file via Bun.file()
|
|
99
|
-
|
|
100
|
-
const { mkdtempSync, readFileSync, rmSync } = require("node:fs");
|
|
101
|
-
const { tmpdir } = require("node:os");
|
|
102
|
+
|
|
102
103
|
const tempDir = mkdtempSync(join(tmpdir(), "nax-plan-"));
|
|
103
104
|
const outFile = join(tempDir, "stdout.txt");
|
|
104
105
|
const errFile = join(tempDir, "stderr.txt");
|
|
@@ -120,8 +121,8 @@ export async function runPlan(
|
|
|
120
121
|
// Unregister PID after exit
|
|
121
122
|
await pidRegistry.unregister(proc.pid);
|
|
122
123
|
|
|
123
|
-
const specContent =
|
|
124
|
-
const conversationLog =
|
|
124
|
+
const specContent = await Bun.file(outFile).text();
|
|
125
|
+
const conversationLog = await Bun.file(errFile).text();
|
|
125
126
|
|
|
126
127
|
if (exitCode !== 0) {
|
|
127
128
|
throw new Error(`Plan mode failed with exit code ${exitCode}: ${conversationLog || "unknown error"}`);
|
package/src/agents/claude.ts
CHANGED
|
@@ -172,7 +172,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
172
172
|
const proc = Bun.spawn(cmd, {
|
|
173
173
|
cwd: options.workdir,
|
|
174
174
|
stdout: "pipe",
|
|
175
|
-
stderr: "
|
|
175
|
+
stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
|
|
176
176
|
env: this.buildAllowedEnv(options),
|
|
177
177
|
});
|
|
178
178
|
|
|
@@ -255,7 +255,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
255
255
|
const proc = Bun.spawn(cmd, {
|
|
256
256
|
cwd: options.workdir,
|
|
257
257
|
stdout: "pipe",
|
|
258
|
-
stderr: "
|
|
258
|
+
stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
|
|
259
259
|
env: this.buildAllowedEnv({
|
|
260
260
|
workdir: options.workdir,
|
|
261
261
|
modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
|
|
@@ -295,7 +295,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
295
295
|
env: { ...this.buildAllowedEnv(options), TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
296
296
|
stdin: "pipe",
|
|
297
297
|
stdout: "pipe",
|
|
298
|
-
stderr: "
|
|
298
|
+
stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
@@ -303,16 +303,26 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
303
303
|
|
|
304
304
|
// Stream stdout to onOutput callback
|
|
305
305
|
(async () => {
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
try {
|
|
307
|
+
for await (const chunk of proc.stdout) {
|
|
308
|
+
options.onOutput(Buffer.from(chunk));
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
// BUG-21: Handle stream errors to avoid unhandled rejections
|
|
312
|
+
getLogger()?.error("agent", "runInteractive stdout error", { err });
|
|
308
313
|
}
|
|
309
314
|
})();
|
|
310
315
|
|
|
311
316
|
// Fire onExit when process completes
|
|
312
|
-
proc.exited
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
proc.exited
|
|
318
|
+
.then((code) => {
|
|
319
|
+
pidRegistry.unregister(proc.pid).catch(() => {});
|
|
320
|
+
options.onExit(code ?? 1);
|
|
321
|
+
})
|
|
322
|
+
.catch((err) => {
|
|
323
|
+
// BUG-22: Guard against onExit or unregister throws
|
|
324
|
+
getLogger()?.error("agent", "runInteractive exit error", { err });
|
|
325
|
+
});
|
|
316
326
|
|
|
317
327
|
return {
|
|
318
328
|
write: (data: string) => {
|
package/src/cli/analyze.ts
CHANGED
package/src/cli/init.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Initializes nax configuration directories and files.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { mkdir } from "node:fs/promises";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { globalConfigDir, projectConfigDir } from "../config/paths";
|
|
10
11
|
import { DEFAULT_CONFIG } from "../config/schema";
|
|
@@ -34,7 +35,7 @@ async function updateGitignore(projectRoot: string): Promise<void> {
|
|
|
34
35
|
|
|
35
36
|
let existing = "";
|
|
36
37
|
if (existsSync(gitignorePath)) {
|
|
37
|
-
existing =
|
|
38
|
+
existing = await Bun.file(gitignorePath).text();
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const missingEntries = NAX_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
|
|
@@ -95,7 +96,7 @@ async function initGlobal(): Promise<void> {
|
|
|
95
96
|
|
|
96
97
|
// Create ~/.nax if it doesn't exist
|
|
97
98
|
if (!existsSync(globalDir)) {
|
|
98
|
-
|
|
99
|
+
await mkdir(globalDir, { recursive: true });
|
|
99
100
|
logger.info("init", "Created global config directory", { path: globalDir });
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -120,7 +121,7 @@ async function initGlobal(): Promise<void> {
|
|
|
120
121
|
// Create ~/.nax/hooks/ directory if it doesn't exist
|
|
121
122
|
const hooksDir = join(globalDir, "hooks");
|
|
122
123
|
if (!existsSync(hooksDir)) {
|
|
123
|
-
|
|
124
|
+
await mkdir(hooksDir, { recursive: true });
|
|
124
125
|
logger.info("init", "Created global hooks directory", { path: hooksDir });
|
|
125
126
|
} else {
|
|
126
127
|
logger.info("init", "Global hooks directory already exists", { path: hooksDir });
|
|
@@ -138,7 +139,7 @@ async function initProject(projectRoot: string): Promise<void> {
|
|
|
138
139
|
|
|
139
140
|
// Create nax/ directory if it doesn't exist
|
|
140
141
|
if (!existsSync(projectDir)) {
|
|
141
|
-
|
|
142
|
+
await mkdir(projectDir, { recursive: true });
|
|
142
143
|
logger.info("init", "Created project config directory", { path: projectDir });
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -163,7 +164,7 @@ async function initProject(projectRoot: string): Promise<void> {
|
|
|
163
164
|
// Create nax/hooks/ directory if it doesn't exist
|
|
164
165
|
const hooksDir = join(projectDir, "hooks");
|
|
165
166
|
if (!existsSync(hooksDir)) {
|
|
166
|
-
|
|
167
|
+
await mkdir(hooksDir, { recursive: true });
|
|
167
168
|
logger.info("init", "Created project hooks directory", { path: hooksDir });
|
|
168
169
|
} else {
|
|
169
170
|
logger.info("init", "Project hooks directory already exists", { path: hooksDir });
|
package/src/config/defaults.ts
CHANGED
|
@@ -80,6 +80,7 @@ export const DEFAULT_CONFIG: NaxConfig = {
|
|
|
80
80
|
detectOpenHandles: true,
|
|
81
81
|
detectOpenHandlesRetries: 1,
|
|
82
82
|
gracePeriodMs: 5000,
|
|
83
|
+
dangerouslySkipPermissions: true,
|
|
83
84
|
drainTimeoutMs: 2000,
|
|
84
85
|
shell: "/bin/sh",
|
|
85
86
|
stripEnvVars: ["CLAUDECODE", "REPL_ID", "AGENT"],
|
package/src/config/types.ts
CHANGED
|
@@ -145,6 +145,8 @@ export interface QualityConfig {
|
|
|
145
145
|
detectOpenHandlesRetries: number;
|
|
146
146
|
/** Grace period in ms after SIGTERM before sending SIGKILL (default: 5000) */
|
|
147
147
|
gracePeriodMs: number;
|
|
148
|
+
/** Use --dangerously-skip-permissions for agent sessions (default: false) */
|
|
149
|
+
dangerouslySkipPermissions: boolean;
|
|
148
150
|
/** Deadline in ms to drain stdout/stderr after killing process (Bun stream workaround, default: 2000) */
|
|
149
151
|
drainTimeoutMs: number;
|
|
150
152
|
/** Shell to use for running verification commands (default: /bin/sh) */
|
package/src/context/injector.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Ruby (Gemfile), Java/Kotlin (pom.xml / build.gradle).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { NaxConfig } from "../config";
|
|
13
13
|
import type { ProjectMetadata } from "./types";
|
|
@@ -68,12 +68,12 @@ async function detectNode(workdir: string): Promise<{ name?: string; lang: strin
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Go: read go.mod for module name + direct dependencies */
|
|
71
|
-
function detectGo(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
71
|
+
async function detectGo(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
72
72
|
const goMod = join(workdir, "go.mod");
|
|
73
73
|
if (!existsSync(goMod)) return null;
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
|
-
const content =
|
|
76
|
+
const content = await Bun.file(goMod).text();
|
|
77
77
|
const moduleMatch = content.match(/^module\s+(\S+)/m);
|
|
78
78
|
const name = moduleMatch?.[1];
|
|
79
79
|
|
|
@@ -95,12 +95,12 @@ function detectGo(workdir: string): { name?: string; lang: string; dependencies:
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Rust: read Cargo.toml for package name + dependencies */
|
|
98
|
-
function detectRust(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
98
|
+
async function detectRust(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
99
99
|
const cargoPath = join(workdir, "Cargo.toml");
|
|
100
100
|
if (!existsSync(cargoPath)) return null;
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
|
-
const content =
|
|
103
|
+
const content = await Bun.file(cargoPath).text();
|
|
104
104
|
const nameMatch = content.match(/^\[package\][^[]*name\s*=\s*"([^"]+)"/ms);
|
|
105
105
|
const name = nameMatch?.[1];
|
|
106
106
|
|
|
@@ -119,7 +119,7 @@ function detectRust(workdir: string): { name?: string; lang: string; dependencie
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/** Python: read pyproject.toml or requirements.txt */
|
|
122
|
-
function detectPython(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
122
|
+
async function detectPython(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
123
123
|
const pyproject = join(workdir, "pyproject.toml");
|
|
124
124
|
const requirements = join(workdir, "requirements.txt");
|
|
125
125
|
|
|
@@ -127,7 +127,7 @@ function detectPython(workdir: string): { name?: string; lang: string; dependenc
|
|
|
127
127
|
|
|
128
128
|
try {
|
|
129
129
|
if (existsSync(pyproject)) {
|
|
130
|
-
const content =
|
|
130
|
+
const content = await Bun.file(pyproject).text();
|
|
131
131
|
const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
132
132
|
const depsSection = content.match(/^\[project\][^[]*dependencies\s*=\s*\[([^\]]*)\]/ms)?.[1] ?? "";
|
|
133
133
|
const deps = depsSection
|
|
@@ -139,7 +139,7 @@ function detectPython(workdir: string): { name?: string; lang: string; dependenc
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// Fallback: requirements.txt
|
|
142
|
-
const lines =
|
|
142
|
+
const lines = (await Bun.file(requirements).text())
|
|
143
143
|
.split("\n")
|
|
144
144
|
.map((l) => l.split(/[>=<!]/)[0].trim())
|
|
145
145
|
.filter((l) => l && !l.startsWith("#"))
|
|
@@ -169,12 +169,12 @@ async function detectPhp(workdir: string): Promise<{ name?: string; lang: string
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/** Ruby: read Gemfile */
|
|
172
|
-
function detectRuby(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
172
|
+
async function detectRuby(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
173
173
|
const gemfile = join(workdir, "Gemfile");
|
|
174
174
|
if (!existsSync(gemfile)) return null;
|
|
175
175
|
|
|
176
176
|
try {
|
|
177
|
-
const content =
|
|
177
|
+
const content = await Bun.file(gemfile).text();
|
|
178
178
|
const gems = [...content.matchAll(/^\s*gem\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]).slice(0, 10);
|
|
179
179
|
return { lang: "Ruby", dependencies: gems };
|
|
180
180
|
} catch {
|
|
@@ -183,7 +183,7 @@ function detectRuby(workdir: string): { name?: string; lang: string; dependencie
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** Java/Kotlin: detect from pom.xml or build.gradle */
|
|
186
|
-
function detectJvm(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
|
|
186
|
+
async function detectJvm(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
|
|
187
187
|
const pom = join(workdir, "pom.xml");
|
|
188
188
|
const gradle = join(workdir, "build.gradle");
|
|
189
189
|
const gradleKts = join(workdir, "build.gradle.kts");
|
|
@@ -192,7 +192,7 @@ function detectJvm(workdir: string): { name?: string; lang: string; dependencies
|
|
|
192
192
|
|
|
193
193
|
try {
|
|
194
194
|
if (existsSync(pom)) {
|
|
195
|
-
const content =
|
|
195
|
+
const content = await Bun.file(pom).text();
|
|
196
196
|
const nameMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
197
197
|
const deps = [...content.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)]
|
|
198
198
|
.map((m) => m[1])
|
|
@@ -203,7 +203,7 @@ function detectJvm(workdir: string): { name?: string; lang: string; dependencies
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
const gradleFile = existsSync(gradleKts) ? gradleKts : gradle;
|
|
206
|
-
const content =
|
|
206
|
+
const content = await Bun.file(gradleFile).text();
|
|
207
207
|
const lang = gradleFile.endsWith(".kts") ? "Kotlin" : "Java";
|
|
208
208
|
const deps = [...content.matchAll(/implementation[^'"]*['"]([^:'"]+:[^:'"]+)[^'"]*['"]/g)]
|
|
209
209
|
.map((m) => m[1].split(":").pop() ?? m[1])
|
|
@@ -223,12 +223,12 @@ function detectJvm(workdir: string): { name?: string; lang: string; dependencies
|
|
|
223
223
|
export async function buildProjectMetadata(workdir: string, config: NaxConfig): Promise<ProjectMetadata> {
|
|
224
224
|
// Priority: Go > Rust > Python > PHP > Ruby > JVM > Node
|
|
225
225
|
const detected =
|
|
226
|
-
detectGo(workdir) ??
|
|
227
|
-
detectRust(workdir) ??
|
|
228
|
-
detectPython(workdir) ??
|
|
226
|
+
(await detectGo(workdir)) ??
|
|
227
|
+
(await detectRust(workdir)) ??
|
|
228
|
+
(await detectPython(workdir)) ??
|
|
229
229
|
(await detectPhp(workdir)) ??
|
|
230
|
-
detectRuby(workdir) ??
|
|
231
|
-
detectJvm(workdir) ??
|
|
230
|
+
(await detectRuby(workdir)) ??
|
|
231
|
+
(await detectJvm(workdir)) ??
|
|
232
232
|
(await detectNode(workdir));
|
|
233
233
|
|
|
234
234
|
return {
|