@nathapp/nax 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -671,6 +671,7 @@ User stories are defined in `nax/features/<name>/prd.json`:
671
671
 
672
672
  ---
673
673
 
674
+
674
675
  ## License
675
676
 
676
677
  MIT
package/bin/nax.ts CHANGED
@@ -78,6 +78,50 @@ const program = new Command();
78
78
 
79
79
  program.name("nax").description("AI Coding Agent Orchestrator — loops until done").version(NAX_VERSION);
80
80
 
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Helpers
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Prompt user for a yes/no confirmation via stdin.
87
+ * In tests or non-TTY environments, defaults to true.
88
+ *
89
+ * @param question - Confirmation question to display
90
+ * @returns true if user answers Y/y, false if N/n, true by default for non-TTY
91
+ */
92
+ async function promptForConfirmation(question: string): Promise<boolean> {
93
+ // In non-TTY mode (tests, pipes), default to true
94
+ if (!process.stdin.isTTY) {
95
+ return true;
96
+ }
97
+
98
+ return new Promise((resolve) => {
99
+ process.stdout.write(chalk.bold(`${question} [Y/n] `));
100
+
101
+ process.stdin.setRawMode(true);
102
+ process.stdin.resume();
103
+ process.stdin.setEncoding("utf8");
104
+
105
+ const handler = (char: string) => {
106
+ process.stdin.setRawMode(false);
107
+ process.stdin.pause();
108
+ process.stdin.removeListener("data", handler);
109
+
110
+ const answer = char.toLowerCase();
111
+ process.stdout.write("\n");
112
+
113
+ if (answer === "n") {
114
+ resolve(false);
115
+ } else {
116
+ // Default to yes for Y, Enter, or any other input
117
+ resolve(true);
118
+ }
119
+ };
120
+
121
+ process.stdin.on("data", handler);
122
+ });
123
+ }
124
+
81
125
  // ── init ─────────────────────────────────────────────
82
126
  program
83
127
  .command("init")
@@ -231,6 +275,9 @@ program
231
275
  .option("--no-context", "Disable context builder (skip file context in prompts)")
232
276
  .option("--no-batch", "Disable story batching (execute all stories individually)")
233
277
  .option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)")
278
+ .option("--plan", "Run plan phase first before execution", false)
279
+ .option("--from <spec-path>", "Path to spec file (required when --plan is used)")
280
+ .option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false)
234
281
  .option("--headless", "Force headless mode (disable TUI, use pipe mode)", false)
235
282
  .option("--verbose", "Enable verbose logging (debug level)", false)
236
283
  .option("--quiet", "Quiet mode (warnings and errors only)", false)
@@ -248,6 +295,18 @@ program
248
295
  process.exit(1);
249
296
  }
250
297
 
298
+ // Validate --plan and --from flags (AC-8: --plan without --from is error)
299
+ if (options.plan && !options.from) {
300
+ console.error(chalk.red("Error: --plan requires --from <spec-path>"));
301
+ process.exit(1);
302
+ }
303
+
304
+ // Validate --from path exists (AC-7: --from without existing file throws error)
305
+ if (options.from && !existsSync(options.from)) {
306
+ console.error(chalk.red(`Error: File not found: ${options.from} (required with --plan)`));
307
+ process.exit(1);
308
+ }
309
+
251
310
  // Determine log level from flags or env var (env var takes precedence)
252
311
  let logLevel: LogLevel = "info"; // default
253
312
  const envLevel = process.env.NAX_LOG_LEVEL?.toLowerCase();
@@ -282,6 +341,51 @@ program
282
341
  const featureDir = join(naxDir, "features", options.feature);
283
342
  const prdPath = join(featureDir, "prd.json");
284
343
 
