@nathapp/nax 0.36.2 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -317,6 +317,25 @@ If `testScoped` is not configured, nax falls back to a heuristic that replaces t
317
317
 
318
318
  ---
319
319
 
320
+ ## Customization
321
+
322
+ ### Prompt Customization
323
+
324
+ Customize the instructions sent to each agent role for your project's specific needs. Override prompts to enforce coding style, domain knowledge, or architectural constraints.
325
+
326
+ **Quick start:**
327
+
328
+ ```bash
329
+ nax prompts --init # Create default templates
330
+ # Edit nax/templates/*.md
331
+ nax prompts --export test-writer # Preview a role's prompt
332
+ nax run -f my-feature # Uses your custom prompts
333
+ ```
334
+
335
+ **Full guide:** See [Prompt Customization Guide](docs/prompt-customization.md) for detailed instructions, role reference, and best practices.
336
+
337
+ ---
338
+
320
339
  ## Test Strategies
321
340
 
322
341
  nax selects a test strategy per story based on complexity and tags:
package/bin/nax.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  displayFeatureStatus,
52
52
  displayLastRunMetrics,
53
53
  displayModelEfficiency,
54
+ exportPromptCommand,
54
55
  planCommand,
55
56
  pluginsListCommand,
56
57
  promptsCommand,
