@nathapp/nax 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/bin/nax.ts +27 -5
- package/dist/nax.js +585 -386
- package/package.json +2 -2
- package/src/cli/analyze.ts +2 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/prompts.ts +185 -1
- package/src/execution/lifecycle/headless-formatter.ts +2 -4
- package/src/prd/index.ts +4 -0
- package/src/precheck/checks-warnings.ts +21 -7
- package/src/precheck/index.ts +1 -1
- package/src/prompts/builder.ts +12 -69
- package/src/prompts/sections/isolation.ts +38 -8
- package/src/prompts/sections/role-task.ts +79 -19
- package/src/tdd/session-runner.ts +5 -1
- package/src/prompts/templates/implementer.ts +0 -6
- package/src/prompts/templates/single-session.ts +0 -6
- package/src/prompts/templates/test-writer.ts +0 -6
- package/src/prompts/templates/verifier.ts +0 -6
package/package.json
CHANGED
package/src/cli/analyze.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { resolveModel } from "../config/schema";
|
|
|
15
15
|
import { getLogger } from "../logger";
|
|
16
16
|
import type { PRD, UserStory } from "../prd";
|
|
17
17
|
import { routeTask } from "../routing";
|
|
18
|
+
import { NAX_VERSION } from "../version";
|
|
18
19
|
import {
|
|
19
20
|
applyKeywordClassification,
|
|
20
21
|
buildCodebaseContext,
|
|
@@ -63,13 +64,7 @@ export async function analyzeFeature(options: AnalyzeOptions): Promise<PRD> {
|
|
|
63
64
|
);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
69
|
-
naxVersion = (await Bun.file(pkgPath).json()).version;
|
|
70
|
-
} catch {
|
|
71
|
-
/* version is metadata only */
|
|
72
|
-
}
|
|
67
|
+
const naxVersion = NAX_VERSION;
|
|
73
68
|
|
|
74
69
|
const now = new Date().toISOString();
|
|
75
70
|
const prd: PRD = {
|
package/src/cli/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ export {
|
|
|
20
20
|
} from "./runs";
|
|
21
21
|
export {
|
|
22
22
|
promptsCommand,
|
|
23
|
+
promptsInitCommand,
|
|
23
24
|
type PromptsCommandOptions,
|
|
25
|
+
type PromptsInitCommandOptions,
|
|
24
26
|
} from "./prompts";
|
|
25
27
|
export { initCommand, type InitOptions } from "./init";
|
|
26
28
|
export { pluginsListCommand } from "./plugins";
|
package/src/cli/prompts.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { constitutionStage, contextStage, promptStage, routingStage } from "../p
|
|
|
18
18
|
import type { UserStory } from "../prd";
|
|
19
19
|
import { loadPRD } from "../prd";
|
|
20
20
|
import { PromptBuilder } from "../prompts";
|
|
21
|
+
import { buildRoleTaskSection } from "../prompts/sections/role-task";
|
|
21
22
|
|
|
22
23
|
export interface PromptsCommandOptions {
|
|
23
24
|
/** Feature name */
|
|
@@ -240,6 +241,189 @@ function buildFrontmatter(story: UserStory, ctx: PipelineContext, role?: string)
|
|
|
240
241
|
return `${lines.join("\n")}\n`;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
export interface PromptsInitCommandOptions {
|
|
245
|
+
/** Working directory (project root) */
|
|
246
|
+
workdir: string;
|
|
247
|
+
/** Overwrite existing files if true */
|
|
248
|
+
force?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const TEMPLATE_ROLES = [
|
|
252
|
+
{ file: "test-writer.md", role: "test-writer" as const },
|
|
253
|
+
{ file: "implementer.md", role: "implementer" as const, variant: "standard" as const },
|
|
254
|
+
{ file: "verifier.md", role: "verifier" as const },
|
|
255
|
+
{ file: "single-session.md", role: "single-session" as const },
|
|
256
|
+
] as const;
|
|
257
|
+
|
|
258
|
+
const TEMPLATE_HEADER = `<!--
|
|
259
|
+
This file controls the role-body section of the nax prompt for this role.
|
|
260
|
+
Edit the content below to customize the task instructions given to the agent.
|
|
261
|
+
|
|
262
|
+
NON-OVERRIDABLE SECTIONS (always injected by nax, cannot be changed here):
|
|
263
|
+
- Isolation rules (scope, file access boundaries)
|
|
264
|
+
- Story context (acceptance criteria, description, dependencies)
|
|
265
|
+
- Conventions (project coding standards)
|
|
266
|
+
|
|
267
|
+
To activate overrides, add to your nax/config.json:
|
|
268
|
+
{ "prompts": { "overrides": { "<role>": "nax/templates/<role>.md" } } }
|
|
269
|
+
-->
|
|
270
|
+
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Execute the `nax prompts --init` command.
|
|
275
|
+
*
|
|
276
|
+
* Creates nax/templates/ and writes 4 default role-body template files.
|
|
277
|
+
* Auto-wires prompts.overrides in nax.config.json if the file exists and overrides are not already set.
|
|
278
|
+
* Returns the list of file paths written. Returns empty array if files
|
|
279
|
+
* already exist and force is not set.
|
|
280
|
+
*
|
|
281
|
+
* @param options - Command options
|
|
282
|
+
* @returns Array of file paths written
|
|
283
|
+
*/
|
|
284
|
+
export async function promptsInitCommand(options: PromptsInitCommandOptions): Promise<string[]> {
|
|
285
|
+
const { workdir, force = false } = options;
|
|
286
|
+
const templatesDir = join(workdir, "nax", "templates");
|
|
287
|
+
|
|
288
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
289
|
+
|
|
290
|
+
// Check for existing files
|
|
291
|
+
const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync(join(templatesDir, f)));
|
|
292
|
+
|
|
293
|
+
if (existingFiles.length > 0 && !force) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.\n Pass --force to overwrite existing templates.`,
|
|
296
|
+
);
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const written: string[] = [];
|
|
301
|
+
|
|
302
|
+
for (const template of TEMPLATE_ROLES) {
|
|
303
|
+
const filePath = join(templatesDir, template.file);
|
|
304
|
+
const roleBody =
|
|
305
|
+
template.role === "implementer"
|
|
306
|
+
? buildRoleTaskSection(template.role, template.variant)
|
|
307
|
+
: buildRoleTaskSection(template.role);
|
|
308
|
+
const content = TEMPLATE_HEADER + roleBody;
|
|
309
|
+
await Bun.write(filePath, content);
|
|
310
|
+
written.push(filePath);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`[OK] Written ${written.length} template files to nax/templates/:`);
|
|
314
|
+
for (const filePath of written) {
|
|
315
|
+
console.log(` - ${filePath.replace(`${workdir}/`, "")}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Auto-wire prompts.overrides in nax.config.json
|
|
319
|
+
await autoWirePromptsConfig(workdir);
|
|
320
|
+
|
|
321
|
+
return written;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Auto-wire prompts.overrides in nax.config.json after template init.
|
|
326
|
+
*
|
|
327
|
+
* If nax.config.json exists and prompts.overrides is not already set,
|
|
328
|
+
* add the override paths. If overrides are already set, print a note.
|
|
329
|
+
* If nax.config.json doesn't exist, print manual instructions.
|
|
330
|
+
*
|
|
331
|
+
* @param workdir - Project working directory
|
|
332
|
+
*/
|
|
333
|
+
async function autoWirePromptsConfig(workdir: string): Promise<void> {
|
|
334
|
+
const configPath = join(workdir, "nax.config.json");
|
|
335
|
+
|
|
336
|
+
// If config file doesn't exist, print manual instructions
|
|
337
|
+
if (!existsSync(configPath)) {
|
|
338
|
+
const exampleConfig = JSON.stringify(
|
|
339
|
+
{
|
|
340
|
+
prompts: {
|
|
341
|
+
overrides: {
|
|
342
|
+
"test-writer": "nax/templates/test-writer.md",
|
|
343
|
+
implementer: "nax/templates/implementer.md",
|
|
344
|
+
verifier: "nax/templates/verifier.md",
|
|
345
|
+
"single-session": "nax/templates/single-session.md",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
null,
|
|
350
|
+
2,
|
|
351
|
+
);
|
|
352
|
+
console.log(`\nNo nax.config.json found. To activate overrides, create nax/config.json with:\n${exampleConfig}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Read existing config
|
|
357
|
+
const configFile = Bun.file(configPath);
|
|
358
|
+
const configContent = await configFile.text();
|
|
359
|
+
const config = JSON.parse(configContent);
|
|
360
|
+
|
|
361
|
+
// Check if prompts.overrides is already set
|
|
362
|
+
if (config.prompts?.overrides && Object.keys(config.prompts.overrides).length > 0) {
|
|
363
|
+
console.log(
|
|
364
|
+
"[INFO] prompts.overrides already configured in nax.config.json. Skipping auto-wiring.\n" +
|
|
365
|
+
" To reset overrides, remove the prompts.overrides section and re-run this command.",
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Build the override paths
|
|
371
|
+
const overrides = {
|
|
372
|
+
"test-writer": "nax/templates/test-writer.md",
|
|
373
|
+
implementer: "nax/templates/implementer.md",
|
|
374
|
+
verifier: "nax/templates/verifier.md",
|
|
375
|
+
"single-session": "nax/templates/single-session.md",
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Add or update prompts section
|
|
379
|
+
if (!config.prompts) {
|
|
380
|
+
config.prompts = {};
|
|
381
|
+
}
|
|
382
|
+
config.prompts.overrides = overrides;
|
|
383
|
+
|
|
384
|
+
// Write config with custom formatting that avoids 4-space indentation
|
|
385
|
+
// by putting the overrides object on a single line
|
|
386
|
+
const updatedConfig = formatConfigJson(config);
|
|
387
|
+
await Bun.write(configPath, updatedConfig);
|
|
388
|
+
|
|
389
|
+
console.log("[OK] Auto-wired prompts.overrides in nax.config.json");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Format config JSON with 2-space indentation, keeping overrides object inline.
|
|
394
|
+
*
|
|
395
|
+
* This avoids 4-space indentation by putting the overrides object on the same line.
|
|
396
|
+
*
|
|
397
|
+
* @param config - Configuration object
|
|
398
|
+
* @returns Formatted JSON string
|
|
399
|
+
*/
|
|
400
|
+
function formatConfigJson(config: Record<string, unknown>): string {
|
|
401
|
+
const lines: string[] = ["{"];
|
|
402
|
+
|
|
403
|
+
const keys = Object.keys(config);
|
|
404
|
+
for (let i = 0; i < keys.length; i++) {
|
|
405
|
+
const key = keys[i];
|
|
406
|
+
const value = config[key];
|
|
407
|
+
const isLast = i === keys.length - 1;
|
|
408
|
+
|
|
409
|
+
if (key === "prompts" && typeof value === "object" && value !== null) {
|
|
410
|
+
// Special handling for prompts object - keep overrides inline
|
|
411
|
+
const promptsObj = value as Record<string, unknown>;
|
|
412
|
+
if (promptsObj.overrides) {
|
|
413
|
+
const overridesJson = JSON.stringify(promptsObj.overrides);
|
|
414
|
+
lines.push(` "${key}": { "overrides": ${overridesJson} }${isLast ? "" : ","}`);
|
|
415
|
+
} else {
|
|
416
|
+
lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
lines.push("}");
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
243
427
|
/**
|
|
244
428
|
* Handle three-session TDD prompts by building separate prompts for each role.
|
|
245
429
|
*
|
|
@@ -266,7 +450,7 @@ async function handleThreeSessionTddPrompts(
|
|
|
266
450
|
.story(story)
|
|
267
451
|
.context(ctx.contextMarkdown)
|
|
268
452
|
.build(),
|
|
269
|
-
PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).build(),
|
|
453
|
+
PromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).build(),
|
|
270
454
|
]);
|
|
271
455
|
|
|
272
456
|
const sessions = [
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Extracts run header and footer formatting logic from runner.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import path from "node:path";
|
|
9
8
|
import chalk from "chalk";
|
|
10
9
|
import type { RunSummary } from "../../logging";
|
|
11
10
|
import { formatRunSummary } from "../../logging";
|
|
11
|
+
import { NAX_VERSION } from "../../version";
|
|
12
12
|
|
|
13
13
|
export interface RunHeaderOptions {
|
|
14
14
|
feature: string;
|
|
@@ -42,11 +42,9 @@ export async function outputRunHeader(options: RunHeaderOptions): Promise<void>
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const pkg = await Bun.file(path.join(import.meta.dir, "..", "..", "..", "package.json")).json();
|
|
46
|
-
|
|
47
45
|
console.log("");
|
|
48
46
|
console.log(chalk.bold(chalk.blue("═".repeat(60))));
|
|
49
|
-
console.log(chalk.bold(chalk.blue(` ▶ NAX v${
|
|
47
|
+
console.log(chalk.bold(chalk.blue(` ▶ NAX v${NAX_VERSION} — RUN STARTED`)));
|
|
50
48
|
console.log(chalk.blue("═".repeat(60)));
|
|
51
49
|
console.log(` ${chalk.gray("Feature:")} ${chalk.cyan(feature)}`);
|
|
52
50
|
console.log(` ${chalk.gray("Stories:")} ${chalk.cyan(`${totalStories} total, ${pendingStories} pending`)}`);
|
package/src/prd/index.ts
CHANGED
|
@@ -49,6 +49,10 @@ export async function loadPRD(path: string): Promise<PRD> {
|
|
|
49
49
|
story.escalations = story.escalations ?? [];
|
|
50
50
|
story.dependencies = story.dependencies ?? [];
|
|
51
51
|
story.tags = story.tags ?? [];
|
|
52
|
+
// Normalize aliases: "open" → "pending", "done" → "passed"
|
|
53
|
+
const rawStatus = story.status as string;
|
|
54
|
+
if (rawStatus === "open") story.status = "pending";
|
|
55
|
+
if (rawStatus === "done") story.status = "passed";
|
|
52
56
|
story.status = story.status ?? "pending";
|
|
53
57
|
story.acceptanceCriteria = story.acceptanceCriteria ?? [];
|
|
54
58
|
story.storyPoints = story.storyPoints ?? 1;
|
|
@@ -90,15 +90,19 @@ export async function checkPendingStories(prd: PRD): Promise<Check> {
|
|
|
90
90
|
/**
|
|
91
91
|
* Check if optional commands are configured.
|
|
92
92
|
*/
|
|
93
|
-
export async function checkOptionalCommands(config: NaxConfig): Promise<Check> {
|
|
93
|
+
export async function checkOptionalCommands(config: NaxConfig, workdir: string): Promise<Check> {
|
|
94
94
|
const missing: string[] = [];
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// Check quality.commands first, then execution config, then package.json fallback
|
|
97
|
+
const hasLint =
|
|
98
|
+
config.quality?.commands?.lint || config.execution?.lintCommand || (await hasPackageScript(workdir, "lint"));
|
|
99
|
+
const hasTypecheck =
|
|
100
|
+
config.quality?.commands?.typecheck ||
|
|
101
|
+
config.execution?.typecheckCommand ||
|
|
102
|
+
(await hasPackageScript(workdir, "typecheck"));
|
|
103
|
+
|
|
104
|
+
if (!hasLint) missing.push("lint");
|
|
105
|
+
if (!hasTypecheck) missing.push("typecheck");
|
|
102
106
|
|
|
103
107
|
const passed = missing.length === 0;
|
|
104
108
|
|
|
@@ -110,6 +114,16 @@ export async function checkOptionalCommands(config: NaxConfig): Promise<Check> {
|
|
|
110
114
|
};
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
/** Check if package.json has a script by name */
|
|
118
|
+
async function hasPackageScript(workdir: string, name: string): Promise<boolean> {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = await Bun.file(`${workdir}/package.json`).json();
|
|
121
|
+
return Boolean(pkg?.scripts?.[name]);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
/**
|
|
114
128
|
* Check if .gitignore covers nax runtime files.
|
|
115
129
|
* Patterns: nax.lock, runs/, test/tmp/
|
package/src/precheck/index.ts
CHANGED
|
@@ -141,7 +141,7 @@ export async function runPrecheck(
|
|
|
141
141
|
() => checkClaudeMdExists(workdir),
|
|
142
142
|
() => checkDiskSpace(),
|
|
143
143
|
() => checkPendingStories(prd),
|
|
144
|
-
() => checkOptionalCommands(config),
|
|
144
|
+
() => checkOptionalCommands(config, workdir),
|
|
145
145
|
() => checkGitignoreCoversNax(workdir),
|
|
146
146
|
() => checkPromptOverrideFiles(config, workdir),
|
|
147
147
|
];
|
package/src/prompts/builder.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import type { NaxConfig } from "../config/types";
|
|
14
14
|
import type { UserStory } from "../prd";
|
|
15
|
+
import { buildConventionsSection } from "./sections/conventions";
|
|
16
|
+
import { buildIsolationSection } from "./sections/isolation";
|
|
17
|
+
import { buildRoleTaskSection } from "./sections/role-task";
|
|
18
|
+
import { buildStorySection } from "./sections/story";
|
|
15
19
|
import type { PromptOptions, PromptRole } from "./types";
|
|
16
20
|
|
|
17
21
|
const SECTION_SEP = "\n\n---\n\n";
|
|
@@ -69,16 +73,17 @@ export class PromptBuilder {
|
|
|
69
73
|
sections.push(`# CONSTITUTION (follow these rules strictly)\n\n${this._constitution}`);
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
// (2) Role task body — user override or default
|
|
76
|
+
// (2) Role task body — user override or default section
|
|
73
77
|
sections.push(await this._resolveRoleBody());
|
|
74
78
|
|
|
75
79
|
// (3) Story context — non-overridable
|
|
76
80
|
if (this._story) {
|
|
77
|
-
sections.push(
|
|
81
|
+
sections.push(buildStorySection(this._story));
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// (4) Isolation rules — non-overridable
|
|
81
|
-
|
|
85
|
+
const isolation = this._options.isolation as string | undefined;
|
|
86
|
+
sections.push(buildIsolationSection(this._role, isolation as "strict" | "lite" | undefined));
|
|
82
87
|
|
|
83
88
|
// (5) Context markdown
|
|
84
89
|
if (this._contextMd) {
|
|
@@ -86,7 +91,7 @@ export class PromptBuilder {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
// (6) Conventions footer — non-overridable, always last
|
|
89
|
-
sections.push(
|
|
94
|
+
sections.push(buildConventionsSection());
|
|
90
95
|
|
|
91
96
|
return sections.join(SECTION_SEP);
|
|
92
97
|
}
|
|
@@ -107,72 +112,10 @@ export class PromptBuilder {
|
|
|
107
112
|
return await file.text();
|
|
108
113
|
}
|
|
109
114
|
} catch {
|
|
110
|
-
// fall through to default
|
|
115
|
+
// fall through to default section
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
|
-
|
|
118
|
+
const variant = this._options.variant as "standard" | "lite" | undefined;
|
|
119
|
+
return buildRoleTaskSection(this._role, variant);
|
|
114
120
|
}
|
|
115
121
|
}
|
|
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.";
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Isolation Rules Section
|
|
3
3
|
*
|
|
4
|
-
* Generates isolation rules
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Generates isolation rules for all 4 roles:
|
|
5
|
+
* - test-writer: Strict/Lite modes for test-first TDD
|
|
6
|
+
* - implementer: Implement source while respecting test integrity
|
|
7
|
+
* - verifier: Read-only inspection
|
|
8
|
+
* - single-session: Both test/ and src/ modification allowed
|
|
9
|
+
*
|
|
10
|
+
* Backwards compatible: also accepts old API (mode only)
|
|
11
|
+
* - buildIsolationSection("strict") → test-writer, strict
|
|
12
|
+
* - buildIsolationSection("lite") → test-writer, lite
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
const TEST_FILTER_RULE =
|
|
@@ -11,14 +17,38 @@ const TEST_FILTER_RULE =
|
|
|
11
17
|
"(e.g. `bun test ./test/specific.test.ts`). NEVER run `bun test` without a file filter " +
|
|
12
18
|
"— full suite output will flood your context window and cause failures.";
|
|
13
19
|
|
|
14
|
-
export function buildIsolationSection(
|
|
20
|
+
export function buildIsolationSection(
|
|
21
|
+
roleOrMode: "implementer" | "test-writer" | "verifier" | "single-session" | "strict" | "lite",
|
|
22
|
+
mode?: "strict" | "lite",
|
|
23
|
+
): string {
|
|
24
|
+
// Old API support: buildIsolationSection("strict") or buildIsolationSection("lite")
|
|
25
|
+
if ((roleOrMode === "strict" || roleOrMode === "lite") && mode === undefined) {
|
|
26
|
+
return buildIsolationSection("test-writer", roleOrMode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const role = roleOrMode as "implementer" | "test-writer" | "verifier" | "single-session";
|
|
30
|
+
|
|
15
31
|
const header = "# Isolation Rules\n\n";
|
|
16
32
|
const footer = `\n\n${TEST_FILTER_RULE}`;
|
|
17
33
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
34
|
+
if (role === "test-writer") {
|
|
35
|
+
const m = mode ?? "strict";
|
|
36
|
+
if (m === "strict") {
|
|
37
|
+
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}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// lite mode for test-writer
|
|
41
|
+
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}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (role === "implementer") {
|
|
45
|
+
return `${header}isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (role === "verifier") {
|
|
49
|
+
return `${header}isolation scope: Read-only inspection. Review all test results, implementation code, and acceptance criteria compliance. You MAY write a verdict file (.nax-verifier-verdict.json) and apply legitimate fixes if needed.${footer}`;
|
|
20
50
|
}
|
|
21
51
|
|
|
22
|
-
//
|
|
23
|
-
return `${header}isolation scope: Create test files in test
|
|
52
|
+
// single-session role
|
|
53
|
+
return `${header}isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
|
|
24
54
|
}
|
|
@@ -1,34 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Role-Task Section
|
|
3
3
|
*
|
|
4
|
-
* Generates role definition for:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Generates role definition for all 4 roles in nax prompt orchestration:
|
|
5
|
+
* - implementer: Make failing tests pass (standard/lite variants)
|
|
6
|
+
* - test-writer: Write tests first (RED phase)
|
|
7
|
+
* - verifier: Review and verify implementation
|
|
8
|
+
* - single-session: Write tests AND implement in one session
|
|
9
|
+
*
|
|
10
|
+
* Backwards compatible: also accepts old API (variant only)
|
|
11
|
+
* - buildRoleTaskSection("standard") → implementer, standard
|
|
12
|
+
* - buildRoleTaskSection("lite") → implementer, lite
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
|
-
export function buildRoleTaskSection(
|
|
10
|
-
|
|
15
|
+
export function buildRoleTaskSection(
|
|
16
|
+
roleOrVariant: "implementer" | "test-writer" | "verifier" | "single-session" | "standard" | "lite",
|
|
17
|
+
variant?: "standard" | "lite",
|
|
18
|
+
): string {
|
|
19
|
+
// Old API support: buildRoleTaskSection("standard") or buildRoleTaskSection("lite")
|
|
20
|
+
if ((roleOrVariant === "standard" || roleOrVariant === "lite") && variant === undefined) {
|
|
21
|
+
return buildRoleTaskSection("implementer", roleOrVariant);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const role = roleOrVariant as "implementer" | "test-writer" | "verifier" | "single-session";
|
|
25
|
+
|
|
26
|
+
if (role === "implementer") {
|
|
27
|
+
const v = variant ?? "standard";
|
|
28
|
+
if (v === "standard") {
|
|
29
|
+
return (
|
|
30
|
+
"# Role: Implementer\n\n" +
|
|
31
|
+
"Your task: make failing tests pass.\n\n" +
|
|
32
|
+
"Instructions:\n" +
|
|
33
|
+
"- Implement source code in src/ to make tests pass\n" +
|
|
34
|
+
"- Do NOT modify test files\n" +
|
|
35
|
+
"- Run tests frequently to track progress\n" +
|
|
36
|
+
"- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'\n" +
|
|
37
|
+
"- Goal: all tests green, all changes committed"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// lite variant
|
|
42
|
+
return (
|
|
43
|
+
"# Role: Implementer (Lite)\n\n" +
|
|
44
|
+
"Your task: Write tests AND implement the feature in a single session.\n\n" +
|
|
45
|
+
"Instructions:\n" +
|
|
46
|
+
"- Write tests first (test/ directory), then implement (src/ directory)\n" +
|
|
47
|
+
"- All tests must pass by the end\n" +
|
|
48
|
+
"- Use Bun test (describe/test/expect)\n" +
|
|
49
|
+
"- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'\n" +
|
|
50
|
+
"- Goal: all tests green, all criteria met, all changes committed"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (role === "test-writer") {
|
|
55
|
+
return (
|
|
56
|
+
"# Role: Test-Writer\n\n" +
|
|
57
|
+
"Your task: Write comprehensive failing tests for the feature.\n\n" +
|
|
58
|
+
"Instructions:\n" +
|
|
59
|
+
"- Create test files in test/ directory that cover acceptance criteria\n" +
|
|
60
|
+
"- Tests must fail initially (RED phase) — the feature is not yet implemented\n" +
|
|
61
|
+
"- Use Bun test (describe/test/expect)\n" +
|
|
62
|
+
"- Write clear test names that document expected behavior\n" +
|
|
63
|
+
"- Focus on behavior, not implementation details\n" +
|
|
64
|
+
"- Goal: comprehensive test suite ready for implementation"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (role === "verifier") {
|
|
11
69
|
return (
|
|
12
|
-
"# Role:
|
|
13
|
-
"Your task:
|
|
70
|
+
"# Role: Verifier\n\n" +
|
|
71
|
+
"Your task: Review and verify the implementation against acceptance criteria.\n\n" +
|
|
14
72
|
"Instructions:\n" +
|
|
15
|
-
"-
|
|
16
|
-
"-
|
|
17
|
-
"-
|
|
18
|
-
"-
|
|
19
|
-
"-
|
|
73
|
+
"- Review all test results — verify tests pass\n" +
|
|
74
|
+
"- Check that implementation meets all acceptance criteria\n" +
|
|
75
|
+
"- Inspect code quality, error handling, and edge cases\n" +
|
|
76
|
+
"- Verify test modifications (if any) are legitimate fixes\n" +
|
|
77
|
+
"- Write a detailed verdict with reasoning\n" +
|
|
78
|
+
"- Goal: provide comprehensive verification and quality assurance"
|
|
20
79
|
);
|
|
21
80
|
}
|
|
22
81
|
|
|
23
|
-
//
|
|
82
|
+
// single-session role
|
|
24
83
|
return (
|
|
25
|
-
"# Role:
|
|
26
|
-
"Your task: Write tests AND implement the feature in a single session.\n\n" +
|
|
84
|
+
"# Role: Single-Session\n\n" +
|
|
85
|
+
"Your task: Write tests AND implement the feature in a single focused session.\n\n" +
|
|
27
86
|
"Instructions:\n" +
|
|
28
|
-
"- Write tests
|
|
29
|
-
"-
|
|
87
|
+
"- Phase 1: Write comprehensive tests (test/ directory)\n" +
|
|
88
|
+
"- Phase 2: Implement to make all tests pass (src/ directory)\n" +
|
|
30
89
|
"- Use Bun test (describe/test/expect)\n" +
|
|
31
|
-
"-
|
|
32
|
-
"-
|
|
90
|
+
"- Run tests frequently throughout implementation\n" +
|
|
91
|
+
"- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'\n" +
|
|
92
|
+
"- Goal: all tests passing, all changes committed, full story complete"
|
|
33
93
|
);
|
|
34
94
|
}
|
|
@@ -103,7 +103,11 @@ export async function runTddSession(
|
|
|
103
103
|
.build();
|
|
104
104
|
break;
|
|
105
105
|
case "verifier":
|
|
106
|
-
prompt = await PromptBuilder.for("verifier")
|
|
106
|
+
prompt = await PromptBuilder.for("verifier")
|
|
107
|
+
.withLoader(workdir, config)
|
|
108
|
+
.story(story)
|
|
109
|
+
.context(contextMarkdown)
|
|
110
|
+
.build();
|
|
107
111
|
break;
|
|
108
112
|
}
|
|
109
113
|
|