344
+ // Run plan phase if --plan flag is set (AC-4: runs plan then execute)
345
+ if (options.plan && options.from) {
346
+ try {
347
+ console.log(chalk.dim(" [Planning phase: generating PRD from spec]"));
348
+ const generatedPrdPath = await planCommand(workdir, config, {
349
+ from: options.from,
350
+ feature: options.feature,
351
+ auto: options.oneShot ?? false, // interactive by default; --one-shot skips Q&A
352
+ branch: undefined,
353
+ });
354
+
355
+ // Load the generated PRD to display confirmation gate
356
+ const generatedPrd = await loadPRD(generatedPrdPath);
357
+
358
+ // Display story breakdown (AC-5: confirmation gate displays story breakdown)
359
+ console.log(chalk.bold("\n── Planning Summary ──────────────────────────────"));
360
+ console.log(chalk.dim(`Feature: ${generatedPrd.feature}`));
361
+ console.log(chalk.dim(`Stories: ${generatedPrd.userStories.length}`));
362
+ console.log();
363
+
364
+ for (const story of generatedPrd.userStories) {
365
+ const complexity = story.routing?.complexity || "unknown";
366
+ console.log(chalk.dim(` ${story.id}: ${story.title} [${complexity}]`));
367
+ }
368
+ console.log();
369
+
370
+ // Show confirmation gate unless --headless (AC-5, AC-6)
371
+ if (!options.headless) {
372
+ // Prompt for user confirmation
373
+ const confirmationResult = await promptForConfirmation("Proceed with execution?");
374
+ if (!confirmationResult) {
375
+ console.log(chalk.yellow("Execution cancelled."));
376
+ process.exit(0);
377
+ }
378
+ }
379
+
380
+ // Continue with normal run using the generated prd.json
381
+ // (prdPath already points to the generated file)
382
+ } catch (err) {
383
+ console.error(chalk.red(`Error during planning: ${(err as Error).message}`));
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ // Check if prd.json exists (skip if --plan already generated it)
285
389
  if (!existsSync(prdPath)) {
286
390
  console.error(chalk.red(`Feature "${options.feature}" not found or missing prd.json`));
287
391
  process.exit(1);
@@ -463,7 +567,7 @@ features
463
567
  console.log(chalk.dim(" ├── plan.md"));
464
568
  console.log(chalk.dim(" ├── tasks.md"));
465
569
  console.log(chalk.dim(" └── progress.txt"));
466
- console.log(chalk.dim(`\nNext: Edit spec.md and tasks.md, then: nax analyze --feature ${name}`));
570
+ console.log(chalk.dim(`\nNext: Edit spec.md and tasks.md, then: nax plan -f ${name} --from spec.md --auto`));
467
571
  });
468
572
 
469
573
  features
@@ -518,11 +622,22 @@ features
518
622
 
519
623
  // ── plan ─────────────────────────────────────────────
520
624
  program
521
- .command("plan <description>")
522
- .description("Interactive planning via agent plan mode")
523
- .option("--from <file>", "Non-interactive mode: read from input file")
625
+ .command("plan [description]")
626
+ .description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')")
627
+ .requiredOption("--from <spec-path>", "Path to spec file (required)")
628
+ .requiredOption("-f, --feature <name>", "Feature name (required)")
629
+ .option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false)
630
+ .option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false)
631
+ .option("-b, --branch <branch>", "Override default branch name")
524
632
  .option("-d, --dir <path>", "Project directory", process.cwd())
525
- .action(async (description: string, options) => {
633
+ .action(async (description, options) => {
634
+ // AC-3: Detect and reject old positional argument form
635
+ if (description) {
636
+ console.error(
637
+ chalk.red("Error: Positional args removed in plan v2.\n\nUse: nax plan -f <feature> --from <spec>"),
638
+ );
639
+ process.exit(1);
640
+ }
526
641
  // Validate directory path
527
642
  let workdir: string;
528
643
  try {
@@ -542,14 +657,16 @@ program
542
657
  const config = await loadConfig(workdir);
543
658
 
544
659
  try {
545
- const specPath = await planCommand(description, workdir, config, {
546
- interactive: !options.from,
660
+ const prdPath = await planCommand(workdir, config, {
547
661
  from: options.from,
662
+ feature: options.feature,
663
+ auto: options.auto || options.oneShot, // --auto and --one-shot are aliases
664
+ branch: options.branch,
548
665
  });
549
666
 
550
- console.log(chalk.green("\n Planning complete"));
551
- console.log(chalk.dim(` Spec: ${specPath}`));
552
- console.log(chalk.dim("\nNext: nax analyze -f <feature-name>"));
667
+ console.log(chalk.green("\n[OK] PRD generated"));
668
+ console.log(chalk.dim(` PRD: ${prdPath}`));
669
+ console.log(chalk.dim(`\nNext: nax run -f ${options.feature}`));
553
670
  } catch (err) {
554
671
  console.error(chalk.red(`Error: ${(err as Error).message}`));
555
672
  process.exit(1);
@@ -559,13 +676,17 @@ program
559
676
  // ── analyze ──────────────────────────────────────────
560
677
  program
561
678
  .command("analyze")
562
- .description("Parse spec.md into prd.json via agent decompose")
679
+ .description("(deprecated) Parse spec.md into prd.json via agent decompose — use 'nax plan' instead")
563
680
  .requiredOption("-f, --feature <name>", "Feature name")
564
681
  .option("-b, --branch <name>", "Branch name", "feat/<feature>")
565
682
  .option("--from <path>", "Explicit spec path (overrides default spec.md)")
566
683
  .option("--reclassify", "Re-classify existing prd.json without decompose", false)
567
684
  .option("-d, --dir <path>", "Project directory", process.cwd())
568
685
  .action(async (options) => {
686
+ // AC-1: Print deprecation warning to stderr
687
+ const deprecationMsg = "⚠️ 'nax analyze' is deprecated. Use 'nax plan -f <feature> --from <spec> --auto' instead.";
688
+ process.stderr.write(`${chalk.yellow(deprecationMsg)}\n`);
689
+
569
690
  // Validate directory path
570
691
  let workdir: string;
571
692
  try {