@@ -191,13 +192,31 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
191
192
  `,
192
193
  );
193
194
 
195
+ // Initialize prompt templates (final step, don't auto-wire config)
196
+ try {
197
+ await promptsInitCommand({
198
+ workdir,
199
+ force: options.force,
200
+ autoWireConfig: false,
201
+ });
202
+ } catch (err) {
203
+ console.error(chalk.red(`Failed to initialize templates: ${(err as Error).message}`));
204
+ process.exit(1);
205
+ }
206
+
194
207
  console.log(chalk.green("✅ Initialized nax"));
195
208
  console.log(chalk.dim(` ${naxDir}/`));
196
209
  console.log(chalk.dim(" ├── config.json"));
197
210
  console.log(chalk.dim(" ├── context.md"));
198
211
  console.log(chalk.dim(" ├── hooks.json"));
199
212
  console.log(chalk.dim(" ├── features/"));
200
- console.log(chalk.dim(" └── hooks/"));
213
+ console.log(chalk.dim(" ├── hooks/"));
214
+ console.log(chalk.dim(" └── templates/"));
215
+ console.log(chalk.dim(" ├── test-writer.md"));
216
+ console.log(chalk.dim(" ├── implementer.md"));
217
+ console.log(chalk.dim(" ├── verifier.md"));
218
+ console.log(chalk.dim(" ├── single-session.md"));
219
+ console.log(chalk.dim(" └── tdd-simple.md"));
201
220
  console.log(chalk.dim("\nNext: nax features create <name>"));
202
221
  });
203
222
 
@@ -860,11 +879,12 @@ program
860
879
  program
861
880
  .command("prompts")
862
881
  .description("Assemble or initialize prompts")
863
- .option("-f, --feature <name>", "Feature name (required unless using --init)")
882
+ .option("-f, --feature <name>", "Feature name (required unless using --init or --export)")
864
883
  .option("--init", "Initialize default prompt templates", false)
884
+ .option("--export <role>", "Export default prompt for a role to stdout or --out file")
865
885
  .option("--force", "Overwrite existing template files", false)
866
886
  .option("--story <id>", "Filter to a single story ID (e.g., US-003)")
867
- .option("--out <dir>", "Output directory for prompt files (default: stdout)")
887
+ .option("--out <path>", "Output file path for --export, or directory for regular prompts (default: stdout)")
868
888
  .option("-d, --dir <path>", "Project directory", process.cwd())
869
889
  .action(async (options) => {
870
890
  // Validate directory path
@@ -890,9 +910,23 @@ program
890
910
  return;
891
911
  }
892
912
 
913
+ // Handle --export command
914
+ if (options.export) {
915
+ try {
916
+ await exportPromptCommand({
917
+ role: options.export,
918
+ out: options.out,
919
+ });
920
+ } catch (err) {
921
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
922
+ process.exit(1);
923
+ }
924
+ return;
925
+ }
926
+
893
927
  // Handle regular prompts command (requires --feature)
894
928
  if (!options.feature) {
895
- console.error(chalk.red("Error: --feature is required (unless using --init)"));
929
+ console.error(chalk.red("Error: --feature is required (unless using --init or --export)"));
896
930
  process.exit(1);
897
931
  }
898
932
 
package/dist/nax.js CHANGED
@@ -18047,7 +18047,9 @@ var init_schemas3 = __esm(() => {
18047
18047
  storySizeGate: StorySizeGateConfigSchema
18048
18048
  });
18049
18049
  PromptsConfigSchema = exports_external.object({
18050
- overrides: exports_external.record(exports_external.enum(["test-writer", "implementer", "verifier", "single-session"]), exports_external.string().min(1, "Override path must be non-empty")).optional()
18050
+ overrides: exports_external.record(exports_external.string().refine((key) => ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
18051
+ message: "Role must be one of: test-writer, implementer, verifier, single-session, tdd-simple"
18052
+ }), exports_external.string().min(1, "Override path must be non-empty")).optional()
18051
18053
  });
18052
18054
  DecomposeConfigSchema = exports_external.object({
18053
18055
  trigger: exports_external.enum(["auto", "confirm", "disabled"]).default("auto"),
@@ -20673,7 +20675,7 @@ var package_default;
20673
20675
  var init_package = __esm(() => {
20674
20676
  package_default = {
20675
20677
  name: "@nathapp/nax",
20676
- version: "0.36.2",
20678
+ version: "0.38.0",
20677
20679
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20678
20680
  type: "module",
20679
20681
  bin: {
@@ -20690,6 +20692,9 @@ var init_package = __esm(() => {
20690
20692
  "test:unit": "bun test ./test/unit/ --timeout=60000",
20691
20693
  "test:integration": "bun test ./test/integration/ --timeout=60000",
20692
20694
  "test:ui": "bun test ./test/ui/ --timeout=60000",
20695
+ "check-test-overlap": "bun run scripts/check-test-overlap.ts",
20696
+ "check-dead-tests": "bun run scripts/check-dead-tests.ts",
20697
+ "check:test-sizes": "bun run scripts/check-test-sizes.ts",
20693
20698
  prepublishOnly: "bun run build"
20694
20699
  },
20695
20700
  dependencies: {
@@ -20734,8 +20739,8 @@ var init_version = __esm(() => {
20734
20739
  NAX_VERSION = package_default.version;
20735
20740
  NAX_COMMIT = (() => {
20736
20741
  try {
20737
- if (/^[0-9a-f]{6,10}$/.test("eb77e7d"))
20738
- return "eb77e7d";
20742
+ if (/^[0-9a-f]{6,10}$/.test("cdad904"))
20743
+ return "cdad904";
20739
20744
  } catch {}
20740
20745
  try {
20741
20746
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22681,18 +22686,24 @@ var init_runner2 = __esm(() => {
22681
22686
 
22682
22687
  // src/review/orchestrator.ts
22683
22688
  var {spawn: spawn2 } = globalThis.Bun;
22684
- async function getChangedFiles(workdir) {
22689
+ async function getChangedFiles(workdir, baseRef) {
22685
22690
  try {
22686
- const [stagedProc, unstagedProc] = [
22687
- spawn2({ cmd: ["git", "diff", "--name-only", "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22688
- spawn2({ cmd: ["git", "diff", "--name-only"], cwd: workdir, stdout: "pipe", stderr: "pipe" })
22691
+ const diffArgs = ["diff", "--name-only"];
22692
+ const [stagedProc, unstagedProc, baseProc] = [
22693
+ spawn2({ cmd: ["git", ...diffArgs, "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22694
+ spawn2({ cmd: ["git", ...diffArgs], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22695
+ baseRef ? spawn2({ cmd: ["git", ...diffArgs, `${baseRef}...HEAD`], cwd: workdir, stdout: "pipe", stderr: "pipe" }) : null
22689
22696
  ];
22690
- await Promise.all([stagedProc.exited, unstagedProc.exited]);
22691
- const staged = (await new Response(stagedProc.stdout).text()).trim().split(`
22692
- `).filter(Boolean);
22693
- const unstaged = (await new Response(unstagedProc.stdout).text()).trim().split(`
22694
- `).filter(Boolean);
22695
- return Array.from(new Set([...staged, ...unstaged]));
22697
+ await Promise.all([stagedProc.exited, unstagedProc.exited, baseProc?.exited]);
22698
+ const [staged, unstaged, based] = await Promise.all([
22699
+ new Response(stagedProc.stdout).text().then((t) => t.trim().split(`
22700
+ `).filter(Boolean)),
22701
+ new Response(unstagedProc.stdout).text().then((t) => t.trim().split(`
22702
+ `).filter(Boolean)),
22703
+ baseProc ? new Response(baseProc.stdout).text().then((t) => t.trim().split(`
22704
+ `).filter(Boolean)) : Promise.resolve([])
22705
+ ]);
22706
+ return Array.from(new Set([...staged, ...unstaged, ...based]));
22696
22707
  } catch {
22697
22708
  return [];
22698
22709
  }
@@ -22708,7 +22719,8 @@ class ReviewOrchestrator {
22708
22719
  if (plugins) {
22709
22720
  const reviewers = plugins.getReviewers();
22710
22721
  if (reviewers.length > 0) {
22711
- const changedFiles = await getChangedFiles(workdir);
22722
+ const baseRef = executionConfig?.storyGitRef;
22723
+ const changedFiles = await getChangedFiles(workdir, baseRef);
22712
22724
  const pluginResults = [];
22713
22725
  for (const reviewer of reviewers) {
22714
22726
  logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
@@ -64739,7 +64751,8 @@ var TEMPLATE_ROLES = [
64739
64751
  { file: "test-writer.md", role: "test-writer" },
64740
64752
  { file: "implementer.md", role: "implementer", variant: "standard" },
64741
64753
  { file: "verifier.md", role: "verifier" },
64742
- { file: "single-session.md", role: "single-session" }
64754
+ { file: "single-session.md", role: "single-session" },
64755
+ { file: "tdd-simple.md", role: "tdd-simple" }
64743
64756
  ];
64744
64757
  var TEMPLATE_HEADER = `<!--
