@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 +25 -2
- package/dist/nax.js +149 -12
- package/package.json +1 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/prompts.ts +185 -1
- package/src/prd/index.ts +4 -0
- package/src/precheck/checks-warnings.ts +21 -7
- package/src/precheck/index.ts +1 -1
- package/src/tdd/session-runner.ts +5 -1
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
|
|
853
|
-
.
|
|
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.
|
|
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("
|
|
19571
|
-
return "
|
|
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
|
-
|
|
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
|
|
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
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";
|
package/src/cli/prompts.ts
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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/
|
package/src/precheck/index.ts
CHANGED
|
@@ -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")
|
|
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
|
|