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

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 +255 -106
  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");
@@ -22251,6 +22252,34 @@ function resolveDefaultAgent(config2) {
22251
22252
  }
22252
22253
  var FALLBACK_DEFAULT_AGENT = "claude";
22253
22254
 
22255
+ // src/agents/shared/agent-profile-resolver.ts
22256
+ function resolveAgentAssignment(selectedProfileId, agentRouting, storyId) {
22257
+ if (agentRouting?.enabled !== true)
22258
+ return null;
22259
+ const profiles = agentRouting.profiles ?? [];
22260
+ if (profiles.length === 0)
22261
+ return null;
22262
+ const defaultProfile = agentRouting.default ? profiles.find((p) => p.id === agentRouting.default) : undefined;
22263
+ if (selectedProfileId) {
22264
+ const profile = profiles.find((p) => p.id === selectedProfileId);
22265
+ if (profile)
22266
+ return toAssignment(profile);
22267
+ getSafeLogger()?.warn("routing", `Story ${storyId} selected unknown agent profile "${selectedProfileId}" \u2014 falling back to ${defaultProfile ? `default profile "${defaultProfile.id}"` : "no profile"}`, { storyId, agentProfileId: selectedProfileId });
22268
+ }
22269
+ return defaultProfile ? toAssignment(defaultProfile) : null;
22270
+ }
22271
+ function toAssignment(p) {
22272
+ return { agent: p.target.agent, agentProfileId: p.id, profileModelTier: p.target.model };
22273
+ }
22274
+ var init_agent_profile_resolver = __esm(() => {
22275
+ init_logger2();
22276
+ });
22277
+
22278
+ // src/agents/shared/index.ts
22279
+ var init_shared = __esm(() => {
22280
+ init_agent_profile_resolver();
22281
+ });
22282
+
22254
22283
  // src/agents/retry/types.ts
22255
22284
  var ParseValidationError;
