@nathapp/nax 0.59.2 → 0.59.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 +526 -296
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -18060,7 +18060,7 @@ function isLegacyFlatModels(val) {
18060
18060
  }
18061
18061
  return false;
18062
18062
  }
18063
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewDialogueConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
18063
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewDialogueConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterPersonaEnum, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
18064
18064
  type: exports_external.enum(RESOLVER_TYPES).default(defaultType),
18065
18065
  agent: exports_external.string().min(1).optional(),
18066
18066
  tieBreaker: exports_external.string().min(1).optional(),
@@ -18072,7 +18072,8 @@ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema
18072
18072
  rounds: exports_external.number().int().min(1).default(defaults.rounds),
18073
18073
  mode: exports_external.enum(["panel", "hybrid"]).default("panel"),
18074
18074
  debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional(),
18075
- timeoutSeconds: exports_external.number().int().positive().default(600)
18075
+ timeoutSeconds: exports_external.number().int().positive().default(600),
18076
+ autoPersona: exports_external.boolean().default(false)
18076
18077
  })), DebateConfigSchema, NaxConfigSchema;
18077
18078
  var init_schemas3 = __esm(() => {
18078
18079
  init_zod();
@@ -18431,9 +18432,11 @@ var init_schemas3 = __esm(() => {
18431
18432
  GenerateConfigSchema = exports_external.object({
18432
18433
  agents: exports_external.array(exports_external.enum(VALID_AGENT_TYPES)).optional()
18433
18434
  });
18435
+ DebaterPersonaEnum = exports_external.enum(["challenger", "pragmatist", "completionist", "security", "testability"]);
18434
18436
  DebaterSchema = exports_external.object({
18435
18437
  agent: exports_external.string().min(1, "debater.agent must be non-empty"),
18436
- model: exports_external.string().min(1, "debater.model must be non-empty").optional()
18438
+ model: exports_external.string().min(1, "debater.model must be non-empty").optional(),
18439
+ persona: DebaterPersonaEnum.optional()
18437
18440
  });
18438
18441
  RESOLVER_TYPES = ["synthesis", "majority-fail-closed", "majority-fail-open", "custom"];
18439
18442
  DebateConfigSchema = exports_external.preprocess(toObject, exports_external.object({
@@ -18722,7 +18725,8 @@ var init_schemas3 = __esm(() => {
18722
18725
  sessionMode: "stateful",
18723
18726
  rounds: 3,
18724
18727
  mode: "panel",
18725
- timeoutSeconds: 600
18728
+ timeoutSeconds: 600,
18729
+ autoPersona: false
18726
18730
  },
18727
18731
  review: {
18728
18732
  enabled: true,
@@ -18730,7 +18734,8 @@ var init_schemas3 = __esm(() => {
18730
18734
  sessionMode: "one-shot",
18731
18735
  rounds: 2,
18732
18736
  mode: "panel",
18733
- timeoutSeconds: 600
18737
+ timeoutSeconds: 600,
18738
+ autoPersona: false
18734
18739
  },
18735
18740
  acceptance: {
18736
18741
  enabled: false,
@@ -18738,7 +18743,8 @@ var init_schemas3 = __esm(() => {
18738
18743
  sessionMode: "one-shot",
18739
18744
  rounds: 1,
18740
18745
  mode: "panel",
18741
- timeoutSeconds: 600
18746
+ timeoutSeconds: 600,
18747
+ autoPersona: false
18742
18748
  },
18743
18749
  rectification: {
18744
18750
  enabled: false,
@@ -18746,7 +18752,8 @@ var init_schemas3 = __esm(() => {
18746
18752
  sessionMode: "one-shot",
18747
18753
  rounds: 1,
18748
18754
  mode: "panel",
18749
- timeoutSeconds: 600
18755
+ timeoutSeconds: 600,
18756
+ autoPersona: false
18750
18757
  },
18751
18758
  escalation: {
18752
18759
  enabled: false,
@@ -18754,7 +18761,8 @@ var init_schemas3 = __esm(() => {
18754
18761
  sessionMode: "one-shot",
18755
18762
  rounds: 1,
18756
18763
  mode: "panel",
18757
- timeoutSeconds: 600
18764
+ timeoutSeconds: 600,
18765
+ autoPersona: false
18758
18766
  }
18759
18767
  }
18760
18768
  })),
@@ -21365,28 +21373,85 @@ var init_config = __esm(() => {
21365
21373
  init_profile();
21366
21374
  });
21367
21375
 
21368
- // src/debate/prompts.ts
21369
- function buildCritiquePrompt(taskPrompt, allProposals, debaterIndex) {
21370
- const othersProposals = allProposals.filter((_, i) => i !== debaterIndex);
21371
- const proposalsSection = othersProposals.map((p, i) => `### Proposal ${i + 1}
21372
- ${p}`).join(`
21373
-
21374
- `);
21375
- return `You are reviewing proposals from other agents for the following task.
21376
-
21377
- ## Task
21378
- ${taskPrompt}
21376
+ // src/utils/llm-json.ts
21377
+ function extractJsonFromMarkdown(text) {
21378
+ const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
21379
+ if (match) {
21380
+ return match[1] ?? text;
21381
+ }
21382
+ return text;
21383
+ }
21384
+ function stripTrailingCommas(text) {
21385
+ return text.replace(/,\s*([}\]])/g, "$1");
21386
+ }
21387
+ function extractJsonObject(text) {
21388
+ const objStart = text.indexOf("{");
21389
+ const arrStart = text.indexOf("[");
21390
+ let start;
21391
+ let closeChar;
21392
+ if (objStart === -1 && arrStart === -1)
21393
+ return null;
21394
+ if (objStart === -1) {
21395
+ start = arrStart;
21396
+ closeChar = "]";
21397
+ } else if (arrStart === -1) {
21398
+ start = objStart;
21399
+ closeChar = "}";
21400
+ } else if (objStart < arrStart) {
21401
+ start = objStart;
21402
+ closeChar = "}";
21403
+ } else {
21404
+ start = arrStart;
21405
+ closeChar = "]";
21406
+ }
21407
+ const end = text.lastIndexOf(closeChar);
21408
+ if (end <= start)
21409
+ return null;
21410
+ return text.slice(start, end + 1);
21411
+ }
21412
+ function wrapJsonPrompt(prompt) {
21413
+ return `IMPORTANT: Your entire response must be a single JSON object or array. Do not explain your reasoning. Do not use markdown formatting. Output ONLY the JSON.
21379
21414
 
21380
- ## Other Agents' Proposals
21381
- ${proposalsSection}
21415
+ ${prompt.trim()}
21382
21416
 
21383
- Please critique these proposals and provide your refined analysis, identifying strengths, weaknesses, and your own updated position.`;
21417
+ YOUR RESPONSE MUST START WITH { OR [ AND END WITH } OR ]. No other text.`;
21384
21418
  }
21385
- function buildSynthesisPrompt(proposals, critiques) {
21386
- const proposalsSection = proposals.map((p, i) => `### Proposal ${i + 1}
21419
+ function parseLLMJson(text) {
21420
+ const trimmed = text.trim();
21421
+ try {
21422
+ return JSON.parse(trimmed);
21423
+ } catch {}
21424
+ const fromFence = extractJsonFromMarkdown(trimmed);
21425
+ if (fromFence !== trimmed) {
21426
+ try {
21427
+ return JSON.parse(stripTrailingCommas(fromFence));
21428
+ } catch {}
21429
+ }
21430
+ const bareJson = extractJsonObject(trimmed);
21431
+ if (bareJson) {
21432
+ try {
21433
+ return JSON.parse(stripTrailingCommas(bareJson));
21434
+ } catch {}
21435
+ }
21436
+ throw new SyntaxError("[llm-json] Failed to parse LLM response as JSON");
21437
+ }
21438
+ function tryParseLLMJson(text) {
21439
+ try {
21440
+ return parseLLMJson(text);
21441
+ } catch {
21442
+ return null;
21443
+ }
21444
+ }
21445
+
21446
+ // src/debate/resolvers.ts
21447
+ function buildProposalsSection(proposals) {
21448
+ return proposals.map((p, i) => `### Proposal ${i + 1}
21387
21449
  ${p}`).join(`
21388
21450
 
21389
21451
  `);
21452
+ }
21453
+ function buildSynthesisPrompt(proposals, critiques) {
21454
+ const proposalsSection = buildProposalsSection(proposals);
21390
21455
  const critiquesSection = critiques.length > 0 ? `
21391
21456
 
21392
21457
  ## Critiques
@@ -21402,10 +21467,7 @@ ${proposalsSection}${critiquesSection}
21402
21467
  Please synthesize these into the best possible unified response, incorporating the strongest elements from each proposal.`;
21403
21468
  }
21404
21469
  function buildJudgePrompt(proposals, critiques) {
21405
- const proposalsSection = proposals.map((p, i) => `### Proposal ${i + 1}
21406
- ${p}`).join(`
21407
-
21408
- `);
21470
+ const proposalsSection = buildProposalsSection(proposals);
21409
21471
  const critiquesSection = critiques.length > 0 ? `
21410
21472
 
21411
21473
  ## Critiques
@@ -21420,28 +21482,6 @@ ${proposalsSection}${critiquesSection}
21420
21482
 
21421
21483
  As the judge, provide your final verdict with clear reasoning, selecting or synthesizing the best approach.`;
21422
21484
  }
21423
- function buildRebuttalContext(prompt, proposals, rebuttalOutputs, currentDebaterIndex) {
21424
- const proposalsSection = proposals.map((p, i) => `### Proposal ${i + 1} (${p.debater.agent})
21425
- ${p.output}`).join(`
21426
-
21427
- `);
21428
- const rebuttalsSection = rebuttalOutputs.length > 0 ? `
21429
-
21430
- ## Previous Rebuttals
21431
- ${rebuttalOutputs.map((r, i) => `${i + 1}. ${r}`).join(`
21432
-
21433
- `)}` : "";
21434
- const debaterNumber = currentDebaterIndex + 1;
21435
- return `${prompt}
21436
-
21437
- ## Proposals
21438
- ${proposalsSection}${rebuttalsSection}
21439
-
21440
- ## Your Task
21441
- You are debater ${debaterNumber}. Provide your rebuttal to the proposals and previous rebuttals above.`;
21442
- }
21443
-
21444
- // src/debate/resolvers.ts
21445
21485
  function stripMarkdownFence(text) {
21446
21486
  const match = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/);
21447
21487
  return match ? match[1] ?? text : text;
@@ -21491,7 +21531,6 @@ async function judgeResolver(proposals, critiques, resolverConfig, opts) {
21491
21531
  return adapter.complete(prompt, opts.completeOptions);
21492
21532
  }
21493
21533
  var DEFAULT_FALLBACK_AGENT = "claude";
21494
- var init_resolvers = () => {};
21495
21534
 
21496
21535
  // src/debate/session-helpers.ts
21497
21536
  function resolveDebaterModel(debater, config2) {
@@ -21581,21 +21620,13 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21581
21620
  let passCount = 0;
21582
21621
  let failCount = 0;
21583
21622
  for (const proposal of proposalOutputs) {
21584
- try {
21585
- const stripped = proposal.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
21586
- const parsed = JSON.parse(stripped);
21587
- if (typeof parsed.passed === "boolean" && parsed.passed)
21588
- passCount++;
21589
- else if (failOpen)
21590
- passCount++;
21591
- else
21592
- failCount++;
21593
- } catch {
21594
- if (failOpen)
21595
- passCount++;
21596
- else
21597
- failCount++;
21598
- }
21623
+ const parsed = tryParseLLMJson(proposal);
21624
+ if (parsed !== null && typeof parsed.passed === "boolean" && parsed.passed)
21625
+ passCount++;
21626
+ else if (failOpen)
21627
+ passCount++;
21628
+ else
21629
+ failCount++;
21599
21630
  }
21600
21631
  debateCtx.majorityVote = { passed: rawOutcome === "passed", passCount, failCount };
21601
21632
  }
@@ -21694,7 +21725,6 @@ var init_session_helpers = __esm(() => {
21694
21725
  init_registry();
21695
21726
  init_config();
21696
21727
  init_logger2();
21697
- init_resolvers();
21698
21728
  _debateSessionDeps = {
21699
21729
  getAgent: (name, config2) => config2 ? createAgentRegistry(config2).getAgent(name) : getAgent(name),
21700
21730
  getSafeLogger,
@@ -21728,6 +21758,171 @@ async function allSettledBounded(tasks, limit) {
21728
21758
  return results;
21729
21759
  }
21730
21760
 
21761
+ // src/debate/personas.ts
21762
+ function resolvePersonas(debaters, stage, autoPersona) {
21763
+ if (!autoPersona)
21764
+ return debaters;
21765
+ const rotation = stage === "plan" ? PLAN_ROTATION : REVIEW_ROTATION;
21766
+ let rotationIndex = 0;
21767
+ return debaters.map((d) => {
21768
+ if (d.persona)
21769
+ return d;
21770
+ const assigned = rotation[rotationIndex % rotation.length];
21771
+ rotationIndex++;
21772
+ return { ...d, persona: assigned };
21773
+ });
21774
+ }
21775
+ var PERSONA_FRAGMENTS, PLAN_ROTATION, REVIEW_ROTATION;
21776
+ var init_personas = __esm(() => {
21777
+ PERSONA_FRAGMENTS = {
21778
+ challenger: {
21779
+ identity: "You are the challenger \u2014 your job is to stress-test proposals and find weaknesses.",
21780
+ lens: "Question every assumption. Look for missing edge cases, unhandled error states, " + "and scenarios where the proposed approach could break under real-world conditions. " + "If a proposal lacks justification for a design choice, call it out."
21781
+ },
21782
+ pragmatist: {
21783
+ identity: "You are the pragmatist \u2014 your job is to find the simplest path that satisfies the spec.",
21784
+ lens: "Favour minimal scope, fewest files changed, and lowest complexity. " + "Challenge any proposal that adds abstraction, configuration, or code beyond what the spec requires. " + "If something can be done in 5 lines instead of 50, advocate for the 5-line version."
21785
+ },
21786
+ completionist: {
21787
+ identity: "You are the completionist \u2014 your job is to ensure nothing is missed.",
21788
+ lens: "Verify every acceptance criterion is addressed. Check that edge cases have tests, " + "that error messages are user-friendly, and that the implementation handles all status/state variants. " + "If the spec is ambiguous, flag it and propose the safer interpretation."
21789
+ },
21790
+ security: {
21791
+ identity: "You are the security reviewer \u2014 your job is to surface risks before they ship.",
21792
+ lens: "Evaluate input validation, secret handling, injection vectors, and trust boundaries. " + "Check that user-supplied data is never used unsanitised in commands, queries, or file paths. " + "If the proposal touches auth, permissions, or external APIs, apply extra scrutiny."
21793
+ },
21794
+ testability: {
21795
+ identity: "You are the testability advocate \u2014 your job is to ensure the design is verifiable.",
21796
+ lens: "Assess whether the proposed implementation can be tested without mocks, " + "whether test boundaries are clean, and whether the acceptance criteria are machine-verifiable. " + "Challenge any design that makes testing harder (global state, tight coupling, hidden side effects)."
21797
+ }
21798
+ };
21799
+ PLAN_ROTATION = ["challenger", "pragmatist", "completionist", "security", "testability"];
21800
+ REVIEW_ROTATION = ["security", "completionist", "testability", "challenger", "pragmatist"];
21801
+ });
21802
+
21803
+ // src/debate/prompt-builder.ts
21804
+ class DebatePromptBuilder {
21805
+ stageContext;
21806
+ options;
21807
+ constructor(stageContext, options) {
21808
+ this.stageContext = stageContext;
21809
+ this.options = options;
21810
+ }
21811
+ buildProposalPrompt(debaterIndex) {
21812
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21813
+ return `${this.stageContext.taskContext}${personaBlock}
21814
+
21815
+ ${this.stageContext.outputFormat}`;
21816
+ }
21817
+ buildCritiquePrompt(debaterIndex, proposals) {
21818
+ const otherProposals = proposals.filter((_, i) => i !== debaterIndex);
21819
+ const proposalsSection = this.buildProposalsSection(otherProposals);
21820
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21821
+ return `You are reviewing proposals for a ${this.stageContext.stage} task.
21822
+
21823
+ ## Task
21824
+ ${this.stageContext.taskContext}${personaBlock}
21825
+
21826
+ ## Other Agents' Proposals
21827
+ ${proposalsSection}
21828
+
21829
+ Please critique these proposals and provide your refined analysis, identifying strengths, weaknesses, and your own updated position.`;
21830
+ }
21831
+ buildRebuttalPrompt(debaterIndex, proposals, priorRebuttals) {
21832
+ const contextBlock = this.options.sessionMode === "one-shot" ? `${this.stageContext.taskContext}
21833
+
21834
+ ` : "";
21835
+ const proposalsSection = this.buildProposalsSection(proposals);
21836
+ const rebuttalsSection = this.buildRebuttalsSection(priorRebuttals);
21837
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21838
+ const debaterNumber = debaterIndex + 1;
21839
+ return `${contextBlock}## Proposals
21840
+ ${proposalsSection}${rebuttalsSection}${personaBlock}
21841
+
21842
+ ## Your Task
21843
+ You are debater ${debaterNumber}. Provide your critique in prose.
21844
+ Identify strengths, weaknesses, and specific improvements for each proposal.
21845
+ Do NOT output JSON \u2014 focus on analysis only.`;
21846
+ }
21847
+ buildSynthesisPrompt(proposals, critiques, promptSuffix) {
21848
+ const proposalsSection = this.buildProposalsSection(proposals);
21849
+ const critiquesSection = this.buildCritiquesSection(critiques);
21850
+ return `You are a synthesis agent. Your task is to combine the strongest elements from multiple proposals into a single, optimal response.
21851
+
21852
+ ${this.stageContext.taskContext}
21853
+
21854
+ ## Proposals
21855
+ ${proposalsSection}
21856
+
21857
+ ## Critiques
21858
+ ${critiquesSection}
21859
+
21860
+ Please synthesize these into the best possible unified response, incorporating the strongest elements from each proposal.
21861
+ ${this.stageContext.outputFormat}${promptSuffix ? `
21862
+ ${promptSuffix}` : ""}`;
21863
+ }
21864
+ buildJudgePrompt(proposals, critiques) {
21865
+ const proposalsSection = this.buildProposalsSection(proposals);
21866
+ const critiquesSection = this.buildCritiquesSection(critiques);
21867
+ return `You are a judge evaluating multiple proposals. Select the best proposal or synthesize the optimal response.
21868
+
21869
+ ${this.stageContext.taskContext}
21870
+
21871
+ ## Proposals
21872
+ ${proposalsSection}
21873
+
21874
+ ## Critiques
21875
+ ${critiquesSection}
21876
+
21877
+ Evaluate each proposal against the critiques and provide the best possible response.
21878
+ ${this.stageContext.outputFormat}`;
21879
+ }
21880
+ buildClosePrompt() {
21881
+ return "Close this debate session.";
21882
+ }
21883
+ buildPersonaBlock(debaterIndex) {
21884
+ const debater = this.options.debaters[debaterIndex];
21885
+ if (!debater?.persona)
21886
+ return "";
21887
+ const { identity, lens } = PERSONA_FRAGMENTS[debater.persona];
21888
+ return `
21889
+
21890
+ ## Your Role
21891
+ ${identity}
21892
+ ${lens}`;
21893
+ }
21894
+ buildProposalsSection(proposals) {
21895
+ return proposals.map((p, i) => `### Proposal ${i + 1} (${this.buildDebaterLabel(p.debater)})
21896
+ ${p.output}`).join(`
21897
+
21898
+ `);
21899
+ }
21900
+ buildRebuttalsSection(rebuttals) {
21901
+ if (rebuttals.length === 0)
21902
+ return "";
21903
+ return `
21904
+
21905
+ ## Previous Rebuttals
21906
+ ${rebuttals.map((r, i) => `${i + 1}. ${r.output}`).join(`
21907
+
21908
+ `)}`;
21909
+ }
21910
+ buildCritiquesSection(critiques) {
21911
+ if (critiques.length === 0)
21912
+ return "";
21913
+ return critiques.map((c, i) => `### Critique ${i + 1} (${this.buildDebaterLabel(c.debater)})
21914
+ ${c.output}`).join(`
21915
+
21916
+ `);
21917
+ }
21918
+ buildDebaterLabel(debater) {
21919
+ return debater.persona ? `${debater.agent} (${debater.persona})` : debater.agent;
21920
+ }
21921
+ }
21922
+ var init_prompt_builder = __esm(() => {
21923
+ init_personas();
21924
+ });
21925
+
21731
21926
  // src/debate/session-stateful.ts
21732
21927
  async function runStatefulTurn(ctx, adapter, debater, prompt, roleKey, keepSessionOpen) {
21733
21928
  const modelTier = modelTierFromDebater(debater);
@@ -21783,7 +21978,9 @@ async function closeStatefulSession(ctx, adapter, debater, roleKey) {
21783
21978
  async function runStateful(ctx, prompt) {
21784
21979
  const logger = _debateSessionDeps.getSafeLogger();
21785
21980
  const config2 = ctx.stageConfig;
21786
- const debaters = config2.debaters ?? [];
21981
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
21982
+ const rawDebaters = config2.debaters ?? [];
21983
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
21787
21984
  let totalCostUsd = 0;
21788
21985
  const resolved = [];
21789
21986
  for (const debater of debaters) {
@@ -21880,8 +22077,9 @@ async function runStateful(ctx, prompt) {
21880
22077
  }
21881
22078
  let critiqueOutputs = [];
21882
22079
  if (config2.rounds > 1) {
21883
- const proposalOutputs2 = successfulProposals.map((s) => s.output);
21884
- const critiqueSettled = await allSettledBounded(successfulProposals.map((proposal, successfulIdx) => () => runStatefulTurn(ctx, proposal.adapter, proposal.debater, buildCritiquePrompt(prompt, proposalOutputs2, successfulIdx), proposal.roleKey ?? `debate-${ctx.stage}-${successfulIdx}`, false)), concurrencyLimit);
22080
+ const proposals2 = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
22081
+ const critiqueBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: proposals2.map((p) => p.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22082
+ const critiqueSettled = await allSettledBounded(successfulProposals.map((proposal, successfulIdx) => () => runStatefulTurn(ctx, proposal.adapter, proposal.debater, critiqueBuilder.buildCritiquePrompt(successfulIdx, proposals2), proposal.roleKey ?? `debate-${ctx.stage}-${successfulIdx}`, false)), concurrencyLimit);
21885
22083
  for (const r of critiqueSettled) {
21886
22084
  if (r.status === "fulfilled") {
21887
22085
  totalCostUsd += r.value.cost;
@@ -21917,11 +22115,13 @@ async function runStateful(ctx, prompt) {
21917
22115
  };
21918
22116
  }
21919
22117
  var init_session_stateful = __esm(() => {
22118
+ init_personas();
22119
+ init_prompt_builder();
21920
22120
  init_session_helpers();
21921
22121
  });
21922
22122
 
21923
22123
  // src/debate/session-hybrid.ts
21924
- async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix) {
22124
+ async function runRebuttalLoop(ctx, proposals, builder, sessionRolePrefix) {
21925
22125
  const logger = _debateSessionDeps.getSafeLogger();
21926
22126
  const config2 = ctx.stageConfig;
21927
22127
  const rebuttals = [];
@@ -21929,7 +22129,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21929
22129
  const proposalList = proposals.map((s) => ({ debater: s.debater, output: s.output }));
21930
22130
  try {
21931
22131
  for (let round = 1;round <= config2.rounds; round++) {
21932
- const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
22132
+ const priorRebuttals = rebuttals.filter((r) => r.round < round);
21933
22133
  for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21934
22134
  const proposal = proposals[debaterIdx];
21935
22135
  const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
@@ -21938,7 +22138,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21938
22138
  round,
21939
22139
  debaterIndex: debaterIdx
21940
22140
  });
21941
- const rebuttalPrompt = buildRebuttalContext(originalPrompt, proposalList, priorRebuttals, debaterIdx);
22141
+ const rebuttalPrompt = builder.buildRebuttalPrompt(debaterIdx, proposalList, priorRebuttals);
21942
22142
  try {
21943
22143
  const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21944
22144
  costUsd += turnResult.cost;
@@ -21968,7 +22168,9 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21968
22168
  async function runHybrid(ctx, prompt) {
21969
22169
  const logger = _debateSessionDeps.getSafeLogger();
21970
22170
  const config2 = ctx.stageConfig;
21971
- const debaters = config2.debaters ?? [];
22171
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
22172
+ const rawDebaters = config2.debaters ?? [];
22173
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
21972
22174
  let totalCostUsd = 0;
21973
22175
  const resolved = [];
21974
22176
  for (const debater of debaters) {
@@ -22033,7 +22235,8 @@ async function runHybrid(ctx, prompt) {
22033
22235
  }
22034
22236
  const proposalOutputs = successfulProposals.map((s) => s.output);
22035
22237
  const proposalList = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
22036
- const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, prompt, "debate-hybrid");
22238
+ const rebuttalBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: successfulProposals.map((s) => s.debater), sessionMode: "stateful" });
22239
+ const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, rebuttalBuilder, "debate-hybrid");
22037
22240
  totalCostUsd += rebuttalCost;
22038
22241
  const critiqueOutputs = rebuttals.map((r) => r.output);
22039
22242
  const fullResolverContext = ctx.resolverContextInput ? {
@@ -22055,6 +22258,8 @@ async function runHybrid(ctx, prompt) {
22055
22258
  };
22056
22259
  }
22057
22260
  var init_session_hybrid = __esm(() => {
22261
+ init_personas();
22262
+ init_prompt_builder();
22058
22263
  init_session_helpers();
22059
22264
  init_session_stateful();
22060
22265
  });
@@ -22063,7 +22268,9 @@ var init_session_hybrid = __esm(() => {
22063
22268
  async function runOneShot(ctx, prompt) {
22064
22269
  const logger = _debateSessionDeps.getSafeLogger();
22065
22270
  const config2 = ctx.stageConfig;
22066
- const debaters = config2.debaters ?? [];
22271
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
22272
+ const rawDebaters = config2.debaters ?? [];
22273
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
22067
22274
  let totalCostUsd = 0;
22068
22275
  const resolved = [];
22069
22276
  for (const debater of debaters) {
@@ -22166,8 +22373,9 @@ async function runOneShot(ctx, prompt) {
22166
22373
  }
22167
22374
  let critiqueOutputs = [];
22168
22375
  if (config2.rounds > 1) {
22169
- const proposalOutputs2 = successful.map((p) => p.output);
22170
- const critiqueSettled = await allSettledBounded(successful.map(({ debater, adapter }, i) => () => runComplete(adapter, buildCritiquePrompt(prompt, proposalOutputs2, i), {
22376
+ const proposals2 = successful.map((p) => ({ debater: p.debater, output: p.output }));
22377
+ const critiqueBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: proposals2.map((p) => p.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22378
+ const critiqueSettled = await allSettledBounded(successful.map(({ debater, adapter }, i) => () => runComplete(adapter, critiqueBuilder.buildCritiquePrompt(i, proposals2), {
22171
22379
  model: resolveDebaterModel(debater, ctx.config),
22172
22380
  featureName: ctx.stage,
22173
22381
  config: ctx.config,
@@ -22210,15 +22418,18 @@ async function runOneShot(ctx, prompt) {
22210
22418
  };
22211
22419
  }
22212
22420
  var init_session_one_shot = __esm(() => {
22421
+ init_personas();
22422
+ init_prompt_builder();
22213
22423
  init_session_helpers();
22214
22424
  });
22215
22425
 
22216
22426
  // src/debate/session-plan.ts
22217
22427
  import { join as join10 } from "path";
22218
- async function runPlan2(ctx, basePrompt, opts) {
22428
+ async function runPlan2(ctx, taskContext, outputFormat, opts) {
22219
22429
  const logger = _debateSessionDeps.getSafeLogger();
22220
22430
  const config2 = ctx.stageConfig;
22221
- const debaters = config2.debaters ?? [];
22431
+ const rawDebaters = config2.debaters ?? [];
22432
+ const debaters = resolvePersonas(rawDebaters, "plan", config2.autoPersona ?? false);
22222
22433
  let totalCostUsd = 0;
22223
22434
  const resolved = [];
22224
22435
  for (const debater of debaters) {
@@ -22236,14 +22447,15 @@ async function runPlan2(ctx, basePrompt, opts) {
22236
22447
  });
22237
22448
  const debate = ctx.config?.debate;
22238
22449
  const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
22239
- const settled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => async () => {
22450
+ const proposalBuilder = new DebatePromptBuilder({ taskContext, outputFormat, stage: "plan" }, { debaters: resolved.map((r) => r.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22451
+ const settled = await allSettledBounded(resolved.map(({ debater: rd, adapter }, i) => async () => {
22240
22452
  const tempOutputPath = join10(opts.outputDir, `prd-debate-${i}.json`);
22241
- const debaterPrompt = `${basePrompt}
22453
+ const debaterPrompt = `${proposalBuilder.buildProposalPrompt(i)}
22242
22454
 
22243
22455
  Write the PRD JSON directly to this file path: ${tempOutputPath}
22244
22456
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.`;
22245
- const modelTier = modelTierFromDebater(debater);
22246
- const modelDef = resolveModelDefForDebater(debater, modelTier, ctx.config);
22457
+ const modelTier = modelTierFromDebater(rd);
22458
+ const modelDef = resolveModelDefForDebater(rd, modelTier, ctx.config);
22247
22459
  const planResult = await adapter.plan({
22248
22460
  prompt: debaterPrompt,
22249
22461
  workdir: opts.workdir,
@@ -22259,7 +22471,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22259
22471
  sessionRole: `plan-${i}`
22260
22472
  });
22261
22473
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22262
- return { debater, adapter, output, cost: planResult.costUsd ?? 0 };
22474
+ return { debater: rd, adapter, output, cost: planResult.costUsd ?? 0 };
22263
22475
  }), concurrencyLimit);
22264
22476
  const successful = [];
22265
22477
  for (let i = 0;i < settled.length; i++) {
@@ -22332,7 +22544,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22332
22544
  featureName: opts.feature,
22333
22545
  timeoutSeconds: opts.timeoutSeconds ?? 600
22334
22546
  };
22335
- const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, basePrompt, "plan-hybrid");
22547
+ const rebuttalBuilder = new DebatePromptBuilder({ taskContext, outputFormat: "", stage: "plan" }, { debaters: successful.map((p) => p.debater), sessionMode });
22548
+ const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, rebuttalBuilder, "plan-hybrid");
22336
22549
  critiqueOutputs = rebuttals.map((r) => r.output);
22337
22550
  rebuttalList = rebuttals;
22338
22551
  totalCostUsd += costUsd;
@@ -22363,6 +22576,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22363
22576
  };
22364
22577
  }
22365
22578
  var init_session_plan = __esm(() => {
22579
+ init_personas();
22580
+ init_prompt_builder();
22366
22581
  init_session_helpers();
22367
22582
  init_session_hybrid();
22368
22583
  });
@@ -22448,13 +22663,13 @@ class DebateSession {
22448
22663
  resolverContextInput: this.resolverContextInput
22449
22664
  }, prompt);
22450
22665
  }
22451
- async runPlan(basePrompt, opts) {
22666
+ async runPlan(taskContext, outputFormat, opts) {
22452
22667
  return runPlan2({
22453
22668
  storyId: this.storyId,
22454
22669
  stage: this.stage,
22455
22670
  stageConfig: this.stageConfig,
22456
22671
  config: this.config
22457
- }, basePrompt, opts);
22672
+ }, taskContext, outputFormat, opts);
22458
22673
  }
22459
22674
  }
22460
22675
  var DEFAULT_TIMEOUT_SECONDS = 600;
@@ -22471,8 +22686,8 @@ var init_session = __esm(() => {
22471
22686
  var init_debate = __esm(() => {
22472
22687
  init_session();
22473
22688
  init_session_helpers();
22474
- init_resolvers();
22475
- init_session_helpers();
22689
+ init_prompt_builder();
22690
+ init_personas();
22476
22691
  });
22477
22692
 
22478
22693
  // src/interaction/bridge-builder.ts
@@ -23653,50 +23868,6 @@ var init_init = __esm(() => {
23653
23868
  init_webhook();
23654
23869
  });
23655
23870
 
23656
- // src/utils/llm-json.ts
23657
- function extractJsonFromMarkdown(text) {
23658
- const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
23659
- if (match) {
23660
- return match[1] ?? text;
23661
- }
23662
- return text;
23663
- }
23664
- function stripTrailingCommas(text) {
23665
- return text.replace(/,\s*([}\]])/g, "$1");
23666
- }
23667
- function extractJsonObject(text) {
23668
- const objStart = text.indexOf("{");
23669
- const arrStart = text.indexOf("[");
23670
- let start;
23671
- let closeChar;
23672
- if (objStart === -1 && arrStart === -1)
23673
- return null;
23674
- if (objStart === -1) {
23675
- start = arrStart;
23676
- closeChar = "]";
23677
- } else if (arrStart === -1) {
23678
- start = objStart;
23679
- closeChar = "}";
23680
- } else if (objStart < arrStart) {
23681
- start = objStart;
23682
- closeChar = "}";
23683
- } else {
23684
- start = arrStart;
23685
- closeChar = "]";
23686
- }
23687
- const end = text.lastIndexOf(closeChar);
23688
- if (end <= start)
23689
- return null;
23690
- return text.slice(start, end + 1);
23691
- }
23692
- function wrapJsonPrompt(prompt) {
23693
- return `IMPORTANT: Your entire response must be a single JSON object or array. Do not explain your reasoning. Do not use markdown formatting. Output ONLY the JSON.
23694
-
23695
- ${prompt.trim()}
23696
-
23697
- YOUR RESPONSE MUST START WITH { OR [ AND END WITH } OR ]. No other text.`;
23698
- }
23699
-
23700
23871
  // src/prd/validate.ts
23701
23872
  function validateStoryId(id) {
23702
23873
  if (!id || id.length === 0) {
@@ -25802,7 +25973,7 @@ Rules:
25802
25973
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
25803
25974
  - **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
25804
25975
  - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
25805
- - **Path anchor (CRITICAL)**: This test file will be saved at \`<repo-root>/.nax/features/${featureName}/${resolvedTestPath}\` and will ALWAYS run from the repo root. The repo root is exactly 4 \`../\` levels above \`__dirname\`: \`join(__dirname, '..', '..', '..', '..')\`. For monorepo projects, navigate into packages from root (e.g. \`join(root, 'apps/api/src')\`).`;
25976
+ - **Path anchor (CRITICAL)**: This test file will be saved at \`<repo-root>/.nax/features/${featureName}/${resolvedTestPath}\` and will ALWAYS run from the repo root. The repo root is exactly 3 \`../\` levels above \`__dirname\`: \`join(__dirname, '..', '..', '..')\`. For monorepo projects, navigate into packages from root (e.g. \`join(root, 'apps/api/src')\`).`;
25806
25977
  }
25807
25978
  async function generateAcceptanceTests(adapter, options) {
25808
25979
  const logger = getLogger();
@@ -26960,6 +27131,7 @@ function buildReviewPrompt(diff, story, _semanticConfig) {
26960
27131
  "## Diff",
26961
27132
  diff,
26962
27133
  "",
27134
+ "Also flag any changes in the diff not required by the acceptance criteria above as out-of-scope findings.",
26963
27135
  "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
26964
27136
  ].join(`
26965
27137
  `);
@@ -26981,7 +27153,7 @@ function buildReReviewPrompt(updatedDiff, previousFindings) {
26981
27153
  ].join(`
26982
27154
  `);
26983
27155
  }
26984
- function buildProposalsSection(proposals) {
27156
+ function buildProposalsSection2(proposals) {
26985
27157
  return proposals.map((p) => `### ${p.debater}
26986
27158
  ${p.output}`).join(`
26987
27159
 
@@ -27025,7 +27197,7 @@ function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticC
27025
27197
  `);
27026
27198
  const framing = buildResolverFraming(resolverContext);
27027
27199
  const voteTally = buildVoteTallyLine(resolverContext);
27028
- const proposalsSection = buildProposalsSection(proposals);
27200
+ const proposalsSection = buildProposalsSection2(proposals);
27029
27201
  const critiquesSection = buildCritiquesSection(critiques);
27030
27202
  return [
27031
27203
  framing,
@@ -27051,7 +27223,7 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27051
27223
  const framing = buildResolverFraming(resolverContext);
27052
27224
  const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
27053
27225
  `) : "(none)";
27054
- const proposalsSection = buildProposalsSection(proposals);
27226
+ const proposalsSection = buildProposalsSection2(proposals);
27055
27227
  const critiquesSection = buildCritiquesSection(critiques);
27056
27228
  return [
27057
27229
  `${framing} This is a re-review after implementer changes.`,
@@ -27074,12 +27246,10 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27074
27246
 
27075
27247
  // src/review/dialogue.ts
27076
27248
  function extractDeltaSummary(rawOutput, previousFindings, newFindings) {
27077
- try {
27078
- const parsed = JSON.parse(rawOutput);
27079
- if (typeof parsed.deltaSummary === "string" && parsed.deltaSummary.length > 0) {
27080
- return parsed.deltaSummary;
27081
- }
27082
- } catch {}
27249
+ const parsed = tryParseLLMJson(rawOutput);
27250
+ if (parsed && typeof parsed.deltaSummary === "string" && parsed.deltaSummary.length > 0) {
27251
+ return parsed.deltaSummary;
27252
+ }
27083
27253
  const newIds = new Set(newFindings.map((f) => f.ruleId));
27084
27254
  const prevIds = new Set(previousFindings.map((f) => f.ruleId));
27085
27255
  const resolved = previousFindings.filter((f) => !newIds.has(f.ruleId));
@@ -27119,7 +27289,7 @@ function compactHistory(history) {
27119
27289
  function parseReviewResponse(output) {
27120
27290
  let parsed;
27121
27291
  try {
27122
- parsed = JSON.parse(output);
27292
+ parsed = parseLLMJson(output);
27123
27293
  } catch {
27124
27294
  throw new NaxError("[dialogue] Failed to parse reviewer JSON response", "REVIEWER_PARSE_FAILED", {
27125
27295
  stage: "review",
@@ -27698,23 +27868,11 @@ function validateLLMShape(parsed) {
27698
27868
  return { passed: obj.passed, findings: obj.findings };
27699
27869
  }
27700
27870
  function parseLLMResponse(raw) {
27701
- const text = raw.trim();
27702
27871
  try {
27703
- return validateLLMShape(JSON.parse(text));
27704
- } catch {}
27705
- const fromFence = extractJsonFromMarkdown(text);
27706
- if (fromFence !== text) {
27707
- try {
27708
- return validateLLMShape(JSON.parse(stripTrailingCommas(fromFence)));
27709
- } catch {}
27710
- }
27711
- const bareJson = extractJsonObject(text);
27712
- if (bareJson) {
27713
- try {
27714
- return validateLLMShape(JSON.parse(stripTrailingCommas(bareJson)));
27715
- } catch {}
27872
+ return validateLLMShape(tryParseLLMJson(raw));
27873
+ } catch {
27874
+ return null;
27716
27875
  }
27717
- return null;
27718
27876
  }
27719
27877
  function formatFindings(findings) {
27720
27878
  return findings.map((f) => `[${f.severity}] ${f.file}:${f.line} \u2014 ${f.issue}
@@ -28689,6 +28847,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28689
28847
  config: ctx.config,
28690
28848
  projectDir: ctx.projectDir,
28691
28849
  maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
28850
+ featureName: ctx.prd.feature,
28692
28851
  storyId: ctx.story.id,
28693
28852
  sessionRole: "implementer"
28694
28853
  });
@@ -30466,61 +30625,34 @@ var init_executor = __esm(() => {
30466
30625
  });
30467
30626
 
30468
30627
  // src/verification/parser.ts
30469
- function parseBunTestOutput(output) {
30470
- if (isJestLikeOutput(output)) {
30471
- return parseJestOutput(output);
30472
- }
30473
- return parseBunOutput(output);
30474
- }
30475
- function isJestLikeOutput(output) {
30476
- return /^\s*Tests:\s+\d+/m.test(output) || /^\s*Test Files\s+\d+/m.test(output);
30628
+ function detectFramework(output) {
30629
+ if (/^\s*Test Files\s+\d+/m.test(output))
30630
+ return "vitest";
30631
+ if (/^\s*Tests:\s+\d+/m.test(output))
30632
+ return "jest";
30633
+ if (/={3,}\s+\d+\s+(?:failed|passed).*in\s+[\d.]+s\s*={3,}/m.test(output))
30634
+ return "pytest";
30635
+ if (/^--- (?:FAIL|PASS):/m.test(output) || /^(?:ok|FAIL)\s+\t/m.test(output))
30636
+ return "go";
30637
+ if (/^\(fail\)\s/m.test(output) || /^bun test/m.test(output) || /[\u2713\u2714\u2717\u2718]/m.test(output))
30638
+ return "bun";
30639
+ return "unknown";
30477
30640
  }
30478
- function parseJestOutput(output) {
30479
- const failures = [];
30480
- let passed = 0;
30481
- let failed = 0;
30482
- const summaryMatches = Array.from(output.matchAll(/^\s*Tests:\s+(.*)/gm));
30483
- if (summaryMatches.length > 0) {
30484
- const summaryLine = summaryMatches[summaryMatches.length - 1][1];
30485
- const failedMatch = summaryLine.match(/(\d+)\s+failed/);
30486
- const passedMatch = summaryLine.match(/(\d+)\s+passed/);
30487
- if (failedMatch)
30488
- failed = Number.parseInt(failedMatch[1], 10);
30489
- if (passedMatch)
30490
- passed = Number.parseInt(passedMatch[1], 10);
30491
- }
30492
- let currentFile = "unknown";
30493
- const lines = output.split(`
30494
- `);
30495
- for (let i = 0;i < lines.length; i++) {
30496
- const line = lines[i];
30497
- const fileMatch = line.match(/^\s*(?:FAIL|PASS)\s+(\S+\.[jt]sx?)/);
30498
- if (fileMatch) {
30499
- currentFile = fileMatch[1];
30500
- continue;
30501
- }
30502
- const bulletMatch = line.match(/^\s+\u25CF\s+(.+)$/);
30503
- if (bulletMatch) {
30504
- const testName = bulletMatch[1].trim();
30505
- let error48 = "";
30506
- for (let j = i + 1;j < lines.length && j < i + 10; j++) {
30507
- const next = lines[j].trim();
30508
- if (!next)
30509
- continue;
30510
- if (next.startsWith("\u25CF") || /^(?:FAIL|PASS)\s/.test(next))
30511
- break;
30512
- error48 = next;
30513
- break;
30514
- }
30515
- failures.push({
30516
- file: currentFile,
30517
- testName,
30518
- error: error48 || "Unknown error",
30519
- stackTrace: []
30520
- });
30521
- }
30641
+ function parseTestOutput(output) {
30642
+ const framework = detectFramework(output);
30643
+ switch (framework) {
30644
+ case "bun":
30645
+ return parseBunOutput(output);
30646
+ case "jest":
30647
+ case "vitest":
30648
+ return parseJestOutput(output);
30649
+ case "pytest":
30650
+ return parsePytestOutput(output);
30651
+ case "go":
30652
+ return parseGoTestOutput(output);
30653
+ default:
30654
+ return parseCommonOutput(output);
30522
30655
  }
30523
- return { passed, failed, failures };
30524
30656
  }
30525
30657
  function parseBunOutput(output) {
30526
30658
  const lines = output.split(`
@@ -30582,6 +30714,122 @@ function parseBunOutput(output) {
30582
30714
  }
30583
30715
  return { passed, failed, failures };
30584
30716
  }
30717
+ function parseJestOutput(output) {
30718
+ const failures = [];
30719
+ let passed = 0;
30720
+ let failed = 0;
30721
+ const summaryMatches = Array.from(output.matchAll(/^\s*Tests:\s+(.*)/gm));
30722
+ if (summaryMatches.length > 0) {
30723
+ const summaryLine = summaryMatches[summaryMatches.length - 1][1];
30724
+ const failedMatch = summaryLine.match(/(\d+)\s+failed/);
30725
+ const passedMatch = summaryLine.match(/(\d+)\s+passed/);
30726
+ if (failedMatch)
30727
+ failed = Number.parseInt(failedMatch[1], 10);
30728
+ if (passedMatch)
30729
+ passed = Number.parseInt(passedMatch[1], 10);
30730
+ }
30731
+ let currentFile = "unknown";
30732
+ const lines = output.split(`
30733
+ `);
30734
+ for (let i = 0;i < lines.length; i++) {
30735
+ const line = lines[i];
30736
+ const fileMatch = line.match(/^\s*(?:FAIL|PASS)\s+(\S+\.[jt]sx?)/);
30737
+ if (fileMatch) {
30738
+ currentFile = fileMatch[1];
30739
+ continue;
30740
+ }
30741
+ const bulletMatch = line.match(/^\s+\u25CF\s+(.+)$/);
30742
+ if (bulletMatch) {
30743
+ const testName = bulletMatch[1].trim();
30744
+ let error48 = "";
30745
+ for (let j = i + 1;j < lines.length && j < i + 10; j++) {
30746
+ const next = lines[j].trim();
30747
+ if (!next)
30748
+ continue;
30749
+ if (next.startsWith("\u25CF") || /^(?:FAIL|PASS)\s/.test(next))
30750
+ break;
30751
+ error48 = next;
30752
+ break;
30753
+ }
30754
+ failures.push({
30755
+ file: currentFile,
30756
+ testName,
30757
+ error: error48 || "Unknown error",
30758
+ stackTrace: []
30759
+ });
30760
+ }
30761
+ }
30762
+ return { passed, failed, failures };
30763
+ }
30764
+ function parsePytestOutput(output) {
30765
+ const common = parseCommonOutput(output);
30766
+ const failures = [];
30767
+ for (const line of output.split(`
30768
+ `)) {
30769
+ const m = line.match(/^FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
30770
+ if (m) {
30771
+ const [, location, reason] = m;
30772
+ const parts = location.split("::");
30773
+ failures.push({
30774
+ file: parts[0] ?? location,
30775
+ testName: parts.slice(1).join(" > ") || location,
30776
+ error: reason?.trim() || "Unknown error",
30777
+ stackTrace: []
30778
+ });
30779
+ }
30780
+ }
30781
+ return {
30782
+ passed: common.passed,
30783
+ failed: common.failed,
30784
+ failures: failures.length > 0 ? failures : common.failures
30785
+ };
30786
+ }
30787
+ function parseGoTestOutput(output) {
30788
+ const common = parseCommonOutput(output);
30789
+ const failures = [];
30790
+ for (const line of output.split(`
30791
+ `)) {
30792
+ const m = line.match(/^--- FAIL:\s+(\S+)\s+\([\d.]+s\)/);
30793
+ if (m) {
30794
+ failures.push({
30795
+ file: "unknown",
30796
+ testName: m[1],
30797
+ error: "Unknown error",
30798
+ stackTrace: []
30799
+ });
30800
+ }
30801
+ }
30802
+ return {
30803
+ passed: common.passed,
30804
+ failed: common.failed,
30805
+ failures: failures.length > 0 ? failures : common.failures
30806
+ };
30807
+ }
30808
+ function parseCommonOutput(output) {
30809
+ let passed = 0;
30810
+ let failed = 0;
30811
+ const patterns = [
30812
+ /(\d+)\s+pass(?:ed)?(?:,\s*|\s+)(\d+)\s+fail/i,
30813
+ /Tests:\s+(\d+)\s+passed,\s+(\d+)\s+failed/i,
30814
+ /(\d+)\s+pass/i
30815
+ ];
30816
+ for (const pattern of patterns) {
30817
+ const matches = Array.from(output.matchAll(new RegExp(pattern, "gi")));
30818
+ if (matches.length > 0) {
30819
+ const last = matches[matches.length - 1];
30820
+ passed = Number.parseInt(last[1], 10);
30821
+ failed = last[2] ? Number.parseInt(last[2], 10) : 0;
30822
+ break;
30823
+ }
30824
+ }
30825
+ if (failed === 0) {
30826
+ const failMatches = Array.from(output.matchAll(/(\d+)\s+fail/gi));
30827
+ if (failMatches.length > 0) {
30828
+ failed = Number.parseInt(failMatches[failMatches.length - 1][1], 10);
30829
+ }
30830
+ }
30831
+ return { passed, failed, failures: [] };
30832
+ }
30585
30833
  function formatFailureSummary(failures, maxChars = 2000) {
30586
30834
  if (failures.length === 0) {
30587
30835
  return "No test failures";
@@ -30595,48 +30843,24 @@ function formatFailureSummary(failures, maxChars = 2000) {
30595
30843
  const errorLine = ` Error: ${failure.error}`;
30596
30844
  const stackLine = failure.stackTrace.length > 0 ? ` ${failure.stackTrace[0]}` : "";
30597
30845
  const blockLines = [header, errorLine];
30598
- if (stackLine) {
30846
+ if (stackLine)
30599
30847
  blockLines.push(stackLine);
30600
- }
30601
30848
  blockLines.push("");
30602
30849
  const block = blockLines.join(`
30603
30850
  `);
30604
- const blockLength = block.length;
30605
- if (totalChars + blockLength > maxChars && lines.length > 0) {
30606
- const remaining = failures.length - i;
30851
+ if (totalChars + block.length > maxChars && lines.length > 0) {
30607
30852
  lines.push(`
30608
- ... and ${remaining} more failure(s) (truncated)`);
30853
+ ... and ${failures.length - i} more failure(s) (truncated)`);
30609
30854
  break;
30610
30855
  }
30611
30856
  lines.push(...blockLines);
30612
- totalChars += blockLength;
30857
+ totalChars += block.length;
30613
30858
  }
30614
30859
  return lines.join(`
30615
30860
  `).trim();
30616
30861
  }
30617
- function parseTestOutput(output, exitCode) {
30618
- const patterns = [
30619
- /(\d+)\s+pass(?:ed)?(?:,\s+|\s+)(\d+)\s+fail/i,
30620
- /Tests:\s+(\d+)\s+passed,\s+(\d+)\s+failed/i,
30621
- /(\d+)\s+pass/i
30622
- ];
30623
- let passCount = 0;
30624
- let failCount = 0;
30625
- for (const pattern of patterns) {
30626
- const matches = Array.from(output.matchAll(new RegExp(pattern, "gi")));
30627
- if (matches.length > 0) {
30628
- const lastMatch = matches[matches.length - 1];
30629
- passCount = Number.parseInt(lastMatch[1], 10);
30630
- failCount = lastMatch[2] ? Number.parseInt(lastMatch[2], 10) : 0;
30631
- break;
30632
- }
30633
- }
30634
- if (failCount === 0) {
30635
- const failMatches = Array.from(output.matchAll(/(\d+)\s+fail/gi));
30636
- if (failMatches.length > 0) {
30637
- failCount = Number.parseInt(failMatches[failMatches.length - 1][1], 10);
30638
- }
30639
- }
30862
+ function analyzeTestExitCode(output, exitCode) {
30863
+ const { passed: passCount, failed: failCount } = parseCommonOutput(output);
30640
30864
  const allTestsPassed = passCount > 0 && failCount === 0;
30641
30865
  const isEnvironmentalFailure = allTestsPassed && exitCode !== 0;
30642
30866
  const result = {
@@ -30707,7 +30931,7 @@ async function runVerificationCore(options) {
30707
30931
  }
30708
30932
  const exitCode = execution.exitCode ?? 1;
30709
30933
  if (exitCode !== 0 && execution.output) {
30710
- const analysis = parseTestOutput(execution.output, exitCode);
30934
+ const analysis = analyzeTestExitCode(execution.output, exitCode);
30711
30935
  if (analysis.isEnvironmentalFailure) {
30712
30936
  return {
30713
30937
  status: "ENVIRONMENTAL_FAILURE",
@@ -31000,7 +31224,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
31000
31224
  });
31001
31225
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
31002
31226
  if (!fullSuitePassed && fullSuiteResult.output) {
31003
- const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
31227
+ const testSummary = _rectificationGateDeps.parseTestOutput(fullSuiteResult.output);
31004
31228
  if (testSummary.failed > 0) {
31005
31229
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName, projectDir);
31006
31230
  }
@@ -31134,7 +31358,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
31134
31358
  return true;
31135
31359
  }
31136
31360
  if (retryFullSuite.output) {
31137
- const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
31361
+ const newTestSummary = _rectificationGateDeps.parseTestOutput(retryFullSuite.output);
31138
31362
  state.currentFailures = newTestSummary.failed;
31139
31363
  testSummary.failures = newTestSummary.failures;
31140
31364
  testSummary.failed = newTestSummary.failed;
@@ -31182,7 +31406,7 @@ var init_rectification_gate = __esm(() => {
31182
31406
  init_prompts();
31183
31407
  _rectificationGateDeps = {
31184
31408
  executeWithTimeout,
31185
- parseBunTestOutput,
31409
+ parseTestOutput,
31186
31410
  shouldRetryRectification
31187
31411
  };
31188
31412
  });
@@ -33152,9 +33376,6 @@ function calculateMaxIterations(tierOrder) {
33152
33376
  return tierOrder.reduce((sum, t) => sum + t.attempts, 0);
33153
33377
  }
33154
33378
 
33155
- // src/execution/test-output-parser.ts
33156
- var init_test_output_parser = () => {};
33157
-
33158
33379
  // src/verification/rectification-loop.ts
33159
33380
  async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
33160
33381
  const logger = getSafeLogger();
@@ -33207,7 +33428,7 @@ async function runRectificationLoop2(opts) {
33207
33428
  } = opts;
33208
33429
  const logger = getSafeLogger();
33209
33430
  const rectificationConfig = config2.execution.rectification;
33210
- const testSummary = parseBunTestOutput(testOutput);
33431
+ const testSummary = parseTestOutput(testOutput);
33211
33432
  const label = promptPrefix ? "regression rectification" : "rectification";
33212
33433
  const rectificationState = {
33213
33434
  attempt: 0,
@@ -33342,13 +33563,13 @@ ${rectificationPrompt}`;
33342
33563
  return true;
33343
33564
  }
33344
33565
  if (retryVerification.output) {
33345
- const newTestSummary = parseBunTestOutput(retryVerification.output);
33566
+ const newTestSummary = parseTestOutput(retryVerification.output);
33346
33567
  state.currentFailures = newTestSummary.failed;
33347
33568
  state.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
33348
33569
  testSummary.failures = newTestSummary.failures;
33349
33570
  testSummary.failed = newTestSummary.failed;
33350
33571
  testSummary.passed = newTestSummary.passed;
33351
- if (newTestSummary.failed === 0) {
33572
+ if (newTestSummary.failed === 0 && (retryVerification.status === "SUCCESS" || newTestSummary.passed > 0)) {
33352
33573
  state.lastExitCode = 0;
33353
33574
  logger?.info("rectification", `[OK] ${label} succeeded after parsing retry output`, {
33354
33575
  storyId: story.id,
@@ -33488,7 +33709,6 @@ var init_rectification_loop = __esm(() => {
33488
33709
  init_cost();
33489
33710
  init_registry();
33490
33711
  init_config();
33491
- init_test_output_parser();
33492
33712
  init_logger2();
33493
33713
  init_prd();
33494
33714
  init_rectification();
@@ -33754,7 +33974,7 @@ class RegressionStrategy {
33754
33974
  });
33755
33975
  const durationMs = Date.now() - start;
33756
33976
  if (result.success) {
33757
- const parsed2 = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33977
+ const parsed2 = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33758
33978
  return makePassResult(ctx.storyId, "regression", {
33759
33979
  rawOutput: result.output,
33760
33980
  passCount: parsed2.passed,
@@ -33770,7 +33990,7 @@ class RegressionStrategy {
33770
33990
  if (result.status === "TIMEOUT") {
33771
33991
  return makeFailResult(ctx.storyId, "regression", "TIMEOUT", { rawOutput: result.output, durationMs });
33772
33992
  }
33773
- const parsed = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33993
+ const parsed = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33774
33994
  return makeFailResult(ctx.storyId, "regression", "TEST_FAILURE", {
33775
33995
  rawOutput: result.output,
33776
33996
  passCount: parsed.passed,
@@ -33994,7 +34214,7 @@ class ScopedStrategy {
33994
34214
  });
33995
34215
  const durationMs = Date.now() - start;
33996
34216
  if (result.success) {
33997
- const parsed2 = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34217
+ const parsed2 = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33998
34218
  return makePassResult(ctx.storyId, "scoped", {
33999
34219
  rawOutput: result.output,
34000
34220
  passCount: parsed2.passed,
@@ -34010,7 +34230,7 @@ class ScopedStrategy {
34010
34230
  scopeTestFallback
34011
34231
  });
34012
34232
  }
34013
- const parsed = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34233
+ const parsed = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34014
34234
  return makeFailResult(ctx.storyId, "scoped", "TEST_FAILURE", {
34015
34235
  rawOutput: result.output,
34016
34236
  passCount: parsed.passed,
@@ -34344,12 +34564,11 @@ function stripCodeFences(text) {
34344
34564
  return trimmed;
34345
34565
  }
34346
34566
  function parseRoutingResponse(output, story, config2) {
34347
- const jsonText = extractJsonFromMarkdown(output.trim());
34348
- const parsed = JSON.parse(jsonText);
34567
+ const parsed = parseLLMJson(output);
34349
34568
  return validateRoutingDecision(parsed, config2, story);
34350
34569
  }
34351
34570
  function parseBatchResponse(output, stories, config2) {
34352
- const parsed = JSON.parse(extractJsonFromMarkdown(output.trim()));
34571
+ const parsed = parseLLMJson(output);
34353
34572
  if (!Array.isArray(parsed)) {
34354
34573
  throw new Error("Batch LLM response must be a JSON array");
34355
34574
  }
@@ -36143,7 +36362,7 @@ var package_default;
36143
36362
  var init_package = __esm(() => {
36144
36363
  package_default = {
36145
36364
  name: "@nathapp/nax",
36146
- version: "0.59.2",
36365
+ version: "0.59.3",
36147
36366
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
36148
36367
  type: "module",
36149
36368
  bin: {
@@ -36223,8 +36442,8 @@ var init_version = __esm(() => {
36223
36442
  NAX_VERSION = package_default.version;
36224
36443
  NAX_COMMIT = (() => {
36225
36444
  try {
36226
- if (/^[0-9a-f]{6,10}$/.test("d42d47be"))
36227
- return "d42d47be";
36445
+ if (/^[0-9a-f]{6,10}$/.test("0c763972"))
36446
+ return "0c763972";
36228
36447
  } catch {}
36229
36448
  try {
36230
36449
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -36887,28 +37106,17 @@ function parseDiagnosisResult(output) {
36887
37106
  if (!output || output.trim() === "") {
36888
37107
  return null;
36889
37108
  }
36890
- try {
36891
- const cleaned = output.trim();
36892
- let jsonStr = cleaned;
36893
- const firstBrace = cleaned.indexOf("{");
36894
- const lastBrace = cleaned.lastIndexOf("}");
36895
- if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
36896
- jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
36897
- }
36898
- const parsed = JSON.parse(jsonStr);
36899
- if (typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
36900
- return {
36901
- verdict: parsed.verdict,
36902
- reasoning: parsed.reasoning,
36903
- confidence: parsed.confidence,
36904
- testIssues: parsed.testIssues,
36905
- sourceIssues: parsed.sourceIssues
36906
- };
36907
- }
36908
- return null;
36909
- } catch {
36910
- return null;
37109
+ const parsed = tryParseLLMJson(output);
37110
+ if (parsed && typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
37111
+ return {
37112
+ verdict: parsed.verdict,
37113
+ reasoning: parsed.reasoning,
37114
+ confidence: parsed.confidence,
37115
+ testIssues: Array.isArray(parsed.testIssues) ? parsed.testIssues : undefined,
37116
+ sourceIssues: Array.isArray(parsed.sourceIssues) ? parsed.sourceIssues : undefined
37117
+ };
36911
37118
  }
37119
+ return null;
36912
37120
  }
36913
37121
  var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
36914
37122
  var init_fix_diagnosis = __esm(() => {
@@ -37106,6 +37314,12 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
37106
37314
  logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
37107
37315
  const { unlink: unlink3 } = await import("fs/promises");
37108
37316
  await unlink3(testPath);
37317
+ if (acceptanceContext.featureDir) {
37318
+ const metaPath = path15.join(acceptanceContext.featureDir, "acceptance-meta.json");
37319
+ try {
37320
+ await unlink3(metaPath);
37321
+ } catch {}
37322
+ }
37109
37323
  let implementationContext;
37110
37324
  const storyGitRef = acceptanceContext.storyGitRef;
37111
37325
  const workdir = acceptanceContext.workdir;
@@ -37746,7 +37960,7 @@ async function runDeferredRegression(options) {
37746
37960
  affectedStories: []
37747
37961
  };
37748
37962
  }
37749
- const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
37963
+ const testSummary = _regressionDeps.parseTestOutput(fullSuiteResult.output);
37750
37964
  if (testSummary.failed === 0 && testSummary.passed === 0) {
37751
37965
  logger?.warn("regression", "No test results parsed from output \u2014 test runner likely crashed or errored (not a regression, accepting as pass)", { output: fullSuiteResult.output.slice(0, 500) });
37752
37966
  return {
@@ -37892,7 +38106,7 @@ var init_run_regression = __esm(() => {
37892
38106
  _regressionDeps = {
37893
38107
  runVerification: fullSuite,
37894
38108
  runRectificationLoop: runRectificationLoop2,
37895
- parseBunTestOutput,
38109
+ parseTestOutput,
37896
38110
  reverseMapTestToSource
37897
38111
  };
37898
38112
  });
@@ -38873,6 +39087,11 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
38873
39087
  const diffSummary = await captureDiffSummary(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
38874
39088
  if (diffSummary) {
38875
39089
  completedStory.diffSummary = diffSummary;
39090
+ } else {
39091
+ logger?.debug("context-chain", "No diff summary captured (agent may not have committed yet)", {
39092
+ storyId: completedStory.id,
39093
+ storyGitRef: ctx.storyGitRef
39094
+ });
38876
39095
  }
38877
39096
  } catch {}
38878
39097
  }
@@ -72809,13 +73028,16 @@ function validateStory(raw, index, allIds) {
72809
73028
  throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
72810
73029
  }
72811
73030
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
72812
- const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
73031
+ let testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
72813
73032
  const rawJustification = routing.noTestJustification ?? s.noTestJustification;
72814
73033
  if (testStrategy === "no-test") {
72815
73034
  if (!rawJustification || typeof rawJustification !== "string" || rawJustification.trim() === "") {
72816
73035
  throw new Error(`[schema] story[${index}].routing.noTestJustification is required when testStrategy is "no-test"`);
72817
73036
  }
72818
73037
  }
73038
+ if (testStrategy !== "no-test" && typeof rawJustification === "string" && rawJustification.trim() !== "") {
73039
+ testStrategy = "no-test";
73040
+ }
72819
73041
  const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
72820
73042
  const rawDeps = s.dependencies;
72821
73043
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
@@ -72975,7 +73197,7 @@ async function planCommand(workdir, config2, options) {
72975
73197
  let rawResponse;
72976
73198
  const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
72977
73199
  if (debateEnabled) {
72978
- const basePrompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
73200
+ const { taskContext: planTaskContext, outputFormat: planOutputFormat } = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
72979
73201
  const resolvedPerm = resolvePermissions(config2, "plan");
72980
73202
  const planStageConfig = config2?.debate?.stages.plan;
72981
73203
  const debateSession = _planDeps.createDebateSession({
@@ -72992,7 +73214,7 @@ async function planCommand(workdir, config2, options) {
72992
73214
  rounds: planStageConfig.rounds,
72993
73215
  feature: options.feature
72994
73216
  });
72995
- const debateResult = await debateSession.runPlan(basePrompt, {
73217
+ const debateResult = await debateSession.runPlan(planTaskContext, planOutputFormat, {
72996
73218
  workdir,
72997
73219
  feature: options.feature,
72998
73220
  outputDir,
@@ -73011,7 +73233,10 @@ async function planCommand(workdir, config2, options) {
73011
73233
  }
73012
73234
  } else if (options.auto) {
73013
73235
  const isAcp = config2?.agent?.protocol === "acp";
73014
- const prompt = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
73236
+ const { taskContext: autoTaskCtx, outputFormat: autoOutputFmt } = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
73237
+ const prompt = `${autoTaskCtx}
73238
+
73239
+ ${autoOutputFmt}`;
73015
73240
  const adapter = _planDeps.getAgent(agentName, config2);
73016
73241
  if (!adapter)
73017
73242
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73097,7 +73322,10 @@ async function planCommand(workdir, config2, options) {
73097
73322
  rawResponse = await runInteractivePlan();
73098
73323
  }
73099
73324
  async function runInteractivePlan() {
73100
- const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
73325
+ const { taskContext: interactiveTaskCtx, outputFormat: interactiveOutputFmt } = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
73326
+ const prompt = `${interactiveTaskCtx}
73327
+
73328
+ ${interactiveOutputFmt}`;
73101
73329
  const adapter = _planDeps.getAgent(agentName, config2);
73102
73330
  if (!adapter)
73103
73331
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73304,7 +73532,7 @@ ${packageDetailsSection}
73304
73532
  For each user story, set the "workdir" field to the relevant package path (e.g. "packages/api"). Stories that span the root should omit "workdir".` : "";
73305
73533
  const workdirField = isMonorepo ? `
73306
73534
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
73307
- return `You are a senior software architect generating a product requirements document (PRD) as JSON.
73535
+ const taskContext = `You are a senior software architect generating a product requirements document (PRD) as JSON.
73308
73536
 
73309
73537
  ## Step 1: Understand the Spec
73310
73538
 
@@ -73332,6 +73560,8 @@ If this is a greenfield project (empty or minimal codebase):
73332
73560
 
73333
73561
  Record ALL findings in the "analysis" field of the output JSON. This analysis is provided to every implementation agent as context \u2014 be thorough.
73334
73562
 
73563
+ **Important:** The codebase context below contains file names and structure only \u2014 no file content. Do NOT assert specific line numbers. The implementer will read the actual files via contextFiles.
73564
+
73335
73565
  ## Codebase Context
73336
73566
 
73337
73567
  ${codebaseContext}${monorepoHint}
@@ -73348,9 +73578,8 @@ For each story, set "contextFiles" to the key source files the agent should read
73348
73578
 
73349
73579
  ${COMPLEXITY_GUIDE}
73350
73580
 
73351
- ${TEST_STRATEGY_GUIDE}
73352
-
73353
- ## Output Schema
73581
+ ${TEST_STRATEGY_GUIDE}`;
73582
+ const outputFormat = `## Output Schema
73354
73583
 
73355
73584
  Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
73356
73585
 
@@ -73386,6 +73615,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
73386
73615
 
73387
73616
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
73388
73617
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.` : "Output ONLY the JSON object. Do not wrap in markdown code blocks."}`;
73618
+ return { taskContext, outputFormat };
73389
73619
  }
73390
73620
  async function planDecomposeCommand(workdir, config2, options) {
73391
73621
  const prdPath = join11(workdir, ".nax", "features", options.feature, "prd.json");