64745
64758
  This file controls the role-body section of the nax prompt for this role.
@@ -64756,7 +64769,7 @@ var TEMPLATE_HEADER = `<!--
64756
64769
 
64757
64770
  `;
64758
64771
  async function promptsInitCommand(options) {
64759
- const { workdir, force = false } = options;
64772
+ const { workdir, force = false, autoWireConfig = true } = options;
64760
64773
  const templatesDir = join18(workdir, "nax", "templates");
64761
64774
  mkdirSync3(templatesDir, { recursive: true });
64762
64775
  const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync15(join18(templatesDir, f)));
@@ -64777,7 +64790,9 @@ async function promptsInitCommand(options) {
64777
64790
  for (const filePath of written) {
64778
64791
  console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
64779
64792
  }
64780
- await autoWirePromptsConfig(workdir);
64793
+ if (autoWireConfig) {
64794
+ await autoWirePromptsConfig(workdir);
64795
+ }
64781
64796
  return written;
64782
64797
  }
64783
64798
  async function autoWirePromptsConfig(workdir) {
@@ -64789,7 +64804,8 @@ async function autoWirePromptsConfig(workdir) {
64789
64804
  "test-writer": "nax/templates/test-writer.md",
64790
64805
  implementer: "nax/templates/implementer.md",
64791
64806
  verifier: "nax/templates/verifier.md",
64792
- "single-session": "nax/templates/single-session.md"
64807
+ "single-session": "nax/templates/single-session.md",
64808
+ "tdd-simple": "nax/templates/tdd-simple.md"
64793
64809
  }
64794
64810
  }
64795
64811
  }, null, 2);
@@ -64810,7 +64826,8 @@ ${exampleConfig}`);
64810
64826
  "test-writer": "nax/templates/test-writer.md",
64811
64827
  implementer: "nax/templates/implementer.md",
64812
64828
  verifier: "nax/templates/verifier.md",
64813
- "single-session": "nax/templates/single-session.md"
64829
+ "single-session": "nax/templates/single-session.md",
64830
+ "tdd-simple": "nax/templates/tdd-simple.md"
64814
64831
  };
64815
64832
  if (!config2.prompts) {
64816
64833
  config2.prompts = {};
@@ -64843,6 +64860,33 @@ function formatConfigJson(config2) {
64843
64860
  return lines.join(`