22256
22285
  var init_types4 = __esm(() => {
@@ -22497,6 +22526,7 @@ var init_agents = __esm(() => {
22497
22526
  init_cost();
22498
22527
  init_version_detection();
22499
22528
  init_manager();
22529
+ init_shared();
22500
22530
  init_retry();
22501
22531
  });
22502
22532
 
@@ -28189,7 +28219,8 @@ function validateStory(raw, index, allIds) {
28189
28219
  complexity,
28190
28220
  testStrategy,
28191
28221
  reasoning: typeof routing.reasoning === "string" && routing.reasoning.trim().length > 0 ? routing.reasoning.trim() : "validated from LLM output",
28192
- ...noTestJustification !== undefined ? { noTestJustification } : {}
28222
+ ...noTestJustification !== undefined ? { noTestJustification } : {},
28223
+ ...typeof routing.agentProfileId === "string" && routing.agentProfileId.trim().length > 0 ? { agentProfileId: routing.agentProfileId.trim() } : {}
28193
28224
  },
28194
28225
  ...workdir !== undefined ? { workdir } : {},
28195
28226
  ...contextFiles.length > 0 ? { contextFiles } : {},
@@ -34691,8 +34722,10 @@ var init_plan = __esm(() => {
34691
34722
  truncated: () => PlanPromptBuilder.jsonRepair(0, "JSON appears truncated \u2014 please rewrite completely")
34692
34723
  }
34693
34724
  }),
34694
- build(input, _ctx) {
34695
- const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile);
34725
+ build(input, ctx) {
34726
+ const agentRouting = ctx.config.routing?.agents;
34727
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
34728
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile, undefined, profiles);
34696
34729
  return {
34697
34730
  role: { id: "role", content: "", overridable: false },
34698
34731
  task: { id: "task", content: `${taskContext}
@@ -34926,8 +34959,10 @@ var init_plan_refine = __esm(() => {
34926
34959
  }
34927
34960
  }),
34928
34961
  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);
34962
+ build(input, ctx) {
34963
+ const agentRouting = ctx.config.routing?.agents;
34964
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
34965
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(input.specContent, input.codebaseContext, input.outputPath, input.packages, input.packageDetails, input.projectProfile, undefined, profiles);
34931
34966
  return {
34932
34967
  role: { id: "role", content: "", overridable: false },
34933
34968
  task: {
@@ -35203,15 +35238,12 @@ Consider:
35203
35238
  });
35204
35239
 
35205
35240
  // src/operations/decompose.ts
35206
- var _decomposeOpDeps, decomposeOp;
35241
+ var decomposeOp;
35207
35242
  var init_decompose2 = __esm(() => {
35208
35243
  init_decompose();
35209
35244
  init_decompose_prompt();
35210
35245
  init_config();
35211
35246
  init_logger2();
35212
- _decomposeOpDeps = {
35213
- getSafeLogger
35214
- };
35215
35247
  decomposeOp = {
35216
35248
  kind: "complete",
35217
35249
  name: "decompose",
@@ -35236,46 +35268,8 @@ var init_decompose2 = __esm(() => {
35236
35268
  task: { id: "task", content: prompt, overridable: false }
35237
35269
  };
35238
35270
  },
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
- });
35271
+ parse(output, _input, _ctx) {
35272
+ return parseDecomposeOutput(output);
35279
35273
  }
35280
35274
  };
35281
35275
  });
@@ -36140,7 +36134,7 @@ var init_acceptance_generate = __esm(() => {
36140
36134
  stage: "acceptance",
36141
36135
  session: { role: "acceptance-gen", lifetime: "fresh" },
36142
36136
  config: acceptanceGenConfigSelector,
36143
- model: (_input, ctx) => ctx.config.acceptance.model,
36137
+ model: (_input, ctx) => ctx.config.acceptance.generateModel ?? ctx.config.acceptance.model,
36144
36138
  timeoutMs: (_input, ctx) => ctx.config.execution.sessionTimeoutSeconds * 1000,
36145
36139
  build(input, _ctx) {
36146
36140
  const prompt = new AcceptancePromptBuilder().buildGeneratorFromPRDPrompt({
@@ -36244,6 +36238,7 @@ var init_refinement = () => {};
36244
36238
  var acceptanceRefineOp;
36245
36239
  var init_acceptance_refine = __esm(() => {
36246
36240
  init_refinement();
36241
+ init_retry();
36247
36242
  init_config();
36248
36243
  init_logger2();
36249
36244
  init_prompts();
@@ -36253,7 +36248,8 @@ var init_acceptance_refine = __esm(() => {
36253
36248
  stage: "acceptance",
36254
36249
  jsonMode: true,
36255
36250
  config: acceptanceConfigSelector,
36256
- model: (_input, ctx) => ctx.config.acceptance.model,
36251
+ retry: { preset: "transient-network", maxAttempts: 2, baseDelayMs: 0 },
36252
+ model: (_input, ctx) => ctx.config.acceptance.generateModel ?? ctx.config.acceptance.model,
36257
36253
  timeoutMs: (_input, ctx) => ctx.config.acceptance.timeoutMs,
36258
36254
  build(input, _ctx) {
36259
36255
  const prompt = new AcceptancePromptBuilder().buildRefinementPrompt(input.criteria, input.codebaseContext, {
@@ -36268,8 +36264,11 @@ var init_acceptance_refine = __esm(() => {
36268
36264
  };
36269
36265
  },
36270
36266
  parse(output, input, _ctx) {
36267
+ if (!output || !output.trim()) {
36268
+ throw new ParseValidationError("acceptance-refine: empty output");
36269
+ }
36271
36270
  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 });
36271
+ 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
36272
  }
36274
36273
  const items = parseRefinementResponse(output, input.criteria);
36275
36274
  return items.map((item) => ({ ...item, storyId: item.storyId || input.storyId }));
@@ -38583,8 +38582,10 @@ var init_plan_draft = __esm(() => {
38583
38582
  model: (_input, ctx) => ctx.config.plan?.model ?? "fast",
38584
38583
  timeoutMs: (_input, ctx) => (ctx.config.plan?.timeoutSeconds ?? 600) * 1000,
38585
38584
  retry: (input) => createDraftRetryStrategy(input.citationThreshold),
38586
- build(input, _ctx) {
38587
- return new PlanPromptBuilder().buildDraft(input);
38585
+ build(input, ctx) {
38586
+ const agentRouting = ctx.config.routing?.agents;
38587
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
38588
+ return new PlanPromptBuilder().buildDraft({ ...input, profiles });
38588
38589
  },
38589
38590
  parse(output, input, _ctx) {
38590
38591
  return parsePlanDraft(output, input);
@@ -39088,11 +39089,58 @@ var init_full_suite_gate = __esm(() => {
39088
39089
  };
39089
39090
  });
39090
39091
 
39092
+ // src/operations/full-suite-rectify-op.ts
39093
+ var fullSuiteRectifyOp;
39094
+ var init_full_suite_rectify_op = __esm(() => {
39095
+ init_config();
39096
+ init_prompts();
39097
+ init_test_edit_declaration();
39098
+ fullSuiteRectifyOp = {
39099
+ kind: "run",
39100
+ name: "full-suite-rectify",
39101
+ stage: "rectification",
39102
+ session: { role: "implementer", lifetime: "warm" },
39103
+ config: autofixConfigSelector,
39104
+ build(input, _ctx) {
39105
+ const prompt = RectifierPromptBuilder.failingTestRectification(input.findings, input.story);
39106
+ return {
39107
+ role: { id: "role", content: "", overridable: false },
39108
+ task: { id: "task", content: prompt, overridable: false }
39109
+ };
39110
+ },
39111
+ parse(output, _input, _ctx) {
39112
+ const declarations = parseTestEditDeclarations(output);
39113
+ return { applied: true, testEditDeclarations: declarations };
39114
+ }
39115
+ };
39116
+ });
39117
+
39091
39118
  // src/operations/full-suite-rectify.ts
39092
- function makeFullSuiteRectifyStrategy(story, config2) {
39119
+ function makeFullSuiteRectifyStrategy(story, config2, sink) {
39120
+ const appliesTo = (finding) => finding.source === "test-runner" && (finding.category === "failed-test" || finding.category === "execution-failed");
39121
+ if (sink) {
39122
+ return {
39123
+ name: "full-suite-rectify",
39124
+ appliesTo,
39125
+ fixOp: fullSuiteRectifyOp,
39126
+ buildInput: (findings) => ({ story, findings }),
39127
+ extractApplied: (output) => {
39128
+ for (const d of output.testEditDeclarations) {
39129
+ if (d.reason === "mock_structure" && d.files && d.files.length > 0) {
39130
+ sink.mockHandoffs.push({ files: d.files, reasonDetail: d.reasonDetail ?? "" });
39131
+ } else if (d.reason !== "mock_structure") {
39132
+ sink.testEdits.push(d);
39133
+ }
39134
+ }
39135
+ return { targetFiles: [], summary: "Fixed failing tests" };
39136
+ },
39137
+ maxAttempts: config2.execution.rectification.maxAttemptsPerStrategy,
39138
+ coRun: "exclusive"
39139
+ };
39140
+ }
39093
39141
  return {
39094
39142
  name: "full-suite-rectify",
39095
- appliesTo: (finding) => finding.source === "test-runner" && (finding.category === "failed-test" || finding.category === "execution-failed"),
39143
+ appliesTo,
39096
39144
  fixOp: implementerOp,
39097
39145
  buildInput: (findings) => ({
39098
39146
  story,
@@ -39105,6 +39153,7 @@ function makeFullSuiteRectifyStrategy(story, config2) {
39105
39153
  }
39106
39154
  var init_full_suite_rectify = __esm(() => {
39107
39155
  init_prompts();
39156
+ init_full_suite_rectify_op();
39108
39157
  init_implement();
39109
39158
  });
39110
39159
 
@@ -39232,7 +39281,8 @@ function applyTestEditDeclarations(findings, declarations, story, invalidMockStr
39232
39281
  const valid = validatePrdQuote(prdQuote, story);
39233
39282
  if (valid) {
39234
39283
  result = result.map((f) => {
39235
- if (f.file === d.file && f.fixTarget === "source") {
39284
+ const eligible = f.file === d.file && (f.fixTarget === "source" || f.fixTarget == null && f.source === "test-runner");
39285
+ if (eligible) {
39236
39286
  return {
39237
39287
  ...f,
39238
39288
  fixTarget: "test",
@@ -40141,6 +40191,7 @@ var init_operations = __esm(() => {
40141
40191
  init_greenfield_gate();
40142
40192
  init_full_suite_gate();
40143
40193
  init_full_suite_rectify();
40194
+ init_full_suite_rectify_op();
40144
40195
  init_autofix_implementer_strategy();
40145
40196
  init_autofix_test_writer_strategy();
40146
40197
  init_apply_test_edit_declarations();
@@ -42247,6 +42298,23 @@ ${adversarialErrors}
42247
42298
  Do NOT add new features \u2014 only fix valid issues.
42248
42299
  Commit your fixes when done.${scopeConstraint}${noTestIsolationBlock(story)}${escapeHatchFor(story)}`;
42249
42300
  }
42301
+ function formatFailingTestsList(findings) {
42302
+ if (findings.length === 0) {
42303
+ return "The full test suite has failing tests. Fix the implementation to make all tests pass.";
42304
+ }
42305
+ const lines = [`Fix the following ${findings.length} failing test${findings.length === 1 ? "" : "s"}:
42306
+ `];
42307
+ for (const f of findings) {
42308
+ const location = f.file ? `${f.file}` : "(unknown file)";
42309
+ const rule = f.rule ? ` Test: ${f.rule}
42310
+ ` : "";
42311
+ lines.push(`- ${location}
42312
+ ${rule} Error: ${f.message}
42313
+ `);
42314
+ }
42315
+ return lines.join(`
42316
+ `);
42317
+ }
42250
42318
  function mechanicalRectification(checks3, story, scopeConstraint, opts) {
42251
42319
  const errors3 = formatCheckErrors(checks3, opts);
42252
42320
  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 +42999,23 @@ Before editing, break the work into one small fix per finding above (fix -> re-r
42931
42999
  `);
42932
43000
  }
42933
43001
  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(`
43002
+ const listing = formatFailingTestsList(findings);
43003
+ if (findings.length === 0)
43004
+ return listing;
43005
+ return `${listing}
43006
+ Fix the implementation (not the tests) to make all failing tests pass. Run the test suite to verify after each change.`;
43007
+ }
43008
+ static failingTestRectification(findings, story) {
43009
+ const listing = formatFailingTestsList(findings);
43010
+ const exCount = exceptionCountWord(story);
43011
+ const prohibition = `Do NOT change test files or test behavior \u2014 see the ${exCount} narrow exceptions appended below.`;
43012
+ const parts = [listing];
43013
+ parts.push(`
43014
+ 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.`);
43015
+ parts.push(`
43016
+ ${testEditHeadline(story, prohibition)}`);
43017
+ parts.push(escapeHatchFor(story));
43018
+ return parts.join(`
42950
43019
  `);
42951
43020
  }
42952
43021
  }
@@ -43218,7 +43287,13 @@ For each one:
43218
43287
  Write the corrected PRD to this file path: ${outputFilePath}
43219
43288
  Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
43220
43289
  }
43221
- build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers) {
43290
+ build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers, profiles) {
43291
+ const cards = OneShotPromptBuilder.agentCapabilityCards(profiles ?? []);
43292
+ const agentProfilesSection = cards.length > 0 ? `
43293
+
43294
+ ${cards}
43295
+
43296
+ ${OneShotPromptBuilder.agentProfileInstruction()}` : "";
43222
43297
  const isMonorepo = packages && packages.length > 0;
43223
43298
  const packageDetailsSection = packageDetails && packageDetails.length > 0 ? buildPackageDetailsSection(packageDetails) : "";
43224
43299
  const monorepoHint = isMonorepo ? `
@@ -43273,7 +43348,7 @@ ${buildSharedQualityRules(specContent, projectProfile)}
43273
43348
 
43274
43349
  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
43350
 
43276
- ${CONTEXT_VS_EXPECTED_FILES_RULE}`;
43351
+ ${CONTEXT_VS_EXPECTED_FILES_RULE}${agentProfilesSection}`;
43277
43352
  const suggestedCriteriaField = specContent.trim() ? `
43278
43353
  "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
43354
  const outputDirective = outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
@@ -43305,7 +43380,8 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
43305
43380
  "complexity": "simple | medium | complex | expert",
43306
43381
  "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
43307
43382
  "noTestJustification": "string \u2014 REQUIRED when testStrategy is no-test, explains why tests are unnecessary",
43308
- "reasoning": "string \u2014 brief classification rationale"
43383
+ "reasoning": "string \u2014 brief classification rationale"${cards.length > 0 ? `,
43384
+ "agentProfileId": "string \u2014 optional, the id of the best-matching profile from the Agent Profiles table above; omit if none fits"` : ""}
43309
43385
  },
