@nathapp/nax 0.30.0 → 0.31.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/bin/nax.ts CHANGED
@@ -54,6 +54,7 @@ import {
54
54
  planCommand,
55
55
  pluginsListCommand,
56
56
  promptsCommand,
57
+ promptsInitCommand,
57
58
  runsListCommand,
58
59
  runsShowCommand,
59
60
  } from "../src/cli";
@@ -849,8 +850,10 @@ program
849
850
  // ── prompts ──────────────────────────────────────────
850
851
  program
851
852
  .command("prompts")
852
- .description("Assemble prompts for stories without executing agents")
853
- .requiredOption("-f, --feature <name>", "Feature name")
853
+ .description("Assemble or initialize prompts")
854
+ .option("-f, --feature <name>", "Feature name (required unless using --init)")
855
+ .option("--init", "Initialize default prompt templates", false)
856
+ .option("--force", "Overwrite existing template files", false)
854
857
  .option("--story <id>", "Filter to a single story ID (e.g., US-003)")
855
858
  .option("--out <dir>", "Output directory for prompt files (default: stdout)")
856
859
  .option("-d, --dir <path>", "Project directory", process.cwd())
@@ -864,6 +867,26 @@ program
864
867
  process.exit(1);
865
868
  }
866
869
 
870
+ // Handle --init command
871
+ if (options.init) {
872
+ try {
873
+ await promptsInitCommand({
874
+ workdir,
875
+ force: options.force,
876
+ });
877
+ } catch (err) {
878
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
879
+ process.exit(1);
880
+ }
881
+ return;
882
+ }
883
+
884
+ // Handle regular prompts command (requires --feature)
885
+ if (!options.feature) {
886
+ console.error(chalk.red("Error: --feature is required (unless using --init)"));
887
+ process.exit(1);
888
+ }
889
+
867
890
  // Load config
868
891
  const config = await loadConfig(workdir);
869
892
 