64844
64861
  `);
64845
64862
  }
64863
+ var VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
64864
+ async function exportPromptCommand(options) {
64865
+ const { role, out } = options;
64866
+ if (!VALID_EXPORT_ROLES.includes(role)) {
64867
+ console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
64868
+ process.exit(1);
64869
+ }
64870
+ const stubStory = {
64871
+ id: "EXAMPLE",
64872
+ title: "Example story",
64873
+ description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
64874
+ acceptanceCriteria: ["AC-1: Example criterion"],
64875
+ tags: [],
64876
+ dependencies: [],
64877
+ status: "pending",
64878
+ passes: false,
64879
+ escalations: [],
64880
+ attempts: 0
64881
+ };
64882
+ const prompt = await PromptBuilder.for(role).story(stubStory).build();
64883
+ if (out) {
64884
+ await Bun.write(out, prompt);
64885
+ console.log(`[OK] Exported prompt for "${role}" to ${out}`);
64886
+ } else {
64887
+ console.log(prompt);
64888
+ }
64889
+ }
64846
64890
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
64847
64891
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
64848
64892
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
@@ -74416,13 +74460,29 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
74416
74460
 
74417
74461
  **Note:** Customize this file to match your project's specific needs.
74418
74462
  `);
74463
+ try {
74464
+ await promptsInitCommand({
74465
+ workdir,
74466
+ force: options.force,
74467
+ autoWireConfig: false
74468
+ });
74469
+ } catch (err) {
74470
+ console.error(source_default.red(`Failed to initialize templates: ${err.message}`));
74471
+ process.exit(1);
74472
+ }
74419
74473
  console.log(source_default.green("\u2705 Initialized nax"));
74420
74474
  console.log(source_default.dim(` ${naxDir}/`));
74421
74475
  console.log(source_default.dim(" \u251C\u2500\u2500 config.json"));
74422
74476
  console.log(source_default.dim(" \u251C\u2500\u2500 context.md"));
74423
74477
  console.log(source_default.dim(" \u251C\u2500\u2500 hooks.json"));
74424
74478
  console.log(source_default.dim(" \u251C\u2500\u2500 features/"));
74425
- console.log(source_default.dim(" \u2514\u2500\u2500 hooks/"));
74479
+ console.log(source_default.dim(" \u251C\u2500\u2500 hooks/"));
74480
+ console.log(source_default.dim(" \u2514\u2500\u2500 templates/"));
74481
+ console.log(source_default.dim(" \u251C\u2500\u2500 test-writer.md"));
74482
+ console.log(source_default.dim(" \u251C\u2500\u2500 implementer.md"));
74483
+ console.log(source_default.dim(" \u251C\u2500\u2500 verifier.md"));
74484
+ console.log(source_default.dim(" \u251C\u2500\u2500 single-session.md"));
74485
+ console.log(source_default.dim(" \u2514\u2500\u2500 tdd-simple.md"));
74426
74486
  console.log(source_default.dim(`
74427
74487
  Next: nax features create <name>`));
74428
74488
  });
@@ -74883,7 +74943,7 @@ program2.command("accept").description("Override failed acceptance criteria").re
74883
74943
  process.exit(1);
74884
74944
  }
74885
74945
  });
74886
- program2.command("prompts").description("Assemble or initialize prompts").option("-f, --feature <name>", "Feature name (required unless using --init)").option("--init", "Initialize default prompt templates", false).option("--force", "Overwrite existing template files", false).option("--story <id>", "Filter to a single story ID (e.g., US-003)").option("--out <dir>", "Output directory for prompt files (default: stdout)").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
74946
+ program2.command("prompts").description("Assemble or initialize prompts").option("-f, --feature <name>", "Feature name (required unless using --init or --export)").option("--init", "Initialize default prompt templates", false).option("--export <role>", "Export default prompt for a role to stdout or --out file").option("--force", "Overwrite existing template files", false).option("--story <id>", "Filter to a single story ID (e.g., US-003)").option("--out <path>", "Output file path for --export, or directory for regular prompts (default: stdout)").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
74887
74947
  let workdir;
74888
74948
  try {
74889
74949
  workdir = validateDirectory(options.dir);
@@ -74903,8 +74963,20 @@ program2.command("prompts").description("Assemble or initialize prompts").option
74903
74963
  }
74904
74964
  return;
