@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/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 Bun.sleep(backoffMs);
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 Bun.sleep(backoffMs);
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 cmdStr = `acpx --model ${model} ${this.name}`;
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 session = null;
19444
- try {
19445
- session = await client.createSession({ agentName: this.name, permissionMode });
19446
- const response = await session.prompt(prompt);
19447
- if (response.stopReason === "error") {
19448
- throw new CompleteError("complete() failed: stop reason is error");
19449
- }
19450
- const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
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
- if (!text) {
19453
- throw new CompleteError("complete() returned empty output");
19454
- }
19455
- return text;
19456
- } finally {
19457
- if (session) {
19458
- await session.close().catch(() => {});
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.41.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("0db1c5f"))
21875
- return "0db1c5f";
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 Bun.sleep(backoffMs);
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 (_deps2.callLlm) {
23042
- const decision2 = await _deps2.callLlm(request);
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 = _deps2.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, _deps2;
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
- _deps2 = {
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 (actualFailures.length === 0 && exitCode !== 0) {
23533
- logger.warn("acceptance", "Tests failed but no specific AC failures detected", {
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
- return { action: "continue" };
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 _deps3.getUncommittedFiles(workdir);
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, _deps3;
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
- _deps3 = {
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 _deps4.autoDetectContextFiles({
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 _deps4;
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
- _deps4 = {
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 Bun.sleep(2000);
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 QUESTION_PATTERNS2 = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
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) => QUESTION_PATTERNS2.some((p) => p.test(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 = _deps6.spawn([agent, "--version"], {
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 _deps6;
29868
+ var _deps7;
29776
29869
  var init_checks_cli = __esm(() => {
29777
- _deps6 = {
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 = Array.from(new Bun.Glob("*").scanSync({
65040
- cwd: dir,
65041
- onlyFiles: false
65042
- }));
65115
+ const dirEntries = readdirSync(dir, { withFileTypes: true });
65043
65116
  dirEntries.sort((a, b) => {
65044
- const aIsDir = !a.includes(".");
65045
- const bIsDir = !b.includes(".");
65046
- if (aIsDir && !bIsDir)
65117
+ if (a.isDirectory() && !b.isDirectory())
65047
65118
  return -1;
65048
- if (!aIsDir && bIsDir)
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 entry = dirEntries[i];
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 stat = await Bun.file(fullPath).stat();
65059
- const isDir = stat.isDirectory();
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(fullPath, maxDepth, currentDepth + 1, prefix + childPrefix);
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
- init_claude();
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
- ## Technical Notes
65510
- Architecture hints, constraints, dependencies.
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
- ## Out of Scope
65513
- What this does NOT include.
65514
- `;
65515
- async function planCommand(prompt, workdir, config2, options = {}) {
65516
- const interactive = options.interactive !== false;
65517
- const ngentDir = join10(workdir, "nax");
65518
- const outputPath = join10(ngentDir, config2.plan.outputPath);
65519
- if (!existsSync9(ngentDir)) {
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.info("cli", "Scanning codebase...");
65524
- const scan = await scanCodebase(workdir);
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 modelTier = config2.plan.model;
65527
- const modelEntry = config2.models[modelTier];
65528
- const modelDef = resolveModel(modelEntry);
65529
- const fullPrompt = buildPlanPrompt(prompt, SPEC_TEMPLATE);
65530
- const planOptions = {
65531
- prompt: fullPrompt,
65532
- workdir,
65533
- interactive,
65534
- codebaseContext,
65535
- inputFile: options.from,
65536
- modelTier,
65537
- modelDef,
65538
- config: config2,
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
- if (!result.specContent) {
65557
- throw new Error("Agent did not produce specification content");
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
- logger.info("cli", "\u2713 Specification written to output", { outputPath });
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 buildPlanPrompt(userPrompt, template) {
65591
- return `You are helping plan a new feature for this codebase.
65824
+ function buildPlanningPrompt(specContent, codebaseContext) {
65825
+ return `You are a senior software architect generating a product requirements document (PRD) as JSON.
65592
65826
 
65593
- Task: ${userPrompt}
65827
+ ## Spec
65594
65828
 
65595
- Please gather requirements and produce a structured specification following this template:
65829
+ ${specContent}
65596
65830
 
65597
- ${template}
65831
+ ## Codebase Context
65598
65832
 
65599
- Ask clarifying questions as needed to ensure the spec is complete and unambiguous.
65600
- Focus on understanding:
65601
- - The problem being solved
65602
- - Specific requirements and constraints
65603
- - Acceptance criteria for success
65604
- - Technical approach and architecture
65605
- - What is explicitly out of scope
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
- When done, output the complete specification in markdown format.`;
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 readdirSync2 } from "fs";
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) ? readdirSync(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
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 = readdirSync2(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
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 = readdirSync2(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
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 readdirSync3 } from "fs";
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 = readdirSync3(runsDir).filter((f) => f.endsWith(".jsonl"));
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 readdirSync4 } from "fs";
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 = readdirSync4(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
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 readdirSync6 } from "fs";
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 readdirSync5 } from "fs";
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 _deps5 = {
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 = _deps5.getRunsDir();
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 = readdirSync5(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
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 = readdirSync5(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
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 = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
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 analyze -f ${featureName}`));
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 _deps7 = {
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 = _deps7.getRunsDir();
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 analyze --feature ${name}`));
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: readdirSync7 } = await import("fs");
76476
- const entries = readdirSync7(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
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 <description>").description("Interactive planning via agent plan mode").option("--from <file>", "Non-interactive mode: read from input file").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
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 specPath = await planCommand(description, workdir, config2, {
76512
- interactive: !options.from,
76513
- from: options.from
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
- \u2705 Planning complete`));
76517
- console.log(source_default.dim(` Spec: ${specPath}`));
76862
+ [OK] PRD generated`));
76863
+ console.log(source_default.dim(` PRD: ${prdPath}`));
76518
76864
  console.log(source_default.dim(`
76519
- Next: nax analyze -f <feature-name>`));
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);