43310
43386
  "escalations": [],
43311
43387
  "attempts": 0
@@ -43322,6 +43398,12 @@ ${outputDirective}`;
43322
43398
  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
43399
  overridable: false
43324
43400
  };
43401
+ const cards = OneShotPromptBuilder.agentCapabilityCards(input.profiles ?? []);
43402
+ const agentProfilesSection = cards.length > 0 ? `
43403
+
43404
+ ${cards}
43405
+
43406
+ ${OneShotPromptBuilder.agentProfileInstruction()}` : "";
43325
43407
  const revisionSection = input.revisionFindings && input.revisionFindings.length > 0 ? `
43326
43408
 
43327
43409
  ## Previous draft rejected for the following issues
@@ -43371,7 +43453,7 @@ ${buildSharedQualityRules(input.specContent, input.projectProfile)}
43371
43453
 
43372
43454
  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
43455
 
43374
- ${CONTEXT_VS_EXPECTED_FILES_RULE}
43456
+ ${CONTEXT_VS_EXPECTED_FILES_RULE}${agentProfilesSection}
43375
43457
 
43376
43458
  ## Output Schema
43377
43459
 
@@ -43394,7 +43476,8 @@ Produce a JSON object with this exact structure. Field names are mandatory \u201
43394
43476
  "routing": {
43395
43477
  "complexity": "simple | medium | complex | expert",
43396
43478
  "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
43397
- "reasoning": "string \u2014 brief classification rationale"
43479
+ "reasoning": "string \u2014 brief classification rationale"${cards.length > 0 ? `,
43480
+ "agentProfileId": "string \u2014 optional, the id of the best-matching profile from the Agent Profiles table above; omit if none fits"` : ""}
43398
43481
  }