74905
74965
  }
74966
+ if (options.export) {
74967
+ try {
74968
+ await exportPromptCommand({
74969
+ role: options.export,
74970
+ out: options.out
74971
+ });
74972
+ } catch (err) {
74973
+ console.error(source_default.red(`Error: ${err.message}`));
74974
+ process.exit(1);
74975
+ }
74976
+ return;
74977
+ }
74906
74978
  if (!options.feature) {
74907
- console.error(source_default.red("Error: --feature is required (unless using --init)"));
74979
+ console.error(source_default.red("Error: --feature is required (unless using --init or --export)"));
74908
74980
  process.exit(1);
74909
74981
  }
74910
74982
  const config2 = await loadConfig(workdir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.36.2",
3
+ "version": "0.38.0",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,9 @@
17
17
  "test:unit": "bun test ./test/unit/ --timeout=60000",
18
18
  "test:integration": "bun test ./test/integration/ --timeout=60000",
19
19
  "test:ui": "bun test ./test/ui/ --timeout=60000",
20
+ "check-test-overlap": "bun run scripts/check-test-overlap.ts",
21
+ "check-dead-tests": "bun run scripts/check-dead-tests.ts",
22
+ "check:test-sizes": "bun run scripts/check-test-sizes.ts",
20
23
  "prepublishOnly": "bun run build"
21
24
  },
