@nathapp/nax 0.59.0 → 0.59.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +612 -204
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -18269,7 +18269,16 @@ var init_schemas3 = __esm(() => {
18269
18269
  modelTier: ModelTierSchema.default("balanced"),
18270
18270
  rules: exports_external.array(exports_external.string()).default([]),
18271
18271
  timeoutMs: exports_external.number().int().positive().default(600000),
18272
- excludePatterns: exports_external.array(exports_external.string()).default([":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"])
18272
+ excludePatterns: exports_external.array(exports_external.string()).default([
18273
+ ":!test/",
18274
+ ":!tests/",
18275
+ ":!*_test.go",
18276
+ ":!*.test.ts",
18277
+ ":!*.spec.ts",
18278
+ ":!**/__tests__/",
18279
+ ":!.nax/",
18280
+ ":!.nax-pids"
18281
+ ])
18273
18282
  });
18274
18283
  ReviewDialogueConfigSchema = exports_external.object({
18275
18284
  enabled: exports_external.boolean().default(false),
@@ -18614,7 +18623,16 @@ var init_schemas3 = __esm(() => {
18614
18623
  modelTier: "balanced",
18615
18624
  rules: [],
18616
18625
  timeoutMs: 600000,
18617
- excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
18626
+ excludePatterns: [
18627
+ ":!test/",
18628
+ ":!tests/",
18629
+ ":!*_test.go",
18630
+ ":!*.test.ts",
18631
+ ":!*.spec.ts",
18632
+ ":!**/__tests__/",
18633
+ ":!.nax/",
18634
+ ":!.nax-pids"
18635
+ ]
18618
18636
  },
18619
18637
  dialogue: {
18620
18638
  enabled: false,
@@ -19242,11 +19260,15 @@ class AcpAgentAdapter {
19242
19260
  }
19243
19261
  runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
19244
19262
  } finally {
19263
+ const isSessionBroken = !runState.succeeded && lastResponse?.stopReason === "error";
19245
19264
  if (runState.succeeded && !options.keepSessionOpen) {
19246
19265
  await closeAcpSession(session);
19247
19266
  if (options.featureName && options.storyId) {
19248
19267
  await clearAcpSession(options.workdir, options.featureName, options.storyId, options.sessionRole);
19249
19268
  }
19269
+ } else if (isSessionBroken) {
19270
+ getSafeLogger()?.debug("acp-adapter", "Closing broken session for retry", { sessionName });
19271
+ await closeAcpSession(session);
19250
19272
  } else if (!runState.succeeded) {
19251
19273
  getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
19252
19274
  } else {
@@ -19495,7 +19517,7 @@ class AcpAgentAdapter {
19495
19517
  this.markUnavailable(this.name);
19496
19518
  throw new Error("[acp-adapter] plan() returned empty spec content");
19497
19519
  }
19498
- return { specContent };
19520
+ return { specContent, costUsd: result.estimatedCost };
19499
19521
  }
19500
19522
  async decompose(options) {
19501
19523
  const model = options.modelDef?.model;
@@ -21453,7 +21475,10 @@ function majorityResolver(proposals, failOpen) {
21453
21475
  return passCount > failCount ? "passed" : "failed";
21454
21476
  }
21455
21477
  async function synthesisResolver(proposals, critiques, opts) {
21456
- const prompt = buildSynthesisPrompt(proposals, critiques);
21478
+ const base = buildSynthesisPrompt(proposals, critiques);
21479
+ const prompt = opts.promptSuffix ? `${base}
21480
+
21481
+ ${opts.promptSuffix}` : base;
21457
21482
  return opts.adapter.complete(prompt, opts.completeOptions);
21458
21483
  }
21459
21484
  async function judgeResolver(proposals, critiques, resolverConfig, opts) {
@@ -21521,25 +21546,89 @@ function pipelineStageForDebate(stage) {
21521
21546
  }
21522
21547
  }
21523
21548
  function resolveModelDefForDebater(debater, tier, config2) {
21524
- if (debater.model && !isTierLabel(debater.model)) {
21525
- return resolveModel(debater.model);
21549
+ const modelOverride = debater.model;
21550
+ let effectiveTier = tier;
21551
+ if (modelOverride) {
21552
+ const aliasedTier = MODEL_SHORTHAND_TIERS[modelOverride.toLowerCase()];
21553
+ if (aliasedTier) {
21554
+ effectiveTier = aliasedTier;
21555
+ } else if (!isTierLabel(modelOverride)) {
21556
+ return resolveModel(modelOverride);
21557
+ }
21526
21558
  }
21527
21559
  const configModels = config2?.models ?? DEFAULT_CONFIG.models;
21528
21560
  const configDefaultAgent = config2?.autoMode?.defaultAgent ?? DEFAULT_CONFIG.autoMode.defaultAgent;
21529
21561
  try {
21530
- return resolveModelForAgent(configModels, debater.agent, tier, configDefaultAgent);
21562
+ return resolveModelForAgent(configModels, debater.agent, effectiveTier, configDefaultAgent);
21531
21563
  } catch {}
21532
21564
  try {
21533
- return resolveModelForAgent(DEFAULT_CONFIG.models, DEFAULT_CONFIG.autoMode.defaultAgent, tier, DEFAULT_CONFIG.autoMode.defaultAgent);
21565
+ return resolveModelForAgent(DEFAULT_CONFIG.models, DEFAULT_CONFIG.autoMode.defaultAgent, effectiveTier, DEFAULT_CONFIG.autoMode.defaultAgent);
21534
21566
  } catch {
21535
21567
  return resolveModelForAgent(configModels, debater.agent, "fast", configDefaultAgent);
21536
21568
  }
21537
21569
  }
21538
- async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName) {
21570
+ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext, promptSuffix) {
21539
21571
  const resolverConfig = stageConfig.resolver;
21540
21572
  const logger = _debateSessionDeps.getSafeLogger();
21573
+ if (reviewerSession && resolverContext) {
21574
+ try {
21575
+ const debateCtx = {
21576
+ resolverType: resolverConfig.type
21577
+ };
21578
+ if (resolverConfig.type === "majority-fail-closed" || resolverConfig.type === "majority-fail-open") {
21579
+ const failOpen = resolverConfig.type === "majority-fail-open";
21580
+ const rawOutcome = majorityResolver(proposalOutputs, failOpen);
21581
+ let passCount = 0;
21582
+ let failCount = 0;
21583
+ 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
+ }
21599
+ }
21600
+ debateCtx.majorityVote = { passed: rawOutcome === "passed", passCount, failCount };
21601
+ }
21602
+ const story = {
21603
+ id: resolverContext.story.id,
21604
+ title: resolverContext.story.title,
21605
+ description: "",
21606
+ acceptanceCriteria: resolverContext.story.acceptanceCriteria
21607
+ };
21608
+ let dialogueResult;
21609
+ if (resolverContext.isReReview) {
21610
+ dialogueResult = await reviewerSession.reReviewDebate(resolverContext.labeledProposals, critiqueOutputs, resolverContext.diff, debateCtx);
21611
+ } else {
21612
+ dialogueResult = await reviewerSession.resolveDebate(resolverContext.labeledProposals, critiqueOutputs, resolverContext.diff, story, resolverContext.semanticConfig, debateCtx);
21613
+ }
21614
+ const outcome = dialogueResult.checkResult.success ? "passed" : "failed";
21615
+ return {
21616
+ outcome,
21617
+ resolverCostUsd: dialogueResult.cost ?? 0,
21618
+ dialogueResult
21619
+ };
21620
+ } catch (err) {
21621
+ logger?.warn("debate", "ReviewerSession.resolveDebate() failed \u2014 falling back to stateless resolver", {
21622
+ storyId,
21623
+ error: err instanceof Error ? err.message : String(err)
21624
+ });
21625
+ }
21626
+ }
21627
+ if (reviewerSession && !resolverContext) {
21628
+ logger?.warn("debate", "ReviewerSession provided but resolverContext is undefined \u2014 falling back to stateless resolver", { storyId });
21629
+ }
21541
21630
  if (resolverConfig.type === "majority-fail-closed" || resolverConfig.type === "majority-fail-open") {
21542
- if (workdir !== undefined) {
21631
+ if (workdir !== undefined && !reviewerSession) {
21543
21632
  logger?.warn("debate", "majority resolver does not support implementer session resumption \u2014 switch to synthesis or custom resolver for context-aware semantic review");
21544
21633
  }
21545
21634
  return {
@@ -21547,31 +21636,36 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21547
21636
  resolverCostUsd: 0
21548
21637
  };
21549
21638
  }
21550
- const implementerSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "implementer") : undefined;
21551
21639
  if (resolverConfig.type === "synthesis") {
21552
21640
  const agentName = resolverConfig.agent ?? RESOLVER_FALLBACK_AGENT;
21553
21641
  const adapter = _debateSessionDeps.getAgent(agentName, config2);
21554
21642
  if (adapter) {
21643
+ const synthesisSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "synthesis") : undefined;
21555
21644
  const resolverResult = await synthesisResolver(proposalOutputs, critiqueOutputs, {
21556
21645
  adapter,
21646
+ promptSuffix,
21557
21647
  completeOptions: {
21558
21648
  model: resolveDebaterModel({ agent: agentName }, config2),
21559
21649
  config: config2,
21560
21650
  storyId,
21651
+ featureName,
21652
+ workdir,
21561
21653
  sessionRole: "synthesis",
21562
21654
  timeoutMs,
21563
- ...implementerSessionName !== undefined && { sessionName: implementerSessionName }
21655
+ ...synthesisSessionName !== undefined && { sessionName: synthesisSessionName }
21564
21656
  }
21565
21657
  });
21566
21658
  return {
21567
21659
  outcome: "passed",
21568
- resolverCostUsd: resolverResult.costUsd
21660
+ resolverCostUsd: resolverResult.costUsd,
21661
+ output: resolverResult.output
21569
21662
  };
21570
21663
  }
21571
21664
  return { outcome: "passed", resolverCostUsd: 0 };
21572
21665
  }
21573
21666
  if (resolverConfig.type === "custom") {
21574
21667
  const agentName = resolverConfig.agent ?? RESOLVER_FALLBACK_AGENT;
21668
+ const judgeSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "judge") : undefined;
21575
21669
  const resolverResult = await judgeResolver(proposalOutputs, critiqueOutputs, resolverConfig, {
21576
21670
  getAgent: (name) => _debateSessionDeps.getAgent(name, config2),
21577
21671
  defaultAgentName: RESOLVER_FALLBACK_AGENT,
@@ -21579,19 +21673,22 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21579
21673
  model: resolveDebaterModel({ agent: agentName }, config2),
21580
21674
  config: config2,
21581
21675
  storyId,
21676
+ featureName,
21677
+ workdir,
21582
21678
  sessionRole: "judge",
21583
21679
  timeoutMs,
21584
- ...implementerSessionName !== undefined && { sessionName: implementerSessionName }
21680
+ ...judgeSessionName !== undefined && { sessionName: judgeSessionName }
21585
21681
  }
21586
21682
  });
21587
21683
  return {
21588
21684
  outcome: "passed",
21589
- resolverCostUsd: resolverResult.costUsd
21685
+ resolverCostUsd: resolverResult.costUsd,
21686
+ output: resolverResult.output
21590
21687
  };
21591
21688
  }
21592
21689
  return { outcome: "passed", resolverCostUsd: 0 };
21593
21690
  }
