@nathapp/nax 0.70.0-canary.1 → 0.70.0-canary.3

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.
Files changed (2) hide show
  1. package/dist/nax.js +298 -115
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17028,6 +17028,7 @@ var init_schemas_infra = __esm(() => {
17028
17028
  testPath: exports_external.string().min(1, "acceptance.testPath must be non-empty"),
17029
17029
  command: exports_external.string().optional(),
17030
17030
  model: ConfiguredModelSchema.default("fast"),
17031
+ generateModel: ConfiguredModelSchema.optional(),
17031
17032
  refinement: exports_external.boolean().default(true),
17032
17033
  refinementConcurrency: exports_external.number().int().min(1).max(10).default(3),
17033
17034
  redGate: exports_external.boolean().default(true),
@@ -19231,7 +19232,7 @@ function reshapeSelector(name, fn) {
19231
19232
  var reviewConfigSelector, planConfigSelector, decomposeConfigSelector, rectifyConfigSelector, acceptanceConfigSelector, acceptanceFixConfigSelector, acceptanceGenConfigSelector, tddConfigSelector, debateConfigSelector, routingConfigSelector, verifyConfigSelector, rectificationGateConfigSelector, agentConfigSelector, agentManagerConfigSelector, interactionConfigSelector, precheckConfigSelector, qualityConfigSelector, autofixConfigSelector, executionGatesConfigSelector, testPatternConfigSelector, contextConfigSelector, contextToolRuntimeConfigSelector, promptLoaderConfigSelector, llmRoutingConfigSelector;
19232
19233
  var init_selectors = __esm(() => {
19233
19234
  reviewConfigSelector = pickSelector("review", "review", "debate", "models", "execution", "project", "quality", "agent");
19234
- planConfigSelector = pickSelector("plan", "plan", "debate", "agent", "project");
19235
+ planConfigSelector = pickSelector("plan", "plan", "debate", "agent", "project", "routing");
19235
19236
  decomposeConfigSelector = pickSelector("decompose", "plan", "agent", "routing");
19236
19237
  rectifyConfigSelector = pickSelector("rectify", "execution");
19237
19238
  acceptanceConfigSelector = pickSelector("acceptance", "acceptance");
@@ -21021,6 +21022,7 @@ class AcpAgentAdapter {
21021
21022
  const MAX_TURNS = opts.maxTurns ?? 10;
21022
21023
  let totalTokenUsage = { inputTokens: 0, outputTokens: 0 };
21023
21024
  let totalExactCostUsd;
21025
+ const interactions = [];
21024
21026
  let turnCount = 0;
21025
21027
  let lastResponse = null;
21026
21028
  let timedOut = false;
@@ -21105,6 +21107,7 @@ class AcpAgentAdapter {
21105
21107
  })
21106
21108
  ]);
21107
21109
  if (response) {
21110
+ interactions.push({ turnIndex: turnCount, question, reply: response.answer });
21108
21111
  currentPrompt = response.answer;
21109
21112
  continue;
21110
21113
  }
@@ -21135,7 +21138,8 @@ class AcpAgentAdapter {
21135
21138
  tokenUsage,
21136
21139
  estimatedCostUsd,
21137
21140
  exactCostUsd,
21138
- internalRoundTrips: turnCount
21141
+ internalRoundTrips: turnCount,
21142
+ ...interactions.length > 0 ? { interactions } : {}
21139
21143
  };
21140
21144
  }
21141
21145
  async closeSession(handle) {
@@ -22138,6 +22142,7 @@ class AgentManager {
22138
22142
  sessionId: handle.protocolIds?.sessionId ?? null,
22139
22143
  recordId: handle.protocolIds?.recordId ?? null
22140
22144
  },
22145
+ ...result.interactions?.length ? { interactions: result.interactions } : {},
22141
22146
  origin: "runAsSession",
22142
22147
  ...opts.callId !== undefined ? { callId: opts.callId } : {},
22143
22148
  ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
@@ -22251,6 +22256,34 @@ function resolveDefaultAgent(config2) {
22251
22256
  }
22252
22257
  var FALLBACK_DEFAULT_AGENT = "claude";
22253
22258
 
22259
+ // src/agents/shared/agent-profile-resolver.ts
22260
+ function resolveAgentAssignment(selectedProfileId, agentRouting, storyId) {
22261
+ if (agentRouting?.enabled !== true)
22262
+ return null;
22263
+ const profiles = agentRouting.profiles ?? [];
22264
+ if (profiles.length === 0)
22265
+ return null;
22266
+ const defaultProfile = agentRouting.default ? profiles.find((p) => p.id === agentRouting.default) : undefined;
22267
+ if (selectedProfileId) {
22268
+ const profile = profiles.find((p) => p.id === selectedProfileId);
22269
+ if (profile)
22270
+ return toAssignment(profile);
22271
+ getSafeLogger()?.warn("routing", `Story ${storyId} selected unknown agent profile "${selectedProfileId}" \u2014 falling back to ${defaultProfile ? `default profile "${defaultProfile.id}"` : "no profile"}`, { storyId, agentProfileId: selectedProfileId });
22272
+ }
22273
+ return defaultProfile ? toAssignment(defaultProfile) : null;
22274
+ }
22275
+ function toAssignment(p) {
22276
+ return { agent: p.target.agent, agentProfileId: p.id, profileModelTier: p.target.model };
22277
+ }
22278
+ var init_agent_profile_resolver = __esm(() => {
22279
+ init_logger2();
22280
+ });
22281
+
22282
+ // src/agents/shared/index.ts
22283
+ var init_shared = __esm(() => {
22284
+ init_agent_profile_resolver();
22285
+ });
22286
+
22254
22287
  // src/agents/retry/types.ts
22255
22288
  var ParseValidationError;
22256
22289
  var init_types4 = __esm(() => {
@@ -22497,6 +22530,7 @@ var init_agents = __esm(() => {
22497
22530
  init_cost();
22498
22531
  init_version_detection();
22499
22532
  init_manager();
22533
+ init_shared();
22500
22534
  init_retry();
22501
22535
  });
22502
22536
 
@@ -28189,7 +28223,8 @@ function validateStory(raw, index, allIds) {
28189
28223
  complexity,
28190
28224
  testStrategy,
28191
28225
  reasoning: typeof routing.reasoning === "string" && routing.reasoning.trim().length > 0 ? routing.reasoning.trim() : "validated from LLM output",
28192
- ...noTestJustification !== undefined ? { noTestJustification } : {}
28226
+ ...noTestJustification !== undefined ? { noTestJustification } : {},
28227
+ ...typeof routing.agentProfileId === "string" && routing.agentProfileId.trim().length > 0 ? { agentProfileId: routing.agentProfileId.trim() } : {}
28193
28228
  },
28194
28229
  ...workdir !== undefined ? { workdir } : {},
28195
28230
  ...contextFiles.length > 0 ? { contextFiles } : {},
@@ -34691,8 +34726,10 @@ var init_plan = __esm(() => {
34691
34726
  truncated: () => PlanPromptBuilder.jsonRepair(0, "JSON appears truncated \u2014 please rewrite completely")
34692
34727
  }
34693
34728
  }),
34694
- build(input, _ctx) {
34695
- const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile);
34729
+ build(input, ctx) {
34730
+ const agentRouting = ctx.config.routing?.agents;
34731
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
34732
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile, undefined, profiles);
34696
34733
  return {
34697
34734
  role: { id: "role", content: "", overridable: false },
34698
34735
  task: { id: "task", content: `${taskContext}
@@ -34926,8 +34963,10 @@ var init_plan_refine = __esm(() => {
34926
34963
  }
34927
34964
  }),
34928
34965
  fileOutput: (input) => input.outputPath,
34929
- build(input, _ctx) {
34930
- const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile);
34966
+ build(input, ctx) {
34967
+ const agentRouting = ctx.config.routing?.agents;
34968
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
34969
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile, undefined, profiles);
34931
34970
  return {
34932
34971
  role: { id: "role", content: "", overridable: false },
34933
34972
  task: {
@@ -35203,15 +35242,12 @@ Consider:
35203
35242
  });
35204
35243
 
35205
35244
  // src/operations/decompose.ts
35206
- var _decomposeOpDeps, decomposeOp;
35245
+ var decomposeOp;
35207
35246
  var init_decompose2 = __esm(() => {
35208
35247
  init_decompose();
35209
35248
  init_decompose_prompt();
35210
35249
  init_config();
35211
35250
  init_logger2();
35212
- _decomposeOpDeps = {
35213
- getSafeLogger
35214
- };
35215
35251
  decomposeOp = {
35216
35252
  kind: "complete",
35217
35253
  name: "decompose",
@@ -35236,46 +35272,8 @@ var init_decompose2 = __esm(() => {
35236
35272
  task: { id: "task", content: prompt, overridable: false }
35237
35273
  };
35238
35274
  },
35239
- parse(output, _input, ctx) {
35240
- const stories = parseDecomposeOutput(output);
35241
- const agentRouting = ctx.config.routing?.agents;
35242
- if (agentRouting?.enabled !== true) {
35243
- return stories;
35244
- }
35245
- const profiles = agentRouting.profiles ?? [];
35246
- if (profiles.length === 0) {
35247
- return stories;
35248
- }
35249
- const defaultProfile = agentRouting.default ? profiles.find((p) => p.id === agentRouting.default) : undefined;
35250
- return stories.map((story) => {
35251
- if (story.agentProfileId) {
35252
- const profile = profiles.find((p) => p.id === story.agentProfileId);
35253
- if (profile) {
35254
- return {
35255
- ...story,
35256
- routing: {
35257
- ...story.routing,
35258
- agent: profile.target.agent,
35259
- agentProfileId: profile.id,
35260
- profileModelTier: profile.target.model
35261
- }
35262
- };
35263
- }
35264
- _decomposeOpDeps.getSafeLogger()?.warn("decompose", `Story ${story.id} selected unknown agent profile "${story.agentProfileId}" \u2014 falling back to ${defaultProfile ? `default profile "${defaultProfile.id}"` : "no profile"}`, { storyId: story.id, agentProfileId: story.agentProfileId });
35265
- }
35266
- if (defaultProfile) {
35267
- return {
35268
- ...story,
35269
- routing: {
35270
- ...story.routing,
35271
- agent: defaultProfile.target.agent,
35272
- agentProfileId: defaultProfile.id,
35273
- profileModelTier: defaultProfile.target.model
35274
- }
35275
- };
35276
- }
35277
- return story;
35278
- });
35275
+ parse(output, _input, _ctx) {
35276
+ return parseDecomposeOutput(output);
35279
35277
  }
35280
35278
  };
35281
35279
  });
@@ -36140,7 +36138,7 @@ var init_acceptance_generate = __esm(() => {
36140
36138
  stage: "acceptance",
36141
36139
  session: { role: "acceptance-gen", lifetime: "fresh" },
36142
36140
  config: acceptanceGenConfigSelector,
36143
- model: (_input, ctx) => ctx.config.acceptance.model,
36141
+ model: (_input, ctx) => ctx.config.acceptance.generateModel ?? ctx.config.acceptance.model,
36144
36142
  timeoutMs: (_input, ctx) => ctx.config.execution.sessionTimeoutSeconds * 1000,
36145
36143
  build(input, _ctx) {
36146
36144
  const prompt = new AcceptancePromptBuilder().buildGeneratorFromPRDPrompt({
@@ -36244,6 +36242,7 @@ var init_refinement = () => {};
36244
36242
  var acceptanceRefineOp;
36245
36243
  var init_acceptance_refine = __esm(() => {
36246
36244
  init_refinement();
36245
+ init_retry();
36247
36246
  init_config();
36248
36247
  init_logger2();
36249
36248
  init_prompts();
@@ -36253,7 +36252,8 @@ var init_acceptance_refine = __esm(() => {
36253
36252
  stage: "acceptance",
36254
36253
  jsonMode: true,
36255
36254
  config: acceptanceConfigSelector,
36256
- model: (_input, ctx) => ctx.config.acceptance.model,
36255
+ retry: { preset: "transient-network", maxAttempts: 2, baseDelayMs: 0 },
36256
+ model: (_input, ctx) => ctx.config.acceptance.generateModel ?? ctx.config.acceptance.model,
36257
36257
  timeoutMs: (_input, ctx) => ctx.config.acceptance.timeoutMs,
36258
36258
  build(input, _ctx) {
36259
36259
  const prompt = new AcceptancePromptBuilder().buildRefinementPrompt(input.criteria, input.codebaseContext, {
@@ -36268,8 +36268,11 @@ var init_acceptance_refine = __esm(() => {
36268
36268
  };
36269
36269
  },
36270
36270
  parse(output, input, _ctx) {
36271
+ if (!output || !output.trim()) {
36272
+ throw new ParseValidationError("acceptance-refine: empty output");
36273
+ }
36271
36274
  if (refinementWouldFallback(output)) {
36272
- getSafeLogger()?.warn("acceptance", "AC refinement returned no usable JSON \u2014 falling back to unrefined criteria", { storyId: input.storyId, criteriaCount: input.criteria.length, responseBytes: output?.length ?? 0 });
36275
+ getSafeLogger()?.warn("acceptance", "AC refinement returned no usable JSON \u2014 falling back to unrefined criteria", { storyId: input.storyId, criteriaCount: input.criteria.length, responseBytes: output.length });
36273
36276
  }
36274
36277
  const items = parseRefinementResponse(output, input.criteria);
36275
36278
  return items.map((item) => ({ ...item, storyId: item.storyId || input.storyId }));
@@ -38583,8 +38586,10 @@ var init_plan_draft = __esm(() => {
38583
38586
  model: (_input, ctx) => ctx.config.plan?.model ?? "fast",
38584
38587
  timeoutMs: (_input, ctx) => (ctx.config.plan?.timeoutSeconds ?? 600) * 1000,
38585
38588
  retry: (input) => createDraftRetryStrategy(input.citationThreshold),
38586
- build(input, _ctx) {
38587
- return new PlanPromptBuilder().buildDraft(input);
38589
+ build(input, ctx) {
38590
+ const agentRouting = ctx.config.routing?.agents;
38591
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
38592
+ return new PlanPromptBuilder().buildDraft({ ...input, profiles });
38588
38593
  },
38589
38594
  parse(output, input, _ctx) {
38590
38595
  return parsePlanDraft(output, input);
@@ -39088,11 +39093,58 @@ var init_full_suite_gate = __esm(() => {
39088
39093
  };
39089
39094
  });
39090
39095
 
39096
+ // src/operations/full-suite-rectify-op.ts
39097
+ var fullSuiteRectifyOp;
39098
+ var init_full_suite_rectify_op = __esm(() => {
39099
+ init_config();
39100
+ init_prompts();
39101
+ init_test_edit_declaration();
39102
+ fullSuiteRectifyOp = {
39103
+ kind: "run",
39104
+ name: "full-suite-rectify",
39105
+ stage: "rectification",
39106
+ session: { role: "implementer", lifetime: "warm" },
39107
+ config: autofixConfigSelector,
39108
+ build(input, _ctx) {
39109
+ const prompt = RectifierPromptBuilder.failingTestRectification(input.findings, input.story);
39110
+ return {
39111
+ role: { id: "role", content: "", overridable: false },
39112
+ task: { id: "task", content: prompt, overridable: false }
39113
+ };
39114
+ },
39115
+ parse(output, _input, _ctx) {
39116
+ const declarations = parseTestEditDeclarations(output);
39117
+ return { applied: true, testEditDeclarations: declarations };
39118
+ }
39119
+ };
39120
+ });
39121
+
39091
39122
  // src/operations/full-suite-rectify.ts
39092
- function makeFullSuiteRectifyStrategy(story, config2) {
39123
+ function makeFullSuiteRectifyStrategy(story, config2, sink) {
39124
+ const appliesTo = (finding) => finding.source === "test-runner" && (finding.category === "failed-test" || finding.category === "execution-failed");
39125
+ if (sink) {
39126
+ return {
39127
+ name: "full-suite-rectify",
39128
+ appliesTo,
39129
+ fixOp: fullSuiteRectifyOp,
39130
+ buildInput: (findings) => ({ story, findings }),
39131
+ extractApplied: (output) => {
39132
+ for (const d of output.testEditDeclarations) {
39133
+ if (d.reason === "mock_structure" && d.files && d.files.length > 0) {
39134
+ sink.mockHandoffs.push({ files: d.files, reasonDetail: d.reasonDetail ?? "" });
39135
+ } else if (d.reason !== "mock_structure") {
39136
+ sink.testEdits.push(d);
39137
+ }
39138
+ }
39139
+ return { targetFiles: [], summary: "Fixed failing tests" };
39140
+ },
39141
+ maxAttempts: config2.execution.rectification.maxAttemptsPerStrategy,
39142
+ coRun: "exclusive"
39143
+ };
39144
+ }
39093
39145
  return {
39094
39146
  name: "full-suite-rectify",
39095
- appliesTo: (finding) => finding.source === "test-runner" && (finding.category === "failed-test" || finding.category === "execution-failed"),
39147
+ appliesTo,
39096
39148
  fixOp: implementerOp,
39097
39149
  buildInput: (findings) => ({
39098
39150
  story,
@@ -39105,6 +39157,7 @@ function makeFullSuiteRectifyStrategy(story, config2) {
39105
39157
  }
39106
39158
  var init_full_suite_rectify = __esm(() => {
39107
39159
  init_prompts();
39160
+ init_full_suite_rectify_op();
39108
39161
  init_implement();
39109
39162
  });
39110
39163
 
@@ -39232,7 +39285,8 @@ function applyTestEditDeclarations(findings, declarations, story, invalidMockStr
39232
39285
  const valid = validatePrdQuote(prdQuote, story);
39233
39286
  if (valid) {
39234
39287
  result = result.map((f) => {
39235
- if (f.file === d.file && f.fixTarget === "source") {
39288
+ const eligible = f.file === d.file && (f.fixTarget === "source" || f.fixTarget == null && f.source === "test-runner");
39289
+ if (eligible) {
39236
39290
  return {
39237
39291
  ...f,
39238
39292
  fixTarget: "test",
@@ -40141,6 +40195,7 @@ var init_operations = __esm(() => {
40141
40195
  init_greenfield_gate();
40142
40196
  init_full_suite_gate();
40143
40197
  init_full_suite_rectify();
40198
+ init_full_suite_rectify_op();
40144
40199
  init_autofix_implementer_strategy();
40145
40200
  init_autofix_test_writer_strategy();
40146
40201
  init_apply_test_edit_declarations();
@@ -42247,6 +42302,23 @@ ${adversarialErrors}
42247
42302
  Do NOT add new features \u2014 only fix valid issues.
42248
42303
  Commit your fixes when done.${scopeConstraint}${noTestIsolationBlock(story)}${escapeHatchFor(story)}`;
42249
42304
  }
42305
+ function formatFailingTestsList(findings) {
42306
+ if (findings.length === 0) {
42307
+ return "The full test suite has failing tests. Fix the implementation to make all tests pass.";
42308
+ }
42309
+ const lines = [`Fix the following ${findings.length} failing test${findings.length === 1 ? "" : "s"}:
42310
+ `];
42311
+ for (const f of findings) {
42312
+ const location = f.file ? `${f.file}` : "(unknown file)";
42313
+ const rule = f.rule ? ` Test: ${f.rule}
42314
+ ` : "";
42315
+ lines.push(`- ${location}
42316
+ ${rule} Error: ${f.message}
42317
+ `);
42318
+ }
42319
+ return lines.join(`
42320
+ `);
42321
+ }
42250
42322
  function mechanicalRectification(checks3, story, scopeConstraint, opts) {
42251
42323
  const errors3 = formatCheckErrors(checks3, opts);
42252
42324
  const scopeDirective = implementerOwnsTests(story) ? `Fix all errors listed above that are within this story's scope. ${SINGLE_SESSION_PERMIT_HEADLINE}` : `Fix all errors listed above that are within this story's scope \u2014 see the ${exceptionCountWord(story)} narrow exceptions appended below for sibling-story spillover. Do NOT change test files or test behavior except via those exceptions.`;
@@ -42931,22 +43003,23 @@ Before editing, break the work into one small fix per finding above (fix -> re-r
42931
43003
  `);
42932
43004
  }
42933
43005
  static failingTestContext(findings) {
42934
- if (findings.length === 0) {
42935
- return "The full test suite has failing tests. Fix the implementation to make all tests pass.";
42936
- }
42937
- const lines = [`Fix the following ${findings.length} failing test${findings.length === 1 ? "" : "s"}:
42938
- `];
42939
- for (const f of findings) {
42940
- const location = f.file ? `${f.file}` : "(unknown file)";
42941
- const rule = f.rule ? ` Test: ${f.rule}
42942
- ` : "";
42943
- lines.push(`- ${location}
42944
- ${rule} Error: ${f.message}
42945
- `);
42946
- }
42947
- lines.push(`
42948
- Fix the implementation (not the tests) to make all failing tests pass. Run the test suite to verify after each change.`);
42949
- return lines.join(`
43006
+ const listing = formatFailingTestsList(findings);
43007
+ if (findings.length === 0)
43008
+ return listing;
43009
+ return `${listing}
43010
+ Fix the implementation (not the tests) to make all failing tests pass. Run the test suite to verify after each change.`;
43011
+ }
43012
+ static failingTestRectification(findings, story) {
43013
+ const listing = formatFailingTestsList(findings);
43014
+ const exCount = exceptionCountWord(story);
43015
+ const prohibition = `Do NOT change test files or test behavior \u2014 see the ${exCount} narrow exceptions appended below.`;
43016
+ const parts = [listing];
43017
+ parts.push(`
43018
+ Fix the implementation (not the tests) to make all failing tests pass. Do not loosen assertions or weaken test expectations. Run the test suite to verify after each change.`);
43019
+ parts.push(`
43020
+ ${testEditHeadline(story, prohibition)}`);
43021
+ parts.push(escapeHatchFor(story));
43022
+ return parts.join(`
42950
43023
  `);
42951
43024
  }
42952
43025
  }
@@ -43218,7 +43291,13 @@ For each one:
43218
43291
  Write the corrected PRD to this file path: ${outputFilePath}
43219
43292
  Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
43220
43293
  }
43221
- build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers) {
43294
+ build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers, profiles) {
43295
+ const cards = OneShotPromptBuilder.agentCapabilityCards(profiles ?? []);
43296
+ const agentProfilesSection = cards.length > 0 ? `
43297
+
43298
+ ${cards}
43299
+
43300
+ ${OneShotPromptBuilder.agentProfileInstruction()}` : "";
43222
43301
  const isMonorepo = packages && packages.length > 0;
43223
43302
  const packageDetailsSection = packageDetails && packageDetails.length > 0 ? buildPackageDetailsSection(packageDetails) : "";
43224
43303
  const monorepoHint = isMonorepo ? `
@@ -43273,7 +43352,7 @@ ${buildSharedQualityRules(specContent, projectProfile)}
43273
43352
 
43274
43353
  For each story, set "contextFiles" to the key source files the agent should read before implementing (max 5 per story). Use your Step 2 analysis to identify the most relevant files. Leave empty for greenfield stories with no existing files to reference. Set "expectedFiles" to the NEW files the story creates.
43275
43354
 
43276
- ${CONTEXT_VS_EXPECTED_FILES_RULE}`;
43355
+ ${CONTEXT_VS_EXPECTED_FILES_RULE}${agentProfilesSection}`;
43277
43356
  const suggestedCriteriaField = specContent.trim() ? `
43278
43357
  "suggestedCriteria": ["string \u2014 optional. Behavioral edge cases or negative paths you identified that are NOT in the spec. Plain assertions only \u2014 observable outputs, return values, state changes, or error conditions. No implementation details or vague descriptions. Omit this field if empty."],` : "";
43279
43358
  const outputDirective = outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
@@ -43305,7 +43384,8 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
43305
43384
  "complexity": "simple | medium | complex | expert",
43306
43385
  "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
43307
43386
  "noTestJustification": "string \u2014 REQUIRED when testStrategy is no-test, explains why tests are unnecessary",
43308
- "reasoning": "string \u2014 brief classification rationale"
43387
+ "reasoning": "string \u2014 brief classification rationale"${cards.length > 0 ? `,
43388
+ "agentProfileId": "string \u2014 optional, the id of the best-matching profile from the Agent Profiles table above; omit if none fits"` : ""}
43309
43389
  },
43310
43390
  "escalations": [],
43311
43391
  "attempts": 0
@@ -43322,6 +43402,12 @@ ${outputDirective}`;
43322
43402
  content: "You are a senior software architect generating a product requirements document (PRD) as JSON. Your intent is to produce a thorough, evidence-grounded plan.",
43323
43403
  overridable: false
43324
43404
  };
43405
+ const cards = OneShotPromptBuilder.agentCapabilityCards(input.profiles ?? []);
43406
+ const agentProfilesSection = cards.length > 0 ? `
43407
+
43408
+ ${cards}
43409
+
43410
+ ${OneShotPromptBuilder.agentProfileInstruction()}` : "";
43325
43411
  const revisionSection = input.revisionFindings && input.revisionFindings.length > 0 ? `
43326
43412
 
43327
43413
  ## Previous draft rejected for the following issues
@@ -43371,7 +43457,7 @@ ${buildSharedQualityRules(input.specContent, input.projectProfile)}
43371
43457
 
43372
43458
  For each story, set "contextFiles" to the key source files the implementer should read before starting (max 5 per story). Cite manifest factIds where relevant. Set "expectedFiles" to the NEW files the story creates.
43373
43459
 
43374
- ${CONTEXT_VS_EXPECTED_FILES_RULE}
43460
+ ${CONTEXT_VS_EXPECTED_FILES_RULE}${agentProfilesSection}
43375
43461
 
43376
43462
  ## Output Schema
43377
43463
 
@@ -43394,7 +43480,8 @@ Produce a JSON object with this exact structure. Field names are mandatory \u201
43394
43480
  "routing": {
43395
43481
  "complexity": "simple | medium | complex | expert",
43396
43482
  "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
43397
- "reasoning": "string \u2014 brief classification rationale"
43483
+ "reasoning": "string \u2014 brief classification rationale"${cards.length > 0 ? `,
43484
+ "agentProfileId": "string \u2014 optional, the id of the best-matching profile from the Agent Profiles table above; omit if none fits"` : ""}
43398
43485
  }
43399
43486
  }
43400
43487
  ]
@@ -43434,6 +43521,7 @@ var CONTEXT_VS_EXPECTED_FILES_RULE = `**\`contextFiles\` rule \u2014 files reada
43434
43521
  **\`expectedFiles\` rule \u2014 files THIS story CREATES.** List every NEW file this story authors (relative paths). A file this story creates belongs here, NEVER in \`contextFiles\` \u2014 these are the story's outputs, not files to read first. A file created by an upstream dependency and only read/modified here belongs in \`contextFiles\`, NOT here (this story does not author it). A single path may appear in \`contextFiles\` (an existing sibling to mirror) AND \`expectedFiles\` (the new file itself), but the same path must never be in both.`, EXPECTED_FILES_SCHEMA_FIELD = `"expectedFiles": ["string \u2014 NEW files this story creates (relative paths, omit if none)"],`;
43435
43522
  var init_plan_builder = __esm(() => {
43436
43523
  init_config();
43524
+ init_one_shot_builder();
43437
43525
  });
43438
43526
 
43439
43527
  // src/prompts/builders/grounder-builder.ts
@@ -44645,11 +44733,22 @@ function buildTxtContent(entry) {
44645
44733
  "",
44646
44734
  "=== RESPONSE ===",
44647
44735
  "",
44648
- entry.response
44736
+ entry.response,
44737
+ ...buildInteractionLines(entry.interactions)
44649
44738
  ];
44650
44739
  return lines.join(`
44651
44740
  `);
44652
44741
  }
44742
+ function buildInteractionLines(interactions) {
44743
+ if (!interactions?.length)
44744
+ return [];
44745
+ const lines = ["", "=== INTERACTIONS ===", ""];
44746
+ for (const ix of interactions) {
44747
+ lines.push(`[turn ${ix.turnIndex}] Q: ${ix.question}`, ` A: ${ix.reply}`, "");
44748
+ }
44749
+ lines.pop();
44750
+ return lines;
44751
+ }
44653
44752
 
44654
44753
  class PromptAuditor {
44655
44754
  _queue = Promise.resolve();
@@ -45158,7 +45257,8 @@ function attachAuditSubscriber(bus, auditor, runId) {
45158
45257
  ...event.kind === "session-turn" && {
45159
45258
  sessionId: event.protocolIds.sessionId ?? null,
45160
45259
  recordId: event.protocolIds.recordId ?? null,
45161
- turn: event.turn
45260
+ turn: event.turn,
45261
+ ...event.interactions?.length ? { interactions: event.interactions } : {}
45162
45262
  }
45163
45263
  };
45164
45264
  auditor.record(entry);
@@ -51771,7 +51871,7 @@ var init_precheck = __esm(() => {
51771
51871
  });
51772
51872
 
51773
51873
  // src/prd/decompose-mapper.ts
51774
- function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir) {
51874
+ function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir, parentRouting) {
51775
51875
  return stories.map((story, entryIndex) => {
51776
51876
  if (!story.id) {
51777
51877
  throw new NaxError(`Entry at index ${entryIndex} is missing required field: id`, "DECOMPOSE_VALIDATION_FAILED", {
@@ -51805,10 +51905,19 @@ function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir
51805
51905
  complexity: story.complexity,
51806
51906
  testStrategy: story.testStrategy ?? "test-after",
51807
51907
  reasoning: story.reasoning,
51808
- modelTier: story.routing?.profileModelTier ?? "balanced",
51908
+ modelTier: parentRouting?.profileModelTier ?? story.routing?.profileModelTier ?? "balanced",
51809
51909
  ...story.routing?.agent !== undefined && { agent: story.routing.agent },
51810
51910
  ...story.routing?.agentProfileId !== undefined && { agentProfileId: story.routing.agentProfileId },
51811
- ...story.routing?.profileModelTier !== undefined && { profileModelTier: story.routing.profileModelTier }
51911
+ ...story.routing?.profileModelTier !== undefined && { profileModelTier: story.routing.profileModelTier },
51912
+ ...parentRouting?.agent !== undefined && { agent: parentRouting.agent },
51913
+ ...parentRouting?.agentProfileId !== undefined && { agentProfileId: parentRouting.agentProfileId },
51914
+ ...parentRouting?.profileModelTier !== undefined && { profileModelTier: parentRouting.profileModelTier },
51915
+ ...parentRouting?.agent !== undefined && {
51916
+ initialAgent: parentRouting.initialAgent ?? parentRouting.agent
51917
+ },
51918
+ ...parentRouting?.agentProfileId !== undefined && {
51919
+ initialProfileId: parentRouting.initialProfileId ?? parentRouting.agentProfileId
51920
+ }
51812
51921
  }
51813
51922
  };
51814
51923
  });
@@ -51874,8 +51983,7 @@ async function planDecomposeCommand(workdir, config2, options) {
51874
51983
  for (let attempt = 0;attempt < maxReplanAttempts; attempt++) {
51875
51984
  if (attempt === 0 && debateDecompEnabled) {
51876
51985
  const decomposeStageConfig = debateStages.decompose;
51877
- const agentRoutingForDebate = config2.routing?.agents;
51878
- const profilesForDebate = agentRoutingForDebate?.enabled === true ? agentRoutingForDebate.profiles ?? [] : [];
51986
+ const profilesForDebate = [];
51879
51987
  const prompt = await buildDecomposePromptAsync({
51880
51988
  specContent: "",
51881
51989
  codebaseContext,
@@ -51950,7 +52058,7 @@ ${repairHint}` : codebaseContext;
51950
52058
  } finally {
51951
52059
  await rt.close().catch(() => {});
51952
52060
  }
51953
- const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId, targetStory.workdir);
52061
+ const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId, targetStory.workdir, targetStory.routing);
51954
52062
  const updatedStories = prd.userStories.map((s) => s.id === options.storyId ? { ...s, status: "decomposed" } : s);
51955
52063
  const originalIndex = updatedStories.findIndex((s) => s.id === options.storyId);
51956
52064
  const finalStories = [
@@ -52912,15 +53020,28 @@ async function processPackageGroup(ctx, packageDir, groupStories, language, resu
52912
53020
  featureName: ctx.prd.feature,
52913
53021
  agentName: ctx.agentManager.getDefault()
52914
53022
  };
52915
- const refined = await _hardeningDeps.callOp(callCtx, acceptanceRefineOp, {
52916
- criteria: story.suggestedCriteria ?? [],
52917
- codebaseContext: "",
52918
- storyId: story.id,
52919
- testStrategy: ctx.config.acceptance?.testStrategy,
52920
- testFramework: ctx.config.acceptance?.testFramework,
52921
- storyTitle: story.title,
52922
- storyDescription: story.description
52923
- });
53023
+ let refined;
53024
+ try {
53025
+ refined = await _hardeningDeps.callOp(callCtx, acceptanceRefineOp, {
53026
+ criteria: story.suggestedCriteria ?? [],
53027
+ codebaseContext: "",
53028
+ storyId: story.id,
53029
+ testStrategy: ctx.config.acceptance?.testStrategy,
53030
+ testFramework: ctx.config.acceptance?.testFramework,
53031
+ storyTitle: story.title,
53032
+ storyDescription: story.description
53033
+ });
53034
+ } catch {
53035
+ logger?.warn("acceptance", "AC refinement failed after retries \u2014 using unrefined criteria", {
53036
+ storyId: story.id
53037
+ });
53038
+ refined = (story.suggestedCriteria ?? []).map((c) => ({
53039
+ original: c,
53040
+ refined: c,
53041
+ testable: true,
53042
+ storyId: story.id
53043
+ }));
53044
+ }
52924
53045
  groupRefined.push(...refined);
52925
53046
  }
52926
53047
  const suggestedTestPath = resolveSuggestedPackageFeatureTestPath(packageDir, ctx.prd.feature, ctx.config.acceptance?.suggestedTestPath, language);
@@ -53066,6 +53187,7 @@ __export(exports_acceptance, {
53066
53187
  resolveSuggestedTestFile: () => resolveSuggestedTestFile,
53067
53188
  resolveSuggestedPackageFeatureTestPath: () => resolveSuggestedPackageFeatureTestPath,
53068
53189
  resolveAcceptanceFeatureTestPath: () => resolveAcceptanceFeatureTestPath,
53190
+ refinementWouldFallback: () => refinementWouldFallback,
53069
53191
  parseRefinementResponse: () => parseRefinementResponse,
53070
53192
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
53071
53193
  parseACTextFromSpec: () => parseACTextFromSpec,
@@ -53480,6 +53602,16 @@ ${stderr}` };
53480
53602
  storyDescription: story.description
53481
53603
  }, story.id).then((refined) => {
53482
53604
  results[i] = refined;
53605
+ }).catch(() => {
53606
+ getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
53607
+ storyId: story.id
53608
+ });
53609
+ results[i] = story.acceptanceCriteria.map((c) => ({
53610
+ original: c,
53611
+ refined: c,
53612
+ testable: true,
53613
+ storyId: story.id
53614
+ }));
53483
53615
  }).finally(() => {
53484
53616
  executing.delete(task);
53485
53617
  });
@@ -55039,7 +55171,8 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
55039
55171
  break;
55040
55172
  }
55041
55173
  }
55042
- const validated = rectification.postValidate ? await rectification.postValidate(findings, _validateCtx) : findings;
55174
+ const postValidateFn = overrides?.postValidate ?? rectification.postValidate;
55175
+ const validated = postValidateFn ? await postValidateFn(findings, _validateCtx) : findings;
55043
55176
  return { findings: validated, shortCircuited };
55044
55177
  }
55045
55178
  };
