@nathapp/nax 0.36.2 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/bin/nax.ts +38 -4
- package/dist/nax.js +95 -23
- package/package.json +4 -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.38.0",
|
|
20677
20679
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20678
20680
|
type: "module",
|
|
20679
20681
|
bin: {
|
|
@@ -20690,6 +20692,9 @@ var init_package = __esm(() => {
|
|
|
20690
20692
|
"test:unit": "bun test ./test/unit/ --timeout=60000",
|
|
20691
20693
|
"test:integration": "bun test ./test/integration/ --timeout=60000",
|
|
20692
20694
|
"test:ui": "bun test ./test/ui/ --timeout=60000",
|
|
20695
|
+
"check-test-overlap": "bun run scripts/check-test-overlap.ts",
|
|
20696
|
+
"check-dead-tests": "bun run scripts/check-dead-tests.ts",
|
|
20697
|
+
"check:test-sizes": "bun run scripts/check-test-sizes.ts",
|
|
20693
20698
|
prepublishOnly: "bun run build"
|
|
20694
20699
|
},
|
|
20695
20700
|
dependencies: {
|
|
@@ -20734,8 +20739,8 @@ var init_version = __esm(() => {
|
|
|
20734
20739
|
NAX_VERSION = package_default.version;
|
|
20735
20740
|
NAX_COMMIT = (() => {
|
|
20736
20741
|
try {
|
|
20737
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20738
|
-
return "
|
|
20742
|
+
if (/^[0-9a-f]{6,10}$/.test("cdad904"))
|
|
20743
|
+
return "cdad904";
|
|
20739
20744
|
} catch {}
|
|
20740
20745
|
try {
|
|
20741
20746
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -22681,18 +22686,24 @@ var init_runner2 = __esm(() => {
|
|
|
22681
22686
|
|
|
22682
22687
|
// src/review/orchestrator.ts
|
|
22683
22688
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
22684
|
-
async function getChangedFiles(workdir) {
|
|
22689
|
+
async function getChangedFiles(workdir, baseRef) {
|
|
22685
22690
|
try {
|
|
22686
|
-
const [
|
|
22687
|
-
|
|
22688
|
-
spawn2({ cmd: ["git",
|
|
22691
|
+
const diffArgs = ["diff", "--name-only"];
|
|
22692
|
+
const [stagedProc, unstagedProc, baseProc] = [
|
|
22693
|
+
spawn2({ cmd: ["git", ...diffArgs, "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
|
|
22694
|
+
spawn2({ cmd: ["git", ...diffArgs], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
|
|
22695
|
+
baseRef ? spawn2({ cmd: ["git", ...diffArgs, `${baseRef}...HEAD`], cwd: workdir, stdout: "pipe", stderr: "pipe" }) : null
|
|
22689
22696
|
];
|
|
22690
|
-
await Promise.all([stagedProc.exited, unstagedProc.exited]);
|
|
22691
|
-
const staged =
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
|
|
22697
|
+
await Promise.all([stagedProc.exited, unstagedProc.exited, baseProc?.exited]);
|
|
22698
|
+
const [staged, unstaged, based] = await Promise.all([
|
|
22699
|
+
new Response(stagedProc.stdout).text().then((t) => t.trim().split(`
|
|
22700
|
+
`).filter(Boolean)),
|
|
22701
|
+
new Response(unstagedProc.stdout).text().then((t) => t.trim().split(`
|
|
22702
|
+
`).filter(Boolean)),
|
|
22703
|
+
baseProc ? new Response(baseProc.stdout).text().then((t) => t.trim().split(`
|
|
22704
|
+
`).filter(Boolean)) : Promise.resolve([])
|
|
22705
|
+
]);
|
|
22706
|
+
return Array.from(new Set([...staged, ...unstaged, ...based]));
|
|
22696
22707
|
} catch {
|
|
22697
22708
|
return [];
|
|
22698
22709
|
}
|
|
@@ -22708,7 +22719,8 @@ class ReviewOrchestrator {
|
|
|
22708
22719
|
if (plugins) {
|
|
22709
22720
|
const reviewers = plugins.getReviewers();
|
|
22710
22721
|
if (reviewers.length > 0) {
|
|
22711
|
-
const
|
|
22722
|
+
const baseRef = executionConfig?.storyGitRef;
|
|
22723
|
+
const changedFiles = await getChangedFiles(workdir, baseRef);
|
|
22712
22724
|
const pluginResults = [];
|
|
22713
22725
|
for (const reviewer of reviewers) {
|
|
22714
22726
|
logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
|
|
@@ -64739,7 +64751,8 @@ var TEMPLATE_ROLES = [
|
|
|
64739
64751
|
{ file: "test-writer.md", role: "test-writer" },
|
|
64740
64752
|
{ file: "implementer.md", role: "implementer", variant: "standard" },
|
|
64741
64753
|
{ file: "verifier.md", role: "verifier" },
|
|
64742
|
-
{ file: "single-session.md", role: "single-session" }
|
|
64754
|
+
{ file: "single-session.md", role: "single-session" },
|
|
64755
|
+
{ file: "tdd-simple.md", role: "tdd-simple" }
|
|
64743
64756
|
];
|
|
64744
64757
|
var TEMPLATE_HEADER = `<!--
|
|
64745
64758
|
This file controls the role-body section of the nax prompt for this role.
|
|
@@ -64756,7 +64769,7 @@ var TEMPLATE_HEADER = `<!--
|
|
|
64756
64769
|
|
|
64757
64770
|
`;
|
|
64758
64771
|
async function promptsInitCommand(options) {
|
|
64759
|
-
const { workdir, force = false } = options;
|
|
64772
|
+
const { workdir, force = false, autoWireConfig = true } = options;
|
|
64760
64773
|
const templatesDir = join18(workdir, "nax", "templates");
|
|
64761
64774
|
mkdirSync3(templatesDir, { recursive: true });
|
|
64762
64775
|
const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync15(join18(templatesDir, f)));
|
|
@@ -64777,7 +64790,9 @@ async function promptsInitCommand(options) {
|
|
|
64777
64790
|
for (const filePath of written) {
|
|
64778
64791
|
console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
|
|
64779
64792
|
}
|
|
64780
|
-
|
|
64793
|
+
if (autoWireConfig) {
|
|
64794
|
+
await autoWirePromptsConfig(workdir);
|
|
64795
|
+
}
|
|
64781
64796
|
return written;
|
|
64782
64797
|
}
|
|
64783
64798
|
async function autoWirePromptsConfig(workdir) {
|
|
@@ -64789,7 +64804,8 @@ async function autoWirePromptsConfig(workdir) {
|
|
|
64789
64804
|
"test-writer": "nax/templates/test-writer.md",
|
|
64790
64805
|
implementer: "nax/templates/implementer.md",
|
|
64791
64806
|
verifier: "nax/templates/verifier.md",
|
|
64792
|
-
"single-session": "nax/templates/single-session.md"
|
|
64807
|
+
"single-session": "nax/templates/single-session.md",
|
|
64808
|
+
"tdd-simple": "nax/templates/tdd-simple.md"
|
|
64793
64809
|
}
|
|
64794
64810
|
}
|
|
64795
64811
|
}, null, 2);
|
|
@@ -64810,7 +64826,8 @@ ${exampleConfig}`);
|
|
|
64810
64826
|
"test-writer": "nax/templates/test-writer.md",
|
|
64811
64827
|
implementer: "nax/templates/implementer.md",
|
|
64812
64828
|
verifier: "nax/templates/verifier.md",
|
|
64813
|
-
"single-session": "nax/templates/single-session.md"
|
|
64829
|
+
"single-session": "nax/templates/single-session.md",
|
|
64830
|
+
"tdd-simple": "nax/templates/tdd-simple.md"
|
|
64814
64831
|
};
|
|
64815
64832
|
if (!config2.prompts) {
|
|
64816
64833
|
config2.prompts = {};
|
|
@@ -64843,6 +64860,33 @@ function formatConfigJson(config2) {
|
|
|
64843
64860
|
return lines.join(`
|
|
64844
64861
|
`);
|
|
64845
64862
|
}
|
|
64863
|
+
var VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
|
|
64864
|
+
async function exportPromptCommand(options) {
|
|
64865
|
+
const { role, out } = options;
|
|
64866
|
+
if (!VALID_EXPORT_ROLES.includes(role)) {
|
|
64867
|
+
console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
|
|
64868
|
+
process.exit(1);
|
|
64869
|
+
}
|
|
64870
|
+
const stubStory = {
|
|
64871
|
+
id: "EXAMPLE",
|
|
64872
|
+
title: "Example story",
|
|
64873
|
+
description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
|
|
64874
|
+
acceptanceCriteria: ["AC-1: Example criterion"],
|
|
64875
|
+
tags: [],
|
|
64876
|
+
dependencies: [],
|
|
64877
|
+
status: "pending",
|
|
64878
|
+
passes: false,
|
|
64879
|
+
escalations: [],
|
|
64880
|
+
attempts: 0
|
|
64881
|
+
};
|
|
64882
|
+
const prompt = await PromptBuilder.for(role).story(stubStory).build();
|
|
64883
|
+
if (out) {
|
|
64884
|
+
await Bun.write(out, prompt);
|
|
64885
|
+
console.log(`[OK] Exported prompt for "${role}" to ${out}`);
|
|
64886
|
+
} else {
|
|
64887
|
+
console.log(prompt);
|
|
64888
|
+
}
|
|
64889
|
+
}
|
|
64846
64890
|
async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
|
|
64847
64891
|
const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
|
|
64848
64892
|
PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
|
|
@@ -74416,13 +74460,29 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
|
|
|
74416
74460
|
|
|
74417
74461
|
**Note:** Customize this file to match your project's specific needs.
|
|
74418
74462
|
`);
|
|
74463
|
+
try {
|
|
74464
|
+
await promptsInitCommand({
|
|
74465
|
+
workdir,
|
|
74466
|
+
force: options.force,
|
|
74467
|
+
autoWireConfig: false
|
|
74468
|
+
});
|
|
74469
|
+
} catch (err) {
|
|
74470
|
+
console.error(source_default.red(`Failed to initialize templates: ${err.message}`));
|
|
74471
|
+
process.exit(1);
|
|
74472
|
+
}
|
|
74419
74473
|
console.log(source_default.green("\u2705 Initialized nax"));
|
|
74420
74474
|
console.log(source_default.dim(` ${naxDir}/`));
|
|
74421
74475
|
console.log(source_default.dim(" \u251C\u2500\u2500 config.json"));
|
|
74422
74476
|
console.log(source_default.dim(" \u251C\u2500\u2500 context.md"));
|
|
74423
74477
|
console.log(source_default.dim(" \u251C\u2500\u2500 hooks.json"));
|
|
74424
74478
|
console.log(source_default.dim(" \u251C\u2500\u2500 features/"));
|
|
74425
|
-
console.log(source_default.dim(" \
|
|
74479
|
+
console.log(source_default.dim(" \u251C\u2500\u2500 hooks/"));
|
|
74480
|
+
console.log(source_default.dim(" \u2514\u2500\u2500 templates/"));
|
|
74481
|
+
console.log(source_default.dim(" \u251C\u2500\u2500 test-writer.md"));
|
|
74482
|
+
console.log(source_default.dim(" \u251C\u2500\u2500 implementer.md"));
|
|
74483
|
+
console.log(source_default.dim(" \u251C\u2500\u2500 verifier.md"));
|
|
74484
|
+
console.log(source_default.dim(" \u251C\u2500\u2500 single-session.md"));
|
|
74485
|
+
console.log(source_default.dim(" \u2514\u2500\u2500 tdd-simple.md"));
|
|
74426
74486
|
console.log(source_default.dim(`
|
|
74427
74487
|
Next: nax features create <name>`));
|
|
74428
74488
|
});
|
|
@@ -74883,7 +74943,7 @@ program2.command("accept").description("Override failed acceptance criteria").re
|
|
|
74883
74943
|
process.exit(1);
|
|
74884
74944
|
}
|
|
74885
74945
|
});
|
|
74886
|
-
program2.command("prompts").description("Assemble or initialize prompts").option("-f, --feature <name>", "Feature name (required unless using --init)").option("--init", "Initialize default prompt templates", false).option("--force", "Overwrite existing template files", false).option("--story <id>", "Filter to a single story ID (e.g., US-003)").option("--out <
|
|
74946
|
+
program2.command("prompts").description("Assemble or initialize prompts").option("-f, --feature <name>", "Feature name (required unless using --init or --export)").option("--init", "Initialize default prompt templates", false).option("--export <role>", "Export default prompt for a role to stdout or --out file").option("--force", "Overwrite existing template files", false).option("--story <id>", "Filter to a single story ID (e.g., US-003)").option("--out <path>", "Output file path for --export, or directory for regular prompts (default: stdout)").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
74887
74947
|
let workdir;
|
|
74888
74948
|
try {
|
|
74889
74949
|
workdir = validateDirectory(options.dir);
|
|
@@ -74903,8 +74963,20 @@ program2.command("prompts").description("Assemble or initialize prompts").option
|
|
|
74903
74963
|
}
|
|
74904
74964
|
return;
|
|
74905
74965
|
}
|
|
74966
|
+
if (options.export) {
|
|
74967
|
+
try {
|
|
74968
|
+
await exportPromptCommand({
|
|
74969
|
+
role: options.export,
|
|
74970
|
+
out: options.out
|
|
74971
|
+
});
|
|
74972
|
+
} catch (err) {
|
|
74973
|
+
console.error(source_default.red(`Error: ${err.message}`));
|
|
74974
|
+
process.exit(1);
|
|
74975
|
+
}
|
|
74976
|
+
return;
|
|
74977
|
+
}
|
|
74906
74978
|
if (!options.feature) {
|
|
74907
|
-
console.error(source_default.red("Error: --feature is required (unless using --init)"));
|
|
74979
|
+
console.error(source_default.red("Error: --feature is required (unless using --init or --export)"));
|
|
74908
74980
|
process.exit(1);
|
|
74909
74981
|
}
|
|
74910
74982
|
const config2 = await loadConfig(workdir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"test:unit": "bun test ./test/unit/ --timeout=60000",
|
|
18
18
|
"test:integration": "bun test ./test/integration/ --timeout=60000",
|
|
19
19
|
"test:ui": "bun test ./test/ui/ --timeout=60000",
|
|
20
|
+
"check-test-overlap": "bun run scripts/check-test-overlap.ts",
|
|
21
|
+
"check-dead-tests": "bun run scripts/check-dead-tests.ts",
|
|
22
|
+
"check:test-sizes": "bun run scripts/check-test-sizes.ts",
|
|
20
23
|
"prepublishOnly": "bun run build"
|
|
21
24
|
},
|
|
22
25
|
"dependencies": {
|
package/src/cli/index.ts
CHANGED
|
@@ -21,8 +21,10 @@ export {
|
|
|
21
21
|
export {
|
|
22
22
|
promptsCommand,
|
|
23
23
|
promptsInitCommand,
|
|
24
|
+
exportPromptCommand,
|
|
24
25
|
type PromptsCommandOptions,
|
|
25
26
|
type PromptsInitCommandOptions,
|
|
27
|
+
type ExportPromptCommandOptions,
|
|
26
28
|
} from "./prompts";
|
|
27
29
|
export { initCommand, type InitOptions } from "./init";
|
|
28
30
|
export { pluginsListCommand } from "./plugins";
|
package/src/cli/init.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { globalConfigDir, projectConfigDir } from "../config/paths";
|
|
11
11
|
import { DEFAULT_CONFIG } from "../config/schema";
|
|
12
12
|
import { getLogger } from "../logger";
|
|
13
|
+
import { promptsInitCommand } from "./prompts";
|
|
13
14
|
|
|
14
15
|
/** Init command options */
|
|
15
16
|
export interface InitOptions {
|
|
@@ -133,7 +134,7 @@ async function initGlobal(): Promise<void> {
|
|
|
133
134
|
/**
|
|
134
135
|
* Initialize project nax directory (nax/)
|
|
135
136
|
*/
|
|
136
|
-
async function initProject(projectRoot: string): Promise<void> {
|
|
137
|
+
export async function initProject(projectRoot: string): Promise<void> {
|
|
137
138
|
const logger = getLogger();
|
|
138
139
|
const projectDir = projectConfigDir(projectRoot);
|
|
139
140
|
|
|
@@ -173,6 +174,11 @@ async function initProject(projectRoot: string): Promise<void> {
|
|
|
173
174
|
// Update .gitignore to include nax-specific entries
|
|
174
175
|
await updateGitignore(projectRoot);
|
|
175
176
|
|
|
177
|
+
// Create prompt templates (final step)
|
|
178
|
+
// Pass autoWireConfig: false to prevent auto-wiring prompts.overrides
|
|
179
|
+
// Templates are created but not activated until user explicitly configures them
|
|
180
|
+
await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
|
|
181
|
+
|
|
176
182
|
logger.info("init", "Project config initialized successfully", { path: projectDir });
|
|
177
183
|
}
|
|
178
184
|
|
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) {
|