43399
43482
  }
43400
43483
  ]
@@ -43434,6 +43517,7 @@ var CONTEXT_VS_EXPECTED_FILES_RULE = `**\`contextFiles\` rule \u2014 files reada
43434
43517
  **\`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
43518
  var init_plan_builder = __esm(() => {
43436
43519
  init_config();
43520
+ init_one_shot_builder();
43437
43521
  });
43438
43522
 
43439
43523
  // src/prompts/builders/grounder-builder.ts
@@ -51771,7 +51855,7 @@ var init_precheck = __esm(() => {
51771
51855
  });
51772
51856
 
51773
51857
  // src/prd/decompose-mapper.ts
51774
- function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir) {
51858
+ function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir, parentRouting) {
51775
51859
  return stories.map((story, entryIndex) => {
51776
51860
  if (!story.id) {
51777
51861
  throw new NaxError(`Entry at index ${entryIndex} is missing required field: id`, "DECOMPOSE_VALIDATION_FAILED", {
@@ -51805,10 +51889,19 @@ function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir
51805
51889
  complexity: story.complexity,
51806
51890
  testStrategy: story.testStrategy ?? "test-after",
51807
51891
  reasoning: story.reasoning,
51808
- modelTier: story.routing?.profileModelTier ?? "balanced",
51892
+ modelTier: parentRouting?.profileModelTier ?? story.routing?.profileModelTier ?? "balanced",
51809
51893
  ...story.routing?.agent !== undefined && { agent: story.routing.agent },
51810
51894
  ...story.routing?.agentProfileId !== undefined && { agentProfileId: story.routing.agentProfileId },
51811
- ...story.routing?.profileModelTier !== undefined && { profileModelTier: story.routing.profileModelTier }
51895
+ ...story.routing?.profileModelTier !== undefined && { profileModelTier: story.routing.profileModelTier },
51896
+ ...parentRouting?.agent !== undefined && { agent: parentRouting.agent },
51897
+ ...parentRouting?.agentProfileId !== undefined && { agentProfileId: parentRouting.agentProfileId },
51898
+ ...parentRouting?.profileModelTier !== undefined && { profileModelTier: parentRouting.profileModelTier },
51899
+ ...parentRouting?.agent !== undefined && {
51900
+ initialAgent: parentRouting.initialAgent ?? parentRouting.agent
51901
+ },
51902
+ ...parentRouting?.agentProfileId !== undefined && {
51903
+ initialProfileId: parentRouting.initialProfileId ?? parentRouting.agentProfileId
51904
+ }
51812
51905
  }
51813
51906
  };
51814
51907
  });
@@ -51874,8 +51967,7 @@ async function planDecomposeCommand(workdir, config2, options) {
51874
51967
  for (let attempt = 0;attempt < maxReplanAttempts; attempt++) {
51875
51968
  if (attempt === 0 && debateDecompEnabled) {
51876
51969
  const decomposeStageConfig = debateStages.decompose;
51877
- const agentRoutingForDebate = config2.routing?.agents;
51878
- const profilesForDebate = agentRoutingForDebate?.enabled === true ? agentRoutingForDebate.profiles ?? [] : [];
51970
+ const profilesForDebate = [];
51879
51971
  const prompt = await buildDecomposePromptAsync({
51880
51972
  specContent: "",
51881
51973
  codebaseContext,
@@ -51950,7 +52042,7 @@ ${repairHint}` : codebaseContext;
51950
52042
  } finally {
51951
52043
  await rt.close().catch(() => {});
51952
52044
  }
