@nathapp/nax 0.59.0 → 0.59.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 +598 -200
  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;
@@ -21521,25 +21543,89 @@ function pipelineStageForDebate(stage) {
21521
21543
  }
21522
21544
  }
21523
21545
  function resolveModelDefForDebater(debater, tier, config2) {
21524
- if (debater.model && !isTierLabel(debater.model)) {
21525
- return resolveModel(debater.model);
21546
+ const modelOverride = debater.model;
21547
+ let effectiveTier = tier;
21548
+ if (modelOverride) {
21549
+ const aliasedTier = MODEL_SHORTHAND_TIERS[modelOverride.toLowerCase()];
21550
+ if (aliasedTier) {
21551
+ effectiveTier = aliasedTier;
21552
+ } else if (!isTierLabel(modelOverride)) {
21553
+ return resolveModel(modelOverride);
21554
+ }
21526
21555
  }
21527
21556
  const configModels = config2?.models ?? DEFAULT_CONFIG.models;
21528
21557
  const configDefaultAgent = config2?.autoMode?.defaultAgent ?? DEFAULT_CONFIG.autoMode.defaultAgent;
21529
21558
  try {
21530
- return resolveModelForAgent(configModels, debater.agent, tier, configDefaultAgent);
21559
+ return resolveModelForAgent(configModels, debater.agent, effectiveTier, configDefaultAgent);
21531
21560
  } catch {}
21532
21561
  try {
21533
- return resolveModelForAgent(DEFAULT_CONFIG.models, DEFAULT_CONFIG.autoMode.defaultAgent, tier, DEFAULT_CONFIG.autoMode.defaultAgent);
21562
+ return resolveModelForAgent(DEFAULT_CONFIG.models, DEFAULT_CONFIG.autoMode.defaultAgent, effectiveTier, DEFAULT_CONFIG.autoMode.defaultAgent);
21534
21563
  } catch {
21535
21564
  return resolveModelForAgent(configModels, debater.agent, "fast", configDefaultAgent);
21536
21565
  }
21537
21566
  }
21538
- async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName) {
21567
+ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, config2, storyId, timeoutMs, workdir, featureName, reviewerSession, resolverContext) {
21539
21568
  const resolverConfig = stageConfig.resolver;
21540
21569
  const logger = _debateSessionDeps.getSafeLogger();
21570
+ if (reviewerSession && resolverContext) {
21571
+ try {
21572
+ const debateCtx = {
21573
+ resolverType: resolverConfig.type
21574
+ };
21575
+ if (resolverConfig.type === "majority-fail-closed" || resolverConfig.type === "majority-fail-open") {
21576
+ const failOpen = resolverConfig.type === "majority-fail-open";
21577
+ const rawOutcome = majorityResolver(proposalOutputs, failOpen);
21578
+ let passCount = 0;
21579
+ let failCount = 0;
21580
+ for (const proposal of proposalOutputs) {
21581
+ try {
21582
+ const stripped = proposal.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
21583
+ const parsed = JSON.parse(stripped);
21584
+ if (typeof parsed.passed === "boolean" && parsed.passed)
21585
+ passCount++;
21586
+ else if (failOpen)
21587
+ passCount++;
21588
+ else
21589
+ failCount++;
21590
+ } catch {
21591
+ if (failOpen)
21592
+ passCount++;
21593
+ else
21594
+ failCount++;
21595
+ }
21596
+ }
21597
+ debateCtx.majorityVote = { passed: rawOutcome === "passed", passCount, failCount };
21598
+ }
21599
+ const story = {
21600
+ id: resolverContext.story.id,
21601
+ title: resolverContext.story.title,
21602
+ description: "",
21603
+ acceptanceCriteria: resolverContext.story.acceptanceCriteria
21604
+ };
21605
+ let dialogueResult;
21606
+ if (resolverContext.isReReview) {
21607
+ dialogueResult = await reviewerSession.reReviewDebate(resolverContext.labeledProposals, critiqueOutputs, resolverContext.diff, debateCtx);
21608
+ } else {
21609
+ dialogueResult = await reviewerSession.resolveDebate(resolverContext.labeledProposals, critiqueOutputs, resolverContext.diff, story, resolverContext.semanticConfig, debateCtx);
21610
+ }
21611
+ const outcome = dialogueResult.checkResult.success ? "passed" : "failed";
21612
+ return {
21613
+ outcome,
21614
+ resolverCostUsd: dialogueResult.cost ?? 0,
21615
+ dialogueResult
21616
+ };
21617
+ } catch (err) {
21618
+ logger?.warn("debate", "ReviewerSession.resolveDebate() failed \u2014 falling back to stateless resolver", {
21619
+ storyId,
21620
+ error: err instanceof Error ? err.message : String(err)
21621
+ });
21622
+ }
21623
+ }
21624
+ if (reviewerSession && !resolverContext) {
21625
+ logger?.warn("debate", "ReviewerSession provided but resolverContext is undefined \u2014 falling back to stateless resolver", { storyId });
21626
+ }
21541
21627
  if (resolverConfig.type === "majority-fail-closed" || resolverConfig.type === "majority-fail-open") {
21542
- if (workdir !== undefined) {
21628
+ if (workdir !== undefined && !reviewerSession) {
21543
21629
  logger?.warn("debate", "majority resolver does not support implementer session resumption \u2014 switch to synthesis or custom resolver for context-aware semantic review");
21544
21630
  }
21545
21631
  return {
@@ -21565,7 +21651,8 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21565
21651
  });
21566
21652
  return {
21567
21653
  outcome: "passed",
21568
- resolverCostUsd: resolverResult.costUsd
21654
+ resolverCostUsd: resolverResult.costUsd,
21655
+ output: resolverResult.output
21569
21656
  };
21570
21657
  }
21571
21658
  return { outcome: "passed", resolverCostUsd: 0 };
@@ -21586,12 +21673,13 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21586
21673
  });
21587
21674
  return {
21588
21675
  outcome: "passed",
21589
- resolverCostUsd: resolverResult.costUsd
21676
+ resolverCostUsd: resolverResult.costUsd,
21677
+ output: resolverResult.output
21590
21678
  };
21591
21679
  }
21592
21680
  return { outcome: "passed", resolverCostUsd: 0 };
21593
21681
  }
