@nathapp/nax 0.41.0 → 0.42.1
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 +132 -11
- package/dist/nax.js +556 -207
- package/package.json +7 -6
- package/src/agents/acp/adapter.ts +63 -30
- package/src/agents/claude.ts +12 -2
- package/src/agents/types.ts +6 -0
- package/src/analyze/scanner.ts +16 -20
- package/src/cli/plan.ts +223 -144
- package/src/commands/precheck.ts +1 -1
- package/src/interaction/plugins/webhook.ts +10 -1
- package/src/pipeline/stages/acceptance.ts +21 -6
- package/src/prd/schema.ts +253 -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 {
|
|
@@ -19436,34 +19439,59 @@ class AcpAgentAdapter {
|
|
|
19436
19439
|
}
|
|
19437
19440
|
async complete(prompt, _options) {
|
|
19438
19441
|
const model = _options?.model ?? "default";
|
|
19439
|
-
const
|
|
19440
|
-
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
19441
|
-
await client.start();
|
|
19442
|
+
const timeoutMs = _options?.timeoutMs ?? 120000;
|
|
19442
19443
|
const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
|
|
19443
|
-
let
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
const
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
|
|
19450
|
-
|
|
19444
|
+
let lastError;
|
|
19445
|
+
for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
19446
|
+
const cmdStr = `acpx --model ${model} ${this.name}`;
|
|
19447
|
+
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
19448
|
+
await client.start();
|
|
19449
|
+
let session = null;
|
|
19450
|
+
try {
|
|
19451
|
+
session = await client.createSession({ agentName: this.name, permissionMode });
|
|
19452
|
+
let timeoutId;
|
|
19453
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
19454
|
+
timeoutId = setTimeout(() => reject(new Error(`complete() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
19455
|
+
});
|
|
19456
|
+
timeoutPromise.catch(() => {});
|
|
19457
|
+
const promptPromise = session.prompt(prompt);
|
|
19458
|
+
let response;
|
|
19459
|
+
try {
|
|
19460
|
+
response = await Promise.race([promptPromise, timeoutPromise]);
|
|
19461
|
+
} finally {
|
|
19462
|
+
clearTimeout(timeoutId);
|
|
19463
|
+
}
|
|
19464
|
+
if (response.stopReason === "error") {
|
|
19465
|
+
throw new CompleteError("complete() failed: stop reason is error");
|
|
19466
|
+
}
|
|
19467
|
+
const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
|
|
19451
19468
|
`).trim();
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19469
|
+
if (!text) {
|
|
19470
|
+
throw new CompleteError("complete() returned empty output");
|
|
19471
|
+
}
|
|
19472
|
+
return text;
|
|
19473
|
+
} catch (err) {
|
|
19474
|
+
const error48 = err instanceof Error ? err : new Error(String(err));
|
|
19475
|
+
lastError = error48;
|
|
19476
|
+
const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
|
|
19477
|
+
if (!shouldRetry)
|
|
19478
|
+
throw error48;
|
|
19479
|
+
const backoffMs = 2 ** (attempt + 1) * 1000;
|
|
19480
|
+
getSafeLogger()?.warn("acp-adapter", "complete() rate limited, retrying", {
|
|
19481
|
+
backoffSeconds: backoffMs / 1000,
|
|
19482
|
+
attempt: attempt + 1
|
|
19483
|
+
});
|
|
19484
|
+
await _acpAdapterDeps.sleep(backoffMs);
|
|
19485
|
+
} finally {
|
|
19486
|
+
if (session) {
|
|
19487
|
+
await session.close().catch(() => {});
|
|
19488
|
+
}
|
|
19489
|
+
await client.close().catch(() => {});
|
|
19459
19490
|
}
|
|
19460
|
-
await client.close().catch(() => {});
|
|
19461
19491
|
}
|
|
19492
|
+
throw lastError ?? new CompleteError("complete() failed with unknown error");
|
|
19462
19493
|
}
|
|
19463
19494
|
async plan(options) {
|
|
19464
|
-
if (options.interactive) {
|
|
19465
|
-
throw new Error("[acp-adapter] plan() interactive mode is not yet supported via ACP");
|
|
19466
|
-
}
|
|
19467
19495
|
const modelDef = options.modelDef ?? { provider: "anthropic", model: "default" };
|
|
19468
19496
|
const timeoutSeconds = options.timeoutSeconds ?? options.config?.execution?.sessionTimeoutSeconds ?? 600;
|
|
19469
19497
|
const result = await this.run({
|
|
@@ -21807,7 +21835,7 @@ var package_default;
|
|
|
21807
21835
|
var init_package = __esm(() => {
|
|
21808
21836
|
package_default = {
|
|
21809
21837
|
name: "@nathapp/nax",
|
|
21810
|
-
version: "0.
|
|
21838
|
+
version: "0.42.1",
|
|
21811
21839
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21812
21840
|
type: "module",
|
|
21813
21841
|
bin: {
|
|
@@ -21819,11 +21847,12 @@ var init_package = __esm(() => {
|
|
|
21819
21847
|
build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
|
|
21820
21848
|
typecheck: "bun x tsc --noEmit",
|
|
21821
21849
|
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",
|
|
21850
|
+
test: "CI=1 NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
|
|
21851
|
+
"test:watch": "CI=1 bun test --watch",
|
|
21852
|
+
"test:unit": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/unit/ --timeout=60000",
|
|
21853
|
+
"test:integration": "CI=1 NAX_SKIP_PRECHECK=1 bun test ./test/integration/ --timeout=60000",
|
|
21854
|
+
"test:ui": "CI=1 bun test ./test/ui/ --timeout=60000",
|
|
21855
|
+
"test:real": "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
|
|
21827
21856
|
"check-test-overlap": "bun run scripts/check-test-overlap.ts",
|
|
21828
21857
|
"check-dead-tests": "bun run scripts/check-dead-tests.ts",
|
|
21829
21858
|
"check:test-sizes": "bun run scripts/check-test-sizes.ts",
|
|
@@ -21871,8 +21900,8 @@ var init_version = __esm(() => {
|
|
|
21871
21900
|
NAX_VERSION = package_default.version;
|
|
21872
21901
|
NAX_COMMIT = (() => {
|
|
21873
21902
|
try {
|
|
21874
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
21875
|
-
return "
|
|
21903
|
+
if (/^[0-9a-f]{6,10}$/.test("29c340c"))
|
|
21904
|
+
return "29c340c";
|
|
21876
21905
|
} catch {}
|
|
21877
21906
|
try {
|
|
21878
21907
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -21890,6 +21919,23 @@ var init_version = __esm(() => {
|
|
|
21890
21919
|
NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
|
|
21891
21920
|
});
|
|
21892
21921
|
|
|
21922
|
+
// src/prd/validate.ts
|
|
21923
|
+
function validateStoryId(id) {
|
|
21924
|
+
if (!id || id.length === 0) {
|
|
21925
|
+
throw new Error("Story ID cannot be empty");
|
|
21926
|
+
}
|
|
21927
|
+
if (id.includes("..")) {
|
|
21928
|
+
throw new Error("Story ID cannot contain path traversal (..)");
|
|
21929
|
+
}
|
|
21930
|
+
if (id.startsWith("--")) {
|
|
21931
|
+
throw new Error("Story ID cannot start with git flags (--)");
|
|
21932
|
+
}
|
|
21933
|
+
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
21934
|
+
if (!validPattern.test(id)) {
|
|
21935
|
+
throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
|
|
21936
|
+
}
|
|
21937
|
+
}
|
|
21938
|
+
|
|
21893
21939
|
// src/errors.ts
|
|
21894
21940
|
var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
|
|
21895
21941
|
var init_errors3 = __esm(() => {
|
|
@@ -22898,7 +22944,7 @@ class WebhookInteractionPlugin {
|
|
|
22898
22944
|
this.pendingResponses.delete(requestId);
|
|
22899
22945
|
return response;
|
|
22900
22946
|
}
|
|
22901
|
-
await
|
|
22947
|
+
await _webhookPluginDeps.sleep(backoffMs);
|
|
22902
22948
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
22903
22949
|
}
|
|
22904
22950
|
return {
|
|
@@ -22997,9 +23043,12 @@ class WebhookInteractionPlugin {
|
|
|
22997
23043
|
}
|
|
22998
23044
|
}
|
|
22999
23045
|
}
|
|
23000
|
-
var WebhookConfigSchema, InteractionResponseSchema;
|
|
23046
|
+
var _webhookPluginDeps, WebhookConfigSchema, InteractionResponseSchema;
|
|
23001
23047
|
var init_webhook = __esm(() => {
|
|
23002
23048
|
init_zod();
|
|
23049
|
+
_webhookPluginDeps = {
|
|
23050
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
23051
|
+
};
|
|
23003
23052
|
WebhookConfigSchema = exports_external.object({
|
|
23004
23053
|
url: exports_external.string().url().optional(),
|
|
23005
23054
|
callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
|
|
@@ -23038,8 +23087,8 @@ class AutoInteractionPlugin {
|
|
|
23038
23087
|
return;
|
|
23039
23088
|
}
|
|
23040
23089
|
try {
|
|
23041
|
-
if (
|
|
23042
|
-
const decision2 = await
|
|
23090
|
+
if (_deps3.callLlm) {
|
|
23091
|
+
const decision2 = await _deps3.callLlm(request);
|
|
23043
23092
|
if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
|
|
23044
23093
|
return;
|
|
23045
23094
|
}
|
|
@@ -23068,7 +23117,7 @@ class AutoInteractionPlugin {
|
|
|
23068
23117
|
}
|
|
23069
23118
|
async callLlm(request) {
|
|
23070
23119
|
const prompt = this.buildPrompt(request);
|
|
23071
|
-
const adapter =
|
|
23120
|
+
const adapter = _deps3.adapter;
|
|
23072
23121
|
if (!adapter) {
|
|
23073
23122
|
throw new Error("Auto plugin requires adapter to be injected via _deps.adapter");
|
|
23074
23123
|
}
|
|
@@ -23156,7 +23205,7 @@ Respond with ONLY this JSON (no markdown, no explanation):
|
|
|
23156
23205
|
return parsed;
|
|
23157
23206
|
}
|
|
23158
23207
|
}
|
|
23159
|
-
var AutoConfigSchema,
|
|
23208
|
+
var AutoConfigSchema, _deps3;
|
|
23160
23209
|
var init_auto = __esm(() => {
|
|
23161
23210
|
init_zod();
|
|
23162
23211
|
init_config();
|
|
@@ -23166,7 +23215,7 @@ var init_auto = __esm(() => {
|
|
|
23166
23215
|
maxCostPerDecision: exports_external.number().positive().optional(),
|
|
23167
23216
|
naxConfig: exports_external.any().optional()
|
|
23168
23217
|
});
|
|
23169
|
-
|
|
23218
|
+
_deps3 = {
|
|
23170
23219
|
adapter: null,
|
|
23171
23220
|
callLlm: null
|
|
23172
23221
|
};
|
|
@@ -23529,11 +23578,23 @@ ${stderr}`;
|
|
|
23529
23578
|
logger.info("acceptance", "All acceptance tests passed");
|
|
23530
23579
|
return { action: "continue" };
|
|
23531
23580
|
}
|
|
23532
|
-
if (
|
|
23533
|
-
logger.
|
|
23581
|
+
if (failedACs.length > 0 && actualFailures.length === 0) {
|
|
23582
|
+
logger.info("acceptance", "All failed ACs are overridden \u2014 treating as pass");
|
|
23583
|
+
return { action: "continue" };
|
|
23584
|
+
}
|
|
23585
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
23586
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
23587
|
+
exitCode,
|
|
23534
23588
|
output
|
|
23535
23589
|
});
|
|
23536
|
-
|
|
23590
|
+
ctx.acceptanceFailures = {
|
|
23591
|
+
failedACs: ["AC-ERROR"],
|
|
23592
|
+
testOutput: output
|
|
23593
|
+
};
|
|
23594
|
+
return {
|
|
23595
|
+
action: "fail",
|
|
23596
|
+
reason: `Acceptance tests errored (exit code ${exitCode}): syntax error, import failure, or unhandled exception`
|
|
23597
|
+
};
|
|
23537
23598
|
}
|
|
23538
23599
|
if (actualFailures.length > 0) {
|
|
23539
23600
|
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
@@ -23847,7 +23908,7 @@ async function runReview(config2, workdir, executionConfig) {
|
|
|
23847
23908
|
const logger = getSafeLogger();
|
|
23848
23909
|
const checks3 = [];
|
|
23849
23910
|
let firstFailure;
|
|
23850
|
-
const allUncommittedFiles = await
|
|
23911
|
+
const allUncommittedFiles = await _deps4.getUncommittedFiles(workdir);
|
|
23851
23912
|
const NAX_RUNTIME_FILES = new Set(["nax/status.json", ".nax-verifier-verdict.json"]);
|
|
23852
23913
|
const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_FILES.has(f) && !f.match(/^nax\/features\/.+\/prd\.json$/));
|
|
23853
23914
|
if (uncommittedFiles.length > 0) {
|
|
@@ -23887,11 +23948,11 @@ Stage and commit these files before running review.`
|
|
|
23887
23948
|
failureReason: firstFailure
|
|
23888
23949
|
};
|
|
23889
23950
|
}
|
|
23890
|
-
var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000,
|
|
23951
|
+
var _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _deps4;
|
|
23891
23952
|
var init_runner2 = __esm(() => {
|
|
23892
23953
|
init_logger2();
|
|
23893
23954
|
_reviewRunnerDeps = { spawn, file: Bun.file };
|
|
23894
|
-
|
|
23955
|
+
_deps4 = {
|
|
23895
23956
|
getUncommittedFiles: getUncommittedFilesImpl
|
|
23896
23957
|
};
|
|
23897
23958
|
});
|
|
@@ -24996,7 +25057,7 @@ async function addFileElements(elements, storyContext, story) {
|
|
|
24996
25057
|
if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
|
|
24997
25058
|
const autoDetectConfig = storyContext.config?.context?.autoDetect;
|
|
24998
25059
|
try {
|
|
24999
|
-
const detected = await
|
|
25060
|
+
const detected = await _deps5.autoDetectContextFiles({
|
|
25000
25061
|
workdir: storyContext.workdir,
|
|
25001
25062
|
storyTitle: story.title,
|
|
25002
25063
|
maxFiles: autoDetectConfig?.maxFiles ?? 5,
|
|
@@ -25054,7 +25115,7 @@ ${content}
|
|
|
25054
25115
|
}
|
|
25055
25116
|
}
|
|
25056
25117
|
}
|
|
25057
|
-
var
|
|
25118
|
+
var _deps5;
|
|
25058
25119
|
var init_builder3 = __esm(() => {
|
|
25059
25120
|
init_logger2();
|
|
25060
25121
|
init_prd();
|
|
@@ -25062,7 +25123,7 @@ var init_builder3 = __esm(() => {
|
|
|
25062
25123
|
init_elements();
|
|
25063
25124
|
init_test_scanner();
|
|
25064
25125
|
init_elements();
|
|
25065
|
-
|
|
25126
|
+
_deps5 = {
|
|
25066
25127
|
autoDetectContextFiles
|
|
25067
25128
|
};
|
|
25068
25129
|
});
|
|
@@ -25544,6 +25605,30 @@ function detectMergeConflict(output) {
|
|
|
25544
25605
|
async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
25545
25606
|
const logger = getSafeLogger();
|
|
25546
25607
|
try {
|
|
25608
|
+
const topLevelProc = _gitDeps.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
25609
|
+
cwd: workdir,
|
|
25610
|
+
stdout: "pipe",
|
|
25611
|
+
stderr: "pipe"
|
|
25612
|
+
});
|
|
25613
|
+
const gitRoot = (await new Response(topLevelProc.stdout).text()).trim();
|
|
25614
|
+
await topLevelProc.exited;
|
|
25615
|
+
const { realpathSync: realpathSync3 } = await import("fs");
|
|
25616
|
+
const realWorkdir = (() => {
|
|
25617
|
+
try {
|
|
25618
|
+
return realpathSync3(workdir);
|
|
25619
|
+
} catch {
|
|
25620
|
+
return workdir;
|
|
25621
|
+
}
|
|
25622
|
+
})();
|
|
25623
|
+
const realGitRoot = (() => {
|
|
25624
|
+
try {
|
|
25625
|
+
return realpathSync3(gitRoot);
|
|
25626
|
+
} catch {
|
|
25627
|
+
return gitRoot;
|
|
25628
|
+
}
|
|
25629
|
+
})();
|
|
25630
|
+
if (realWorkdir !== realGitRoot)
|
|
25631
|
+
return;
|
|
25547
25632
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
25548
25633
|
cwd: workdir,
|
|
25549
25634
|
stdout: "pipe",
|
|
@@ -25929,11 +26014,15 @@ async function fullSuite(options) {
|
|
|
25929
26014
|
return runVerificationCore(options);
|
|
25930
26015
|
}
|
|
25931
26016
|
async function regression(options) {
|
|
25932
|
-
await
|
|
26017
|
+
await _regressionRunnerDeps.sleep(2000);
|
|
25933
26018
|
return runVerificationCore({ ...options, expectedFiles: undefined });
|
|
25934
26019
|
}
|
|
26020
|
+
var _regressionRunnerDeps;
|
|
25935
26021
|
var init_runners = __esm(() => {
|
|
25936
26022
|
init_executor();
|
|
26023
|
+
_regressionRunnerDeps = {
|
|
26024
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
26025
|
+
};
|
|
25937
26026
|
});
|
|
25938
26027
|
|
|
25939
26028
|
// src/verification/rectification.ts
|
|
@@ -26762,7 +26851,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
26762
26851
|
exitCode: result.exitCode
|
|
26763
26852
|
});
|
|
26764
26853
|
}
|
|
26765
|
-
await autoCommitIfDirty(workdir, "tdd", role, story.id);
|
|
26854
|
+
await _sessionRunnerDeps.autoCommitIfDirty(workdir, "tdd", role, story.id);
|
|
26766
26855
|
let isolation;
|
|
26767
26856
|
if (!skipIsolation) {
|
|
26768
26857
|
if (role === "test-writer") {
|
|
@@ -26809,6 +26898,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
26809
26898
|
estimatedCost: result.estimatedCost
|
|
26810
26899
|
};
|
|
26811
26900
|
}
|
|
26901
|
+
var _sessionRunnerDeps;
|
|
26812
26902
|
var init_session_runner = __esm(() => {
|
|
26813
26903
|
init_config();
|
|
26814
26904
|
init_logger2();
|
|
@@ -26816,6 +26906,9 @@ var init_session_runner = __esm(() => {
|
|
|
26816
26906
|
init_git();
|
|
26817
26907
|
init_cleanup();
|
|
26818
26908
|
init_isolation();
|
|
26909
|
+
_sessionRunnerDeps = {
|
|
26910
|
+
autoCommitIfDirty
|
|
26911
|
+
};
|
|
26819
26912
|
});
|
|
26820
26913
|
|
|
26821
26914
|
// src/tdd/verdict-reader.ts
|
|
@@ -27492,9 +27585,9 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
|
|
|
27492
27585
|
const plugin = ctx.interaction?.getPrimary();
|
|
27493
27586
|
if (!plugin)
|
|
27494
27587
|
return;
|
|
27495
|
-
const
|
|
27588
|
+
const QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
|
|
27496
27589
|
return {
|
|
27497
|
-
detectQuestion: async (text) =>
|
|
27590
|
+
detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
|
|
27498
27591
|
onQuestionDetected: async (text) => {
|
|
27499
27592
|
const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
27500
27593
|
await plugin.send({
|
|
@@ -29751,7 +29844,7 @@ var init_checks_config = () => {};
|
|
|
29751
29844
|
async function checkAgentCLI(config2) {
|
|
29752
29845
|
const agent = config2.execution?.agent || "claude";
|
|
29753
29846
|
try {
|
|
29754
|
-
const proc =
|
|
29847
|
+
const proc = _deps7.spawn([agent, "--version"], {
|
|
29755
29848
|
stdout: "pipe",
|
|
29756
29849
|
stderr: "pipe"
|
|
29757
29850
|
});
|
|
@@ -29772,9 +29865,9 @@ async function checkAgentCLI(config2) {
|
|
|
29772
29865
|
};
|
|
29773
29866
|
}
|
|
29774
29867
|
}
|
|
29775
|
-
var
|
|
29868
|
+
var _deps7;
|
|
29776
29869
|
var init_checks_cli = __esm(() => {
|
|
29777
|
-
|
|
29870
|
+
_deps7 = {
|
|
29778
29871
|
spawn: Bun.spawn
|
|
29779
29872
|
};
|
|
29780
29873
|
});
|
|
@@ -31363,23 +31456,6 @@ var init_headless_formatter = __esm(() => {
|
|
|
31363
31456
|
init_version();
|
|
31364
31457
|
});
|
|
31365
31458
|
|
|
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
31459
|
// src/worktree/manager.ts
|
|
31384
31460
|
var exports_manager = {};
|
|
31385
31461
|
__export(exports_manager, {
|
|
@@ -65007,7 +65083,7 @@ import { existsSync as existsSync8 } from "fs";
|
|
|
65007
65083
|
import { join as join9 } from "path";
|
|
65008
65084
|
|
|
65009
65085
|
// src/analyze/scanner.ts
|
|
65010
|
-
import { existsSync as existsSync2 } from "fs";
|
|
65086
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
65011
65087
|
import { join as join4 } from "path";
|
|
65012
65088
|
async function scanCodebase(workdir) {
|
|
65013
65089
|
const srcPath = join4(workdir, "src");
|
|
@@ -65036,30 +65112,23 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
|
|
|
65036
65112
|
}
|
|
65037
65113
|
const entries = [];
|
|
65038
65114
|
try {
|
|
65039
|
-
const dirEntries =
|
|
65040
|
-
cwd: dir,
|
|
65041
|
-
onlyFiles: false
|
|
65042
|
-
}));
|
|
65115
|
+
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
|
65043
65116
|
dirEntries.sort((a, b) => {
|
|
65044
|
-
|
|
65045
|
-
const bIsDir = !b.includes(".");
|
|
65046
|
-
if (aIsDir && !bIsDir)
|
|
65117
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
65047
65118
|
return -1;
|
|
65048
|
-
if (!
|
|
65119
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
65049
65120
|
return 1;
|
|
65050
|
-
return a.localeCompare(b);
|
|
65121
|
+
return a.name.localeCompare(b.name);
|
|
65051
65122
|
});
|
|
65052
65123
|
for (let i = 0;i < dirEntries.length; i++) {
|
|
65053
|
-
const
|
|
65054
|
-
const fullPath = join4(dir, entry);
|
|
65124
|
+
const dirent = dirEntries[i];
|
|
65055
65125
|
const isLast = i === dirEntries.length - 1;
|
|
65056
65126
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
65057
65127
|
const childPrefix = isLast ? " " : "\u2502 ";
|
|
65058
|
-
const
|
|
65059
|
-
|
|
65060
|
-
entries.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
65128
|
+
const isDir = dirent.isDirectory();
|
|
65129
|
+
entries.push(`${prefix}${connector}${dirent.name}${isDir ? "/" : ""}`);
|
|
65061
65130
|
if (isDir) {
|
|
65062
|
-
const subtree = await generateFileTree(
|
|
65131
|
+
const subtree = await generateFileTree(join4(dir, dirent.name), maxDepth, currentDepth + 1, prefix + childPrefix);
|
|
65063
65132
|
if (subtree) {
|
|
65064
65133
|
entries.push(subtree);
|
|
65065
65134
|
}
|
|
@@ -65473,94 +65542,259 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
|
|
|
65473
65542
|
}
|
|
65474
65543
|
}
|
|
65475
65544
|
// src/cli/plan.ts
|
|
65476
|
-
|
|
65545
|
+
init_registry();
|
|
65477
65546
|
import { existsSync as existsSync9 } from "fs";
|
|
65478
65547
|
import { join as join10 } from "path";
|
|
65479
65548
|
import { createInterface } from "readline";
|
|
65480
|
-
init_schema();
|
|
65481
65549
|
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
|
-
|
|
65499
|
-
## Problem
|
|
65500
|
-
Why this is needed.
|
|
65501
|
-
|
|
65502
|
-
## Requirements
|
|
65503
|
-
- REQ-1: ...
|
|
65504
|
-
- REQ-2: ...
|
|
65505
|
-
|
|
65506
|
-
## Acceptance Criteria
|
|
65507
|
-
- AC-1: ...
|
|
65508
65550
|
|
|
65509
|
-
|
|
65510
|
-
|
|
65551
|
+
// src/prd/schema.ts
|
|
65552
|
+
var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
|
|
65553
|
+
var VALID_TEST_STRATEGIES = [
|
|
65554
|
+
"test-after",
|
|
65555
|
+
"tdd-simple",
|
|
65556
|
+
"three-session-tdd",
|
|
65557
|
+
"three-session-tdd-lite"
|
|
65558
|
+
];
|
|
65559
|
+
var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
|
|
65560
|
+
function extractJsonFromMarkdown(text) {
|
|
65561
|
+
const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
|
|
65562
|
+
if (match) {
|
|
65563
|
+
return match[1] ?? text;
|
|
65564
|
+
}
|
|
65565
|
+
return text;
|
|
65566
|
+
}
|
|
65567
|
+
function stripTrailingCommas(text) {
|
|
65568
|
+
return text.replace(/,\s*([}\]])/g, "$1");
|
|
65569
|
+
}
|
|
65570
|
+
function normalizeStoryId(id) {
|
|
65571
|
+
const match = id.match(STORY_ID_NO_SEPARATOR);
|
|
65572
|
+
if (match) {
|
|
65573
|
+
return `${match[1]}-${match[2]}`;
|
|
65574
|
+
}
|
|
65575
|
+
return id;
|
|
65576
|
+
}
|
|
65577
|
+
function normalizeComplexity2(raw) {
|
|
65578
|
+
const lower = raw.toLowerCase();
|
|
65579
|
+
if (VALID_COMPLEXITY.includes(lower)) {
|
|
65580
|
+
return lower;
|
|
65581
|
+
}
|
|
65582
|
+
return null;
|
|
65583
|
+
}
|
|
65584
|
+
function validateStory(raw, index, allIds) {
|
|
65585
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
65586
|
+
throw new Error(`[schema] story[${index}] must be an object`);
|
|
65587
|
+
}
|
|
65588
|
+
const s = raw;
|
|
65589
|
+
const rawId = s.id;
|
|
65590
|
+
if (rawId === undefined || rawId === null || rawId === "") {
|
|
65591
|
+
throw new Error(`[schema] story[${index}].id is required and must be non-empty`);
|
|
65592
|
+
}
|
|
65593
|
+
if (typeof rawId !== "string") {
|
|
65594
|
+
throw new Error(`[schema] story[${index}].id must be a string`);
|
|
65595
|
+
}
|
|
65596
|
+
const id = normalizeStoryId(rawId);
|
|
65597
|
+
validateStoryId(id);
|
|
65598
|
+
const title = s.title;
|
|
65599
|
+
if (!title || typeof title !== "string" || title.trim() === "") {
|
|
65600
|
+
throw new Error(`[schema] story[${index}].title is required and must be non-empty`);
|
|
65601
|
+
}
|
|
65602
|
+
const description = s.description;
|
|
65603
|
+
if (!description || typeof description !== "string" || description.trim() === "") {
|
|
65604
|
+
throw new Error(`[schema] story[${index}].description is required and must be non-empty`);
|
|
65605
|
+
}
|
|
65606
|
+
const ac = s.acceptanceCriteria;
|
|
65607
|
+
if (!Array.isArray(ac) || ac.length === 0) {
|
|
65608
|
+
throw new Error(`[schema] story[${index}].acceptanceCriteria is required and must be a non-empty array`);
|
|
65609
|
+
}
|
|
65610
|
+
for (let i = 0;i < ac.length; i++) {
|
|
65611
|
+
if (typeof ac[i] !== "string") {
|
|
65612
|
+
throw new Error(`[schema] story[${index}].acceptanceCriteria[${i}] must be a string`);
|
|
65613
|
+
}
|
|
65614
|
+
}
|
|
65615
|
+
const routing = typeof s.routing === "object" && s.routing !== null ? s.routing : {};
|
|
65616
|
+
const rawComplexity = routing.complexity ?? s.complexity;
|
|
65617
|
+
if (rawComplexity === undefined || rawComplexity === null) {
|
|
65618
|
+
throw new Error(`[schema] story[${index}] missing complexity. Set routing.complexity to one of: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65619
|
+
}
|
|
65620
|
+
if (typeof rawComplexity !== "string") {
|
|
65621
|
+
throw new Error(`[schema] story[${index}].routing.complexity must be a string`);
|
|
65622
|
+
}
|
|
65623
|
+
const complexity = normalizeComplexity2(rawComplexity);
|
|
65624
|
+
if (complexity === null) {
|
|
65625
|
+
throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65626
|
+
}
|
|
65627
|
+
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
65628
|
+
const STRATEGY_ALIASES = { "tdd-lite": "three-session-tdd-lite" };
|
|
65629
|
+
const normalizedStrategy = typeof rawTestStrategy === "string" ? STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy : rawTestStrategy;
|
|
65630
|
+
const testStrategy = normalizedStrategy !== undefined && VALID_TEST_STRATEGIES.includes(normalizedStrategy) ? normalizedStrategy : "tdd-simple";
|
|
65631
|
+
const rawDeps = s.dependencies;
|
|
65632
|
+
const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
|
|
65633
|
+
for (const dep of dependencies) {
|
|
65634
|
+
if (!allIds.has(dep)) {
|
|
65635
|
+
throw new Error(`[schema] story[${index}].dependencies references unknown story ID "${dep}"`);
|
|
65636
|
+
}
|
|
65637
|
+
}
|
|
65638
|
+
const rawTags = s.tags;
|
|
65639
|
+
const tags = Array.isArray(rawTags) ? rawTags : [];
|
|
65640
|
+
return {
|
|
65641
|
+
id,
|
|
65642
|
+
title: title.trim(),
|
|
65643
|
+
description: description.trim(),
|
|
65644
|
+
acceptanceCriteria: ac,
|
|
65645
|
+
tags,
|
|
65646
|
+
dependencies,
|
|
65647
|
+
status: "pending",
|
|
65648
|
+
passes: false,
|
|
65649
|
+
attempts: 0,
|
|
65650
|
+
escalations: [],
|
|
65651
|
+
routing: {
|
|
65652
|
+
complexity,
|
|
65653
|
+
testStrategy,
|
|
65654
|
+
reasoning: "validated from LLM output"
|
|
65655
|
+
}
|
|
65656
|
+
};
|
|
65657
|
+
}
|
|
65658
|
+
function parseRawString(text) {
|
|
65659
|
+
const extracted = extractJsonFromMarkdown(text);
|
|
65660
|
+
const cleaned = stripTrailingCommas(extracted);
|
|
65661
|
+
try {
|
|
65662
|
+
return JSON.parse(cleaned);
|
|
65663
|
+
} catch (err) {
|
|
65664
|
+
const parseErr = err;
|
|
65665
|
+
throw new Error(`[schema] Failed to parse JSON: ${parseErr.message}`, { cause: parseErr });
|
|
65666
|
+
}
|
|
65667
|
+
}
|
|
65668
|
+
function validatePlanOutput(raw, feature, branch) {
|
|
65669
|
+
const parsed = typeof raw === "string" ? parseRawString(raw) : raw;
|
|
65670
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
65671
|
+
throw new Error("[schema] PRD output must be a JSON object");
|
|
65672
|
+
}
|
|
65673
|
+
const obj = parsed;
|
|
65674
|
+
const rawStories = obj.userStories;
|
|
65675
|
+
if (!Array.isArray(rawStories) || rawStories.length === 0) {
|
|
65676
|
+
throw new Error("[schema] userStories is required and must be a non-empty array");
|
|
65677
|
+
}
|
|
65678
|
+
const allIds = new Set;
|
|
65679
|
+
for (const story of rawStories) {
|
|
65680
|
+
if (typeof story === "object" && story !== null && !Array.isArray(story)) {
|
|
65681
|
+
const s = story;
|
|
65682
|
+
const rawId = s.id;
|
|
65683
|
+
if (typeof rawId === "string" && rawId !== "") {
|
|
65684
|
+
allIds.add(normalizeStoryId(rawId));
|
|
65685
|
+
}
|
|
65686
|
+
}
|
|
65687
|
+
}
|
|
65688
|
+
const userStories = rawStories.map((story, index) => validateStory(story, index, allIds));
|
|
65689
|
+
const now = new Date().toISOString();
|
|
65690
|
+
return {
|
|
65691
|
+
project: typeof obj.project === "string" && obj.project !== "" ? obj.project : feature,
|
|
65692
|
+
feature,
|
|
65693
|
+
branchName: branch,
|
|
65694
|
+
createdAt: typeof obj.createdAt === "string" ? obj.createdAt : now,
|
|
65695
|
+
updatedAt: now,
|
|
65696
|
+
userStories
|
|
65697
|
+
};
|
|
65698
|
+
}
|
|
65511
65699
|
|
|
65512
|
-
|
|
65513
|
-
|
|
65514
|
-
|
|
65515
|
-
|
|
65516
|
-
|
|
65517
|
-
|
|
65518
|
-
|
|
65519
|
-
|
|
65700
|
+
// src/cli/plan.ts
|
|
65701
|
+
var _deps2 = {
|
|
65702
|
+
readFile: (path) => Bun.file(path).text(),
|
|
65703
|
+
writeFile: (path, content) => Bun.write(path, content).then(() => {}),
|
|
65704
|
+
scanCodebase: (workdir) => scanCodebase(workdir),
|
|
65705
|
+
getAgent: (name) => getAgent(name),
|
|
65706
|
+
readPackageJson: (workdir) => Bun.file(join10(workdir, "package.json")).json().catch(() => null),
|
|
65707
|
+
spawnSync: (cmd, opts) => {
|
|
65708
|
+
const result = Bun.spawnSync(cmd, opts ? { cwd: opts.cwd } : {});
|
|
65709
|
+
return { stdout: result.stdout, exitCode: result.exitCode };
|
|
65710
|
+
},
|
|
65711
|
+
mkdirp: (path) => Bun.spawn(["mkdir", "-p", path]).exited.then(() => {})
|
|
65712
|
+
};
|
|
65713
|
+
async function planCommand(workdir, config2, options) {
|
|
65714
|
+
const naxDir = join10(workdir, "nax");
|
|
65715
|
+
if (!existsSync9(naxDir)) {
|
|
65520
65716
|
throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
|
|
65521
65717
|
}
|
|
65522
65718
|
const logger = getLogger();
|
|
65523
|
-
logger
|
|
65524
|
-
const
|
|
65719
|
+
logger?.info("plan", "Reading spec", { from: options.from });
|
|
65720
|
+
const specContent = await _deps2.readFile(options.from);
|
|
65721
|
+
logger?.info("plan", "Scanning codebase...");
|
|
65722
|
+
const scan = await _deps2.scanCodebase(workdir);
|
|
65525
65723
|
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
|
-
}
|
|
65724
|
+
const pkg = await _deps2.readPackageJson(workdir);
|
|
65725
|
+
const projectName = detectProjectName(workdir, pkg);
|
|
65726
|
+
const branchName = options.branch ?? `feat/${options.feature}`;
|
|
65727
|
+
const prompt = buildPlanningPrompt(specContent, codebaseContext);
|
|
65728
|
+
const agentName = config2?.autoMode?.defaultAgent ?? "claude";
|
|
65729
|
+
const adapter = _deps2.getAgent(agentName);
|
|
65730
|
+
if (!adapter) {
|
|
65731
|
+
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65732
|
+
}
|
|
65733
|
+
const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
|
|
65734
|
+
let rawResponse;
|
|
65735
|
+
if (options.auto) {
|
|
65736
|
+
rawResponse = await adapter.complete(prompt, { jsonMode: true });
|
|
65555
65737
|
} else {
|
|
65556
|
-
|
|
65557
|
-
|
|
65738
|
+
const interactionBridge = createCliInteractionBridge();
|
|
65739
|
+
logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
|
|
65740
|
+
try {
|
|
65741
|
+
const result = await adapter.plan({
|
|
65742
|
+
prompt,
|
|
65743
|
+
workdir,
|
|
65744
|
+
interactive: true,
|
|
65745
|
+
timeoutSeconds,
|
|
65746
|
+
interactionBridge
|
|
65747
|
+
});
|
|
65748
|
+
rawResponse = result.specContent;
|
|
65749
|
+
} finally {
|
|
65750
|
+
logger?.info("plan", "Interactive session ended");
|
|
65558
65751
|
}
|
|
65559
|
-
await Bun.write(outputPath, result.specContent);
|
|
65560
65752
|
}
|
|
65561
|
-
|
|
65753
|
+
const finalPrd = validatePlanOutput(rawResponse, options.feature, branchName);
|
|
65754
|
+
finalPrd.project = projectName;
|
|
65755
|
+
const outputDir = join10(naxDir, "features", options.feature);
|
|
65756
|
+
const outputPath = join10(outputDir, "prd.json");
|
|
65757
|
+
await _deps2.mkdirp(outputDir);
|
|
65758
|
+
await _deps2.writeFile(outputPath, JSON.stringify(finalPrd, null, 2));
|
|
65759
|
+
logger?.info("plan", "[OK] PRD written", { outputPath });
|
|
65562
65760
|
return outputPath;
|
|
65563
65761
|
}
|
|
65762
|
+
function createCliInteractionBridge() {
|
|
65763
|
+
return {
|
|
65764
|
+
async detectQuestion(text) {
|
|
65765
|
+
return text.includes("?");
|
|
65766
|
+
},
|
|
65767
|
+
async onQuestionDetected(text) {
|
|
65768
|
+
if (!process.stdin.isTTY) {
|
|
65769
|
+
return "";
|
|
65770
|
+
}
|
|
65771
|
+
process.stdout.write(`
|
|
65772
|
+
\uD83E\uDD16 Agent: ${text}
|
|
65773
|
+
You: `);
|
|
65774
|
+
return new Promise((resolve6) => {
|
|
65775
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
65776
|
+
rl.once("line", (line) => {
|
|
65777
|
+
rl.close();
|
|
65778
|
+
resolve6(line.trim());
|
|
65779
|
+
});
|
|
65780
|
+
rl.once("close", () => resolve6(""));
|
|
65781
|
+
});
|
|
65782
|
+
}
|
|
65783
|
+
};
|
|
65784
|
+
}
|
|
65785
|
+
function detectProjectName(workdir, pkg) {
|
|
65786
|
+
if (pkg?.name && typeof pkg.name === "string") {
|
|
65787
|
+
return pkg.name;
|
|
65788
|
+
}
|
|
65789
|
+
const result = _deps2.spawnSync(["git", "remote", "get-url", "origin"], { cwd: workdir });
|
|
65790
|
+
if (result.exitCode === 0) {
|
|
65791
|
+
const url2 = result.stdout.toString().trim();
|
|
65792
|
+
const match = url2.match(/\/([^/]+?)(?:\.git)?$/);
|
|
65793
|
+
if (match?.[1])
|
|
65794
|
+
return match[1];
|
|
65795
|
+
}
|
|
65796
|
+
return "unknown";
|
|
65797
|
+
}
|
|
65564
65798
|
function buildCodebaseContext2(scan) {
|
|
65565
65799
|
const sections = [];
|
|
65566
65800
|
sections.push(`## Codebase Structure
|
|
@@ -65587,24 +65821,63 @@ function buildCodebaseContext2(scan) {
|
|
|
65587
65821
|
return sections.join(`
|
|
65588
65822
|
`);
|
|
65589
65823
|
}
|
|
65590
|
-
function
|
|
65591
|
-
return `You are
|
|
65824
|
+
function buildPlanningPrompt(specContent, codebaseContext) {
|
|
65825
|
+
return `You are a senior software architect generating a product requirements document (PRD) as JSON.
|
|
65592
65826
|
|
|
65593
|
-
|
|
65827
|
+
## Spec
|
|
65594
65828
|
|
|
65595
|
-
|
|
65829
|
+
${specContent}
|
|
65596
65830
|
|
|
65597
|
-
|
|
65831
|
+
## Codebase Context
|
|
65598
65832
|
|
|
65599
|
-
|
|
65600
|
-
|
|
65601
|
-
|
|
65602
|
-
|
|
65603
|
-
|
|
65604
|
-
|
|
65605
|
-
|
|
65833
|
+
${codebaseContext}
|
|
65834
|
+
|
|
65835
|
+
## Output Schema
|
|
65836
|
+
|
|
65837
|
+
Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
|
|
65838
|
+
|
|
65839
|
+
{
|
|
65840
|
+
"project": "string \u2014 project name",
|
|
65841
|
+
"feature": "string \u2014 feature name",
|
|
65842
|
+
"branchName": "string \u2014 git branch (e.g. feat/my-feature)",
|
|
65843
|
+
"createdAt": "ISO 8601 timestamp",
|
|
65844
|
+
"updatedAt": "ISO 8601 timestamp",
|
|
65845
|
+
"userStories": [
|
|
65846
|
+
{
|
|
65847
|
+
"id": "string \u2014 e.g. US-001",
|
|
65848
|
+
"title": "string \u2014 concise story title",
|
|
65849
|
+
"description": "string \u2014 detailed description of the story",
|
|
65850
|
+
"acceptanceCriteria": ["string \u2014 each AC line"],
|
|
65851
|
+
"tags": ["string \u2014 routing tags, e.g. feature, security, api"],
|
|
65852
|
+
"dependencies": ["string \u2014 story IDs this story depends on"],
|
|
65853
|
+
"status": "pending",
|
|
65854
|
+
"passes": false,
|
|
65855
|
+
"routing": {
|
|
65856
|
+
"complexity": "simple | medium | complex | expert",
|
|
65857
|
+
"testStrategy": "test-after | tdd-simple | three-session-tdd | three-session-tdd-lite",
|
|
65858
|
+
"reasoning": "string \u2014 brief classification rationale"
|
|
65859
|
+
},
|
|
65860
|
+
"escalations": [],
|
|
65861
|
+
"attempts": 0
|
|
65862
|
+
}
|
|
65863
|
+
]
|
|
65864
|
+
}
|
|
65865
|
+
|
|
65866
|
+
## Complexity Classification Guide
|
|
65867
|
+
|
|
65868
|
+
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
|
|
65869
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
65870
|
+
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
65871
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
65606
65872
|
|
|
65607
|
-
|
|
65873
|
+
## Test Strategy Guide
|
|
65874
|
+
|
|
65875
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
65876
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
65877
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
65878
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
65879
|
+
|
|
65880
|
+
Output ONLY the JSON object. Do not wrap in markdown code blocks.`;
|
|
65608
65881
|
}
|
|
65609
65882
|
// src/cli/accept.ts
|
|
65610
65883
|
init_config();
|
|
@@ -65753,13 +66026,13 @@ async function displayModelEfficiency(workdir) {
|
|
|
65753
66026
|
}
|
|
65754
66027
|
// src/cli/status-features.ts
|
|
65755
66028
|
init_source();
|
|
65756
|
-
import { existsSync as existsSync11, readdirSync as
|
|
66029
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
|
|
65757
66030
|
import { join as join13 } from "path";
|
|
65758
66031
|
|
|
65759
66032
|
// src/commands/common.ts
|
|
65760
66033
|
init_path_security2();
|
|
65761
66034
|
init_errors3();
|
|
65762
|
-
import { existsSync as existsSync10, readdirSync, realpathSync as realpathSync2 } from "fs";
|
|
66035
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
65763
66036
|
import { join as join11, resolve as resolve6 } from "path";
|
|
65764
66037
|
function resolveProject(options = {}) {
|
|
65765
66038
|
const { dir, feature } = options;
|
|
@@ -65798,7 +66071,7 @@ Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, co
|
|
|
65798
66071
|
const featuresDir = join11(naxDir, "features");
|
|
65799
66072
|
featureDir = join11(featuresDir, feature);
|
|
65800
66073
|
if (!existsSync10(featureDir)) {
|
|
65801
|
-
const availableFeatures = existsSync10(featuresDir) ?
|
|
66074
|
+
const availableFeatures = existsSync10(featuresDir) ? readdirSync2(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
65802
66075
|
const availableMsg = availableFeatures.length > 0 ? `
|
|
65803
66076
|
|
|
65804
66077
|
Available features:
|
|
@@ -65910,7 +66183,7 @@ async function getFeatureSummary(featureName, featureDir) {
|
|
|
65910
66183
|
}
|
|
65911
66184
|
const runsDir = join13(featureDir, "runs");
|
|
65912
66185
|
if (existsSync11(runsDir)) {
|
|
65913
|
-
const runs =
|
|
66186
|
+
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
66187
|
if (runs.length > 0) {
|
|
65915
66188
|
const latestRun = runs[0].replace(".jsonl", "");
|
|
65916
66189
|
summary.lastRun = latestRun;
|
|
@@ -65924,7 +66197,7 @@ async function displayAllFeatures(projectDir) {
|
|
|
65924
66197
|
console.log(source_default.dim("No features found."));
|
|
65925
66198
|
return;
|
|
65926
66199
|
}
|
|
65927
|
-
const features =
|
|
66200
|
+
const features = readdirSync3(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
65928
66201
|
if (features.length === 0) {
|
|
65929
66202
|
console.log(source_default.dim("No features found."));
|
|
65930
66203
|
return;
|
|
@@ -66100,7 +66373,7 @@ async function displayFeatureStatus(options = {}) {
|
|
|
66100
66373
|
// src/cli/runs.ts
|
|
66101
66374
|
init_errors3();
|
|
66102
66375
|
init_logger2();
|
|
66103
|
-
import { existsSync as existsSync12, readdirSync as
|
|
66376
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "fs";
|
|
66104
66377
|
import { join as join14 } from "path";
|
|
66105
66378
|
async function parseRunLog(logPath) {
|
|
66106
66379
|
const logger = getLogger();
|
|
@@ -66122,7 +66395,7 @@ async function runsListCommand(options) {
|
|
|
66122
66395
|
logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
|
|
66123
66396
|
return;
|
|
66124
66397
|
}
|
|
66125
|
-
const files =
|
|
66398
|
+
const files = readdirSync4(runsDir).filter((f) => f.endsWith(".jsonl"));
|
|
66126
66399
|
if (files.length === 0) {
|
|
66127
66400
|
logger.info("cli", "No runs found for feature", { feature });
|
|
66128
66401
|
return;
|
|
@@ -66627,7 +66900,7 @@ function pad(str, width) {
|
|
|
66627
66900
|
init_config();
|
|
66628
66901
|
init_logger2();
|
|
66629
66902
|
init_prd();
|
|
66630
|
-
import { existsSync as existsSync17, readdirSync as
|
|
66903
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5 } from "fs";
|
|
66631
66904
|
import { join as join25 } from "path";
|
|
66632
66905
|
|
|
66633
66906
|
// src/cli/diagnose-analysis.ts
|
|
@@ -66888,7 +67161,7 @@ async function diagnoseCommand(options = {}) {
|
|
|
66888
67161
|
const featuresDir = join25(projectDir, "nax", "features");
|
|
66889
67162
|
if (!existsSync17(featuresDir))
|
|
66890
67163
|
throw new Error("No features found in project");
|
|
66891
|
-
const features =
|
|
67164
|
+
const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
66892
67165
|
if (features.length === 0)
|
|
66893
67166
|
throw new Error("No features found");
|
|
66894
67167
|
feature = features[0];
|
|
@@ -67896,19 +68169,19 @@ import { join as join33 } from "path";
|
|
|
67896
68169
|
// src/commands/logs-formatter.ts
|
|
67897
68170
|
init_source();
|
|
67898
68171
|
init_formatter();
|
|
67899
|
-
import { readdirSync as
|
|
68172
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
67900
68173
|
import { join as join32 } from "path";
|
|
67901
68174
|
|
|
67902
68175
|
// src/commands/logs-reader.ts
|
|
67903
|
-
import { existsSync as existsSync23, readdirSync as
|
|
68176
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6 } from "fs";
|
|
67904
68177
|
import { readdir as readdir3 } from "fs/promises";
|
|
67905
68178
|
import { homedir as homedir3 } from "os";
|
|
67906
68179
|
import { join as join31 } from "path";
|
|
67907
|
-
var
|
|
68180
|
+
var _deps6 = {
|
|
67908
68181
|
getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
|
|
67909
68182
|
};
|
|
67910
68183
|
async function resolveRunFileFromRegistry(runId) {
|
|
67911
|
-
const runsDir =
|
|
68184
|
+
const runsDir = _deps6.getRunsDir();
|
|
67912
68185
|
let entries;
|
|
67913
68186
|
try {
|
|
67914
68187
|
entries = await readdir3(runsDir);
|
|
@@ -67933,7 +68206,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
67933
68206
|
console.log(`Log directory unavailable for run: ${runId}`);
|
|
67934
68207
|
return null;
|
|
67935
68208
|
}
|
|
67936
|
-
const files =
|
|
68209
|
+
const files = readdirSync6(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
67937
68210
|
if (files.length === 0) {
|
|
67938
68211
|
console.log(`No log files found for run: ${runId}`);
|
|
67939
68212
|
return null;
|
|
@@ -67942,7 +68215,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
67942
68215
|
return join31(matched.eventsDir, specificFile ?? files[0]);
|
|
67943
68216
|
}
|
|
67944
68217
|
async function selectRunFile(runsDir) {
|
|
67945
|
-
const files =
|
|
68218
|
+
const files = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
67946
68219
|
if (files.length === 0) {
|
|
67947
68220
|
return null;
|
|
67948
68221
|
}
|
|
@@ -68020,7 +68293,7 @@ var LOG_LEVEL_PRIORITY2 = {
|
|
|
68020
68293
|
error: 3
|
|
68021
68294
|
};
|
|
68022
68295
|
async function displayRunsList(runsDir) {
|
|
68023
|
-
const files =
|
|
68296
|
+
const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
68024
68297
|
if (files.length === 0) {
|
|
68025
68298
|
console.log(source_default.dim("No runs found"));
|
|
68026
68299
|
return;
|
|
@@ -68215,7 +68488,7 @@ async function precheckCommand(options) {
|
|
|
68215
68488
|
}
|
|
68216
68489
|
if (!existsSync29(prdPath)) {
|
|
68217
68490
|
console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
|
|
68218
|
-
console.error(source_default.dim(`Run: nax
|
|
68491
|
+
console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
|
|
68219
68492
|
process.exit(EXIT_CODES.INVALID_PRD);
|
|
68220
68493
|
}
|
|
68221
68494
|
const config2 = await loadConfig(resolved.projectDir);
|
|
@@ -68234,7 +68507,7 @@ import { readdir as readdir4 } from "fs/promises";
|
|
|
68234
68507
|
import { homedir as homedir4 } from "os";
|
|
68235
68508
|
import { join as join35 } from "path";
|
|
68236
68509
|
var DEFAULT_LIMIT = 20;
|
|
68237
|
-
var
|
|
68510
|
+
var _deps8 = {
|
|
68238
68511
|
getRunsDir: () => join35(homedir4(), ".nax", "runs")
|
|
68239
68512
|
};
|
|
68240
68513
|
function formatDuration3(ms) {
|
|
@@ -68277,7 +68550,7 @@ function pad3(str, width) {
|
|
|
68277
68550
|
return str + " ".repeat(padding);
|
|
68278
68551
|
}
|
|
68279
68552
|
async function runsCommand(options = {}) {
|
|
68280
|
-
const runsDir =
|
|
68553
|
+
const runsDir = _deps8.getRunsDir();
|
|
68281
68554
|
let entries;
|
|
68282
68555
|
try {
|
|
68283
68556
|
entries = await readdir4(runsDir);
|
|
@@ -76153,6 +76426,31 @@ function renderTui(props) {
|
|
|
76153
76426
|
init_version();
|
|
76154
76427
|
var program2 = new Command;
|
|
76155
76428
|
program2.name("nax").description("AI Coding Agent Orchestrator \u2014 loops until done").version(NAX_VERSION);
|
|
76429
|
+
async function promptForConfirmation(question) {
|
|
76430
|
+
if (!process.stdin.isTTY) {
|
|
76431
|
+
return true;
|
|
76432
|
+
}
|
|
76433
|
+
return new Promise((resolve9) => {
|
|
76434
|
+
process.stdout.write(source_default.bold(`${question} [Y/n] `));
|
|
76435
|
+
process.stdin.setRawMode(true);
|
|
76436
|
+
process.stdin.resume();
|
|
76437
|
+
process.stdin.setEncoding("utf8");
|
|
76438
|
+
const handler = (char) => {
|
|
76439
|
+
process.stdin.setRawMode(false);
|
|
76440
|
+
process.stdin.pause();
|
|
76441
|
+
process.stdin.removeListener("data", handler);
|
|
76442
|
+
const answer = char.toLowerCase();
|
|
76443
|
+
process.stdout.write(`
|
|
76444
|
+
`);
|
|
76445
|
+
if (answer === "n") {
|
|
76446
|
+
resolve9(false);
|
|
76447
|
+
} else {
|
|
76448
|
+
resolve9(true);
|
|
76449
|
+
}
|
|
76450
|
+
};
|
|
76451
|
+
process.stdin.on("data", handler);
|
|
76452
|
+
});
|
|
76453
|
+
}
|
|
76156
76454
|
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
76455
|
let workdir;
|
|
76158
76456
|
try {
|
|
@@ -76267,7 +76565,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
|
|
|
76267
76565
|
console.log(source_default.dim(`
|
|
76268
76566
|
Next: nax features create <name>`));
|
|
76269
76567
|
});
|
|
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) => {
|
|
76568
|
+
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("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).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
76569
|
let workdir;
|
|
76272
76570
|
try {
|
|
76273
76571
|
workdir = validateDirectory(options.dir);
|
|
@@ -76275,6 +76573,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76275
76573
|
console.error(source_default.red(`Invalid directory: ${err.message}`));
|
|
76276
76574
|
process.exit(1);
|
|
76277
76575
|
}
|
|
76576
|
+
if (options.plan && !options.from) {
|
|
76577
|
+
console.error(source_default.red("Error: --plan requires --from <spec-path>"));
|
|
76578
|
+
process.exit(1);
|
|
76579
|
+
}
|
|
76580
|
+
if (options.from && !existsSync32(options.from)) {
|
|
76581
|
+
console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
|
|
76582
|
+
process.exit(1);
|
|
76583
|
+
}
|
|
76278
76584
|
let logLevel = "info";
|
|
76279
76585
|
const envLevel = process.env.NAX_LOG_LEVEL?.toLowerCase();
|
|
76280
76586
|
if (envLevel && ["error", "warn", "info", "debug"].includes(envLevel)) {
|
|
@@ -76302,6 +76608,38 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76302
76608
|
}
|
|
76303
76609
|
const featureDir = join43(naxDir, "features", options.feature);
|
|
76304
76610
|
const prdPath = join43(featureDir, "prd.json");
|
|
76611
|
+
if (options.plan && options.from) {
|
|
76612
|
+
try {
|
|
76613
|
+
console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
|
|
76614
|
+
const generatedPrdPath = await planCommand(workdir, config2, {
|
|
76615
|
+
from: options.from,
|
|
76616
|
+
feature: options.feature,
|
|
76617
|
+
auto: options.oneShot ?? false,
|
|
76618
|
+
branch: undefined
|
|
76619
|
+
});
|
|
76620
|
+
const generatedPrd = await loadPRD(generatedPrdPath);
|
|
76621
|
+
console.log(source_default.bold(`
|
|
76622
|
+
\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`));
|
|
76623
|
+
console.log(source_default.dim(`Feature: ${generatedPrd.feature}`));
|
|
76624
|
+
console.log(source_default.dim(`Stories: ${generatedPrd.userStories.length}`));
|
|
76625
|
+
console.log();
|
|
76626
|
+
for (const story of generatedPrd.userStories) {
|
|
76627
|
+
const complexity = story.routing?.complexity || "unknown";
|
|
76628
|
+
console.log(source_default.dim(` ${story.id}: ${story.title} [${complexity}]`));
|
|
76629
|
+
}
|
|
76630
|
+
console.log();
|
|
76631
|
+
if (!options.headless) {
|
|
76632
|
+
const confirmationResult = await promptForConfirmation("Proceed with execution?");
|
|
76633
|
+
if (!confirmationResult) {
|
|
76634
|
+
console.log(source_default.yellow("Execution cancelled."));
|
|
76635
|
+
process.exit(0);
|
|
76636
|
+
}
|
|
76637
|
+
}
|
|
76638
|
+
} catch (err) {
|
|
76639
|
+
console.error(source_default.red(`Error during planning: ${err.message}`));
|
|
76640
|
+
process.exit(1);
|
|
76641
|
+
}
|
|
76642
|
+
}
|
|
76305
76643
|
if (!existsSync32(prdPath)) {
|
|
76306
76644
|
console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
|
|
76307
76645
|
process.exit(1);
|
|
@@ -76452,7 +76790,7 @@ Created: ${new Date().toISOString()}
|
|
|
76452
76790
|
console.log(source_default.dim(" \u251C\u2500\u2500 tasks.md"));
|
|
76453
76791
|
console.log(source_default.dim(" \u2514\u2500\u2500 progress.txt"));
|
|
76454
76792
|
console.log(source_default.dim(`
|
|
76455
|
-
Next: Edit spec.md and tasks.md, then: nax
|
|
76793
|
+
Next: Edit spec.md and tasks.md, then: nax plan -f ${name} --from spec.md --auto`));
|
|
76456
76794
|
});
|
|
76457
76795
|
features.command("list").description("List all features").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
76458
76796
|
let workdir;
|
|
@@ -76472,8 +76810,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
76472
76810
|
console.log(source_default.dim("No features yet."));
|
|
76473
76811
|
return;
|
|
76474
76812
|
}
|
|
76475
|
-
const { readdirSync:
|
|
76476
|
-
const entries =
|
|
76813
|
+
const { readdirSync: readdirSync8 } = await import("fs");
|
|
76814
|
+
const entries = readdirSync8(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
76477
76815
|
if (entries.length === 0) {
|
|
76478
76816
|
console.log(source_default.dim("No features yet."));
|
|
76479
76817
|
return;
|
|
@@ -76493,7 +76831,13 @@ Features:
|
|
|
76493
76831
|
}
|
|
76494
76832
|
console.log();
|
|
76495
76833
|
});
|
|
76496
|
-
program2.command("plan
|
|
76834
|
+
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 one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
|
|
76835
|
+
if (description) {
|
|
76836
|
+
console.error(source_default.red(`Error: Positional args removed in plan v2.
|
|
76837
|
+
|
|
76838
|
+
Use: nax plan -f <feature> --from <spec>`));
|
|
76839
|
+
process.exit(1);
|
|
76840
|
+
}
|
|
76497
76841
|
let workdir;
|
|
76498
76842
|
try {
|
|
76499
76843
|
workdir = validateDirectory(options.dir);
|
|
@@ -76508,21 +76852,26 @@ program2.command("plan <description>").description("Interactive planning via age
|
|
|
76508
76852
|
}
|
|
76509
76853
|
const config2 = await loadConfig(workdir);
|
|
76510
76854
|
try {
|
|
76511
|
-
const
|
|
76512
|
-
|
|
76513
|
-
|
|
76855
|
+
const prdPath = await planCommand(workdir, config2, {
|
|
76856
|
+
from: options.from,
|
|
76857
|
+
feature: options.feature,
|
|
76858
|
+
auto: options.auto || options.oneShot,
|
|
76859
|
+
branch: options.branch
|
|
76514
76860
|
});
|
|
76515
76861
|
console.log(source_default.green(`
|
|
76516
|
-
|
|
76517
|
-
console.log(source_default.dim(`
|
|
76862
|
+
[OK] PRD generated`));
|
|
76863
|
+
console.log(source_default.dim(` PRD: ${prdPath}`));
|
|
76518
76864
|
console.log(source_default.dim(`
|
|
76519
|
-
Next: nax
|
|
76865
|
+
Next: nax run -f ${options.feature}`));
|
|
76520
76866
|
} catch (err) {
|
|
76521
76867
|
console.error(source_default.red(`Error: ${err.message}`));
|
|
76522
76868
|
process.exit(1);
|
|
76523
76869
|
}
|
|
76524
76870
|
});
|
|
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) => {
|
|
76871
|
+
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) => {
|
|
76872
|
+
const deprecationMsg = "\u26A0\uFE0F 'nax analyze' is deprecated. Use 'nax plan -f <feature> --from <spec> --auto' instead.";
|
|
76873
|
+
process.stderr.write(`${source_default.yellow(deprecationMsg)}
|
|
76874
|
+
`);
|
|
76526
76875
|
let workdir;
|
|
76527
76876
|
try {
|
|
76528
76877
|
workdir = validateDirectory(options.dir);
|