package/dist/nax.js CHANGED
@@ -19505,7 +19505,7 @@ var package_default;
19505
19505
  var init_package = __esm(() => {
19506
19506
  package_default = {
19507
19507
  name: "@nathapp/nax",
19508
- version: "0.30.0",
19508
+ version: "0.31.0",
19509
19509
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
19510
19510
  type: "module",
19511
19511
  bin: {
@@ -19567,8 +19567,8 @@ var init_version = __esm(() => {
19567
19567
  NAX_VERSION = package_default.version;
19568
19568
  NAX_COMMIT = (() => {
19569
19569
  try {
19570
- if (/^[0-9a-f]{6,10}$/.test("f7c3de1"))
19571
- return "f7c3de1";
19570
+ if (/^[0-9a-f]{6,10}$/.test("6b2cc85"))
19571
+ return "6b2cc85";
19572
19572
  } catch {}
19573
19573
  try {
19574
19574
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -19621,6 +19621,11 @@ async function loadPRD(path) {
19621
19621
  story.escalations = story.escalations ?? [];
19622
19622
  story.dependencies = story.dependencies ?? [];
19623
19623
  story.tags = story.tags ?? [];
19624
+ const rawStatus = story.status;
19625
+ if (rawStatus === "open")
19626
+ story.status = "pending";
19627
+ if (rawStatus === "done")
19628
+ story.status = "passed";
19624
19629
  story.status = story.status ?? "pending";
19625
19630
  story.acceptanceCriteria = story.acceptanceCriteria ?? [];
19626
19631
  story.storyPoints = story.storyPoints ?? 1;
@@ -23980,7 +23985,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
23980
23985
  prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).build();
23981
23986
  break;
23982
23987
  case "verifier":
23983
- prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).build();
23988
+ prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).build();
23984
23989
  break;
23985
23990
  }
23986
23991
  const logger = getLogger();
@@ -26889,14 +26894,14 @@ async function checkPendingStories(prd) {
26889
26894
  message: passed ? `${pendingStories.length} pending stories found` : "no pending stories to execute"
26890
26895
  };
26891
26896
  }
26892
- async function checkOptionalCommands(config2) {
26897
+ async function checkOptionalCommands(config2, workdir) {
26893
26898
  const missing = [];
26894
- if (!config2.execution.lintCommand) {
26899
+ const hasLint = config2.quality?.commands?.lint || config2.execution?.lintCommand || await hasPackageScript(workdir, "lint");
26900
+ const hasTypecheck = config2.quality?.commands?.typecheck || config2.execution?.typecheckCommand || await hasPackageScript(workdir, "typecheck");
26901
+ if (!hasLint)
26895
26902
  missing.push("lint");
26896
- }
26897
- if (!config2.execution.typecheckCommand) {
26903
+ if (!hasTypecheck)
26898
26904
  missing.push("typecheck");
26899
- }
26900
26905
  const passed = missing.length === 0;
26901
26906
  return {
26902
26907
  name: "optional-commands-configured",
@@ -26905,6 +26910,14 @@ async function checkOptionalCommands(config2) {
26905
26910
  message: passed ? "All optional commands configured" : `Optional commands not configured: ${missing.join(", ")}`
26906
26911
  };
26907
26912
  }
26913
+ async function hasPackageScript(workdir, name) {
26914
+ try {
26915
+ const pkg = await Bun.file(`${workdir}/package.json`).json();
26916
+ return Boolean(pkg?.scripts?.[name]);
26917
+ } catch {
26918
+ return false;
26919
+ }
26920
+ }
26908
26921
  async function checkGitignoreCoversNax(workdir) {
26909
26922
  const gitignorePath = `${workdir}/.gitignore`;
26910
26923
  const exists = existsSync23(gitignorePath);
@@ -27096,7 +27109,7 @@ async function runPrecheck(config2, prd, options) {
27096
27109
  () => checkClaudeMdExists(workdir),
27097
27110
  () => checkDiskSpace(),
27098
27111
  () => checkPendingStories(prd),
27099
- () => checkOptionalCommands(config2),
27112
+ () => checkOptionalCommands(config2, workdir),
27100
27113
  () => checkGitignoreCoversNax(workdir),
27101
27114
  () => checkPromptOverrideFiles(config2, workdir)
27102
27115
  ];
@@ -62763,11 +62776,119 @@ function buildFrontmatter(story, ctx, role) {
62763
62776
  `)}
62764
62777
  `;
62765
62778
  }
62779
+ var TEMPLATE_ROLES = [
62780
+ { file: "test-writer.md", role: "test-writer" },
62781
+ { file: "implementer.md", role: "implementer", variant: "standard" },
62782
+ { file: "verifier.md", role: "verifier" },
62783
+ { file: "single-session.md", role: "single-session" }
62784
+ ];
62785
+ var TEMPLATE_HEADER = `<!--
62786
+ This file controls the role-body section of the nax prompt for this role.
62787
+ Edit the content below to customize the task instructions given to the agent.
62788
+
62789
+ NON-OVERRIDABLE SECTIONS (always injected by nax, cannot be changed here):
62790
+ - Isolation rules (scope, file access boundaries)
62791
+ - Story context (acceptance criteria, description, dependencies)
62792
+ - Conventions (project coding standards)
62793
+
62794
+ To activate overrides, add to your nax/config.json:
62795
+ { "prompts": { "overrides": { "<role>": "nax/templates/<role>.md" } } }
62796
+ -->
62797
+
62798
+ `;
62799
+ async function promptsInitCommand(options) {
62800
+ const { workdir, force = false } = options;
62801
+ const templatesDir = join18(workdir, "nax", "templates");
62802
+ mkdirSync3(templatesDir, { recursive: true });
62803
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync15(join18(templatesDir, f)));
62804
+ if (existingFiles.length > 0 && !force) {
62805
+ console.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
62806
+ Pass --force to overwrite existing templates.`);
62807
+ return [];
62808
+ }
62809
+ const written = [];
62810
+ for (const template of TEMPLATE_ROLES) {
62811
+ const filePath = join18(templatesDir, template.file);
62812
+ const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
62813
+ const content = TEMPLATE_HEADER + roleBody;
62814
+ await Bun.write(filePath, content);
62815
+ written.push(filePath);
62816
+ }
62817
+ console.log(`[OK] Written ${written.length} template files to nax/templates/:`);
62818
+ for (const filePath of written) {
62819
+ console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
62820
+ }
62821
+ await autoWirePromptsConfig(workdir);
62822
+ return written;
62823
+ }
62824
+ async function autoWirePromptsConfig(workdir) {
62825
+ const configPath = join18(workdir, "nax.config.json");
62826
+ if (!existsSync15(configPath)) {
62827
+ const exampleConfig = JSON.stringify({
62828
+ prompts: {
62829
+ overrides: {
62830
+ "test-writer": "nax/templates/test-writer.md",
62831
+ implementer: "nax/templates/implementer.md",
62832
+ verifier: "nax/templates/verifier.md",
62833
+ "single-session": "nax/templates/single-session.md"
62834
+ }
62835
+ }
62836
+ }, null, 2);
62837
+ console.log(`
62838
+ No nax.config.json found. To activate overrides, create nax/config.json with:
62839
+ ${exampleConfig}`);
62840
+ return;
62841
+ }
62842
+ const configFile = Bun.file(configPath);
62843
+ const configContent = await configFile.text();
62844
+ const config2 = JSON.parse(configContent);
62845
+ if (config2.prompts?.overrides && Object.keys(config2.prompts.overrides).length > 0) {
62846
+ console.log(`[INFO] prompts.overrides already configured in nax.config.json. Skipping auto-wiring.
62847
+ ` + " To reset overrides, remove the prompts.overrides section and re-run this command.");
62848
+ return;
62849
+ }
62850
+ const overrides = {
62851
+ "test-writer": "nax/templates/test-writer.md",
62852
+ implementer: "nax/templates/implementer.md",
62853
+ verifier: "nax/templates/verifier.md",
62854
+ "single-session": "nax/templates/single-session.md"
62855
+ };
62856
+ if (!config2.prompts) {
62857
+ config2.prompts = {};
62858
+ }
62859
+ config2.prompts.overrides = overrides;
62860
+ const updatedConfig = formatConfigJson(config2);
62861
+ await Bun.write(configPath, updatedConfig);
62862
+ console.log("[OK] Auto-wired prompts.overrides in nax.config.json");
62863
+ }
62864
+ function formatConfigJson(config2) {
62865
+ const lines = ["{"];
62866
+ const keys = Object.keys(config2);
62867
+ for (let i = 0;i < keys.length; i++) {
62868
+ const key = keys[i];
62869
+ const value = config2[key];
62870
+ const isLast = i === keys.length - 1;
62871
+ if (key === "prompts" && typeof value === "object" && value !== null) {
62872
+ const promptsObj = value;
62873
+ if (promptsObj.overrides) {
62874
+ const overridesJson = JSON.stringify(promptsObj.overrides);
62875
+ lines.push(` "${key}": { "overrides": ${overridesJson} }${isLast ? "" : ","}`);
62876
+ } else {
62877
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
62878
+ }
62879
+ } else {
62880
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
62881
+ }
62882
+ }
62883
+ lines.push("}");
62884
+ return lines.join(`
62885
+ `);
62886
+ }
62766
62887
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
62767
62888
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
62768
62889
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
62769
62890
  PromptBuilder.for("implementer", { variant: "standard" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
62770
- PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).build()
62891
+ PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build()
62771
62892
  ]);
62772
62893
  const sessions = [
62773
62894
  { role: "test-writer", prompt: testWriterPrompt },
@@ -72689,7 +72810,7 @@ program2.command("accept").description("Override failed acceptance criteria").re
72689
72810
  process.exit(1);
72690
72811
  }
72691
72812
  });
72692
- program2.command("prompts").description("Assemble prompts for stories without executing agents").requiredOption("-f, --feature <name>", "Feature name").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) => {
72813
+ 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) => {
72693
72814
  let workdir;
72694
72815
  try {
72695
72816
  workdir = validateDirectory(options.dir);
@@ -72697,6 +72818,22 @@ program2.command("prompts").description("Assemble prompts for stories without ex
72697
72818
  console.error(source_default.red(`Invalid directory: ${err.message}`));
72698
72819
  process.exit(1);
72699
72820
  }
72821
+ if (options.init) {
72822
+ try {
72823
+ await promptsInitCommand({
72824
+ workdir,
72825
+ force: options.force
72826
+ });
72827
+ } catch (err) {
72828
+ console.error(source_default.red(`Error: ${err.message}`));
72829
+ process.exit(1);
72830
+ }
72831
+ return;
72832
+ }
72833
+ if (!options.feature) {
72834
+ console.error(source_default.red("Error: --feature is required (unless using --init)"));
72835
+ process.exit(1);
72836
+ }
72700
72837
  const config2 = await loadConfig(workdir);
72701
72838
  try {
72702
72839
  const processedStories = await promptsCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.30.0",
3
+ "version": "0.31.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
@@ -20,7 +20,9 @@ export {
20
20
  } from "./runs";
21
21
  export {
22
22
  promptsCommand,
23
+ promptsInitCommand,
23
24
  type PromptsCommandOptions,
25
+ type PromptsInitCommandOptions,
24
26
  } from "./prompts";
25
27
  export { initCommand, type InitOptions } from "./init";
26
28
  export { pluginsListCommand } from "./plugins";
@@ -18,6 +18,7 @@ import { constitutionStage, contextStage, promptStage, routingStage } from "../p
18
18
  import type { UserStory } from "../prd";
19
19
  import { loadPRD } from "../prd";
20
20
  import { PromptBuilder } from "../prompts";
21
+ import { buildRoleTaskSection } from "../prompts/sections/role-task";
21
22
 
22
23
  export interface PromptsCommandOptions {
23
24
  /** Feature name */
@@ -240,6 +241,189 @@ function buildFrontmatter(story: UserStory, ctx: PipelineContext, role?: string)
240
241
  return `${lines.join("\n")}\n`;
241
242
  }
242
243
 
244
+ export interface PromptsInitCommandOptions {
245
+ /** Working directory (project root) */
246
+ workdir: string;
247
+ /** Overwrite existing files if true */
248
+ force?: boolean;
249
+ }
250
+
251
+ const TEMPLATE_ROLES = [
252
+ { file: "test-writer.md", role: "test-writer" as const },
253
+ { file: "implementer.md", role: "implementer" as const, variant: "standard" as const },
254
+ { file: "verifier.md", role: "verifier" as const },
255
+ { file: "single-session.md", role: "single-session" as const },
256
+ ] as const;
257
+
258
+ const TEMPLATE_HEADER = `<!--
259
+ This file controls the role-body section of the nax prompt for this role.
260
+ Edit the content below to customize the task instructions given to the agent.
261
+
262
+ NON-OVERRIDABLE SECTIONS (always injected by nax, cannot be changed here):
263
+ - Isolation rules (scope, file access boundaries)
264
+ - Story context (acceptance criteria, description, dependencies)
265
+ - Conventions (project coding standards)
266
+
267
+ To activate overrides, add to your nax/config.json:
268
+ { "prompts": { "overrides": { "<role>": "nax/templates/<role>.md" } } }
269
+ -->
270
+
271
+ `;
272
+
273
+ /**
274
+ * Execute the `nax prompts --init` command.
275
+ *
276
+ * Creates nax/templates/ and writes 4 default role-body template files.
277
+ * Auto-wires prompts.overrides in nax.config.json if the file exists and overrides are not already set.
278
+ * Returns the list of file paths written. Returns empty array if files
279
+ * already exist and force is not set.
280
+ *
281
+ * @param options - Command options
282
+ * @returns Array of file paths written
283
+ */
284
+ export async function promptsInitCommand(options: PromptsInitCommandOptions): Promise<string[]> {
285
+ const { workdir, force = false } = options;
286
+ const templatesDir = join(workdir, "nax", "templates");
287
+
288
+ mkdirSync(templatesDir, { recursive: true });
289
+
290
+ // Check for existing files
291
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync(join(templatesDir, f)));
292
+
293
+ if (existingFiles.length > 0 && !force) {
294
+ console.warn(
295
+ `[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.\n Pass --force to overwrite existing templates.`,
296
+ );
297
+ return [];
298
+ }
299
+
300
+ const written: string[] = [];
301
+
302
+ for (const template of TEMPLATE_ROLES) {
303
+ const filePath = join(templatesDir, template.file);
304
+ const roleBody =
305
+ template.role === "implementer"
306
+ ? buildRoleTaskSection(template.role, template.variant)
307
+ : buildRoleTaskSection(template.role);
308
+ const content = TEMPLATE_HEADER + roleBody;
309
+ await Bun.write(filePath, content);
310
+ written.push(filePath);
311
+ }
312
+
313
+ console.log(`[OK] Written ${written.length} template files to nax/templates/:`);
314
+ for (const filePath of written) {
315
+ console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
316
+ }
317
+
318
+ // Auto-wire prompts.overrides in nax.config.json
319
+ await autoWirePromptsConfig(workdir);
320
+
321
+ return written;
322
+ }
323
+
324
+ /**
325
+ * Auto-wire prompts.overrides in nax.config.json after template init.
326
+ *
327
+ * If nax.config.json exists and prompts.overrides is not already set,
328
+ * add the override paths. If overrides are already set, print a note.
329
+ * If nax.config.json doesn't exist, print manual instructions.
330
+ *
331
+ * @param workdir - Project working directory
332
+ */
333
+ async function autoWirePromptsConfig(workdir: string): Promise<void> {
334
+ const configPath = join(workdir, "nax.config.json");
335
+
336
+ // If config file doesn't exist, print manual instructions
337
+ if (!existsSync(configPath)) {
338
+ const exampleConfig = JSON.stringify(
339
+ {
340
+ prompts: {
341
+ overrides: {
342
+ "test-writer": "nax/templates/test-writer.md",
343
+ implementer: "nax/templates/implementer.md",
344
+ verifier: "nax/templates/verifier.md",
345
+ "single-session": "nax/templates/single-session.md",
346
+ },
347
+ },
348
+ },
349
+ null,
350
+ 2,
351
+ );
352
+ console.log(`\nNo nax.config.json found. To activate overrides, create nax/config.json with:\n${exampleConfig}`);
353
+ return;
354
+ }
355
+
356
+ // Read existing config
357
+ const configFile = Bun.file(configPath);
358
+ const configContent = await configFile.text();
359
+ const config = JSON.parse(configContent);
360
+
361
+ // Check if prompts.overrides is already set
362
+ if (config.prompts?.overrides && Object.keys(config.prompts.overrides).length > 0) {
363
+ console.log(
364
+ "[INFO] prompts.overrides already configured in nax.config.json. Skipping auto-wiring.\n" +
365
+ " To reset overrides, remove the prompts.overrides section and re-run this command.",
366
+ );
367
+ return;
368
+ }
369
+
370
+ // Build the override paths
371
+ const overrides = {
372
+ "test-writer": "nax/templates/test-writer.md",
373
+ implementer: "nax/templates/implementer.md",
374
+ verifier: "nax/templates/verifier.md",
375
+ "single-session": "nax/templates/single-session.md",
376
+ };
377
+
378
+ // Add or update prompts section
379
+ if (!config.prompts) {
380
+ config.prompts = {};
381
+ }
382
+ config.prompts.overrides = overrides;
383
+
384
+ // Write config with custom formatting that avoids 4-space indentation
385
+ // by putting the overrides object on a single line
386
+ const updatedConfig = formatConfigJson(config);
387
+ await Bun.write(configPath, updatedConfig);
388
+
389
+ console.log("[OK] Auto-wired prompts.overrides in nax.config.json");
390
+ }
391
+
392
+ /**
393
+ * Format config JSON with 2-space indentation, keeping overrides object inline.
394
+ *
395
+ * This avoids 4-space indentation by putting the overrides object on the same line.
396
+ *
397
+ * @param config - Configuration object
398
+ * @returns Formatted JSON string
399
+ */
400
+ function formatConfigJson(config: Record<string, unknown>): string {
401
+ const lines: string[] = ["{"];
402
+
403
+ const keys = Object.keys(config);
404
+ for (let i = 0; i < keys.length; i++) {
405
+ const key = keys[i];
406
+ const value = config[key];
407
+ const isLast = i === keys.length - 1;
408
+
409
+ if (key === "prompts" && typeof value === "object" && value !== null) {
410
+ // Special handling for prompts object - keep overrides inline
411
+ const promptsObj = value as Record<string, unknown>;
412
+ if (promptsObj.overrides) {
413
+ const overridesJson = JSON.stringify(promptsObj.overrides);
414
+ lines.push(` "${key}": { "overrides": ${overridesJson} }${isLast ? "" : ","}`);
415
+ } else {
416
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
417
+ }
418
+ } else {
419
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
420
+ }
421
+ }
422
+
423
+ lines.push("}");
424
+ return lines.join("\n");
425
+ }
426
+
243
427
  /**
244
428
  * Handle three-session TDD prompts by building separate prompts for each role.
245
429
  *
@@ -266,7 +450,7 @@ async function handleThreeSessionTddPrompts(
266
450
  .story(story)
267
451
  .context(ctx.contextMarkdown)
268
452
  .build(),
269
- PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).build(),
453
+ PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
270
454
  ]);
271
455
 
272
456
  const sessions = [
package/src/prd/index.ts CHANGED
@@ -49,6 +49,10 @@ export async function loadPRD(path: string): Promise<PRD> {
49
49
  story.escalations = story.escalations ?? [];
50
50
  story.dependencies = story.dependencies ?? [];
51
51
  story.tags = story.tags ?? [];
52
+ // Normalize aliases: "open" → "pending", "done" → "passed"
53
+ const rawStatus = story.status as string;
54
+ if (rawStatus === "open") story.status = "pending";
55
+ if (rawStatus === "done") story.status = "passed";
52
56
  story.status = story.status ?? "pending";
53
57
  story.acceptanceCriteria = story.acceptanceCriteria ?? [];
54
58
  story.storyPoints = story.storyPoints ?? 1;
@@ -90,15 +90,19 @@ export async function checkPendingStories(prd: PRD): Promise<Check> {
90
90
  /**
91
91
  * Check if optional commands are configured.
92
92
  */
93
- export async function checkOptionalCommands(config: NaxConfig): Promise<Check> {
93
+ export async function checkOptionalCommands(config: NaxConfig, workdir: string): Promise<Check> {
94
94
  const missing: string[] = [];
95
95
 
96
- if (!config.execution.lintCommand) {
97
- missing.push("lint");
98
- }
99
- if (!config.execution.typecheckCommand) {
100
- missing.push("typecheck");
101
- }
96
+ // Check quality.commands first, then execution config, then package.json fallback
97
+ const hasLint =
98
+ config.quality?.commands?.lint || config.execution?.lintCommand || (await hasPackageScript(workdir, "lint"));
99
+ const hasTypecheck =
100
+ config.quality?.commands?.typecheck ||
101
+ config.execution?.typecheckCommand ||
102
+ (await hasPackageScript(workdir, "typecheck"));
103
+
104
+ if (!hasLint) missing.push("lint");
105
+ if (!hasTypecheck) missing.push("typecheck");
102
106
 
103
107
  const passed = missing.length === 0;
104
108
 
@@ -110,6 +114,16 @@ export async function checkOptionalCommands(config: NaxConfig): Promise<Check> {
110
114
  };
111
115
  }
112
116
 
117
+ /** Check if package.json has a script by name */
118
+ async function hasPackageScript(workdir: string, name: string): Promise<boolean> {
119
+ try {
120
+ const pkg = await Bun.file(`${workdir}/package.json`).json();
121
+ return Boolean(pkg?.scripts?.[name]);
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
113
127
  /**
114
128
  * Check if .gitignore covers nax runtime files.
115
129
  * Patterns: nax.lock, runs/, test/tmp/
@@ -141,7 +141,7 @@ export async function runPrecheck(
141
141
  () => checkClaudeMdExists(workdir),
142
142
  () => checkDiskSpace(),
143
143
  () => checkPendingStories(prd),
144
- () => checkOptionalCommands(config),
144
+ () => checkOptionalCommands(config, workdir),
145
145
  () => checkGitignoreCoversNax(workdir),
146
146
  () => checkPromptOverrideFiles(config, workdir),
147
147
  ];
@@ -103,7 +103,11 @@ export async function runTddSession(
103
103
  .build();
104
104
  break;
105
105
  case "verifier":
106
- prompt = await PromptBuilder.for("verifier").withLoader(workdir, config).story(story).build();
106
+ prompt = await PromptBuilder.for("verifier")
107
+ .withLoader(workdir, config)
108
+ .story(story)
109
+ .context(contextMarkdown)
110
+ .build();
107
111
  break;
108
112
  }
109
113