@nathapp/nax 0.36.0 → 0.36.1
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/dist/nax.js +223 -106
- package/package.json +1 -1
- package/src/agents/claude-decompose.ts +3 -3
- package/src/cli/constitution.ts +0 -92
- package/src/constitution/generator.ts +0 -33
- package/src/constitution/index.ts +2 -1
- package/src/constitution/loader.ts +1 -13
- package/src/context/builder.ts +1 -2
- package/src/context/elements.ts +1 -12
- package/src/context/index.ts +2 -1
- package/src/context/test-scanner.ts +1 -1
- package/src/interaction/chain.ts +17 -1
- package/src/pipeline/stages/execution.ts +23 -1
- package/src/prompts/builder.ts +13 -6
- package/src/prompts/sections/conventions.ts +5 -7
- package/src/prompts/sections/isolation.ts +7 -7
- package/src/prompts/sections/role-task.ts +64 -64
- package/src/review/orchestrator.ts +11 -1
- package/src/routing/strategies/llm-prompts.ts +1 -1
- package/src/routing/strategies/llm.ts +3 -3
- package/src/tdd/index.ts +2 -3
- package/src/tdd/isolation.ts +0 -13
- package/src/tdd/orchestrator.ts +5 -0
- package/src/tdd/prompts.ts +1 -231
- package/src/tdd/session-runner.ts +2 -0
- package/src/tdd/verdict.ts +20 -2
- package/src/verification/parser.ts +0 -10
- package/src/verification/rectification-loop.ts +2 -51
- package/src/worktree/dispatcher.ts +0 -59
package/package.json
CHANGED
|
@@ -130,7 +130,7 @@ export function parseDecomposeOutput(output: string): DecomposedStory[] {
|
|
|
130
130
|
: ["Implementation complete"],
|
|
131
131
|
tags: Array.isArray(record.tags) ? record.tags : [],
|
|
132
132
|
dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
|
|
133
|
-
complexity:
|
|
133
|
+
complexity: coerceComplexity(record.complexity),
|
|
134
134
|
// contextFiles: prefer the new field; fall back to legacy relevantFiles from older LLM responses
|
|
135
135
|
contextFiles: Array.isArray(record.contextFiles)
|
|
136
136
|
? record.contextFiles
|
|
@@ -158,9 +158,9 @@ export function parseDecomposeOutput(output: string): DecomposedStory[] {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* Coerce complexity value from decompose output.
|
|
162
162
|
*/
|
|
163
|
-
export function
|
|
163
|
+
export function coerceComplexity(value: unknown): "simple" | "medium" | "complex" | "expert" {
|
|
164
164
|
if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
|
|
165
165
|
return value;
|
|
166
166
|
}
|
package/src/cli/constitution.ts
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
* Generates agent-specific config files from nax/constitution.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import { generateAll, generateFor } from "../constitution/generator";
|
|
11
|
-
import type { AgentType } from "../constitution/generators/types";
|
|
12
|
-
|
|
13
7
|
/** Constitution generate options */
|
|
14
8
|
export interface ConstitutionGenerateOptions {
|
|
15
9
|
/** Path to constitution file (default: nax/constitution.md) */
|
|
@@ -21,89 +15,3 @@ export interface ConstitutionGenerateOptions {
|
|
|
21
15
|
/** Dry run mode (don't write files) */
|
|
22
16
|
dryRun?: boolean;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Constitution generate command
|
|
27
|
-
*/
|
|
28
|
-
export async function constitutionGenerateCommand(options: ConstitutionGenerateOptions): Promise<void> {
|
|
29
|
-
const workdir = process.cwd();
|
|
30
|
-
const constitutionPath = options.constitution
|
|
31
|
-
? join(workdir, options.constitution)
|
|
32
|
-
: join(workdir, "nax/constitution.md");
|
|
33
|
-
const outputDir = options.output ? join(workdir, options.output) : workdir;
|
|
34
|
-
|
|
35
|
-
// Validate constitution file exists
|
|
36
|
-
if (!existsSync(constitutionPath)) {
|
|
37
|
-
console.error(chalk.red(`[FAIL] Constitution file not found: ${constitutionPath}`));
|
|
38
|
-
console.error(chalk.yellow(`Create ${constitutionPath} first or use --constitution to specify a different path.`));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(chalk.blue(`[OK] Loading constitution from ${constitutionPath}`));
|
|
43
|
-
|
|
44
|
-
// Validate agent type if specified
|
|
45
|
-
const validAgents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
46
|
-
if (options.agent && !validAgents.includes(options.agent as AgentType)) {
|
|
47
|
-
console.error(chalk.red(`[FAIL] Unknown agent type: ${options.agent}`));
|
|
48
|
-
console.error(chalk.yellow(`Valid agents: ${validAgents.join(", ")}`));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const dryRun = options.dryRun ?? false;
|
|
53
|
-
|
|
54
|
-
if (dryRun) {
|
|
55
|
-
console.log(chalk.yellow("[DRY RUN] No files will be written"));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Generate for specific agent or all agents
|
|
60
|
-
if (options.agent) {
|
|
61
|
-
const agent = options.agent as AgentType;
|
|
62
|
-
console.log(chalk.blue(`-> Generating config for ${agent}...`));
|
|
63
|
-
|
|
64
|
-
const result = await generateFor(agent, constitutionPath, outputDir, dryRun);
|
|
65
|
-
|
|
66
|
-
if (result.error) {
|
|
67
|
-
console.error(chalk.red(`[FAIL] ${agent}: ${result.error}`));
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (dryRun) {
|
|
72
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`));
|
|
73
|
-
} else {
|
|
74
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
console.log(chalk.blue("-> Generating configs for all agents..."));
|
|
78
|
-
|
|
79
|
-
const results = await generateAll(constitutionPath, outputDir, dryRun);
|
|
80
|
-
|
|
81
|
-
let errorCount = 0;
|
|
82
|
-
for (const result of results) {
|
|
83
|
-
if (result.error) {
|
|
84
|
-
console.error(chalk.red(`[FAIL] ${result.agent}: ${result.error}`));
|
|
85
|
-
errorCount++;
|
|
86
|
-
} else if (dryRun) {
|
|
87
|
-
console.log(
|
|
88
|
-
chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`),
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
console.log(chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (errorCount > 0) {
|
|
96
|
-
console.error(chalk.red(`[FAIL] ${errorCount} generation(s) failed`));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!dryRun) {
|
|
102
|
-
console.log(chalk.green(`\n[OK] Constitution config(s) generated in ${outputDir}`));
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
106
|
-
console.error(chalk.red(`[FAIL] Generation failed: ${error}`));
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -156,36 +156,3 @@ export async function generateAll(
|
|
|
156
156
|
|
|
157
157
|
return results;
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check if constitution file is newer than generated configs
|
|
162
|
-
*/
|
|
163
|
-
export function isConstitutionNewer(constitutionPath: string, outputDir: string): boolean {
|
|
164
|
-
if (!existsSync(constitutionPath)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const constitutionStat = Bun.file(constitutionPath);
|
|
169
|
-
const constitutionMtime = constitutionStat.lastModified;
|
|
170
|
-
|
|
171
|
-
// Check all generated files
|
|
172
|
-
const agents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
173
|
-
for (const agent of agents) {
|
|
174
|
-
const outputFile = GENERATORS[agent].outputFile;
|
|
175
|
-
const outputPath = join(outputDir, outputFile);
|
|
176
|
-
|
|
177
|
-
if (!existsSync(outputPath)) {
|
|
178
|
-
// If any config file is missing, consider constitution newer
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const outputStat = Bun.file(outputPath);
|
|
183
|
-
const outputMtime = outputStat.lastModified;
|
|
184
|
-
|
|
185
|
-
if (constitutionMtime > outputMtime) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
10
|
-
export { loadConstitution,
|
|
10
|
+
export { loadConstitution, truncateToTokens } from "./loader";
|
|
11
|
+
export { estimateTokens } from "../optimizer/types";
|
|
@@ -8,21 +8,9 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { validateFilePath } from "../config/path-security";
|
|
10
10
|
import { globalConfigDir } from "../config/paths";
|
|
11
|
+
import { estimateTokens } from "../optimizer/types";
|
|
11
12
|
import type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Estimate token count for text
|
|
15
|
-
*
|
|
16
|
-
* Uses simple heuristic: 1 token ≈ 3 characters (conservative estimate)
|
|
17
|
-
* This is a rough approximation sufficient for quota management.
|
|
18
|
-
*
|
|
19
|
-
* @param text - Text to estimate tokens for
|
|
20
|
-
* @returns Estimated token count
|
|
21
|
-
*/
|
|
22
|
-
export function estimateTokens(text: string): number {
|
|
23
|
-
return Math.ceil(text.length / 3);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
/**
|
|
27
15
|
* Truncate text to fit within token limit
|
|
28
16
|
*
|
package/src/context/builder.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { NaxConfig } from "../config";
|
|
9
9
|
import { getLogger } from "../logger";
|
|
10
|
+
import { estimateTokens } from "../optimizer/types";
|
|
10
11
|
import type { UserStory } from "../prd";
|
|
11
12
|
import { countStories, getContextFiles } from "../prd";
|
|
12
13
|
import { autoDetectContextFiles } from "./auto-detect";
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
createProgressContext,
|
|
19
20
|
createStoryContext,
|
|
20
21
|
createTestCoverageContext,
|
|
21
|
-
estimateTokens,
|
|
22
22
|
} from "./elements";
|
|
23
23
|
import { generateTestCoverageSummary } from "./test-scanner";
|
|
24
24
|
import type { BuiltContext, ContextBudget, ContextElement, StoryContext } from "./types";
|
|
@@ -30,7 +30,6 @@ export const _deps = {
|
|
|
30
30
|
|
|
31
31
|
// Re-export for backward compatibility
|
|
32
32
|
export {
|
|
33
|
-
estimateTokens,
|
|
34
33
|
createStoryContext,
|
|
35
34
|
createDependencyContext,
|
|
36
35
|
createErrorContext,
|
package/src/context/elements.ts
CHANGED
|
@@ -5,21 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getLogger } from "../logger";
|
|
8
|
+
import { estimateTokens } from "../optimizer/types";
|
|
8
9
|
import type { StructuredFailure, UserStory } from "../prd";
|
|
9
10
|
import type { ContextElement } from "./types";
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* Approximate character-to-token ratio for token estimation.
|
|
13
|
-
* Value of 3 is a middle ground optimized for mixed content (prose + code + markdown).
|
|
14
|
-
* Slightly overestimates tokens, preventing budget overflow.
|
|
15
|
-
*/
|
|
16
|
-
const CHARS_PER_TOKEN = 3;
|
|
17
|
-
|
|
18
|
-
/** Estimate token count for text using character-to-token ratio. */
|
|
19
|
-
export function estimateTokens(text: string): number {
|
|
20
|
-
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
/** Create context element from current story */
|
|
24
13
|
export function createStoryContext(story: UserStory, priority: number): ContextElement {
|
|
25
14
|
const content = formatStoryAsText(story);
|
package/src/context/index.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
export type { ContextElement, ContextBudget, StoryContext, BuiltContext } from "./types";
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
|
-
estimateTokens,
|
|
9
8
|
createStoryContext,
|
|
10
9
|
createDependencyContext,
|
|
11
10
|
createErrorContext,
|
|
@@ -17,6 +16,8 @@ export {
|
|
|
17
16
|
formatContextAsMarkdown,
|
|
18
17
|
} from "./builder";
|
|
19
18
|
|
|
19
|
+
export { estimateTokens } from "../optimizer/types";
|
|
20
|
+
|
|
20
21
|
export {
|
|
21
22
|
generateTestCoverageSummary,
|
|
22
23
|
scanTestFiles,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { Glob } from "bun";
|
|
11
11
|
import { getLogger } from "../logger";
|
|
12
|
-
import { estimateTokens } from "
|
|
12
|
+
import { estimateTokens } from "../optimizer/types";
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Types
|
package/src/interaction/chain.ts
CHANGED
|
@@ -86,11 +86,27 @@ export class InteractionChain {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Send and receive in one call (convenience method)
|
|
89
|
+
* Send and receive in one call (convenience method).
|
|
90
|
+
*
|
|
91
|
+
* Normalizes "choose" type responses: when the plugin returns
|
|
92
|
+
* `action: "choose"` + `value: "<key>"`, remaps action to the selected
|
|
93
|
+
* option key so all consumers can switch on action directly without
|
|
94
|
+
* needing to inspect value themselves.
|
|
90
95
|
*/
|
|
91
96
|
async prompt(request: InteractionRequest): Promise<InteractionResponse> {
|
|
92
97
|
await this.send(request);
|
|
93
98
|
const response = await this.receive(request.id, request.timeout);
|
|
99
|
+
|
|
100
|
+
// Normalize choose responses: action="choose" means the user picked an option;
|
|
101
|
+
// the actual selection is in value. Remap to the selected key if it matches
|
|
102
|
+
// one of the declared options.
|
|
103
|
+
if (response.action === "choose" && response.value && request.options) {
|
|
104
|
+
const matched = request.options.find((o) => o.key === response.value);
|
|
105
|
+
if (matched) {
|
|
106
|
+
return { ...response, action: matched.key as InteractionResponse["action"] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
return response;
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -134,6 +134,7 @@ export const executionStage: PipelineStage = {
|
|
|
134
134
|
workdir: ctx.workdir,
|
|
135
135
|
modelTier: ctx.routing.modelTier,
|
|
136
136
|
contextMarkdown: ctx.contextMarkdown,
|
|
137
|
+
constitution: ctx.constitution?.content,
|
|
137
138
|
dryRun: false,
|
|
138
139
|
lite: isLiteMode,
|
|
139
140
|
});
|
|
@@ -156,7 +157,7 @@ export const executionStage: PipelineStage = {
|
|
|
156
157
|
// Store failure category in context for runner to use at max-attempts decision
|
|
157
158
|
ctx.tddFailureCategory = tddResult.failureCategory;
|
|
158
159
|
|
|
159
|
-
// Log
|
|
160
|
+
// Log and notify when human review is needed
|
|
160
161
|
if (tddResult.needsHumanReview) {
|
|
161
162
|
logger.warn("execution", "Human review needed", {
|
|
162
163
|
storyId: ctx.story.id,
|
|
@@ -164,6 +165,27 @@ export const executionStage: PipelineStage = {
|
|
|
164
165
|
lite: tddResult.lite,
|
|
165
166
|
failureCategory: tddResult.failureCategory,
|
|
166
167
|
});
|
|
168
|
+
// Send notification via interaction chain (Telegram in headless mode)
|
|
169
|
+
if (ctx.interaction) {
|
|
170
|
+
try {
|
|
171
|
+
await ctx.interaction.send({
|
|
172
|
+
id: `human-review-${ctx.story.id}-${Date.now()}`,
|
|
173
|
+
type: "notify",
|
|
174
|
+
featureName: ctx.featureDir ? (ctx.featureDir.split("/").pop() ?? "unknown") : "unknown",
|
|
175
|
+
storyId: ctx.story.id,
|
|
176
|
+
stage: "execution",
|
|
177
|
+
summary: `⚠️ Human review needed: ${ctx.story.id}`,
|
|
178
|
+
detail: `Story: ${ctx.story.title}\nReason: ${tddResult.reviewReason ?? "No reason provided"}\nCategory: ${tddResult.failureCategory ?? "unknown"}`,
|
|
179
|
+
fallback: "continue",
|
|
180
|
+
createdAt: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
} catch (notifyErr) {
|
|
183
|
+
logger.warn("execution", "Failed to send human review notification", {
|
|
184
|
+
storyId: ctx.story.id,
|
|
185
|
+
error: String(notifyErr),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
return routeTddFailure(tddResult.failureCategory, isLiteMode, ctx, tddResult.reviewReason);
|
package/src/prompts/builder.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* (1) Constitution
|
|
6
6
|
* (2) Role task body (user override OR default template)
|
|
7
7
|
* (3) Story context [non-overridable]
|
|
8
|
-
* (4)
|
|
9
|
-
* (5)
|
|
10
|
-
* (6)
|
|
8
|
+
* (4) Verdict section [verifier only, non-overridable]
|
|
9
|
+
* (5) Isolation rules [non-overridable]
|
|
10
|
+
* (6) Context markdown
|
|
11
|
+
* (7) Conventions footer [non-overridable, always last]
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { NaxConfig } from "../config/types";
|
|
@@ -16,6 +17,7 @@ import { buildConventionsSection } from "./sections/conventions";
|
|
|
16
17
|
import { buildIsolationSection } from "./sections/isolation";
|
|
17
18
|
import { buildRoleTaskSection } from "./sections/role-task";
|
|
18
19
|
import { buildStorySection } from "./sections/story";
|
|
20
|
+
import { buildVerdictSection } from "./sections/verdict";
|
|
19
21
|
import type { PromptOptions, PromptRole } from "./types";
|
|
20
22
|
|
|
21
23
|
const SECTION_SEP = "\n\n---\n\n";
|
|
@@ -81,16 +83,21 @@ export class PromptBuilder {
|
|
|
81
83
|
sections.push(buildStorySection(this._story));
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
// (4)
|
|
86
|
+
// (4) Verdict section — verifier only, non-overridable
|
|
87
|
+
if (this._role === "verifier" && this._story) {
|
|
88
|
+
sections.push(buildVerdictSection(this._story));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// (5) Isolation rules — non-overridable
|
|
85
92
|
const isolation = this._options.isolation as string | undefined;
|
|
86
93
|
sections.push(buildIsolationSection(this._role, isolation as "strict" | "lite" | undefined));
|
|
87
94
|
|
|
88
|
-
// (
|
|
95
|
+
// (6) Context markdown
|
|
89
96
|
if (this._contextMd) {
|
|
90
97
|
sections.push(this._contextMd);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
// (
|
|
100
|
+
// (7) Conventions footer — non-overridable, always last
|
|
94
101
|
sections.push(buildConventionsSection());
|
|
95
102
|
|
|
96
103
|
return sections.join(SECTION_SEP);
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export function buildConventionsSection(): string {
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"Commit your changes when done using conventional commit format (e.g. `feat:`, `fix:`, `test:`)."
|
|
14
|
-
);
|
|
8
|
+
return `# Conventions
|
|
9
|
+
|
|
10
|
+
Follow existing code patterns and conventions. Write idiomatic, maintainable code.
|
|
11
|
+
|
|
12
|
+
Commit your changes when done using conventional commit format (e.g. \`feat:\`, \`fix:\`, \`test:\`).`;
|
|
15
13
|
}
|
|
@@ -29,31 +29,31 @@ export function buildIsolationSection(
|
|
|
29
29
|
|
|
30
30
|
const role = roleOrMode as "implementer" | "test-writer" | "verifier" | "single-session" | "tdd-simple";
|
|
31
31
|
|
|
32
|
-
const header = "# Isolation Rules
|
|
32
|
+
const header = "# Isolation Rules";
|
|
33
33
|
const footer = `\n\n${TEST_FILTER_RULE}`;
|
|
34
34
|
|
|
35
35
|
if (role === "test-writer") {
|
|
36
36
|
const m = mode ?? "strict";
|
|
37
37
|
if (m === "strict") {
|
|
38
|
-
return `${header}
|
|
38
|
+
return `${header}\n\nisolation 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}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// lite mode for test-writer
|
|
42
|
-
return `${header}
|
|
42
|
+
return `${header}\n\nisolation 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}`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (role === "implementer") {
|
|
46
|
-
return `${header}
|
|
46
|
+
return `${header}\n\nisolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (role === "verifier") {
|
|
50
|
-
return `${header}
|
|
50
|
+
return `${header}\n\nisolation 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}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
if (role === "single-session") {
|
|
54
|
-
return `${header}
|
|
54
|
+
return `${header}\n\nisolation 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}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// tdd-simple role — no isolation restrictions (no footer needed)
|
|
58
|
-
return `${header}
|
|
58
|
+
return `${header}\n\nisolation scope: You may modify both src/ and test/ files. Write failing tests FIRST, then implement to make them pass.`;
|
|
59
59
|
}
|
|
@@ -27,83 +27,83 @@ export function buildRoleTaskSection(
|
|
|
27
27
|
if (role === "implementer") {
|
|
28
28
|
const v = variant ?? "standard";
|
|
29
29
|
if (v === "standard") {
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
return `# Role: Implementer
|
|
31
|
+
|
|
32
|
+
Your task: make failing tests pass.
|
|
33
|
+
|
|
34
|
+
Instructions:
|
|
35
|
+
- Implement source code in src/ to make tests pass
|
|
36
|
+
- Do NOT modify test files
|
|
37
|
+
- Run tests frequently to track progress
|
|
38
|
+
- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
|
|
39
|
+
- Goal: all tests green, all changes committed`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// lite variant
|
|
43
|
-
return (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
return `# Role: Implementer (Lite)
|
|
44
|
+
|
|
45
|
+
Your task: Write tests AND implement the feature in a single session.
|
|
46
|
+
|
|
47
|
+
Instructions:
|
|
48
|
+
- Write tests first (test/ directory), then implement (src/ directory)
|
|
49
|
+
- All tests must pass by the end
|
|
50
|
+
- Use Bun test (describe/test/expect)
|
|
51
|
+
- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
|
|
52
|
+
- Goal: all tests green, all criteria met, all changes committed`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (role === "test-writer") {
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
return `# Role: Test-Writer
|
|
57
|
+
|
|
58
|
+
Your task: Write comprehensive failing tests for the feature.
|
|
59
|
+
|
|
60
|
+
Instructions:
|
|
61
|
+
- Create test files in test/ directory that cover acceptance criteria
|
|
62
|
+
- Tests must fail initially (RED phase) — the feature is not yet implemented
|
|
63
|
+
- Use Bun test (describe/test/expect)
|
|
64
|
+
- Write clear test names that document expected behavior
|
|
65
|
+
- Focus on behavior, not implementation details
|
|
66
|
+
- Goal: comprehensive test suite ready for implementation`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (role === "verifier") {
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
return `# Role: Verifier
|
|
71
|
+
|
|
72
|
+
Your task: Review and verify the implementation against acceptance criteria.
|
|
73
|
+
|
|
74
|
+
Instructions:
|
|
75
|
+
- Review all test results — verify tests pass
|
|
76
|
+
- Check that implementation meets all acceptance criteria
|
|
77
|
+
- Inspect code quality, error handling, and edge cases
|
|
78
|
+
- Verify test modifications (if any) are legitimate fixes
|
|
79
|
+
- Write a detailed verdict with reasoning
|
|
80
|
+
- Goal: provide comprehensive verification and quality assurance`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (role === "single-session") {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
return `# Role: Single-Session
|
|
85
|
+
|
|
86
|
+
Your task: Write tests AND implement the feature in a single focused session.
|
|
87
|
+
|
|
88
|
+
Instructions:
|
|
89
|
+
- Phase 1: Write comprehensive tests (test/ directory)
|
|
90
|
+
- Phase 2: Implement to make all tests pass (src/ directory)
|
|
91
|
+
- Use Bun test (describe/test/expect)
|
|
92
|
+
- Run tests frequently throughout implementation
|
|
93
|
+
- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
|
|
94
|
+
- Goal: all tests passing, all changes committed, full story complete`;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// tdd-simple role — test-driven development in one session
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
return `# Role: TDD-Simple
|
|
99
|
+
|
|
100
|
+
Your task: Write failing tests FIRST, then implement to make them pass.
|
|
101
|
+
|
|
102
|
+
Instructions:
|
|
103
|
+
- RED phase: Write failing tests FIRST for the acceptance criteria
|
|
104
|
+
- RED phase: Run the tests to confirm they fail
|
|
105
|
+
- GREEN phase: Implement the minimum code to make tests pass
|
|
106
|
+
- REFACTOR phase: Refactor while keeping tests green
|
|
107
|
+
- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
|
|
108
|
+
- Goal: all tests passing, feature complete, all changes committed`;
|
|
109
109
|
}
|
|
@@ -64,9 +64,18 @@ export class ReviewOrchestrator {
|
|
|
64
64
|
const pluginResults: ReviewResult["pluginReviewers"] = [];
|
|
65
65
|
|
|
66
66
|
for (const reviewer of reviewers) {
|
|
67
|
-
logger?.info("review", `Running plugin reviewer: ${reviewer.name}
|
|
67
|
+
logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
|
|
68
|
+
changedFiles: changedFiles.length,
|
|
69
|
+
});
|
|
68
70
|
try {
|
|
69
71
|
const result = await reviewer.check(workdir, changedFiles);
|
|
72
|
+
// Always log the result so skips/passes are visible in the log
|
|
73
|
+
logger?.info("review", `Plugin reviewer result: ${reviewer.name}`, {
|
|
74
|
+
passed: result.passed,
|
|
75
|
+
exitCode: result.exitCode,
|
|
76
|
+
output: result.output?.slice(0, 500),
|
|
77
|
+
findings: result.findings?.length ?? 0,
|
|
78
|
+
});
|
|
70
79
|
pluginResults.push({
|
|
71
80
|
name: reviewer.name,
|
|
72
81
|
passed: result.passed,
|
|
@@ -85,6 +94,7 @@ export class ReviewOrchestrator {
|
|
|
85
94
|
}
|
|
86
95
|
} catch (error) {
|
|
87
96
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
97
|
+
logger?.warn("review", `Plugin reviewer threw error: ${reviewer.name}`, { error: errorMsg });
|
|
88
98
|
pluginResults.push({ name: reviewer.name, passed: false, output: "", error: errorMsg });
|
|
89
99
|
builtIn.pluginReviewers = pluginResults;
|
|
90
100
|
return {
|
|
@@ -59,7 +59,7 @@ Respond with ONLY this JSON (no markdown, no explanation):
|
|
|
59
59
|
* @param config - nax configuration
|
|
60
60
|
* @returns Formatted batch prompt string
|
|
61
61
|
*/
|
|
62
|
-
export function
|
|
62
|
+
export function buildBatchRoutingPrompt(stories: UserStory[], config: NaxConfig): string {
|
|
63
63
|
const storyBlocks = stories
|
|
64
64
|
.map((story, idx) => {
|
|
65
65
|
const criteria = story.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
|