51953
- const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId, targetStory.workdir);
52045
+ const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId, targetStory.workdir, targetStory.routing);
51954
52046
  const updatedStories = prd.userStories.map((s) => s.id === options.storyId ? { ...s, status: "decomposed" } : s);
51955
52047
  const originalIndex = updatedStories.findIndex((s) => s.id === options.storyId);
51956
52048
  const finalStories = [
@@ -52912,15 +53004,28 @@ async function processPackageGroup(ctx, packageDir, groupStories, language, resu
52912
53004
  featureName: ctx.prd.feature,
52913
53005
  agentName: ctx.agentManager.getDefault()
52914
53006
  };
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
- });
53007
+ let refined;
53008
+ try {
53009
+ refined = await _hardeningDeps.callOp(callCtx, acceptanceRefineOp, {
53010
+ criteria: story.suggestedCriteria ?? [],
53011
+ codebaseContext: "",
53012
+ storyId: story.id,
53013
+ testStrategy: ctx.config.acceptance?.testStrategy,
53014
+ testFramework: ctx.config.acceptance?.testFramework,
53015
+ storyTitle: story.title,
53016
+ storyDescription: story.description
53017
+ });
53018
+ } catch {
53019
+ logger?.warn("acceptance", "AC refinement failed after retries \u2014 using unrefined criteria", {
53020
+ storyId: story.id
53021
+ });
53022
+ refined = (story.suggestedCriteria ?? []).map((c) => ({
53023
+ original: c,
53024
+ refined: c,
53025
+ testable: true,
53026
+ storyId: story.id
53027
+ }));
53028
+ }
52924
53029
  groupRefined.push(...refined);
