@nathapp/nax 0.36.2 → 0.37.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.37.0",
20677
20679
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20678
20680
  type: "module",
20679
20681
  bin: {
@@ -20734,8 +20736,8 @@ var init_version = __esm(() => {
20734
20736
  NAX_VERSION = package_default.version;
20735
20737
  NAX_COMMIT = (() => {
20736
20738
  try {
20737
- if (/^[0-9a-f]{6,10}$/.test("eb77e7d"))
20738
- return "eb77e7d";
20739
+ if (/^[0-9a-f]{6,10}$/.test("0a7a065"))
20740
+ return "0a7a065";
20739
20741
  } catch {}
20740
20742
  try {
20741
20743
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22681,18 +22683,24 @@ var init_runner2 = __esm(() => {
22681
22683
 
22682
22684
  // src/review/orchestrator.ts
22683
22685
  var {spawn: spawn2 } = globalThis.Bun;
22684
- async function getChangedFiles(workdir) {
22686
+ async function getChangedFiles(workdir, baseRef) {
22685
22687
  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" })
22688
+ const diffArgs = ["diff", "--name-only"];
22689
+ const [stagedProc, unstagedProc, baseProc] = [
22690
+ spawn2({ cmd: ["git", ...diffArgs, "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22691
+ spawn2({ cmd: ["git", ...diffArgs], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
22692
+ baseRef ? spawn2({ cmd: ["git", ...diffArgs, `${baseRef}...HEAD`], cwd: workdir, stdout: "pipe", stderr: "pipe" }) : null
22689
22693
  ];
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]));
22694
+ await Promise.all([stagedProc.exited, unstagedProc.exited, baseProc?.exited]);
22695
+ const [staged, unstaged, based] = await Promise.all([
22696
+ new Response(stagedProc.stdout).text().then((t) => t.trim().split(`
22697
+ `).filter(Boolean)),
22698
+ new Response(unstagedProc.stdout).text().then((t) => t.trim().split(`
22699
+ `).filter(Boolean)),
22700
+ baseProc ? new Response(baseProc.stdout).text().then((t) => t.trim().split(`
22701
+ `).filter(Boolean)) : Promise.resolve([])
22702
+ ]);
22703
+ return Array.from(new Set([...staged, ...unstaged, ...based]));
22696
22704
  } catch {
22697
22705
  return [];
22698
22706
  }
@@ -22708,7 +22716,8 @@ class ReviewOrchestrator {
22708
22716
  if (plugins) {
22709
22717
  const reviewers = plugins.getReviewers();
22710
22718
  if (reviewers.length > 0) {
22711
- const changedFiles = await getChangedFiles(workdir);
22719
+ const baseRef = executionConfig?.storyGitRef;
22720
+ const changedFiles = await getChangedFiles(workdir, baseRef);
22712
22721
  const pluginResults = [];
22713
22722
  for (const reviewer of reviewers) {
22714
22723
  logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
@@ -64739,7 +64748,8 @@ var TEMPLATE_ROLES = [
64739
64748
  { file: "test-writer.md", role: "test-writer" },
64740
64749
  { file: "implementer.md", role: "implementer", variant: "standard" },
64741
64750
  { file: "verifier.md", role: "verifier" },
64742
- { file: "single-session.md", role: "single-session" }
64751
+ { file: "single-session.md", role: "single-session" },
64752
+ { file: "tdd-simple.md", role: "tdd-simple" }
64743
64753
  ];
64744
64754
  var TEMPLATE_HEADER = `<!--
64745
64755
  This file controls the role-body section of the nax prompt for this role.
@@ -64756,7 +64766,7 @@ var TEMPLATE_HEADER = `<!--
64756
64766
 
64757
64767
  `;
64758
64768
  async function promptsInitCommand(options) {
64759
- const { workdir, force = false } = options;
64769
+ const { workdir, force = false, autoWireConfig = true } = options;
64760
64770
  const templatesDir = join18(workdir, "nax", "templates");
64761
64771
  mkdirSync3(templatesDir, { recursive: true });
64762
64772
  const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync15(join18(templatesDir, f)));
@@ -64777,7 +64787,9 @@ async function promptsInitCommand(options) {
64777
64787
  for (const filePath of written) {
64778
64788
  console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
64779
64789
  }
64780
- await autoWirePromptsConfig(workdir);
64790
+ if (autoWireConfig) {
64791
+ await autoWirePromptsConfig(workdir);
64792
+ }
64781
64793
  return written;
64782
64794
  }
64783
64795
  async function autoWirePromptsConfig(workdir) {
@@ -64789,7 +64801,8 @@ async function autoWirePromptsConfig(workdir) {
64789
64801
  "test-writer": "nax/templates/test-writer.md",
64790
64802
  implementer: "nax/templates/implementer.md",
64791
64803
  verifier: "nax/templates/verifier.md",
64792
- "single-session": "nax/templates/single-session.md"
64804
+ "single-session": "nax/templates/single-session.md",
64805
+ "tdd-simple": "nax/templates/tdd-simple.md"
64793
64806
  }
64794
64807
  }
64795
64808
  }, null, 2);
@@ -64810,7 +64823,8 @@ ${exampleConfig}`);
64810
64823
  "test-writer": "nax/templates/test-writer.md",
64811
64824
  implementer: "nax/templates/implementer.md",
64812
64825
  verifier: "nax/templates/verifier.md",
64813
- "single-session": "nax/templates/single-session.md"
64826
+ "single-session": "nax/templates/single-session.md",
64827
+ "tdd-simple": "nax/templates/tdd-simple.md"
64814
64828
  };
64815
64829
  if (!config2.prompts) {
64816
64830
  config2.prompts = {};
@@ -64843,6 +64857,33 @@ function formatConfigJson(config2) {
64843
64857
  return lines.join(`
64844
64858
  `);
64845
64859
  }
64860
+ var VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
64861
+ async function exportPromptCommand(options) {
64862
+ const { role, out } = options;
64863
+ if (!VALID_EXPORT_ROLES.includes(role)) {
64864
+ console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
64865
+ process.exit(1);
64866
+ }
64867
+ const stubStory = {
64868
+ id: "EXAMPLE",
64869
+ title: "Example story",
64870
+ description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
64871
+ acceptanceCriteria: ["AC-1: Example criterion"],
64872
+ tags: [],
64873
+ dependencies: [],
64874
+ status: "pending",
64875
+ passes: false,
64876
+ escalations: [],
64877
+ attempts: 0
64878
+ };
64879
+ const prompt = await PromptBuilder.for(role).story(stubStory).build();
64880
+ if (out) {
64881
+ await Bun.write(out, prompt);
64882
+ console.log(`[OK] Exported prompt for "${role}" to ${out}`);
64883
+ } else {
64884
+ console.log(prompt);
64885
+ }
64886
+ }
64846
64887
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
64847
64888
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
64848
64889
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
@@ -74416,13 +74457,29 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
74416
74457
 
74417
74458
  **Note:** Customize this file to match your project's specific needs.
74418
74459
  `);
74460
+ try {
74461
+ await promptsInitCommand({
74462
+ workdir,
74463
+ force: options.force,
74464
+ autoWireConfig: false
74465
+ });
74466
+ } catch (err) {
74467
+ console.error(source_default.red(`Failed to initialize templates: ${err.message}`));
74468
+ process.exit(1);
74469
+ }
74419
74470
  console.log(source_default.green("\u2705 Initialized nax"));
74420
74471
  console.log(source_default.dim(` ${naxDir}/`));
74421
74472
  console.log(source_default.dim(" \u251C\u2500\u2500 config.json"));
74422
74473
  console.log(source_default.dim(" \u251C\u2500\u2500 context.md"));
74423
74474
  console.log(source_default.dim(" \u251C\u2500\u2500 hooks.json"));
74424
74475
  console.log(source_default.dim(" \u251C\u2500\u2500 features/"));
74425
- console.log(source_default.dim(" \u2514\u2500\u2500 hooks/"));
74476
+ console.log(source_default.dim(" \u251C\u2500\u2500 hooks/"));
74477
+ console.log(source_default.dim(" \u2514\u2500\u2500 templates/"));
74478
+ console.log(source_default.dim(" \u251C\u2500\u2500 test-writer.md"));
74479
+ console.log(source_default.dim(" \u251C\u2500\u2500 implementer.md"));
74480
+ console.log(source_default.dim(" \u251C\u2500\u2500 verifier.md"));
74481
+ console.log(source_default.dim(" \u251C\u2500\u2500 single-session.md"));
74482
+ console.log(source_default.dim(" \u2514\u2500\u2500 tdd-simple.md"));
74426
74483
  console.log(source_default.dim(`
74427
74484
  Next: nax features create <name>`));
74428
74485
  });
@@ -74883,7 +74940,7 @@ program2.command("accept").description("Override failed acceptance criteria").re
74883
74940
  process.exit(1);
74884
74941
  }
74885
74942
  });
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) => {
74943
+ 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
74944
  let workdir;
74888
74945
  try {
74889
74946
  workdir = validateDirectory(options.dir);
@@ -74903,8 +74960,20 @@ program2.command("prompts").description("Assemble or initialize prompts").option
74903
74960
  }
74904
74961
  return;
74905
74962
  }
74963
+ if (options.export) {
74964
+ try {
74965
+ await exportPromptCommand({
74966
+ role: options.export,
74967
+ out: options.out
74968
+ });
74969
+ } catch (err) {
74970
+ console.error(source_default.red(`Error: ${err.message}`));
74971
+ process.exit(1);
74972
+ }
74973
+ return;
74974
+ }
74906
74975
  if (!options.feature) {
74907
- console.error(source_default.red("Error: --feature is required (unless using --init)"));
74976
+ console.error(source_default.red("Error: --feature is required (unless using --init or --export)"));
74908
74977
  process.exit(1);
74909
74978
  }
74910
74979
  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.37.0",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
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) {