@nathapp/nax 0.27.0 → 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/CLAUDE.md +38 -8
- package/docs/ROADMAP.md +42 -17
- 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/runner.ts +2 -1
- package/src/pipeline/stages/autofix.ts +5 -0
- package/src/pipeline/stages/execution.ts +5 -0
- package/src/pipeline/stages/prompt.ts +13 -4
- package/src/pipeline/stages/rectify.ts +5 -0
- package/src/pipeline/stages/regression.ts +6 -1
- package/src/pipeline/stages/verify.ts +2 -1
- package/src/pipeline/types.ts +9 -0
- 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/orchestrator.ts +11 -1
- package/src/tdd/rectification-gate.ts +18 -13
- package/src/tdd/session-runner.ts +12 -12
- package/src/tdd/types.ts +2 -0
- 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
|
@@ -26,12 +26,17 @@ export const regressionStage: PipelineStage = {
|
|
|
26
26
|
const mode = ctx.config.execution.regressionGate?.mode ?? "deferred";
|
|
27
27
|
if (mode !== "per-story") return false;
|
|
28
28
|
// Only run when verify passed (or was skipped/not set)
|
|
29
|
-
// Only run when verify passed (or was skipped/not set)
|
|
30
29
|
if (ctx.verifyResult && !ctx.verifyResult.success) return false;
|
|
31
30
|
const gateEnabled = ctx.config.execution.regressionGate?.enabled ?? true;
|
|
32
31
|
return gateEnabled;
|
|
33
32
|
},
|
|
34
33
|
|
|
34
|
+
skipReason(ctx: PipelineContext): string {
|
|
35
|
+
const mode = ctx.config.execution.regressionGate?.mode ?? "deferred";
|
|
36
|
+
if (mode !== "per-story") return `not needed (regression mode is '${mode}', not 'per-story')`;
|
|
37
|
+
return "disabled (regression gate not enabled in config)";
|
|
38
|
+
},
|
|
39
|
+
|
|
35
40
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
36
41
|
const logger = getLogger();
|
|
37
42
|
const testCommand = ctx.config.review?.commands?.test ?? ctx.config.quality.commands.test ?? "bun test";
|
|
@@ -45,7 +45,8 @@ function buildScopedCommand(testFiles: string[], baseCommand: string, testScoped
|
|
|
45
45
|
|
|
46
46
|
export const verifyStage: PipelineStage = {
|
|
47
47
|
name: "verify",
|
|
48
|
-
enabled: () =>
|
|
48
|
+
enabled: (ctx: PipelineContext) => !ctx.fullSuiteGatePassed,
|
|
49
|
+
skipReason: () => "not needed (full-suite gate already passed)",
|
|
49
50
|
|
|
50
51
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
51
52
|
const logger = getLogger();
|
package/src/pipeline/types.ts
CHANGED
|
@@ -108,6 +108,8 @@ export interface PipelineContext {
|
|
|
108
108
|
retryAsLite?: boolean;
|
|
109
109
|
/** Failure category from TDD orchestrator (set by executionStage on TDD failure) */
|
|
110
110
|
tddFailureCategory?: FailureCategory;
|
|
111
|
+
/** Set to true when TDD full-suite gate already passed — verify stage skips to avoid redundant run (BUG-054) */
|
|
112
|
+
fullSuiteGatePassed?: boolean;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -167,6 +169,13 @@ export interface PipelineStage {
|
|
|
167
169
|
*/
|
|
168
170
|
enabled: (ctx: PipelineContext) => boolean;
|
|
169
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Optional human-readable reason why the stage was skipped.
|
|
174
|
+
* Distinguishes "not needed" (conditions not met) from "disabled" (config).
|
|
175
|
+
* Used by the pipeline runner for better observability (BUG-055).
|
|
176
|
+
*/
|
|
177
|
+
skipReason?: (ctx: PipelineContext) => string;
|
|
178
|
+
|
|
170
179
|
/**
|
|
171
180
|
* Execute the stage logic.
|
|
172
181
|
*
|
|
@@ -140,3 +140,40 @@ export async function checkGitignoreCoversNax(workdir: string): Promise<Check> {
|
|
|
140
140
|
message: passed ? ".gitignore covers nax runtime files" : `.gitignore missing patterns: ${missing.join(", ")}`,
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if configured prompt override files exist.
|
|
146
|
+
*
|
|
147
|
+
* For each role in config.prompts.overrides, verify the file exists.
|
|
148
|
+
* Emits one warning per missing file (non-blocking).
|
|
149
|
+
* Returns empty array if config.prompts is absent or overrides is empty.
|
|
150
|
+
*
|
|
151
|
+
* @param config - nax configuration
|
|
152
|
+
* @param workdir - working directory for resolving relative paths
|
|
153
|
+
* @returns Array of warning checks (one per missing file)
|
|
154
|
+
*/
|
|
155
|
+
export async function checkPromptOverrideFiles(config: NaxConfig, workdir: string): Promise<Check[]> {
|
|
156
|
+
// Skip if prompts config is absent or overrides is empty
|
|
157
|
+
if (!config.prompts?.overrides || Object.keys(config.prompts.overrides).length === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const checks: Check[] = [];
|
|
162
|
+
|
|
163
|
+
// Check each override file
|
|
164
|
+
for (const [role, relativePath] of Object.entries(config.prompts.overrides)) {
|
|
165
|
+
const resolvedPath = `${workdir}/${relativePath}`;
|
|
166
|
+
const exists = existsSync(resolvedPath);
|
|
167
|
+
|
|
168
|
+
if (!exists) {
|
|
169
|
+
checks.push({
|
|
170
|
+
name: `prompt-override-${role}`,
|
|
171
|
+
tier: "warning",
|
|
172
|
+
passed: false,
|
|
173
|
+
message: `Prompt override file not found for role ${role}: ${resolvedPath}`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return checks;
|
|
179
|
+
}
|
package/src/precheck/checks.ts
CHANGED
package/src/precheck/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
checkOptionalCommands,
|
|
21
21
|
checkPRDValid,
|
|
22
22
|
checkPendingStories,
|
|
23
|
+
checkPromptOverrideFiles,
|
|
23
24
|
checkStaleLock,
|
|
24
25
|
checkTestCommand,
|
|
25
26
|
checkTypecheckCommand,
|
|
@@ -142,19 +143,25 @@ export async function runPrecheck(
|
|
|
142
143
|
() => checkPendingStories(prd),
|
|
143
144
|
() => checkOptionalCommands(config),
|
|
144
145
|
() => checkGitignoreCoversNax(workdir),
|
|
146
|
+
() => checkPromptOverrideFiles(config, workdir),
|
|
145
147
|
];
|
|
146
148
|
|
|
147
149
|
for (const checkFn of tier2Checks) {
|
|
148
150
|
const result = await checkFn();
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
// Handle both single checks and arrays of checks
|
|
153
|
+
const checksToProcess = Array.isArray(result) ? result : [result];
|
|
154
|
+
|
|
155
|
+
for (const check of checksToProcess) {
|
|
156
|
+
if (format === "human") {
|
|
157
|
+
printCheckResult(check);
|
|
158
|
+
}
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
if (check.passed) {
|
|
161
|
+
passed.push(check);
|
|
162
|
+
} else {
|
|
163
|
+
warnings.push(check);
|
|
164
|
+
}
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -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>;
|
package/src/tdd/orchestrator.ts
CHANGED
|
@@ -255,7 +255,16 @@ export async function runThreeSessionTdd(options: ThreeSessionTddOptions): Promi
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// Full-Suite Gate (v0.11 Rectification)
|
|
258
|
-
await runFullSuiteGate(
|
|
258
|
+
const fullSuiteGatePassed = await runFullSuiteGate(
|
|
259
|
+
story,
|
|
260
|
+
config,
|
|
261
|
+
workdir,
|
|
262
|
+
agent,
|
|
263
|
+
implementerTier,
|
|
264
|
+
contextMarkdown,
|
|
265
|
+
lite,
|
|
266
|
+
logger,
|
|
267
|
+
);
|
|
259
268
|
|
|
260
269
|
// Session 3: Verifier
|
|
261
270
|
const session3Ref = (await captureGitRef(workdir)) ?? "HEAD";
|
|
@@ -379,5 +388,6 @@ export async function runThreeSessionTdd(options: ThreeSessionTddOptions): Promi
|
|
|
379
388
|
verdict,
|
|
380
389
|
totalCost,
|
|
381
390
|
lite,
|
|
391
|
+
fullSuiteGatePassed,
|
|
382
392
|
};
|
|
383
393
|
}
|