21594
- var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
21682
+ var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps, MODEL_SHORTHAND_TIERS;
21595
21683
  var init_session_helpers = __esm(() => {
21596
21684
  init_adapter();
21597
21685
  init_registry();
@@ -21603,6 +21691,11 @@ var init_session_helpers = __esm(() => {
21603
21691
  getSafeLogger,
21604
21692
  readFile: (path) => Bun.file(path).text()
21605
21693
  };
21694
+ MODEL_SHORTHAND_TIERS = {
21695
+ haiku: "fast",
21696
+ sonnet: "balanced",
21697
+ opus: "powerful"
21698
+ };
21606
21699
  });
21607
21700
 
21608
21701
  // src/debate/concurrency.ts
@@ -21788,7 +21881,11 @@ async function runStateful(ctx, prompt) {
21788
21881
  critiqueOutputs = critiqueSettled.filter((r) => r.status === "fulfilled").map((r) => r.value.output);
21789
21882
  }
21790
21883
  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);
21884
+ const fullResolverContext = ctx.resolverContextInput ? {
21885
+ ...ctx.resolverContextInput,
21886
+ labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
21887
+ } : undefined;
21888
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
21792
21889
  totalCostUsd += outcome.resolverCostUsd;
21793
21890
  const proposals = successfulProposals.map((s) => ({
21794
21891
  debater: s.debater,
@@ -21815,6 +21912,50 @@ var init_session_stateful = __esm(() => {
21815
21912
  });
21816
21913
 
21817
21914
  // src/debate/session-hybrid.ts
21915
+ async function runRebuttalLoop(ctx, proposals, originalPrompt, sessionRolePrefix) {
21916
+ const logger = _debateSessionDeps.getSafeLogger();
21917
+ const config2 = ctx.stageConfig;
21918
+ const rebuttals = [];
21919
+ let costUsd = 0;
21920
+ const proposalList = proposals.map((s) => ({ debater: s.debater, output: s.output }));
21921
+ try {
21922
+ for (let round = 1;round <= config2.rounds; round++) {
21923
+ const priorRebuttals = rebuttals.filter((r) => r.round < round).map((r) => r.output);
21924
+ for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21925
+ const proposal = proposals[debaterIdx];
21926
+ const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
21927
+ logger?.info("debate:rebuttal-start", "debate:rebuttal-start", {
21928
+ storyId: ctx.storyId,
21929
+ round,
21930
+ debaterIndex: debaterIdx
21931
+ });
21932
+ const rebuttalPrompt = buildRebuttalContext(originalPrompt, proposalList, priorRebuttals, debaterIdx);
21933
+ try {
21934
+ const turnResult = await runStatefulTurn(ctx, proposal.adapter, proposal.debater, rebuttalPrompt, sessionRole, true);
21935
+ costUsd += turnResult.cost;
21936
+ rebuttals.push({ debater: proposal.debater, round, output: turnResult.output });
21937
+ } catch (err) {
21938
+ logger?.warn("debate", "debate:rebuttal-failed", {
21939
+ storyId: ctx.storyId,
21940
+ round,
21941
+ debaterIndex: debaterIdx,
21942
+ error: err instanceof Error ? err.message : String(err)
21943
+ });
21944
+ }
21945
+ }
21946
+ }
21947
+ } finally {
21948
+ for (let debaterIdx = 0;debaterIdx < proposals.length; debaterIdx++) {
21949
+ const proposal = proposals[debaterIdx];
21950
+ const sessionRole = `${sessionRolePrefix}-${debaterIdx}`;
21951
+ try {
21952
+ const closeCost = await closeStatefulSession(ctx, proposal.adapter, proposal.debater, sessionRole);
21953
+ costUsd += closeCost;
21954
+ } catch {}
21955
+ }
21956
+ }
21957
+ return { rebuttals, costUsd };
21958
+ }
21818
21959
  async function runHybrid(ctx, prompt) {
21819
21960
  const logger = _debateSessionDeps.getSafeLogger();
21820
21961
  const config2 = ctx.stageConfig;
@@ -21883,45 +22024,14 @@ async function runHybrid(ctx, prompt) {
21883
22024
  }
21884
22025
  const proposalOutputs = successfulProposals.map((s) => s.output);
21885
22026
  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
- }
22027
+ const { rebuttals, costUsd: rebuttalCost } = await runRebuttalLoop(ctx, successfulProposals, prompt, "debate-hybrid");
22028
+ totalCostUsd += rebuttalCost;
21923
22029
  const critiqueOutputs = rebuttals.map((r) => r.output);
21924
- const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000);
22030
+ const fullResolverContext = ctx.resolverContextInput ? {
22031
+ ...ctx.resolverContextInput,
22032
+ labeledProposals: successfulProposals.map((s) => ({ debater: s.debater.agent, output: s.output }))
22033
+ } : undefined;
22034
+ const resolveResult = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutSeconds * 1000, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
21925
22035
  totalCostUsd += resolveResult.resolverCostUsd;
21926
22036
  return {
21927
22037
  storyId: ctx.storyId,
@@ -22064,7 +22174,11 @@ async function runOneShot(ctx, prompt) {
22064
22174
  critiqueOutputs = critiqueSettled.filter((r) => r.status === "fulfilled").map((r) => r.value.output);
22065
22175
  }
22066
22176
  const proposalOutputs = successful.map((p) => p.output);
22067
- const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs);
22177
+ const fullResolverContext = ctx.resolverContextInput ? {
22178
+ ...ctx.resolverContextInput,
22179
+ labeledProposals: successful.map((p) => ({ debater: p.debater.agent, output: p.output }))
22180
+ } : undefined;
22181
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, ctx.timeoutMs, ctx.workdir, ctx.featureName, ctx.reviewerSession, fullResolverContext);
22068
22182
  totalCostUsd += outcome.resolverCostUsd;
