@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 +19 -0
- package/bin/nax.ts +38 -4
- package/dist/nax.js +92 -23
- package/package.json +1 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/init.ts +7 -1
- package/src/cli/prompts.ts +62 -8
- package/src/config/schemas.ts +6 -2
- package/src/review/orchestrator.ts +24 -9
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("
|
|
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 <
|
|
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.
|
|
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.
|
|
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("
|
|
20738
|
-
return "
|
|
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 [
|
|
22687
|
-
|
|
22688
|
-
spawn2({ cmd: ["git",
|
|
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 =
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
|
|
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
|
|
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
|
-
|
|
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(" \
|
|
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 <
|
|
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
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
|
|
package/src/cli/prompts.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
*
|
package/src/config/schemas.ts
CHANGED
|
@@ -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
|
|
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 [
|
|
21
|
-
|
|
22
|
-
spawn({ cmd: ["git",
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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) {
|