@nathapp/nax 0.59.1 → 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 +542 -302
  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;
@@ -21475,7 +21515,10 @@ function majorityResolver(proposals, failOpen) {
21475
21515
  return passCount > failCount ? "passed" : "failed";
21476
21516
  }
21477
21517
  async function synthesisResolver(proposals, critiques, opts) {
21478
- const prompt = buildSynthesisPrompt(proposals, critiques);
21518
+ const base = buildSynthesisPrompt(proposals, critiques);
21519
+ const prompt = opts.promptSuffix ? `${base}
21520
+
21521
+ ${opts.promptSuffix}` : base;
21479
21522
  return opts.adapter.complete(prompt, opts.completeOptions);
21480
21523
  }
21481
21524
  async function judgeResolver(proposals, critiques, resolverConfig, opts) {
@@ -21488,7 +21531,6 @@ async function judgeResolver(proposals, critiques, resolverConfig, opts) {
21488
21531
  return adapter.complete(prompt, opts.completeOptions);
21489
21532
  }
21490
21533
  var DEFAULT_FALLBACK_AGENT = "claude";
21491
- var init_resolvers = () => {};
21492
21534
 
21493
21535
  // src/debate/session-helpers.ts
21494
21536
  function resolveDebaterModel(debater, config2) {
@@ -21564,7 +21606,7 @@ function resolveModelDefForDebater(debater, tier, config2) {
21564
21606
  return resolveModelForAgent(configModels, debater.agent, "fast", configDefaultAgent);
21565
21607
  }
21566
21608
  }
21567
- async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext) {
21609
+ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext, promptSuffix) {
21568
21610
  const resolverConfig = stageConfig.resolver;
21569
21611
  const logger = _debateSessionDeps.getSafeLogger();
21570
21612
  if (reviewerSession && resolverContext) {
@@ -21578,21 +21620,13 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21578
21620
  let passCount = 0;
21579
21621
  let failCount = 0;
21580
21622
  for (const proposal of proposalOutputs) {
21581
- try {
21582
- const stripped = proposal.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
21583
- const parsed = JSON.parse(stripped);
21584
- if (typeof parsed.passed === "boolean" && parsed.passed)
21585
- passCount++;
21586
- else if (failOpen)
21587
- passCount++;
21588
- else
21589
- failCount++;
21590
- } catch {
21591
- if (failOpen)
21592
- passCount++;
21593
- else
21594
- failCount++;
21595
- }
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++;
21596
21630
  }
21597
21631
  debateCtx.majorityVote = { passed: rawOutcome === "passed", passCount, failCount };
21598
21632
  }
@@ -21633,20 +21667,23 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21633
21667
  resolverCostUsd: 0
21634
21668
  };
21635
21669
  }
21636
- const implementerSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "implementer") : undefined;
21637
21670
  if (resolverConfig.type === "synthesis") {
21638
21671
  const agentName = resolverConfig.agent ?? RESOLVER_FALLBACK_AGENT;
21639
21672
  const adapter = _debateSessionDeps.getAgent(agentName, config2);
21640
21673
  if (adapter) {
21674
+ const synthesisSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "synthesis") : undefined;
21641
21675
  const resolverResult = await synthesisResolver(proposalOutputs, critiqueOutputs, {
21642
21676
  adapter,
21677
+ promptSuffix,
21643
21678
  completeOptions: {
21644
21679
  model: resolveDebaterModel({ agent: agentName }, config2),
21645
21680
  config: config2,
21646
21681
  storyId,
21682
+ featureName,
21683
+ workdir,
21647
21684
  sessionRole: "synthesis",
21648
21685
  timeoutMs,
21649
- ...implementerSessionName !== undefined && { sessionName: implementerSessionName }
21686
+ ...synthesisSessionName !== undefined && { sessionName: synthesisSessionName }
21650
21687
  }
21651
21688
  });
21652
21689
  return {
@@ -21659,6 +21696,7 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21659
21696
  }
21660
21697
  if (resolverConfig.type === "custom") {
21661
21698
  const agentName = resolverConfig.agent ?? RESOLVER_FALLBACK_AGENT;
21699
+ const judgeSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "judge") : undefined;
21662
21700
  const resolverResult = await judgeResolver(proposalOutputs, critiqueOutputs, resolverConfig, {
21663
21701
  getAgent: (name) => _debateSessionDeps.getAgent(name, config2),
21664
21702
  defaultAgentName: RESOLVER_FALLBACK_AGENT,
@@ -21666,9 +21704,11 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21666
21704
  model: resolveDebaterModel({ agent: agentName }, config2),
21667
21705
  config: config2,
21668
21706
  storyId,
21707
+ featureName,
21708
+ workdir,
21669
21709
  sessionRole: "judge",
21670
21710
  timeoutMs,
21671
- ...implementerSessionName !== undefined && { sessionName: implementerSessionName }
21711
+ ...judgeSessionName !== undefined && { sessionName: judgeSessionName }
21672
21712
  }
21673
21713
  });
21674
21714
  return {
@@ -21685,7 +21725,6 @@ var init_session_helpers = __esm(() => {
21685
21725
  init_registry();
21686
21726
  init_config();
21687
21727
  init_logger2();
21688
- init_resolvers();
21689
21728
  _debateSessionDeps = {
21690
21729
  getAgent: (name, config2) => config2 ? createAgentRegistry(config2).getAgent(name) : getAgent(name),
21691
21730
  getSafeLogger,
@@ -21719,6 +21758,171 @@ async function allSettledBounded(tasks, limit) {
21719
21758
  return results;
21720
21759
  }
21721
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
+
21722
21926
  // src/debate/session-stateful.ts
21723
21927
  async function runStatefulTurn(ctx, adapter, debater, prompt, roleKey, keepSessionOpen) {
21724
21928
  const modelTier = modelTierFromDebater(debater);
@@ -21774,7 +21978,9 @@ async function closeStatefulSession(ctx, adapter, debater, roleKey) {
21774
21978
  async function runStateful(ctx, prompt) {
21775
21979
  const logger = _debateSessionDeps.getSafeLogger();
21776
21980
  const config2 = ctx.stageConfig;
21777
- 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);
21778
21984
  let totalCostUsd = 0;
21779
21985
  const resolved = [];
21780
21986
  for (const debater of debaters) {
@@ -21871,8 +22077,9 @@ async function runStateful(ctx, prompt) {
21871
22077
  }
21872
22078
  let critiqueOutputs = [];
21873
22079
  if (config2.rounds > 1) {
21874
- const proposalOutputs2 = successfulProposals.map((s) => s.output);
21875
- 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);
21876
22083
  for (const r of critiqueSettled) {
21877
22084
  if (r.status === "fulfilled") {
21878
22085
  totalCostUsd += r.value.cost;
@@ -21908,11 +22115,13 @@ async function runStateful(ctx, prompt) {
21908
22115
  };
21909
22116
  }
21910
22117
  var init_session_stateful = __esm(() => {
22118
+ init_personas();
22119
+ init_prompt_builder();
21911
22120
  init_session_helpers();
21912
22121
  });
21913
22122
 
21914
22123
  // src/debate/session-hybrid.ts
21915
- async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix) {
22124
+ async function runRebuttalLoop(ctx, proposals, builder, sessionRolePrefix) {
21916
22125
  const logger = _debateSessionDeps.getSafeLogger();
21917
22126
  const config2 = ctx.stageConfig;
21918
22127
  const rebuttals = [];
@@ -21920,7 +22129,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21920
22129
  const proposalList = proposals.map((s) => ({ debater: s.debater, output: s.output }));
21921
22130
  try {
21922
22131
  for (let round = 1;round <= config2.rounds; round++) {
21923
- const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
22132
+ const priorRebuttals = rebuttals.filter((r) => r.round < round);
21924
22133
  for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21925
22134
  const proposal = proposals[debaterIdx];
21926
22135
  const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
@@ -21929,7 +22138,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21929
22138
  round,
21930
22139
  debaterIndex: debaterIdx
21931
22140
  });
21932
- const rebuttalPrompt = buildRebuttalContext(originalPrompt, proposalList, priorRebuttals, debaterIdx);
22141
+ const rebuttalPrompt = builder.buildRebuttalPrompt(debaterIdx, proposalList, priorRebuttals);
21933
22142
  try {
21934
22143
  const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21935
22144
  costUsd += turnResult.cost;
@@ -21959,7 +22168,9 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21959
22168
  async function runHybrid(ctx, prompt) {
21960
22169
  const logger = _debateSessionDeps.getSafeLogger();
21961
22170
  const config2 = ctx.stageConfig;
21962
- 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);
21963
22174
  let totalCostUsd = 0;
21964
22175
  const resolved = [];
21965
22176
  for (const debater of debaters) {
@@ -22024,7 +22235,8 @@ async function runHybrid(ctx, prompt) {
22024
22235
  }
22025
22236
  const proposalOutputs = successfulProposals.map((s) => s.output);
22026
22237
  const proposalList = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
22027
- 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");
22028
22240
  totalCostUsd += rebuttalCost;
22029
22241
  const critiqueOutputs = rebuttals.map((r) => r.output);
22030
22242
  const fullResolverContext = ctx.resolverContextInput ? {
@@ -22046,6 +22258,8 @@ async function runHybrid(ctx, prompt) {
22046
22258
  };
22047
22259
  }
22048
22260
  var init_session_hybrid = __esm(() => {
22261
+ init_personas();
22262
+ init_prompt_builder();
22049
22263
  init_session_helpers();
22050
22264
  init_session_stateful();
22051
22265
  });
@@ -22054,7 +22268,9 @@ var init_session_hybrid = __esm(() => {
22054
22268
  async function runOneShot(ctx, prompt) {
22055
22269
  const logger = _debateSessionDeps.getSafeLogger();
22056
22270
  const config2 = ctx.stageConfig;
22057
- 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);
22058
22274
  let totalCostUsd = 0;
22059
22275
  const resolved = [];
22060
22276
  for (const debater of debaters) {
@@ -22157,8 +22373,9 @@ async function runOneShot(ctx, prompt) {
22157
22373
  }
22158
22374
  let critiqueOutputs = [];
22159
22375
  if (config2.rounds > 1) {
22160
- const proposalOutputs2 = successful.map((p) => p.output);
22161
- 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), {
22162
22379
  model: resolveDebaterModel(debater, ctx.config),
22163
22380
  featureName: ctx.stage,
22164
22381
  config: ctx.config,
@@ -22201,15 +22418,18 @@ async function runOneShot(ctx, prompt) {
22201
22418
  };
22202
22419
  }
22203
22420
  var init_session_one_shot = __esm(() => {
22421
+ init_personas();
22422
+ init_prompt_builder();
22204
22423
  init_session_helpers();
22205
22424
  });
22206
22425
 
22207
22426
  // src/debate/session-plan.ts
22208
22427
  import { join as join10 } from "path";
22209
- async function runPlan2(ctx, basePrompt, opts) {
22428
+ async function runPlan2(ctx, taskContext, outputFormat, opts) {
22210
22429
  const logger = _debateSessionDeps.getSafeLogger();
22211
22430
  const config2 = ctx.stageConfig;
22212
- const debaters = config2.debaters ?? [];
22431
+ const rawDebaters = config2.debaters ?? [];
22432
+ const debaters = resolvePersonas(rawDebaters, "plan", config2.autoPersona ?? false);
22213
22433
  let totalCostUsd = 0;
22214
22434
  const resolved = [];
22215
22435
  for (const debater of debaters) {
@@ -22227,14 +22447,15 @@ async function runPlan2(ctx, basePrompt, opts) {
22227
22447
  });
22228
22448
  const debate = ctx.config?.debate;
22229
22449
  const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
22230
- 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 () => {
22231
22452
  const tempOutputPath = join10(opts.outputDir, `prd-debate-${i}.json`);
22232
- const debaterPrompt = `${basePrompt}
22453
+ const debaterPrompt = `${proposalBuilder.buildProposalPrompt(i)}
22233
22454
 
22234
22455
  Write the PRD JSON directly to this file path: ${tempOutputPath}
22235
22456
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.`;
22236
- const modelTier = modelTierFromDebater(debater);
22237
- const modelDef = resolveModelDefForDebater(debater, modelTier, ctx.config);
22457
+ const modelTier = modelTierFromDebater(rd);
22458
+ const modelDef = resolveModelDefForDebater(rd, modelTier, ctx.config);
22238
22459
  const planResult = await adapter.plan({
22239
22460
  prompt: debaterPrompt,
22240
22461
  workdir: opts.workdir,
@@ -22250,7 +22471,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22250
22471
  sessionRole: `plan-${i}`
22251
22472
  });
22252
22473
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22253
- return { debater, adapter, output, cost: planResult.costUsd ?? 0 };
22474
+ return { debater: rd, adapter, output, cost: planResult.costUsd ?? 0 };
22254
22475
  }), concurrencyLimit);
22255
22476
  const successful = [];
22256
22477
  for (let i = 0;i < settled.length; i++) {
@@ -22323,7 +22544,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22323
22544
  featureName: opts.feature,
22324
22545
  timeoutSeconds: opts.timeoutSeconds ?? 600
22325
22546
  };
22326
- 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");
22327
22549
  critiqueOutputs = rebuttals.map((r) => r.output);
22328
22550
  rebuttalList = rebuttals;
22329
22551
  totalCostUsd += costUsd;
@@ -22331,7 +22553,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22331
22553
  logger?.warn("debate", "hybrid mode requires sessionMode: stateful for plan \u2014 running as panel");
22332
22554
  }
22333
22555
  const resolverTimeoutMs = (ctx.stageConfig.timeoutSeconds ?? 600) * 1000;
22334
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature);
22556
+ const planSynthesisSuffix = "IMPORTANT: Your response must be a single valid JSON object in PRD format (with project, feature, branchName, userStories array, etc.). Do NOT wrap it in markdown fences. Output raw JSON only.";
22557
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature, undefined, undefined, planSynthesisSuffix);
22335
22558
  const winningOutput = outcome.output ?? successful[0].output;
22336
22559
  const proposals = successful.map((p) => ({ debater: p.debater, output: p.output }));
22337
22560
  logger?.info("debate", "debate:result", {
@@ -22353,6 +22576,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22353
22576
  };
22354
22577
  }
22355
22578
  var init_session_plan = __esm(() => {
22579
+ init_personas();
22580
+ init_prompt_builder();
22356
22581
  init_session_helpers();
22357
22582
  init_session_hybrid();
22358
22583
  });
@@ -22438,13 +22663,13 @@ class DebateSession {
22438
22663
  resolverContextInput: this.resolverContextInput
22439
22664
  }, prompt);
22440
22665
  }
22441
- async runPlan(basePrompt, opts) {
22666
+ async runPlan(taskContext, outputFormat, opts) {
22442
22667
  return runPlan2({
22443
22668
  storyId: this.storyId,
22444
22669
  stage: this.stage,
22445
22670
  stageConfig: this.stageConfig,
22446
22671
  config: this.config
22447
- }, basePrompt, opts);
22672
+ }, taskContext, outputFormat, opts);
22448
22673
  }
22449
22674
  }
22450
22675
  var DEFAULT_TIMEOUT_SECONDS = 600;
@@ -22461,8 +22686,8 @@ var init_session = __esm(() => {
22461
22686
  var init_debate = __esm(() => {
22462
22687
  init_session();
22463
22688
  init_session_helpers();
22464
- init_resolvers();
22465
- init_session_helpers();
22689
+ init_prompt_builder();
22690
+ init_personas();
22466
22691
  });
22467
22692
 
22468
22693
  // src/interaction/bridge-builder.ts
@@ -23643,50 +23868,6 @@ var init_init = __esm(() => {
23643
23868
  init_webhook();
23644
23869
  });
23645
23870
 
23646
- // src/utils/llm-json.ts
23647
- function extractJsonFromMarkdown(text) {
23648
- const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
23649
- if (match) {
23650
- return match[1] ?? text;
23651
- }
23652
- return text;
23653
- }
23654
- function stripTrailingCommas(text) {
23655
- return text.replace(/,\s*([}\]])/g, "$1");
23656
- }
23657
- function extractJsonObject(text) {
23658
- const objStart = text.indexOf("{");
23659
- const arrStart = text.indexOf("[");
23660
- let start;
23661
- let closeChar;
23662
- if (objStart === -1 && arrStart === -1)
23663
- return null;
23664
- if (objStart === -1) {
23665
- start = arrStart;
23666
- closeChar = "]";
23667
- } else if (arrStart === -1) {
23668
- start = objStart;
23669
- closeChar = "}";
23670
- } else if (objStart < arrStart) {
23671
- start = objStart;
23672
- closeChar = "}";
23673
- } else {
23674
- start = arrStart;
23675
- closeChar = "]";
23676
- }
23677
- const end = text.lastIndexOf(closeChar);
23678
- if (end <= start)
23679
- return null;
23680
- return text.slice(start, end + 1);
23681
- }
23682
- function wrapJsonPrompt(prompt) {
23683
- 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.
23684
-
23685
- ${prompt.trim()}
23686
-
23687
- YOUR RESPONSE MUST START WITH { OR [ AND END WITH } OR ]. No other text.`;
23688
- }
23689
-
23690
23871
  // src/prd/validate.ts
23691
23872
  function validateStoryId(id) {
23692
23873
  if (!id || id.length === 0) {
@@ -25792,7 +25973,7 @@ Rules:
25792
25973
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
25793
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.
25794
25975
  - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
25795
- - **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')\`).`;
25796
25977
  }
25797
25978
  async function generateAcceptanceTests(adapter, options) {
25798
25979
  const logger = getLogger();
@@ -26950,6 +27131,7 @@ function buildReviewPrompt(diff, story, _semanticConfig) {
26950
27131
  "## Diff",
26951
27132
  diff,
26952
27133
  "",
27134
+ "Also flag any changes in the diff not required by the acceptance criteria above as out-of-scope findings.",
26953
27135
  "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
26954
27136
  ].join(`
26955
27137
  `);
@@ -26971,7 +27153,7 @@ function buildReReviewPrompt(updatedDiff, previousFindings) {
26971
27153
  ].join(`
26972
27154
  `);
26973
27155
  }
26974
- function buildProposalsSection(proposals) {
27156
+ function buildProposalsSection2(proposals) {
26975
27157
  return proposals.map((p) => `### ${p.debater}
26976
27158
  ${p.output}`).join(`
26977
27159
 
@@ -27015,7 +27197,7 @@ function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticC
27015
27197
  `);
27016
27198
  const framing = buildResolverFraming(resolverContext);
27017
27199
  const voteTally = buildVoteTallyLine(resolverContext);
27018
- const proposalsSection = buildProposalsSection(proposals);
27200
+ const proposalsSection = buildProposalsSection2(proposals);
27019
27201
  const critiquesSection = buildCritiquesSection(critiques);
27020
27202
  return [
27021
27203
  framing,
@@ -27041,7 +27223,7 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27041
27223
  const framing = buildResolverFraming(resolverContext);
27042
27224
  const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
27043
27225
  `) : "(none)";
27044
- const proposalsSection = buildProposalsSection(proposals);
27226
+ const proposalsSection = buildProposalsSection2(proposals);
27045
27227
  const critiquesSection = buildCritiquesSection(critiques);
27046
27228
  return [
27047
27229
  `${framing} This is a re-review after implementer changes.`,
@@ -27064,12 +27246,10 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27064
27246
 
27065
27247
  // src/review/dialogue.ts
27066
27248
  function extractDeltaSummary(rawOutput, previousFindings, newFindings) {
27067
- try {
27068
- const parsed = JSON.parse(rawOutput);
27069
- if (typeof parsed.deltaSummary === "string" && parsed.deltaSummary.length > 0) {
27070
- return parsed.deltaSummary;
27071
- }
27072
- } catch {}
27249
+ const parsed = tryParseLLMJson(rawOutput);
27250
+ if (parsed && typeof parsed.deltaSummary === "string" && parsed.deltaSummary.length > 0) {
27251
+ return parsed.deltaSummary;
27252
+ }
27073
27253
  const newIds = new Set(newFindings.map((f) => f.ruleId));
27074
27254
  const prevIds = new Set(previousFindings.map((f) => f.ruleId));
27075
27255
  const resolved = previousFindings.filter((f) => !newIds.has(f.ruleId));
@@ -27109,7 +27289,7 @@ function compactHistory(history) {
27109
27289
  function parseReviewResponse(output) {
27110
27290
  let parsed;
27111
27291
  try {
27112
- parsed = JSON.parse(output);
27292
+ parsed = parseLLMJson(output);
27113
27293
  } catch {
27114
27294
  throw new NaxError("[dialogue] Failed to parse reviewer JSON response", "REVIEWER_PARSE_FAILED", {
27115
27295
  stage: "review",
@@ -27688,23 +27868,11 @@ function validateLLMShape(parsed) {
27688
27868
  return { passed: obj.passed, findings: obj.findings };
27689
27869
  }
27690
27870
  function parseLLMResponse(raw) {
27691
- const text = raw.trim();
27692
27871
  try {
27693
- return validateLLMShape(JSON.parse(text));
27694
- } catch {}
27695
- const fromFence = extractJsonFromMarkdown(text);
27696
- if (fromFence !== text) {
27697
- try {
27698
- return validateLLMShape(JSON.parse(stripTrailingCommas(fromFence)));
27699
- } catch {}
27700
- }
27701
- const bareJson = extractJsonObject(text);
27702
- if (bareJson) {
27703
- try {
27704
- return validateLLMShape(JSON.parse(stripTrailingCommas(bareJson)));
27705
- } catch {}
27872
+ return validateLLMShape(tryParseLLMJson(raw));
27873
+ } catch {
27874
+ return null;
27706
27875
  }
27707
- return null;
27708
27876
  }
27709
27877
  function formatFindings(findings) {
27710
27878
  return findings.map((f) => `[${f.severity}] ${f.file}:${f.line} \u2014 ${f.issue}
@@ -28679,6 +28847,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28679
28847
  config: ctx.config,
28680
28848
  projectDir: ctx.projectDir,
28681
28849
  maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
28850
+ featureName: ctx.prd.feature,
28682
28851
  storyId: ctx.story.id,
28683
28852
  sessionRole: "implementer"
28684
28853
  });
@@ -30456,61 +30625,34 @@ var init_executor = __esm(() => {
30456
30625
  });
30457
30626
 
30458
30627
  // src/verification/parser.ts
30459
- function parseBunTestOutput(output) {
30460
- if (isJestLikeOutput(output)) {
30461
- return parseJestOutput(output);
30462
- }
30463
- return parseBunOutput(output);
30464
- }
30465
- function isJestLikeOutput(output) {
30466
- 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";
30467
30640
  }
30468
- function parseJestOutput(output) {
30469
- const failures = [];
30470
- let passed = 0;
30471
- let failed = 0;
30472
- const summaryMatches = Array.from(output.matchAll(/^\s*Tests:\s+(.*)/gm));
30473
- if (summaryMatches.length > 0) {
30474
- const summaryLine = summaryMatches[summaryMatches.length - 1][1];
30475
- const failedMatch = summaryLine.match(/(\d+)\s+failed/);
30476
- const passedMatch = summaryLine.match(/(\d+)\s+passed/);
30477
- if (failedMatch)
30478
- failed = Number.parseInt(failedMatch[1], 10);
30479
- if (passedMatch)
30480
- passed = Number.parseInt(passedMatch[1], 10);
30481
- }
30482
- let currentFile = "unknown";
30483
- const lines = output.split(`
30484
- `);
30485
- for (let i = 0;i < lines.length; i++) {
30486
- const line = lines[i];
30487
- const fileMatch = line.match(/^\s*(?:FAIL|PASS)\s+(\S+\.[jt]sx?)/);
30488
- if (fileMatch) {
30489
- currentFile = fileMatch[1];
30490
- continue;
30491
- }
30492
- const bulletMatch = line.match(/^\s+\u25CF\s+(.+)$/);
30493
- if (bulletMatch) {
30494
- const testName = bulletMatch[1].trim();
30495
- let error48 = "";
30496
- for (let j = i + 1;j < lines.length && j < i + 10; j++) {
30497
- const next = lines[j].trim();
30498
- if (!next)
30499
- continue;
30500
- if (next.startsWith("\u25CF") || /^(?:FAIL|PASS)\s/.test(next))
30501
- break;
30502
- error48 = next;
30503
- break;
30504
- }
30505
- failures.push({
30506
- file: currentFile,
30507
- testName,
30508
- error: error48 || "Unknown error",
30509
- stackTrace: []
30510
- });
30511
- }
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);
30512
30655
  }
30513
- return { passed, failed, failures };
30514
30656
  }
30515
30657
  function parseBunOutput(output) {
30516
30658
  const lines = output.split(`
@@ -30572,6 +30714,122 @@ function parseBunOutput(output) {
30572
30714
  }
30573
30715
  return { passed, failed, failures };
30574
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
+ }
30575
30833
  function formatFailureSummary(failures, maxChars = 2000) {
30576
30834
  if (failures.length === 0) {
30577
30835
  return "No test failures";
@@ -30585,48 +30843,24 @@ function formatFailureSummary(failures, maxChars = 2000) {
30585
30843
  const errorLine = ` Error: ${failure.error}`;
30586
30844
  const stackLine = failure.stackTrace.length > 0 ? ` ${failure.stackTrace[0]}` : "";
30587
30845
  const blockLines = [header, errorLine];
30588
- if (stackLine) {
30846
+ if (stackLine)
30589
30847
  blockLines.push(stackLine);
30590
- }
30591
30848
  blockLines.push("");
30592
30849
  const block = blockLines.join(`
30593
30850
  `);
30594
- const blockLength = block.length;
30595
- if (totalChars + blockLength > maxChars && lines.length > 0) {
30596
- const remaining = failures.length - i;
30851
+ if (totalChars + block.length > maxChars && lines.length > 0) {
30597
30852
  lines.push(`
30598
- ... and ${remaining} more failure(s) (truncated)`);
30853
+ ... and ${failures.length - i} more failure(s) (truncated)`);
30599
30854
  break;
30600
30855
  }
30601
30856
  lines.push(...blockLines);
30602
- totalChars += blockLength;
30857
+ totalChars += block.length;
30603
30858
  }
30604
30859
  return lines.join(`
30605
30860
  `).trim();
30606
30861
  }
30607
- function parseTestOutput(output, exitCode) {
30608
- const patterns = [
30609
- /(\d+)\s+pass(?:ed)?(?:,\s+|\s+)(\d+)\s+fail/i,
30610
- /Tests:\s+(\d+)\s+passed,\s+(\d+)\s+failed/i,
30611
- /(\d+)\s+pass/i
30612
- ];
30613
- let passCount = 0;
30614
- let failCount = 0;
30615
- for (const pattern of patterns) {
30616
- const matches = Array.from(output.matchAll(new RegExp(pattern, "gi")));
30617
- if (matches.length > 0) {
30618
- const lastMatch = matches[matches.length - 1];
30619
- passCount = Number.parseInt(lastMatch[1], 10);
30620
- failCount = lastMatch[2] ? Number.parseInt(lastMatch[2], 10) : 0;
30621
- break;
30622
- }
30623
- }
30624
- if (failCount === 0) {
30625
- const failMatches = Array.from(output.matchAll(/(\d+)\s+fail/gi));
30626
- if (failMatches.length > 0) {
30627
- failCount = Number.parseInt(failMatches[failMatches.length - 1][1], 10);
30628
- }
30629
- }
30862
+ function analyzeTestExitCode(output, exitCode) {
30863
+ const { passed: passCount, failed: failCount } = parseCommonOutput(output);
30630
30864
  const allTestsPassed = passCount > 0 && failCount === 0;
30631
30865
  const isEnvironmentalFailure = allTestsPassed && exitCode !== 0;
30632
30866
  const result = {
@@ -30697,7 +30931,7 @@ async function runVerificationCore(options) {
30697
30931
  }
30698
30932
  const exitCode = execution.exitCode ?? 1;
30699
30933
  if (exitCode !== 0 && execution.output) {
30700
- const analysis = parseTestOutput(execution.output, exitCode);
30934
+ const analysis = analyzeTestExitCode(execution.output, exitCode);
30701
30935
  if (analysis.isEnvironmentalFailure) {
30702
30936
  return {
30703
30937
  status: "ENVIRONMENTAL_FAILURE",
@@ -30990,7 +31224,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
30990
31224
  });
30991
31225
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
30992
31226
  if (!fullSuitePassed && fullSuiteResult.output) {
30993
- const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
31227
+ const testSummary = _rectificationGateDeps.parseTestOutput(fullSuiteResult.output);
30994
31228
  if (testSummary.failed > 0) {
30995
31229
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName, projectDir);
30996
31230
  }
@@ -31124,7 +31358,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
31124
31358
  return true;
31125
31359
  }
31126
31360
  if (retryFullSuite.output) {
31127
- const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
31361
+ const newTestSummary = _rectificationGateDeps.parseTestOutput(retryFullSuite.output);
31128
31362
  state.currentFailures = newTestSummary.failed;
31129
31363
  testSummary.failures = newTestSummary.failures;
31130
31364
  testSummary.failed = newTestSummary.failed;
@@ -31172,7 +31406,7 @@ var init_rectification_gate = __esm(() => {
31172
31406
  init_prompts();
31173
31407
  _rectificationGateDeps = {
31174
31408
  executeWithTimeout,
31175
- parseBunTestOutput,
31409
+ parseTestOutput,
31176
31410
  shouldRetryRectification
31177
31411
  };
31178
31412
  });
@@ -33142,9 +33376,6 @@ function calculateMaxIterations(tierOrder) {
33142
33376
  return tierOrder.reduce((sum, t) => sum + t.attempts, 0);
33143
33377
  }
33144
33378
 
33145
- // src/execution/test-output-parser.ts
33146
- var init_test_output_parser = () => {};
33147
-
33148
33379
  // src/verification/rectification-loop.ts
33149
33380
  async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
33150
33381
  const logger = getSafeLogger();
@@ -33197,7 +33428,7 @@ async function runRectificationLoop2(opts) {
33197
33428
  } = opts;
33198
33429
  const logger = getSafeLogger();
33199
33430
  const rectificationConfig = config2.execution.rectification;
33200
- const testSummary = parseBunTestOutput(testOutput);
33431
+ const testSummary = parseTestOutput(testOutput);
33201
33432
  const label = promptPrefix ? "regression rectification" : "rectification";
33202
33433
  const rectificationState = {
33203
33434
  attempt: 0,
@@ -33332,13 +33563,13 @@ ${rectificationPrompt}`;
33332
33563
  return true;
33333
33564
  }
33334
33565
  if (retryVerification.output) {
33335
- const newTestSummary = parseBunTestOutput(retryVerification.output);
33566
+ const newTestSummary = parseTestOutput(retryVerification.output);
33336
33567
  state.currentFailures = newTestSummary.failed;
33337
33568
  state.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
33338
33569
  testSummary.failures = newTestSummary.failures;
33339
33570
  testSummary.failed = newTestSummary.failed;
33340
33571
  testSummary.passed = newTestSummary.passed;
33341
- if (newTestSummary.failed === 0) {
33572
+ if (newTestSummary.failed === 0 && (retryVerification.status === "SUCCESS" || newTestSummary.passed > 0)) {
33342
33573
  state.lastExitCode = 0;
33343
33574
  logger?.info("rectification", `[OK] ${label} succeeded after parsing retry output`, {
33344
33575
  storyId: story.id,
@@ -33478,7 +33709,6 @@ var init_rectification_loop = __esm(() => {
33478
33709
  init_cost();
33479
33710
  init_registry();
33480
33711
  init_config();
33481
- init_test_output_parser();
33482
33712
  init_logger2();
33483
33713
  init_prd();
33484
33714
  init_rectification();
@@ -33744,7 +33974,7 @@ class RegressionStrategy {
33744
33974
  });
33745
33975
  const durationMs = Date.now() - start;
33746
33976
  if (result.success) {
33747
- 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: [] };
33748
33978
  return makePassResult(ctx.storyId, "regression", {
33749
33979
  rawOutput: result.output,
33750
33980
  passCount: parsed2.passed,
@@ -33760,7 +33990,7 @@ class RegressionStrategy {
33760
33990
  if (result.status === "TIMEOUT") {
33761
33991
  return makeFailResult(ctx.storyId, "regression", "TIMEOUT", { rawOutput: result.output, durationMs });
33762
33992
  }
33763
- 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: [] };
33764
33994
  return makeFailResult(ctx.storyId, "regression", "TEST_FAILURE", {
33765
33995
  rawOutput: result.output,
33766
33996
  passCount: parsed.passed,
@@ -33984,7 +34214,7 @@ class ScopedStrategy {
33984
34214
  });
33985
34215
  const durationMs = Date.now() - start;
33986
34216
  if (result.success) {
33987
- 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: [] };
33988
34218
  return makePassResult(ctx.storyId, "scoped", {
33989
34219
  rawOutput: result.output,
33990
34220
  passCount: parsed2.passed,
@@ -34000,7 +34230,7 @@ class ScopedStrategy {
34000
34230
  scopeTestFallback
34001
34231
  });
34002
34232
  }
34003
- 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: [] };
34004
34234
  return makeFailResult(ctx.storyId, "scoped", "TEST_FAILURE", {
34005
34235
  rawOutput: result.output,
34006
34236
  passCount: parsed.passed,
@@ -34334,12 +34564,11 @@ function stripCodeFences(text) {
34334
34564
  return trimmed;
34335
34565
  }
34336
34566
  function parseRoutingResponse(output, story, config2) {
34337
- const jsonText = extractJsonFromMarkdown(output.trim());
34338
- const parsed = JSON.parse(jsonText);
34567
+ const parsed = parseLLMJson(output);
34339
34568
  return validateRoutingDecision(parsed, config2, story);
34340
34569
  }
34341
34570
  function parseBatchResponse(output, stories, config2) {
34342
- const parsed = JSON.parse(extractJsonFromMarkdown(output.trim()));
34571
+ const parsed = parseLLMJson(output);
34343
34572
  if (!Array.isArray(parsed)) {
34344
34573
  throw new Error("Batch LLM response must be a JSON array");
34345
34574
  }
@@ -36133,7 +36362,7 @@ var package_default;
36133
36362
  var init_package = __esm(() => {
36134
36363
  package_default = {
36135
36364
  name: "@nathapp/nax",
36136
- version: "0.59.1",
36365
+ version: "0.59.3",
36137
36366
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
36138
36367
  type: "module",
36139
36368
  bin: {
@@ -36213,8 +36442,8 @@ var init_version = __esm(() => {
36213
36442
  NAX_VERSION = package_default.version;
36214
36443
  NAX_COMMIT = (() => {
36215
36444
  try {
36216
- if (/^[0-9a-f]{6,10}$/.test("b8492d03"))
36217
- return "b8492d03";
36445
+ if (/^[0-9a-f]{6,10}$/.test("0c763972"))
36446
+ return "0c763972";
36218
36447
  } catch {}
36219
36448
  try {
36220
36449
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -36877,28 +37106,17 @@ function parseDiagnosisResult(output) {
36877
37106
  if (!output || output.trim() === "") {
36878
37107
  return null;
36879
37108
  }
36880
- try {
36881
- const cleaned = output.trim();
36882
- let jsonStr = cleaned;
36883
- const firstBrace = cleaned.indexOf("{");
36884
- const lastBrace = cleaned.lastIndexOf("}");
36885
- if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
36886
- jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
36887
- }
36888
- const parsed = JSON.parse(jsonStr);
36889
- if (typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
36890
- return {
36891
- verdict: parsed.verdict,
36892
- reasoning: parsed.reasoning,
36893
- confidence: parsed.confidence,
36894
- testIssues: parsed.testIssues,
36895
- sourceIssues: parsed.sourceIssues
36896
- };
36897
- }
36898
- return null;
36899
- } catch {
36900
- 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
+ };
36901
37118
  }
37119
+ return null;
36902
37120
  }
36903
37121
  var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
36904
37122
  var init_fix_diagnosis = __esm(() => {
@@ -37096,6 +37314,12 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
37096
37314
  logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
37097
37315
  const { unlink: unlink3 } = await import("fs/promises");
37098
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
+ }
37099
37323
  let implementationContext;
37100
37324
  const storyGitRef = acceptanceContext.storyGitRef;
37101
37325
  const workdir = acceptanceContext.workdir;
@@ -37736,7 +37960,7 @@ async function runDeferredRegression(options) {
37736
37960
  affectedStories: []
37737
37961
  };
37738
37962
  }
37739
- const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
37963
+ const testSummary = _regressionDeps.parseTestOutput(fullSuiteResult.output);
37740
37964
  if (testSummary.failed === 0 && testSummary.passed === 0) {
37741
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) });
37742
37966
  return {
@@ -37882,7 +38106,7 @@ var init_run_regression = __esm(() => {
37882
38106
  _regressionDeps = {
37883
38107
  runVerification: fullSuite,
37884
38108
  runRectificationLoop: runRectificationLoop2,
37885
- parseBunTestOutput,
38109
+ parseTestOutput,
37886
38110
  reverseMapTestToSource
37887
38111
  };
37888
38112
  });
@@ -38863,6 +39087,11 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
38863
39087
  const diffSummary = await captureDiffSummary(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
38864
39088
  if (diffSummary) {
38865
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
+ });
38866
39095
  }
38867
39096
  } catch {}
38868
39097
  }
@@ -72799,13 +73028,16 @@ function validateStory(raw, index, allIds) {
72799
73028
  throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
72800
73029
  }
72801
73030
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
72802
- const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
73031
+ let testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
72803
73032
  const rawJustification = routing.noTestJustification ?? s.noTestJustification;
72804
73033
  if (testStrategy === "no-test") {
72805
73034
  if (!rawJustification || typeof rawJustification !== "string" || rawJustification.trim() === "") {
72806
73035
  throw new Error(`[schema] story[${index}].routing.noTestJustification is required when testStrategy is "no-test"`);
72807
73036
  }
72808
73037
  }
73038
+ if (testStrategy !== "no-test" && typeof rawJustification === "string" && rawJustification.trim() !== "") {
73039
+ testStrategy = "no-test";
73040
+ }
72809
73041
  const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
72810
73042
  const rawDeps = s.dependencies;
72811
73043
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
@@ -72965,7 +73197,7 @@ async function planCommand(workdir, config2, options) {
72965
73197
  let rawResponse;
72966
73198
  const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
72967
73199
  if (debateEnabled) {
72968
- const basePrompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
73200
+ const { taskContext: planTaskContext, outputFormat: planOutputFormat } = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
72969
73201
  const resolvedPerm = resolvePermissions(config2, "plan");
72970
73202
  const planStageConfig = config2?.debate?.stages.plan;
72971
73203
  const debateSession = _planDeps.createDebateSession({
@@ -72982,7 +73214,7 @@ async function planCommand(workdir, config2, options) {
72982
73214
  rounds: planStageConfig.rounds,
72983
73215
  feature: options.feature
72984
73216
  });
72985
- const debateResult = await debateSession.runPlan(basePrompt, {
73217
+ const debateResult = await debateSession.runPlan(planTaskContext, planOutputFormat, {
72986
73218
  workdir,
72987
73219
  feature: options.feature,
72988
73220
  outputDir,
@@ -73001,7 +73233,10 @@ async function planCommand(workdir, config2, options) {
73001
73233
  }
73002
73234
  } else if (options.auto) {
73003
73235
  const isAcp = config2?.agent?.protocol === "acp";
73004
- 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}`;
73005
73240
  const adapter = _planDeps.getAgent(agentName, config2);
73006
73241
  if (!adapter)
73007
73242
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73087,7 +73322,10 @@ async function planCommand(workdir, config2, options) {
73087
73322
  rawResponse = await runInteractivePlan();
73088
73323
  }
73089
73324
  async function runInteractivePlan() {
73090
- 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}`;
73091
73329
  const adapter = _planDeps.getAgent(agentName, config2);
73092
73330
  if (!adapter)
73093
73331
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73294,7 +73532,7 @@ ${packageDetailsSection}
73294
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".` : "";
73295
73533
  const workdirField = isMonorepo ? `
73296
73534
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
73297
- 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.
73298
73536
 
73299
73537
  ## Step 1: Understand the Spec
73300
73538
 
@@ -73322,6 +73560,8 @@ If this is a greenfield project (empty or minimal codebase):
73322
73560
 
73323
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.
73324
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
+
73325
73565
  ## Codebase Context
73326
73566
 
73327
73567
  ${codebaseContext}${monorepoHint}
@@ -73338,9 +73578,8 @@ For each story, set "contextFiles" to the key source files the agent should read
73338
73578
 
73339
73579
  ${COMPLEXITY_GUIDE}
73340
73580
 
73341
- ${TEST_STRATEGY_GUIDE}
73342
-
73343
- ## Output Schema
73581
+ ${TEST_STRATEGY_GUIDE}`;
73582
+ const outputFormat = `## Output Schema
73344
73583
 
73345
73584
  Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
73346
73585
 
@@ -73376,6 +73615,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
73376
73615
 
73377
73616
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
73378
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 };
73379
73619
  }
73380
73620
  async function planDecomposeCommand(workdir, config2, options) {
73381
73621
  const prdPath = join11(workdir, ".nax", "features", options.feature, "prd.json");