52925
53030
  }
52926
53031
  const suggestedTestPath = resolveSuggestedPackageFeatureTestPath(packageDir, ctx.prd.feature, ctx.config.acceptance?.suggestedTestPath, language);
@@ -53066,6 +53171,7 @@ __export(exports_acceptance, {
53066
53171
  resolveSuggestedTestFile: () => resolveSuggestedTestFile,
53067
53172
  resolveSuggestedPackageFeatureTestPath: () => resolveSuggestedPackageFeatureTestPath,
53068
53173
  resolveAcceptanceFeatureTestPath: () => resolveAcceptanceFeatureTestPath,
53174
+ refinementWouldFallback: () => refinementWouldFallback,
53069
53175
  parseRefinementResponse: () => parseRefinementResponse,
53070
53176
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
53071
53177
  parseACTextFromSpec: () => parseACTextFromSpec,
@@ -53480,6 +53586,16 @@ ${stderr}` };
53480
53586
  storyDescription: story.description
53481
53587
  }, story.id).then((refined) => {
53482
53588
  results[i] = refined;
53589
+ }).catch(() => {
53590
+ getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
53591
+ storyId: story.id
53592
+ });
53593
+ results[i] = story.acceptanceCriteria.map((c) => ({
53594
+ original: c,
53595
+ refined: c,
53596
+ testable: true,
53597
+ storyId: story.id
53598
+ }));
53483
53599
  }).finally(() => {
53484
53600
  executing.delete(task);
53485
53601
  });
@@ -55450,7 +55566,7 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55450
55566
  strategies.push(makeMechanicalFormatFixStrategy());
55451
55567
  }
55452
55568
  if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
55453
- strategies.push(makeFullSuiteRectifyStrategy(story, config2));
55569
+ strategies.push(makeFullSuiteRectifyStrategy(story, config2, sink));
55454
55570
  }
55455
55571
  if (config2.quality.autofix?.enabled !== false) {
55456
55572
  strategies.push(makeAutofixImplementerStrategy(story, config2, sink, {
@@ -55495,7 +55611,7 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55495
55611
  includeAdversarialReview: false
55496
55612
  }), makeAutofixTestWriterStrategy(story, config2, nbSink));
55497
55613
  }
55498
- nbStrategies.push(makeFullSuiteRectifyStrategy(story, config2));
55614
+ nbStrategies.push(makeFullSuiteRectifyStrategy(story, config2, nbSink));
55499
55615
  builder.addNonBlockingFix(nbf, nbStrategies);
55500
55616
  }
55501
55617
  return builder.build(ctx, { isThreeSession });
@@ -60095,7 +60211,7 @@ var package_default;
60095
60211
  var init_package = __esm(() => {
60096
60212
  package_default = {
60097
60213
  name: "@nathapp/nax",
60098
- version: "0.70.0-canary.1",
60214
+ version: "0.70.0-canary.2",
60099
60215
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60100
60216
  type: "module",
60101
60217
  bin: {
@@ -60190,8 +60306,8 @@ var init_version = __esm(() => {
60190
60306
  NAX_VERSION = package_default.version;
60191
60307
  NAX_COMMIT = (() => {
60192
60308
  try {
60193
- if (/^[0-9a-f]{6,10}$/.test("f895b7d3"))
60194
- return "f895b7d3";
60309
+ if (/^[0-9a-f]{6,10}$/.test("b070f4c1"))
60310
+ return "b070f4c1";
60195
60311
  } catch {}
60196
60312
  try {
60197
60313
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -95450,6 +95566,7 @@ async function buildPlanModeContext(workdir, fullConfig, options, deps) {
95450
95566
  const branchName = options.branch ?? `feat/${options.feature}`;
95451
95567
  const timeoutSeconds = fullConfig?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
95452
95568
  const config2 = planConfigSelector.select(fullConfig);
95569
+ const profileName = typeof fullConfig?.profile === "string" && fullConfig.profile ? fullConfig.profile : undefined;
95453
95570
  const runtime = createPlanRuntime(fullConfig, workdir, options.feature);
95454
95571
  const interactionChain = fullConfig ? await deps.initInteractionChain(fullConfig, !process.stdin.isTTY) : null;
95455
95572
  let configuredBridge;
@@ -95477,6 +95594,7 @@ async function buildPlanModeContext(workdir, fullConfig, options, deps) {
95477
95594
  branchName,
95478
95595
  timeoutSeconds,
95479
95596
  config: config2,
95597
+ profileName,
95480
95598
  options,
95481
95599
  runtime,
95482
95600
  interactionChain,
@@ -95525,6 +95643,28 @@ function buildPlanComposition(userStageConfig) {
95525
95643
  // src/plan/strategies/write-prd.ts
95526
95644
  init_errors();
95527
95645
  init_prd();
95646
+
95647
+ // src/plan/strategies/finalize-routing.ts
95648
+ init_agents();
95649
+ function finalizePrdRouting(prd, agentRouting, profileName) {
95650
+ const userStories = prd.userStories.map((story) => {
95651
+ const assignment = resolveAgentAssignment(story.routing?.agentProfileId, agentRouting, story.id);
95652
+ if (!assignment)
95653
+ return story;
95654
+ const routing = {
95655
+ ...story.routing,
95656
+ agent: assignment.agent,
95657
+ agentProfileId: assignment.agentProfileId,
95658
+ profileModelTier: assignment.profileModelTier,
95659
+ initialAgent: story.routing?.initialAgent ?? assignment.agent,
95660
+ initialProfileId: story.routing?.initialProfileId ?? assignment.agentProfileId
95661
+ };
95662
+ return { ...story, routing };
95663
+ });
95664
+ return { ...prd, userStories, routingProfile: profileName ?? "default" };
95665
+ }
95666
+
95667
+ // src/plan/strategies/write-prd.ts
95528
95668
  async function writeOrRecoverPrd(ctx, prd, err) {
95529
95669
  const tryExtractPrd = (value) => {
95530
95670
  if (value === null || typeof value !== "object")
@@ -95543,12 +95683,14 @@ async function writeOrRecoverPrd(ctx, prd, err) {
95543
95683
  };
95544
95684
  if (prd !== null) {
95545
95685
  if (Array.isArray(prd.userStories)) {
95546
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...prd, project: ctx.projectName }, null, 2));
95686
+ const finalized = finalizePrdRouting({ ...prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95687
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95547
95688
  return ctx.outputPath;
95548
95689
  }
95549
95690
  const normalizedPrd = tryExtractPrd(prd);
95550
95691
  if (normalizedPrd !== null) {
95551
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...normalizedPrd, project: ctx.projectName }, null, 2));
95692
+ const finalized = finalizePrdRouting({ ...normalizedPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95693
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95552
95694
  return ctx.outputPath;
95553
95695
  }
95554
95696
  }
@@ -95568,7 +95710,8 @@ async function writeOrRecoverPrd(ctx, prd, err) {
95568
95710
  }
95569
95711
  }
95570
95712
  recoveredPrd = recoveredPrd ?? validatePlanOutput(rawContent, ctx.options.feature, ctx.branchName);
95571
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...recoveredPrd, project: ctx.projectName }, null, 2));
95713
+ const finalized = finalizePrdRouting({ ...recoveredPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95714
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95572
95715
  return ctx.outputPath;
95573
95716
  } catch {
95574
95717
  throw err;
@@ -95587,7 +95730,9 @@ var _debatePlanDeps = {
95587
95730
  class DebatePlanStrategy {
95588
95731
  mode = "debate";
95589
95732
  async execute(ctx) {
95590
- const { taskContext, outputFormat } = new PlanPromptBuilder().build(ctx.specContent, ctx.codebaseContext, undefined, ctx.relativePackages, ctx.packageDetails, ctx.config.project);
95733
+ const agentRouting = ctx.config.routing?.agents;
95734
+ const profiles = agentRouting?.enabled === true ? agentRouting.profiles ?? [] : [];
95735
+ const { taskContext, outputFormat } = new PlanPromptBuilder().build(ctx.specContent, ctx.codebaseContext, undefined, ctx.relativePackages, ctx.packageDetails, ctx.config.project, undefined, profiles);
95591
95736
  const planStage = ctx.config.debate?.stages?.plan;
95592
95737
  if (!planStage) {
95593
95738
  throw new NaxError("[plan] debate strategy requires config.debate.stages.plan", "PLAN_DEBATE_STAGE_CONFIG_MISSING", {
@@ -95723,7 +95868,8 @@ class PipelinePlanStrategy {
95723
95868
  if (verdict.outcome !== "passed") {
95724
95869
  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
95870
  }
95726
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...verdict.prd, project: ctx.projectName }, null, 2));
95871
+ const prdToWrite = finalizePrdRouting({ ...verdict.prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95872
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(prdToWrite, null, 2));
95727
95873
  return ctx.outputPath;
95728
95874
  } finally {
95729
95875
  await ctx.runtime.close().catch(() => {});
@@ -95804,13 +95950,15 @@ class SinglePlanStrategy {
95804
95950
  projectProfile: ctx.config.project
95805
95951
  });
95806
95952
  assertIsValidPrd(prd);
95807
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...prd, project: ctx.projectName }, null, 2));
95953
+ const finalized = finalizePrdRouting({ ...prd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95954
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalized, null, 2));
95808
95955
  return ctx.outputPath;
95809
95956
  } catch (err) {
95810
95957
  if (ctx.deps.existsSync(ctx.outputPath)) {
95811
95958
  const rawContent = await ctx.deps.readFile(ctx.outputPath);
95812
95959
  const recoveredPrd = validatePlanOutput(rawContent, ctx.options.feature, ctx.branchName);
95813
- await ctx.deps.writeFile(ctx.outputPath, JSON.stringify({ ...recoveredPrd, project: ctx.projectName }, null, 2));
95960
+ const finalizedRecovered = finalizePrdRouting({ ...recoveredPrd, project: ctx.projectName }, ctx.config.routing?.agents, ctx.profileName);
95961
+ await ctx.deps.writeFile(ctx.outputPath, JSON.stringify(finalizedRecovered, null, 2));
95814
95962
  return ctx.outputPath;
95815
95963
  }
95816
95964
  throw err;
@@ -96856,6 +97004,7 @@ var FIELD_DESCRIPTIONS = {
96856
97004
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
96857
97005
  "acceptance.command": "Override command to run acceptance tests. Use {{FILE}} as placeholder for the test file path (default: 'bun test {{FILE}} --timeout=60000')",
96858
97006
  "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".',
97007
+ "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
97008
  "acceptance.refinement": "Enable acceptance criteria refinement step before execution (default: true). Disable to skip refinement and use generated criteria as-is.",
96860
97009
  "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
96861
97010
  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.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {