@nathapp/nax 0.65.2 → 0.65.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/dist/nax.js +130 -26
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -32506,7 +32506,8 @@ function acceptanceTestFilename(language) {
|
|
|
32506
32506
|
}
|
|
32507
32507
|
}
|
|
32508
32508
|
function resolveAcceptanceTestFile(language, testPathConfig) {
|
|
32509
|
-
|
|
32509
|
+
const candidate = testPathConfig ?? acceptanceTestFilename(language);
|
|
32510
|
+
return sanitizeTestFileName(candidate, "acceptance.testPath");
|
|
32510
32511
|
}
|
|
32511
32512
|
function resolveAcceptanceFeatureTestPath(featureDir, testPathConfig, language) {
|
|
32512
32513
|
return path3.join(featureDir, resolveAcceptanceTestFile(language, testPathConfig));
|
|
@@ -32560,7 +32561,21 @@ function suggestedTestFilename(language) {
|
|
|
32560
32561
|
}
|
|
32561
32562
|
}
|
|
32562
32563
|
function resolveSuggestedTestFile(language, testPathConfig) {
|
|
32563
|
-
|
|
32564
|
+
const candidate = testPathConfig ?? suggestedTestFilename(language);
|
|
32565
|
+
return sanitizeTestFileName(candidate, "acceptance.suggestedTestPath");
|
|
32566
|
+
}
|
|
32567
|
+
function sanitizeTestFileName(value, fieldName) {
|
|
32568
|
+
const filename = value.trim();
|
|
32569
|
+
if (filename.length === 0) {
|
|
32570
|
+
throw new Error(`${fieldName} must be non-empty`);
|
|
32571
|
+
}
|
|
32572
|
+
if (filename.includes("/") || filename.includes("\\")) {
|
|
32573
|
+
throw new Error(`${fieldName} must be a filename, not a path: ${filename}`);
|
|
32574
|
+
}
|
|
32575
|
+
if (filename.includes("..")) {
|
|
32576
|
+
throw new Error(`${fieldName} cannot contain '..': ${filename}`);
|
|
32577
|
+
}
|
|
32578
|
+
return filename;
|
|
32564
32579
|
}
|
|
32565
32580
|
function resolveSuggestedPackageFeatureTestPath(packageDir, featureName, testPathConfig, language) {
|
|
32566
32581
|
return path3.join(packageDir, ".nax", "features", featureName, resolveSuggestedTestFile(language, testPathConfig));
|
|
@@ -33915,6 +33930,20 @@ function killProcessGroup(pid, signal) {
|
|
|
33915
33930
|
|
|
33916
33931
|
// src/quality/runner.ts
|
|
33917
33932
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
33933
|
+
function createDrainDeadline(deadlineMs) {
|
|
33934
|
+
let timeoutId;
|
|
33935
|
+
const promise2 = new Promise((resolve11) => {
|
|
33936
|
+
timeoutId = setTimeout(() => resolve11(""), deadlineMs);
|
|
33937
|
+
});
|
|
33938
|
+
return {
|
|
33939
|
+
promise: promise2,
|
|
33940
|
+
cancel: () => {
|
|
33941
|
+
if (timeoutId !== undefined) {
|
|
33942
|
+
clearTimeout(timeoutId);
|
|
33943
|
+
}
|
|
33944
|
+
}
|
|
33945
|
+
};
|
|
33946
|
+
}
|
|
33918
33947
|
async function runQualityCommand(opts) {
|
|
33919
33948
|
const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
|
|
33920
33949
|
const startTime = Date.now();
|
|
@@ -33947,11 +33976,22 @@ async function runQualityCommand(opts) {
|
|
|
33947
33976
|
}
|
|
33948
33977
|
}, SIGKILL_GRACE_PERIOD_MS);
|
|
33949
33978
|
}, timeoutMs);
|
|
33950
|
-
const
|
|
33951
|
-
|
|
33952
|
-
|
|
33953
|
-
|
|
33954
|
-
|
|
33979
|
+
const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
|
|
33980
|
+
const stderrPromise = new Response(proc.stderr).text().catch(() => "");
|
|
33981
|
+
const exitCode = await proc.exited;
|
|
33982
|
+
const [stdout, stderr] = timedOut ? await (async () => {
|
|
33983
|
+
const stdoutDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
|
|
33984
|
+
const stderrDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
|
|
33985
|
+
try {
|
|
33986
|
+
return await Promise.all([
|
|
33987
|
+
Promise.race([stdoutPromise, stdoutDrain.promise]),
|
|
33988
|
+
Promise.race([stderrPromise, stderrDrain.promise])
|
|
33989
|
+
]);
|
|
33990
|
+
} finally {
|
|
33991
|
+
stdoutDrain.cancel();
|
|
33992
|
+
stderrDrain.cancel();
|
|
33993
|
+
}
|
|
33994
|
+
})() : await Promise.all([stdoutPromise, stderrPromise]);
|
|
33955
33995
|
clearTimeout(killTimer);
|
|
33956
33996
|
if (sigkillTimer !== undefined) {
|
|
33957
33997
|
clearTimeout(sigkillTimer);
|
|
@@ -34003,7 +34043,7 @@ async function runQualityCommand(opts) {
|
|
|
34003
34043
|
};
|
|
34004
34044
|
}
|
|
34005
34045
|
}
|
|
34006
|
-
var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, _qualityRunnerDeps;
|
|
34046
|
+
var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, STREAM_DRAIN_TIMEOUT_MS = 2000, _qualityRunnerDeps;
|
|
34007
34047
|
var init_runner = __esm(() => {
|
|
34008
34048
|
init_logger2();
|
|
34009
34049
|
_qualityRunnerDeps = {
|
|
@@ -34368,6 +34408,8 @@ function buildSmartTestCommand(testFiles, baseCommand) {
|
|
|
34368
34408
|
if (testFiles.length === 0) {
|
|
34369
34409
|
return baseCommand;
|
|
34370
34410
|
}
|
|
34411
|
+
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
34412
|
+
const quotedTestFiles = testFiles.map(shellQuote);
|
|
34371
34413
|
const parts = baseCommand.trim().split(/\s+/);
|
|
34372
34414
|
let lastPathIndex = -1;
|
|
34373
34415
|
for (let i = parts.length - 1;i >= 0; i--) {
|
|
@@ -34377,11 +34419,11 @@ function buildSmartTestCommand(testFiles, baseCommand) {
|
|
|
34377
34419
|
}
|
|
34378
34420
|
}
|
|
34379
34421
|
if (lastPathIndex === -1) {
|
|
34380
|
-
return `${baseCommand} ${
|
|
34422
|
+
return `${baseCommand} ${quotedTestFiles.join(" ")}`;
|
|
34381
34423
|
}
|
|
34382
34424
|
const beforePath = parts.slice(0, lastPathIndex);
|
|
34383
34425
|
const afterPath = parts.slice(lastPathIndex + 1);
|
|
34384
|
-
const newParts = [...beforePath, ...
|
|
34426
|
+
const newParts = [...beforePath, ...quotedTestFiles, ...afterPath];
|
|
34385
34427
|
return newParts.join(" ");
|
|
34386
34428
|
}
|
|
34387
34429
|
async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileRegex = [], naxIgnoreIndex, repoRoot) {
|
|
@@ -34468,7 +34510,8 @@ function coerceSmartRunner(val) {
|
|
|
34468
34510
|
}
|
|
34469
34511
|
function buildScopedCommand(testFiles, baseCommand, testScopedTemplate) {
|
|
34470
34512
|
if (testScopedTemplate) {
|
|
34471
|
-
|
|
34513
|
+
const quotedFiles = testFiles.map((file3) => `'${file3.replaceAll("'", "'\\''")}'`);
|
|
34514
|
+
return testScopedTemplate.replace("{{files}}", quotedFiles.join(" "));
|
|
34472
34515
|
}
|
|
34473
34516
|
return _scopedDeps.buildSmartTestCommand(testFiles, baseCommand);
|
|
34474
34517
|
}
|
|
@@ -35262,6 +35305,23 @@ var init_operations = __esm(() => {
|
|
|
35262
35305
|
init_auto_approve();
|
|
35263
35306
|
});
|
|
35264
35307
|
|
|
35308
|
+
// src/utils/feature-name.ts
|
|
35309
|
+
function validateFeatureName(feature) {
|
|
35310
|
+
if (!feature || feature.trim() === "") {
|
|
35311
|
+
throw new Error("Feature name must be non-empty");
|
|
35312
|
+
}
|
|
35313
|
+
if (feature.includes("/") || feature.includes("\\")) {
|
|
35314
|
+
throw new Error(`Feature name must be a single path segment: ${feature}`);
|
|
35315
|
+
}
|
|
35316
|
+
if (feature.includes("..")) {
|
|
35317
|
+
throw new Error(`Feature name cannot contain '..': ${feature}`);
|
|
35318
|
+
}
|
|
35319
|
+
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
35320
|
+
if (!validPattern.test(feature)) {
|
|
35321
|
+
throw new Error(`Feature name contains invalid characters: ${feature}`);
|
|
35322
|
+
}
|
|
35323
|
+
}
|
|
35324
|
+
|
|
35265
35325
|
// src/cli/plan-helpers.ts
|
|
35266
35326
|
import { createInterface } from "readline";
|
|
35267
35327
|
function createCliInteractionBridge() {
|
|
@@ -39424,7 +39484,7 @@ class SessionManager {
|
|
|
39424
39484
|
_pidRegistry;
|
|
39425
39485
|
_watchdogControllerRegistry;
|
|
39426
39486
|
_onStreamActivity;
|
|
39427
|
-
|
|
39487
|
+
_watchdogCancelledCallsBySession = new Map;
|
|
39428
39488
|
_agentStreamUnsubscribe;
|
|
39429
39489
|
constructor(opts) {
|
|
39430
39490
|
this._getAdapter = opts?.getAdapter ?? (() => {
|
|
@@ -39454,22 +39514,26 @@ class SessionManager {
|
|
|
39454
39514
|
this._agentStreamUnsubscribe = opts.agentStreamEvents.onAgentStream((event) => {
|
|
39455
39515
|
if (event.kind === "agent.call_ended") {
|
|
39456
39516
|
this._watchdogControllerRegistry?.delete(event.callId);
|
|
39457
|
-
this._watchdogCancelledCalls.delete(event.callId);
|
|
39458
39517
|
}
|
|
39459
39518
|
});
|
|
39460
39519
|
}
|
|
39461
39520
|
}
|
|
39462
|
-
_buildOnActiveCall() {
|
|
39521
|
+
_buildOnActiveCall(sessionName) {
|
|
39463
39522
|
const registry2 = this._watchdogControllerRegistry;
|
|
39464
39523
|
if (!registry2)
|
|
39465
39524
|
return;
|
|
39466
39525
|
return (callId, cancel) => {
|
|
39467
39526
|
registry2.set(callId, async () => {
|
|
39468
|
-
this.
|
|
39527
|
+
const cancelledCalls = this._watchdogCancelledCallsBySession.get(sessionName) ?? new Set;
|
|
39528
|
+
cancelledCalls.add(callId);
|
|
39529
|
+
this._watchdogCancelledCallsBySession.set(sessionName, cancelledCalls);
|
|
39469
39530
|
await cancel();
|
|
39470
39531
|
});
|
|
39471
39532
|
};
|
|
39472
39533
|
}
|
|
39534
|
+
_clearWatchdogCancelledCalls(sessionName) {
|
|
39535
|
+
this._watchdogCancelledCallsBySession.delete(sessionName);
|
|
39536
|
+
}
|
|
39473
39537
|
_persistDescriptor(descriptor) {
|
|
39474
39538
|
if (!descriptor.scratchDir)
|
|
39475
39539
|
return;
|
|
@@ -39692,7 +39756,7 @@ class SessionManager {
|
|
|
39692
39756
|
onSessionEstablished: opts.onSessionEstablished,
|
|
39693
39757
|
signal: opts.signal,
|
|
39694
39758
|
resume,
|
|
39695
|
-
onActiveCall: this._buildOnActiveCall(),
|
|
39759
|
+
onActiveCall: this._buildOnActiveCall(name),
|
|
39696
39760
|
onStreamActivity: this._onStreamActivity
|
|
39697
39761
|
});
|
|
39698
39762
|
this._liveHandles.set(name, handle);
|
|
@@ -39779,8 +39843,7 @@ class SessionManager {
|
|
|
39779
39843
|
return { ...result, protocolIds: result.protocolIds ?? handle.protocolIds };
|
|
39780
39844
|
} catch (err) {
|
|
39781
39845
|
if (err instanceof SessionTurnError && err.cancelled) {
|
|
39782
|
-
const wasWatchdog = this.
|
|
39783
|
-
this._watchdogCancelledCalls.clear();
|
|
39846
|
+
const wasWatchdog = (this._watchdogCancelledCallsBySession.get(handle.id)?.size ?? 0) > 0;
|
|
39784
39847
|
if (wasWatchdog) {
|
|
39785
39848
|
throw new SessionFailureError("idle watchdog cancelled session \u2014 no stream activity", {
|
|
39786
39849
|
category: "availability",
|
|
@@ -39799,6 +39862,7 @@ class SessionManager {
|
|
|
39799
39862
|
}
|
|
39800
39863
|
throw err;
|
|
39801
39864
|
} finally {
|
|
39865
|
+
this._clearWatchdogCancelledCalls(handle.id);
|
|
39802
39866
|
this._busySessions.delete(handle.id);
|
|
39803
39867
|
}
|
|
39804
39868
|
}
|
|
@@ -40478,8 +40542,10 @@ import { basename as basename5, join as join28 } from "path";
|
|
|
40478
40542
|
function createRuntime(config2, workdir, opts) {
|
|
40479
40543
|
const runId = crypto.randomUUID();
|
|
40480
40544
|
const controller = new AbortController;
|
|
40545
|
+
let parentAbortHandler;
|
|
40481
40546
|
if (opts?.parentSignal) {
|
|
40482
|
-
|
|
40547
|
+
parentAbortHandler = () => controller.abort(opts.parentSignal?.reason);
|
|
40548
|
+
opts.parentSignal.addEventListener("abort", parentAbortHandler, { once: true });
|
|
40483
40549
|
}
|
|
40484
40550
|
const configLoader = createConfigLoader(config2);
|
|
40485
40551
|
const dispatchEvents = new DispatchEventBus;
|
|
@@ -40580,6 +40646,9 @@ function createRuntime(config2, workdir, opts) {
|
|
|
40580
40646
|
offReviewAudit();
|
|
40581
40647
|
offAgentStreamLogging();
|
|
40582
40648
|
offWatchdog();
|
|
40649
|
+
if (opts?.parentSignal && parentAbortHandler) {
|
|
40650
|
+
opts.parentSignal.removeEventListener("abort", parentAbortHandler);
|
|
40651
|
+
}
|
|
40583
40652
|
const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
|
|
40584
40653
|
for (const r of results) {
|
|
40585
40654
|
if (r.status === "rejected") {
|
|
@@ -42299,6 +42368,11 @@ Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, co
|
|
|
42299
42368
|
}
|
|
42300
42369
|
let featureDir;
|
|
42301
42370
|
if (feature) {
|
|
42371
|
+
try {
|
|
42372
|
+
validateFeatureName(feature);
|
|
42373
|
+
} catch (error48) {
|
|
42374
|
+
throw new NaxError(error48.message, "FEATURE_INVALID", { feature });
|
|
42375
|
+
}
|
|
42302
42376
|
const featuresDir = join32(naxDir, "features");
|
|
42303
42377
|
featureDir = join32(featuresDir, feature);
|
|
42304
42378
|
if (!existsSync16(featureDir)) {
|
|
@@ -54459,6 +54533,20 @@ var init_command_argv = __esm(() => {
|
|
|
54459
54533
|
|
|
54460
54534
|
// src/hooks/runner.ts
|
|
54461
54535
|
import { join as join67 } from "path";
|
|
54536
|
+
function createDrainDeadline2(deadlineMs) {
|
|
54537
|
+
let timeoutId;
|
|
54538
|
+
const promise2 = new Promise((resolve16) => {
|
|
54539
|
+
timeoutId = setTimeout(() => resolve16(""), deadlineMs);
|
|
54540
|
+
});
|
|
54541
|
+
return {
|
|
54542
|
+
promise: promise2,
|
|
54543
|
+
cancel: () => {
|
|
54544
|
+
if (timeoutId !== undefined) {
|
|
54545
|
+
clearTimeout(timeoutId);
|
|
54546
|
+
}
|
|
54547
|
+
}
|
|
54548
|
+
};
|
|
54549
|
+
}
|
|
54462
54550
|
async function loadHooksConfig(projectDir, globalDir) {
|
|
54463
54551
|
let globalHooks = { hooks: {} };
|
|
54464
54552
|
let projectHooks = { hooks: {} };
|
|
@@ -54561,15 +54649,30 @@ async function executeHook(hookDef, ctx, workdir) {
|
|
|
54561
54649
|
stderr: "pipe",
|
|
54562
54650
|
env: buildAllowedEnv({ env: env2 })
|
|
54563
54651
|
});
|
|
54652
|
+
let timedOut = false;
|
|
54564
54653
|
const timeoutId = setTimeout(() => {
|
|
54654
|
+
timedOut = true;
|
|
54565
54655
|
killProcessGroup(proc.pid, "SIGTERM");
|
|
54566
54656
|
}, timeout);
|
|
54657
|
+
const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
|
|
54658
|
+
const stderrPromise = new Response(proc.stderr).text().catch(() => "");
|
|
54567
54659
|
const exitCode = await proc.exited;
|
|
54568
54660
|
clearTimeout(timeoutId);
|
|
54569
|
-
const stdout = await
|
|
54570
|
-
|
|
54661
|
+
const [stdout, stderr] = timedOut ? await (async () => {
|
|
54662
|
+
const stdoutDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
|
|
54663
|
+
const stderrDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
|
|
54664
|
+
try {
|
|
54665
|
+
return await Promise.all([
|
|
54666
|
+
Promise.race([stdoutPromise, stdoutDrain.promise]),
|
|
54667
|
+
Promise.race([stderrPromise, stderrDrain.promise])
|
|
54668
|
+
]);
|
|
54669
|
+
} finally {
|
|
54670
|
+
stdoutDrain.cancel();
|
|
54671
|
+
stderrDrain.cancel();
|
|
54672
|
+
}
|
|
54673
|
+
})() : await Promise.all([stdoutPromise, stderrPromise]);
|
|
54571
54674
|
const output = (stdout + stderr).trim();
|
|
54572
|
-
if (
|
|
54675
|
+
if (timedOut) {
|
|
54573
54676
|
return {
|
|
54574
54677
|
success: false,
|
|
54575
54678
|
output: `Hook timed out after ${timeout}ms`
|
|
@@ -54607,7 +54710,7 @@ async function fireHook(config2, event, ctx, workdir) {
|
|
|
54607
54710
|
}
|
|
54608
54711
|
}
|
|
54609
54712
|
}
|
|
54610
|
-
var DEFAULT_TIMEOUT = 5000;
|
|
54713
|
+
var DEFAULT_TIMEOUT = 5000, STREAM_DRAIN_TIMEOUT_MS2 = 2000;
|
|
54611
54714
|
var init_runner5 = __esm(() => {
|
|
54612
54715
|
init_env();
|
|
54613
54716
|
init_logger2();
|
|
@@ -54625,7 +54728,7 @@ var package_default;
|
|
|
54625
54728
|
var init_package = __esm(() => {
|
|
54626
54729
|
package_default = {
|
|
54627
54730
|
name: "@nathapp/nax",
|
|
54628
|
-
version: "0.65.
|
|
54731
|
+
version: "0.65.3",
|
|
54629
54732
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
54630
54733
|
type: "module",
|
|
54631
54734
|
bin: {
|
|
@@ -54711,8 +54814,8 @@ var init_version = __esm(() => {
|
|
|
54711
54814
|
NAX_VERSION = package_default.version;
|
|
54712
54815
|
NAX_COMMIT = (() => {
|
|
54713
54816
|
try {
|
|
54714
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
54715
|
-
return "
|
|
54817
|
+
if (/^[0-9a-f]{6,10}$/.test("9ff2ea7d"))
|
|
54818
|
+
return "9ff2ea7d";
|
|
54716
54819
|
} catch {}
|
|
54717
54820
|
try {
|
|
54718
54821
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -91422,6 +91525,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
91422
91525
|
if (!existsSync15(naxDir)) {
|
|
91423
91526
|
throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
|
|
91424
91527
|
}
|
|
91528
|
+
validateFeatureName(options.feature);
|
|
91425
91529
|
const logger = getLogger();
|
|
91426
91530
|
logger?.info("plan", "Reading spec", { from: options.from });
|
|
91427
91531
|
const specContent = await _planDeps.readFile(options.from);
|