21594
- var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
21691
+ var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps, MODEL_SHORTHAND_TIERS;
21595
21692
  var init_session_helpers = __esm(() => {
21596
21693
  init_adapter();
21597
21694
  init_registry();
@@ -21603,6 +21700,11 @@ var init_session_helpers = __esm(() => {
21603
21700
  getSafeLogger,
21604
21701
  readFile: (path) => Bun.file(path).text()
21605
21702
  };
21703
+ MODEL_SHORTHAND_TIERS = {
21704
+ haiku: "fast",
21705
+ sonnet: "balanced",
21706
+ opus: "powerful"
21707
+ };
21606
21708
  });
21607
21709
 
21608
21710
  // src/debate/concurrency.ts
@@ -21788,7 +21890,11 @@ async function runStateful(ctx, prompt) {
21788
21890
  critiqueOutputs = critiqueSettled.filter((r) => r.status === "fulfilled").map((r) => r.value.output);
21789
21891
  }
21790
21892
  const proposalOutputs = successfulProposals.map((s) => s.output);
21791
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName);
21893
+ const fullResolverContext = ctx.resolverContextInput ? {
21894
+ ...ctx.resolverContextInput,
21895
+ labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
21896
+ } : undefined;
21897
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
21792
21898
  totalCostUsd += outcome.resolverCostUsd;
21793
21899
  const proposals = successfulProposals.map((s) => ({
21794
21900
  debater: s.debater,
@@ -21815,6 +21921,50 @@ var init_session_stateful = __esm(() => {
21815
21921
  });
21816
21922
 
21817
21923
  // src/debate/session-hybrid.ts
21924
+ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix) {
21925
+ const logger = _debateSessionDeps.getSafeLogger();
21926
+ const config2 = ctx.stageConfig;
21927
+ const rebuttals = [];
21928
+ let costUsd = 0;
21929
+ const proposalList = proposals.map((s) => ({ debater: s.debater, output: s.output }));
21930
+ try {
21931
+ for (let round = 1;round <= config2.rounds; round++) {
21932
+ const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
21933
+ for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21934
+ const proposal = proposals[debaterIdx];
21935
+ const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
21936
+ logger?.info("debate:rebuttal-start", "debate:rebuttal-start", {
21937
+ storyId: ctx.storyId,
21938
+ round,
21939
+ debaterIndex: debaterIdx
21940
+ });
21941
+ const rebuttalPrompt = buildRebuttalContext(originalPrompt, proposalList, priorRebuttals, debaterIdx);
21942
+ try {
21943
+ const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21944
+ costUsd += turnResult.cost;
21945
+ rebuttals.push({ debater: proposal.debater, round, output: turnResult.output });
21946
+ } catch (err) {
21947
+ logger?.warn("debate", "debate:rebuttal-failed", {
21948
+ storyId: ctx.storyId,
21949
+ round,
21950
+ debaterIndex: debaterIdx,
21951
+ error: err instanceof Error ? err.message : String(err)
21952
+ });
21953
+ }
21954
+ }
21955
+ }
21956
+ } finally {
21957
+ for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21958
+ const proposal = proposals[debaterIdx];
21959
+ const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
21960
+ try {
21961
+ const closeCost = await closeStatefulSession(ctx, proposal.adapter, proposal.debater, sessionRole);
21962
+ costUsd += closeCost;
21963
+ } catch {}
21964
+ }
21965
+ }
21966
+ return { rebuttals, costUsd };
21967
+ }
21818
21968
  async function runHybrid(ctx, prompt) {
21819
21969
  const logger = _debateSessionDeps.getSafeLogger();
21820
21970
  const config2 = ctx.stageConfig;
@@ -21883,45 +22033,14 @@ async function runHybrid(ctx, prompt) {
21883
22033
  }
21884
22034
  const proposalOutputs = successfulProposals.map((s) => s.output);
21885
22035
  const proposalList = successfulProposals.map((s) => ({ debater: s.debater, output: s.output }));
21886
- const rebuttals = [];
21887
- try {
21888
- for (let round = 1;round <= config2.rounds; round++) {
21889
- const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
21890
- for (let debaterIdx = 0;debaterIdx < successfulProposals.length; debaterIdx++) {
21891
- const proposal = successfulProposals[debaterIdx];
21892
- const sessionRole = `debate-hybrid-${debaterIdx}`;
21893
- logger?.info("debate:rebuttal-start", "debate:rebuttal-start", {
21894
- storyId: ctx.storyId,
21895
- round,
21896
- debaterIndex: debaterIdx
21897
- });
21898
- const rebuttalPrompt = buildRebuttalContext(prompt, proposalList, priorRebuttals, debaterIdx);
21899
- try {
21900
- const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21901
- totalCostUsd += turnResult.cost;
21902
- rebuttals.push({ debater: proposal.debater, round, output: turnResult.output });
21903
- } catch (err) {
21904
- logger?.warn("debate", "debate:rebuttal-failed", {
21905
- storyId: ctx.storyId,
21906
- round,
21907
- debaterIndex: debaterIdx,
21908
- error: err instanceof Error ? err.message : String(err)
21909
- });
21910
- }
21911
- }
21912
- }
21913
- } finally {
21914
- for (let debaterIdx = 0;debaterIdx < successfulProposals.length; debaterIdx++) {
21915
- const proposal = successfulProposals[debaterIdx];
21916
- const sessionRole = `debate-hybrid-${debaterIdx}`;
21917
- try {
21918
- const closeCost = await closeStatefulSession(ctx, proposal.adapter, proposal.debater, sessionRole);
21919
- totalCostUsd += closeCost;
21920
- } catch {}
21921
- }
21922
- }
22036
+ const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, prompt, "debate-hybrid");
22037
+ totalCostUsd += rebuttalCost;
21923
22038
  const critiqueOutputs = rebuttals.map((r) => r.output);
21924
- const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000);
22039
+ const fullResolverContext = ctx.resolverContextInput ? {
22040
+ ...ctx.resolverContextInput,
22041
+ labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
22042
+ } : undefined;
22043
+ const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
21925
22044
  totalCostUsd += resolveResult.resolverCostUsd;
21926
22045
  return {
21927
22046
  storyId: ctx.storyId,
@@ -22064,7 +22183,11 @@ async function runOneShot(ctx, prompt) {
22064
22183
  critiqueOutputs = critiqueSettled.filter((r) => r.status === "fulfilled").map((r) => r.value.output);
22065
22184
  }
22066
22185
  const proposalOutputs = successful.map((p) => p.output);
22067
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs);
22186
+ const fullResolverContext = ctx.resolverContextInput ? {
22187
+ ...ctx.resolverContextInput,
22188
+ labeledProposals: successful.map((p) => ({ debater: p.debater.agent, output: p.output }))
22189
+ } : undefined;
22190
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
22068
22191
  totalCostUsd += outcome.resolverCostUsd;
22069
22192
  const proposals = successful.map((p) => ({
22070
22193
  debater: p.debater,
@@ -22096,7 +22219,7 @@ async function runPlan2(ctx, basePrompt, opts) {
22096
22219
  const logger = _debateSessionDeps.getSafeLogger();
22097
22220
  const config2 = ctx.stageConfig;
22098
22221
  const debaters = config2.debaters ?? [];
22099
- const totalCostUsd = 0;
22222
+ let totalCostUsd = 0;
22100
22223
  const resolved = [];
22101
22224
  for (const debater of debaters) {
22102
22225
  const adapter = _debateSessionDeps.getAgent(debater.agent, ctx.config);
@@ -22119,13 +22242,16 @@ async function runPlan2(ctx, basePrompt, opts) {
22119
22242
 
22120
22243
  Write the PRD JSON directly to this file path: ${tempOutputPath}
22121
22244
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.`;
22122
- await adapter.plan({
22245
+ const modelTier = modelTierFromDebater(debater);
22246
+ const modelDef = resolveModelDefForDebater(debater, modelTier, ctx.config);
22247
+ const planResult = await adapter.plan({
22123
22248
  prompt: debaterPrompt,
22124
22249
  workdir: opts.workdir,
22125
22250
  interactive: false,
22126
22251
  timeoutSeconds: opts.timeoutSeconds,
22127
22252
  config: ctx.config,
22128
- modelTier: debater.model ?? "balanced",
22253
+ modelTier,
22254
+ modelDef,
22129
22255
  dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
22130
22256
  maxInteractionTurns: opts.maxInteractionTurns,
22131
22257
  featureName: opts.feature,
@@ -22133,13 +22259,14 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22133
22259
  sessionRole: `plan-${i}`
22134
22260
  });
22135
22261
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22136
- return { debater, adapter, output, cost: 0 };
22262
+ return { debater, adapter, output, cost: planResult.costUsd ?? 0 };
22137
22263
  }), concurrencyLimit);
22138
22264
  const successful = [];
22139
22265
  for (let i = 0;i < settled.length; i++) {
22140
22266
  const res = settled[i];
22141
22267
  if (res.status === "fulfilled") {
22142
22268
  successful.push(res.value);
22269
+ totalCostUsd += res.value.cost;
22143
22270
  } else {
22144
22271
  const { debater } = resolved[i];
22145
22272
  logger?.warn("debate", "debate:debater-failed", {
@@ -22191,9 +22318,31 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22191
22318
  };
22192
22319
  }
22193
22320
  const proposalOutputs = successful.map((p) => p.output);
22321
+ const mode = ctx.stageConfig.mode ?? "panel";
22322
+ const sessionMode = ctx.stageConfig.sessionMode ?? "one-shot";
22323
+ let critiqueOutputs = [];
22324
+ let rebuttalList;
22325
+ if (mode === "hybrid" && sessionMode === "stateful") {
22326
+ const hybridCtx = {
22327
+ storyId: ctx.storyId,
22328
+ stage: ctx.stage,
22329
+ stageConfig: ctx.stageConfig,
22330
+ config: ctx.config,
22331
+ workdir: opts.workdir,
22332
+ featureName: opts.feature,
22333
+ timeoutSeconds: opts.timeoutSeconds ?? 600
22334
+ };
22335
+ const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, basePrompt, "plan-hybrid");
22336
+ critiqueOutputs = rebuttals.map((r) => r.output);
22337
+ rebuttalList = rebuttals;
22338
+ totalCostUsd += costUsd;
22339
+ } else if (mode === "hybrid") {
22340
+ logger?.warn("debate", "hybrid mode requires sessionMode: stateful for plan \u2014 running as panel");
22341
+ }
22194
22342
  const resolverTimeoutMs = (ctx.stageConfig.timeoutSeconds ?? 600) * 1000;
22195
- const outcome = await resolveOutcome(proposalOutputs, [], ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs);
22196
- const winningOutput = successful[0].output;
22343
+ 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);
22345
+ const winningOutput = outcome.output ?? successful[0].output;
22197
22346
  const proposals = successful.map((p) => ({ debater: p.debater, output: p.output }));
22198
22347
  logger?.info("debate", "debate:result", {
22199
22348
  storyId: ctx.storyId,
@@ -22204,16 +22353,18 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22204
22353
  storyId: ctx.storyId,
22205
22354
  stage: ctx.stage,
22206
22355
  outcome: outcome.outcome,
22207
- rounds: 1,
22356
+ rounds: rebuttalList ? config2.rounds : 1,
22208
22357
  debaters: successful.map((p) => p.debater.agent),
22209
22358
  resolverType: config2.resolver.type,
22210
22359
  proposals,
22360
+ rebuttals: rebuttalList,
22211
22361
  output: winningOutput,
22212
22362
  totalCostUsd
22213
22363
  };
22214
22364
  }
22215
22365
  var init_session_plan = __esm(() => {
22216
22366
  init_session_helpers();
22367
+ init_session_hybrid();
22217
22368
  });
22218
22369
 
22219
22370
  // src/debate/session.ts
@@ -22225,6 +22376,8 @@ class DebateSession {
22225
22376
  workdir;
22226
22377
  featureName;
22227
22378
  timeoutSeconds;
22379
+ reviewerSession;
22380
+ resolverContextInput;
22228
22381
  get timeoutMs() {
22229
22382
  return this.timeoutSeconds * 1000;
22230
22383
  }
@@ -22236,6 +22389,8 @@ class DebateSession {
22236
22389
  this.workdir = opts.workdir ?? process.cwd();
22237
22390
  this.featureName = opts.featureName ?? opts.stage;
22238
22391
  this.timeoutSeconds = opts.timeoutSeconds ?? opts.stageConfig.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
22392
+ this.reviewerSession = opts.reviewerSession;
22393
+ this.resolverContextInput = opts.resolverContextInput;
22239
22394
  }
22240
22395
  async run(prompt) {
22241
22396
  const sessionMode = this.stageConfig.sessionMode ?? "one-shot";
@@ -22249,7 +22404,9 @@ class DebateSession {
22249
22404
  config: this.config,
22250
22405
  workdir: this.workdir,
22251
22406
  featureName: this.featureName,
22252
- timeoutSeconds: this.timeoutSeconds
22407
+ timeoutSeconds: this.timeoutSeconds,
22408
+ reviewerSession: this.reviewerSession,
22409
+ resolverContextInput: this.resolverContextInput
22253
22410
  }, prompt);
22254
22411
  }
22255
22412
  const logger = _debateSessionDeps.getSafeLogger();
@@ -22259,7 +22416,11 @@ class DebateSession {
22259
22416
  stage: this.stage,
22260
22417
  stageConfig: this.stageConfig,
22261
22418
  config: this.config,
22262
- timeoutMs: this.timeoutMs
22419
+ timeoutMs: this.timeoutMs,
22420
+ workdir: this.workdir,
22421
+ featureName: this.featureName,
22422
+ reviewerSession: this.reviewerSession,
22423
+ resolverContextInput: this.resolverContextInput
22263
22424
  }, prompt);
22264
22425
  }
22265
22426
  if (sessionMode === "stateful") {
@@ -22270,7 +22431,9 @@ class DebateSession {
22270
22431
  config: this.config,
22271
22432
  workdir: this.workdir,
22272
22433
  featureName: this.featureName,
22273
- timeoutSeconds: this.timeoutSeconds
22434
+ timeoutSeconds: this.timeoutSeconds,
22435
+ reviewerSession: this.reviewerSession,
22436
+ resolverContextInput: this.resolverContextInput
22274
22437
  }, prompt);
22275
22438
  }
22276
22439
  return runOneShot({
@@ -22278,7 +22441,11 @@ class DebateSession {
22278
22441
  stage: this.stage,
22279
22442
  stageConfig: this.stageConfig,
22280
22443
  config: this.config,
22281
- timeoutMs: this.timeoutMs
22444
+ timeoutMs: this.timeoutMs,
22445
+ workdir: this.workdir,
22446
+ featureName: this.featureName,
22447
+ reviewerSession: this.reviewerSession,
22448
+ resolverContextInput: this.resolverContextInput
22282
22449
  }, prompt);
22283
22450
  }
22284
22451
  async runPlan(basePrompt, opts) {
@@ -22613,30 +22780,41 @@ class CLIInteractionPlugin {
22613
22780
  }
22614
22781
  async send(request) {
22615
22782
  this.pendingRequests.set(request.id, request);
22616
- console.log(`
22617
- ${"=".repeat(80)}`);
22618
- console.log(`[INTERACTION] ${request.stage.toUpperCase()} \u2014 ${request.type.toUpperCase()}`);
22619
- console.log("=".repeat(80));
22620
- console.log(`
22783
+ process.stdout.write(`
22784
+ ${"=".repeat(80)}
22785
+ `);
22786
+ process.stdout.write(`[INTERACTION] ${request.stage.toUpperCase()} \u2014 ${request.type.toUpperCase()}
22787
+ `);
22788
+ process.stdout.write(`${"=".repeat(80)}
22789
+ `);
22790
+ process.stdout.write(`
22621
22791
  ${request.summary}
22792
+
22622
22793
  `);
22623
22794
  if (request.detail) {
22624
- console.log(request.detail);
22625
- console.log("");
22795
+ process.stdout.write(`${request.detail}
22796
+ `);
22797
+ process.stdout.write(`
22798
+ `);
22626
22799
  }
22627
22800
  if (request.options && request.options.length > 0) {
22628
- console.log("Options:");
22801
+ process.stdout.write(`Options:
22802
+ `);
22629
22803
  for (const opt of request.options) {
22630
22804
  const desc = opt.description ? ` \u2014 ${opt.description}` : "";
22631
- console.log(` [${opt.key}] ${opt.label}${desc}`);
22805
+ process.stdout.write(` [${opt.key}] ${opt.label}${desc}
22806
+ `);
22632
22807
  }
22633
- console.log("");
22808
+ process.stdout.write(`
22809
+ `);
22634
22810
  }
22635
22811
  if (request.timeout) {
22636
22812
  const timeoutSec = Math.floor(request.timeout / 1000);
22637
- console.log(`[Timeout: ${timeoutSec}s | Fallback: ${request.fallback}]`);
22813
+ process.stdout.write(`[Timeout: ${timeoutSec}s | Fallback: ${request.fallback}]
22814
+ `);
22638
22815
  }
22639
- console.log(`${"=".repeat(80)}
22816
+ process.stdout.write(`${"=".repeat(80)}
22817
+
22640
22818
  `);
22641
22819
  }
22642
22820
  async receive(requestId, timeout = 60000) {
@@ -26414,59 +26592,6 @@ ${stderr}` };
26414
26592
  };
26415
26593
  });
26416
26594
 
26417
- // src/agents/claude/index.ts
26418
- var init_claude = __esm(() => {
26419
- init_adapter3();
26420
- init_execution();
26421
- });
26422
-
26423
- // src/agents/shared/validation.ts
26424
- function validateAgentForTier(agent, tier) {
26425
- return agent.capabilities.supportedTiers.includes(tier);
26426
- }
26427
- function validateAgentFeature(agent, feature) {
26428
- return agent.capabilities.features.has(feature);
26429
- }
26430
- function describeAgentCapabilities(agent) {
26431
- const tiers = agent.capabilities.supportedTiers.join(",");
26432
- const features = Array.from(agent.capabilities.features).join(",");
26433
- const maxTokens = agent.capabilities.maxContextTokens;
26434
- return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
26435
- }
26436
-
26437
- // src/agents/index.ts
26438
- var exports_agents = {};
26439
- __export(exports_agents, {
26440
- validateAgentForTier: () => validateAgentForTier,
26441
- validateAgentFeature: () => validateAgentFeature,
26442
- parseTokenUsage: () => parseTokenUsage,
26443
- getInstalledAgents: () => getInstalledAgents,
26444
- getAllAgentNames: () => getAllAgentNames,
26445
- getAgentVersions: () => getAgentVersions,
26446
- getAgentVersion: () => getAgentVersion,
26447
- getAgent: () => getAgent,
26448
- formatCostWithConfidence: () => formatCostWithConfidence,
26449
- estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
26450
- estimateCostFromOutput: () => estimateCostFromOutput,
26451
- estimateCostByDuration: () => estimateCostByDuration,
26452
- estimateCost: () => estimateCost,
26453
- describeAgentCapabilities: () => describeAgentCapabilities,
26454
- checkAgentHealth: () => checkAgentHealth,
26455
- MODEL_PRICING: () => MODEL_PRICING,
26456
- CompleteError: () => CompleteError,
26457
- ClaudeCodeAdapter: () => ClaudeCodeAdapter,
26458
- COST_RATES: () => COST_RATES,
26459
- AllAgentsUnavailableError: () => AllAgentsUnavailableError
26460
- });
26461
- var init_agents = __esm(() => {
26462
- init_types2();
26463
- init_claude();
26464
- init_registry();
26465
- init_cost();
26466
- init_version_detection();
26467
- init_errors();
26468
- });
26469
-
26470
26595
  // src/quality/runner.ts
26471
26596
  var {spawn: spawn2 } = globalThis.Bun;
26472
26597
  async function runQualityCommand(opts) {
@@ -26769,7 +26894,60 @@ Do NOT add new features \u2014 only fix the identified issues.
26769
26894
  Commit your fixes when done.${scopeConstraint}`;
26770
26895
  }
26771
26896
 
26772
- // src/review/dialogue.ts
26897
+ // src/agents/claude/index.ts
26898
+ var init_claude = __esm(() => {
26899
+ init_adapter3();
26900
+ init_execution();
26901
+ });
26902
+
26903
+ // src/agents/shared/validation.ts
26904
+ function validateAgentForTier(agent, tier) {
26905
+ return agent.capabilities.supportedTiers.includes(tier);
26906
+ }
26907
+ function validateAgentFeature(agent, feature) {
26908
+ return agent.capabilities.features.has(feature);
26909
+ }
26910
+ function describeAgentCapabilities(agent) {
26911
+ const tiers = agent.capabilities.supportedTiers.join(",");
26912
+ const features = Array.from(agent.capabilities.features).join(",");
26913
+ const maxTokens = agent.capabilities.maxContextTokens;
26914
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
26915
+ }
26916
+
26917
+ // src/agents/index.ts
26918
+ var exports_agents = {};
26919
+ __export(exports_agents, {
26920
+ validateAgentForTier: () => validateAgentForTier,
26921
+ validateAgentFeature: () => validateAgentFeature,
26922
+ parseTokenUsage: () => parseTokenUsage,
26923
+ getInstalledAgents: () => getInstalledAgents,
26924
+ getAllAgentNames: () => getAllAgentNames,
26925
+ getAgentVersions: () => getAgentVersions,
26926
+ getAgentVersion: () => getAgentVersion,
26927
+ getAgent: () => getAgent,
26928
+ formatCostWithConfidence: () => formatCostWithConfidence,
26929
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
26930
+ estimateCostFromOutput: () => estimateCostFromOutput,
26931
+ estimateCostByDuration: () => estimateCostByDuration,
26932
+ estimateCost: () => estimateCost,
26933
+ describeAgentCapabilities: () => describeAgentCapabilities,
26934
+ checkAgentHealth: () => checkAgentHealth,
26935
+ MODEL_PRICING: () => MODEL_PRICING,
26936
+ CompleteError: () => CompleteError,
26937
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
26938
+ COST_RATES: () => COST_RATES,
26939
+ AllAgentsUnavailableError: () => AllAgentsUnavailableError
26940
+ });
26941
+ var init_agents = __esm(() => {
26942
+ init_types2();
26943
+ init_claude();
26944
+ init_registry();
26945
+ init_cost();
26946
+ init_version_detection();
26947
+ init_errors();
26948
+ });
26949
+
26950
+ // src/review/dialogue-prompts.ts
26773
26951
  function buildReviewPrompt(diff, story, _semanticConfig) {
26774
26952
  const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
26775
26953
  `);
@@ -26803,6 +26981,98 @@ function buildReReviewPrompt(updatedDiff, previousFindings) {
26803
26981
  ].join(`
26804
26982
  `);
26805
26983
  }
26984
+ function buildProposalsSection(proposals) {
26985
+ return proposals.map((p) => `### ${p.debater}
26986
+ ${p.output}`).join(`
26987
+
26988
+ `);
26989
+ }
26990
+ function buildCritiquesSection(critiques) {
26991
+ if (critiques.length === 0)
26992
+ return "";
26993
+ return `
26994
+
26995
+ ## Critiques
26996
+ ${critiques.map((c, i) => `### Critique ${i + 1}
26997
+ ${c}`).join(`
26998
+
26999
+ `)}`;
27000
+ }
27001
+ function buildVoteTallyLine(ctx) {
27002
+ if (!ctx.majorityVote)
27003
+ return "";
27004
+ const { passCount, failCount } = ctx.majorityVote;
27005
+ const failOpenNote = ctx.resolverType === "majority-fail-open" ? " (unparseable proposals count as pass)" : " (unparseable proposals count as fail)";
27006
+ return `
27007
+
27008
+ The preliminary majority vote is: **${passCount} passed, ${failCount} failed**${failOpenNote}. Verify the failing findings with tools before giving your authoritative verdict.`;
27009
+ }
27010
+ function buildResolverFraming(ctx) {
27011
+ switch (ctx.resolverType) {
27012
+ case "majority-fail-closed":
27013
+ case "majority-fail-open":
27014
+ return "You are the authoritative reviewer resolving a debate. A preliminary vote was taken \u2014 see tally below. Verify disputed findings using tools (READ files, GREP for usage) and give your final verdict.";
27015
+ case "synthesis":
27016
+ return "You are a synthesis reviewer. Synthesize the debater proposals into a single, coherent, tool-verified verdict. Use READ and GREP to verify claims before ruling.";
27017
+ case "custom":
27018
+ return "You are the judge. Evaluate the debater proposals independently. Verify claims with tools (READ, GREP) and give your final authoritative verdict.";
27019
+ default:
27020
+ return "You are the reviewer. Evaluate the debater proposals and give your final authoritative verdict.";
27021
+ }
27022
+ }
27023
+ function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticConfig, resolverContext) {
27024
+ const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
27025
+ `);
27026
+ const framing = buildResolverFraming(resolverContext);
27027
+ const voteTally = buildVoteTallyLine(resolverContext);
27028
+ const proposalsSection = buildProposalsSection(proposals);
27029
+ const critiquesSection = buildCritiquesSection(critiques);
27030
+ return [
27031
+ framing,
27032
+ "",
27033
+ `## Story ${story.id}: ${story.title}`,
27034
+ "",
27035
+ "## Acceptance Criteria",
27036
+ criteria,
27037
+ "",
27038
+ "## Debater Proposals",
27039
+ proposalsSection,
27040
+ critiquesSection,
27041
+ "",
27042
+ "## Diff",
27043
+ diff,
27044
+ voteTally,
27045
+ "",
27046
+ "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
27047
+ ].filter((line) => line !== undefined).join(`
27048
+ `);
27049
+ }
27050
+ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext) {
27051
+ const framing = buildResolverFraming(resolverContext);
27052
+ const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
27053
+ `) : "(none)";
27054
+ const proposalsSection = buildProposalsSection(proposals);
27055
+ const critiquesSection = buildCritiquesSection(critiques);
27056
+ return [
27057
+ `${framing} This is a re-review after implementer changes.`,
27058
+ "",
27059
+ "## Previous Findings",
27060
+ findingsList,
27061
+ "",
27062
+ "## Updated Debater Proposals",
27063
+ proposalsSection,
27064
+ critiquesSection,
27065
+ "",
27066
+ "## Updated Diff",
27067
+ updatedDiff,
27068
+ "",
27069
+ "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
27070
+ "deltaSummary should describe which previous findings are resolved vs still present."
27071
+ ].filter((line) => line !== undefined).join(`
27072
+ `);
27073
+ }
27074
+
27075
+ // src/review/dialogue.ts
26806
27076
  function extractDeltaSummary(rawOutput, previousFindings, newFindings) {
26807
27077
  try {
26808
27078
  const parsed = JSON.parse(rawOutput);
@@ -26874,6 +27144,7 @@ function createReviewerSession(agent, storyId, workdir, featureName, _config) {
26874
27144
  let lastCheckResult = null;
26875
27145
  let lastStory = null;
26876
27146
  let lastSemanticConfig = null;
27147
+ let lastWasDebateResolve = false;
26877
27148
  const sessionState = {
26878
27149
  generation: 1,
26879
27150
  pendingCompactionContext: null
@@ -26936,6 +27207,7 @@ ${prompt}`,
26936
27207
  lastCheckResult = reviewResult;
26937
27208
  lastStory = story;
26938
27209
  lastSemanticConfig = semanticConfig;
27210
+ lastWasDebateResolve = false;
26939
27211
  return reviewResult;
26940
27212
  },
26941
27213
  async reReview(updatedDiff) {
@@ -27005,6 +27277,76 @@ ${prompt}`,
27005
27277
  history.push({ role: "reviewer", content: result.output });
27006
27278
  return result.output;
27007
27279
  },
27280
+ async resolveDebate(proposals, critiques, diff, story, semanticConfig, resolverContext) {
27281
+ if (!active) {
27282
+ throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
27283
+ }
27284
+ const prompt = buildDebateResolverPrompt(proposals, critiques, diff, story, semanticConfig, resolverContext);
27285
+ const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(semanticConfig);
27286
+ const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
27287
+ const result = await agent.run({
27288
+ prompt: effectivePrompt,
27289
+ workdir,
27290
+ modelTier,
27291
+ modelDef,
27292
+ timeoutSeconds,
27293
+ sessionRole: "reviewer",
27294
+ keepSessionOpen: true,
27295
+ pipelineStage: "review",
27296
+ config: _config,
27297
+ storyId,
27298
+ featureName,
27299
+ acpSessionName
27300
+ });
27301
+ history.push({ role: "implementer", content: prompt });
27302
+ history.push({ role: "reviewer", content: result.output });
27303
+ const parsed = parseReviewResponse(result.output);
27304
+ const reviewResult = { ...parsed, cost: result.estimatedCost ?? 0 };
27305
+ lastCheckResult = reviewResult;
27306
+ lastStory = story;
27307
+ lastSemanticConfig = semanticConfig;
27308
+ lastWasDebateResolve = true;
27309
+ return reviewResult;
27310
+ },
27311
+ async reReviewDebate(proposals, critiques, updatedDiff, resolverContext) {
27312
+ if (!active) {
27313
+ throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
27314
+ }
27315
+ if (!lastCheckResult || !lastSemanticConfig || !lastWasDebateResolve) {
27316
+ throw new NaxError(`[dialogue] reReviewDebate() called before any resolveDebate() on story ${storyId}`, "NO_REVIEW_RESULT", { stage: "review", storyId });
27317
+ }
27318
+ const previousFindings = lastCheckResult.checkResult.findings;
27319
+ const prompt = buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext);
27320
+ const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(lastSemanticConfig);
27321
+ const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
27322
+ const result = await agent.run({
27323
+ prompt: effectivePrompt,
27324
+ workdir,
27325
+ modelTier,
27326
+ modelDef,
27327
+ timeoutSeconds,
27328
+ sessionRole: "reviewer",
27329
+ keepSessionOpen: true,
27330
+ pipelineStage: "review",
27331
+ config: _config,
27332
+ storyId,
27333
+ featureName,
27334
+ acpSessionName
27335
+ });
27336
+ history.push({ role: "implementer", content: prompt });
27337
+ history.push({ role: "reviewer", content: result.output });
27338
+ const parsed = parseReviewResponse(result.output);
27339
+ const deltaSummary = extractDeltaSummary(result.output, previousFindings, parsed.checkResult.findings);
27340
+ const dialogueResult = { ...parsed, deltaSummary, cost: result.estimatedCost ?? 0 };
27341
+ lastCheckResult = dialogueResult;
27342
+ const maxMessages = _config.review?.dialogue?.maxDialogueMessages ?? 20;
27343
+ if (history.length > maxMessages) {
27344
+ const compactedSummary = compactHistory(history);
27345
+ sessionState.generation++;
27346
+ sessionState.pendingCompactionContext = compactedSummary;
27347
+ }
27348
+ return dialogueResult;
27349
+ },
27008
27350
  getVerdict() {
27009
27351
  if (!lastCheckResult || !lastStory) {
27010
27352
  throw new NaxError(`[dialogue] getVerdict() called before any review() on story ${storyId}`, "NO_REVIEW_RESULT", { stage: "review", storyId });
@@ -27240,10 +27582,8 @@ var init_language_commands = __esm(() => {
27240
27582
  // src/review/semantic.ts
27241
27583
  var {spawn: spawn3 } = globalThis.Bun;
27242
27584
  async function collectDiff(workdir, storyGitRef, excludePatterns) {
27243
- const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`];
27244
- if (excludePatterns.length > 0) {
27245
- cmd.push("--", ".", ...excludePatterns);
27246
- }
27585
+ const merged = [...new Set([...excludePatterns, ...ALWAYS_EXCLUDED])];
27586
+ const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`, "--", ".", ...merged];
27247
27587
  const proc = _semanticDeps.spawn({
27248
27588
  cmd,
27249
27589
  cwd: workdir,
@@ -27403,7 +27743,7 @@ function toReviewFindings(findings) {
27403
27743
  source: "semantic-review"
27404
27744
  }));
27405
27745
  }
27406
- async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig, featureName) {
27746
+ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig, featureName, resolverSession) {
27407
27747
  const startTime = Date.now();
27408
27748
  const logger = getSafeLogger();
27409
27749
  if (featureName === undefined) {
@@ -27473,6 +27813,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27473
27813
  const reviewDebateEnabled = naxConfig?.debate?.enabled && naxConfig?.debate?.stages?.review?.enabled;
27474
27814
  if (reviewDebateEnabled) {
27475
27815
  const reviewStageConfig = naxConfig?.debate?.stages.review;
27816
+ const isReReview = resolverSession !== undefined && resolverSession.history.length > 0;
27476
27817
  const debateSession = _semanticDeps.createDebateSession({
27477
27818
  storyId: story.id,
27478
27819
  stage: "review",
@@ -27480,26 +27821,69 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27480
27821
  config: naxConfig ?? DEFAULT_CONFIG,
27481
27822
  workdir,
27482
27823
  featureName,
27483
- timeoutSeconds: naxConfig?.execution?.sessionTimeoutSeconds
27824
+ timeoutSeconds: naxConfig?.execution?.sessionTimeoutSeconds,
27825
+ reviewerSession: resolverSession,
27826
+ resolverContextInput: resolverSession ? {
27827
+ diff,
27828
+ story: { id: story.id, title: story.title, acceptanceCriteria: story.acceptanceCriteria },
27829
+ semanticConfig,
27830
+ resolverType: reviewStageConfig.resolver.type,
27831
+ isReReview
27832
+ } : undefined
27484
27833
  });
27834
+ const historyLenBefore = resolverSession?.history.length ?? 0;
27485
27835
  const debateResult = await debateSession.run(prompt);
27486
27836
  const debateCost = debateResult.totalCostUsd ?? 0;
27487
- let passCount = 0;
27488
- let failCount = 0;
27837
+ const sessionUsed = resolverSession && resolverSession.history.length > historyLenBefore;
27838
+ if (sessionUsed) {
27839
+ const durationMs3 = Date.now() - startTime;
27840
+ try {
27841
+ const verdict = resolverSession.getVerdict();
27842
+ const findings = verdict.findings ?? [];
27843
+ if (!verdict.passed && findings.length > 0) {
27844
+ logger?.warn("review", `Semantic review failed (debate+dialogue): ${findings.length} findings`, {
27845
+ storyId: story.id,
27846
+ durationMs: durationMs3
27847
+ });
27848
+ return {
27849
+ check: "semantic",
27850
+ success: false,
27851
+ command: "",
27852
+ exitCode: 1,
27853
+ output: `Semantic review failed:
27854
+
27855
+ ${findings.map((f) => `${f.ruleId}: ${f.message}`).join(`
27856
+ `)}`,
27857
+ durationMs: durationMs3,
27858
+ findings,
27859
+ cost: debateCost
27860
+ };
27861
+ }
27862
+ const label = verdict.passed ? "Semantic review passed (debate+dialogue)" : "Semantic review passed (debate+dialogue, all findings non-blocking)";
27863
+ logger?.info("review", label, { storyId: story.id, durationMs: durationMs3 });
27864
+ return {
27865
+ check: "semantic",
27866
+ success: true,
27867
+ command: "",
27868
+ exitCode: 0,
27869
+ output: label,
27870
+ durationMs: durationMs3,
27871
+ cost: debateCost
27872
+ };
27873
+ } catch {
27874
+ logger?.warn("review", "getVerdict() failed after debate+dialogue \u2014 falling back to stateless verdict", {
27875
+ storyId: story.id
27876
+ });
27877
+ }
27878
+ }
27879
+ const resolverPassed = debateResult.outcome === "passed";
27489
27880
  const allFindings = [];
27490
27881
  for (const p of debateResult.proposals) {
27491
27882
  const parsed2 = parseLLMResponse(p.output);
27492
27883
  if (parsed2) {
27493
- if (parsed2.passed)
27494
- passCount++;
27495
- else
27496
- failCount++;
27497
27884
  allFindings.push(...parsed2.findings);
27498
- } else {
27499
- failCount++;
27500
27885
  }
27501
27886
  }
27502
- const majorityPassed = passCount > failCount;
27503
27887
  const seen = new Set;
27504
27888
  const deduped = [];
27505
27889
  for (const f of allFindings) {
@@ -27511,7 +27895,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27511
27895
  }
27512
27896
  const debateBlocking = deduped.filter((f) => isBlockingSeverity(f.severity));
27513
27897
  const durationMs2 = Date.now() - startTime;
27514
- if (!majorityPassed) {
27898
+ if (!resolverPassed) {
27515
27899
  if (debateBlocking.length > 0) {
27516
27900
  logger?.warn("review", `Semantic review failed (debate): ${debateBlocking.length} findings`, {
27517
27901
  storyId: story.id,
@@ -27710,7 +28094,7 @@ ${formatFindings(blockingFindings)}`;
27710
28094
  cost: llmCost
27711
28095
  };
27712
28096
  }
27713
- var _semanticDeps, DIFF_CAP_BYTES = 51200;
28097
+ var _semanticDeps, DIFF_CAP_BYTES = 51200, ALWAYS_EXCLUDED;
27714
28098
  var init_semantic = __esm(() => {
27715
28099
  init_adapter();
27716
28100
  init_config();
@@ -27724,6 +28108,7 @@ var init_semantic = __esm(() => {
27724
28108
  createDebateSession: (opts) => new DebateSession(opts),
27725
28109
  readAcpSession
27726
28110
  };
28111
+ ALWAYS_EXCLUDED = [":!.nax/", ":!.nax-pids"];
27727
28112
  });
27728
28113
 
27729
28114
  // src/review/runner.ts
@@ -27808,7 +28193,7 @@ async function getUncommittedFilesImpl(workdir) {
27808
28193
  return [];
27809
28194
  }
27810
28195
  }
27811
- async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName) {
28196
+ async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession) {
27812
28197
  const startTime = Date.now();
27813
28198
  const logger = getSafeLogger();
27814
28199
  const checks3 = [];
@@ -27865,10 +28250,19 @@ Stage and commit these files before running review.`
27865
28250
  modelTier: "balanced",
27866
28251
  rules: [],
27867
28252
  timeoutMs: 600000,
27868
- excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
28253
+ excludePatterns: [
28254
+ ":!test/",
28255
+ ":!tests/",
28256
+ ":!*_test.go",
28257
+ ":!*.test.ts",
28258
+ ":!*.spec.ts",
28259
+ ":!**/__tests__/",
28260
+ ":!.nax/",
28261
+ ":!.nax-pids"
28262
+ ]
27869
28263
  };
27870
28264
  const runSemantic = _reviewSemanticDeps.runSemanticReview;
27871
- const result2 = featureName !== undefined ? await runSemantic(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null), naxConfig, featureName) : await runSemantic(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null), naxConfig);
28265
+ const result2 = await runSemantic(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null), naxConfig, featureName, resolverSession);
27872
28266
  checks3.push(result2);
27873
28267
  if (!result2.success && !firstFailure) {
27874
28268
  firstFailure = `${checkName} failed`;
@@ -27950,9 +28344,9 @@ async function getChangedFiles(workdir, baseRef) {
27950
28344
  }
27951
28345
 
27952
28346
  class ReviewOrchestrator {
27953
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig, retrySkipChecks, featureName) {
28347
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession) {
27954
28348
  const logger = getSafeLogger();
27955
- const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName);
28349
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession);
27956
28350
  if (!builtIn.success) {
27957
28351
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
27958
28352
  }
@@ -28019,12 +28413,14 @@ class ReviewOrchestrator {
28019
28413
  const agentResolver = ctx.agentGetFn ?? undefined;
28020
28414
  const agentName = ctx.rootConfig.autoMode?.defaultAgent;
28021
28415
  const modelResolver = agentName ? (_tier) => agentResolver ? agentResolver(agentName) ?? null : null : undefined;
28416
+ const reviewDebateEnabled = ctx.rootConfig?.debate?.enabled && ctx.rootConfig?.debate?.stages?.review?.enabled;
28417
+ const resolverSession = reviewDebateEnabled ? ctx.reviewerSession : undefined;
28022
28418
  return this.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir, ctx.config.quality?.commands, ctx.story.id, {
28023
28419
  id: ctx.story.id,
28024
28420
  title: ctx.story.title,
28025
28421
  description: ctx.story.description,
28026
28422
  acceptanceCriteria: ctx.story.acceptanceCriteria
28027
- }, modelResolver, ctx.config, retrySkipChecks, ctx.prd.feature);
28423
+ }, modelResolver, ctx.config, retrySkipChecks, ctx.prd.feature, resolverSession);
28028
28424
  }
28029
28425
  }
28030
28426
  var _orchestratorDeps, reviewOrchestrator;
@@ -28053,11 +28449,12 @@ var init_review = __esm(() => {
28053
28449
  enabled: (ctx) => ctx.config.review.enabled,
28054
28450
  async execute(ctx) {
28055
28451
  const logger = getLogger();
28452
+ const reviewDebateEnabled = ctx.rootConfig?.debate?.enabled && ctx.rootConfig?.debate?.stages?.review?.enabled;
28056
28453
  const dialogueEnabled = ctx.config.review?.dialogue?.enabled ?? false;
28057
28454
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
28058
28455
  const agentResolver = ctx.agentGetFn ?? getAgent;
28059
28456
  const agentName = ctx.rootConfig.autoMode?.defaultAgent;
28060
- if (dialogueEnabled && ctx.reviewerSession) {
28457
+ if (dialogueEnabled && !reviewDebateEnabled && ctx.reviewerSession) {
28061
28458
  try {
28062
28459
  const diff = ctx.storyGitRef ?? "";
28063
28460
  const reReviewResult = await ctx.reviewerSession.reReview(diff);
@@ -28095,47 +28492,49 @@ var init_review = __esm(() => {
28095
28492
  if (dialogueEnabled && !ctx.reviewerSession) {
28096
28493
  const agent = agentName ? agentResolver(agentName) ?? null : null;
28097
28494
  ctx.reviewerSession = _reviewDeps.createReviewerSession(agent ?? null, ctx.story.id, ctx.workdir, ctx.prd.feature ?? "", ctx.config);
28098
- const semanticConfig = ctx.config.review?.semantic;
28099
- if (semanticConfig && agent) {
28100
- try {
28101
- const diff = ctx.storyGitRef ?? "";
28102
- const story = {
28103
- id: ctx.story.id,
28104
- title: ctx.story.title,
28105
- description: ctx.story.description,
28106
- acceptanceCriteria: ctx.story.acceptanceCriteria
28107
- };
28108
- const sessionResult = await ctx.reviewerSession.review(diff, story, semanticConfig);
28109
- const passed = sessionResult.checkResult.success;
28110
- ctx.reviewResult = {
28111
- success: passed,
28112
- checks: passed ? [] : [
28113
- {
28114
- check: "semantic",
28115
- success: false,
28116
- command: "reviewer-session-review",
28117
- exitCode: 1,
28118
- output: sessionResult.checkResult.findings.map((f) => f.message).join(`
28495
+ if (!reviewDebateEnabled) {
28496
+ const semanticConfig = ctx.config.review?.semantic;
28497
+ if (semanticConfig && agent) {
28498
+ try {
28499
+ const diff = ctx.storyGitRef ?? "";
28500
+ const story = {
28501
+ id: ctx.story.id,
28502
+ title: ctx.story.title,
28503
+ description: ctx.story.description,
28504
+ acceptanceCriteria: ctx.story.acceptanceCriteria
28505
+ };
28506
+ const sessionResult = await ctx.reviewerSession.review(diff, story, semanticConfig);
28507
+ const passed = sessionResult.checkResult.success;
28508
+ ctx.reviewResult = {
28509
+ success: passed,
28510
+ checks: passed ? [] : [
28511
+ {
28512
+ check: "semantic",
28513
+ success: false,
28514
+ command: "reviewer-session-review",
28515
+ exitCode: 1,
28516
+ output: sessionResult.checkResult.findings.map((f) => f.message).join(`
28119
28517
  `),
28120
- durationMs: 0,
28121
- findings: sessionResult.checkResult.findings
28122
- }
28123
- ],
28124
- totalDurationMs: 0
28125
- };
28126
- const dialogueCost = sessionResult.cost ?? 0;
28127
- if (passed) {
28128
- logger.info("review", "Review passed (dialogue session)", { storyId: ctx.story.id });
28129
- } else {
28130
- logger.warn("review", "Review failed (dialogue session) \u2014 handing off to autofix", {
28518
+ durationMs: 0,
28519
+ findings: sessionResult.checkResult.findings
28520
+ }
28521
+ ],
28522
+ totalDurationMs: 0
28523
+ };
28524
+ const dialogueCost = sessionResult.cost ?? 0;
28525
+ if (passed) {
28526
+ logger.info("review", "Review passed (dialogue session)", { storyId: ctx.story.id });
28527
+ } else {
28528
+ logger.warn("review", "Review failed (dialogue session) \u2014 handing off to autofix", {
28529
+ storyId: ctx.story.id
28530
+ });
28531
+ }
28532
+ return { action: "continue", cost: dialogueCost || undefined };
28533
+ } catch (err) {
28534
+ logger.warn("review", "ReviewerSession.review() failed \u2014 falling back to one-shot review", {
28131
28535
  storyId: ctx.story.id
28132
28536
  });
28133
28537
  }
28134
- return { action: "continue", cost: dialogueCost || undefined };
28135
- } catch (err) {
28136
- logger.warn("review", "ReviewerSession.review() failed \u2014 falling back to one-shot review", {
28137
- storyId: ctx.story.id
28138
- });
28139
28538
  }
28140
28539
  }
28141
28540
  }
@@ -28239,7 +28638,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28239
28638
  }
28240
28639
  const remainingBudget = maxTotal - consumed;
28241
28640
  const maxAttempts = Math.min(maxPerCycle, remainingBudget);
28242
- const agentGetFn = ctx.agentGetFn ?? _autofixDeps.getAgent;
28641
+ const agentGetFn = ctx.agentGetFn ?? ((name) => _autofixDeps.getAgent(name, ctx.rootConfig));
28243
28642
  const loopState = {
28244
28643
  attempt: 0,
28245
28644
  failedChecks
@@ -28382,7 +28781,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28382
28781
  }
28383
28782
  var CLARIFY_REGEX, autofixStage, _autofixDeps;
28384
28783
  var init_autofix = __esm(() => {
28385
- init_agents();
28784
+ init_registry();
28386
28785
  init_config();
28387
28786
  init_loader();
28388
28787
  init_logger2();
@@ -28458,6 +28857,14 @@ var init_autofix = __esm(() => {
28458
28857
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
28459
28858
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
28460
28859
  if (recheckPassed) {
28860
+ const passedChecks = (ctx.reviewResult?.checks ?? []).filter((c) => c.success).map((c) => c.check);
28861
+ if (passedChecks.length > 0) {
28862
+ ctx.retrySkipChecks = new Set(passedChecks);
28863
+ logger.debug("autofix", "Skipping already-passed checks on retry", {
28864
+ storyId: ctx.story.id,
28865
+ skippedChecks: passedChecks
28866
+ });
28867
+ }
28461
28868
  logger.info("autofix", "Mechanical autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
28462
28869
  return { action: "retry", fromStage: "review" };
28463
28870
  }
@@ -28489,7 +28896,7 @@ var init_autofix = __esm(() => {
28489
28896
  }
28490
28897
  };
28491
28898
  _autofixDeps = {
28492
- getAgent,
28899
+ getAgent: (name, config2) => createAgentRegistry(config2).getAgent(name),
28493
28900
  runQualityCommand,
28494
28901
  recheckReview,
28495
28902
  runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir),
@@ -28583,7 +28990,7 @@ var init_completion = __esm(() => {
28583
28990
  const logger = getLogger();
28584
28991
  const isBatch = ctx.stories.length > 1;
28585
28992
  const sessionCost = ctx.agentResult?.estimatedCost || 0;
28586
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
28993
+ const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
28587
28994
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
28588
28995
  if (isBatch) {
28589
28996
  ctx.storyMetrics = collectBatchMetrics(ctx, storyStartTime);
@@ -28634,7 +29041,7 @@ var init_completion = __esm(() => {
28634
29041
  };
28635
29042
  await _completionDeps.persistSemanticVerdict(ctx.featureDir, ctx.story.id, verdict);
28636
29043
  }
28637
- await savePRD(ctx.prd, prdPath);
29044
+ await _completionDeps.savePRD(ctx.prd, prdPath);
28638
29045
  const updatedCounts = countStories(ctx.prd);
28639
29046
  logger.info("completion", "Progress update", {
28640
29047
  storyId: ctx.story.id,
@@ -28653,7 +29060,8 @@ var init_completion = __esm(() => {
28653
29060
  };
28654
29061
  _completionDeps = {
28655
29062
  checkReviewGate,
28656
- persistSemanticVerdict
29063
+ persistSemanticVerdict,
29064
+ savePRD
28657
29065
  };
28658
29066
  });
28659
29067
 
@@ -35735,7 +36143,7 @@ var package_default;
35735
36143
  var init_package = __esm(() => {
35736
36144
  package_default = {
35737
36145
  name: "@nathapp/nax",
35738
- version: "0.59.0",
36146
+ version: "0.59.2",
35739
36147
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
35740
36148
  type: "module",
35741
36149
  bin: {
@@ -35815,8 +36223,8 @@ var init_version = __esm(() => {
35815
36223
  NAX_VERSION = package_default.version;
35816
36224
  NAX_COMMIT = (() => {
35817
36225
  try {
35818
- if (/^[0-9a-f]{6,10}$/.test("13990b5b"))
35819
- return "13990b5b";
36226
+ if (/^[0-9a-f]{6,10}$/.test("d42d47be"))
36227
+ return "d42d47be";
35820
36228
  } catch {}
35821
36229
  try {
35822
36230
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -40496,7 +40904,7 @@ function validateStoryCount(counts, config2) {
40496
40904
  }
40497
40905
  function logActiveProtocol(config2) {
40498
40906
  const logger = getSafeLogger();
40499
- const protocol = config2.agent?.protocol ?? "cli";
40907
+ const protocol = config2.agent?.protocol;
40500
40908
  logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
40501
40909
  }
40502
40910
  async function initializeRun(ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.59.0",
3
+ "version": "0.59.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {