@nathapp/nax 0.59.2 → 0.60.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +555 -311
  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,91 @@ 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.`;
21418
+ }
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");
21384
21437
  }
21385
- function buildSynthesisPrompt(proposals, critiques) {
21386
- const proposalsSection = proposals.map((p, i) => `### Proposal ${i + 1}
21387
- ${p}`).join(`
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 buildDebaterLabel(debater) {
21448
+ return debater.persona ? `${debater.agent} (${debater.persona})` : debater.agent;
21449
+ }
21450
+ function buildProposalsSection(proposals, debaters) {
21451
+ return proposals.map((p, i) => {
21452
+ const label = debaters?.[i] ? buildDebaterLabel(debaters[i]) : String(i + 1);
21453
+ return `### Proposal ${label}
21454
+ ${p}`;
21455
+ }).join(`
21388
21456
 
21389
21457
  `);
21458
+ }
21459
+ function buildSynthesisPrompt(proposals, critiques, debaters) {
21460
+ const proposalsSection = buildProposalsSection(proposals, debaters);
21390
21461
  const critiquesSection = critiques.length > 0 ? `
21391
21462
 
21392
21463
  ## Critiques
@@ -21401,11 +21472,8 @@ ${proposalsSection}${critiquesSection}
21401
21472
 
21402
21473
  Please synthesize these into the best possible unified response, incorporating the strongest elements from each proposal.`;
21403
21474
  }
21404
- function buildJudgePrompt(proposals, critiques) {
21405
- const proposalsSection = proposals.map((p, i) => `### Proposal ${i + 1}
21406
- ${p}`).join(`
21407
-
21408
- `);
21475
+ function buildJudgePrompt(proposals, critiques, debaters) {
21476
+ const proposalsSection = buildProposalsSection(proposals, debaters);
21409
21477
  const critiquesSection = critiques.length > 0 ? `
21410
21478
 
21411
21479
  ## Critiques
@@ -21420,28 +21488,6 @@ ${proposalsSection}${critiquesSection}
21420
21488
 
21421
21489
  As the judge, provide your final verdict with clear reasoning, selecting or synthesizing the best approach.`;
21422
21490
  }
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
21491
  function stripMarkdownFence(text) {
21446
21492
  const match = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/);
21447
21493
  return match ? match[1] ?? text : text;
@@ -21475,7 +21521,7 @@ function majorityResolver(proposals, failOpen) {
21475
21521
  return passCount > failCount ? "passed" : "failed";
21476
21522
  }
21477
21523
  async function synthesisResolver(proposals, critiques, opts) {
21478
- const base = buildSynthesisPrompt(proposals, critiques);
21524
+ const base = buildSynthesisPrompt(proposals, critiques, opts.debaters);
21479
21525
  const prompt = opts.promptSuffix ? `${base}
21480
21526
 
21481
21527
  ${opts.promptSuffix}` : base;
@@ -21487,11 +21533,10 @@ async function judgeResolver(proposals, critiques, resolverConfig, opts) {
21487
21533
  if (!adapter) {
21488
21534
  throw new Error(`[debate] Judge agent '${agentName}' not found`);
21489
21535
  }
21490
- const prompt = buildJudgePrompt(proposals, critiques);
21536
+ const prompt = buildJudgePrompt(proposals, critiques, opts.debaters);
21491
21537
  return adapter.complete(prompt, opts.completeOptions);
21492
21538
  }
21493
21539
  var DEFAULT_FALLBACK_AGENT = "claude";
21494
- var init_resolvers = () => {};
21495
21540
 
21496
21541
  // src/debate/session-helpers.ts
21497
21542
  function resolveDebaterModel(debater, config2) {
@@ -21567,7 +21612,7 @@ function resolveModelDefForDebater(debater, tier, config2) {
21567
21612
  return resolveModelForAgent(configModels, debater.agent, "fast", configDefaultAgent);
21568
21613
  }
21569
21614
  }
21570
- async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext, promptSuffix) {
21615
+ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext, promptSuffix, debaters) {
21571
21616
  const resolverConfig = stageConfig.resolver;
21572
21617
  const logger = _debateSessionDeps.getSafeLogger();
21573
21618
  if (reviewerSession && resolverContext) {
@@ -21581,21 +21626,13 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21581
21626
  let passCount = 0;
21582
21627
  let failCount = 0;
21583
21628
  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
- }
21629
+ const parsed = tryParseLLMJson(proposal);
21630
+ if (parsed !== null && typeof parsed.passed === "boolean" && parsed.passed)
21631
+ passCount++;
21632
+ else if (failOpen)
21633
+ passCount++;
21634
+ else
21635
+ failCount++;
21599
21636
  }
21600
21637
  debateCtx.majorityVote = { passed: rawOutcome === "passed", passCount, failCount };
21601
21638
  }