22069
22183
  const proposals = successful.map((p) => ({
22070
22184
  debater: p.debater,
@@ -22096,7 +22210,7 @@ async function runPlan2(ctx, basePrompt, opts) {
22096
22210
  const logger = _debateSessionDeps.getSafeLogger();
22097
22211
  const config2 = ctx.stageConfig;
22098
22212
  const debaters = config2.debaters ?? [];
22099
- const totalCostUsd = 0;
22213
+ let totalCostUsd = 0;
22100
22214
  const resolved = [];
22101
22215
  for (const debater of debaters) {
22102
22216
  const adapter = _debateSessionDeps.getAgent(debater.agent, ctx.config);
@@ -22119,13 +22233,16 @@ async function runPlan2(ctx, basePrompt, opts) {
22119
22233
 
22120
22234
  Write the PRD JSON directly to this file path: ${tempOutputPath}
22121
22235
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.`;
22122
- await adapter.plan({
22236
+ const modelTier = modelTierFromDebater(debater);
22237
+ const modelDef = resolveModelDefForDebater(debater, modelTier, ctx.config);
22238
+ const planResult = await adapter.plan({
22123
22239
  prompt: debaterPrompt,
22124
22240
  workdir: opts.workdir,
22125
22241
  interactive: false,
22126
22242
  timeoutSeconds: opts.timeoutSeconds,
22127
22243
  config: ctx.config,
22128
- modelTier: debater.model ?? "balanced",
22244
+ modelTier,
22245
+ modelDef,
22129
22246
  dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
22130
22247
  maxInteractionTurns: opts.maxInteractionTurns,
22131
22248
  featureName: opts.feature,
@@ -22133,13 +22250,14 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22133
22250
  sessionRole: `plan-${i}`
22134
22251
  });
22135
22252
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22136
- return { debater, adapter, output, cost: 0 };
22253
+ return { debater, adapter, output, cost: planResult.costUsd ?? 0 };
22137
22254
  }), concurrencyLimit);
22138
22255
  const successful = [];
22139
22256
  for (let i = 0;i < settled.length; i++) {
22140
22257
  const res = settled[i];
22141
22258
  if (res.status === "fulfilled") {
22142
22259
  successful.push(res.value);
22260
+ totalCostUsd += res.value.cost;
22143
22261
  } else {
22144
22262
  const { debater } = resolved[i];
22145
22263
  logger?.warn("debate", "debate:debater-failed", {
@@ -22191,9 +22309,30 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22191
22309
  };
22192
22310
  }
22193
22311
  const proposalOutputs = successful.map((p) => p.output);
22312
+ const mode = ctx.stageConfig.mode ?? "panel";
22313
+ const sessionMode = ctx.stageConfig.sessionMode ?? "one-shot";
22314
+ let critiqueOutputs = [];
22315
+ let rebuttalList;
22316
+ if (mode === "hybrid" && sessionMode === "stateful") {
22317
+ const hybridCtx = {
22318
+ storyId: ctx.storyId,
22319
+ stage: ctx.stage,
22320
+ stageConfig: ctx.stageConfig,
22321
+ config: ctx.config,
22322
+ workdir: opts.workdir,
22323
+ featureName: opts.feature,
22324
+ timeoutSeconds: opts.timeoutSeconds ?? 600
22325
+ };
22326
+ const { rebuttals, costUsd } = await runRebuttalLoop(hybridCtx, successful, basePrompt, "plan-hybrid");
22327
+ critiqueOutputs = rebuttals.map((r) => r.output);
22328
+ rebuttalList = rebuttals;
22329
+ totalCostUsd += costUsd;
22330
+ } else if (mode === "hybrid") {
22331
+ logger?.warn("debate", "hybrid mode requires sessionMode: stateful for plan \u2014 running as panel");
22332
+ }
22194
22333
  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;
22334
+ const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature);
22335
+ const winningOutput = outcome.output ?? successful[0].output;
22197
22336
  const proposals = successful.map((p) => ({ debater: p.debater, output: p.output }));
22198
22337
  logger?.info("debate", "debate:result", {
22199
22338
  storyId: ctx.storyId,
@@ -22204,16 +22343,18 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22204
22343
  storyId: ctx.storyId,
22205
22344
  stage: ctx.stage,
22206
22345
  outcome: outcome.outcome,
22207
- rounds: 1,
22346
+ rounds: rebuttalList ? config2.rounds : 1,
22208
22347
  debaters: successful.map((p) => p.debater.agent),
22209
22348
  resolverType: config2.resolver.type,
22210
22349
  proposals,
22350
+ rebuttals: rebuttalList,
22211
22351
  output: winningOutput,
22212
22352
  totalCostUsd
22213
22353
  };
22214
22354
  }
22215
22355
  var init_session_plan = __esm(() => {
22216
22356
  init_session_helpers();
22357
+ init_session_hybrid();
22217
22358
  });
22218
22359
 
22219
22360
  // src/debate/session.ts
@@ -22225,6 +22366,8 @@ class DebateSession {
22225
22366
  workdir;
22226
22367
  featureName;
22227
22368
  timeoutSeconds;
22369
+ reviewerSession;
22370
+ resolverContextInput;
22228
22371
  get timeoutMs() {
22229
22372
  return this.timeoutSeconds * 1000;
22230
22373
  }
@@ -22236,6 +22379,8 @@ class DebateSession {
22236
22379
  this.workdir = opts.workdir ?? process.cwd();
22237
22380
  this.featureName = opts.featureName ?? opts.stage;
22238
22381
  this.timeoutSeconds = opts.timeoutSeconds ?? opts.stageConfig.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
22382
+ this.reviewerSession = opts.reviewerSession;
22383
+ this.resolverContextInput = opts.resolverContextInput;
22239
22384
  }
22240
22385
  async run(prompt) {
22241
22386
  const sessionMode = this.stageConfig.sessionMode ?? "one-shot";
@@ -22249,7 +22394,9 @@ class DebateSession {
22249
22394
  config: this.config,
22250
22395
  workdir: this.workdir,
22251
22396
  featureName: this.featureName,
22252
- timeoutSeconds: this.timeoutSeconds
22397
+ timeoutSeconds: this.timeoutSeconds,
22398
+ reviewerSession: this.reviewerSession,
22399
+ resolverContextInput: this.resolverContextInput
22253
22400
  }, prompt);
22254
22401
  }
22255
22402
  const logger = _debateSessionDeps.getSafeLogger();
@@ -22259,7 +22406,11 @@ class DebateSession {
22259
22406
  stage: this.stage,
22260
22407
  stageConfig: this.stageConfig,
22261
22408
  config: this.config,
22262
- timeoutMs: this.timeoutMs
22409
+ timeoutMs: this.timeoutMs,
22410
+ workdir: this.workdir,
22411
+ featureName: this.featureName,
22412
+ reviewerSession: this.reviewerSession,
22413
+ resolverContextInput: this.resolverContextInput
22263
22414
  }, prompt);
22264
22415
  }
22265
22416
  if (sessionMode === "stateful") {
@@ -22270,7 +22421,9 @@ class DebateSession {
22270
22421
  config: this.config,
22271
22422
  workdir: this.workdir,
22272
22423
  featureName: this.featureName,
22273
- timeoutSeconds: this.timeoutSeconds
22424
+ timeoutSeconds: this.timeoutSeconds,
22425
+ reviewerSession: this.reviewerSession,
22426
+ resolverContextInput: this.resolverContextInput
22274
22427
  }, prompt);
22275
22428
  }
22276
22429
  return runOneShot({
@@ -22278,7 +22431,11 @@ class DebateSession {
22278
22431
  stage: this.stage,
22279
22432
  stageConfig: this.stageConfig,
22280
22433
  config: this.config,
22281
- timeoutMs: this.timeoutMs
22434
+ timeoutMs: this.timeoutMs,
22435
+ workdir: this.workdir,
22436
+ featureName: this.featureName,
22437
+ reviewerSession: this.reviewerSession,
22438
+ resolverContextInput: this.resolverContextInput
22282
22439
  }, prompt);
22283
22440
  }
22284
22441
  async runPlan(basePrompt, opts) {
@@ -22613,30 +22770,41 @@ class CLIInteractionPlugin {
22613
22770
  }
22614
22771
  async send(request) {
22615
22772
  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(`
22773
+ process.stdout.write(`
22774
+ ${"=".repeat(80)}
22775
+ `);
22776
+ process.stdout.write(`[INTERACTION] ${request.stage.toUpperCase()} \u2014 ${request.type.toUpperCase()}
22777
+ `);
22778
+ process.stdout.write(`${"=".repeat(80)}
22779
+ `);
22780
+ process.stdout.write(`
22621
22781
  ${request.summary}
22782
+
22622
22783
  `);
22623
22784
  if (request.detail) {
22624
- console.log(request.detail);
22625
- console.log("");
22785
+ process.stdout.write(`${request.detail}
22786
+ `);
22787
+ process.stdout.write(`
22788
+ `);
22626
22789
  }
22627
22790
  if (request.options && request.options.length > 0) {
22628
- console.log("Options:");
22791
+ process.stdout.write(`Options:
22792
+ `);
22629
22793
  for (const opt of request.options) {
22630
22794
  const desc = opt.description ? ` \u2014 ${opt.description}` : "";
22631
- console.log(` [${opt.key}] ${opt.label}${desc}`);
22795
+ process.stdout.write(` [${opt.key}] ${opt.label}${desc}
22796
+ `);
22632
22797
  }
22633
- console.log("");
22798
+ process.stdout.write(`
22799
+ `);
22634
22800
  }
22635
22801
  if (request.timeout) {
22636
22802
  const timeoutSec = Math.floor(request.timeout / 1000);
22637
- console.log(`[Timeout: ${timeoutSec}s | Fallback: ${request.fallback}]`);
22803
+ process.stdout.write(`[Timeout: ${timeoutSec}s | Fallback: ${request.fallback}]
22804
+ `);
22638
22805
  }
22639
- console.log(`${"=".repeat(80)}
22806
+ process.stdout.write(`${"=".repeat(80)}
22807
+
22640
22808
  `);
22641
22809
  }
22642
22810
  async receive(requestId, timeout = 60000) {
@@ -26414,59 +26582,6 @@ ${stderr}` };
26414
26582
  };
26415
26583
  });
26416
26584
 
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
26585
  // src/quality/runner.ts
26471
26586
  var {spawn: spawn2 } = globalThis.Bun;
26472
26587
  async function runQualityCommand(opts) {
@@ -26769,7 +26884,60 @@ Do NOT add new features \u2014 only fix the identified issues.
26769
26884
  Commit your fixes when done.${scopeConstraint}`;
26770
26885
  }
26771
26886
 
26772
- // src/review/dialogue.ts
26887
+ // src/agents/claude/index.ts
26888
+ var init_claude = __esm(() => {
26889
+ init_adapter3();
26890
+ init_execution();
26891
+ });
26892
+
26893
+ // src/agents/shared/validation.ts
26894
+ function validateAgentForTier(agent, tier) {
26895
+ return agent.capabilities.supportedTiers.includes(tier);
26896
+ }
26897
+ function validateAgentFeature(agent, feature) {
26898
+ return agent.capabilities.features.has(feature);
26899
+ }
26900
+ function describeAgentCapabilities(agent) {
26901
+ const tiers = agent.capabilities.supportedTiers.join(",");
26902
+ const features = Array.from(agent.capabilities.features).join(",");
26903
+ const maxTokens = agent.capabilities.maxContextTokens;
26904
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
26905
+ }
26906
+
26907
+ // src/agents/index.ts
26908
+ var exports_agents = {};
26909
+ __export(exports_agents, {
26910
+ validateAgentForTier: () => validateAgentForTier,
26911
+ validateAgentFeature: () => validateAgentFeature,
26912
+ parseTokenUsage: () => parseTokenUsage,
26913
+ getInstalledAgents: () => getInstalledAgents,
26914
+ getAllAgentNames: () => getAllAgentNames,
26915
+ getAgentVersions: () => getAgentVersions,
26916
+ getAgentVersion: () => getAgentVersion,
26917
+ getAgent: () => getAgent,
26918
+ formatCostWithConfidence: () => formatCostWithConfidence,
26919
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
26920
+ estimateCostFromOutput: () => estimateCostFromOutput,
26921
+ estimateCostByDuration: () => estimateCostByDuration,
26922
+ estimateCost: () => estimateCost,
26923
+ describeAgentCapabilities: () => describeAgentCapabilities,
26924
+ checkAgentHealth: () => checkAgentHealth,
26925
+ MODEL_PRICING: () => MODEL_PRICING,
26926
+ CompleteError: () => CompleteError,
26927
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
26928
+ COST_RATES: () => COST_RATES,
26929
+ AllAgentsUnavailableError: () => AllAgentsUnavailableError
26930
+ });
26931
+ var init_agents = __esm(() => {
26932
+ init_types2();
26933
+ init_claude();
26934
+ init_registry();
26935
+ init_cost();
26936
+ init_version_detection();
26937
+ init_errors();
26938
+ });
26939
+
26940
+ // src/review/dialogue-prompts.ts
26773
26941
  function buildReviewPrompt(diff, story, _semanticConfig) {
26774
26942
  const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
26775
26943
  `);
@@ -26803,6 +26971,98 @@ function buildReReviewPrompt(updatedDiff, previousFindings) {
26803
26971
  ].join(`
26804
26972
  `);
26805
26973
  }
26974
+ function buildProposalsSection(proposals) {
26975
+ return proposals.map((p) => `### ${p.debater}
26976
+ ${p.output}`).join(`
26977
+
26978
+ `);
26979
+ }
26980
+ function buildCritiquesSection(critiques) {
26981
+ if (critiques.length === 0)
26982
+ return "";
26983
+ return `
26984
+
26985
+ ## Critiques
26986
+ ${critiques.map((c, i) => `### Critique ${i + 1}
26987
+ ${c}`).join(`
26988
+
26989
+ `)}`;
26990
+ }
26991
+ function buildVoteTallyLine(ctx) {
26992
+ if (!ctx.majorityVote)
26993
+ return "";
26994
+ const { passCount, failCount } = ctx.majorityVote;
26995
+ const failOpenNote = ctx.resolverType === "majority-fail-open" ? " (unparseable proposals count as pass)" : " (unparseable proposals count as fail)";
26996
+ return `
26997
+
26998
+ The preliminary majority vote is: **${passCount} passed, ${failCount} failed**${failOpenNote}. Verify the failing findings with tools before giving your authoritative verdict.`;
26999
+ }
27000
+ function buildResolverFraming(ctx) {
27001
+ switch (ctx.resolverType) {
27002
+ case "majority-fail-closed":
27003
+ case "majority-fail-open":
27004
+ 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.";
27005
+ case "synthesis":
27006
+ 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.";
27007
+ case "custom":
27008
+ return "You are the judge. Evaluate the debater proposals independently. Verify claims with tools (READ, GREP) and give your final authoritative verdict.";
27009
+ default:
27010
+ return "You are the reviewer. Evaluate the debater proposals and give your final authoritative verdict.";
27011
+ }
27012
+ }
27013
+ function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticConfig, resolverContext) {
27014
+ const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
27015
+ `);
27016
+ const framing = buildResolverFraming(resolverContext);
27017
+ const voteTally = buildVoteTallyLine(resolverContext);
27018
+ const proposalsSection = buildProposalsSection(proposals);
27019
+ const critiquesSection = buildCritiquesSection(critiques);
27020
+ return [
27021
+ framing,
27022
+ "",
27023
+ `## Story ${story.id}: ${story.title}`,
27024
+ "",
27025
+ "## Acceptance Criteria",
27026
+ criteria,
27027
+ "",
27028
+ "## Debater Proposals",
27029
+ proposalsSection,
27030
+ critiquesSection,
27031
+ "",
27032
+ "## Diff",
27033
+ diff,
27034
+ voteTally,
27035
+ "",
27036
+ "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
27037
+ ].filter((line) => line !== undefined).join(`
27038
+ `);
27039
+ }
27040
+ function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext) {
27041
+ const framing = buildResolverFraming(resolverContext);
27042
+ const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
27043
+ `) : "(none)";
27044
+ const proposalsSection = buildProposalsSection(proposals);
27045
+ const critiquesSection = buildCritiquesSection(critiques);
27046
+ return [
27047
+ `${framing} This is a re-review after implementer changes.`,
27048
+ "",
27049
+ "## Previous Findings",
27050
+ findingsList,
27051
+ "",
27052
+ "## Updated Debater Proposals",
27053
+ proposalsSection,
27054
+ critiquesSection,
27055
+ "",
27056
+ "## Updated Diff",
27057
+ updatedDiff,
27058
+ "",
27059
+ "Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
27060
+ "deltaSummary should describe which previous findings are resolved vs still present."
27061
+ ].filter((line) => line !== undefined).join(`
27062
+ `);
27063
+ }
27064
+
27065
+ // src/review/dialogue.ts
26806
27066
  function extractDeltaSummary(rawOutput, previousFindings, newFindings) {
26807
27067
  try {
26808
27068
  const parsed = JSON.parse(rawOutput);
@@ -26874,6 +27134,7 @@ function createReviewerSession(agent, storyId, workdir, featureName, _config) {
26874
27134
  let lastCheckResult = null;
26875
27135
  let lastStory = null;
26876
27136
  let lastSemanticConfig = null;
27137
+ let lastWasDebateResolve = false;
26877
27138
  const sessionState = {
26878
27139
  generation: 1,
26879
27140
  pendingCompactionContext: null
@@ -26936,6 +27197,7 @@ ${prompt}`,
26936
27197
  lastCheckResult = reviewResult;
26937
27198
  lastStory = story;
26938
27199
  lastSemanticConfig = semanticConfig;
27200
+ lastWasDebateResolve = false;
26939
27201
  return reviewResult;
26940
27202
  },
