@nathapp/nax 0.48.4 → 0.49.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/CHANGELOG.md +8 -0
- package/dist/nax.js +283 -184
- package/package.json +1 -1
- package/src/acceptance/generator.ts +7 -5
- package/src/agents/acp/adapter.ts +7 -2
- package/src/agents/acp/parser.ts +1 -1
- package/src/agents/acp/spawn-client.ts +2 -0
- package/src/agents/types.ts +6 -0
- package/src/cli/plan.ts +11 -1
- package/src/cli/prompts-main.ts +1 -0
- package/src/config/merge.ts +55 -9
- package/src/execution/iteration-runner.ts +15 -0
- package/src/execution/lifecycle/acceptance-loop.ts +2 -0
- package/src/execution/parallel-coordinator.ts +1 -0
- package/src/execution/parallel-executor-rectify.ts +1 -0
- package/src/execution/parallel-worker.ts +1 -0
- package/src/execution/sequential-executor.ts +1 -0
- package/src/pipeline/stages/acceptance.ts +6 -2
- package/src/pipeline/stages/autofix.ts +15 -7
- package/src/pipeline/stages/execution.ts +6 -0
- package/src/pipeline/stages/prompt.ts +5 -2
- package/src/pipeline/stages/rectify.ts +4 -2
- package/src/pipeline/stages/regression.ts +10 -6
- package/src/pipeline/stages/review.ts +11 -7
- package/src/pipeline/stages/routing.ts +20 -1
- package/src/pipeline/stages/verify.ts +23 -20
- package/src/pipeline/types.ts +7 -0
- package/src/utils/git.ts +10 -2
package/package.json
CHANGED
|
@@ -94,9 +94,8 @@ ${criteriaList}
|
|
|
94
94
|
|
|
95
95
|
${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Structure example (do NOT wrap in markdown fences — output raw TypeScript only):
|
|
98
98
|
|
|
99
|
-
\`\`\`typescript
|
|
100
99
|
import { describe, test, expect } from "bun:test";
|
|
101
100
|
|
|
102
101
|
describe("${options.featureName} - Acceptance Tests", () => {
|
|
@@ -104,13 +103,16 @@ describe("${options.featureName} - Acceptance Tests", () => {
|
|
|
104
103
|
// Test implementation
|
|
105
104
|
});
|
|
106
105
|
});
|
|
107
|
-
\`\`\`
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
|
|
110
108
|
|
|
111
109
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
112
110
|
|
|
113
|
-
const
|
|
111
|
+
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
112
|
+
model: options.modelDef.model,
|
|
113
|
+
config: options.config,
|
|
114
|
+
});
|
|
115
|
+
const testCode = extractTestCode(rawOutput);
|
|
114
116
|
|
|
115
117
|
const refinedJsonContent = JSON.stringify(
|
|
116
118
|
refinedCriteria.map((c, i) => ({
|
|
@@ -694,8 +694,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
694
694
|
|
|
695
695
|
let session: AcpSession | null = null;
|
|
696
696
|
try {
|
|
697
|
-
// complete() is one-shot — ephemeral session, no
|
|
698
|
-
|
|
697
|
+
// complete() is one-shot — ephemeral session, no sidecar
|
|
698
|
+
// Use caller-provided sessionName if available (aids tracing), otherwise timestamp-based
|
|
699
|
+
session = await client.createSession({
|
|
700
|
+
agentName: this.name,
|
|
701
|
+
permissionMode,
|
|
702
|
+
sessionName: _options?.sessionName,
|
|
703
|
+
});
|
|
699
704
|
|
|
700
705
|
// Enforce timeout via Promise.race — session.prompt() can hang indefinitely
|
|
701
706
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
package/src/agents/acp/parser.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function parseAcpxJsonOutput(rawOutput: string): {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Final result with token breakdown
|
|
73
|
+
// Final result with token breakdown
|
|
74
74
|
if (event.id !== undefined && event.result && typeof event.result === "object") {
|
|
75
75
|
const result = event.result as Record<string, unknown>;
|
|
76
76
|
|
package/src/agents/types.ts
CHANGED
|
@@ -127,6 +127,12 @@ export interface CompleteOptions {
|
|
|
127
127
|
* Pass when available so complete() honours permissionProfile / dangerouslySkipPermissions.
|
|
128
128
|
*/
|
|
129
129
|
config?: NaxConfig;
|
|
130
|
+
/**
|
|
131
|
+
* Named session to use for this completion call.
|
|
132
|
+
* If omitted, a timestamp-based ephemeral session name is generated.
|
|
133
|
+
* Pass a meaningful name (e.g. "nax-decompose-us-001") to aid debugging.
|
|
134
|
+
*/
|
|
135
|
+
sessionName?: string;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
package/src/cli/plan.ts
CHANGED
|
@@ -138,7 +138,17 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
138
138
|
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails);
|
|
139
139
|
const cliAdapter = _deps.getAgent(agentName);
|
|
140
140
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
141
|
-
|
|
141
|
+
let autoModel: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const planTier = config?.plan?.model ?? "balanced";
|
|
144
|
+
const { resolveModel } = await import("../config/schema");
|
|
145
|
+
const models = config?.models as Record<string, unknown> | undefined;
|
|
146
|
+
const entry = models?.[planTier] ?? models?.balanced;
|
|
147
|
+
if (entry) autoModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
148
|
+
} catch {
|
|
149
|
+
// fall through — complete() will use its own fallback
|
|
150
|
+
}
|
|
151
|
+
rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config });
|
|
142
152
|
// CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
|
|
143
153
|
try {
|
|
144
154
|
const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
|
package/src/cli/prompts-main.ts
CHANGED
|
@@ -97,6 +97,7 @@ export async function promptsCommand(options: PromptsCommandOptions): Promise<st
|
|
|
97
97
|
// Build initial pipeline context
|
|
98
98
|
const ctx: PipelineContext = {
|
|
99
99
|
config,
|
|
100
|
+
effectiveConfig: config,
|
|
100
101
|
prd,
|
|
101
102
|
story,
|
|
102
103
|
stories: [story], // Single story, not batch
|
package/src/config/merge.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-Package Config Merge Utility (MW-008)
|
|
2
|
+
* Per-Package Config Merge Utility (MW-008, v0.49.0 expansion)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Merges a package-level partial config override into a root config.
|
|
5
|
+
* Covers all fields that make sense at the per-package level.
|
|
6
|
+
* Root-only fields (models, autoMode, routing, agent, etc.) are unchanged.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { NaxConfig } from "./schema";
|
|
@@ -10,27 +11,72 @@ import type { NaxConfig } from "./schema";
|
|
|
10
11
|
/**
|
|
11
12
|
* Merge a package-level partial config override into a root config.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Mergeable sections:
|
|
15
|
+
* - execution: smartTestRunner, regressionGate (deep), verificationTimeoutSeconds
|
|
16
|
+
* - review: enabled, checks, commands (deep), pluginMode
|
|
17
|
+
* - acceptance: enabled, generateTests, testPath
|
|
18
|
+
* - quality: requireTests, requireTypecheck, requireLint, commands (deep)
|
|
19
|
+
* - context: testCoverage (deep)
|
|
20
|
+
*
|
|
21
|
+
* All other sections (models, autoMode, routing, agent, generate, tdd,
|
|
22
|
+
* decompose, plan, constitution, interaction) remain root-only.
|
|
15
23
|
*
|
|
16
24
|
* @param root - Full root NaxConfig (already validated)
|
|
17
|
-
* @param packageOverride - Partial package-level override
|
|
25
|
+
* @param packageOverride - Partial package-level override
|
|
18
26
|
* @returns New merged NaxConfig (immutable — does not mutate inputs)
|
|
19
27
|
*/
|
|
20
28
|
export function mergePackageConfig(root: NaxConfig, packageOverride: Partial<NaxConfig>): NaxConfig {
|
|
21
|
-
const
|
|
29
|
+
const hasAnyMergeableField =
|
|
30
|
+
packageOverride.execution !== undefined ||
|
|
31
|
+
packageOverride.review !== undefined ||
|
|
32
|
+
packageOverride.acceptance !== undefined ||
|
|
33
|
+
packageOverride.quality !== undefined ||
|
|
34
|
+
packageOverride.context !== undefined;
|
|
22
35
|
|
|
23
|
-
if (!
|
|
36
|
+
if (!hasAnyMergeableField) {
|
|
24
37
|
return root;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
return {
|
|
28
41
|
...root,
|
|
42
|
+
execution: {
|
|
43
|
+
...root.execution,
|
|
44
|
+
...packageOverride.execution,
|
|
45
|
+
smartTestRunner: packageOverride.execution?.smartTestRunner ?? root.execution.smartTestRunner,
|
|
46
|
+
regressionGate: {
|
|
47
|
+
...root.execution.regressionGate,
|
|
48
|
+
...packageOverride.execution?.regressionGate,
|
|
49
|
+
},
|
|
50
|
+
verificationTimeoutSeconds:
|
|
51
|
+
packageOverride.execution?.verificationTimeoutSeconds ?? root.execution.verificationTimeoutSeconds,
|
|
52
|
+
},
|
|
53
|
+
review: {
|
|
54
|
+
...root.review,
|
|
55
|
+
...packageOverride.review,
|
|
56
|
+
commands: {
|
|
57
|
+
...root.review.commands,
|
|
58
|
+
...packageOverride.review?.commands,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
acceptance: {
|
|
62
|
+
...root.acceptance,
|
|
63
|
+
...packageOverride.acceptance,
|
|
64
|
+
},
|
|
29
65
|
quality: {
|
|
30
66
|
...root.quality,
|
|
67
|
+
requireTests: packageOverride.quality?.requireTests ?? root.quality.requireTests,
|
|
68
|
+
requireTypecheck: packageOverride.quality?.requireTypecheck ?? root.quality.requireTypecheck,
|
|
69
|
+
requireLint: packageOverride.quality?.requireLint ?? root.quality.requireLint,
|
|
31
70
|
commands: {
|
|
32
71
|
...root.quality.commands,
|
|
33
|
-
...
|
|
72
|
+
...packageOverride.quality?.commands,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
context: {
|
|
76
|
+
...root.context,
|
|
77
|
+
testCoverage: {
|
|
78
|
+
...root.context.testCoverage,
|
|
79
|
+
...packageOverride.context?.testCoverage,
|
|
34
80
|
},
|
|
35
81
|
},
|
|
36
82
|
};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Extracted from sequential-executor.ts to slim it below 120 lines.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { loadConfigForWorkdir } from "../config/loader";
|
|
8
10
|
import { getSafeLogger } from "../logger";
|
|
9
11
|
import type { StoryMetrics } from "../metrics";
|
|
10
12
|
import { runPipeline } from "../pipeline/runner";
|
|
@@ -64,8 +66,14 @@ export async function runIteration(
|
|
|
64
66
|
// BUG-067: Accumulate cost from all prior failed attempts (stored in priorFailures by handleTierEscalation)
|
|
65
67
|
const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
|
|
66
68
|
|
|
69
|
+
// PKG-003: Resolve per-package effective config once per story (not per-stage)
|
|
70
|
+
const effectiveConfig = story.workdir
|
|
71
|
+
? await _iterationRunnerDeps.loadConfigForWorkdir(join(ctx.workdir, "nax", "config.json"), story.workdir)
|
|
72
|
+
: ctx.config;
|
|
73
|
+
|
|
67
74
|
const pipelineContext: PipelineContext = {
|
|
68
75
|
config: ctx.config,
|
|
76
|
+
effectiveConfig,
|
|
69
77
|
prd,
|
|
70
78
|
story,
|
|
71
79
|
stories: storiesToExecute,
|
|
@@ -140,3 +148,10 @@ export async function runIteration(
|
|
|
140
148
|
reason: pipelineResult.reason,
|
|
141
149
|
};
|
|
142
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
154
|
+
*/
|
|
155
|
+
export const _iterationRunnerDeps = {
|
|
156
|
+
loadConfigForWorkdir,
|
|
157
|
+
};
|
|
@@ -134,6 +134,7 @@ async function executeFixStory(
|
|
|
134
134
|
);
|
|
135
135
|
const fixContext: PipelineContext = {
|
|
136
136
|
config: ctx.config,
|
|
137
|
+
effectiveConfig: ctx.config,
|
|
137
138
|
prd,
|
|
138
139
|
story,
|
|
139
140
|
stories: [story],
|
|
@@ -177,6 +178,7 @@ export async function runAcceptanceLoop(ctx: AcceptanceLoopContext): Promise<Acc
|
|
|
177
178
|
const firstStory = prd.userStories[0];
|
|
178
179
|
const acceptanceContext: PipelineContext = {
|
|
179
180
|
config: ctx.config,
|
|
181
|
+
effectiveConfig: ctx.config,
|
|
180
182
|
prd,
|
|
181
183
|
story: firstStory,
|
|
182
184
|
stories: [firstStory],
|
|
@@ -72,6 +72,7 @@ export async function executeSequential(
|
|
|
72
72
|
logger?.info("execution", "Running pre-run pipeline (acceptance test setup)");
|
|
73
73
|
const preRunCtx: PipelineContext = {
|
|
74
74
|
config: ctx.config,
|
|
75
|
+
effectiveConfig: ctx.config,
|
|
75
76
|
prd,
|
|
76
77
|
workdir: ctx.workdir,
|
|
77
78
|
featureDir: ctx.featureDir,
|
|
@@ -92,7 +92,8 @@ export const acceptanceStage: PipelineStage = {
|
|
|
92
92
|
// Only run when:
|
|
93
93
|
// 1. Acceptance validation is enabled
|
|
94
94
|
// 2. All stories are complete
|
|
95
|
-
|
|
95
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
96
|
+
if (!effectiveConfig.acceptance.enabled) {
|
|
96
97
|
return false;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -106,6 +107,9 @@ export const acceptanceStage: PipelineStage = {
|
|
|
106
107
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
107
108
|
const logger = getLogger();
|
|
108
109
|
|
|
110
|
+
// PKG-004: use centrally resolved effective config
|
|
111
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
112
|
+
|
|
109
113
|
logger.info("acceptance", "Running acceptance tests");
|
|
110
114
|
|
|
111
115
|
// Build path to acceptance test file
|
|
@@ -114,7 +118,7 @@ export const acceptanceStage: PipelineStage = {
|
|
|
114
118
|
return { action: "continue" };
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
const testPath = path.join(ctx.featureDir,
|
|
121
|
+
const testPath = path.join(ctx.featureDir, effectiveConfig.acceptance.testPath);
|
|
118
122
|
|
|
119
123
|
// Check if test file exists
|
|
120
124
|
const testFile = Bun.file(testPath);
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* - `escalate` — max attempts exhausted or agent unavailable
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { join } from "node:path";
|
|
22
23
|
import { getAgent } from "../../agents";
|
|
23
24
|
import { resolveModel } from "../../config";
|
|
25
|
+
import { loadConfigForWorkdir } from "../../config/loader";
|
|
24
26
|
import { resolvePermissions } from "../../config/permissions";
|
|
25
27
|
import { getLogger } from "../../logger";
|
|
26
28
|
import type { UserStory } from "../../prd";
|
|
@@ -34,7 +36,7 @@ export const autofixStage: PipelineStage = {
|
|
|
34
36
|
enabled(ctx: PipelineContext): boolean {
|
|
35
37
|
if (!ctx.reviewResult) return false;
|
|
36
38
|
if (ctx.reviewResult.success) return false;
|
|
37
|
-
const autofixEnabled = ctx.config.quality.autofix?.enabled ?? true;
|
|
39
|
+
const autofixEnabled = (ctx.effectiveConfig ?? ctx.config).quality.autofix?.enabled ?? true;
|
|
38
40
|
return autofixEnabled;
|
|
39
41
|
},
|
|
40
42
|
|
|
@@ -51,14 +53,19 @@ export const autofixStage: PipelineStage = {
|
|
|
51
53
|
return { action: "continue" };
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
+
// PKG-004: use centrally resolved effective config (ctx.effectiveConfig set once per story)
|
|
57
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
58
|
+
const lintFixCmd = effectiveConfig.quality.commands.lintFix;
|
|
59
|
+
const formatFixCmd = effectiveConfig.quality.commands.formatFix;
|
|
60
|
+
|
|
61
|
+
// Effective workdir for running commands (scoped to package if monorepo)
|
|
62
|
+
const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
56
63
|
|
|
57
64
|
// Phase 1: Mechanical fix (if commands are configured)
|
|
58
65
|
if (lintFixCmd || formatFixCmd) {
|
|
59
66
|
if (lintFixCmd) {
|
|
60
67
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
|
|
61
|
-
const lintResult = await _autofixDeps.runCommand(lintFixCmd,
|
|
68
|
+
const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
|
|
62
69
|
logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id });
|
|
63
70
|
if (lintResult.exitCode !== 0) {
|
|
64
71
|
logger.warn("autofix", "lintFix command failed — may not have fixed all issues", {
|
|
@@ -70,7 +77,7 @@ export const autofixStage: PipelineStage = {
|
|
|
70
77
|
|
|
71
78
|
if (formatFixCmd) {
|
|
72
79
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
|
|
73
|
-
const fmtResult = await _autofixDeps.runCommand(formatFixCmd,
|
|
80
|
+
const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
|
|
74
81
|
logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, { storyId: ctx.story.id });
|
|
75
82
|
if (fmtResult.exitCode !== 0) {
|
|
76
83
|
logger.warn("autofix", "formatFix command failed — may not have fixed all issues", {
|
|
@@ -155,7 +162,8 @@ Commit your fixes when done.`;
|
|
|
155
162
|
|
|
156
163
|
async function runAgentRectification(ctx: PipelineContext): Promise<boolean> {
|
|
157
164
|
const logger = getLogger();
|
|
158
|
-
const
|
|
165
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
166
|
+
const maxAttempts = effectiveConfig.quality.autofix?.maxAttempts ?? 2;
|
|
159
167
|
const failedChecks = collectFailedChecks(ctx);
|
|
160
168
|
|
|
161
169
|
if (failedChecks.length === 0) {
|
|
@@ -224,4 +232,4 @@ async function runAgentRectification(ctx: PipelineContext): Promise<boolean> {
|
|
|
224
232
|
/**
|
|
225
233
|
* Injectable deps for testing.
|
|
226
234
|
*/
|
|
227
|
-
export const _autofixDeps = { runCommand, recheckReview, runAgentRectification };
|
|
235
|
+
export const _autofixDeps = { runCommand, recheckReview, runAgentRectification, loadConfigForWorkdir };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - `session-failure` → escalate
|
|
17
17
|
* - `tests-failing` → escalate
|
|
18
18
|
* - `verifier-rejected` → escalate
|
|
19
|
+
* - `greenfield-no-tests` → escalate (tier-escalation switches to test-after)
|
|
19
20
|
* - no category / unknown → pause (backward compatible)
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
@@ -111,6 +112,11 @@ export function routeTddFailure(
|
|
|
111
112
|
return { action: "escalate" };
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// S5: greenfield-no-tests → escalate so tier-escalation can switch to test-after
|
|
116
|
+
if (failureCategory === "greenfield-no-tests") {
|
|
117
|
+
return { action: "escalate" };
|
|
118
|
+
}
|
|
119
|
+
|
|
114
120
|
// Default: no category or unknown — backward-compatible pause for human review
|
|
115
121
|
return {
|
|
116
122
|
action: "pause",
|
|
@@ -34,6 +34,9 @@ export const promptStage: PipelineStage = {
|
|
|
34
34
|
const logger = getLogger();
|
|
35
35
|
const isBatch = ctx.stories.length > 1;
|
|
36
36
|
|
|
37
|
+
// PKG-004: use centrally resolved effective config
|
|
38
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
39
|
+
|
|
37
40
|
let prompt: string;
|
|
38
41
|
if (isBatch) {
|
|
39
42
|
const builder = PromptBuilder.for("batch")
|
|
@@ -41,7 +44,7 @@ export const promptStage: PipelineStage = {
|
|
|
41
44
|
.stories(ctx.stories)
|
|
42
45
|
.context(ctx.contextMarkdown)
|
|
43
46
|
.constitution(ctx.constitution?.content)
|
|
44
|
-
.testCommand(
|
|
47
|
+
.testCommand(effectiveConfig.quality?.commands?.test);
|
|
45
48
|
prompt = await builder.build();
|
|
46
49
|
} else {
|
|
47
50
|
// Both test-after and tdd-simple use the tdd-simple prompt (RED/GREEN/REFACTOR)
|
|
@@ -51,7 +54,7 @@ export const promptStage: PipelineStage = {
|
|
|
51
54
|
.story(ctx.story)
|
|
52
55
|
.context(ctx.contextMarkdown)
|
|
53
56
|
.constitution(ctx.constitution?.content)
|
|
54
|
-
.testCommand(
|
|
57
|
+
.testCommand(effectiveConfig.quality?.commands?.test);
|
|
55
58
|
prompt = await builder.build();
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -59,13 +59,15 @@ export const rectifyStage: PipelineStage = {
|
|
|
59
59
|
testOutput,
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// PKG-004: use centrally resolved effective config
|
|
63
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
64
|
+
const testCommand = effectiveConfig.review?.commands?.test ?? effectiveConfig.quality.commands.test ?? "bun test";
|
|
63
65
|
const fixed = await _rectifyDeps.runRectificationLoop({
|
|
64
66
|
config: ctx.config,
|
|
65
67
|
workdir: ctx.workdir,
|
|
66
68
|
story: ctx.story,
|
|
67
69
|
testCommand,
|
|
68
|
-
timeoutSeconds:
|
|
70
|
+
timeoutSeconds: effectiveConfig.execution.verificationTimeoutSeconds,
|
|
69
71
|
testOutput,
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -24,24 +24,28 @@ export const regressionStage: PipelineStage = {
|
|
|
24
24
|
name: "regression",
|
|
25
25
|
|
|
26
26
|
enabled(ctx: PipelineContext): boolean {
|
|
27
|
-
const
|
|
27
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
28
|
+
const mode = effectiveConfig.execution.regressionGate?.mode ?? "deferred";
|
|
28
29
|
if (mode !== "per-story") return false;
|
|
29
30
|
// Only run when verify passed (or was skipped/not set)
|
|
30
31
|
if (ctx.verifyResult && !ctx.verifyResult.success) return false;
|
|
31
|
-
const gateEnabled =
|
|
32
|
+
const gateEnabled = effectiveConfig.execution.regressionGate?.enabled ?? true;
|
|
32
33
|
return gateEnabled;
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
skipReason(ctx: PipelineContext): string {
|
|
36
|
-
const
|
|
37
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
38
|
+
const mode = effectiveConfig.execution.regressionGate?.mode ?? "deferred";
|
|
37
39
|
if (mode !== "per-story") return `not needed (regression mode is '${mode}', not 'per-story')`;
|
|
38
40
|
return "disabled (regression gate not enabled in config)";
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
42
44
|
const logger = getLogger();
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
+
// PKG-004: use centrally resolved effective config
|
|
46
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
47
|
+
const testCommand = effectiveConfig.review?.commands?.test ?? effectiveConfig.quality.commands.test ?? "bun test";
|
|
48
|
+
const timeoutSeconds = effectiveConfig.execution.regressionGate?.timeoutSeconds ?? 120;
|
|
45
49
|
|
|
46
50
|
logger.info("regression", "Running full-suite regression gate", { storyId: ctx.story.id });
|
|
47
51
|
|
|
@@ -50,7 +54,7 @@ export const regressionStage: PipelineStage = {
|
|
|
50
54
|
testCommand,
|
|
51
55
|
timeoutSeconds,
|
|
52
56
|
storyId: ctx.story.id,
|
|
53
|
-
acceptOnTimeout:
|
|
57
|
+
acceptOnTimeout: effectiveConfig.execution.regressionGate?.acceptOnTimeout ?? true,
|
|
54
58
|
config: ctx.config,
|
|
55
59
|
};
|
|
56
60
|
|
|
@@ -19,20 +19,23 @@ import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
|
19
19
|
|
|
20
20
|
export const reviewStage: PipelineStage = {
|
|
21
21
|
name: "review",
|
|
22
|
-
enabled: (ctx) => ctx.config.review.enabled,
|
|
22
|
+
enabled: (ctx) => (ctx.effectiveConfig ?? ctx.config).review.enabled,
|
|
23
23
|
|
|
24
24
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
25
25
|
const logger = getLogger();
|
|
26
26
|
|
|
27
|
+
// PKG-004: use centrally resolved effective config
|
|
28
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
29
|
+
|
|
27
30
|
logger.info("review", "Running review phase", { storyId: ctx.story.id });
|
|
28
31
|
|
|
29
32
|
// MW-010: scope review to package directory when story.workdir is set
|
|
30
33
|
const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
31
34
|
|
|
32
35
|
const result = await reviewOrchestrator.review(
|
|
33
|
-
|
|
36
|
+
effectiveConfig.review,
|
|
34
37
|
effectiveWorkdir,
|
|
35
|
-
|
|
38
|
+
effectiveConfig.execution,
|
|
36
39
|
ctx.plugins,
|
|
37
40
|
ctx.storyGitRef,
|
|
38
41
|
ctx.story.workdir, // MW-010: scope changed-file checks to package
|
|
@@ -49,10 +52,10 @@ export const reviewStage: PipelineStage = {
|
|
|
49
52
|
|
|
50
53
|
if (result.pluginFailed) {
|
|
51
54
|
// security-review trigger: prompt before permanently failing
|
|
52
|
-
if (ctx.interaction && isTriggerEnabled("security-review",
|
|
55
|
+
if (ctx.interaction && isTriggerEnabled("security-review", effectiveConfig)) {
|
|
53
56
|
const shouldContinue = await _reviewDeps.checkSecurityReview(
|
|
54
57
|
{ featureName: ctx.prd.feature, storyId: ctx.story.id },
|
|
55
|
-
|
|
58
|
+
effectiveConfig,
|
|
56
59
|
ctx.interaction,
|
|
57
60
|
);
|
|
58
61
|
if (!shouldContinue) {
|
|
@@ -67,11 +70,12 @@ export const reviewStage: PipelineStage = {
|
|
|
67
70
|
return { action: "fail", reason: `Review failed: ${result.failureReason}` };
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
logger.warn("review", "Review failed (built-in checks) —
|
|
73
|
+
logger.warn("review", "Review failed (built-in checks) — handing off to autofix", {
|
|
71
74
|
reason: result.failureReason,
|
|
72
75
|
storyId: ctx.story.id,
|
|
73
76
|
});
|
|
74
|
-
|
|
77
|
+
// ctx.reviewResult is already set with success:false — autofixStage handles it next
|
|
78
|
+
return { action: "continue" };
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
logger.info("review", "Review passed", {
|
|
@@ -65,9 +65,28 @@ async function runDecompose(
|
|
|
65
65
|
if (!agent) {
|
|
66
66
|
throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// Resolve decompose model: config.decompose.model tier → actual model string
|
|
70
|
+
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
71
|
+
let decomposeModel: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
const { resolveModel } = await import("../../config/schema");
|
|
74
|
+
const models = config.models as Record<string, unknown>;
|
|
75
|
+
const entry = models[decomposeTier] ?? models.balanced;
|
|
76
|
+
if (entry) decomposeModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
77
|
+
} catch {
|
|
78
|
+
// resolveModel can throw on malformed entries — fall through to let complete() handle it
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
68
82
|
const adapter = {
|
|
69
83
|
async decompose(prompt: string): Promise<string> {
|
|
70
|
-
return agent.complete(prompt, {
|
|
84
|
+
return agent.complete(prompt, {
|
|
85
|
+
model: decomposeModel,
|
|
86
|
+
jsonMode: true,
|
|
87
|
+
config,
|
|
88
|
+
sessionName: storySessionName,
|
|
89
|
+
});
|
|
71
90
|
},
|
|
72
91
|
};
|
|
73
92
|
|