@@ -21644,6 +21681,7 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21644
21681
  const resolverResult = await synthesisResolver(proposalOutputs, critiqueOutputs, {
21645
21682
  adapter,
21646
21683
  promptSuffix,
21684
+ debaters,
21647
21685
  completeOptions: {
21648
21686
  model: resolveDebaterModel({ agent: agentName }, config2),
21649
21687
  config: config2,
@@ -21669,6 +21707,7 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21669
21707
  const resolverResult = await judgeResolver(proposalOutputs, critiqueOutputs, resolverConfig, {
21670
21708
  getAgent: (name) => _debateSessionDeps.getAgent(name, config2),
21671
21709
  defaultAgentName: RESOLVER_FALLBACK_AGENT,
21710
+ debaters,
21672
21711
  completeOptions: {
21673
21712
  model: resolveDebaterModel({ agent: agentName }, config2),
21674
21713
  config: config2,
@@ -21694,7 +21733,6 @@ var init_session_helpers = __esm(() => {
21694
21733
  init_registry();
21695
21734
  init_config();
21696
21735
  init_logger2();
21697
- init_resolvers();
21698
21736
  _debateSessionDeps = {
21699
21737
  getAgent: (name, config2) => config2 ? createAgentRegistry(config2).getAgent(name) : getAgent(name),
21700
21738
  getSafeLogger,
@@ -21728,6 +21766,174 @@ async function allSettledBounded(tasks, limit) {
21728
21766
  return results;
21729
21767
  }
21730
21768
 
21769
+ // src/debate/personas.ts
21770
+ function resolvePersonas(debaters, stage, autoPersona) {
21771
+ if (!autoPersona)
21772
+ return debaters;
21773
+ const rotation = stage === "plan" ? PLAN_ROTATION : REVIEW_ROTATION;
21774
+ let rotationIndex = 0;
21775
+ return debaters.map((d) => {
21776
+ if (d.persona)
21777
+ return d;
21778
+ const assigned = rotation[rotationIndex % rotation.length];
21779
+ rotationIndex++;
21780
+ return { ...d, persona: assigned };
21781
+ });
21782
+ }
21783
+ function buildDebaterLabel2(debater) {
21784
+ return debater.persona ? `${debater.agent} (${debater.persona})` : debater.agent;
21785
+ }
21786
+ var PERSONA_FRAGMENTS, PLAN_ROTATION, REVIEW_ROTATION;
21787
+ var init_personas = __esm(() => {
21788
+ PERSONA_FRAGMENTS = {
21789
+ challenger: {
21790
+ identity: "You are the challenger \u2014 your job is to stress-test proposals and find weaknesses.",
21791
+ 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."
21792
+ },
21793
+ pragmatist: {
21794
+ identity: "You are the pragmatist \u2014 your job is to find the simplest path that satisfies the spec.",
21795
+ 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."
21796
+ },
21797
+ completionist: {
21798
+ identity: "You are the completionist \u2014 your job is to ensure nothing is missed.",
21799
+ 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."
21800
+ },
21801
+ security: {
21802
+ identity: "You are the security reviewer \u2014 your job is to surface risks before they ship.",
21803
+ 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."
21804
+ },
21805
+ testability: {
21806
+ identity: "You are the testability advocate \u2014 your job is to ensure the design is verifiable.",
21807
+ 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)."
21808
+ }
21809
+ };
21810
+ PLAN_ROTATION = ["challenger", "pragmatist", "completionist", "security", "testability"];
21811
+ REVIEW_ROTATION = ["security", "completionist", "testability", "challenger", "pragmatist"];
21812
+ });
21813
+
21814
+ // src/debate/prompt-builder.ts
21815
+ class DebatePromptBuilder {
21816
+ stageContext;
21817
+ options;
21818
+ constructor(stageContext, options) {
21819
+ this.stageContext = stageContext;
21820
+ this.options = options;
21821
+ }
21822
+ buildProposalPrompt(debaterIndex) {
21823
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21824
+ return `${this.stageContext.taskContext}${personaBlock}
21825
+
21826
+ ${this.stageContext.outputFormat}`;
21827
+ }
21828
+ buildCritiquePrompt(debaterIndex, proposals) {
21829
+ const otherProposals = proposals.filter((_, i) => i !== debaterIndex);
21830
+ const proposalsSection = this.buildProposalsSection(otherProposals);
21831
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21832
+ return `You are reviewing proposals for a ${this.stageContext.stage} task.
21833
+
21834
+ ## Task
21835
+ ${this.stageContext.taskContext}${personaBlock}
21836
+
21837
+ ## Other Agents' Proposals
21838
+ ${proposalsSection}
21839
+
21840
+ Please critique these proposals and provide your refined analysis, identifying strengths, weaknesses, and your own updated position.`;
21841
+ }
21842
+ buildRebuttalPrompt(debaterIndex, proposals, priorRebuttals) {
21843
+ const contextBlock = this.options.sessionMode === "one-shot" ? `${this.stageContext.taskContext}
21844
+
21845
+ ` : "";
21846
+ const proposalsSection = this.buildProposalsSection(proposals);
21847
+ const rebuttalsSection = this.buildRebuttalsSection(priorRebuttals);
21848
+ const personaBlock = this.buildPersonaBlock(debaterIndex);
21849
+ const debaterNumber = debaterIndex + 1;
21850
+ return `${contextBlock}## Proposals
21851
+ ${proposalsSection}${rebuttalsSection}${personaBlock}
21852
+
21853
+ ## Your Task
21854
+ You are debater ${debaterNumber}. Provide your critique in prose.
21855
+ Identify strengths, weaknesses, and specific improvements for each proposal.
21856
+ Do NOT output JSON \u2014 focus on analysis only.`;
21857
+ }
21858
+ buildSynthesisPrompt(proposals, critiques, promptSuffix) {
21859
+ const proposalsSection = this.buildProposalsSection(proposals);
21860
+ const critiquesSection = this.buildCritiquesSection(critiques);
21861
+ return `You are a synthesis agent. Your task is to combine the strongest elements from multiple proposals into a single, optimal response.
21862
+
21863
+ ${this.stageContext.taskContext}
21864
+
21865
+ ## Proposals
21866
+ ${proposalsSection}
21867
+
21868
+ ## Critiques
21869
+ ${critiquesSection}
21870
+
21871
+ Please synthesize these into the best possible unified response, incorporating the strongest elements from each proposal.
21872
+ ${this.stageContext.outputFormat}${promptSuffix ? `
21873
+ ${promptSuffix}` : ""}`;
21874
+ }
21875
+ buildJudgePrompt(proposals, critiques) {
21876
+ const proposalsSection = this.buildProposalsSection(proposals);
21877
+ const critiquesSection = this.buildCritiquesSection(critiques);
21878
+ return `You are a judge evaluating multiple proposals. Select the best proposal or synthesize the optimal response.
21879
+
21880
+ ${this.stageContext.taskContext}
21881
+
21882
+ ## Proposals
21883
+ ${proposalsSection}
21884
+
21885
+ ## Critiques
21886
+ ${critiquesSection}
21887
+
21888
+ Evaluate each proposal against the critiques and provide the best possible response.
21889
+ ${this.stageContext.outputFormat}`;
21890
+ }
21891
+ buildClosePrompt() {
21892
+ return "Close this debate session.";
21893
+ }
21894
+ buildPersonaBlock(debaterIndex) {
21895
+ const debater = this.options.debaters[debaterIndex];
21896
+ if (!debater?.persona)
21897
+ return "";
21898
+ const { identity, lens } = PERSONA_FRAGMENTS[debater.persona];
21899
+ return `
21900
+
21901
+ ## Your Role
21902
+ ${identity}
21903
+ ${lens}`;
21904
+ }
21905
+ buildProposalsSection(proposals) {
21906
+ return proposals.map((p, i) => `### Proposal ${i + 1} (${this.buildDebaterLabel(p.debater)})
21907
+ ${p.output}`).join(`
21908
+
21909
+ `);
21910
+ }
21911
+ buildRebuttalsSection(rebuttals) {
21912
+ if (rebuttals.length === 0)
21913
+ return "";
21914
+ return `
21915
+
21916
+ ## Previous Rebuttals
21917
+ ${rebuttals.map((r, i) => `${i + 1}. ${r.output}`).join(`
21918
+
21919
+ `)}`;
21920
+ }
21921
+ buildCritiquesSection(critiques) {
21922
+ if (critiques.length === 0)
21923
+ return "";
21924
+ return critiques.map((c, i) => `### Critique ${i + 1} (${this.buildDebaterLabel(c.debater)})
21925
+ ${c.output}`).join(`
21926
+
21927
+ `);
21928
+ }
21929
+ buildDebaterLabel(debater) {
21930
+ return debater.persona ? `${debater.agent} (${debater.persona})` : debater.agent;
21931
+ }
21932
+ }
21933
+ var init_prompt_builder = __esm(() => {
21934
+ init_personas();
21935
+ });
21936
+
21731
21937
  // src/debate/session-stateful.ts
21732
21938
  async function runStatefulTurn(ctx, adapter, debater, prompt, roleKey, keepSessionOpen) {
21733
21939
  const modelTier = modelTierFromDebater(debater);
@@ -21783,7 +21989,9 @@ async function closeStatefulSession(ctx, adapter, debater, roleKey) {
21783
21989
  async function runStateful(ctx, prompt) {
21784
21990
  const logger = _debateSessionDeps.getSafeLogger();
21785
21991
  const config2 = ctx.stageConfig;
21786
- const debaters = config2.debaters ?? [];
21992
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
21993
+ const rawDebaters = config2.debaters ?? [];
21994
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
21787
21995
  let totalCostUsd = 0;
21788
21996
  const resolved = [];
21789
21997
  for (const debater of debaters) {
@@ -21801,7 +22009,8 @@ async function runStateful(ctx, prompt) {
21801
22009
  });
21802
22010
  const debate = ctx.config?.debate;
21803
22011
  const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
21804
- const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, debaterIdx) => () => runStatefulTurn(ctx, adapter, debater, prompt, `debate-${ctx.stage}-${debaterIdx}`, config2.rounds > 1)), concurrencyLimit);
22012
+ const proposalBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: resolved.map((r) => r.debater), sessionMode: "stateful" });
22013
+ const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, debaterIdx) => () => runStatefulTurn(ctx, adapter, debater, proposalBuilder.buildProposalPrompt(debaterIdx), `debate-${ctx.stage}-${debaterIdx}`, config2.rounds > 1)), concurrencyLimit);
21805
22014
  const successfulProposals = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
21806
22015
  for (const r of proposalSettled) {
21807
22016
  if (r.status === "fulfilled") {
@@ -21880,8 +22089,9 @@ async function runStateful(ctx, prompt) {
21880
22089
  }
21881
22090
  let critiqueOutputs = [];
21882
22091
  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);
22092
+ const proposals2 = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
22093
+ const critiqueBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: proposals2.map((p) => p.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22094
+ 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
22095
  for (const r of critiqueSettled) {
21886
22096
  if (r.status === "fulfilled") {
21887
22097
  totalCostUsd += r.value.cost;
@@ -21892,9 +22102,9 @@ async function runStateful(ctx, prompt) {
21892
22102
  const proposalOutputs = successfulProposals.map((s) => s.output);
21893
22103
  const fullResolverContext = ctx.resolverContextInput ? {
21894
22104
  ...ctx.resolverContextInput,
21895
- labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
22105
+ labeledProposals: successfulProposals.map((s) => ({ debater: buildDebaterLabel2(s.debater), output: s.output }))
21896
22106
  } : undefined;
21897
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
22107
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext, undefined, successfulProposals.map((s) => s.debater));
21898
22108
  totalCostUsd += outcome.resolverCostUsd;
21899
22109
  const proposals = successfulProposals.map((s) => ({
21900
22110
  debater: s.debater,
@@ -21917,11 +22127,13 @@ async function runStateful(ctx, prompt) {
21917
22127
  };
21918
22128
  }
21919
22129
  var init_session_stateful = __esm(() => {
22130
+ init_personas();
22131
+ init_prompt_builder();
21920
22132
  init_session_helpers();
21921
22133
  });
21922
22134
 
21923
22135
  // src/debate/session-hybrid.ts
21924
- async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix) {
22136
+ async function runRebuttalLoop(ctx, proposals, builder, sessionRolePrefix) {
21925
22137
  const logger = _debateSessionDeps.getSafeLogger();
21926
22138
  const config2 = ctx.stageConfig;
21927
22139
  const rebuttals = [];
@@ -21929,7 +22141,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21929
22141
  const proposalList = proposals.map((s) => ({ debater: s.debater, output: s.output }));
21930
22142
  try {
21931
22143
  for (let round = 1;round <= config2.rounds; round++) {
21932
- const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
22144
+ const priorRebuttals = rebuttals.filter((r) => r.round < round);
21933
22145
  for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21934
22146
  const proposal = proposals[debaterIdx];
21935
22147
  const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
@@ -21938,7 +22150,7 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21938
22150
  round,
21939
22151
  debaterIndex: debaterIdx
21940
22152
  });
21941
- const rebuttalPrompt = buildRebuttalContext(originalPrompt, proposalList, priorRebuttals, debaterIdx);
22153
+ const rebuttalPrompt = builder.buildRebuttalPrompt(debaterIdx, proposalList, priorRebuttals);
21942
22154
  try {
21943
22155
  const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21944
22156
  costUsd += turnResult.cost;
@@ -21968,7 +22180,9 @@ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix
21968
22180
  async function runHybrid(ctx, prompt) {
21969
22181
  const logger = _debateSessionDeps.getSafeLogger();
21970
22182
  const config2 = ctx.stageConfig;
21971
- const debaters = config2.debaters ?? [];
22183
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
22184
+ const rawDebaters = config2.debaters ?? [];
22185
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
21972
22186
  let totalCostUsd = 0;
21973
22187
  const resolved = [];
21974
22188
  for (const debater of debaters) {
@@ -22033,14 +22247,15 @@ async function runHybrid(ctx, prompt) {
22033
22247
  }
22034
22248
  const proposalOutputs = successfulProposals.map((s) => s.output);
22035
22249
  const proposalList = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
22036
- const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, prompt, "debate-hybrid");
22250
+ const rebuttalBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: successfulProposals.map((s) => s.debater), sessionMode: "stateful" });
22251
+ const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, rebuttalBuilder, "debate-hybrid");
22037
22252
  totalCostUsd += rebuttalCost;
22038
22253
  const critiqueOutputs = rebuttals.map((r) => r.output);
22039
22254
  const fullResolverContext = ctx.resolverContextInput ? {
22040
22255
  ...ctx.resolverContextInput,
22041
- labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
22256
+ labeledProposals: successfulProposals.map((s) => ({ debater: buildDebaterLabel2(s.debater), output: s.output }))
22042
22257
  } : undefined;
22043
- const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
22258
+ const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext, undefined, successfulProposals.map((s) => s.debater));
22044
22259
  totalCostUsd += resolveResult.resolverCostUsd;
22045
22260
  return {
22046
22261
  storyId: ctx.storyId,
@@ -22055,6 +22270,8 @@ async function runHybrid(ctx, prompt) {
22055
22270
  };
22056
22271
  }
22057
22272
  var init_session_hybrid = __esm(() => {
22273
+ init_personas();
22274
+ init_prompt_builder();
22058
22275
  init_session_helpers();
22059
22276
  init_session_stateful();
22060
22277
  });
@@ -22063,7 +22280,9 @@ var init_session_hybrid = __esm(() => {
22063
22280
  async function runOneShot(ctx, prompt) {
22064
22281
  const logger = _debateSessionDeps.getSafeLogger();
22065
22282
  const config2 = ctx.stageConfig;
22066
- const debaters = config2.debaters ?? [];
22283
+ const personaStage = ctx.stage === "plan" ? "plan" : "review";
22284
+ const rawDebaters = config2.debaters ?? [];
22285
+ const debaters = resolvePersonas(rawDebaters, personaStage, config2.autoPersona ?? false);
22067
22286
  let totalCostUsd = 0;
22068
22287
  const resolved = [];
22069
22288
  for (const debater of debaters) {
@@ -22081,7 +22300,8 @@ async function runOneShot(ctx, prompt) {
22081
22300
  });
22082
22301
  const debate = ctx.config?.debate;
22083
22302
  const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
22084
- const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => () => runComplete(adapter, prompt, {
22303
+ const proposalBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: resolved.map((r) => r.debater), sessionMode: "one-shot" });
22304
+ const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => () => runComplete(adapter, proposalBuilder.buildProposalPrompt(i), {
22085
22305
  model: resolveDebaterModel(debater, ctx.config),
22086
22306
  featureName: ctx.stage,
22087
22307
  config: ctx.config,
@@ -22166,8 +22386,9 @@ async function runOneShot(ctx, prompt) {
22166
22386
  }
22167
22387
  let critiqueOutputs = [];
22168
22388
  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), {
22389
+ const proposals2 = successful.map((p) => ({ debater: p.debater, output: p.output }));
22390
+ const critiqueBuilder = new DebatePromptBuilder({ taskContext: prompt, outputFormat: "", stage: ctx.stage }, { debaters: proposals2.map((p) => p.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22391
+ const critiqueSettled = await allSettledBounded(successful.map(({ debater, adapter }, i) => () => runComplete(adapter, critiqueBuilder.buildCritiquePrompt(i, proposals2), {
22171
22392
  model: resolveDebaterModel(debater, ctx.config),
22172
22393
  featureName: ctx.stage,
22173
22394
  config: ctx.config,
@@ -22185,9 +22406,9 @@ async function runOneShot(ctx, prompt) {
22185
22406
  const proposalOutputs = successful.map((p) => p.output);
22186
22407
  const fullResolverContext = ctx.resolverContextInput ? {
22187
22408
  ...ctx.resolverContextInput,
22188
- labeledProposals: successful.map((p) => ({ debater: p.debater.agent, output: p.output }))
22409
+ labeledProposals: successful.map((p) => ({ debater: buildDebaterLabel2(p.debater), output: p.output }))
22189
22410
  } : undefined;
22190
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
22411
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext, undefined, successful.map((p) => p.debater));
22191
22412
  totalCostUsd += outcome.resolverCostUsd;
22192
22413
  const proposals = successful.map((p) => ({
22193
22414
  debater: p.debater,
@@ -22210,15 +22431,18 @@ async function runOneShot(ctx, prompt) {
22210
22431
  };
22211
22432
  }
22212
22433
  var init_session_one_shot = __esm(() => {
22434
+ init_personas();
22435
+ init_prompt_builder();
22213
22436
  init_session_helpers();
22214
22437
  });
22215
22438
 
22216
22439
  // src/debate/session-plan.ts
22217
22440
  import { join as join10 } from "path";
22218
- async function runPlan2(ctx, basePrompt, opts) {
22441
+ async function runPlan2(ctx, taskContext, outputFormat, opts) {
22219
22442
  const logger = _debateSessionDeps.getSafeLogger();
22220
22443
  const config2 = ctx.stageConfig;
22221
- const debaters = config2.debaters ?? [];
22444
+ const rawDebaters = config2.debaters ?? [];
22445
+ const debaters = resolvePersonas(rawDebaters, "plan", config2.autoPersona ?? false);
22222
22446
  let totalCostUsd = 0;
22223
22447
  const resolved = [];
22224
22448
  for (const debater of debaters) {
@@ -22236,14 +22460,15 @@ async function runPlan2(ctx, basePrompt, opts) {
22236
22460
  });
22237
22461
  const debate = ctx.config?.debate;
22238
22462
  const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
22239
- const settled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => async () => {
22463
+ const proposalBuilder = new DebatePromptBuilder({ taskContext, outputFormat, stage: "plan" }, { debaters: resolved.map((r) => r.debater), sessionMode: ctx.stageConfig.sessionMode ?? "one-shot" });
22464
+ const settled = await allSettledBounded(resolved.map(({ debater: rd, adapter }, i) => async () => {
22240
22465
  const tempOutputPath = join10(opts.outputDir, `prd-debate-${i}.json`);
22241
- const debaterPrompt = `${basePrompt}
22466
+ const debaterPrompt = `${proposalBuilder.buildProposalPrompt(i)}
22242
22467
 
22243
22468
  Write the PRD JSON directly to this file path: ${tempOutputPath}
22244
22469
  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);
22470
+ const modelTier = modelTierFromDebater(rd);
22471
+ const modelDef = resolveModelDefForDebater(rd, modelTier, ctx.config);
22247
22472
  const planResult = await adapter.plan({
22248
22473
  prompt: debaterPrompt,
22249
22474
  workdir: opts.workdir,
@@ -22259,7 +22484,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22259
22484
  sessionRole: `plan-${i}`
22260
22485
  });
22261
22486
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22262
- return { debater, adapter, output, cost: planResult.costUsd ?? 0 };
22487
+ return { debater: rd, adapter, output, cost: planResult.costUsd ?? 0 };
22263
22488
  }), concurrencyLimit);
22264
22489
  const successful = [];
22265
22490
  for (let i = 0;i < settled.length; i++) {
@@ -22332,7 +22557,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22332
22557
  featureName: opts.feature,
22333
22558
  timeoutSeconds: opts.timeoutSeconds ?? 600
22334
22559
  };
22335
- const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, basePrompt, "plan-hybrid");
22560
+ const rebuttalBuilder = new DebatePromptBuilder({ taskContext, outputFormat: "", stage: "plan" }, { debaters: successful.map((p) => p.debater), sessionMode });
22561
+ const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, rebuttalBuilder, "plan-hybrid");
22336
22562
  critiqueOutputs = rebuttals.map((r) => r.output);
22337
22563
  rebuttalList = rebuttals;
22338
22564
  totalCostUsd += costUsd;
@@ -22341,7 +22567,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22341
22567
  }
22342
22568
  const resolverTimeoutMs = (ctx.stageConfig.timeoutSeconds ?? 600) * 1000;
22343
22569
  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.";
22344
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature, undefined, undefined, planSynthesisSuffix);
22570
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature, undefined, undefined, planSynthesisSuffix, successful.map((p) => p.debater));
22345
22571
  const winningOutput = outcome.output ?? successful[0].output;
22346
22572
  const proposals = successful.map((p) => ({ debater: p.debater, output: p.output }));
22347
22573
  logger?.info("debate", "debate:result", {
@@ -22363,6 +22589,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22363
22589
  };
22364
22590
  }
22365
22591
  var init_session_plan = __esm(() => {
22592
+ init_personas();
22593
+ init_prompt_builder();
22366
22594
  init_session_helpers();
22367
22595
  init_session_hybrid();
22368
22596
  });
@@ -22448,13 +22676,13 @@ class DebateSession {
22448
22676
  resolverContextInput: this.resolverContextInput
22449
22677
  }, prompt);
22450
22678
  }
22451
- async runPlan(basePrompt, opts) {
22679
+ async runPlan(taskContext, outputFormat, opts) {
22452
22680
  return runPlan2({
22453
22681
  storyId: this.storyId,
22454
22682
  stage: this.stage,
22455
22683
  stageConfig: this.stageConfig,
22456
22684
  config: this.config
22457
- }, basePrompt, opts);
22685
+ }, taskContext, outputFormat, opts);
22458
22686
  }
22459
22687
  }
22460
22688
  var DEFAULT_TIMEOUT_SECONDS = 600;
@@ -22471,8 +22699,8 @@ var init_session = __esm(() => {
22471
22699
  var init_debate = __esm(() => {
22472
22700
  init_session();
22473
22701
  init_session_helpers();
22474
- init_resolvers();
22475
- init_session_helpers();
22702
+ init_prompt_builder();
22703
+ init_personas();
22476
22704
  });
22477
22705
 
22478
22706
  // src/interaction/bridge-builder.ts
@@ -23653,50 +23881,6 @@ var init_init = __esm(() => {
23653
23881
  init_webhook();
23654
23882
  });
23655
23883
 
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
23884
  // src/prd/validate.ts
23701
23885
  function validateStoryId(id) {
23702
23886
  if (!id || id.length === 0) {
@@ -25611,7 +25795,8 @@ Rules:
25611
25795
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
25612
25796
  - **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.
25613
25797
  - **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
25614
- - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
25798
+ - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).
25799
+ - **Process cwd**: When spawning child processes to invoke a CLI or binary, set the working directory to the **package root** (\`join(import.meta.dir, "../../..")\`) as your default \u2014 unless your Step 2 exploration reveals the CLI uses a different working directory convention (e.g. reads config from \`~/.config/\`, or resolves paths relative to a flag value). Always check how the CLI resolves file paths before assuming.`;
25615
25800
  const implementationSection = options.implementationContext && options.implementationContext.length > 0 ? `
25616
25801
 
25617
25802
  ## Implementation (already exists)
@@ -25802,7 +25987,7 @@ Rules:
25802
25987
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
25803
25988
  - **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
25989
  - 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')\`).`;
25990
+ - **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
25991
  }
25807
25992
  async function generateAcceptanceTests(adapter, options) {
25808
25993
  const logger = getLogger();
@@ -26960,6 +27145,7 @@ function buildReviewPrompt(diff, story, _semanticConfig) {
26960
27145
  "## Diff",
26961
27146
  diff,
26962
27147
  "",
27148
+ "Also flag any changes in the diff not required by the acceptance criteria above as out-of-scope findings.",
26963
27149
  "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
26964
27150
  ].join(`
26965
27151
  `);
@@ -26981,7 +27167,7 @@ function buildReReviewPrompt(updatedDiff, previousFindings) {
26981
27167
  ].join(`
26982
27168
  `);
26983
27169
  }
26984
- function buildProposalsSection(proposals) {
27170
+ function buildProposalsSection2(proposals) {
26985
27171
  return proposals.map((p) => `### ${p.debater}
26986
27172
  ${p.output}`).join(`
26987
27173
 
@@ -27025,7 +27211,7 @@ function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticC
27025
27211
  `);
27026
27212
  const framing = buildResolverFraming(resolverContext);
27027
27213
  const voteTally = buildVoteTallyLine(resolverContext);
27028
- const proposalsSection = buildProposalsSection(proposals);
27214
+ const proposalsSection = buildProposalsSection2(proposals);
27029
27215
  const critiquesSection = buildCritiquesSection(critiques);
27030
27216
  return [
27031
27217
  framing,
@@ -27051,7 +27237,7 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27051
27237
  const framing = buildResolverFraming(resolverContext);
27052
27238
  const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
27053
27239
  `) : "(none)";
27054
- const proposalsSection = buildProposalsSection(proposals);
27240
+ const proposalsSection = buildProposalsSection2(proposals);
27055
27241
  const critiquesSection = buildCritiquesSection(critiques);
27056
27242
  return [
27057
27243
  `${framing} This is a re-review after implementer changes.`,
@@ -27074,12 +27260,10 @@ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFi
27074
27260
 
27075
27261
  // src/review/dialogue.ts
27076
27262
  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 {}
27263
+ const parsed = tryParseLLMJson(rawOutput);
27264
+ if (parsed && typeof parsed.deltaSummary === "string" && parsed.deltaSummary.length > 0) {
27265
+ return parsed.deltaSummary;
27266
+ }
27083
27267
  const newIds = new Set(newFindings.map((f) => f.ruleId));
27084
27268
  const prevIds = new Set(previousFindings.map((f) => f.ruleId));
27085
27269
  const resolved = previousFindings.filter((f) => !newIds.has(f.ruleId));
@@ -27119,7 +27303,7 @@ function compactHistory(history) {
27119
27303
  function parseReviewResponse(output) {
27120
27304
  let parsed;
27121
27305
  try {
27122
- parsed = JSON.parse(output);
27306
+ parsed = parseLLMJson(output);
27123
27307
  } catch {
27124
27308
  throw new NaxError("[dialogue] Failed to parse reviewer JSON response", "REVIEWER_PARSE_FAILED", {
27125
27309
  stage: "review",
@@ -27698,23 +27882,11 @@ function validateLLMShape(parsed) {
27698
27882
  return { passed: obj.passed, findings: obj.findings };
27699
27883
  }
27700
27884
  function parseLLMResponse(raw) {
27701
- const text = raw.trim();
27702
27885
  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 {}
27886
+ return validateLLMShape(tryParseLLMJson(raw));
27887
+ } catch {
27888
+ return null;
27716
27889
  }
27717
- return null;
27718
27890
  }
27719
27891
  function formatFindings(findings) {
27720
27892
  return findings.map((f) => `[${f.severity}] ${f.file}:${f.line} \u2014 ${f.issue}
@@ -28689,6 +28861,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28689
28861
  config: ctx.config,
28690
28862
  projectDir: ctx.projectDir,
28691
28863
  maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
28864
+ featureName: ctx.prd.feature,
28692
28865
  storyId: ctx.story.id,
28693
28866
  sessionRole: "implementer"
28694
28867
  });
@@ -30466,61 +30639,34 @@ var init_executor = __esm(() => {
30466
30639
  });
30467
30640
 
30468
30641
  // 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);
30642
+ function detectFramework(output) {
30643
+ if (/^\s*Test Files\s+\d+/m.test(output))
30644
+ return "vitest";
30645
+ if (/^\s*Tests:\s+\d+/m.test(output))
30646
+ return "jest";
30647
+ if (/={3,}\s+\d+\s+(?:failed|passed).*in\s+[\d.]+s\s*={3,}/m.test(output))
30648
+ return "pytest";
30649
+ if (/^--- (?:FAIL|PASS):/m.test(output) || /^(?:ok|FAIL)\s+\t/m.test(output))
30650
+ return "go";
30651
+ if (/^\(fail\)\s/m.test(output) || /^bun test/m.test(output) || /[\u2713\u2714\u2717\u2718]/m.test(output))
30652
+ return "bun";
30653
+ return "unknown";
30477
30654
  }
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
- }
30655
+ function parseTestOutput(output) {
30656
+ const framework = detectFramework(output);
30657
+ switch (framework) {
30658
+ case "bun":
30659
+ return parseBunOutput(output);
30660
+ case "jest":
30661
+ case "vitest":
30662
+ return parseJestOutput(output);
30663
+ case "pytest":
30664
+ return parsePytestOutput(output);
30665
+ case "go":
30666
+ return parseGoTestOutput(output);
30667
+ default:
30668
+ return parseCommonOutput(output);
30522
30669
  }
30523
- return { passed, failed, failures };
30524
30670
  }
30525
30671
  function parseBunOutput(output) {
30526
30672
  const lines = output.split(`
@@ -30582,6 +30728,122 @@ function parseBunOutput(output) {
30582
30728
  }
30583
30729
  return { passed, failed, failures };
30584
30730
  }
30731
+ function parseJestOutput(output) {
30732
+ const failures = [];
30733
+ let passed = 0;
30734
+ let failed = 0;
30735
+ const summaryMatches = Array.from(output.matchAll(/^\s*Tests:\s+(.*)/gm));
30736
+ if (summaryMatches.length > 0) {
30737
+ const summaryLine = summaryMatches[summaryMatches.length - 1][1];
30738
+ const failedMatch = summaryLine.match(/(\d+)\s+failed/);
30739
+ const passedMatch = summaryLine.match(/(\d+)\s+passed/);
30740
+ if (failedMatch)
30741
+ failed = Number.parseInt(failedMatch[1], 10);
30742
+ if (passedMatch)
30743
+ passed = Number.parseInt(passedMatch[1], 10);
30744
+ }
30745
+ let currentFile = "unknown";
30746
+ const lines = output.split(`
30747
+ `);
30748
+ for (let i = 0;i < lines.length; i++) {
30749
+ const line = lines[i];
30750
+ const fileMatch = line.match(/^\s*(?:FAIL|PASS)\s+(\S+\.[jt]sx?)/);
30751
+ if (fileMatch) {
30752
+ currentFile = fileMatch[1];
30753
+ continue;
30754
+ }
30755
+ const bulletMatch = line.match(/^\s+\u25CF\s+(.+)$/);
30756
+ if (bulletMatch) {
30757
+ const testName = bulletMatch[1].trim();
30758
+ let error48 = "";
30759
+ for (let j = i + 1;j < lines.length && j < i + 10; j++) {
30760
+ const next = lines[j].trim();
30761
+ if (!next)
30762
+ continue;
30763
+ if (next.startsWith("\u25CF") || /^(?:FAIL|PASS)\s/.test(next))
30764
+ break;
30765
+ error48 = next;
30766
+ break;
30767
+ }
30768
+ failures.push({
30769
+ file: currentFile,
30770
+ testName,
30771
+ error: error48 || "Unknown error",
30772
+ stackTrace: []
30773
+ });
30774
+ }
30775
+ }
30776
+ return { passed, failed, failures };
30777
+ }
30778
+ function parsePytestOutput(output) {
30779
+ const common = parseCommonOutput(output);
30780
+ const failures = [];
30781
+ for (const line of output.split(`
30782
+ `)) {
30783
+ const m = line.match(/^FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
30784
+ if (m) {
30785
+ const [, location, reason] = m;
30786
+ const parts = location.split("::");
30787
+ failures.push({
30788
+ file: parts[0] ?? location,
30789
+ testName: parts.slice(1).join(" > ") || location,
30790
+ error: reason?.trim() || "Unknown error",
30791
+ stackTrace: []
30792
+ });
30793
+ }
30794
+ }
30795
+ return {
30796
+ passed: common.passed,
30797
+ failed: common.failed,
30798
+ failures: failures.length > 0 ? failures : common.failures
30799
+ };
30800
+ }
30801
+ function parseGoTestOutput(output) {
30802
+ const common = parseCommonOutput(output);
30803
+ const failures = [];
30804
+ for (const line of output.split(`
30805
+ `)) {
30806
+ const m = line.match(/^--- FAIL:\s+(\S+)\s+\([\d.]+s\)/);
30807
+ if (m) {
30808
+ failures.push({
30809
+ file: "unknown",
30810
+ testName: m[1],
30811
+ error: "Unknown error",
30812
+ stackTrace: []
30813
+ });
30814
+ }
30815
+ }
30816
+ return {
30817
+ passed: common.passed,
30818
+ failed: common.failed,
30819
+ failures: failures.length > 0 ? failures : common.failures
30820
+ };
30821
+ }
30822
+ function parseCommonOutput(output) {
30823
+ let passed = 0;
30824
+ let failed = 0;
30825
+ const patterns = [
30826
+ /(\d+)\s+pass(?:ed)?(?:,\s*|\s+)(\d+)\s+fail/i,
30827
+ /Tests:\s+(\d+)\s+passed,\s+(\d+)\s+failed/i,
30828
+ /(\d+)\s+pass/i
30829
+ ];
30830
+ for (const pattern of patterns) {
30831
+ const matches = Array.from(output.matchAll(new RegExp(pattern, "gi")));
30832
+ if (matches.length > 0) {
30833
+ const last = matches[matches.length - 1];
30834
+ passed = Number.parseInt(last[1], 10);
30835
+ failed = last[2] ? Number.parseInt(last[2], 10) : 0;
30836
+ break;
30837
+ }
30838
+ }
30839
+ if (failed === 0) {
30840
+ const failMatches = Array.from(output.matchAll(/(\d+)\s+fail/gi));
30841
+ if (failMatches.length > 0) {
30842
+ failed = Number.parseInt(failMatches[failMatches.length - 1][1], 10);
30843
+ }
30844
+ }
30845
+ return { passed, failed, failures: [] };
30846
+ }
30585
30847
  function formatFailureSummary(failures, maxChars = 2000) {
30586
30848
  if (failures.length === 0) {
30587
30849
  return "No test failures";
@@ -30595,48 +30857,24 @@ function formatFailureSummary(failures, maxChars = 2000) {
30595
30857
  const errorLine = ` Error: ${failure.error}`;
30596
30858
  const stackLine = failure.stackTrace.length > 0 ? ` ${failure.stackTrace[0]}` : "";
30597
30859
  const blockLines = [header, errorLine];
30598
- if (stackLine) {
30860
+ if (stackLine)
30599
30861
  blockLines.push(stackLine);
30600
- }
30601
30862
  blockLines.push("");
30602
30863
  const block = blockLines.join(`
30603
30864
  `);
30604
- const blockLength = block.length;
30605
- if (totalChars + blockLength > maxChars && lines.length > 0) {
30606
- const remaining = failures.length - i;
30865
+ if (totalChars + block.length > maxChars && lines.length > 0) {
30607
30866
  lines.push(`
30608
- ... and ${remaining} more failure(s) (truncated)`);
30867
+ ... and ${failures.length - i} more failure(s) (truncated)`);
30609
30868
  break;
30610
30869
  }
30611
30870
  lines.push(...blockLines);
30612
- totalChars += blockLength;
30871
+ totalChars += block.length;
30613
30872
  }
30614
30873
  return lines.join(`
30615
30874
  `).trim();
30616
30875
  }
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
- }
30876
+ function analyzeTestExitCode(output, exitCode) {
30877
+ const { passed: passCount, failed: failCount } = parseCommonOutput(output);
30640
30878
  const allTestsPassed = passCount > 0 && failCount === 0;
30641
30879
  const isEnvironmentalFailure = allTestsPassed && exitCode !== 0;
30642
30880
  const result = {
@@ -30707,7 +30945,7 @@ async function runVerificationCore(options) {
30707
30945
  }
30708
30946
  const exitCode = execution.exitCode ?? 1;
30709
30947
  if (exitCode !== 0 && execution.output) {
30710
- const analysis = parseTestOutput(execution.output, exitCode);
30948
+ const analysis = analyzeTestExitCode(execution.output, exitCode);
30711
30949
  if (analysis.isEnvironmentalFailure) {
30712
30950
  return {
30713
30951
  status: "ENVIRONMENTAL_FAILURE",
@@ -31000,7 +31238,7 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
31000
31238
  });
31001
31239
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
31002
31240
  if (!fullSuitePassed && fullSuiteResult.output) {
31003
- const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
31241
+ const testSummary = _rectificationGateDeps.parseTestOutput(fullSuiteResult.output);
31004
31242
  if (testSummary.failed > 0) {
31005
31243
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName, projectDir);
31006
31244
  }
@@ -31134,7 +31372,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
31134
31372
  return true;
31135
31373
  }
31136
31374
  if (retryFullSuite.output) {
31137
- const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
31375
+ const newTestSummary = _rectificationGateDeps.parseTestOutput(retryFullSuite.output);
31138
31376
  state.currentFailures = newTestSummary.failed;
31139
31377
  testSummary.failures = newTestSummary.failures;
31140
31378
  testSummary.failed = newTestSummary.failed;
@@ -31182,7 +31420,7 @@ var init_rectification_gate = __esm(() => {
31182
31420
  init_prompts();
31183
31421
  _rectificationGateDeps = {
31184
31422
  executeWithTimeout,
31185
- parseBunTestOutput,
31423
+ parseTestOutput,
31186
31424
  shouldRetryRectification
31187
31425
  };
31188
31426
  });
@@ -33152,9 +33390,6 @@ function calculateMaxIterations(tierOrder) {
33152
33390
  return tierOrder.reduce((sum, t) => sum + t.attempts, 0);
33153
33391
  }
33154
33392
 
33155
- // src/execution/test-output-parser.ts
33156
- var init_test_output_parser = () => {};
33157
-
33158
33393
  // src/verification/rectification-loop.ts
33159
33394
  async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
33160
33395
  const logger = getSafeLogger();
@@ -33207,7 +33442,7 @@ async function runRectificationLoop2(opts) {
33207
33442
  } = opts;
33208
33443
  const logger = getSafeLogger();
33209
33444
  const rectificationConfig = config2.execution.rectification;
33210
- const testSummary = parseBunTestOutput(testOutput);
33445
+ const testSummary = parseTestOutput(testOutput);
33211
33446
  const label = promptPrefix ? "regression rectification" : "rectification";
33212
33447
  const rectificationState = {
33213
33448
  attempt: 0,
@@ -33342,13 +33577,13 @@ ${rectificationPrompt}`;
33342
33577
  return true;
33343
33578
  }
33344
33579
  if (retryVerification.output) {
33345
- const newTestSummary = parseBunTestOutput(retryVerification.output);
33580
+ const newTestSummary = parseTestOutput(retryVerification.output);
33346
33581
  state.currentFailures = newTestSummary.failed;
33347
33582
  state.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
33348
33583
  testSummary.failures = newTestSummary.failures;
33349
33584
  testSummary.failed = newTestSummary.failed;
33350
33585
  testSummary.passed = newTestSummary.passed;
33351
- if (newTestSummary.failed === 0) {
33586
+ if (newTestSummary.failed === 0 && (retryVerification.status === "SUCCESS" || newTestSummary.passed > 0)) {
33352
33587
  state.lastExitCode = 0;
33353
33588
  logger?.info("rectification", `[OK] ${label} succeeded after parsing retry output`, {
33354
33589
  storyId: story.id,
@@ -33488,7 +33723,6 @@ var init_rectification_loop = __esm(() => {
33488
33723
  init_cost();
33489
33724
  init_registry();
33490
33725
  init_config();
33491
- init_test_output_parser();
33492
33726
  init_logger2();
33493
33727
  init_prd();
33494
33728
  init_rectification();
@@ -33754,7 +33988,7 @@ class RegressionStrategy {
33754
33988
  });
33755
33989
  const durationMs = Date.now() - start;
33756
33990
  if (result.success) {
33757
- const parsed2 = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33991
+ const parsed2 = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33758
33992
  return makePassResult(ctx.storyId, "regression", {
33759
33993
  rawOutput: result.output,
33760
33994
  passCount: parsed2.passed,
@@ -33770,7 +34004,7 @@ class RegressionStrategy {
33770
34004
  if (result.status === "TIMEOUT") {
33771
34005
  return makeFailResult(ctx.storyId, "regression", "TIMEOUT", { rawOutput: result.output, durationMs });
33772
34006
  }
33773
- const parsed = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34007
+ const parsed = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33774
34008
  return makeFailResult(ctx.storyId, "regression", "TEST_FAILURE", {
33775
34009
  rawOutput: result.output,
33776
34010
  passCount: parsed.passed,
@@ -33994,7 +34228,7 @@ class ScopedStrategy {
33994
34228
  });
33995
34229
  const durationMs = Date.now() - start;
33996
34230
  if (result.success) {
33997
- const parsed2 = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34231
+ const parsed2 = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
33998
34232
  return makePassResult(ctx.storyId, "scoped", {
33999
34233
  rawOutput: result.output,
34000
34234
  passCount: parsed2.passed,
@@ -34010,7 +34244,7 @@ class ScopedStrategy {
34010
34244
  scopeTestFallback
34011
34245
  });
34012
34246
  }
34013
- const parsed = result.output ? parseBunTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34247
+ const parsed = result.output ? parseTestOutput(result.output) : { passed: 0, failed: 0, failures: [] };
34014
34248
  return makeFailResult(ctx.storyId, "scoped", "TEST_FAILURE", {
34015
34249
  rawOutput: result.output,
34016
34250
  passCount: parsed.passed,
@@ -34344,12 +34578,11 @@ function stripCodeFences(text) {
34344
34578
  return trimmed;
34345
34579
  }
34346
34580
  function parseRoutingResponse(output, story, config2) {
34347
- const jsonText = extractJsonFromMarkdown(output.trim());
34348
- const parsed = JSON.parse(jsonText);
34581
+ const parsed = parseLLMJson(output);
34349
34582
  return validateRoutingDecision(parsed, config2, story);
34350
34583
  }
34351
34584
  function parseBatchResponse(output, stories, config2) {
34352
- const parsed = JSON.parse(extractJsonFromMarkdown(output.trim()));
34585
+ const parsed = parseLLMJson(output);
34353
34586
  if (!Array.isArray(parsed)) {
34354
34587
  throw new Error("Batch LLM response must be a JSON array");
34355
34588
  }
@@ -36143,7 +36376,7 @@ var package_default;
36143
36376
  var init_package = __esm(() => {
36144
36377
  package_default = {
36145
36378
  name: "@nathapp/nax",
36146
- version: "0.59.2",
36379
+ version: "0.60.0-canary.1",
36147
36380
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
36148
36381
  type: "module",
36149
36382
  bin: {
@@ -36223,8 +36456,8 @@ var init_version = __esm(() => {
36223
36456
  NAX_VERSION = package_default.version;
36224
36457
  NAX_COMMIT = (() => {
36225
36458
  try {
36226
- if (/^[0-9a-f]{6,10}$/.test("d42d47be"))
36227
- return "d42d47be";
36459
+ if (/^[0-9a-f]{6,10}$/.test("1de1f9cc"))
36460
+ return "1de1f9cc";
36228
36461
  } catch {}
36229
36462
  try {
36230
36463
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -36887,28 +37120,17 @@ function parseDiagnosisResult(output) {
36887
37120
  if (!output || output.trim() === "") {
36888
37121
  return null;
36889
37122
  }
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;
37123
+ const parsed = tryParseLLMJson(output);
37124
+ if (parsed && typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
37125
+ return {
37126
+ verdict: parsed.verdict,
37127
+ reasoning: parsed.reasoning,
37128
+ confidence: parsed.confidence,
37129
+ testIssues: Array.isArray(parsed.testIssues) ? parsed.testIssues : undefined,
37130
+ sourceIssues: Array.isArray(parsed.sourceIssues) ? parsed.sourceIssues : undefined
37131
+ };
36911
37132
  }
37133
+ return null;
36912
37134
  }
36913
37135
  var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
36914
37136
  var init_fix_diagnosis = __esm(() => {
@@ -37106,6 +37328,12 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
37106
37328
  logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
37107
37329
  const { unlink: unlink3 } = await import("fs/promises");
37108
37330
  await unlink3(testPath);
37331
+ if (acceptanceContext.featureDir) {
37332
+ const metaPath = path15.join(acceptanceContext.featureDir, "acceptance-meta.json");
37333
+ try {
37334
+ await unlink3(metaPath);
37335
+ } catch {}
37336
+ }
37109
37337
  let implementationContext;
37110
37338
  const storyGitRef = acceptanceContext.storyGitRef;
37111
37339
  const workdir = acceptanceContext.workdir;
@@ -37746,7 +37974,7 @@ async function runDeferredRegression(options) {
37746
37974
  affectedStories: []
37747
37975
  };
37748
37976
  }
37749
- const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
37977
+ const testSummary = _regressionDeps.parseTestOutput(fullSuiteResult.output);
37750
37978
  if (testSummary.failed === 0 && testSummary.passed === 0) {
37751
37979
  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
37980
  return {
@@ -37892,7 +38120,7 @@ var init_run_regression = __esm(() => {
37892
38120
  _regressionDeps = {
37893
38121
  runVerification: fullSuite,
37894
38122
  runRectificationLoop: runRectificationLoop2,
37895
- parseBunTestOutput,
38123
+ parseTestOutput,
37896
38124
  reverseMapTestToSource
37897
38125
  };
37898
38126
  });
@@ -38873,6 +39101,11 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
38873
39101
  const diffSummary = await captureDiffSummary(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
38874
39102
  if (diffSummary) {
38875
39103
  completedStory.diffSummary = diffSummary;
39104
+ } else {
39105
+ logger?.debug("context-chain", "No diff summary captured (agent may not have committed yet)", {
39106
+ storyId: completedStory.id,
39107
+ storyGitRef: ctx.storyGitRef
39108
+ });
38876
39109
  }
38877
39110
  } catch {}
38878
39111
  }
@@ -72809,13 +73042,16 @@ function validateStory(raw, index, allIds) {
72809
73042
  throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
72810
73043
  }
72811
73044
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
72812
- const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
73045
+ let testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
72813
73046
  const rawJustification = routing.noTestJustification ?? s.noTestJustification;
72814
73047
  if (testStrategy === "no-test") {
72815
73048
  if (!rawJustification || typeof rawJustification !== "string" || rawJustification.trim() === "") {
72816
73049
  throw new Error(`[schema] story[${index}].routing.noTestJustification is required when testStrategy is "no-test"`);
72817
73050
  }
72818
73051
  }
73052
+ if (testStrategy !== "no-test" && typeof rawJustification === "string" && rawJustification.trim() !== "") {
73053
+ testStrategy = "no-test";
73054
+ }
72819
73055
  const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
72820
73056
  const rawDeps = s.dependencies;
72821
73057
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
@@ -72975,7 +73211,7 @@ async function planCommand(workdir, config2, options) {
72975
73211
  let rawResponse;
72976
73212
  const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
72977
73213
  if (debateEnabled) {
72978
- const basePrompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
73214
+ const { taskContext: planTaskContext, outputFormat: planOutputFormat } = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
72979
73215
  const resolvedPerm = resolvePermissions(config2, "plan");
72980
73216
  const planStageConfig = config2?.debate?.stages.plan;
72981
73217
  const debateSession = _planDeps.createDebateSession({
@@ -72992,7 +73228,7 @@ async function planCommand(workdir, config2, options) {
72992
73228
  rounds: planStageConfig.rounds,
72993
73229
  feature: options.feature
72994
73230
  });
72995
- const debateResult = await debateSession.runPlan(basePrompt, {
73231
+ const debateResult = await debateSession.runPlan(planTaskContext, planOutputFormat, {
72996
73232
  workdir,
72997
73233
  feature: options.feature,
72998
73234
  outputDir,
@@ -73011,7 +73247,10 @@ async function planCommand(workdir, config2, options) {
73011
73247
  }
73012
73248
  } else if (options.auto) {
73013
73249
  const isAcp = config2?.agent?.protocol === "acp";
73014
- const prompt = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
73250
+ const { taskContext: autoTaskCtx, outputFormat: autoOutputFmt } = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
73251
+ const prompt = `${autoTaskCtx}
73252
+
73253
+ ${autoOutputFmt}`;
73015
73254
  const adapter = _planDeps.getAgent(agentName, config2);
73016
73255
  if (!adapter)
73017
73256
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73097,7 +73336,10 @@ async function planCommand(workdir, config2, options) {
73097
73336
  rawResponse = await runInteractivePlan();
73098
73337
  }
73099
73338
  async function runInteractivePlan() {
73100
- const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
73339
+ const { taskContext: interactiveTaskCtx, outputFormat: interactiveOutputFmt } = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
73340
+ const prompt = `${interactiveTaskCtx}
73341
+
73342
+ ${interactiveOutputFmt}`;
73101
73343
  const adapter = _planDeps.getAgent(agentName, config2);
73102
73344
  if (!adapter)
73103
73345
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
@@ -73304,7 +73546,7 @@ ${packageDetailsSection}
73304
73546
  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
73547
  const workdirField = isMonorepo ? `
73306
73548
  "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.
73549
+ const taskContext = `You are a senior software architect generating a product requirements document (PRD) as JSON.
73308
73550
 
73309
73551
  ## Step 1: Understand the Spec
73310
73552
 
@@ -73332,6 +73574,8 @@ If this is a greenfield project (empty or minimal codebase):
73332
73574
 
73333
73575
  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
73576
 
73577
+ **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.
73578
+
73335
73579
  ## Codebase Context
73336
73580
 
73337
73581
  ${codebaseContext}${monorepoHint}
@@ -73348,9 +73592,8 @@ For each story, set "contextFiles" to the key source files the agent should read
73348
73592
 
73349
73593
  ${COMPLEXITY_GUIDE}
73350
73594
 
73351
- ${TEST_STRATEGY_GUIDE}
73352
-
73353
- ## Output Schema
73595
+ ${TEST_STRATEGY_GUIDE}`;
73596
+ const outputFormat = `## Output Schema
73354
73597
 
73355
73598
  Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
73356
73599
 
@@ -73386,6 +73629,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
73386
73629
 
73387
73630
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
73388
73631
  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."}`;
73632
+ return { taskContext, outputFormat };
73389
73633
  }
73390
73634
  async function planDecomposeCommand(workdir, config2, options) {
73391
73635
  const prdPath = join11(workdir, ".nax", "features", options.feature, "prd.json");