@@ -55209,7 +55342,8 @@ class ExecutionPlan {
55209
55342
  strategies: this.state.nonBlockingFixStrategies ?? [],
55210
55343
  excludePhaseKinds: nonBlockingExcludePhases(),
55211
55344
  extraRevalidationKinds: nonBlockingExtraPhases(advCfg),
55212
- maxAttempts
55345
+ maxAttempts,
55346
+ postValidate: this.state.nonBlockingFixPostValidate
55213
55347
  })
55214
55348
  });
55215
55349
  }
@@ -55305,9 +55439,10 @@ class StoryOrchestratorBuilder {
55305
55439
  this.state.rectification = opts;
55306
55440
  return this;
55307
55441
  }
55308
- addNonBlockingFix(cfg, strategies) {
55442
+ addNonBlockingFix(cfg, strategies, postValidate) {
55309
55443
  this.state.nonBlockingFix = cfg;
55310
55444
  this.state.nonBlockingFixStrategies = strategies;
55445
+ this.state.nonBlockingFixPostValidate = postValidate;
55311
55446
  return this;
55312
55447
  }
55313
55448
  build(ctx, opts = {}) {
@@ -55437,10 +55572,10 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55437
55572
  if (inputs.adversarialReview) {
55438
55573
  builder.addAdversarialReview(inputs.adversarialReview);
55439
55574
  }
55575
+ const packageDir = join47(ctx.packageDir, story.workdir ?? "");
55576
+ const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
55440
55577
  if (shouldRunRectification(config2) && inputs.rectification) {
55441
55578
  const sink = makeDeclarationSink();
55442
- const packageDir = join47(ctx.packageDir, story.workdir ?? "");
55443
- const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
55444
55579
  const strategies = [];
55445
55580
  const pkgQuality = ctx.packageView.select(qualityConfigSelector).quality;
55446
55581
  if (pkgQuality?.commands?.lintFix || pkgQuality?.commands?.lintFixScoped) {
@@ -55450,7 +55585,7 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55450
55585
  strategies.push(makeMechanicalFormatFixStrategy());
55451
55586
  }
55452
55587
  if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
55453
- strategies.push(makeFullSuiteRectifyStrategy(story, config2));
55588
+ strategies.push(makeFullSuiteRectifyStrategy(story, config2, sink));
55454
55589
  }
55455
55590
  if (config2.quality.autofix?.enabled !== false) {
55456
55591
  strategies.push(makeAutofixImplementerStrategy(story, config2, sink, {
@@ -55495,8 +55630,23 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55495
55630
  includeAdversarialReview: false
55496
55631
  }), makeAutofixTestWriterStrategy(story, config2, nbSink));
55497
55632
  }
55498
- nbStrategies.push(makeFullSuiteRectifyStrategy(story, config2));
55499
- builder.addNonBlockingFix(nbf, nbStrategies);
55633
+ nbStrategies.push(makeFullSuiteRectifyStrategy(story, config2, nbSink));
55634
+ const nbPostValidate = async (findings, _validateCtx) => {
55635
+ if (nbSink.testEdits.length === 0 && nbSink.mockHandoffs.length === 0)
55636
+ return findings;
55637
+ const pendingMock = nbSink.mockHandoffs.map((h) => ({
55638
+ reason: "mock_structure",
55639
+ file: h.files[0] ?? "",
55640
+ files: h.files,
55641
+ reasonDetail: h.reasonDetail
55642
+ }));
55643
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
55644
+ nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
55645
+ const allDeclarations = [...nbSink.testEdits, ...valid];
55646
+ nbSink.testEdits = [];
55647
+ return applyTestEditDeclarations(findings, allDeclarations, story, invalid);
55648
+ };
55649
+ builder.addNonBlockingFix(nbf, nbStrategies, nbPostValidate);
55500
55650
  }
55501
55651
  return builder.build(ctx, { isThreeSession });
55502
55652
  }