26941
27203
  async reReview(updatedDiff) {
@@ -27005,6 +27267,76 @@ ${prompt}`,
27005
27267
  history.push({ role: "reviewer", content: result.output });
27006
27268
  return result.output;
27007
27269
  },
27270
+ async resolveDebate(proposals, critiques, diff, story, semanticConfig, resolverContext) {
27271
+ if (!active) {
27272
+ throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
27273
+ }
27274
+ const prompt = buildDebateResolverPrompt(proposals, critiques, diff, story, semanticConfig, resolverContext);
27275
+ const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(semanticConfig);
27276
+ const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
27277
+ const result = await agent.run({
27278
+ prompt: effectivePrompt,
27279
+ workdir,
27280
+ modelTier,
27281
+ modelDef,
27282
+ timeoutSeconds,
27283
+ sessionRole: "reviewer",
27284
+ keepSessionOpen: true,
27285
+ pipelineStage: "review",
27286
+ config: _config,
27287
+ storyId,
27288
+ featureName,
27289
+ acpSessionName
27290
+ });
27291
+ history.push({ role: "implementer", content: prompt });
27292
+ history.push({ role: "reviewer", content: result.output });
27293
+ const parsed = parseReviewResponse(result.output);
27294
+ const reviewResult = { ...parsed, cost: result.estimatedCost ?? 0 };
27295
+ lastCheckResult = reviewResult;
27296
+ lastStory = story;
27297
+ lastSemanticConfig = semanticConfig;
27298
+ lastWasDebateResolve = true;
27299
+ return reviewResult;
27300
+ },
27301
+ async reReviewDebate(proposals, critiques, updatedDiff, resolverContext) {
27302
+ if (!active) {
27303
+ throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
27304
+ }
27305
+ if (!lastCheckResult || !lastSemanticConfig || !lastWasDebateResolve) {
27306
+ throw new NaxError(`[dialogue] reReviewDebate() called before any resolveDebate() on story ${storyId}`, "NO_REVIEW_RESULT", { stage: "review", storyId });
27307
+ }
27308
+ const previousFindings = lastCheckResult.checkResult.findings;
27309
+ const prompt = buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext);
27310
+ const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(lastSemanticConfig);
27311
+ const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
27312
+ const result = await agent.run({
27313
+ prompt: effectivePrompt,
27314
+ workdir,
27315
+ modelTier,
27316
+ modelDef,
27317
+ timeoutSeconds,
27318
+ sessionRole: "reviewer",
27319
+ keepSessionOpen: true,
27320
+ pipelineStage: "review",
27321
+ config: _config,
27322
+ storyId,
27323
+ featureName,
27324
+ acpSessionName
27325
+ });
27326
+ history.push({ role: "implementer", content: prompt });
27327
+ history.push({ role: "reviewer", content: result.output });
27328
+ const parsed = parseReviewResponse(result.output);
27329
+ const deltaSummary = extractDeltaSummary(result.output, previousFindings, parsed.checkResult.findings);
27330
+ const dialogueResult = { ...parsed, deltaSummary, cost: result.estimatedCost ?? 0 };
27331
+ lastCheckResult = dialogueResult;
27332
+ const maxMessages = _config.review?.dialogue?.maxDialogueMessages ?? 20;
27333
+ if (history.length > maxMessages) {
27334
+ const compactedSummary = compactHistory(history);
27335
+ sessionState.generation++;
27336
+ sessionState.pendingCompactionContext = compactedSummary;
27337
+ }
27338
+ return dialogueResult;
27339
+ },
27008
27340
  getVerdict() {
27009
27341
  if (!lastCheckResult || !lastStory) {
27010
27342
  throw new NaxError(`[dialogue] getVerdict() called before any review() on story ${storyId}`, "NO_REVIEW_RESULT", { stage: "review", storyId });
@@ -27240,10 +27572,8 @@ var init_language_commands = __esm(() => {
27240
27572
  // src/review/semantic.ts
27241
27573
  var {spawn: spawn3 } = globalThis.Bun;
27242
27574
  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
- }
27575
+ const merged = [...new Set([...excludePatterns, ...ALWAYS_EXCLUDED])];
27576
+ const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`, "--", ".", ...merged];
27247
27577
  const proc = _semanticDeps.spawn({
27248
27578
  cmd,
27249
27579
  cwd: workdir,
@@ -27403,7 +27733,7 @@ function toReviewFindings(findings) {
27403
27733
  source: "semantic-review"
27404
27734
  }));
27405
27735
  }
27406
- async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig, featureName) {
27736
+ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig, featureName, resolverSession) {
27407
27737
  const startTime = Date.now();
27408
27738
  const logger = getSafeLogger();
27409
27739
  if (featureName === undefined) {
@@ -27473,6 +27803,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27473
27803
  const reviewDebateEnabled = naxConfig?.debate?.enabled && naxConfig?.debate?.stages?.review?.enabled;
27474
27804
  if (reviewDebateEnabled) {
27475
27805
  const reviewStageConfig = naxConfig?.debate?.stages.review;
27806
+ const isReReview = resolverSession !== undefined && resolverSession.history.length > 0;
27476
27807
  const debateSession = _semanticDeps.createDebateSession({
27477
27808
  storyId: story.id,
27478
27809
  stage: "review",
@@ -27480,26 +27811,69 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27480
27811
  config: naxConfig ?? DEFAULT_CONFIG,
27481
27812
  workdir,
27482
27813
  featureName,
27483
- timeoutSeconds: naxConfig?.execution?.sessionTimeoutSeconds
27814
+ timeoutSeconds: naxConfig?.execution?.sessionTimeoutSeconds,
27815
+ reviewerSession: resolverSession,
27816
+ resolverContextInput: resolverSession ? {
27817
+ diff,
27818
+ story: { id: story.id, title: story.title, acceptanceCriteria: story.acceptanceCriteria },
27819
+ semanticConfig,
27820
+ resolverType: reviewStageConfig.resolver.type,
27821
+ isReReview
27822
+ } : undefined
27484
27823
  });
27824
+ const historyLenBefore = resolverSession?.history.length ?? 0;
27485
27825
  const debateResult = await debateSession.run(prompt);
27486
27826
  const debateCost = debateResult.totalCostUsd ?? 0;
27487
- let passCount = 0;
27488
- let failCount = 0;
27827
+ const sessionUsed = resolverSession && resolverSession.history.length > historyLenBefore;
27828
+ if (sessionUsed) {
27829
+ const durationMs3 = Date.now() - startTime;
27830
+ try {
27831
+ const verdict = resolverSession.getVerdict();
27832
+ const findings = verdict.findings ?? [];
27833
+ if (!verdict.passed && findings.length > 0) {
27834
+ logger?.warn("review", `Semantic review failed (debate+dialogue): ${findings.length} findings`, {
27835
+ storyId: story.id,
27836
+ durationMs: durationMs3
27837
+ });
27838
+ return {
27839
+ check: "semantic",
27840
+ success: false,
27841
+ command: "",
27842
+ exitCode: 1,
27843
+ output: `Semantic review failed:
27844
+
27845
+ ${findings.map((f) => `${f.ruleId}: ${f.message}`).join(`
27846
+ `)}`,
27847
+ durationMs: durationMs3,
27848
+ findings,
27849
+ cost: debateCost
27850
+ };
27851
+ }
27852
+ const label = verdict.passed ? "Semantic review passed (debate+dialogue)" : "Semantic review passed (debate+dialogue, all findings non-blocking)";
27853
+ logger?.info("review", label, { storyId: story.id, durationMs: durationMs3 });
27854
+ return {
27855
+ check: "semantic",
27856
+ success: true,
27857
+ command: "",
27858
+ exitCode: 0,
27859
+ output: label,
27860
+ durationMs: durationMs3,
27861
+ cost: debateCost
27862
+ };
27863
+ } catch {
27864
+ logger?.warn("review", "getVerdict() failed after debate+dialogue \u2014 falling back to stateless verdict", {
27865
+ storyId: story.id
27866
+ });
27867
+ }
27868
+ }
27869
+ const resolverPassed = debateResult.outcome === "passed";
27489
27870
  const allFindings = [];
27490
27871
  for (const p of debateResult.proposals) {
27491
27872
  const parsed2 = parseLLMResponse(p.output);
27492
27873
  if (parsed2) {
27493
- if (parsed2.passed)
27494
- passCount++;
27495
- else
27496
- failCount++;
27497
27874
  allFindings.push(...parsed2.findings);
27498
- } else {
27499
- failCount++;
27500
27875
  }
27501
27876
  }
27502
- const majorityPassed = passCount > failCount;
27503
27877
  const seen = new Set;
27504
27878
  const deduped = [];
27505
27879
  for (const f of allFindings) {
@@ -27511,7 +27885,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27511
27885
  }
27512
27886
  const debateBlocking = deduped.filter((f) => isBlockingSeverity(f.severity));
27513
27887
  const durationMs2 = Date.now() - startTime;
27514
- if (!majorityPassed) {
27888
+ if (!resolverPassed) {
27515
27889
  if (debateBlocking.length > 0) {
27516
27890
  logger?.warn("review", `Semantic review failed (debate): ${debateBlocking.length} findings`, {
27517
27891
  storyId: story.id,
@@ -27710,7 +28084,7 @@ ${formatFindings(blockingFindings)}`;
27710
28084
  cost: llmCost
27711
28085
  };
27712
28086
  }
27713
- var _semanticDeps, DIFF_CAP_BYTES = 51200;
28087
+ var _semanticDeps, DIFF_CAP_BYTES = 51200, ALWAYS_EXCLUDED;
27714
28088
  var init_semantic = __esm(() => {
27715
28089
  init_adapter();
27716
28090
  init_config();
@@ -27724,6 +28098,7 @@ var init_semantic = __esm(() => {
27724
28098
  createDebateSession: (opts) => new DebateSession(opts),
27725
28099
  readAcpSession
27726
28100
  };
28101
+ ALWAYS_EXCLUDED = [":!.nax/", ":!.nax-pids"];
27727
28102
  });
27728
28103
 
27729
28104
  // src/review/runner.ts
@@ -27808,7 +28183,7 @@ async function getUncommittedFilesImpl(workdir) {
27808
28183
  return [];
27809
28184
  }
27810
28185
  }
27811
- async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName) {
28186
+ async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession) {
27812
28187
  const startTime = Date.now();
27813
28188
  const logger = getSafeLogger();
27814
28189
  const checks3 = [];
@@ -27865,10 +28240,19 @@ Stage and commit these files before running review.`
27865
28240
  modelTier: "balanced",
27866
28241
  rules: [],
27867
28242
  timeoutMs: 600000,
27868
- excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
28243
+ excludePatterns: [
28244
+ ":!test/",
28245
+ ":!tests/",
28246
+ ":!*_test.go",
28247
+ ":!*.test.ts",
28248
+ ":!*.spec.ts",
28249
+ ":!**/__tests__/",
28250
+ ":!.nax/",
28251
+ ":!.nax-pids"
28252
+ ]
27869
28253
  };
27870
28254
  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);
28255
+ const result2 = await runSemantic(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null), naxConfig, featureName, resolverSession);
27872
28256
  checks3.push(result2);
27873
28257
  if (!result2.success && !firstFailure) {
27874
28258
  firstFailure = `${checkName} failed`;
@@ -27950,9 +28334,9 @@ async function getChangedFiles(workdir, baseRef) {
27950
28334
  }
27951
28335
 
27952
28336
  class ReviewOrchestrator {
27953
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig, retrySkipChecks, featureName) {
28337
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession) {
27954
28338
  const logger = getSafeLogger();
27955
- const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName);
28339
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks, featureName, resolverSession);
27956
28340
  if (!builtIn.success) {
27957
28341
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
27958
28342
  }
@@ -28019,12 +28403,14 @@ class ReviewOrchestrator {
28019
28403
  const agentResolver = ctx.agentGetFn ?? undefined;
28020
28404
  const agentName = ctx.rootConfig.autoMode?.defaultAgent;
28021
28405
  const modelResolver = agentName ? (_tier) => agentResolver ? agentResolver(agentName) ?? null : null : undefined;
28406
+ const reviewDebateEnabled = ctx.rootConfig?.debate?.enabled && ctx.rootConfig?.debate?.stages?.review?.enabled;
28407
+ const resolverSession = reviewDebateEnabled ? ctx.reviewerSession : undefined;
28022
28408
  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
28409
  id: ctx.story.id,
28024
28410
  title: ctx.story.title,
28025
28411
  description: ctx.story.description,
28026
28412
  acceptanceCriteria: ctx.story.acceptanceCriteria
28027
- }, modelResolver, ctx.config, retrySkipChecks, ctx.prd.feature);
28413
+ }, modelResolver, ctx.config, retrySkipChecks, ctx.prd.feature, resolverSession);
28028
28414
  }
28029
28415
  }
28030
28416
  var _orchestratorDeps, reviewOrchestrator;
@@ -28053,11 +28439,12 @@ var init_review = __esm(() => {
28053
28439
  enabled: (ctx) => ctx.config.review.enabled,
28054
28440
  async execute(ctx) {
28055
28441
  const logger = getLogger();
28442
+ const reviewDebateEnabled = ctx.rootConfig?.debate?.enabled && ctx.rootConfig?.debate?.stages?.review?.enabled;
28056
28443
  const dialogueEnabled = ctx.config.review?.dialogue?.enabled ?? false;
28057
28444
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
28058
28445
  const agentResolver = ctx.agentGetFn ?? getAgent;
28059
28446
  const agentName = ctx.rootConfig.autoMode?.defaultAgent;
28060
- if (dialogueEnabled && ctx.reviewerSession) {
28447
+ if (dialogueEnabled && !reviewDebateEnabled && ctx.reviewerSession) {
28061
28448
  try {
28062
28449
  const diff = ctx.storyGitRef ?? "";
28063
28450
  const reReviewResult = await ctx.reviewerSession.reReview(diff);
@@ -28095,47 +28482,49 @@ var init_review = __esm(() => {
28095
28482
  if (dialogueEnabled && !ctx.reviewerSession) {
28096
28483
  const agent = agentName ? agentResolver(agentName) ?? null : null;
28097
28484
  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(`
28485
+ if (!reviewDebateEnabled) {
28486
+ const semanticConfig = ctx.config.review?.semantic;
28487
+ if (semanticConfig && agent) {
28488
+ try {
28489
+ const diff = ctx.storyGitRef ?? "";
28490
+ const story = {
28491
+ id: ctx.story.id,
28492
+ title: ctx.story.title,
28493
+ description: ctx.story.description,
28494
+ acceptanceCriteria: ctx.story.acceptanceCriteria
28495
+ };
28496
+ const sessionResult = await ctx.reviewerSession.review(diff, story, semanticConfig);
28497
+ const passed = sessionResult.checkResult.success;
28498
+ ctx.reviewResult = {
28499
+ success: passed,
28500
+ checks: passed ? [] : [
28501
+ {
28502
+ check: "semantic",
28503
+ success: false,
28504
+ command: "reviewer-session-review",
28505
+ exitCode: 1,
28506
+ output: sessionResult.checkResult.findings.map((f) => f.message).join(`
28119
28507
  `),
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", {
28508
+ durationMs: 0,
28509
+ findings: sessionResult.checkResult.findings
28510
+ }
28511
+ ],
28512
+ totalDurationMs: 0
28513
+ };
28514
+ const dialogueCost = sessionResult.cost ?? 0;
28515
+ if (passed) {
28516
+ logger.info("review", "Review passed (dialogue session)", { storyId: ctx.story.id });
28517
+ } else {
28518
+ logger.warn("review", "Review failed (dialogue session) \u2014 handing off to autofix", {
28519
+ storyId: ctx.story.id
28520
+ });
28521
+ }
28522
+ return { action: "continue", cost: dialogueCost || undefined };
28523
+ } catch (err) {
28524
+ logger.warn("review", "ReviewerSession.review() failed \u2014 falling back to one-shot review", {
28131
28525
  storyId: ctx.story.id
28132
28526
  });
28133
28527
  }
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
28528
  }
28140
28529
  }
28141
28530
  }
@@ -28239,7 +28628,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28239
28628
  }
28240
28629
  const remainingBudget = maxTotal - consumed;
28241
28630
  const maxAttempts = Math.min(maxPerCycle, remainingBudget);
28242
- const agentGetFn = ctx.agentGetFn ?? _autofixDeps.getAgent;
28631
+ const agentGetFn = ctx.agentGetFn ?? ((name) => _autofixDeps.getAgent(name, ctx.rootConfig));
28243
28632
  const loopState = {
28244
28633
  attempt: 0,
28245
28634
  failedChecks
@@ -28382,7 +28771,7 @@ async function runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWor
28382
28771
  }
28383
28772
  var CLARIFY_REGEX, autofixStage, _autofixDeps;
28384
28773
  var init_autofix = __esm(() => {
28385
- init_agents();
28774
+ init_registry();
28386
28775
  init_config();
28387
28776
  init_loader();
28388
28777
  init_logger2();
@@ -28458,6 +28847,14 @@ var init_autofix = __esm(() => {
28458
28847
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
28459
28848
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
28460
28849
  if (recheckPassed) {
28850
+ const passedChecks = (ctx.reviewResult?.checks ?? []).filter((c) => c.success).map((c) => c.check);
28851
+ if (passedChecks.length > 0) {
28852
+ ctx.retrySkipChecks = new Set(passedChecks);
28853
+ logger.debug("autofix", "Skipping already-passed checks on retry", {
28854
+ storyId: ctx.story.id,
28855
+ skippedChecks: passedChecks
28856
+ });
28857
+ }
28461
28858
  logger.info("autofix", "Mechanical autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
28462
28859
  return { action: "retry", fromStage: "review" };
28463
28860
  }
@@ -28489,7 +28886,7 @@ var init_autofix = __esm(() => {
28489
28886
  }
28490
28887
  };
28491
28888
  _autofixDeps = {
28492
- getAgent,
28889
+ getAgent: (name, config2) => createAgentRegistry(config2).getAgent(name),
28493
28890
  runQualityCommand,
28494
28891
  recheckReview,
28495
28892
  runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir),
@@ -28583,7 +28980,7 @@ var init_completion = __esm(() => {
28583
28980
  const logger = getLogger();
28584
28981
  const isBatch = ctx.stories.length > 1;
28585
28982
  const sessionCost = ctx.agentResult?.estimatedCost || 0;
28586
- const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
28983
+ const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
28587
28984
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
28588
28985
  if (isBatch) {
28589
28986
  ctx.storyMetrics = collectBatchMetrics(ctx, storyStartTime);
@@ -28634,7 +29031,7 @@ var init_completion = __esm(() => {
28634
29031
  };
28635
29032
  await _completionDeps.persistSemanticVerdict(ctx.featureDir, ctx.story.id, verdict);
28636
29033
  }
28637
- await savePRD(ctx.prd, prdPath);
29034
+ await _completionDeps.savePRD(ctx.prd, prdPath);
28638
29035
  const updatedCounts = countStories(ctx.prd);
28639
29036
  logger.info("completion", "Progress update", {
28640
29037
  storyId: ctx.story.id,
@@ -28653,7 +29050,8 @@ var init_completion = __esm(() => {
28653
29050
  };
28654
29051
  _completionDeps = {
28655
29052
  checkReviewGate,
28656
- persistSemanticVerdict
29053
+ persistSemanticVerdict,
29054
+ savePRD
28657
29055
  };
28658
29056
  });
28659
29057
 
@@ -35735,7 +36133,7 @@ var package_default;
35735
36133
  var init_package = __esm(() => {
35736
36134
  package_default = {
35737
36135
  name: "@nathapp/nax",
35738
- version: "0.59.0",
36136
+ version: "0.59.1",
35739
36137
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
35740
36138
  type: "module",
35741
36139
  bin: {
@@ -35815,8 +36213,8 @@ var init_version = __esm(() => {
35815
36213
  NAX_VERSION = package_default.version;
35816
36214
  NAX_COMMIT = (() => {
35817
36215
  try {
35818
- if (/^[0-9a-f]{6,10}$/.test("13990b5b"))
35819
- return "13990b5b";
36216
+ if (/^[0-9a-f]{6,10}$/.test("b8492d03"))
36217
+ return "b8492d03";
35820
36218
  } catch {}
35821
36219
  try {
35822
36220
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -40496,7 +40894,7 @@ function validateStoryCount(counts, config2) {
40496
40894
  }
40497
40895
  function logActiveProtocol(config2) {
40498
40896
  const logger = getSafeLogger();
40499
- const protocol = config2.agent?.protocol ?? "cli";
40897
+ const protocol = config2.agent?.protocol;
40500
40898
  logger?.info("run-initialization", `Agent protocol: ${protocol}`, { protocol });
40501
40899
  }
40502
40900
  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.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {