@nathapp/nax 0.56.0 → 0.56.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 +221 -77
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -18275,7 +18275,7 @@ var init_schemas3 = __esm(() => {
18275
18275
  maxIterations: exports_external.number().int().positive({ message: "maxIterations must be > 0" }),
18276
18276
  iterationDelayMs: exports_external.number().int().nonnegative(),
18277
18277
  costLimit: exports_external.number().positive({ message: "costLimit must be > 0" }),
18278
- sessionTimeoutSeconds: exports_external.number().int().positive({ message: "sessionTimeoutSeconds must be > 0" }),
18278
+ sessionTimeoutSeconds: exports_external.number().int().positive({ message: "sessionTimeoutSeconds must be > 0" }).default(3600),
18279
18279
  verificationTimeoutSeconds: exports_external.number().int().min(1).max(3600).default(300),
18280
18280
  maxStoriesPerFeature: exports_external.number().int().positive(),
18281
18281
  rectification: RectificationConfigSchema,
@@ -18472,9 +18472,9 @@ var init_schemas3 = __esm(() => {
18472
18472
  });
18473
18473
  StorySizeGateConfigSchema = exports_external.object({
18474
18474
  enabled: exports_external.boolean().default(true),
18475
- maxAcCount: exports_external.number().int().min(1).max(50).default(6),
18476
- maxDescriptionLength: exports_external.number().int().min(100).max(1e4).default(2000),
18477
- maxBulletPoints: exports_external.number().int().min(1).max(100).default(8),
18475
+ maxAcCount: exports_external.number().int().min(1).max(50).default(10),
18476
+ maxDescriptionLength: exports_external.number().int().min(100).max(1e4).default(3000),
18477
+ maxBulletPoints: exports_external.number().int().min(1).max(100).default(12),
18478
18478
  action: exports_external.enum(["block", "warn", "skip"]).default("block"),
18479
18479
  maxReplanAttempts: exports_external.number().int().min(1).default(3)
18480
18480
  });
@@ -22056,7 +22056,7 @@ var package_default;
22056
22056
  var init_package = __esm(() => {
22057
22057
  package_default = {
22058
22058
  name: "@nathapp/nax",
22059
- version: "0.56.0",
22059
+ version: "0.56.2",
22060
22060
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22061
22061
  type: "module",
22062
22062
  bin: {
@@ -22135,8 +22135,8 @@ var init_version = __esm(() => {
22135
22135
  NAX_VERSION = package_default.version;
22136
22136
  NAX_COMMIT = (() => {
22137
22137
  try {
22138
- if (/^[0-9a-f]{6,10}$/.test("fa0f6b70"))
22139
- return "fa0f6b70";
22138
+ if (/^[0-9a-f]{6,10}$/.test("b590070f"))
22139
+ return "b590070f";
22140
22140
  } catch {}
22141
22141
  try {
22142
22142
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22389,6 +22389,18 @@ var DEFAULT_FALLBACK_AGENT = "claude";
22389
22389
  var init_resolvers = () => {};
22390
22390
 
22391
22391
  // src/debate/session.ts
22392
+ function resolveDebaterModel(debater, config2) {
22393
+ const tier = debater.model ?? "fast";
22394
+ if (!config2?.models)
22395
+ return debater.model;
22396
+ try {
22397
+ const defaultAgent = config2.autoMode?.defaultAgent ?? debater.agent;
22398
+ const modelDef = resolveModelForAgent(config2.models, debater.agent, tier, defaultAgent);
22399
+ return modelDef.model;
22400
+ } catch {
22401
+ return debater.model;
22402
+ }
22403
+ }
22392
22404
  function buildFailedResult(storyId, stage, stageConfig, totalCostUsd = 0) {
22393
22405
  return {
22394
22406
  storyId,
@@ -22411,10 +22423,12 @@ class DebateSession {
22411
22423
  storyId;
22412
22424
  stage;
22413
22425
  stageConfig;
22426
+ config;
22414
22427
  constructor(opts) {
22415
22428
  this.storyId = opts.storyId;
22416
22429
  this.stage = opts.stage;
22417
22430
  this.stageConfig = opts.stageConfig;
22431
+ this.config = opts.config;
22418
22432
  }
22419
22433
  async run(prompt) {
22420
22434
  const sessionMode = this.stageConfig.sessionMode ?? "one-shot";
@@ -22437,11 +22451,17 @@ class DebateSession {
22437
22451
  }
22438
22452
  resolved.push({ debater, adapter });
22439
22453
  }
22454
+ logger?.info("debate", "debate:start", {
22455
+ storyId: this.storyId,
22456
+ stage: this.stage,
22457
+ debaters: resolved.map((r) => r.debater.agent)
22458
+ });
22440
22459
  const sessions = [];
22441
22460
  try {
22442
22461
  for (let i = 0;i < resolved.length; i++) {
22443
22462
  const { debater, adapter } = resolved[i];
22444
- const cmdStr = `acpx --model ${debater.model} ${debater.agent}`;
22463
+ const resolvedModel = resolveDebaterModel(debater, this.config);
22464
+ const cmdStr = resolvedModel ? `acpx --model ${resolvedModel} ${debater.agent}` : `acpx ${debater.agent}`;
22445
22465
  const client = _debateSessionDeps.createSpawnAcpClient(cmdStr);
22446
22466
  const sessionName = `nax-debate-${this.storyId}-${i}`;
22447
22467
  try {
@@ -22457,9 +22477,19 @@ class DebateSession {
22457
22477
  }
22458
22478
  if (sessions.length < 2) {
22459
22479
  if (sessions.length === 1) {
22480
+ logger?.warn("debate", "debate:fallback", {
22481
+ storyId: this.storyId,
22482
+ stage: this.stage,
22483
+ reason: "only 1 session created"
22484
+ });
22460
22485
  const solo = sessions[0];
22461
22486
  const response = await solo.session.prompt(prompt);
22462
22487
  const output = extractSessionOutput(response);
22488
+ logger?.info("debate", "debate:result", {
22489
+ storyId: this.storyId,
22490
+ stage: this.stage,
22491
+ outcome: "passed"
22492
+ });
22463
22493
  return {
22464
22494
  storyId: this.storyId,
22465
22495
  stage: this.stage,
@@ -22471,6 +22501,11 @@ class DebateSession {
22471
22501
  totalCostUsd
22472
22502
  };
22473
22503
  }
22504
+ logger?.warn("debate", "debate:fallback", {
22505
+ storyId: this.storyId,
22506
+ stage: this.stage,
22507
+ reason: "no sessions created"
22508
+ });
22474
22509
  return buildFailedResult(this.storyId, this.stage, config2, totalCostUsd);
22475
22510
  }
22476
22511
  const proposalSettled = await Promise.allSettled(sessions.map(({ session }) => session.prompt(prompt)));
@@ -22486,8 +22521,22 @@ class DebateSession {
22486
22521
  }
22487
22522
  }
22488
22523
  if (successfulSessions.length < 2) {
22524
+ logger?.warn("debate", "debate:fallback", {
22525
+ storyId: this.storyId,
22526
+ stage: this.stage,
22527
+ reason: "fewer than 2 proposal rounds succeeded"
22528
+ });
22489
22529
  return buildFailedResult(this.storyId, this.stage, config2, totalCostUsd);
22490
22530
  }
22531
+ for (let i = 0;i < successfulSessions.length; i++) {
22532
+ const s = successfulSessions[i];
22533
+ logger?.info("debate", "debate:proposal", {
22534
+ storyId: this.storyId,
22535
+ stage: this.stage,
22536
+ debaterIndex: i,
22537
+ agent: s.entry.debater.agent
22538
+ });
22539
+ }
22491
22540
  let critiqueOutputs = [];
22492
22541
  if (config2.rounds > 1) {
22493
22542
  const proposalOutputs2 = successfulSessions.map((s) => s.output);
@@ -22506,6 +22555,11 @@ class DebateSession {
22506
22555
  debater: s.entry.debater,
22507
22556
  output: s.output
22508
22557
  }));
22558
+ logger?.info("debate", "debate:result", {
22559
+ storyId: this.storyId,
22560
+ stage: this.stage,
22561
+ outcome
22562
+ });
22509
22563
  return {
22510
22564
  storyId: this.storyId,
22511
22565
  stage: this.stage,
@@ -22534,16 +22588,40 @@ class DebateSession {
22534
22588
  }
22535
22589
  resolved.push({ debater, adapter });
22536
22590
  }
22537
- const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, { model: debater.model }).then((output) => ({ debater, adapter, output, cost: 0 }))));
22591
+ logger?.info("debate", "debate:start", {
22592
+ storyId: this.storyId,
22593
+ stage: this.stage,
22594
+ debaters: resolved.map((r) => r.debater.agent)
22595
+ });
22596
+ const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, { model: resolveDebaterModel(debater, this.config) }).then((output) => ({ debater, adapter, output, cost: 0 }))));
22538
22597
  const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
22539
22598
  for (const r of proposalSettled) {
22540
22599
  if (r.status === "fulfilled") {
22541
22600
  totalCostUsd += r.value.cost;
22542
22601
  }
22543
22602
  }
22603
+ for (let i = 0;i < successful.length; i++) {
22604
+ logger?.info("debate", "debate:proposal", {
22605
+ storyId: this.storyId,
22606
+ stage: this.stage,
22607
+ debaterIndex: i,
22608
+ agent: successful[i].debater.agent,
22609
+ model: resolveDebaterModel(successful[i].debater, this.config)
22610
+ });
22611
+ }
22544
22612
  if (successful.length < 2) {
22545
22613
  if (successful.length === 1) {
22614
+ logger?.warn("debate", "debate:fallback", {
22615
+ storyId: this.storyId,
22616
+ stage: this.stage,
22617
+ reason: "only 1 debater succeeded"
22618
+ });
22546
22619
  const solo = successful[0];
22620
+ logger?.info("debate", "debate:result", {
22621
+ storyId: this.storyId,
22622
+ stage: this.stage,
22623
+ outcome: "passed"
22624
+ });
22547
22625
  return {
22548
22626
  storyId: this.storyId,
22549
22627
  stage: this.stage,
@@ -22557,8 +22635,20 @@ class DebateSession {
22557
22635
  }
22558
22636
  if (resolved.length > 0) {
22559
22637
  const { adapter: fallbackAdapter, debater: fallbackDebater } = resolved[0];
22638
+ logger?.warn("debate", "debate:fallback", {
22639
+ storyId: this.storyId,
22640
+ stage: this.stage,
22641
+ reason: "all debaters failed \u2014 retrying with first adapter"
22642
+ });
22560
22643
  try {
22561
- const fallbackOutput = await fallbackAdapter.complete(prompt, { model: fallbackDebater.model });
22644
+ const fallbackOutput = await fallbackAdapter.complete(prompt, {
22645
+ model: resolveDebaterModel(fallbackDebater, this.config)
22646
+ });
22647
+ logger?.info("debate", "debate:result", {
22648
+ storyId: this.storyId,
22649
+ stage: this.stage,
22650
+ outcome: "passed"
22651
+ });
22562
22652
  return {
22563
22653
  storyId: this.storyId,
22564
22654
  stage: this.stage,
@@ -22577,7 +22667,7 @@ class DebateSession {
22577
22667
  if (config2.rounds > 1) {
22578
22668
  const proposalOutputs2 = successful.map((p) => p.output);
22579
22669
  const critiqueSettled = await Promise.allSettled(successful.map(({ debater, adapter }, i) => adapter.complete(buildCritiquePrompt(prompt, proposalOutputs2, i), {
22580
- model: debater.model
22670
+ model: resolveDebaterModel(debater, this.config)
22581
22671
  })));
22582
22672
  for (const r of critiqueSettled) {
22583
22673
  if (r.status === "fulfilled") {
@@ -22593,6 +22683,11 @@ class DebateSession {
22593
22683
  debater: p.debater,
22594
22684
  output: p.output
22595
22685
  }));
22686
+ logger?.info("debate", "debate:result", {
22687
+ storyId: this.storyId,
22688
+ stage: this.stage,
22689
+ outcome
22690
+ });
22596
22691
  return {
22597
22692
  storyId: this.storyId,
22598
22693
  stage: this.stage,
@@ -22604,7 +22699,7 @@ class DebateSession {
22604
22699
  totalCostUsd
22605
22700
  };
22606
22701
  }
22607
- async resolve(proposalOutputs, critiqueOutputs, successful) {
22702
+ async resolve(proposalOutputs, critiqueOutputs, _successful) {
22608
22703
  const resolverConfig = this.stageConfig.resolver;
22609
22704
  if (resolverConfig.type === "majority-fail-closed" || resolverConfig.type === "majority-fail-open") {
22610
22705
  return majorityResolver(proposalOutputs, resolverConfig.type === "majority-fail-open");
@@ -22631,6 +22726,7 @@ var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
22631
22726
  var init_session = __esm(() => {
22632
22727
  init_spawn_client();
22633
22728
  init_registry();
22729
+ init_config();
22634
22730
  init_logger2();
22635
22731
  init_resolvers();
22636
22732
  _debateSessionDeps = {
@@ -23500,6 +23596,11 @@ class WebhookInteractionPlugin {
23500
23596
  serverStartPromise = null;
23501
23597
  pendingResponses = new Map;
23502
23598
  receiveCallbacks = new Map;
23599
+ get callbackServerPort() {
23600
+ if (!this.server)
23601
+ return null;
23602
+ return this.server.port;
23603
+ }
23503
23604
  async init(config2) {
23504
23605
  const cfg = WebhookConfigSchema.parse(config2);
23505
23606
  this.config = {
@@ -23675,7 +23776,9 @@ var init_webhook = __esm(() => {
23675
23776
  init_bun_deps();
23676
23777
  WebhookConfigSchema = exports_external.object({
23677
23778
  url: exports_external.string().url().optional(),
23678
- callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
23779
+ callbackPort: exports_external.number().int().refine((p) => p === 0 || p >= 1024 && p <= 65535, {
23780
+ message: "Port must be 0 (auto-assign) or between 1024 and 65535"
23781
+ }).optional(),
23679
23782
  secret: exports_external.string().optional(),
23680
23783
  maxPayloadBytes: exports_external.number().int().positive().optional()
23681
23784
  });
@@ -26269,7 +26372,8 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
26269
26372
  const debateSession = _semanticDeps.createDebateSession({
26270
26373
  storyId: story.id,
26271
26374
  stage: "review",
26272
- stageConfig: reviewStageConfig
26375
+ stageConfig: reviewStageConfig,
26376
+ config: naxConfig
26273
26377
  });
26274
26378
  const debateResult = await debateSession.run(prompt);
26275
26379
  let passCount = 0;
@@ -28788,11 +28892,26 @@ function shouldRetryRectification(state, config2) {
28788
28892
  return true;
28789
28893
  }
28790
28894
  function buildEscalationPreamble(attempt, config2) {
28895
+ const logger = getSafeLogger();
28791
28896
  const rethinkAt = Math.min(config2.rethinkAtAttempt ?? 2, config2.maxRetries);
28792
28897
  const urgencyAt = Math.min(config2.urgencyAtAttempt ?? 3, config2.maxRetries);
28793
28898
  if (attempt < rethinkAt)
28794
28899
  return "";
28795
28900
  const isUrgent = attempt >= urgencyAt;
28901
+ if (isUrgent) {
28902
+ logger?.info("rectification", "Progressive prompt escalation: urgency + rethink injected", {
28903
+ attempt,
28904
+ urgencyAtAttempt: urgencyAt,
28905
+ rethinkAtAttempt: rethinkAt,
28906
+ maxRetries: config2.maxRetries
28907
+ });
28908
+ } else {
28909
+ logger?.info("rectification", "Progressive prompt escalation: rethink injected", {
28910
+ attempt,
28911
+ rethinkAtAttempt: rethinkAt,
28912
+ maxRetries: config2.maxRetries
28913
+ });
28914
+ }
28796
28915
  const rethinkSection = `## \u26A0\uFE0F Previous Attempt Did Not Fix the Failures
28797
28916
 
28798
28917
  Your previous fix attempt (attempt ${attempt}) did not resolve all failures. **Step back and reconsider your approach.**
@@ -28934,7 +29053,9 @@ ${testCommands}
28934
29053
  - When running tests, run ONLY the failing test files shown above \u2014 NEVER run \`bun test\` without a file filter.
28935
29054
  `;
28936
29055
  }
28937
- var init_rectification = () => {};
29056
+ var init_rectification = __esm(() => {
29057
+ init_logger2();
29058
+ });
28938
29059
 
28939
29060
  // src/verification/index.ts
28940
29061
  var init_verification = __esm(() => {
@@ -30552,6 +30673,10 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
30552
30673
  });
30553
30674
  }
30554
30675
  }
30676
+ return {
30677
+ action: "pause",
30678
+ reason: tddResult.reviewReason || `Human review needed: ${tddResult.failureCategory ?? "unknown"}`
30679
+ };
30555
30680
  }
30556
30681
  return routeTddFailure(tddResult.failureCategory, isLiteMode, ctx, tddResult.reviewReason);
30557
30682
  }
@@ -69425,78 +69550,75 @@ async function planCommand(workdir, config2, options) {
69425
69550
  autoModel = resolveModelForAgent2(config2.models, defaultAgent, planTier, defaultAgent).model;
69426
69551
  }
69427
69552
  } catch {}
69428
- if (isAcp) {
69429
- logger?.info("plan", "Starting ACP auto planning session", {
69430
- agent: agentName,
69431
- model: autoModel ?? config2?.plan?.model ?? "balanced",
69432
- workdir,
69433
- feature: options.feature,
69434
- timeoutSeconds
69435
- });
69436
- const pidRegistry = new PidRegistry(workdir);
69437
- try {
69438
- await adapter.plan({
69439
- prompt,
69553
+ const runSingleAgentPlan = async () => {
69554
+ if (isAcp) {
69555
+ logger?.info("plan", "Starting ACP auto planning session", {
69556
+ agent: agentName,
69557
+ model: autoModel ?? config2?.plan?.model ?? "balanced",
69440
69558
  workdir,
69441
- interactive: false,
69442
- timeoutSeconds,
69443
- config: config2,
69444
- modelTier: config2?.plan?.model ?? "balanced",
69445
- dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
69446
- maxInteractionTurns: config2?.agent?.maxInteractionTurns,
69447
- featureName: options.feature,
69448
- pidRegistry,
69449
- sessionRole: "plan"
69450
- });
69451
- } finally {
69452
- await pidRegistry.killAll().catch(() => {});
69453
- }
69454
- if (!_planDeps.existsSync(outputPath)) {
69455
- throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
69456
- }
69457
- rawResponse = await _planDeps.readFile(outputPath);
69458
- } else {
69459
- const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
69460
- if (debateEnabled) {
69461
- const planStageConfig = config2.debate?.stages.plan;
69462
- const debateSession = _planDeps.createDebateSession({
69463
- storyId: options.feature,
69464
- stage: "plan",
69465
- stageConfig: planStageConfig
69559
+ feature: options.feature,
69560
+ timeoutSeconds
69466
69561
  });
69467
- const debateResult = await debateSession.run(prompt);
69468
- if (debateResult.outcome !== "failed" && debateResult.output) {
69469
- rawResponse = debateResult.output;
69470
- } else {
69471
- logger?.warn("debate", "All debaters failed \u2014 falling back to single agent", {
69472
- stage: "debate",
69473
- event: "fallback"
69474
- });
69475
- rawResponse = await adapter.complete(prompt, {
69476
- model: autoModel,
69477
- jsonMode: true,
69562
+ const pidRegistry = new PidRegistry(workdir);
69563
+ try {
69564
+ await adapter.plan({
69565
+ prompt,
69478
69566
  workdir,
69567
+ interactive: false,
69568
+ timeoutSeconds,
69479
69569
  config: config2,
69570
+ modelTier: config2?.plan?.model ?? "balanced",
69571
+ dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
69572
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
69480
69573
  featureName: options.feature,
69574
+ pidRegistry,
69481
69575
  sessionRole: "plan"
69482
69576
  });
69577
+ } finally {
69578
+ await pidRegistry.killAll().catch(() => {});
69483
69579
  }
69484
- } else {
69485
- rawResponse = await adapter.complete(prompt, {
69486
- model: autoModel,
69487
- jsonMode: true,
69488
- workdir,
69489
- config: config2,
69490
- featureName: options.feature,
69491
- sessionRole: "plan"
69492
- });
69580
+ if (!_planDeps.existsSync(outputPath)) {
69581
+ throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
69582
+ }
69583
+ return await _planDeps.readFile(outputPath);
69493
69584
  }
69585
+ let result = await adapter.complete(prompt, {
69586
+ model: autoModel,
69587
+ jsonMode: true,
69588
+ workdir,
69589
+ config: config2,
69590
+ featureName: options.feature,
69591
+ sessionRole: "plan"
69592
+ });
69494
69593
  try {
69495
- const envelope = JSON.parse(rawResponse);
69594
+ const envelope = JSON.parse(result);
69496
69595
  if (envelope?.type === "result" && typeof envelope?.result === "string") {
69497
- rawResponse = envelope.result;
69596
+ result = envelope.result;
69498
69597
  }
69499
69598
  } catch {}
69599
+ return result;
69600
+ };
69601
+ const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
69602
+ if (debateEnabled) {
69603
+ const planStageConfig = config2?.debate?.stages.plan;
69604
+ const debateSession = _planDeps.createDebateSession({
69605
+ storyId: options.feature,
69606
+ stage: "plan",
69607
+ stageConfig: planStageConfig,
69608
+ config: config2
69609
+ });
69610
+ const debateResult = await debateSession.run(prompt);
69611
+ if (debateResult.outcome !== "failed" && debateResult.output) {
69612
+ rawResponse = debateResult.output;
69613
+ } else {
69614
+ logger?.warn("debate", "All debaters failed \u2014 falling back to single agent", {
69615
+ stage: "debate",
69616
+ event: "fallback"
69617
+ });
69618
+ rawResponse = await runSingleAgentPlan();
69619
+ }
69620
+ } else {
69621
+ rawResponse = await runSingleAgentPlan();
69500
69622
  }
69501
69623
  } else {
69502
69624
  const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
@@ -69868,12 +69990,34 @@ async function planDecomposeCommand(workdir, config2, options) {
69868
69990
  if (debateResult.outcome !== "failed" && debateResult.output) {
69869
69991
  rawResponse = debateResult.output;
69870
69992
  } else {
69871
- rawResponse = await adapter.complete(prompt, { jsonMode: true });
69993
+ rawResponse = await adapter.complete(prompt, {
69994
+ jsonMode: true,
69995
+ workdir,
69996
+ sessionRole: "decompose",
69997
+ featureName: options.feature,
69998
+ storyId: options.storyId
69999
+ });
69872
70000
  }
69873
70001
  } else {
69874
- rawResponse = await adapter.complete(prompt, { jsonMode: true });
70002
+ rawResponse = await adapter.complete(prompt, {
70003
+ jsonMode: true,
70004
+ workdir,
70005
+ sessionRole: "decompose",
70006
+ featureName: options.feature,
70007
+ storyId: options.storyId
70008
+ });
70009
+ }
70010
+ const jsonMatch = rawResponse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
70011
+ const cleanedResponse = jsonMatch ? jsonMatch[1] : rawResponse;
70012
+ let parsed;
70013
+ try {
70014
+ parsed = JSON.parse(cleanedResponse.trim());
70015
+ } catch (err) {
70016
+ throw new NaxError(`Failed to parse decompose response as JSON: ${err.message}
70017
+
70018
+ Response (first 500 chars):
70019
+ ${rawResponse.slice(0, 500)}`, "DECOMPOSE_PARSE_FAILED", { stage: "decompose", storyId: options.storyId });
69875
70020
  }
69876
- const parsed = JSON.parse(rawResponse);
69877
70021
  const subStories = parsed.subStories;
69878
70022
  const maxAcCount = config2?.precheck?.storySizeGate?.maxAcCount ?? Number.POSITIVE_INFINITY;
69879
70023
  for (const sub of subStories) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.56.0",
3
+ "version": "0.56.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {