@nathapp/nax 0.41.0 → 0.42.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/README.md +1 -0
- package/bin/nax.ts +130 -11
- package/dist/nax.js +478 -186
- package/package.json +7 -6
- package/src/agents/acp/adapter.ts +3 -5
- package/src/agents/claude.ts +12 -2
- package/src/analyze/scanner.ts +16 -20
- package/src/cli/plan.ts +211 -145
- package/src/commands/precheck.ts +1 -1
- package/src/interaction/plugins/webhook.ts +10 -1
- package/src/prd/schema.ts +249 -0
- package/src/tdd/session-runner.ts +11 -2
- package/src/utils/git.ts +30 -0
- package/src/verification/runners.ts +10 -1
package/dist/nax.js
CHANGED
|
@@ -18253,7 +18253,7 @@ class ClaudeCodeAdapter {
|
|
|
18253
18253
|
const backoffMs = 2 ** attempt * 1000;
|
|
18254
18254
|
const logger = getLogger();
|
|
18255
18255
|
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
18256
|
-
await
|
|
18256
|
+
await _claudeAdapterDeps.sleep(backoffMs);
|
|
18257
18257
|
continue;
|
|
18258
18258
|
}
|
|
18259
18259
|
return result;
|
|
@@ -18269,7 +18269,7 @@ class ClaudeCodeAdapter {
|
|
|
18269
18269
|
attempt,
|
|
18270
18270
|
maxRetries
|
|
18271
18271
|
});
|
|
18272
|
-
await
|
|
18272
|
+
await _claudeAdapterDeps.sleep(backoffMs);
|
|
18273
18273
|
continue;
|
|
18274
18274
|
}
|
|
18275
18275
|
throw lastError;
|
|
@@ -18349,7 +18349,7 @@ class ClaudeCodeAdapter {
|
|
|
18349
18349
|
return runInteractiveMode(this.binary, options, pidRegistry);
|
|
18350
18350
|
}
|
|
18351
18351
|
}
|
|
18352
|
-
var _decomposeDeps;
|
|
18352
|
+
var _decomposeDeps, _claudeAdapterDeps;
|
|
18353
18353
|
var init_claude = __esm(() => {
|
|
18354
18354
|
init_pid_registry();
|
|
18355
18355
|
init_logger2();
|
|
@@ -18362,6 +18362,9 @@ var init_claude = __esm(() => {
|
|
|
18362
18362
|
return Bun.spawn(cmd, opts);
|
|
18363
18363
|
}
|
|
18364
18364
|
};
|
|
18365
|
+
_claudeAdapterDeps = {
|
|
18366
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
18367
|
+
};
|
|
18365
18368
|
});
|
|
18366
18369
|
|
|
18367
18370
|
// src/utils/errors.ts
|
|
@@ -19401,7 +19404,7 @@ class AcpAgentAdapter {
|
|
|
19401
19404
|
break;
|
|
19402
19405
|
}
|
|
19403
19406
|
}
|
|
19404
|
-
if (turnCount >= MAX_TURNS) {
|
|
19407
|
+
if (turnCount >= MAX_TURNS && options.interactionBridge) {
|
|
19405
19408
|
getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
|
|
19406
19409
|
}
|
|
19407
19410
|
} finally {
|
|
@@ -19461,9 +19464,6 @@ class AcpAgentAdapter {
|
|
|
19461
19464
|
}
|
|
19462
19465
|
}
|
|
19463
19466
|
async plan(options) {
|
|
19464
|
-
if (options.interactive) {
|
|
19465
|
-
throw new Error("[acp-adapter] plan() interactive mode is not yet supported via ACP");
|
|
19466
|
-
}
|
|
19467
19467
|
const modelDef = options.modelDef ?? { provider: "anthropic", model: "default" };
|
|
19468
19468
|
const timeoutSeconds = options.timeoutSeconds ?? options.config?.execution?.sessionTimeoutSeconds ?? 600;
|
|
19469
19469
|
const result = await this.run({
|
|
@@ -21807,7 +21807,7 @@ var package_default;
|
|
|
21807
21807
|
var init_package = __esm(() => {
|
|
21808
21808
|
package_default = {
|
|
21809
21809
|
name: "@nathapp/nax",
|
|
21810
|
-
version: "0.
|
|
21810
|
+
version: "0.42.0",
|
|
21811
21811
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21812
21812
|
type: "module",
|
|
21813
21813
|
bin: {
|
|
@@ -21819,11 +21819,12 @@ var init_package = __esm(() => {
|
|
|
21819
21819
|
build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
|
|
21820
21820
|
typecheck: "bun x tsc --noEmit",
|
|
21821
21821
|
lint: "bun x biome check src/ bin/",
|
|
21822
|
-
test: "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
|
|
21823
|
-
"test:watch": "bun test --watch",
|
|
21824
|
-
"test:unit": "bun test ./test/unit/ --timeout=60000",
|
|
21825
|
-
"test:integration": "bun test ./test/integration/ --timeout=60000",
|
|
21826
|
-
"test:ui": "bun test ./test/ui/ --timeout=60000",
|
|
21822
|
+
test: "CI=1 NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
|
|
21823
|
+
"test:watch": "CI=1 bun test --watch",
|
|
21824
|
+
"test:unit": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/unit/ --timeout=60000",
|
|
21825
|
+
"test:integration": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/integration/ --timeout=60000",
|
|
21826
|
+
"test:ui": "CI=1 bun test ./test/ui/ --timeout=60000",
|
|
21827
|
+
"test:real": "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
|
|
21827
21828
|
"check-test-overlap": "bun run scripts/check-test-overlap.ts",
|
|
21828
21829
|
"check-dead-tests": "bun run scripts/check-dead-tests.ts",
|
|
21829
21830
|
"check:test-sizes": "bun run scripts/check-test-sizes.ts",
|
|
@@ -21871,8 +21872,8 @@ var init_version = __esm(() => {
|
|
|
21871
21872
|
NAX_VERSION = package_default.version;
|
|
21872
21873
|
NAX_COMMIT = (() => {
|
|
21873
21874
|
try {
|
|
21874
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
21875
|
-
return "
|
|
21875
|
+
if (/^[0-9a-f]{6,10}$/.test("a59af3a"))
|
|
21876
|
+
return "a59af3a";
|
|
21876
21877
|
} catch {}
|
|
21877
21878
|
try {
|
|
21878
21879
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -21890,6 +21891,23 @@ var init_version = __esm(() => {
|
|
|
21890
21891
|
NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
|
|
21891
21892
|
});
|
|
21892
21893
|
|
|
21894
|
+
// src/prd/validate.ts
|
|
21895
|
+
function validateStoryId(id) {
|
|
21896
|
+
if (!id || id.length === 0) {
|
|
21897
|
+
throw new Error("Story ID cannot be empty");
|
|
21898
|
+
}
|
|
21899
|
+
if (id.includes("..")) {
|
|
21900
|
+
throw new Error("Story ID cannot contain path traversal (..)");
|
|
21901
|
+
}
|
|
21902
|
+
if (id.startsWith("--")) {
|
|
21903
|
+
throw new Error("Story ID cannot start with git flags (--)");
|
|
21904
|
+
}
|
|
21905
|
+
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
21906
|
+
if (!validPattern.test(id)) {
|
|
21907
|
+
throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
|
|
21908
|
+
}
|
|
21909
|
+
}
|
|
21910
|
+
|
|
21893
21911
|
// src/errors.ts
|
|
21894
21912
|
var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
|
|
21895
21913
|
var init_errors3 = __esm(() => {
|
|
@@ -22898,7 +22916,7 @@ class WebhookInteractionPlugin {
|
|
|
22898
22916
|
this.pendingResponses.delete(requestId);
|
|
22899
22917
|
return response;
|
|
22900
22918
|
}
|
|
22901
|
-
await
|
|
22919
|
+
await _webhookPluginDeps.sleep(backoffMs);
|
|
22902
22920
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
22903
22921
|
}
|
|
22904
22922
|
return {
|
|
@@ -22997,9 +23015,12 @@ class WebhookInteractionPlugin {
|
|
|
22997
23015
|
}
|
|
22998
23016
|
}
|
|
22999
23017
|
}
|
|
23000
|
-
var WebhookConfigSchema, InteractionResponseSchema;
|
|
23018
|
+
var _webhookPluginDeps, WebhookConfigSchema, InteractionResponseSchema;
|
|
23001
23019
|
var init_webhook = __esm(() => {
|
|
23002
23020
|
init_zod();
|
|
23021
|
+
_webhookPluginDeps = {
|
|
23022
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
23023
|
+
};
|
|
23003
23024
|
WebhookConfigSchema = exports_external.object({
|
|
23004
23025
|
url: exports_external.string().url().optional(),
|
|
23005
23026
|
callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
|
|
@@ -23038,8 +23059,8 @@ class AutoInteractionPlugin {
|
|
|
23038
23059
|
return;
|
|
23039
23060
|
}
|
|
23040
23061
|
try {
|
|
23041
|
-
if (
|
|
23042
|
-
const decision2 = await
|
|
23062
|
+
if (_deps3.callLlm) {
|
|
23063
|
+
const decision2 = await _deps3.callLlm(request);
|
|
23043
23064
|
if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
23044
23065
|
return;
|
|
23045
23066
|
}
|
|
@@ -23068,7 +23089,7 @@ class AutoInteractionPlugin {
|
|
|
23068
23089
|
}
|
|
23069
23090
|
async callLlm(request) {
|
|
23070
23091
|
const prompt = this.buildPrompt(request);
|
|
23071
|
-
const adapter =
|
|
23092
|
+
const adapter = _deps3.adapter;
|
|
23072
23093
|
if (!adapter) {
|
|
23073
23094
|
throw new Error("Auto plugin requires adapter to be injected via _deps.adapter");
|
|
23074
23095
|
}
|
|
@@ -23156,7 +23177,7 @@ Respond with ONLY this JSON (no markdown, no explanation):
|
|
|
23156
23177
|
return parsed;
|
|
23157
23178
|
}
|
|
23158
23179
|
}
|
|
23159
|
-
var AutoConfigSchema,
|
|
23180
|
+
var AutoConfigSchema, _deps3;
|
|
23160
23181
|
var init_auto = __esm(() => {
|
|
23161
23182
|
init_zod();
|
|
23162
23183
|
init_config();
|
|
@@ -23166,7 +23187,7 @@ var init_auto = __esm(() => {
|
|
|
23166
23187
|
maxCostPerDecision: exports_external.number().positive().optional(),
|
|
23167
23188
|
naxConfig: exports_external.any().optional()
|
|
23168
23189
|
});
|
|
23169
|
-
|
|
23190
|
+
_deps3 = {
|
|
23170
23191
|
adapter: null,
|
|
23171
23192
|
callLlm: null
|
|
23172
23193
|
};
|
|
@@ -23847,7 +23868,7 @@ async function runReview(config2, workdir, executionConfig) {
|
|
|
23847
23868
|
const logger = getSafeLogger();
|
|
23848
23869
|
const checks3 = [];
|
|
23849
23870
|
let firstFailure;
|
|
23850
|
-
const allUncommittedFiles = await
|
|
23871
|
+
const allUncommittedFiles = await _deps4.getUncommittedFiles(workdir);
|
|
23851
23872
|
const NAX_RUNTIME_FILES = new Set(["nax/status.json", ".nax-verifier-verdict.json"]);
|
|
23852
23873
|
const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_FILES.has(f) && !f.match(/^nax\/features\/.+\/prd\.json$/));
|
|
23853
23874
|
if (uncommittedFiles.length > 0) {
|
|
@@ -23887,11 +23908,11 @@ Stage and commit these files before running review.`
|
|
|
23887
23908
|
failureReason: firstFailure
|
|
23888
23909
|
};
|
|
23889
23910
|
}
|
|
23890
|
-
var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000,
|
|
23911
|
+
var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _deps4;
|
|
23891
23912
|
var init_runner2 = __esm(() => {
|
|
23892
23913
|
init_logger2();
|
|
23893
23914
|
_reviewRunnerDeps = { spawn, file: Bun.file };
|
|
23894
|
-
|
|
23915
|
+
_deps4 = {
|
|
23895
23916
|
getUncommittedFiles: getUncommittedFilesImpl
|
|
23896
23917
|
};
|
|
23897
23918
|
});
|
|
@@ -24996,7 +25017,7 @@ async function addFileElements(elements, storyContext, story) {
|
|
|
24996
25017
|
if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
|
|
24997
25018
|
const autoDetectConfig = storyContext.config?.context?.autoDetect;
|
|
24998
25019
|
try {
|
|
24999
|
-
const detected = await
|
|
25020
|
+
const detected = await _deps5.autoDetectContextFiles({
|
|
25000
25021
|
workdir: storyContext.workdir,
|
|
25001
25022
|
storyTitle: story.title,
|
|
25002
25023
|
maxFiles: autoDetectConfig?.maxFiles ?? 5,
|
|
@@ -25054,7 +25075,7 @@ ${content}
|
|
|
25054
25075
|
}
|
|
25055
25076
|
}
|
|
25056
25077
|
}
|
|
25057
|
-
var
|
|
25078
|
+
var _deps5;
|
|
25058
25079
|
var init_builder3 = __esm(() => {
|
|
25059
25080
|
init_logger2();
|
|
25060
25081
|
init_prd();
|
|
@@ -25062,7 +25083,7 @@ var init_builder3 = __esm(() => {
|
|
|
25062
25083
|
init_elements();
|
|
25063
25084
|
init_test_scanner();
|
|
25064
25085
|
init_elements();
|
|
25065
|
-
|
|
25086
|
+
_deps5 = {
|
|
25066
25087
|
autoDetectContextFiles
|
|
25067
25088
|
};
|
|
25068
25089
|
});
|
|
@@ -25544,6 +25565,30 @@ function detectMergeConflict(output) {
|
|
|
25544
25565
|
async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
25545
25566
|
const logger = getSafeLogger();
|
|
25546
25567
|
try {
|
|
25568
|
+
const topLevelProc = _gitDeps.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
25569
|
+
cwd: workdir,
|
|
25570
|
+
stdout: "pipe",
|
|
25571
|
+
stderr: "pipe"
|
|
25572
|
+
});
|
|
25573
|
+
const gitRoot = (await new Response(topLevelProc.stdout).text()).trim();
|
|
25574
|
+
await topLevelProc.exited;
|
|
25575
|
+
const { realpathSync: realpathSync3 } = await import("fs");
|
|
25576
|
+
const realWorkdir = (() => {
|
|
25577
|
+
try {
|
|
25578
|
+
return realpathSync3(workdir);
|
|
25579
|
+
} catch {
|
|
25580
|
+
return workdir;
|
|
25581
|
+
}
|
|
25582
|
+
})();
|
|
25583
|
+
const realGitRoot = (() => {
|
|
25584
|
+
try {
|
|
25585
|
+
return realpathSync3(gitRoot);
|
|
25586
|
+
} catch {
|
|
25587
|
+
return gitRoot;
|
|
25588
|
+
}
|
|
25589
|
+
})();
|
|
25590
|
+
if (realWorkdir !== realGitRoot)
|
|
25591
|
+
return;
|
|
25547
25592
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
25548
25593
|
cwd: workdir,
|
|
25549
25594
|
stdout: "pipe",
|
|
@@ -25929,11 +25974,15 @@ async function fullSuite(options) {
|
|
|
25929
25974
|
return runVerificationCore(options);
|
|
25930
25975
|
}
|
|
25931
25976
|
async function regression(options) {
|
|
25932
|
-
await
|
|
25977
|
+
await _regressionRunnerDeps.sleep(2000);
|
|
25933
25978
|
return runVerificationCore({ ...options, expectedFiles: undefined });
|
|
25934
25979
|
}
|
|
25980
|
+
var _regressionRunnerDeps;
|
|
25935
25981
|
var init_runners = __esm(() => {
|
|
25936
25982
|
init_executor();
|
|
25983
|
+
_regressionRunnerDeps = {
|
|
25984
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
25985
|
+
};
|
|
25937
25986
|
});
|
|
25938
25987
|
|
|
25939
25988
|
// src/verification/rectification.ts
|
|
@@ -26762,7 +26811,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
26762
26811
|
exitCode: result.exitCode
|
|
26763
26812
|
});
|
|
26764
26813
|
}
|
|
26765
|
-
await autoCommitIfDirty(workdir, "tdd", role, story.id);
|
|
26814
|
+
await _sessionRunnerDeps.autoCommitIfDirty(workdir, "tdd", role, story.id);
|
|
26766
26815
|
let isolation;
|
|
26767
26816
|
if (!skipIsolation) {
|
|
26768
26817
|
if (role === "test-writer") {
|
|
@@ -26809,6 +26858,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
26809
26858
|
estimatedCost: result.estimatedCost
|
|
26810
26859
|
};
|
|
26811
26860
|
}
|
|
26861
|
+
var _sessionRunnerDeps;
|
|
26812
26862
|
var init_session_runner = __esm(() => {
|
|
26813
26863
|
init_config();
|
|
26814
26864
|
init_logger2();
|
|
@@ -26816,6 +26866,9 @@ var init_session_runner = __esm(() => {
|
|
|
26816
26866
|
init_git();
|
|
26817
26867
|
init_cleanup();
|
|
26818
26868
|
init_isolation();
|
|
26869
|
+
_sessionRunnerDeps = {
|
|
26870
|
+
autoCommitIfDirty
|
|
26871
|
+
};
|
|
26819
26872
|
});
|
|
26820
26873
|
|
|
26821
26874
|
// src/tdd/verdict-reader.ts
|
|
@@ -27492,9 +27545,9 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
|
|
|
27492
27545
|
const plugin = ctx.interaction?.getPrimary();
|
|
27493
27546
|
if (!plugin)
|
|
27494
27547
|
return;
|
|
27495
|
-
const
|
|
27548
|
+
const QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
|
|
27496
27549
|
return {
|
|
27497
|
-
detectQuestion: async (text) =>
|
|
27550
|
+
detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
|
|
27498
27551
|
onQuestionDetected: async (text) => {
|
|
27499
27552
|
const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
27500
27553
|
await plugin.send({
|
|
@@ -29751,7 +29804,7 @@ var init_checks_config = () => {};
|
|
|
29751
29804
|
async function checkAgentCLI(config2) {
|
|
29752
29805
|
const agent = config2.execution?.agent || "claude";
|
|
29753
29806
|
try {
|
|
29754
|
-
const proc =
|
|
29807
|
+
const proc = _deps7.spawn([agent, "--version"], {
|
|
29755
29808
|
stdout: "pipe",
|
|
29756
29809
|
stderr: "pipe"
|
|
29757
29810
|
});
|
|
@@ -29772,9 +29825,9 @@ async function checkAgentCLI(config2) {
|
|
|
29772
29825
|
};
|
|
29773
29826
|
}
|
|
29774
29827
|
}
|
|
29775
|
-
var
|
|
29828
|
+
var _deps7;
|
|
29776
29829
|
var init_checks_cli = __esm(() => {
|
|
29777
|
-
|
|
29830
|
+
_deps7 = {
|
|
29778
29831
|
spawn: Bun.spawn
|
|
29779
29832
|
};
|
|
29780
29833
|
});
|
|
@@ -31363,23 +31416,6 @@ var init_headless_formatter = __esm(() => {
|
|
|
31363
31416
|
init_version();
|
|
31364
31417
|
});
|
|
31365
31418
|
|
|
31366
|
-
// src/prd/validate.ts
|
|
31367
|
-
function validateStoryId(id) {
|
|
31368
|
-
if (!id || id.length === 0) {
|
|
31369
|
-
throw new Error("Story ID cannot be empty");
|
|
31370
|
-
}
|
|
31371
|
-
if (id.includes("..")) {
|
|
31372
|
-
throw new Error("Story ID cannot contain path traversal (..)");
|
|
31373
|
-
}
|
|
31374
|
-
if (id.startsWith("--")) {
|
|
31375
|
-
throw new Error("Story ID cannot start with git flags (--)");
|
|
31376
|
-
}
|
|
31377
|
-
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
31378
|
-
if (!validPattern.test(id)) {
|
|
31379
|
-
throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
|
|
31380
|
-
}
|
|
31381
|
-
}
|
|
31382
|
-
|
|
31383
31419
|
// src/worktree/manager.ts
|
|
31384
31420
|
var exports_manager = {};
|
|
31385
31421
|
__export(exports_manager, {
|
|
@@ -65007,7 +65043,7 @@ import { existsSync as existsSync8 } from "fs";
|
|
|
65007
65043
|
import { join as join9 } from "path";
|
|
65008
65044
|
|
|
65009
65045
|
// src/analyze/scanner.ts
|
|
65010
|
-
import { existsSync as existsSync2 } from "fs";
|
|
65046
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
65011
65047
|
import { join as join4 } from "path";
|
|
65012
65048
|
async function scanCodebase(workdir) {
|
|
65013
65049
|
const srcPath = join4(workdir, "src");
|
|
@@ -65036,30 +65072,23 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
|
|
|
65036
65072
|
}
|
|
65037
65073
|
const entries = [];
|
|
65038
65074
|
try {
|
|
65039
|
-
const dirEntries =
|
|
65040
|
-
cwd: dir,
|
|
65041
|
-
onlyFiles: false
|
|
65042
|
-
}));
|
|
65075
|
+
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
|
65043
65076
|
dirEntries.sort((a, b) => {
|
|
65044
|
-
|
|
65045
|
-
const bIsDir = !b.includes(".");
|
|
65046
|
-
if (aIsDir && !bIsDir)
|
|
65077
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
65047
65078
|
return -1;
|
|
65048
|
-
if (!
|
|
65079
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
65049
65080
|
return 1;
|
|
65050
|
-
return a.localeCompare(b);
|
|
65081
|
+
return a.name.localeCompare(b.name);
|
|
65051
65082
|
});
|
|
65052
65083
|
for (let i = 0;i < dirEntries.length; i++) {
|
|
65053
|
-
const
|
|
65054
|
-
const fullPath = join4(dir, entry);
|
|
65084
|
+
const dirent = dirEntries[i];
|
|
65055
65085
|
const isLast = i === dirEntries.length - 1;
|
|
65056
65086
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
65057
65087
|
const childPrefix = isLast ? " " : "\u2502 ";
|
|
65058
|
-
const
|
|
65059
|
-
|
|
65060
|
-
entries.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
65088
|
+
const isDir = dirent.isDirectory();
|
|
65089
|
+
entries.push(`${prefix}${connector}${dirent.name}${isDir ? "/" : ""}`);
|
|
65061
65090
|
if (isDir) {
|
|
65062
|
-
const subtree = await generateFileTree(
|
|
65091
|
+
const subtree = await generateFileTree(join4(dir, dirent.name), maxDepth, currentDepth + 1, prefix + childPrefix);
|
|
65063
65092
|
if (subtree) {
|
|
65064
65093
|
entries.push(subtree);
|
|
65065
65094
|
}
|
|
@@ -65473,94 +65502,243 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
|
|
|
65473
65502
|
}
|
|
65474
65503
|
}
|
|
65475
65504
|
// src/cli/plan.ts
|
|
65476
|
-
|
|
65505
|
+
init_registry();
|
|
65477
65506
|
import { existsSync as existsSync9 } from "fs";
|
|
65478
65507
|
import { join as join10 } from "path";
|
|
65479
|
-
import { createInterface } from "readline";
|
|
65480
|
-
init_schema();
|
|
65481
65508
|
init_logger2();
|
|
65482
|
-
var QUESTION_PATTERNS = [/\?[\s]*$/, /\bwhich\b/i, /\bshould i\b/i, /\bdo you want\b/i, /\bwould you like\b/i];
|
|
65483
|
-
async function detectQuestion(text) {
|
|
65484
|
-
return QUESTION_PATTERNS.some((p) => p.test(text.trim()));
|
|
65485
|
-
}
|
|
65486
|
-
async function askHuman(question) {
|
|
65487
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
65488
|
-
return new Promise((resolve6) => {
|
|
65489
|
-
rl.question(`
|
|
65490
|
-
[Agent asks]: ${question}
|
|
65491
|
-
Your answer: `, (answer) => {
|
|
65492
|
-
rl.close();
|
|
65493
|
-
resolve6(answer.trim());
|
|
65494
|
-
});
|
|
65495
|
-
});
|
|
65496
|
-
}
|
|
65497
|
-
var SPEC_TEMPLATE = `# Feature: [title]
|
|
65498
65509
|
|
|
65499
|
-
|
|
65500
|
-
|
|
65501
|
-
|
|
65502
|
-
|
|
65503
|
-
-
|
|
65504
|
-
-
|
|
65505
|
-
|
|
65506
|
-
|
|
65507
|
-
|
|
65508
|
-
|
|
65509
|
-
|
|
65510
|
-
|
|
65510
|
+
// src/prd/schema.ts
|
|
65511
|
+
var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
|
|
65512
|
+
var VALID_TEST_STRATEGIES = [
|
|
65513
|
+
"test-after",
|
|
65514
|
+
"tdd-simple",
|
|
65515
|
+
"three-session-tdd",
|
|
65516
|
+
"three-session-tdd-lite"
|
|
65517
|
+
];
|
|
65518
|
+
var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
|
|
65519
|
+
function extractJsonFromMarkdown(text) {
|
|
65520
|
+
const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
|
|
65521
|
+
if (match) {
|
|
65522
|
+
return match[1] ?? text;
|
|
65523
|
+
}
|
|
65524
|
+
return text;
|
|
65525
|
+
}
|
|
65526
|
+
function stripTrailingCommas(text) {
|
|
65527
|
+
return text.replace(/,\s*([}\]])/g, "$1");
|
|
65528
|
+
}
|
|
65529
|
+
function normalizeStoryId(id) {
|
|
65530
|
+
const match = id.match(STORY_ID_NO_SEPARATOR);
|
|
65531
|
+
if (match) {
|
|
65532
|
+
return `${match[1]}-${match[2]}`;
|
|
65533
|
+
}
|
|
65534
|
+
return id;
|
|
65535
|
+
}
|
|
65536
|
+
function normalizeComplexity2(raw) {
|
|
65537
|
+
const lower = raw.toLowerCase();
|
|
65538
|
+
if (VALID_COMPLEXITY.includes(lower)) {
|
|
65539
|
+
return lower;
|
|
65540
|
+
}
|
|
65541
|
+
return null;
|
|
65542
|
+
}
|
|
65543
|
+
function validateStory(raw, index, allIds) {
|
|
65544
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
65545
|
+
throw new Error(`[schema] story[${index}] must be an object`);
|
|
65546
|
+
}
|
|
65547
|
+
const s = raw;
|
|
65548
|
+
const rawId = s.id;
|
|
65549
|
+
if (rawId === undefined || rawId === null || rawId === "") {
|
|
65550
|
+
throw new Error(`[schema] story[${index}].id is required and must be non-empty`);
|
|
65551
|
+
}
|
|
65552
|
+
if (typeof rawId !== "string") {
|
|
65553
|
+
throw new Error(`[schema] story[${index}].id must be a string`);
|
|
65554
|
+
}
|
|
65555
|
+
const id = normalizeStoryId(rawId);
|
|
65556
|
+
validateStoryId(id);
|
|
65557
|
+
const title = s.title;
|
|
65558
|
+
if (!title || typeof title !== "string" || title.trim() === "") {
|
|
65559
|
+
throw new Error(`[schema] story[${index}].title is required and must be non-empty`);
|
|
65560
|
+
}
|
|
65561
|
+
const description = s.description;
|
|
65562
|
+
if (!description || typeof description !== "string" || description.trim() === "") {
|
|
65563
|
+
throw new Error(`[schema] story[${index}].description is required and must be non-empty`);
|
|
65564
|
+
}
|
|
65565
|
+
const ac = s.acceptanceCriteria;
|
|
65566
|
+
if (!Array.isArray(ac) || ac.length === 0) {
|
|
65567
|
+
throw new Error(`[schema] story[${index}].acceptanceCriteria is required and must be a non-empty array`);
|
|
65568
|
+
}
|
|
65569
|
+
for (let i = 0;i < ac.length; i++) {
|
|
65570
|
+
if (typeof ac[i] !== "string") {
|
|
65571
|
+
throw new Error(`[schema] story[${index}].acceptanceCriteria[${i}] must be a string`);
|
|
65572
|
+
}
|
|
65573
|
+
}
|
|
65574
|
+
const routing = typeof s.routing === "object" && s.routing !== null ? s.routing : {};
|
|
65575
|
+
const rawComplexity = routing.complexity ?? s.complexity;
|
|
65576
|
+
if (rawComplexity === undefined || rawComplexity === null) {
|
|
65577
|
+
throw new Error(`[schema] story[${index}] missing complexity. Set routing.complexity to one of: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65578
|
+
}
|
|
65579
|
+
if (typeof rawComplexity !== "string") {
|
|
65580
|
+
throw new Error(`[schema] story[${index}].routing.complexity must be a string`);
|
|
65581
|
+
}
|
|
65582
|
+
const complexity = normalizeComplexity2(rawComplexity);
|
|
65583
|
+
if (complexity === null) {
|
|
65584
|
+
throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65585
|
+
}
|
|
65586
|
+
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
65587
|
+
const testStrategy = rawTestStrategy !== undefined && VALID_TEST_STRATEGIES.includes(rawTestStrategy) ? rawTestStrategy : "tdd-simple";
|
|
65588
|
+
const rawDeps = s.dependencies;
|
|
65589
|
+
const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
|
|
65590
|
+
for (const dep of dependencies) {
|
|
65591
|
+
if (!allIds.has(dep)) {
|
|
65592
|
+
throw new Error(`[schema] story[${index}].dependencies references unknown story ID "${dep}"`);
|
|
65593
|
+
}
|
|
65594
|
+
}
|
|
65595
|
+
const rawTags = s.tags;
|
|
65596
|
+
const tags = Array.isArray(rawTags) ? rawTags : [];
|
|
65597
|
+
return {
|
|
65598
|
+
id,
|
|
65599
|
+
title: title.trim(),
|
|
65600
|
+
description: description.trim(),
|
|
65601
|
+
acceptanceCriteria: ac,
|
|
65602
|
+
tags,
|
|
65603
|
+
dependencies,
|
|
65604
|
+
status: "pending",
|
|
65605
|
+
passes: false,
|
|
65606
|
+
attempts: 0,
|
|
65607
|
+
escalations: [],
|
|
65608
|
+
routing: {
|
|
65609
|
+
complexity,
|
|
65610
|
+
testStrategy,
|
|
65611
|
+
reasoning: "validated from LLM output"
|
|
65612
|
+
}
|
|
65613
|
+
};
|
|
65614
|
+
}
|
|
65615
|
+
function parseRawString(text) {
|
|
65616
|
+
const extracted = extractJsonFromMarkdown(text);
|
|
65617
|
+
const cleaned = stripTrailingCommas(extracted);
|
|
65618
|
+
try {
|
|
65619
|
+
return JSON.parse(cleaned);
|
|
65620
|
+
} catch (err) {
|
|
65621
|
+
const parseErr = err;
|
|
65622
|
+
throw new Error(`[schema] Failed to parse JSON: ${parseErr.message}`, { cause: parseErr });
|
|
65623
|
+
}
|
|
65624
|
+
}
|
|
65625
|
+
function validatePlanOutput(raw, feature, branch) {
|
|
65626
|
+
const parsed = typeof raw === "string" ? parseRawString(raw) : raw;
|
|
65627
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
65628
|
+
throw new Error("[schema] PRD output must be a JSON object");
|
|
65629
|
+
}
|
|
65630
|
+
const obj = parsed;
|
|
65631
|
+
const rawStories = obj.userStories;
|
|
65632
|
+
if (!Array.isArray(rawStories) || rawStories.length === 0) {
|
|
65633
|
+
throw new Error("[schema] userStories is required and must be a non-empty array");
|
|
65634
|
+
}
|
|
65635
|
+
const allIds = new Set;
|
|
65636
|
+
for (const story of rawStories) {
|
|
65637
|
+
if (typeof story === "object" && story !== null && !Array.isArray(story)) {
|
|
65638
|
+
const s = story;
|
|
65639
|
+
const rawId = s.id;
|
|
65640
|
+
if (typeof rawId === "string" && rawId !== "") {
|
|
65641
|
+
allIds.add(normalizeStoryId(rawId));
|
|
65642
|
+
}
|
|
65643
|
+
}
|
|
65644
|
+
}
|
|
65645
|
+
const userStories = rawStories.map((story, index) => validateStory(story, index, allIds));
|
|
65646
|
+
const now = new Date().toISOString();
|
|
65647
|
+
return {
|
|
65648
|
+
project: typeof obj.project === "string" && obj.project !== "" ? obj.project : feature,
|
|
65649
|
+
feature,
|
|
65650
|
+
branchName: branch,
|
|
65651
|
+
createdAt: typeof obj.createdAt === "string" ? obj.createdAt : now,
|
|
65652
|
+
updatedAt: now,
|
|
65653
|
+
userStories
|
|
65654
|
+
};
|
|
65655
|
+
}
|
|
65511
65656
|
|
|
65512
|
-
|
|
65513
|
-
|
|
65514
|
-
|
|
65515
|
-
|
|
65516
|
-
|
|
65517
|
-
|
|
65518
|
-
|
|
65519
|
-
|
|
65657
|
+
// src/cli/plan.ts
|
|
65658
|
+
var _deps2 = {
|
|
65659
|
+
readFile: (path) => Bun.file(path).text(),
|
|
65660
|
+
writeFile: (path, content) => Bun.write(path, content).then(() => {}),
|
|
65661
|
+
scanCodebase: (workdir) => scanCodebase(workdir),
|
|
65662
|
+
getAgent: (name) => getAgent(name),
|
|
65663
|
+
readPackageJson: (workdir) => Bun.file(join10(workdir, "package.json")).json().catch(() => null),
|
|
65664
|
+
spawnSync: (cmd, opts) => {
|
|
65665
|
+
const result = Bun.spawnSync(cmd, opts ? { cwd: opts.cwd } : {});
|
|
65666
|
+
return { stdout: result.stdout, exitCode: result.exitCode };
|
|
65667
|
+
},
|
|
65668
|
+
mkdirp: (path) => Bun.spawn(["mkdir", "-p", path]).exited.then(() => {})
|
|
65669
|
+
};
|
|
65670
|
+
async function planCommand(workdir, config2, options) {
|
|
65671
|
+
const naxDir = join10(workdir, "nax");
|
|
65672
|
+
if (!existsSync9(naxDir)) {
|
|
65520
65673
|
throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
|
|
65521
65674
|
}
|
|
65522
65675
|
const logger = getLogger();
|
|
65523
|
-
logger
|
|
65524
|
-
const
|
|
65676
|
+
logger?.info("plan", "Reading spec", { from: options.from });
|
|
65677
|
+
const specContent = await _deps2.readFile(options.from);
|
|
65678
|
+
logger?.info("plan", "Scanning codebase...");
|
|
65679
|
+
const scan = await _deps2.scanCodebase(workdir);
|
|
65525
65680
|
const codebaseContext = buildCodebaseContext2(scan);
|
|
65526
|
-
const
|
|
65527
|
-
const
|
|
65528
|
-
const
|
|
65529
|
-
const
|
|
65530
|
-
const
|
|
65531
|
-
|
|
65532
|
-
|
|
65533
|
-
|
|
65534
|
-
|
|
65535
|
-
|
|
65536
|
-
|
|
65537
|
-
|
|
65538
|
-
|
|
65539
|
-
interactionBridge: interactive ? { detectQuestion, onQuestionDetected: askHuman } : undefined
|
|
65540
|
-
};
|
|
65541
|
-
const adapter = new ClaudeCodeAdapter;
|
|
65542
|
-
logger.info("cli", interactive ? "Starting interactive planning session..." : `Reading from ${options.from}...`, {
|
|
65543
|
-
interactive,
|
|
65544
|
-
from: options.from
|
|
65545
|
-
});
|
|
65546
|
-
const result = await adapter.plan(planOptions);
|
|
65547
|
-
if (interactive) {
|
|
65548
|
-
if (result.specContent) {
|
|
65549
|
-
await Bun.write(outputPath, result.specContent);
|
|
65550
|
-
} else {
|
|
65551
|
-
if (!existsSync9(outputPath)) {
|
|
65552
|
-
throw new Error(`Interactive planning completed but spec not found at ${outputPath}`);
|
|
65553
|
-
}
|
|
65554
|
-
}
|
|
65681
|
+
const pkg = await _deps2.readPackageJson(workdir);
|
|
65682
|
+
const projectName = detectProjectName(workdir, pkg);
|
|
65683
|
+
const branchName = options.branch ?? `feat/${options.feature}`;
|
|
65684
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext);
|
|
65685
|
+
const agentName = config2?.autoMode?.defaultAgent ?? "claude";
|
|
65686
|
+
const adapter = _deps2.getAgent(agentName);
|
|
65687
|
+
if (!adapter) {
|
|
65688
|
+
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65689
|
+
}
|
|
65690
|
+
const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
|
|
65691
|
+
let rawResponse;
|
|
65692
|
+
if (options.auto) {
|
|
65693
|
+
rawResponse = await adapter.complete(prompt, { jsonMode: true });
|
|
65555
65694
|
} else {
|
|
65556
|
-
|
|
65557
|
-
|
|
65695
|
+
const interactionBridge = createCliInteractionBridge();
|
|
65696
|
+
logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
|
|
65697
|
+
try {
|
|
65698
|
+
const result = await adapter.plan({
|
|
65699
|
+
prompt,
|
|
65700
|
+
workdir,
|
|
65701
|
+
interactive: true,
|
|
65702
|
+
timeoutSeconds,
|
|
65703
|
+
interactionBridge
|
|
65704
|
+
});
|
|
65705
|
+
rawResponse = result.specContent;
|
|
65706
|
+
} finally {
|
|
65707
|
+
logger?.info("plan", "Interactive session ended");
|
|
65558
65708
|
}
|
|
65559
|
-
await Bun.write(outputPath, result.specContent);
|
|
65560
65709
|
}
|
|
65561
|
-
|
|
65710
|
+
const finalPrd = validatePlanOutput(rawResponse, options.feature, branchName);
|
|
65711
|
+
finalPrd.project = projectName;
|
|
65712
|
+
const outputDir = join10(naxDir, "features", options.feature);
|
|
65713
|
+
const outputPath = join10(outputDir, "prd.json");
|
|
65714
|
+
await _deps2.mkdirp(outputDir);
|
|
65715
|
+
await _deps2.writeFile(outputPath, JSON.stringify(finalPrd, null, 2));
|
|
65716
|
+
logger?.info("plan", "[OK] PRD written", { outputPath });
|
|
65562
65717
|
return outputPath;
|
|
65563
65718
|
}
|
|
65719
|
+
function createCliInteractionBridge() {
|
|
65720
|
+
return {
|
|
65721
|
+
async detectQuestion(text) {
|
|
65722
|
+
return text.includes("?");
|
|
65723
|
+
},
|
|
65724
|
+
async onQuestionDetected(text) {
|
|
65725
|
+
return text;
|
|
65726
|
+
}
|
|
65727
|
+
};
|
|
65728
|
+
}
|
|
65729
|
+
function detectProjectName(workdir, pkg) {
|
|
65730
|
+
if (pkg?.name && typeof pkg.name === "string") {
|
|
65731
|
+
return pkg.name;
|
|
65732
|
+
}
|
|
65733
|
+
const result = _deps2.spawnSync(["git", "remote", "get-url", "origin"], { cwd: workdir });
|
|
65734
|
+
if (result.exitCode === 0) {
|
|
65735
|
+
const url2 = result.stdout.toString().trim();
|
|
65736
|
+
const match = url2.match(/\/([^/]+?)(?:\.git)?$/);
|
|
65737
|
+
if (match?.[1])
|
|
65738
|
+
return match[1];
|
|
65739
|
+
}
|
|
65740
|
+
return "unknown";
|
|
65741
|
+
}
|
|
65564
65742
|
function buildCodebaseContext2(scan) {
|
|
65565
65743
|
const sections = [];
|
|
65566
65744
|
sections.push(`## Codebase Structure
|
|
@@ -65587,24 +65765,62 @@ function buildCodebaseContext2(scan) {
|
|
|
65587
65765
|
return sections.join(`
|
|
65588
65766
|
`);
|
|
65589
65767
|
}
|
|
65590
|
-
function
|
|
65591
|
-
return `You are
|
|
65768
|
+
function buildPlanningPrompt(specContent, codebaseContext) {
|
|
65769
|
+
return `You are a senior software architect generating a product requirements document (PRD) as JSON.
|
|
65770
|
+
|
|
65771
|
+
## Spec
|
|
65772
|
+
|
|
65773
|
+
${specContent}
|
|
65774
|
+
|
|
65775
|
+
## Codebase Context
|
|
65776
|
+
|
|
65777
|
+
${codebaseContext}
|
|
65778
|
+
|
|
65779
|
+
## Output Schema
|
|
65780
|
+
|
|
65781
|
+
Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
|
|
65782
|
+
|
|
65783
|
+
{
|
|
65784
|
+
"project": "string \u2014 project name",
|
|
65785
|
+
"feature": "string \u2014 feature name",
|
|
65786
|
+
"branchName": "string \u2014 git branch (e.g. feat/my-feature)",
|
|
65787
|
+
"createdAt": "ISO 8601 timestamp",
|
|
65788
|
+
"updatedAt": "ISO 8601 timestamp",
|
|
65789
|
+
"userStories": [
|
|
65790
|
+
{
|
|
65791
|
+
"id": "string \u2014 e.g. US-001",
|
|
65792
|
+
"title": "string \u2014 concise story title",
|
|
65793
|
+
"description": "string \u2014 detailed description of the story",
|
|
65794
|
+
"acceptanceCriteria": ["string \u2014 each AC line"],
|
|
65795
|
+
"tags": ["string \u2014 routing tags, e.g. feature, security, api"],
|
|
65796
|
+
"dependencies": ["string \u2014 story IDs this story depends on"],
|
|
65797
|
+
"status": "pending",
|
|
65798
|
+
"passes": false,
|
|
65799
|
+
"routing": {
|
|
65800
|
+
"complexity": "simple | medium | complex | expert",
|
|
65801
|
+
"testStrategy": "test-after | tdd-lite | three-session-tdd",
|
|
65802
|
+
"reasoning": "string \u2014 brief classification rationale"
|
|
65803
|
+
},
|
|
65804
|
+
"escalations": [],
|
|
65805
|
+
"attempts": 0
|
|
65806
|
+
}
|
|
65807
|
+
]
|
|
65808
|
+
}
|
|
65592
65809
|
|
|
65593
|
-
|
|
65810
|
+
## Complexity Classification Guide
|
|
65594
65811
|
|
|
65595
|
-
|
|
65812
|
+
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
|
|
65813
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-lite
|
|
65814
|
+
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
65815
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
|
|
65596
65816
|
|
|
65597
|
-
|
|
65817
|
+
## Test Strategy Guide
|
|
65598
65818
|
|
|
65599
|
-
|
|
65600
|
-
|
|
65601
|
-
-
|
|
65602
|
-
- Specific requirements and constraints
|
|
65603
|
-
- Acceptance criteria for success
|
|
65604
|
-
- Technical approach and architecture
|
|
65605
|
-
- What is explicitly out of scope
|
|
65819
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
65820
|
+
- tdd-lite: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
65821
|
+
- three-session-tdd: Complex/expert. Full TDD cycle with separate sessions for tests and implementation.
|
|
65606
65822
|
|
|
65607
|
-
|
|
65823
|
+
Output ONLY the JSON object. Do not wrap in markdown code blocks.`;
|
|
65608
65824
|
}
|
|
65609
65825
|
// src/cli/accept.ts
|
|
65610
65826
|
init_config();
|
|
@@ -65753,13 +65969,13 @@ async function displayModelEfficiency(workdir) {
|
|
|
65753
65969
|
}
|
|
65754
65970
|
// src/cli/status-features.ts
|
|
65755
65971
|
init_source();
|
|
65756
|
-
import { existsSync as existsSync11, readdirSync as
|
|
65972
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
|
|
65757
65973
|
import { join as join13 } from "path";
|
|
65758
65974
|
|
|
65759
65975
|
// src/commands/common.ts
|
|
65760
65976
|
init_path_security2();
|
|
65761
65977
|
init_errors3();
|
|
65762
|
-
import { existsSync as existsSync10, readdirSync, realpathSync as realpathSync2 } from "fs";
|
|
65978
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
65763
65979
|
import { join as join11, resolve as resolve6 } from "path";
|
|
65764
65980
|
function resolveProject(options = {}) {
|
|
65765
65981
|
const { dir, feature } = options;
|
|
@@ -65798,7 +66014,7 @@ Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, co
|
|
|
65798
66014
|
const featuresDir = join11(naxDir, "features");
|
|
65799
66015
|
featureDir = join11(featuresDir, feature);
|
|
65800
66016
|
if (!existsSync10(featureDir)) {
|
|
65801
|
-
const availableFeatures = existsSync10(featuresDir) ?
|
|
66017
|
+
const availableFeatures = existsSync10(featuresDir) ? readdirSync2(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
65802
66018
|
const availableMsg = availableFeatures.length > 0 ? `
|
|
65803
66019
|
|
|
65804
66020
|
Available features:
|
|
@@ -65910,7 +66126,7 @@ async function getFeatureSummary(featureName, featureDir) {
|
|
|
65910
66126
|
}
|
|
65911
66127
|
const runsDir = join13(featureDir, "runs");
|
|
65912
66128
|
if (existsSync11(runsDir)) {
|
|
65913
|
-
const runs =
|
|
66129
|
+
const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
|
|
65914
66130
|
if (runs.length > 0) {
|
|
65915
66131
|
const latestRun = runs[0].replace(".jsonl", "");
|
|
65916
66132
|
summary.lastRun = latestRun;
|
|
@@ -65924,7 +66140,7 @@ async function displayAllFeatures(projectDir) {
|
|
|
65924
66140
|
console.log(source_default.dim("No features found."));
|
|
65925
66141
|
return;
|
|
65926
66142
|
}
|
|
65927
|
-
const features =
|
|
66143
|
+
const features = readdirSync3(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
65928
66144
|
if (features.length === 0) {
|
|
65929
66145
|
console.log(source_default.dim("No features found."));
|
|
65930
66146
|
return;
|
|
@@ -66100,7 +66316,7 @@ async function displayFeatureStatus(options = {}) {
|
|
|
66100
66316
|
// src/cli/runs.ts
|
|
66101
66317
|
init_errors3();
|
|
66102
66318
|
init_logger2();
|
|
66103
|
-
import { existsSync as existsSync12, readdirSync as
|
|
66319
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "fs";
|
|
66104
66320
|
import { join as join14 } from "path";
|
|
66105
66321
|
async function parseRunLog(logPath) {
|
|
66106
66322
|
const logger = getLogger();
|
|
@@ -66122,7 +66338,7 @@ async function runsListCommand(options) {
|
|
|
66122
66338
|
logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
|
|
66123
66339
|
return;
|
|
66124
66340
|
}
|
|
66125
|
-
const files =
|
|
66341
|
+
const files = readdirSync4(runsDir).filter((f) => f.endsWith(".jsonl"));
|
|
66126
66342
|
if (files.length === 0) {
|
|
66127
66343
|
logger.info("cli", "No runs found for feature", { feature });
|
|
66128
66344
|
return;
|
|
@@ -66627,7 +66843,7 @@ function pad(str, width) {
|
|
|
66627
66843
|
init_config();
|
|
66628
66844
|
init_logger2();
|
|
66629
66845
|
init_prd();
|
|
66630
|
-
import { existsSync as existsSync17, readdirSync as
|
|
66846
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5 } from "fs";
|
|
66631
66847
|
import { join as join25 } from "path";
|
|
66632
66848
|
|
|
66633
66849
|
// src/cli/diagnose-analysis.ts
|
|
@@ -66888,7 +67104,7 @@ async function diagnoseCommand(options = {}) {
|
|
|
66888
67104
|
const featuresDir = join25(projectDir, "nax", "features");
|
|
66889
67105
|
if (!existsSync17(featuresDir))
|
|
66890
67106
|
throw new Error("No features found in project");
|
|
66891
|
-
const features =
|
|
67107
|
+
const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
66892
67108
|
if (features.length === 0)
|
|
66893
67109
|
throw new Error("No features found");
|
|
66894
67110
|
feature = features[0];
|
|
@@ -67896,19 +68112,19 @@ import { join as join33 } from "path";
|
|
|
67896
68112
|
// src/commands/logs-formatter.ts
|
|
67897
68113
|
init_source();
|
|
67898
68114
|
init_formatter();
|
|
67899
|
-
import { readdirSync as
|
|
68115
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
67900
68116
|
import { join as join32 } from "path";
|
|
67901
68117
|
|
|
67902
68118
|
// src/commands/logs-reader.ts
|
|
67903
|
-
import { existsSync as existsSync23, readdirSync as
|
|
68119
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6 } from "fs";
|
|
67904
68120
|
import { readdir as readdir3 } from "fs/promises";
|
|
67905
68121
|
import { homedir as homedir3 } from "os";
|
|
67906
68122
|
import { join as join31 } from "path";
|
|
67907
|
-
var
|
|
68123
|
+
var _deps6 = {
|
|
67908
68124
|
getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
|
|
67909
68125
|
};
|
|
67910
68126
|
async function resolveRunFileFromRegistry(runId) {
|
|
67911
|
-
const runsDir =
|
|
68127
|
+
const runsDir = _deps6.getRunsDir();
|
|
67912
68128
|
let entries;
|
|
67913
68129
|
try {
|
|
67914
68130
|
entries = await readdir3(runsDir);
|
|
@@ -67933,7 +68149,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
67933
68149
|
console.log(`Log directory unavailable for run: ${runId}`);
|
|
67934
68150
|
return null;
|
|
67935
68151
|
}
|
|
67936
|
-
const files =
|
|
68152
|
+
const files = readdirSync6(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
67937
68153
|
if (files.length === 0) {
|
|
67938
68154
|
console.log(`No log files found for run: ${runId}`);
|
|
67939
68155
|
return null;
|
|
@@ -67942,7 +68158,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
67942
68158
|
return join31(matched.eventsDir, specificFile ?? files[0]);
|
|
67943
68159
|
}
|
|
67944
68160
|
async function selectRunFile(runsDir) {
|
|
67945
|
-
const files =
|
|
68161
|
+
const files = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
67946
68162
|
if (files.length === 0) {
|
|
67947
68163
|
return null;
|
|
67948
68164
|
}
|
|
@@ -68020,7 +68236,7 @@ var LOG_LEVEL_PRIORITY2 = {
|
|
|
68020
68236
|
error: 3
|
|
68021
68237
|
};
|
|
68022
68238
|
async function displayRunsList(runsDir) {
|
|
68023
|
-
const files =
|
|
68239
|
+
const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
68024
68240
|
if (files.length === 0) {
|
|
68025
68241
|
console.log(source_default.dim("No runs found"));
|
|
68026
68242
|
return;
|
|
@@ -68215,7 +68431,7 @@ async function precheckCommand(options) {
|
|
|
68215
68431
|
}
|
|
68216
68432
|
if (!existsSync29(prdPath)) {
|
|
68217
68433
|
console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
|
|
68218
|
-
console.error(source_default.dim(`Run: nax
|
|
68434
|
+
console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
|
|
68219
68435
|
process.exit(EXIT_CODES.INVALID_PRD);
|
|
68220
68436
|
}
|
|
68221
68437
|
const config2 = await loadConfig(resolved.projectDir);
|
|
@@ -68234,7 +68450,7 @@ import { readdir as readdir4 } from "fs/promises";
|
|
|
68234
68450
|
import { homedir as homedir4 } from "os";
|
|
68235
68451
|
import { join as join35 } from "path";
|
|
68236
68452
|
var DEFAULT_LIMIT = 20;
|
|
68237
|
-
var
|
|
68453
|
+
var _deps8 = {
|
|
68238
68454
|
getRunsDir: () => join35(homedir4(), ".nax", "runs")
|
|
68239
68455
|
};
|
|
68240
68456
|
function formatDuration3(ms) {
|
|
@@ -68277,7 +68493,7 @@ function pad3(str, width) {
|
|
|
68277
68493
|
return str + " ".repeat(padding);
|
|
68278
68494
|
}
|
|
68279
68495
|
async function runsCommand(options = {}) {
|
|
68280
|
-
const runsDir =
|
|
68496
|
+
const runsDir = _deps8.getRunsDir();
|
|
68281
68497
|
let entries;
|
|
68282
68498
|
try {
|
|
68283
68499
|
entries = await readdir4(runsDir);
|
|
@@ -76153,6 +76369,31 @@ function renderTui(props) {
|
|
|
76153
76369
|
init_version();
|
|
76154
76370
|
var program2 = new Command;
|
|
76155
76371
|
program2.name("nax").description("AI Coding Agent Orchestrator \u2014 loops until done").version(NAX_VERSION);
|
|
76372
|
+
async function promptForConfirmation(question) {
|
|
76373
|
+
if (!process.stdin.isTTY) {
|
|
76374
|
+
return true;
|
|
76375
|
+
}
|
|
76376
|
+
return new Promise((resolve9) => {
|
|
76377
|
+
process.stdout.write(source_default.bold(`${question} [Y/n] `));
|
|
76378
|
+
process.stdin.setRawMode(true);
|
|
76379
|
+
process.stdin.resume();
|
|
76380
|
+
process.stdin.setEncoding("utf8");
|
|
76381
|
+
const handler = (char) => {
|
|
76382
|
+
process.stdin.setRawMode(false);
|
|
76383
|
+
process.stdin.pause();
|
|
76384
|
+
process.stdin.removeListener("data", handler);
|
|
76385
|
+
const answer = char.toLowerCase();
|
|
76386
|
+
process.stdout.write(`
|
|
76387
|
+
`);
|
|
76388
|
+
if (answer === "n") {
|
|
76389
|
+
resolve9(false);
|
|
76390
|
+
} else {
|
|
76391
|
+
resolve9(true);
|
|
76392
|
+
}
|
|
76393
|
+
};
|
|
76394
|
+
process.stdin.on("data", handler);
|
|
76395
|
+
});
|
|
76396
|
+
}
|
|
76156
76397
|
program2.command("init").description("Initialize nax in the current project").option("-d, --dir <path>", "Project directory", process.cwd()).option("-f, --force", "Force overwrite existing files", false).action(async (options) => {
|
|
76157
76398
|
let workdir;
|
|
76158
76399
|
try {
|
|
@@ -76267,7 +76508,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
|
|
|
76267
76508
|
console.log(source_default.dim(`
|
|
76268
76509
|
Next: nax features create <name>`));
|
|
76269
76510
|
});
|
|
76270
|
-
program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
|
|
76511
|
+
program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
|
|
76271
76512
|
let workdir;
|
|
76272
76513
|
try {
|
|
76273
76514
|
workdir = validateDirectory(options.dir);
|
|
@@ -76275,6 +76516,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76275
76516
|
console.error(source_default.red(`Invalid directory: ${err.message}`));
|
|
76276
76517
|
process.exit(1);
|
|
76277
76518
|
}
|
|
76519
|
+
if (options.plan && !options.from) {
|
|
76520
|
+
console.error(source_default.red("Error: --plan requires --from <spec-path>"));
|
|
76521
|
+
process.exit(1);
|
|
76522
|
+
}
|
|
76523
|
+
if (options.from && !existsSync32(options.from)) {
|
|
76524
|
+
console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
|
|
76525
|
+
process.exit(1);
|
|
76526
|
+
}
|
|
76278
76527
|
let logLevel = "info";
|
|
76279
76528
|
const envLevel = process.env.NAX_LOG_LEVEL?.toLowerCase();
|
|
76280
76529
|
if (envLevel && ["error", "warn", "info", "debug"].includes(envLevel)) {
|
|
@@ -76302,6 +76551,38 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76302
76551
|
}
|
|
76303
76552
|
const featureDir = join43(naxDir, "features", options.feature);
|
|
76304
76553
|
const prdPath = join43(featureDir, "prd.json");
|
|
76554
|
+
if (options.plan && options.from) {
|
|
76555
|
+
try {
|
|
76556
|
+
console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
|
|
76557
|
+
const generatedPrdPath = await planCommand(workdir, config2, {
|
|
76558
|
+
from: options.from,
|
|
76559
|
+
feature: options.feature,
|
|
76560
|
+
auto: true,
|
|
76561
|
+
branch: undefined
|
|
76562
|
+
});
|
|
76563
|
+
const generatedPrd = await loadPRD(generatedPrdPath);
|
|
76564
|
+
console.log(source_default.bold(`
|
|
76565
|
+
\u2500\u2500 Planning Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
76566
|
+
console.log(source_default.dim(`Feature: ${generatedPrd.feature}`));
|
|
76567
|
+
console.log(source_default.dim(`Stories: ${generatedPrd.userStories.length}`));
|
|
76568
|
+
console.log();
|
|
76569
|
+
for (const story of generatedPrd.userStories) {
|
|
76570
|
+
const complexity = story.routing?.complexity || "unknown";
|
|
76571
|
+
console.log(source_default.dim(` ${story.id}: ${story.title} [${complexity}]`));
|
|
76572
|
+
}
|
|
76573
|
+
console.log();
|
|
76574
|
+
if (!options.headless) {
|
|
76575
|
+
const confirmationResult = await promptForConfirmation("Proceed with execution?");
|
|
76576
|
+
if (!confirmationResult) {
|
|
76577
|
+
console.log(source_default.yellow("Execution cancelled."));
|
|
76578
|
+
process.exit(0);
|
|
76579
|
+
}
|
|
76580
|
+
}
|
|
76581
|
+
} catch (err) {
|
|
76582
|
+
console.error(source_default.red(`Error during planning: ${err.message}`));
|
|
76583
|
+
process.exit(1);
|
|
76584
|
+
}
|
|
76585
|
+
}
|
|
76305
76586
|
if (!existsSync32(prdPath)) {
|
|
76306
76587
|
console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
|
|
76307
76588
|
process.exit(1);
|
|
@@ -76452,7 +76733,7 @@ Created: ${new Date().toISOString()}
|
|
|
76452
76733
|
console.log(source_default.dim(" \u251C\u2500\u2500 tasks.md"));
|
|
76453
76734
|
console.log(source_default.dim(" \u2514\u2500\u2500 progress.txt"));
|
|
76454
76735
|
console.log(source_default.dim(`
|
|
76455
|
-
Next: Edit spec.md and tasks.md, then: nax
|
|
76736
|
+
Next: Edit spec.md and tasks.md, then: nax plan -f ${name} --from spec.md --auto`));
|
|
76456
76737
|
});
|
|
76457
76738
|
features.command("list").description("List all features").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
76458
76739
|
let workdir;
|
|
@@ -76472,8 +76753,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
76472
76753
|
console.log(source_default.dim("No features yet."));
|
|
76473
76754
|
return;
|
|
76474
76755
|
}
|
|
76475
|
-
const { readdirSync:
|
|
76476
|
-
const entries =
|
|
76756
|
+
const { readdirSync: readdirSync8 } = await import("fs");
|
|
76757
|
+
const entries = readdirSync8(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
76477
76758
|
if (entries.length === 0) {
|
|
76478
76759
|
console.log(source_default.dim("No features yet."));
|
|
76479
76760
|
return;
|
|
@@ -76493,7 +76774,13 @@ Features:
|
|
|
76493
76774
|
}
|
|
76494
76775
|
console.log();
|
|
76495
76776
|
});
|
|
76496
|
-
program2.command("plan
|
|
76777
|
+
program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").requiredOption("--from <spec-path>", "Path to spec file (required)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in auto (one-shot LLM) mode", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
|
|
76778
|
+
if (description) {
|
|
76779
|
+
console.error(source_default.red(`Error: Positional args removed in plan v2.
|
|
76780
|
+
|
|
76781
|
+
Use: nax plan -f <feature> --from <spec>`));
|
|
76782
|
+
process.exit(1);
|
|
76783
|
+
}
|
|
76497
76784
|
let workdir;
|
|
76498
76785
|
try {
|
|
76499
76786
|
workdir = validateDirectory(options.dir);
|
|
@@ -76508,21 +76795,26 @@ program2.command("plan <description>").description("Interactive planning via age
|
|
|
76508
76795
|
}
|
|
76509
76796
|
const config2 = await loadConfig(workdir);
|
|
76510
76797
|
try {
|
|
76511
|
-
const
|
|
76512
|
-
|
|
76513
|
-
|
|
76798
|
+
const prdPath = await planCommand(workdir, config2, {
|
|
76799
|
+
from: options.from,
|
|
76800
|
+
feature: options.feature,
|
|
76801
|
+
auto: options.auto,
|
|
76802
|
+
branch: options.branch
|
|
76514
76803
|
});
|
|
76515
76804
|
console.log(source_default.green(`
|
|
76516
|
-
|
|
76517
|
-
console.log(source_default.dim(`
|
|
76805
|
+
[OK] PRD generated`));
|
|
76806
|
+
console.log(source_default.dim(` PRD: ${prdPath}`));
|
|
76518
76807
|
console.log(source_default.dim(`
|
|
76519
|
-
Next: nax
|
|
76808
|
+
Next: nax run -f ${options.feature}`));
|
|
76520
76809
|
} catch (err) {
|
|
76521
76810
|
console.error(source_default.red(`Error: ${err.message}`));
|
|
76522
76811
|
process.exit(1);
|
|
76523
76812
|
}
|
|
76524
76813
|
});
|
|
76525
|
-
program2.command("analyze").description("Parse spec.md into prd.json via agent decompose").requiredOption("-f, --feature <name>", "Feature name").option("-b, --branch <name>", "Branch name", "feat/<feature>").option("--from <path>", "Explicit spec path (overrides default spec.md)").option("--reclassify", "Re-classify existing prd.json without decompose", false).option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
76814
|
+
program2.command("analyze").description("(deprecated) Parse spec.md into prd.json via agent decompose \u2014 use 'nax plan' instead").requiredOption("-f, --feature <name>", "Feature name").option("-b, --branch <name>", "Branch name", "feat/<feature>").option("--from <path>", "Explicit spec path (overrides default spec.md)").option("--reclassify", "Re-classify existing prd.json without decompose", false).option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
76815
|
+
const deprecationMsg = "\u26A0\uFE0F 'nax analyze' is deprecated. Use 'nax plan -f <feature> --from <spec> --auto' instead.";
|
|
76816
|
+
process.stderr.write(`${source_default.yellow(deprecationMsg)}
|
|
76817
|
+
`);
|
|
76526
76818
|
let workdir;
|
|
76527
76819
|
try {
|
|
76528
76820
|
workdir = validateDirectory(options.dir);
|