22
25
  "dependencies": {
package/src/cli/index.ts CHANGED
@@ -21,8 +21,10 @@ export {
21
21
  export {
22
22
  promptsCommand,
23
23
  promptsInitCommand,
24
+ exportPromptCommand,
24
25
  type PromptsCommandOptions,
25
26
  type PromptsInitCommandOptions,
27
+ type ExportPromptCommandOptions,
26
28
  } from "./prompts";
27
29
  export { initCommand, type InitOptions } from "./init";
28
30
  export { pluginsListCommand } from "./plugins";
package/src/cli/init.ts CHANGED
@@ -10,6 +10,7 @@ import { join } from "node:path";
10
10
  import { globalConfigDir, projectConfigDir } from "../config/paths";
11
11
  import { DEFAULT_CONFIG } from "../config/schema";
12
12
  import { getLogger } from "../logger";
13
+ import { promptsInitCommand } from "./prompts";
13
14
 
14
15
  /** Init command options */
15
16
  export interface InitOptions {
@@ -133,7 +134,7 @@ async function initGlobal(): Promise<void> {
133
134
  /**
134
135
  * Initialize project nax directory (nax/)
135
136
  */
136
- async function initProject(projectRoot: string): Promise<void> {
137
+ export async function initProject(projectRoot: string): Promise<void> {
137
138
  const logger = getLogger();
138
139
  const projectDir = projectConfigDir(projectRoot);
139
140
 
@@ -173,6 +174,11 @@ async function initProject(projectRoot: string): Promise<void> {
173
174
  // Update .gitignore to include nax-specific entries
174
175
  await updateGitignore(projectRoot);
175
176
 
177
+ // Create prompt templates (final step)
178
+ // Pass autoWireConfig: false to prevent auto-wiring prompts.overrides
179
+ // Templates are created but not activated until user explicitly configures them
180
+ await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
181
+
176
182
  logger.info("init", "Project config initialized successfully", { path: projectDir });
177
183
  }
178
184
 
@@ -10,7 +10,6 @@
10
10
  import { existsSync, mkdirSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import type { NaxConfig } from "../config";
13
- import type { BuiltContext } from "../context/types";
14
13
  import { getLogger } from "../logger";
15
14
  import { runPipeline } from "../pipeline";
16
15
  import type { PipelineContext } from "../pipeline";
@@ -246,6 +245,8 @@ export interface PromptsInitCommandOptions {
246
245
  workdir: string;
247
246
  /** Overwrite existing files if true */
248
247
  force?: boolean;
248
+ /** Auto-wire prompts.overrides in nax.config.json (default: true) */
249
+ autoWireConfig?: boolean;
249
250
  }
250
251
 
251
252
  const TEMPLATE_ROLES = [
@@ -253,6 +254,7 @@ const TEMPLATE_ROLES = [
253
254
  { file: "implementer.md", role: "implementer" as const, variant: "standard" as const },
254
255
  { file: "verifier.md", role: "verifier" as const },
255
256
  { file: "single-session.md", role: "single-session" as const },
257
+ { file: "tdd-simple.md", role: "tdd-simple" as const },
256
258
  ] as const;
257
259
 
258
260
  const TEMPLATE_HEADER = `<!--
@@ -273,19 +275,17 @@ const TEMPLATE_HEADER = `<!--
273
275
  /**
274
276
  * Execute the `nax prompts --init` command.
275
277
  *
276
- * Creates nax/templates/ and writes 4 default role-body template files
277
- * (test-writer, implementer, verifier, single-session).
278
+ * Creates nax/templates/ and writes 5 default role-body template files
279
+ * (test-writer, implementer, verifier, single-session, tdd-simple).
278
280
  * Auto-wires prompts.overrides in nax.config.json if the file exists and overrides are not already set.
279
281
  * Returns the list of file paths written. Returns empty array if files
280
282
  * already exist and force is not set.
281
283
  *
282
- * Note: tdd-simple role is supported in the prompt system but not auto-generated as a template.
283
- *
284
284
  * @param options - Command options
285
285
  * @returns Array of file paths written
286
286
  */
287
287
  export async function promptsInitCommand(options: PromptsInitCommandOptions): Promise<string[]> {
288
- const { workdir, force = false } = options;
288
+ const { workdir, force = false, autoWireConfig = true } = options;
289
289
  const templatesDir = join(workdir, "nax", "templates");
290
290
 
291
291
  mkdirSync(templatesDir, { recursive: true });
@@ -318,8 +318,10 @@ export async function promptsInitCommand(options: PromptsInitCommandOptions): Pr
318
318
  console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
319
319
  }
320
320
 
321
- // Auto-wire prompts.overrides in nax.config.json
322
- await autoWirePromptsConfig(workdir);
321
+ // Auto-wire prompts.overrides in nax.config.json (if enabled)
322
+ if (autoWireConfig) {
323
+ await autoWirePromptsConfig(workdir);
324
+ }
323
325
 
324
326
  return written;
325
327
  }
@@ -346,6 +348,7 @@ async function autoWirePromptsConfig(workdir: string): Promise<void> {
346
348
  implementer: "nax/templates/implementer.md",
347
349
  verifier: "nax/templates/verifier.md",
348
350
  "single-session": "nax/templates/single-session.md",
351
+ "tdd-simple": "nax/templates/tdd-simple.md",
349
352
  },
350
353
  },
351
354
  },
@@ -376,6 +379,7 @@ async function autoWirePromptsConfig(workdir: string): Promise<void> {
376
379
  implementer: "nax/templates/implementer.md",
377
380
  verifier: "nax/templates/verifier.md",
378
381
  "single-session": "nax/templates/single-session.md",
382
+ "tdd-simple": "nax/templates/tdd-simple.md",
379
383
  };
380
384
 
381
385
  // Add or update prompts section
@@ -427,6 +431,56 @@ function formatConfigJson(config: Record<string, unknown>): string {
427
431
  return lines.join("\n");
428
432
  }
429
433
 
434
+ const VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"] as const;
435
+
436
+ export interface ExportPromptCommandOptions {
437
+ /** Role to export prompt for */
438
+ role: string;
439
+ /** Optional output file path (stdout if not provided) */
440
+ out?: string;
441
+ }
442
+
443
+ /**
444
+ * Execute the `nax prompts --export <role>` command.
445
+ *
446
+ * Builds the full default prompt for the given role using a stub story
447
+ * and empty context, then writes it to stdout or a file.
448
+ *
449
+ * @param options - Command options
450
+ */
451
+ export async function exportPromptCommand(options: ExportPromptCommandOptions): Promise<void> {
452
+ const { role, out } = options;
453
+
454
+ if (!VALID_EXPORT_ROLES.includes(role as (typeof VALID_EXPORT_ROLES)[number])) {
455
+ console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
456
+ process.exit(1);
457
+ }
458
+
459
+ const stubStory: UserStory = {
460
+ id: "EXAMPLE",
461
+ title: "Example story",
462
+ description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
463
+ acceptanceCriteria: ["AC-1: Example criterion"],
464
+ tags: [],
465
+ dependencies: [],
466
+ status: "pending",
467
+ passes: false,
468
+ escalations: [],
469
+ attempts: 0,
470
+ };
471
+
472
+ const prompt = await PromptBuilder.for(role as (typeof VALID_EXPORT_ROLES)[number])
473
+ .story(stubStory)
474
+ .build();
475
+
476
+ if (out) {
477
+ await Bun.write(out, prompt);
478
+ console.log(`[OK] Exported prompt for "${role}" to ${out}`);
479
+ } else {
480
+ console.log(prompt);
481
+ }
482
+ }
483
+
430
484
  /**
431
485
  * Handle three-session TDD prompts by building separate prompts for each role.
432
486
  *
@@ -291,10 +291,14 @@ const PrecheckConfigSchema = z.object({
291
291
  storySizeGate: StorySizeGateConfigSchema,
292
292
  });
293
293
 
294
- const PromptsConfigSchema = z.object({
294
+ export const PromptsConfigSchema = z.object({
295
295
  overrides: z
296
296
  .record(
297
- z.enum(["test-writer", "implementer", "verifier", "single-session"]),
297
+ z
298
+ .string()
299
+ .refine((key) => ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
300
+ message: "Role must be one of: test-writer, implementer, verifier, single-session, tdd-simple",
301
+ }),
298
302
  z.string().min(1, "Override path must be non-empty"),
299
303
  )
300
304
  .optional(),
@@ -15,16 +15,28 @@ import type { PluginRegistry } from "../plugins";
15
15
  import { runReview } from "./runner";
16
16
  import type { ReviewConfig, ReviewResult } from "./types";
17
17
 
18
- async function getChangedFiles(workdir: string): Promise<string[]> {
18
+ async function getChangedFiles(workdir: string, baseRef?: string): Promise<string[]> {
19
19
  try {
20
- const [stagedProc, unstagedProc] = [
21
- spawn({ cmd: ["git", "diff", "--name-only", "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22
- spawn({ cmd: ["git", "diff", "--name-only"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
20
+ const diffArgs = ["diff", "--name-only"];
21
+ const [stagedProc, unstagedProc, baseProc] = [
22
+ spawn({ cmd: ["git", ...diffArgs, "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
23
+ spawn({ cmd: ["git", ...diffArgs], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
24
+ baseRef
25
+ ? spawn({ cmd: ["git", ...diffArgs, `${baseRef}...HEAD`], cwd: workdir, stdout: "pipe", stderr: "pipe" })
26
+ : null,
23
27
  ];
24
- await Promise.all([stagedProc.exited, unstagedProc.exited]);
25
- const staged = (await new Response(stagedProc.stdout).text()).trim().split("\n").filter(Boolean);
26
- const unstaged = (await new Response(unstagedProc.stdout).text()).trim().split("\n").filter(Boolean);
27
- return Array.from(new Set([...staged, ...unstaged]));
28
+
29
+ await Promise.all([stagedProc.exited, unstagedProc.exited, baseProc?.exited]);
30
+
31
+ const [staged, unstaged, based] = await Promise.all([
32
+ new Response(stagedProc.stdout).text().then((t) => t.trim().split("\n").filter(Boolean)),
33
+ new Response(unstagedProc.stdout).text().then((t) => t.trim().split("\n").filter(Boolean)),
34
+ baseProc
35
+ ? new Response(baseProc.stdout).text().then((t) => t.trim().split("\n").filter(Boolean))
36
+ : Promise.resolve([]),
37
+ ]);
38
+
39
+ return Array.from(new Set([...staged, ...unstaged, ...based]));
28
40
  } catch {
29
41
  return [];
30
42
  }
@@ -60,7 +72,10 @@ export class ReviewOrchestrator {
60
72
  if (plugins) {
61
73
  const reviewers = plugins.getReviewers();
62
74
  if (reviewers.length > 0) {
63
- const changedFiles = await getChangedFiles(workdir);
75
+ // Use the story's start ref if available to capture auto-committed changes
76
+ // biome-ignore lint/suspicious/noExplicitAny: baseRef injected into config for pipeline use
77
+ const baseRef = (executionConfig as any)?.storyGitRef;
78
+ const changedFiles = await getChangedFiles(workdir, baseRef);
64
79
  const pluginResults: ReviewResult["pluginReviewers"] = [];
65
80
 
66
81
  for (const reviewer of reviewers) {