@nathapp/nax 0.27.1 → 0.28.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/docs/ROADMAP.md +29 -3
- package/nax/features/prompt-builder/prd.json +152 -0
- package/nax/features/prompt-builder/progress.txt +3 -0
- package/nax/status.json +14 -14
- package/package.json +1 -1
- package/src/cli/config.ts +40 -1
- package/src/cli/prompts.ts +18 -6
- package/src/config/defaults.ts +1 -0
- package/src/config/schemas.ts +10 -0
- package/src/config/types.ts +7 -0
- package/src/pipeline/stages/execution.ts +5 -0
- package/src/pipeline/stages/prompt.ts +13 -4
- package/src/precheck/checks-warnings.ts +37 -0
- package/src/precheck/checks.ts +1 -0
- package/src/precheck/index.ts +14 -7
- package/src/prompts/builder.ts +178 -0
- package/src/prompts/index.ts +2 -0
- package/src/prompts/loader.ts +43 -0
- package/src/prompts/sections/conventions.ts +15 -0
- package/src/prompts/sections/index.ts +11 -0
- package/src/prompts/sections/isolation.ts +24 -0
- package/src/prompts/sections/role-task.ts +32 -0
- package/src/prompts/sections/story.ts +13 -0
- package/src/prompts/sections/verdict.ts +70 -0
- package/src/prompts/templates/implementer.ts +6 -0
- package/src/prompts/templates/single-session.ts +6 -0
- package/src/prompts/templates/test-writer.ts +6 -0
- package/src/prompts/templates/verifier.ts +6 -0
- package/src/prompts/types.ts +21 -0
- package/src/tdd/session-runner.ts +12 -12
- package/test/integration/cli/cli-config-prompts-explain.test.ts +74 -0
- package/test/integration/prompts/pb-004-migration.test.ts +523 -0
- package/test/unit/precheck/checks-warnings.test.ts +114 -0
- package/test/unit/prompts/builder.test.ts +258 -0
- package/test/unit/prompts/loader.test.ts +355 -0
- package/test/unit/prompts/sections/conventions.test.ts +30 -0
- package/test/unit/prompts/sections/isolation.test.ts +35 -0
- package/test/unit/prompts/sections/role-task.test.ts +40 -0
- package/test/unit/prompts/sections/sections.test.ts +238 -0
- package/test/unit/prompts/sections/story.test.ts +45 -0
- package/test/unit/prompts/sections/verdict.test.ts +58 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptBuilder — unified entry point for composing agent prompts.
|
|
3
|
+
*
|
|
4
|
+
* Composes prompts from ordered sections:
|
|
5
|
+
* (1) Constitution
|
|
6
|
+
* (2) Role task body (user override OR default template)
|
|
7
|
+
* (3) Story context [non-overridable]
|
|
8
|
+
* (4) Isolation rules [non-overridable]
|
|
9
|
+
* (5) Context markdown
|
|
10
|
+
* (6) Conventions footer [non-overridable, always last]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { NaxConfig } from "../config/types";
|
|
14
|
+
import type { UserStory } from "../prd";
|
|
15
|
+
import type { PromptOptions, PromptRole } from "./types";
|
|
16
|
+
|
|
17
|
+
const SECTION_SEP = "\n\n---\n\n";
|
|
18
|
+
|
|
19
|
+
export class PromptBuilder {
|
|
20
|
+
private _role: PromptRole;
|
|
21
|
+
private _options: PromptOptions;
|
|
22
|
+
private _story: UserStory | undefined;
|
|
23
|
+
private _contextMd: string | undefined;
|
|
24
|
+
private _constitution: string | undefined;
|
|
25
|
+
private _overridePath: string | undefined;
|
|
26
|
+
private _workdir: string | undefined;
|
|
27
|
+
private _loaderConfig: NaxConfig | undefined;
|
|
28
|
+
|
|
29
|
+
private constructor(role: PromptRole, options: PromptOptions = {}) {
|
|
30
|
+
this._role = role;
|
|
31
|
+
this._options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static for(role: PromptRole, options?: PromptOptions): PromptBuilder {
|
|
35
|
+
return new PromptBuilder(role, options ?? {});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
story(story: UserStory): PromptBuilder {
|
|
39
|
+
this._story = story;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
context(md: string | undefined): PromptBuilder {
|
|
44
|
+
if (md) this._contextMd = md;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constitution(c: string | undefined): PromptBuilder {
|
|
49
|
+
if (c) this._constitution = c;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override(path: string): PromptBuilder {
|
|
54
|
+
this._overridePath = path;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
withLoader(workdir: string, config: NaxConfig): PromptBuilder {
|
|
59
|
+
this._workdir = workdir;
|
|
60
|
+
this._loaderConfig = config;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async build(): Promise<string> {
|
|
65
|
+
const sections: string[] = [];
|
|
66
|
+
|
|
67
|
+
// (1) Constitution
|
|
68
|
+
if (this._constitution) {
|
|
69
|
+
sections.push(`# CONSTITUTION (follow these rules strictly)\n\n${this._constitution}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// (2) Role task body — user override or default template
|
|
73
|
+
sections.push(await this._resolveRoleBody());
|
|
74
|
+
|
|
75
|
+
// (3) Story context — non-overridable
|
|
76
|
+
if (this._story) {
|
|
77
|
+
sections.push(buildStoryContext(this._story));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// (4) Isolation rules — non-overridable
|
|
81
|
+
sections.push(buildIsolationRules(this._role, this._options));
|
|
82
|
+
|
|
83
|
+
// (5) Context markdown
|
|
84
|
+
if (this._contextMd) {
|
|
85
|
+
sections.push(this._contextMd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// (6) Conventions footer — non-overridable, always last
|
|
89
|
+
sections.push(CONVENTIONS_FOOTER);
|
|
90
|
+
|
|
91
|
+
return sections.join(SECTION_SEP);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async _resolveRoleBody(): Promise<string> {
|
|
95
|
+
// withLoader takes priority over explicit override path
|
|
96
|
+
if (this._workdir && this._loaderConfig) {
|
|
97
|
+
const { loadOverride } = await import("./loader");
|
|
98
|
+
const content = await loadOverride(this._role, this._workdir, this._loaderConfig);
|
|
99
|
+
if (content !== null) {
|
|
100
|
+
return content;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (this._overridePath) {
|
|
104
|
+
try {
|
|
105
|
+
const file = Bun.file(this._overridePath);
|
|
106
|
+
if (await file.exists()) {
|
|
107
|
+
return await file.text();
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// fall through to default template
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return buildDefaultRoleBody(this._role, this._story?.title, this._options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Section builders (module-private)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function buildDefaultRoleBody(role: PromptRole, title = "", options: PromptOptions = {}): string {
|
|
122
|
+
const variant = options.variant as string | undefined;
|
|
123
|
+
switch (role) {
|
|
124
|
+
case "test-writer":
|
|
125
|
+
return `# Test Writer — "${title}"\n\nYour role: Write failing tests ONLY. Do NOT implement any source code.`;
|
|
126
|
+
case "implementer":
|
|
127
|
+
if (variant === "lite") {
|
|
128
|
+
return `# Implementer (Lite) — "${title}"\n\nYour role: Write tests AND implement the feature in a single session.`;
|
|
129
|
+
}
|
|
130
|
+
return `# Implementer — "${title}"\n\nYour role: Make all failing tests pass.`;
|
|
131
|
+
case "verifier":
|
|
132
|
+
return `# Verifier — "${title}"\n\nYour role: Verify the implementation and tests.`;
|
|
133
|
+
case "single-session":
|
|
134
|
+
return `# Task — "${title}"\n\nYour role: Write tests AND implement the feature in a single session.`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildStoryContext(story: UserStory): string {
|
|
139
|
+
return `# Story Context
|
|
140
|
+
|
|
141
|
+
**Story:** ${story.title}
|
|
142
|
+
|
|
143
|
+
**Description:**
|
|
144
|
+
${story.description}
|
|
145
|
+
|
|
146
|
+
**Acceptance Criteria:**
|
|
147
|
+
${story.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const TEST_FILTER_RULE =
|
|
151
|
+
"When running tests, run ONLY test files related to your changes" +
|
|
152
|
+
" (e.g. `bun test ./test/specific.test.ts`). NEVER run `bun test` without a file filter" +
|
|
153
|
+
" — full suite output will flood your context window and cause failures.";
|
|
154
|
+
|
|
155
|
+
function buildIsolationRules(role: PromptRole, options: PromptOptions = {}): string {
|
|
156
|
+
const header = "# Isolation Rules\n\n";
|
|
157
|
+
const footer = `\n\n${TEST_FILTER_RULE}`;
|
|
158
|
+
const isolation = options.isolation as string | undefined;
|
|
159
|
+
|
|
160
|
+
switch (role) {
|
|
161
|
+
case "test-writer":
|
|
162
|
+
if (isolation === "lite") {
|
|
163
|
+
return `${header}isolation scope: Primarily create test files in the test/ directory. You MAY read source files and MAY import from source files to ensure correct types/interfaces. Stub-only src/ files are allowed (empty exports, no logic). Tests must fail for the right reasons (feature not implemented).${footer}`;
|
|
164
|
+
}
|
|
165
|
+
return `${header}isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
|
|
166
|
+
case "implementer":
|
|
167
|
+
return `${header}isolation scope: Implement source code in src/ to make the tests pass. Do NOT modify test files. Run tests frequently to track progress.${footer}`;
|
|
168
|
+
case "verifier":
|
|
169
|
+
return `${header}isolation scope: Verify and fix only — do not change behaviour unless it violates acceptance criteria. Ensure all tests pass and all criteria are met.${footer}`;
|
|
170
|
+
case "single-session":
|
|
171
|
+
return `${header}isolation scope: Write tests first (test/ directory), then implement (src/ directory). All tests must pass by the end.${footer}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const CONVENTIONS_FOOTER =
|
|
176
|
+
"# Conventions\n\n" +
|
|
177
|
+
"Follow existing code patterns and conventions. Write idiomatic, maintainable code." +
|
|
178
|
+
" Commit your changes when done.";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Override Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves and reads user-supplied override files relative to workdir.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { NaxConfig } from "../config/types";
|
|
9
|
+
import type { PromptRole } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load a user override for the given role from the path specified in config.
|
|
13
|
+
*
|
|
14
|
+
* @param role - The prompt role
|
|
15
|
+
* @param workdir - The project working directory
|
|
16
|
+
* @param config - The merged NaxConfig
|
|
17
|
+
* @returns The override file contents, or null if absent/missing
|
|
18
|
+
* @throws Error when file path is set but file is unreadable (e.g. permissions error)
|
|
19
|
+
*/
|
|
20
|
+
export async function loadOverride(role: PromptRole, workdir: string, config: NaxConfig): Promise<string | null> {
|
|
21
|
+
const overridePath = config.prompts?.overrides?.[role];
|
|
22
|
+
|
|
23
|
+
if (!overridePath) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const absolutePath = join(workdir, overridePath);
|
|
28
|
+
const file = Bun.file(absolutePath);
|
|
29
|
+
|
|
30
|
+
if (!(await file.exists())) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await file.text();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Cannot read prompt override for role "${role}" at "${absolutePath}": ${
|
|
39
|
+
err instanceof Error ? err.message : String(err)
|
|
40
|
+
}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conventions Section
|
|
3
|
+
*
|
|
4
|
+
* Includes bun test scoping warning and commit message instructions (non-overridable).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function buildConventionsSection(): string {
|
|
8
|
+
return (
|
|
9
|
+
"# Conventions\n\n" +
|
|
10
|
+
"Follow existing code patterns and conventions. Write idiomatic, maintainable code.\n\n" +
|
|
11
|
+
"When running tests, run ONLY test files related to your changes (e.g. `bun test ./test/specific.test.ts`). " +
|
|
12
|
+
"NEVER run `bun test` without a file filter — full suite output will flood your context window and cause failures.\n\n" +
|
|
13
|
+
"Commit your changes when done using conventional commit format (e.g. `feat:`, `fix:`, `test:`)."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Sections
|
|
3
|
+
*
|
|
4
|
+
* Non-overridable section builders for the PromptBuilder.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { buildIsolationSection } from "./isolation";
|
|
8
|
+
export { buildRoleTaskSection } from "./role-task";
|
|
9
|
+
export { buildStorySection } from "./story";
|
|
10
|
+
export { buildVerdictSection } from "./verdict";
|
|
11
|
+
export { buildConventionsSection } from "./conventions";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolation Rules Section
|
|
3
|
+
*
|
|
4
|
+
* Generates isolation rules based on mode:
|
|
5
|
+
* - strict: No access to src/ files
|
|
6
|
+
* - lite: May read src/ and create minimal stubs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TEST_FILTER_RULE =
|
|
10
|
+
"When running tests, run ONLY test files related to your changes " +
|
|
11
|
+
"(e.g. `bun test ./test/specific.test.ts`). NEVER run `bun test` without a file filter " +
|
|
12
|
+
"— full suite output will flood your context window and cause failures.";
|
|
13
|
+
|
|
14
|
+
export function buildIsolationSection(mode: "strict" | "lite"): string {
|
|
15
|
+
const header = "# Isolation Rules\n\n";
|
|
16
|
+
const footer = `\n\n${TEST_FILTER_RULE}`;
|
|
17
|
+
|
|
18
|
+
if (mode === "strict") {
|
|
19
|
+
return `${header}isolation scope: Isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// lite mode
|
|
23
|
+
return `${header}isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-Task Section
|
|
3
|
+
*
|
|
4
|
+
* Generates role definition for:
|
|
5
|
+
* - standard: Make failing tests pass (implementer role)
|
|
6
|
+
* - lite: Write tests first then implement (combined role)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function buildRoleTaskSection(variant: "standard" | "lite"): string {
|
|
10
|
+
if (variant === "standard") {
|
|
11
|
+
return (
|
|
12
|
+
"# Role: Implementer\n\n" +
|
|
13
|
+
"Your task: make failing tests pass.\n\n" +
|
|
14
|
+
"Instructions:\n" +
|
|
15
|
+
"- Implement source code in src/ to make tests pass\n" +
|
|
16
|
+
"- Do NOT modify test files\n" +
|
|
17
|
+
"- Run tests frequently to track progress\n" +
|
|
18
|
+
"- Goal: all tests green"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// lite variant
|
|
23
|
+
return (
|
|
24
|
+
"# Role: Implementer (Lite)\n\n" +
|
|
25
|
+
"Your task: Write tests AND implement the feature in a single session.\n\n" +
|
|
26
|
+
"Instructions:\n" +
|
|
27
|
+
"- Write tests first (test/ directory), then implement (src/ directory)\n" +
|
|
28
|
+
"- All tests must pass by the end\n" +
|
|
29
|
+
"- Use Bun test (describe/test/expect)\n" +
|
|
30
|
+
"- Goal: all tests green, all criteria met"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story Section
|
|
3
|
+
*
|
|
4
|
+
* Formats story title, description, and numbered acceptance criteria.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserStory } from "../../prd/types";
|
|
8
|
+
|
|
9
|
+
export function buildStorySection(story: UserStory): string {
|
|
10
|
+
const criteria = story.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
11
|
+
|
|
12
|
+
return `# Story Context\n\n**Story:** ${story.title}\n\n**Description:**\n${story.description}\n\n**Acceptance Criteria:**\n${criteria}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict Section
|
|
3
|
+
*
|
|
4
|
+
* Verifier verdict JSON schema instructions (non-overridable).
|
|
5
|
+
* Provides instructions for writing the .nax-verifier-verdict.json file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UserStory } from "../../prd/types";
|
|
9
|
+
|
|
10
|
+
export function buildVerdictSection(story: UserStory): string {
|
|
11
|
+
return `# Verdict Instructions
|
|
12
|
+
|
|
13
|
+
## Write Verdict File
|
|
14
|
+
|
|
15
|
+
After completing your verification, you **MUST** write a verdict file at the **project root**:
|
|
16
|
+
|
|
17
|
+
**File:** \`.nax-verifier-verdict.json\`
|
|
18
|
+
|
|
19
|
+
Set \`approved: true\` when ALL of these conditions are met:
|
|
20
|
+
- All tests pass
|
|
21
|
+
- Implementation is clean and follows conventions
|
|
22
|
+
- All acceptance criteria met
|
|
23
|
+
- Any test modifications by implementer are legitimate fixes
|
|
24
|
+
|
|
25
|
+
Set \`approved: false\` when ANY of these conditions are true:
|
|
26
|
+
- Tests are failing and you cannot fix them
|
|
27
|
+
- The implementer loosened test assertions to mask bugs
|
|
28
|
+
- Critical acceptance criteria are not met
|
|
29
|
+
- Code quality is poor (security issues, severe bugs, etc.)
|
|
30
|
+
|
|
31
|
+
**Full JSON schema example** (fill in all fields with real values):
|
|
32
|
+
|
|
33
|
+
\`\`\`json
|
|
34
|
+
{
|
|
35
|
+
"version": 1,
|
|
36
|
+
"approved": true,
|
|
37
|
+
"tests": {
|
|
38
|
+
"allPassing": true,
|
|
39
|
+
"passCount": 42,
|
|
40
|
+
"failCount": 0
|
|
41
|
+
},
|
|
42
|
+
"testModifications": {
|
|
43
|
+
"detected": false,
|
|
44
|
+
"files": [],
|
|
45
|
+
"legitimate": true,
|
|
46
|
+
"reasoning": "No test files were modified by the implementer"
|
|
47
|
+
},
|
|
48
|
+
"acceptanceCriteria": {
|
|
49
|
+
"allMet": true,
|
|
50
|
+
"criteria": [
|
|
51
|
+
{ "criterion": "Example criterion", "met": true }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"quality": {
|
|
55
|
+
"rating": "good",
|
|
56
|
+
"issues": []
|
|
57
|
+
},
|
|
58
|
+
"fixes": [],
|
|
59
|
+
"reasoning": "All tests pass, implementation is clean, all acceptance criteria are met."
|
|
60
|
+
}
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
**Field notes:**
|
|
64
|
+
- \`quality.rating\` must be one of: \`"good"\`, \`"acceptable"\`, \`"poor"\`
|
|
65
|
+
- \`testModifications.files\` — list any test files the implementer changed
|
|
66
|
+
- \`fixes\` — list any fixes you applied yourself during this verification session
|
|
67
|
+
- \`reasoning\` — brief summary of your overall assessment
|
|
68
|
+
|
|
69
|
+
When done, commit any fixes with message: "fix: verify and adjust ${story.title}"`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptBuilder Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for the unified prompt building system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Role determining which default template body to use */
|
|
8
|
+
export type PromptRole = "test-writer" | "implementer" | "verifier" | "single-session";
|
|
9
|
+
|
|
10
|
+
/** A single section of a composed prompt */
|
|
11
|
+
export interface PromptSection {
|
|
12
|
+
/** Unique section identifier */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Section content */
|
|
15
|
+
content: string;
|
|
16
|
+
/** Whether this section can be removed by user override */
|
|
17
|
+
overridable: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Options passed to PromptBuilder.for() */
|
|
21
|
+
export type PromptOptions = Record<string, unknown>;
|
|
@@ -9,15 +9,9 @@ import type { ModelTier, NaxConfig } from "../config";
|
|
|
9
9
|
import { resolveModel } from "../config";
|
|
10
10
|
import { getLogger } from "../logger";
|
|
11
11
|
import type { UserStory } from "../prd";
|
|
12
|
+
import { PromptBuilder } from "../prompts";
|
|
12
13
|
import { cleanupProcessTree } from "./cleanup";
|
|
13
14
|
import { getChangedFiles, verifyImplementerIsolation, verifyTestWriterIsolation } from "./isolation";
|
|
14
|
-
import {
|
|
15
|
-
buildImplementerLitePrompt,
|
|
16
|
-
buildImplementerPrompt,
|
|
17
|
-
buildTestWriterLitePrompt,
|
|
18
|
-
buildTestWriterPrompt,
|
|
19
|
-
buildVerifierPrompt,
|
|
20
|
-
} from "./prompts";
|
|
21
15
|
import type { IsolationCheck } from "./types";
|
|
22
16
|
import type { TddSessionResult, TddSessionRole } from "./types";
|
|
23
17
|
|
|
@@ -95,15 +89,21 @@ export async function runTddSession(
|
|
|
95
89
|
let prompt: string;
|
|
96
90
|
switch (role) {
|
|
97
91
|
case "test-writer":
|
|
98
|
-
prompt =
|
|
92
|
+
prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" })
|
|
93
|
+
.withLoader(workdir, config)
|
|
94
|
+
.story(story)
|
|
95
|
+
.context(contextMarkdown)
|
|
96
|
+
.build();
|
|
99
97
|
break;
|
|
100
98
|
case "implementer":
|
|
101
|
-
prompt = lite
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" })
|
|
100
|
+
.withLoader(workdir, config)
|
|
101
|
+
.story(story)
|
|
102
|
+
.context(contextMarkdown)
|
|
103
|
+
.build();
|
|
104
104
|
break;
|
|
105
105
|
case "verifier":
|
|
106
|
-
prompt =
|
|
106
|
+
prompt = await PromptBuilder.for("verifier").withLoader(workdir, config).story(story).build();
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for `nax config --explain` prompts section (PB-005)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the prompts.overrides config block is documented in the
|
|
5
|
+
* --explain output with example paths.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { configCommand } from "../../../src/cli/config";
|
|
13
|
+
import { loadConfig } from "../../../src/config/loader";
|
|
14
|
+
|
|
15
|
+
describe("config --explain: prompts section", () => {
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
let originalCwd: string;
|
|
18
|
+
let consoleOutput: string[];
|
|
19
|
+
let originalConsoleLog: typeof console.log;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = mkdtempSync(join(tmpdir(), "nax-config-prompts-test-"));
|
|
23
|
+
originalCwd = process.cwd();
|
|
24
|
+
|
|
25
|
+
consoleOutput = [];
|
|
26
|
+
originalConsoleLog = console.log;
|
|
27
|
+
console.log = (...args: unknown[]) => {
|
|
28
|
+
consoleOutput.push(args.map((a) => String(a)).join(" "));
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
console.log = originalConsoleLog;
|
|
34
|
+
process.chdir(originalCwd);
|
|
35
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("explain output includes a prompts section", async () => {
|
|
39
|
+
const config = await loadConfig(tempDir);
|
|
40
|
+
await configCommand(config, { explain: true });
|
|
41
|
+
|
|
42
|
+
const output = consoleOutput.join("\n");
|
|
43
|
+
expect(output).toContain("prompts");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("explain output documents prompts.overrides", async () => {
|
|
47
|
+
const config = await loadConfig(tempDir);
|
|
48
|
+
await configCommand(config, { explain: true });
|
|
49
|
+
|
|
50
|
+
const output = consoleOutput.join("\n");
|
|
51
|
+
expect(output).toContain("prompts.overrides");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("explain output includes example path .nax/prompts/test-writer.md", async () => {
|
|
55
|
+
const config = await loadConfig(tempDir);
|
|
56
|
+
await configCommand(config, { explain: true });
|
|
57
|
+
|
|
58
|
+
const output = consoleOutput.join("\n");
|
|
59
|
+
expect(output).toContain(".nax/prompts/test-writer.md");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("explain output mentions override roles (test-writer, implementer, verifier)", async () => {
|
|
63
|
+
const config = await loadConfig(tempDir);
|
|
64
|
+
await configCommand(config, { explain: true });
|
|
65
|
+
|
|
66
|
+
const output = consoleOutput.join("\n");
|
|
67
|
+
// At least one of the roles should appear in the prompts documentation
|
|
68
|
+
const mentionsRole =
|
|
69
|
+
output.includes("test-writer") ||
|
|
70
|
+
output.includes("implementer") ||
|
|
71
|
+
output.includes("verifier");
|
|
72
|
+
expect(mentionsRole).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|