@@ -60095,7 +60245,7 @@ var package_default;
60095
60245
  var init_package = __esm(() => {
60096
60246
  package_default = {
60097
60247
  name: "@nathapp/nax",
60098
- version: "0.70.0-canary.1",
60248
+ version: "0.70.0-canary.3",
60099
60249
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60100
60250
  type: "module",
60101
60251
  bin: {
@@ -60190,8 +60340,8 @@ var init_version = __esm(() => {
60190
60340
  NAX_VERSION = package_default.version;
60191
60341
  NAX_COMMIT = (() => {
60192
60342
  try {
60193
- if (/^[0-9a-f]{6,10}$/.test("f895b7d3"))
60194
- return "f895b7d3";
60343
+ if (/^[0-9a-f]{6,10}$/.test("905b80cf"))
60344
+ return "905b80cf";
60195
60345
  } catch {}
60196
60346
  try {
60197
60347
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -95450,6 +95600,7 @@ async function buildPlanModeContext(workdir, fullConfig, options, deps) {
95450
95600
  const branchName = options.branch ?? `feat/${options.feature}`;
95451
95601
  const timeoutSeconds = fullConfig?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
95452
95602
  const config2 = planConfigSelector.select(fullConfig);
95603
+ const profileName = typeof fullConfig?.profile === "string" && fullConfig.profile ? fullConfig.profile : undefined;
95453
95604
  const runtime = createPlanRuntime(fullConfig, workdir, options.feature);
95454
95605
  const interactionChain = fullConfig ? await deps.initInteractionChain(fullConfig, !process.stdin.isTTY) : null;
95455
95606
  let configuredBridge;
@@ -95477,6 +95628,7 @@ async function buildPlanModeContext(workdir, fullConfig, options, deps) {
95477
95628
  branchName,
95478
95629
  timeoutSeconds,
95479
95630
  config: config2,
95631
+ profileName,
95480
95632
  options,
95481
95633
  runtime,
95482
95634
  interactionChain,
@@ -95525,6 +95677,28 @@ function buildPlanComposition(userStageConfig) {
95525
95677
  // src/plan/strategies/write-prd.ts
95526
95678
  init_errors();
95527
95679
  init_prd();
95680
+
95681
+ // src/plan/strategies/finalize-routing.ts
95682
+ init_agents();
95683
+ function finalizePrdRouting(prd, agentRouting, profileName) {
95684
+ const userStories = prd.userStories.map((story) => {
95685
+ const assignment = resolveAgentAssignment(story.routing?.agentProfileId, agentRouting, story.id);
95686
+ if (!assignment)
95687
+ return story;
95688
+ const routing = {
95689
+ ...story.routing,
95690
+ agent: assignment.agent,
95691
+ agentProfileId: assignment.agentProfileId,
95692
+ profileModelTier: assignment.profileModelTier,
95693
+ initialAgent: story.routing?.initialAgent ?? assignment.agent,
95694
+ initialProfileId: story.routing?.initialProfileId ?? assignment.agentProfileId
95695
+ };
95696
+ return { ...story, routing };
95697
+ });
95698
+ return { ...prd, userStories, routingProfile: profileName ?? "default" };
95699
+ }
95700
+
95701
+ // src/plan/strategies/write-prd.ts
95528
95702
  async function writeOrRecoverPrd(ctx, prd, err) {
95529
95703
  const tryExtractPrd = (value) => {
95530
95704
  if (value === null || typeof value !== "object")
@@ -95543,12 +95717,14 @@ async function writeOrRecoverPrd(ctx, prd, err) {
95543
95717
  };
95544
95718
  if (prd !== null) {
95545
95719
  if (Array.isArray(prd.userStories)) {
95546
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...prd, project: ctx.projectName }, null, 2));
95720
+ const finalized = finalizePrdRouting({ ...prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95721
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95547
95722
  return ctx.outputPath;
95548
95723
  }
95549
95724
  const normalizedPrd = tryExtractPrd(prd);
95550
95725
  if (normalizedPrd !== null) {
95551
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...normalizedPrd, project: ctx.projectName }, null, 2));
95726
+ const finalized = finalizePrdRouting({ ...normalizedPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95727
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95552
95728
  return ctx.outputPath;
95553
95729
  }
95554
95730
  }
@@ -95568,7 +95744,8 @@ async function writeOrRecoverPrd(ctx, prd, err) {
95568
95744
  }
95569
95745
  }
95570
95746
  recoveredPrd = recoveredPrd ?? validatePlanOutput(rawContent, ctx.options.feature, ctx.branchName);
95571
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...recoveredPrd, project: ctx.projectName }, null, 2));
95747
+ const finalized = finalizePrdRouting({ ...recoveredPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95748
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95572
95749
  return ctx.outputPath;
95573
95750
  } catch {
95574
95751
  throw err;
@@ -95587,7 +95764,9 @@ var _debatePlanDeps = {
95587
95764
  class DebatePlanStrategy {
95588
95765
  mode = "debate";
95589
95766
  async execute(ctx) {
95590
- const { taskContext, outputFormat } = new PlanPromptBuilder().build(ctx.specContent, ctx.codebaseContext, undefined, ctx.relativePackages, ctx.packageDetails, ctx.config.project);
95767
+ const agentRouting = ctx.config.routing?.agents;
95768
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
95769
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(ctx.specContent, ctx.codebaseContext, undefined, ctx.relativePackages, ctx.packageDetails, ctx.config.project, undefined, profiles);
95591
95770
  const planStage = ctx.config.debate?.stages?.plan;
95592
95771
  if (!planStage) {
95593
95772
  throw new NaxError("[plan] debate strategy requires config.debate.stages.plan", "PLAN_DEBATE_STAGE_CONFIG_MISSING", {
@@ -95723,7 +95902,8 @@ class PipelinePlanStrategy {
95723
95902
  if (verdict.outcome !== "passed") {
95724
95903
  throw new NaxError(verdict.specDeltasPath ? `Plan pipeline failed; see ${verdict.specDeltasPath}` : "Plan pipeline failed with no spec-deltas path", "PLAN_CRITIC_BLOCKED", { stage: "plan", specDeltasPath: verdict.specDeltasPath });
95725
95904
  }
95726
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...verdict.prd, project: ctx.projectName }, null, 2));
95905
+ const prdToWrite = finalizePrdRouting({ ...verdict.prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95906
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(prdToWrite, null, 2));
95727
95907
  return ctx.outputPath;
95728
95908
  } finally {
95729
95909
  await ctx.runtime.close().catch(() => {});
@@ -95804,13 +95984,15 @@ class SinglePlanStrategy {
95804
95984
  projectProfile: ctx.config.project
95805
95985
  });
95806
95986
  assertIsValidPrd(prd);
95807
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...prd, project: ctx.projectName }, null, 2));
95987
+ const finalized = finalizePrdRouting({ ...prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95988
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95808
95989
  return ctx.outputPath;
95809
95990
  } catch (err) {
95810
95991
  if (ctx.deps.existsSync(ctx.outputPath)) {
95811
95992
  const rawContent = await ctx.deps.readFile(ctx.outputPath);
95812
95993
  const recoveredPrd = validatePlanOutput(rawContent, ctx.options.feature, ctx.branchName);
95813
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...recoveredPrd, project: ctx.projectName }, null, 2));
95994
+ const finalizedRecovered = finalizePrdRouting({ ...recoveredPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95995
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalizedRecovered, null, 2));
95814
95996
  return ctx.outputPath;
95815
95997
  }
95816
95998
  throw err;
@@ -96856,6 +97038,7 @@ var FIELD_DESCRIPTIONS = {
96856
97038
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
96857
97039
  "acceptance.command": "Override command to run acceptance tests. Use {{FILE}} as placeholder for the test file path (default: 'bun test {{FILE}} --timeout=60000')",
96858
97040
  "acceptance.model": 'Model selector for acceptance generation/refinement LLM calls. Accepts a tier string such as "fast", "balanced", or "powerful", or an explicit object like { agent: "codex", model: "gpt-5.4" }. Default: "fast".',
97041
+ "acceptance.generateModel": 'Model selector specifically for acceptance test generation and criteria refinement LLM calls. When set, overrides acceptance.model for these two operations. Accepts a tier string such as "fast", "balanced", or "powerful", or an explicit object like { agent: "opencode", model: "opencode-go/deepseek-v4-flash" }. Defaults to acceptance.model when not set.',
96859
97042
  "acceptance.refinement": "Enable acceptance criteria refinement step before execution (default: true). Disable to skip refinement and use generated criteria as-is.",
96860
97043
  "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
96861
97044
  context: "Context injection configuration",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.70.0-canary.1",
3
+ "version": "0.70.0-canary.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {