@nathapp/nax 0.39.0 → 0.39.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.
package/dist/nax.js CHANGED
@@ -18281,7 +18281,8 @@ var init_schemas3 = __esm(() => {
18281
18281
  typecheck: exports_external.string().optional(),
18282
18282
  lint: exports_external.string().optional(),
18283
18283
  test: exports_external.string().optional()
18284
- })
18284
+ }),
18285
+ pluginMode: exports_external.enum(["per-story", "deferred"]).default("per-story")
18285
18286
  });
18286
18287
  PlanConfigSchema = exports_external.object({
18287
18288
  model: ModelTierSchema,
@@ -18529,7 +18530,8 @@ var init_defaults = __esm(() => {
18529
18530
  review: {
18530
18531
  enabled: true,
18531
18532
  checks: ["typecheck", "lint"],
18532
- commands: {}
18533
+ commands: {},
18534
+ pluginMode: "per-story"
18533
18535
  },
18534
18536
  plan: {
18535
18537
  model: "balanced",
@@ -20159,7 +20161,9 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
20159
20161
  const logger = getSafeLogger();
20160
20162
  try {
20161
20163
  logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
20162
- await routeBatch(stories, { config: config2 });
20164
+ const agentName = config2.execution?.agent ?? "claude";
20165
+ const adapter = getAgent(agentName);
20166
+ await routeBatch(stories, { config: config2, adapter });
20163
20167
  logger?.debug("routing", "LLM batch routing complete", { label });
20164
20168
  } catch (err) {
20165
20169
  logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
@@ -20169,6 +20173,7 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
20169
20173
  }
20170
20174
  }
20171
20175
  var init_batch_route = __esm(() => {
20176
+ init_registry();
20172
20177
  init_logger2();
20173
20178
  init_llm();
20174
20179
  });
@@ -20793,7 +20798,7 @@ var package_default;
20793
20798
  var init_package = __esm(() => {
20794
20799
  package_default = {
20795
20800
  name: "@nathapp/nax",
20796
- version: "0.39.0",
20801
+ version: "0.39.2",
20797
20802
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20798
20803
  type: "module",
20799
20804
  bin: {
@@ -20857,8 +20862,8 @@ var init_version = __esm(() => {
20857
20862
  NAX_VERSION = package_default.version;
20858
20863
  NAX_COMMIT = (() => {
20859
20864
  try {
20860
- if (/^[0-9a-f]{6,10}$/.test("e6f293e"))
20861
- return "e6f293e";
20865
+ if (/^[0-9a-f]{6,10}$/.test("d6c0898"))
20866
+ return "d6c0898";
20862
20867
  } catch {}
20863
20868
  try {
20864
20869
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22813,16 +22818,20 @@ async function getChangedFiles(workdir, baseRef) {
22813
22818
  }
22814
22819
 
22815
22820
  class ReviewOrchestrator {
22816
- async review(reviewConfig, workdir, executionConfig, plugins) {
22821
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef) {
22817
22822
  const logger = getSafeLogger();
22818
22823
  const builtIn = await runReview(reviewConfig, workdir, executionConfig);
22819
22824
  if (!builtIn.success) {
22820
22825
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
22821
22826
  }
22827
+ if (reviewConfig.pluginMode === "deferred") {
22828
+ logger?.debug("review", "Plugin reviewers deferred \u2014 skipping per-story execution");
22829
+ return { builtIn, success: true, pluginFailed: false };
22830
+ }
22822
22831
  if (plugins) {
22823
22832
  const reviewers = plugins.getReviewers();
22824
22833
  if (reviewers.length > 0) {
22825
- const baseRef = executionConfig?.storyGitRef;
22834
+ const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
22826
22835
  const changedFiles = await getChangedFiles(workdir, baseRef);
22827
22836
  const pluginResults = [];
22828
22837
  for (const reviewer of reviewers) {
@@ -22897,7 +22906,7 @@ var init_review = __esm(() => {
22897
22906
  async execute(ctx) {
22898
22907
  const logger = getLogger();
22899
22908
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
22900
- const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins);
22909
+ const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins, ctx.storyGitRef);
22901
22910
  ctx.reviewResult = result.builtIn;
22902
22911
  if (!result.success) {
22903
22912
  const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
@@ -31461,6 +31470,78 @@ var init_reporters = __esm(() => {
31461
31470
  init_logger2();
31462
31471
  });
31463
31472
 
31473
+ // src/execution/deferred-review.ts
31474
+ var {spawn: spawn3 } = globalThis.Bun;
31475
+ async function captureRunStartRef(workdir) {
31476
+ try {
31477
+ const proc = _deferredReviewDeps.spawn({
31478
+ cmd: ["git", "rev-parse", "HEAD"],
31479
+ cwd: workdir,
31480
+ stdout: "pipe",
31481
+ stderr: "pipe"
31482
+ });
31483
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
31484
+ return stdout.trim();
31485
+ } catch {
31486
+ return "";
31487
+ }
31488
+ }
31489
+ async function getChangedFilesForDeferred(workdir, baseRef) {
31490
+ try {
31491
+ const proc = _deferredReviewDeps.spawn({
31492
+ cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
31493
+ cwd: workdir,
31494
+ stdout: "pipe",
31495
+ stderr: "pipe"
31496
+ });
31497
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
31498
+ return stdout.trim().split(`
31499
+ `).filter(Boolean);
31500
+ } catch {
31501
+ return [];
31502
+ }
31503
+ }
31504
+ async function runDeferredReview(workdir, reviewConfig, plugins, runStartRef) {
31505
+ if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
31506
+ return;
31507
+ }
31508
+ const reviewers = plugins.getReviewers();
31509
+ if (reviewers.length === 0) {
31510
+ return;
31511
+ }
31512
+ const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
31513
+ const reviewerResults = [];
31514
+ let anyFailed = false;
31515
+ for (const reviewer of reviewers) {
31516
+ try {
31517
+ const result = await reviewer.check(workdir, changedFiles);
31518
+ reviewerResults.push({
31519
+ name: reviewer.name,
31520
+ passed: result.passed,
31521
+ output: result.output,
31522
+ exitCode: result.exitCode
31523
+ });
31524
+ if (!result.passed) {
31525
+ anyFailed = true;
31526
+ }
31527
+ } catch (error48) {
31528
+ const errorMsg = error48 instanceof Error ? error48.message : String(error48);
31529
+ reviewerResults.push({
31530
+ name: reviewer.name,
31531
+ passed: false,
31532
+ output: "",
31533
+ error: errorMsg
31534
+ });
31535
+ anyFailed = true;
31536
+ }
31537
+ }
31538
+ return { runStartRef, changedFiles, reviewerResults, anyFailed };
31539
+ }
31540
+ var _deferredReviewDeps;
31541
+ var init_deferred_review = __esm(() => {
31542
+ _deferredReviewDeps = { spawn: spawn3 };
31543
+ });
31544
+
31464
31545
  // src/execution/dry-run.ts
31465
31546
  async function handleDryRun(ctx) {
31466
31547
  const logger = getSafeLogger();
@@ -31644,22 +31725,6 @@ function resolveMaxAttemptsOutcome(failureCategory) {
31644
31725
  return "fail";
31645
31726
  }
31646
31727
  }
31647
- async function tryLlmBatchRoute2(config2, stories, label = "routing") {
31648
- const mode = config2.routing.llm?.mode ?? "hybrid";
31649
- if (config2.routing.strategy !== "llm" || mode === "per-story" || stories.length === 0)
31650
- return;
31651
- const logger = getSafeLogger();
31652
- try {
31653
- logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
31654
- await routeBatch(stories, { config: config2 });
31655
- logger?.debug("routing", "LLM batch routing complete", { label });
31656
- } catch (err) {
31657
- logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
31658
- error: err.message,
31659
- label
31660
- });
31661
- }
31662
- }
31663
31728
  function shouldRetrySameTier(verifyResult) {
31664
31729
  return verifyResult?.status === "RUNTIME_CRASH";
31665
31730
  }
@@ -31739,7 +31804,7 @@ async function handleTierEscalation(ctx) {
31739
31804
  clearCacheForStory(story.id);
31740
31805
  }
31741
31806
  if (routingMode === "hybrid") {
31742
- await tryLlmBatchRoute2(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
31807
+ await tryLlmBatchRoute(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
31743
31808
  }
31744
31809
  return {
31745
31810
  outcome: "escalated",
@@ -31752,6 +31817,7 @@ var init_tier_escalation = __esm(() => {
31752
31817
  init_hooks();
31753
31818
  init_logger2();
31754
31819
  init_prd();
31820
+ init_batch_route();
31755
31821
  init_llm();
31756
31822
  init_escalation();
31757
31823
  init_helpers();
@@ -32062,6 +32128,8 @@ async function executeSequential(ctx, initialPrd) {
32062
32128
  ];
32063
32129
  const allStoryMetrics = [];
32064
32130
  let warningSent = false;
32131
+ let deferredReview;
32132
+ const runStartRef = await captureRunStartRef(ctx.workdir);
32065
32133
  pipelineEventBus.clear();
32066
32134
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
32067
32135
  wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime);
@@ -32074,7 +32142,8 @@ async function executeSequential(ctx, initialPrd) {
32074
32142
  storiesCompleted,
32075
32143
  totalCost,
32076
32144
  allStoryMetrics,
32077
- exitReason
32145
+ exitReason,
32146
+ deferredReview
32078
32147
  });
32079
32148
  startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
32080
32149
  try {
@@ -32093,6 +32162,7 @@ async function executeSequential(ctx, initialPrd) {
32093
32162
  return buildResult2("pre-merge-aborted");
32094
32163
  }
32095
32164
  }
32165
+ deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
32096
32166
  return buildResult2("completed");
32097
32167
  }
32098
32168
  const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
@@ -32176,6 +32246,7 @@ var init_sequential_executor = __esm(() => {
32176
32246
  init_reporters();
32177
32247
  init_prd();
32178
32248
  init_crash_recovery();
32249
+ init_deferred_review();
32179
32250
  init_iteration_runner();
32180
32251
  init_story_selector();
32181
32252
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.39.0",
3
+ "version": "0.39.2",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -116,6 +116,7 @@ export const DEFAULT_CONFIG: NaxConfig = {
116
116
  enabled: true,
117
117
  checks: ["typecheck", "lint"],
118
118
  commands: {},
119
+ pluginMode: "per-story",
119
120
  },
120
121
  plan: {
121
122
  model: "balanced",
@@ -216,6 +216,8 @@ export interface ReviewConfig {
216
216
  lint?: string;
217
217
  test?: string;
218
218
  };
219
+ /** Plugin reviewer mode: "per-story" (run after each story) or "deferred" (run once at end of run, default: "per-story") */
220
+ pluginMode?: "per-story" | "deferred";
219
221
  }
220
222
 
221
223
  /** Plan config */
@@ -170,6 +170,7 @@ const ReviewConfigSchema = z.object({
170
170
  lint: z.string().optional(),
171
171
  test: z.string().optional(),
172
172
  }),
173
+ pluginMode: z.enum(["per-story", "deferred"]).default("per-story"),
173
174
  });
174
175
 
175
176
  const PlanConfigSchema = z.object({
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Deferred Plugin Review (DR-003)
3
+ *
4
+ * Captures the run-start git ref and runs all plugin reviewers once after
5
+ * all stories complete, using the full diff from run-start to HEAD.
6
+ */
7
+
8
+ import { spawn } from "bun";
9
+ import type { PluginRegistry } from "../plugins";
10
+ import type { ReviewConfig } from "../review/types";
11
+
12
+ /** Injectable deps for testing */
13
+ export const _deferredReviewDeps = { spawn };
14
+
15
+ export interface DeferredReviewResult {
16
+ runStartRef: string;
17
+ changedFiles: string[];
18
+ reviewerResults: Array<{
19
+ name: string;
20
+ passed: boolean;
21
+ output: string;
22
+ exitCode?: number;
23
+ error?: string;
24
+ }>;
25
+ anyFailed: boolean;
26
+ }
27
+
28
+ /** Capture the current HEAD git ref. Returns "" on failure. */
29
+ export async function captureRunStartRef(workdir: string): Promise<string> {
30
+ try {
31
+ const proc = _deferredReviewDeps.spawn({
32
+ cmd: ["git", "rev-parse", "HEAD"],
33
+ cwd: workdir,
34
+ stdout: "pipe",
35
+ stderr: "pipe",
36
+ });
37
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
38
+ return stdout.trim();
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+
44
+ async function getChangedFilesForDeferred(workdir: string, baseRef: string): Promise<string[]> {
45
+ try {
46
+ const proc = _deferredReviewDeps.spawn({
47
+ cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
48
+ cwd: workdir,
49
+ stdout: "pipe",
50
+ stderr: "pipe",
51
+ });
52
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
53
+ return stdout.trim().split("\n").filter(Boolean);
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ /** Run all plugin reviewers once with the full diff since runStartRef. */
60
+ export async function runDeferredReview(
61
+ workdir: string,
62
+ reviewConfig: ReviewConfig,
63
+ plugins: PluginRegistry,
64
+ runStartRef: string,
65
+ ): Promise<DeferredReviewResult | undefined> {
66
+ if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
67
+ return undefined;
68
+ }
69
+
70
+ const reviewers = plugins.getReviewers();
71
+ if (reviewers.length === 0) {
72
+ return undefined;
73
+ }
74
+
75
+ const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
76
+
77
+ const reviewerResults: DeferredReviewResult["reviewerResults"] = [];
78
+ let anyFailed = false;
79
+
80
+ for (const reviewer of reviewers) {
81
+ try {
82
+ const result = await reviewer.check(workdir, changedFiles);
83
+ reviewerResults.push({
84
+ name: reviewer.name,
85
+ passed: result.passed,
86
+ output: result.output,
87
+ exitCode: result.exitCode,
88
+ });
89
+ if (!result.passed) {
90
+ anyFailed = true;
91
+ }
92
+ } catch (error) {
93
+ const errorMsg = error instanceof Error ? error.message : String(error);
94
+ reviewerResults.push({
95
+ name: reviewer.name,
96
+ passed: false,
97
+ output: "",
98
+ error: errorMsg,
99
+ });
100
+ anyFailed = true;
101
+ }
102
+ }
103
+
104
+ return { runStartRef, changedFiles, reviewerResults, anyFailed };
105
+ }
@@ -12,7 +12,8 @@ import { type LoadedHooksConfig, fireHook } from "../../hooks";
12
12
  import { getSafeLogger } from "../../logger";
13
13
  import type { PRD, StructuredFailure, UserStory } from "../../prd";
14
14
  import { markStoryFailed, savePRD } from "../../prd";
15
- import { clearCacheForStory, routeBatch as llmRouteBatch } from "../../routing/strategies/llm";
15
+ import { tryLlmBatchRoute } from "../../routing/batch-route";
16
+ import { clearCacheForStory } from "../../routing/strategies/llm";
16
17
  import type { FailureCategory } from "../../tdd/types";
17
18
  import { calculateMaxIterations, escalateTier, getTierConfig } from "../escalation";
18
19
  import { hookCtx } from "../helpers";
@@ -172,25 +173,6 @@ export async function preIterationTierCheck(
172
173
  return { shouldSkipIteration: true, prdDirty: true, prd: failedPrd };
173
174
  }
174
175
 
175
- /**
176
- * Try LLM batch routing for ready stories. Logs and swallows errors (falls back to per-story routing).
177
- */
178
- async function tryLlmBatchRoute(config: NaxConfig, stories: UserStory[], label = "routing"): Promise<void> {
179
- const mode = config.routing.llm?.mode ?? "hybrid";
180
- if (config.routing.strategy !== "llm" || mode === "per-story" || stories.length === 0) return;
181
- const logger = getSafeLogger();
182
- try {
183
- logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
184
- await llmRouteBatch(stories, { config });
185
- logger?.debug("routing", "LLM batch routing complete", { label });
186
- } catch (err) {
187
- logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
188
- error: (err as Error).message,
189
- label,
190
- });
191
- }
192
- }
193
-
194
176
  export interface EscalationHandlerContext {
195
177
  story: UserStory;
196
178
  storiesToExecute: UserStory[];
@@ -13,6 +13,7 @@ import type { RoutingResult } from "../pipeline/types";
13
13
  import type { PluginRegistry } from "../plugins";
14
14
  import type { PRD, UserStory } from "../prd/types";
15
15
  import type { StoryBatch } from "./batching";
16
+ import type { DeferredReviewResult } from "./deferred-review";
16
17
  import type { StatusWriter } from "./status-writer";
17
18
 
18
19
  export interface SequentialExecutionContext {
@@ -41,6 +42,7 @@ export interface SequentialExecutionResult {
41
42
  totalCost: number;
42
43
  allStoryMetrics: StoryMetrics[];
43
44
  exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories" | "pre-merge-aborted";
45
+ deferredReview?: DeferredReviewResult;
44
46
  }
45
47
 
46
48
  /**
@@ -15,6 +15,8 @@ import type { PipelineContext } from "../pipeline/types";
15
15
  import { generateHumanHaltSummary, isComplete, isStalled, loadPRD } from "../prd";
16
16
  import type { PRD } from "../prd/types";
17
17
  import { startHeartbeat } from "./crash-recovery";
18
+ import { captureRunStartRef, runDeferredReview } from "./deferred-review";
19
+ import type { DeferredReviewResult } from "./deferred-review";
18
20
  import type { SequentialExecutionContext, SequentialExecutionResult } from "./executor-types";
19
21
  import { runIteration } from "./iteration-runner";
20
22
  import { selectNextStories } from "./story-selector";
@@ -37,6 +39,9 @@ export async function executeSequential(
37
39
  ];
38
40
  const allStoryMetrics: StoryMetrics[] = [];
39
41
  let warningSent = false;
42
+ let deferredReview: DeferredReviewResult | undefined;
43
+
44
+ const runStartRef = await captureRunStartRef(ctx.workdir);
40
45
 
41
46
  pipelineEventBus.clear();
42
47
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
@@ -52,6 +57,7 @@ export async function executeSequential(
52
57
  totalCost,
53
58
  allStoryMetrics,
54
59
  exitReason,
60
+ deferredReview,
55
61
  });
56
62
 
57
63
  startHeartbeat(
@@ -82,6 +88,7 @@ export async function executeSequential(
82
88
  return buildResult("pre-merge-aborted");
83
89
  }
84
90
  }
91
+ deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
85
92
  return buildResult("completed");
86
93
  }
87
94
 
@@ -25,7 +25,13 @@ export const reviewStage: PipelineStage = {
25
25
 
26
26
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
27
27
 
28
- const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins);
28
+ const result = await reviewOrchestrator.review(
29
+ ctx.config.review,
30
+ ctx.workdir,
31
+ ctx.config.execution,
32
+ ctx.plugins,
33
+ ctx.storyGitRef,
34
+ );
29
35
 
30
36
  ctx.reviewResult = result.builtIn;
31
37
 
@@ -74,6 +74,7 @@ export class ReviewOrchestrator {
74
74
  workdir: string,
75
75
  executionConfig: NaxConfig["execution"],
76
76
  plugins?: PluginRegistry,
77
+ storyGitRef?: string,
77
78
  ): Promise<OrchestratorReviewResult> {
78
79
  const logger = getSafeLogger();
79
80
 
@@ -83,11 +84,16 @@ export class ReviewOrchestrator {
83
84
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
84
85
  }
85
86
 
87
+ if (reviewConfig.pluginMode === "deferred") {
88
+ logger?.debug("review", "Plugin reviewers deferred — skipping per-story execution");
89
+ return { builtIn, success: true, pluginFailed: false };
90
+ }
91
+
86
92
  if (plugins) {
87
93
  const reviewers = plugins.getReviewers();
88
94
  if (reviewers.length > 0) {
89
95
  // Use the story's start ref if available to capture auto-committed changes
90
- const baseRef = executionConfig?.storyGitRef;
96
+ const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
91
97
  const changedFiles = await getChangedFiles(workdir, baseRef);
92
98
  const pluginResults: ReviewResult["pluginReviewers"] = [];
93
99
 
@@ -65,4 +65,6 @@ export interface ReviewConfig {
65
65
  lint?: string;
66
66
  test?: string;
67
67
  };
68
+ /** When to run plugin reviewers: per-story (default) or deferred (skip per-story, run once at end) */
69
+ pluginMode?: "per-story" | "deferred";
68
70
  }
@@ -2,6 +2,7 @@
2
2
  * LLM Batch Routing Helper
3
3
  */
4
4
 
5
+ import { getAgent } from "../agents/registry";
5
6
  import type { NaxConfig } from "../config";
6
7
  import { getSafeLogger } from "../logger";
7
8
  import type { UserStory } from "../prd";
@@ -21,7 +22,9 @@ export async function tryLlmBatchRoute(config: NaxConfig, stories: UserStory[],
21
22
  const logger = getSafeLogger();
22
23
  try {
23
24
  logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
24
- await llmRouteBatch(stories, { config });
25
+ const agentName = config.execution?.agent ?? "claude";
26
+ const adapter = getAgent(agentName);
27
+ await llmRouteBatch(stories, { config, adapter });
25
28
  logger?.debug("routing", "LLM batch routing complete", { label });
26
29
  } catch (err) {
27
30
  logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {