@nathapp/nax 0.41.0 → 0.42.0

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