@nathapp/nax 0.74.0 → 0.75.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 +932 -609
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -16949,9 +16949,6 @@ var init_schemas_execution = __esm(() => {
16949
16949
  })
16950
16950
  });
16951
16951
  QualityConfigSchema = exports_external.object({
16952
- requireTypecheck: exports_external.boolean().default(true),
16953
- requireLint: exports_external.boolean().default(true),
16954
- requireTests: exports_external.boolean().default(true),
16955
16952
  scopeTestThreshold: exports_external.number().int().min(0).default(10),
16956
16953
  commands: exports_external.object({
16957
16954
  typecheck: exports_external.string().optional(),
@@ -17047,7 +17044,7 @@ var init_schemas_execution = __esm(() => {
17047
17044
  });
17048
17045
 
17049
17046
  // src/config/schemas-infra.ts
17050
- var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, AgentRoutingProfileSchema, AgentRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentFallbackConfigSchema, DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG, AgentIdleWatchdogConfigSchema, AgentAcpConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, CuratorThresholdsSchema, CuratorConfigSchema;
17047
+ var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, AgentRoutingProfileSchema, AgentRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentFallbackConfigSchema, DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG, AgentIdleWatchdogConfigSchema, AgentAcpConfigSchema, AgentTimeoutRetryConfigSchema, DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, CuratorThresholdsSchema, CuratorConfigSchema;
17051
17048
  var init_schemas_infra = __esm(() => {
17052
17049
  init_zod();
17053
17050
  init_schemas_model();
@@ -17225,6 +17222,14 @@ var init_schemas_infra = __esm(() => {
17225
17222
  AgentAcpConfigSchema = exports_external.object({
17226
17223
  promptRetries: exports_external.number().int().min(0).max(5).default(0)
17227
17224
  });
17225
+ AgentTimeoutRetryConfigSchema = exports_external.object({
17226
+ maxAttempts: exports_external.number().int().nonnegative().default(1),
17227
+ budgetMultiplier: exports_external.number().gt(0).max(1).default(0.5)
17228
+ });
17229
+ DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG = {
17230
+ maxAttempts: 1,
17231
+ budgetMultiplier: 0.5
17232
+ };
17228
17233
  AgentConfigSchema = exports_external.object({
17229
17234
  protocol: exports_external.literal("acp").default("acp"),
17230
17235
  default: exports_external.string().trim().min(1, "agent.default must be non-empty").default("claude"),
@@ -17238,7 +17243,8 @@ var init_schemas_infra = __esm(() => {
17238
17243
  rebuildContext: true
17239
17244
  }),
17240
17245
  acp: AgentAcpConfigSchema.default({ promptRetries: 0 }),
17241
- idleWatchdog: AgentIdleWatchdogConfigSchema.default(DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG)
17246
+ idleWatchdog: AgentIdleWatchdogConfigSchema.default(DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG),
17247
+ timeoutRetry: AgentTimeoutRetryConfigSchema.default(DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG)
17242
17248
  });
17243
17249
  PrecheckConfigSchema = exports_external.object({
17244
17250
  storySizeGate: StorySizeGateConfigSchema
@@ -17525,9 +17531,6 @@ var init_schemas3 = __esm(() => {
17525
17531
  }
17526
17532
  }),
17527
17533
  quality: QualityConfigSchema.default({
17528
- requireTypecheck: true,
17529
- requireLint: true,
17530
- requireTests: true,
17531
17534
  scopeTestThreshold: 10,
17532
17535
  commands: {},
17533
17536
  lintOutput: {
@@ -17714,7 +17717,8 @@ var init_schemas3 = __esm(() => {
17714
17717
  promptAudit: { enabled: false },
17715
17718
  fallback: { enabled: false, map: {}, maxHopsPerStory: 2, onQualityFailure: false, rebuildContext: true },
17716
17719
  acp: { promptRetries: 0 },
17717
- idleWatchdog: DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG
17720
+ idleWatchdog: DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG,
17721
+ timeoutRetry: DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG
17718
17722
  }),
17719
17723
  precheck: PrecheckConfigSchema.optional().default({
17720
17724
  storySizeGate: {
@@ -18328,15 +18332,22 @@ function redactString(value) {
18328
18332
  }
18329
18333
  return out;
18330
18334
  }
18331
- function redactSecrets(input) {
18335
+ function redactEntry(entry) {
18336
+ return {
18337
+ ...entry,
18338
+ message: redactString(entry.message),
18339
+ ...entry.data ? { data: redactValue(entry.data) } : {}
18340
+ };
18341
+ }
18342
+ function redactValue(input) {
18332
18343
  if (typeof input === "string")
18333
18344
  return redactString(input);
18334
18345
  if (Array.isArray(input))
18335
- return input.map(redactSecrets);
18346
+ return input.map(redactValue);
18336
18347
  if (input !== null && typeof input === "object") {
18337
18348
  const out = {};
18338
18349
  for (const [key, value] of Object.entries(input)) {
18339
- out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactSecrets(value);
18350
+ out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactValue(value);
18340
18351
  }
18341
18352
  return out;
18342
18353
  }
@@ -18367,6 +18378,7 @@ class Logger {
18367
18378
  formatterMode;
18368
18379
  suppressConsole;
18369
18380
  writeQueueTail = Promise.resolve();
18381
+ pendingLines = [];
18370
18382
  constructor(options) {
18371
18383
  this.level = options.level;
18372
18384
  this.filePath = options.filePath;
@@ -18401,7 +18413,7 @@ class Logger {
18401
18413
  const { sessionRole: _omit, ...rest } = data;
18402
18414
  strippedData = Object.keys(rest).length > 0 ? rest : undefined;
18403
18415
  }
18404
- const entry = {
18416
+ const rawEntry = {
18405
18417
  timestamp: new Date().toISOString(),
18406
18418
  level,
18407
18419
  stage,
@@ -18410,7 +18422,11 @@ class Logger {
18410
18422
  ...sessionRole && { sessionRole },
18411
18423
  ...strippedData && { data: strippedData }
18412
18424
  };
18413
- if (this.shouldLog(level) && !this.suppressConsole) {
18425
+ const consoleEnabled = this.shouldLog(level) && !this.suppressConsole;
18426
+ if (!consoleEnabled && !this.filePath)
18427
+ return;
18428
+ const entry = redactEntry(rawEntry);
18429
+ if (consoleEnabled) {
18414
18430
  let consoleOutput = null;
18415
18431
  if (this.formatterMode) {
18416
18432
  const formatterOptions = {
@@ -18451,14 +18467,24 @@ ${JSON.stringify(entry.data, null, 2)}`;
18451
18467
  writeToFile(entry) {
18452
18468
  if (!this.filePath)
18453
18469
  return;
18454
- const safeEntry = entry.data ? { ...entry, data: redactSecrets(entry.data) } : entry;
18455
- const line = `${formatJsonl(safeEntry)}
18456
- `;
18457
18470
  const filePath = this.filePath;
18458
- this.writeQueueTail = this.writeQueueTail.then(() => appendFile(filePath, line).catch((error48) => {
18459
- process.stderr.write(`[logger] Failed to write to log file: ${error48}
18471
+ this.pendingLines.push(`${formatJsonl(entry)}
18460
18472
  `);
18461
- }));
18473
+ this.writeQueueTail = this.writeQueueTail.then(async () => {
18474
+ while (this.pendingLines.length > 0) {
18475
+ let bytes = 0;
18476
+ let count = 0;
18477
+ while (count < this.pendingLines.length && bytes < MAX_BATCH_BYTES) {
18478
+ bytes += this.pendingLines[count].length;
18479
+ count++;
18480
+ }
18481
+ const batch = this.pendingLines.splice(0, count).join("");
18482
+ await appendFile(filePath, batch).catch((error48) => {
18483
+ process.stderr.write(`[logger] Failed to write to log file: ${error48}
18484
+ `);
18485
+ });
18486
+ }
18487
+ });
18462
18488
  }
18463
18489
  async flush() {
18464
18490
  await this.writeQueueTail;
@@ -18511,7 +18537,7 @@ function resetLogger() {
18511
18537
  }
18512
18538
  instance = null;
18513
18539
  }
18514
- var LOG_LEVEL_PRIORITY, instance = null, noopLogger;
18540
+ var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, noopLogger;
18515
18541
  var init_logger = __esm(() => {
18516
18542
  init_log_format();
18517
18543
  init_formatters();
@@ -18523,6 +18549,7 @@ var init_logger = __esm(() => {
18523
18549
  info: 2,
18524
18550
  debug: 3
18525
18551
  };
18552
+ MAX_BATCH_BYTES = 64 * 1024;
18526
18553
  noopLogger = new Logger({ level: "silent", useChalk: false, headless: false });
18527
18554
  });
18528
18555
 
@@ -18567,6 +18594,121 @@ var init_json_file = __esm(() => {
18567
18594
  init_logger2();
18568
18595
  });
18569
18596
 
18597
+ // src/config/config-guards.ts
18598
+ function rejectLegacyAgentKeys(conf) {
18599
+ const legacyKeys = [];
18600
+ const migrationHints = [];
18601
+ const autoMode = conf.autoMode;
18602
+ if (autoMode && typeof autoMode === "object") {
18603
+ if ("defaultAgent" in autoMode) {
18604
+ legacyKeys.push("autoMode.defaultAgent");
18605
+ migrationHints.push("- Move `autoMode.defaultAgent` \u2192 `agent.default`");
18606
+ }
18607
+ if ("fallbackOrder" in autoMode) {
18608
+ legacyKeys.push("autoMode.fallbackOrder");
18609
+ migrationHints.push("- Move `autoMode.fallbackOrder: [primary, ...]` \u2192 `agent.fallback.map: { <primary>: [<rest>] }` and `agent.fallback.enabled: true`");
18610
+ }
18611
+ }
18612
+ const context = conf.context;
18613
+ const contextV2 = context?.v2;
18614
+ if (contextV2 && typeof contextV2 === "object" && "fallback" in contextV2) {
18615
+ legacyKeys.push("context.v2.fallback");
18616
+ migrationHints.push("- Move `context.v2.fallback` \u2192 `agent.fallback` (see ADR-012 Phase 6)");
18617
+ }
18618
+ if (legacyKeys.length === 0)
18619
+ return;
18620
+ const message = [
18621
+ `Invalid configuration \u2014 legacy agent keys detected: ${legacyKeys.join(", ")}.`,
18622
+ "These were removed in ADR-012 Phase 6. Migrate to the canonical `agent.*` shape:",
18623
+ ...migrationHints,
18624
+ "See docs/adr/ADR-012-agent-manager-ownership.md for the full migration guide."
18625
+ ].join(`
18626
+ `);
18627
+ throw new NaxError(message, "CONFIG_LEGACY_AGENT_KEYS", { stage: "config", legacyKeys });
18628
+ }
18629
+ function rejectLegacyRectificationKeys(conf) {
18630
+ const legacyKeys = [];
18631
+ const migrationHints = [];
18632
+ const quality = conf.quality;
18633
+ const autofix = quality?.autofix;
18634
+ if (autofix && typeof autofix === "object") {
18635
+ if ("maxTotalAttempts" in autofix) {
18636
+ legacyKeys.push("quality.autofix.maxTotalAttempts");
18637
+ migrationHints.push("- Move `quality.autofix.maxTotalAttempts` \u2192 `execution.rectification.maxAttemptsTotal`");
18638
+ }
18639
+ if ("rethinkAtAttempt" in autofix) {
18640
+ legacyKeys.push("quality.autofix.rethinkAtAttempt");
18641
+ migrationHints.push("- Move `quality.autofix.rethinkAtAttempt` \u2192 `execution.rectification.rethinkAtAttempt`");
18642
+ }
18643
+ if ("urgencyAtAttempt" in autofix) {
18644
+ legacyKeys.push("quality.autofix.urgencyAtAttempt");
18645
+ migrationHints.push("- Move `quality.autofix.urgencyAtAttempt` \u2192 `execution.rectification.urgencyAtAttempt`");
18646
+ }
18647
+ }
18648
+ const execution = conf.execution;
18649
+ const rectification = execution?.rectification;
18650
+ if (rectification && typeof rectification === "object" && "maxRetries" in rectification) {
18651
+ legacyKeys.push("execution.rectification.maxRetries");
18652
+ migrationHints.push("- Rename `execution.rectification.maxRetries` \u2192 `execution.rectification.maxAttemptsTotal` (default changed from 2 to 12)");
18653
+ }
18654
+ const regressionGate = execution?.regressionGate;
18655
+ if (regressionGate && typeof regressionGate === "object" && "maxRectificationAttempts" in regressionGate) {
18656
+ legacyKeys.push("execution.regressionGate.maxRectificationAttempts");
18657
+ migrationHints.push("- Remove `execution.regressionGate.maxRectificationAttempts` \u2014 the regression cycle now shares `execution.rectification.maxAttemptsTotal`");
18658
+ }
18659
+ if (legacyKeys.length === 0)
18660
+ return;
18661
+ const message = [
18662
+ `Invalid configuration \u2014 legacy rectification-cap keys detected: ${legacyKeys.join(", ")}.`,
18663
+ "These were consolidated under `execution.rectification.*` so one config controls the unified",
18664
+ "fix cycle (semantic + adversarial + mechanical + regression). Migrate as follows:",
18665
+ ...migrationHints
18666
+ ].join(`
18667
+ `);
18668
+ throw new NaxError(message, "CONFIG_LEGACY_RECTIFICATION_KEYS", { stage: "config", legacyKeys });
18669
+ }
18670
+ function rejectDeadQualityFlags(conf) {
18671
+ const quality = conf.quality;
18672
+ if (!quality || typeof quality !== "object")
18673
+ return;
18674
+ const dead = Object.entries(DEAD_QUALITY_FLAGS).filter(([flag]) => (flag in quality));
18675
+ if (dead.length === 0)
18676
+ return;
18677
+ const deadKeys = dead.map(([flag]) => `quality.${flag}`);
18678
+ const message = [
18679
+ `Invalid configuration \u2014 removed quality flags detected: ${deadKeys.join(", ")}.`,
18680
+ "These flags were never read at any gate site: typecheck, lint, and test gates fire",
18681
+ "whenever a command resolves, so setting one to `false` never skipped its gate.",
18682
+ "",
18683
+ "To skip a gate, remove its command from `quality.commands` instead:",
18684
+ ...dead.map(([flag, command]) => `- Delete \`quality.${flag}\`; to disable that gate, unset \`quality.commands.${command}\``)
18685
+ ].join(`
18686
+ `);
18687
+ throw new NaxError(message, "CONFIG_DEAD_QUALITY_FLAGS", { stage: "config", deadKeys });
18688
+ }
18689
+ function rejectUnimplementedScopedProfile(conf) {
18690
+ const execution = conf.execution;
18691
+ if (execution?.permissionProfile !== "scoped")
18692
+ return;
18693
+ const message = [
18694
+ 'Invalid configuration \u2014 execution.permissionProfile: "scoped" is not yet implemented.',
18695
+ "The scoped (per-stage tool allowlist) profile is tracked by GitHub #374 and would",
18696
+ 'otherwise silently run as "safe", giving you weaker permissions than intended.',
18697
+ 'Use "unrestricted" or "safe" for now.'
18698
+ ].join(`
18699
+ `);
18700
+ throw new NaxError(message, "CONFIG_SCOPED_PROFILE_UNIMPLEMENTED", { stage: "config" });
18701
+ }
18702
+ var DEAD_QUALITY_FLAGS;
18703
+ var init_config_guards = __esm(() => {
18704
+ init_errors();
18705
+ DEAD_QUALITY_FLAGS = {
18706
+ requireTypecheck: "typecheck",
18707
+ requireLint: "lint",
18708
+ requireTests: "test"
18709
+ };
18710
+ });
18711
+
18570
18712
  // src/config/merge.ts
18571
18713
  function mergePackageConfig(root, packageOverride) {
18572
18714
  const hasAnyMergeableField = packageOverride.agent !== undefined || packageOverride.models !== undefined || packageOverride.routing !== undefined || packageOverride.execution !== undefined || packageOverride.review !== undefined || packageOverride.acceptance !== undefined || packageOverride.quality !== undefined || packageOverride.context !== undefined || packageOverride.project !== undefined;
@@ -18649,9 +18791,6 @@ function mergePackageConfig(root, packageOverride) {
18649
18791
  },
18650
18792
  quality: {
18651
18793
  ...root.quality,
18652
- requireTests: packageOverride.quality?.requireTests ?? root.quality.requireTests,
18653
- requireTypecheck: packageOverride.quality?.requireTypecheck ?? root.quality.requireTypecheck,
18654
- requireLint: packageOverride.quality?.requireLint ?? root.quality.requireLint,
18655
18794
  commands: {
18656
18795
  ...root.quality.commands,
18657
18796
  ...packageOverride.quality?.commands
@@ -19102,91 +19241,6 @@ function applyRemovedStrategyCompat(conf) {
19102
19241
  }
19103
19242
  return conf;
19104
19243
  }
19105
- function rejectLegacyAgentKeys(conf) {
19106
- const legacyKeys = [];
19107
- const migrationHints = [];
19108
- const autoMode = conf.autoMode;
19109
- if (autoMode && typeof autoMode === "object") {
19110
- if ("defaultAgent" in autoMode) {
19111
- legacyKeys.push("autoMode.defaultAgent");
19112
- migrationHints.push("- Move `autoMode.defaultAgent` \u2192 `agent.default`");
19113
- }
19114
- if ("fallbackOrder" in autoMode) {
19115
- legacyKeys.push("autoMode.fallbackOrder");
19116
- migrationHints.push("- Move `autoMode.fallbackOrder: [primary, ...]` \u2192 `agent.fallback.map: { <primary>: [<rest>] }` and `agent.fallback.enabled: true`");
19117
- }
19118
- }
19119
- const context = conf.context;
19120
- const contextV2 = context?.v2;
19121
- if (contextV2 && typeof contextV2 === "object" && "fallback" in contextV2) {
19122
- legacyKeys.push("context.v2.fallback");
19123
- migrationHints.push("- Move `context.v2.fallback` \u2192 `agent.fallback` (see ADR-012 Phase 6)");
19124
- }
19125
- if (legacyKeys.length === 0)
19126
- return;
19127
- const message = [
19128
- `Invalid configuration \u2014 legacy agent keys detected: ${legacyKeys.join(", ")}.`,
19129
- "These were removed in ADR-012 Phase 6. Migrate to the canonical `agent.*` shape:",
19130
- ...migrationHints,
19131
- "See docs/adr/ADR-012-agent-manager-ownership.md for the full migration guide."
19132
- ].join(`
19133
- `);
19134
- throw new NaxError(message, "CONFIG_LEGACY_AGENT_KEYS", { stage: "config", legacyKeys });
19135
- }
19136
- function rejectLegacyRectificationKeys(conf) {
19137
- const legacyKeys = [];
19138
- const migrationHints = [];
19139
- const quality = conf.quality;
19140
- const autofix = quality?.autofix;
19141
- if (autofix && typeof autofix === "object") {
19142
- if ("maxTotalAttempts" in autofix) {
19143
- legacyKeys.push("quality.autofix.maxTotalAttempts");
19144
- migrationHints.push("- Move `quality.autofix.maxTotalAttempts` \u2192 `execution.rectification.maxAttemptsTotal`");
19145
- }
19146
- if ("rethinkAtAttempt" in autofix) {
19147
- legacyKeys.push("quality.autofix.rethinkAtAttempt");
19148
- migrationHints.push("- Move `quality.autofix.rethinkAtAttempt` \u2192 `execution.rectification.rethinkAtAttempt`");
19149
- }
19150
- if ("urgencyAtAttempt" in autofix) {
19151
- legacyKeys.push("quality.autofix.urgencyAtAttempt");
19152
- migrationHints.push("- Move `quality.autofix.urgencyAtAttempt` \u2192 `execution.rectification.urgencyAtAttempt`");
19153
- }
19154
- }
19155
- const execution = conf.execution;
19156
- const rectification = execution?.rectification;
19157
- if (rectification && typeof rectification === "object" && "maxRetries" in rectification) {
19158
- legacyKeys.push("execution.rectification.maxRetries");
19159
- migrationHints.push("- Rename `execution.rectification.maxRetries` \u2192 `execution.rectification.maxAttemptsTotal` (default changed from 2 to 12)");
19160
- }
19161
- const regressionGate = execution?.regressionGate;
19162
- if (regressionGate && typeof regressionGate === "object" && "maxRectificationAttempts" in regressionGate) {
19163
- legacyKeys.push("execution.regressionGate.maxRectificationAttempts");
19164
- migrationHints.push("- Remove `execution.regressionGate.maxRectificationAttempts` \u2014 the regression cycle now shares `execution.rectification.maxAttemptsTotal`");
19165
- }
19166
- if (legacyKeys.length === 0)
19167
- return;
19168
- const message = [
19169
- `Invalid configuration \u2014 legacy rectification-cap keys detected: ${legacyKeys.join(", ")}.`,
19170
- "These were consolidated under `execution.rectification.*` so one config controls the unified",
19171
- "fix cycle (semantic + adversarial + mechanical + regression). Migrate as follows:",
19172
- ...migrationHints
19173
- ].join(`
19174
- `);
19175
- throw new NaxError(message, "CONFIG_LEGACY_RECTIFICATION_KEYS", { stage: "config", legacyKeys });
19176
- }
19177
- function rejectUnimplementedScopedProfile(conf) {
19178
- const execution = conf.execution;
19179
- if (execution?.permissionProfile !== "scoped")
19180
- return;
19181
- const message = [
19182
- 'Invalid configuration \u2014 execution.permissionProfile: "scoped" is not yet implemented.',
19183
- "The scoped (per-stage tool allowlist) profile is tracked by GitHub #374 and would",
19184
- 'otherwise silently run as "safe", giving you weaker permissions than intended.',
19185
- 'Use "unrestricted" or "safe" for now.'
19186
- ].join(`
19187
- `);
19188
- throw new NaxError(message, "CONFIG_SCOPED_PROFILE_UNIMPLEMENTED", { stage: "config" });
19189
- }
19190
19244
  function applyBatchModeCompat(conf) {
19191
19245
  const routing = conf.routing;
19192
19246
  const llm = routing?.llm;
@@ -19296,6 +19350,7 @@ async function loadConfig(startDir, cliOverrides) {
19296
19350
  }
19297
19351
  rejectLegacyAgentKeys(rawConfig);
19298
19352
  rejectLegacyRectificationKeys(rawConfig);
19353
+ rejectDeadQualityFlags(rawConfig);
19299
19354
  rejectUnimplementedScopedProfile(rawConfig);
19300
19355
  const result = NaxConfigSchema.safeParse(rawConfig);
19301
19356
  if (!result.success) {
@@ -19366,6 +19421,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19366
19421
  rawMerged.profileChain = packageChain;
19367
19422
  rejectLegacyAgentKeys(rawMerged);
19368
19423
  rejectLegacyRectificationKeys(rawMerged);
19424
+ rejectDeadQualityFlags(rawMerged);
19369
19425
  rejectUnimplementedScopedProfile(rawMerged);
19370
19426
  const result = NaxConfigSchema.safeParse(rawMerged);
19371
19427
  if (!result.success) {
@@ -19386,6 +19442,7 @@ var init_loader = __esm(() => {
19386
19442
  init_errors();
19387
19443
  init_logger2();
19388
19444
  init_json_file();
19445
+ init_config_guards();
19389
19446
  init_path_security();
19390
19447
  init_paths();
19391
19448
  init_profile();
@@ -19679,7 +19736,7 @@ GOOD (write ACs like these):
19679
19736
 
19680
19737
  When a spec is provided, these rules govern acceptance criteria generation:
19681
19738
 
19682
- 1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`. Never silently drop a spec AC. ACs the spec tags \`[verbatim]\` (typically executable greps, file-existence checks, regex/count assertions, or architectural invariants) MUST be copied **character-for-character** into an \`acceptanceCriteria\` entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly; do not paraphrase, retag, split, or move them into a description. Untagged ACs may be lightly rephrased for testability, but must retain the same assertion and concrete identifiers.
19739
+ 1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`. Never silently drop a spec AC. ACs may be lightly rephrased for testability, but must retain the same assertion and concrete identifiers. An AC carrying a deprecated \`[grep]\`/\`[file]\`/\`[verbatim]\` tag describes a file-content check rather than a runtime behaviour: rewrite it as the behaviour that check was meant to prove, and drop the tag.
19683
19740
  2. **Do not invent spec ACs.** If you identify useful behavioral edge cases or negative paths that the spec did not explicitly list, place them in \`suggestedCriteria\` (a string array on the same story object) \u2014 never in \`acceptanceCriteria\`. These go through a separate hardening pass.
19684
19741
  3. **Respect story scope.** Each story's criteria must only cover what the spec says for that story. Do not assign criteria that belong to a different story's scope (wrong feature area, wrong file, wrong dependency chain).
19685
19742
  4. **\`suggestedCriteria\` format.** Each element must be a plain behavioral assertion \u2014 an observable output, return value, state change, or error condition that a test can assert. Never include implementation details (imports, internal structure), design suggestions, or vague descriptions.
@@ -19850,6 +19907,7 @@ __export(exports_config, {
19850
19907
  DebateConfigSchema: () => DebateConfigSchema,
19851
19908
  DESCRIPTION_QUALITY_RULES: () => DESCRIPTION_QUALITY_RULES,
19852
19909
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
19910
+ DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG: () => DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG,
19853
19911
  ConfiguredModelSchema: () => ConfiguredModelSchema,
19854
19912
  COMPLEXITY_GUIDE: () => COMPLEXITY_GUIDE,
19855
19913
  AutoRouteConfigSchema: () => AutoRouteConfigSchema,
@@ -20338,6 +20396,60 @@ var init_env = __esm(() => {
20338
20396
  ];
20339
20397
  });
20340
20398
 
20399
+ // src/agents/acp/model-spec.ts
20400
+ function parseModelSpec(raw) {
20401
+ const match = EFFORT_SUFFIX.exec(raw);
20402
+ if (!match)
20403
+ return { model: raw };
20404
+ return { model: match[1], effort: match[2] };
20405
+ }
20406
+ var EFFORT_SUFFIX;
20407
+ var init_model_spec = __esm(() => {
20408
+ EFFORT_SUFFIX = /^([^[\]]+)\[([^[\]]+)\]$/;
20409
+ });
20410
+
20411
+ // src/agents/acp/reasoning-effort.ts
20412
+ async function applyReasoningEffort(params) {
20413
+ const { effort, agentName, sessionName, cwd, storyId, spawn: spawn2 } = params;
20414
+ if (!effort)
20415
+ return;
20416
+ const cmd = ["acpx", "--cwd", cwd, agentName, "set", "reasoning_effort", effort, "-s", sessionName];
20417
+ const { exitCode, stdout, stderr } = await spawn2(cmd);
20418
+ if (exitCode !== 0) {
20419
+ getSafeLogger()?.warn("acp-adapter", "Failed to set reasoning_effort; continuing at adapter default", {
20420
+ storyId,
20421
+ effort,
20422
+ session: sessionName,
20423
+ cause: stdout || stderr
20424
+ });
20425
+ }
20426
+ }
20427
+ var init_reasoning_effort = __esm(() => {
20428
+ init_logger2();
20429
+ });
20430
+
20431
+ // src/agents/acp/session-ids.ts
20432
+ function parseSessionIds(stdout) {
20433
+ for (const line of stdout.split(`
20434
+ `).reverse()) {
20435
+ const trimmed = line.trim();
20436
+ if (!trimmed.startsWith("{"))
20437
+ continue;
20438
+ try {
20439
+ const parsed = JSON.parse(trimmed);
20440
+ const sessionId = parsed.acpxSessionId;
20441
+ const recordId = parsed.acpxRecordId;
20442
+ if (typeof sessionId === "string" && sessionId.length > 0) {
20443
+ return {
20444
+ sessionId,
20445
+ recordId: typeof recordId === "string" && recordId.length > 0 ? recordId : undefined
20446
+ };
20447
+ }
20448
+ } catch {}
20449
+ }
20450
+ return { sessionId: undefined, recordId: undefined };
20451
+ }
20452
+
20341
20453
  // src/agents/acp/spawn-client.ts
20342
20454
  import { randomUUID } from "crypto";
20343
20455
  async function readAndParseLines(stream, state, onActivity) {
@@ -20380,6 +20492,7 @@ class SpawnAcpSession {
20380
20492
  sessionName;
20381
20493
  cwd;
20382
20494
  model;
20495
+ modelLabel;
20383
20496
  timeoutSeconds;
20384
20497
  promptRetries;
20385
20498
  permissionMode;
@@ -20400,6 +20513,7 @@ class SpawnAcpSession {
20400
20513
  this.sessionName = opts.sessionName;
20401
20514
  this.cwd = opts.cwd;
20402
20515
  this.model = opts.model;
20516
+ this.modelLabel = opts.modelLabel ?? opts.model;
20403
20517
  this.timeoutSeconds = opts.timeoutSeconds;
20404
20518
  this.promptRetries = opts.promptRetries;
20405
20519
  this.permissionMode = opts.permissionMode;
@@ -20472,7 +20586,7 @@ class SpawnAcpSession {
20472
20586
  emit?.({
20473
20587
  ...baseEvent,
20474
20588
  kind: "agent.call_started",
20475
- model: this.model,
20589
+ model: this.modelLabel,
20476
20590
  timeoutSeconds: this.timeoutSeconds,
20477
20591
  timestamp: now()
20478
20592
  });
@@ -20643,29 +20757,11 @@ class SpawnAcpSession {
20643
20757
  await this.trackedSpawn(cmd);
20644
20758
  }
20645
20759
  }
20646
- function parseSessionIds(stdout) {
20647
- for (const line of stdout.split(`
20648
- `).reverse()) {
20649
- const trimmed = line.trim();
20650
- if (!trimmed.startsWith("{"))
20651
- continue;
20652
- try {
20653
- const parsed = JSON.parse(trimmed);
20654
- const sessionId = parsed.acpxSessionId;
20655
- const recordId = parsed.acpxRecordId;
20656
- if (typeof sessionId === "string" && sessionId.length > 0) {
20657
- return {
20658
- sessionId,
20659
- recordId: typeof recordId === "string" && recordId.length > 0 ? recordId : undefined
20660
- };
20661
- }
20662
- } catch {}
20663
- }
20664
- return { sessionId: undefined, recordId: undefined };
20665
- }
20666
20760
 
20667
20761
  class SpawnAcpClient {
20668
20762
  model;
20763
+ rawModel;
20764
+ reasoningEffort;
20669
20765
  cwd;
20670
20766
  timeoutSeconds;
20671
20767
  promptRetries;
@@ -20680,7 +20776,11 @@ class SpawnAcpClient {
20680
20776
  constructor(cmdStr, cwd, timeoutSeconds, onPidSpawned, promptRetries, onPidExited, opts) {
20681
20777
  const parts = cmdStr.split(/\s+/);
20682
20778
  const modelIdx = parts.indexOf("--model");
20683
- this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
20779
+ const rawModel = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
20780
+ const spec = parseModelSpec(rawModel);
20781
+ this.rawModel = rawModel;
20782
+ this.model = spec.model;
20783
+ this.reasoningEffort = spec.effort;
20684
20784
  const lastToken = parts[parts.length - 1];
20685
20785
  if (!lastToken || lastToken.startsWith("-")) {
20686
20786
  throw new Error(`[acp-adapter] Could not parse agentName from cmdStr: "${cmdStr}"`);
@@ -20736,11 +20836,20 @@ class SpawnAcpClient {
20736
20836
  throw new Error(`[acp-adapter] Failed to create session: ${stdout || stderr || `exit code ${exitCode}`}`);
20737
20837
  }
20738
20838
  const { sessionId, recordId } = parseSessionIds(stdout);
20839
+ await applyReasoningEffort({
20840
+ effort: this.reasoningEffort,
20841
+ agentName: opts.agentName,
20842
+ sessionName,
20843
+ cwd: this.cwd,
20844
+ storyId: this.storyId,
20845
+ spawn: (c) => this.trackedSpawn(c)
20846
+ });
20739
20847
  return new SpawnAcpSession({
20740
20848
  agentName: opts.agentName,
20741
20849
  sessionName,
20742
20850
  cwd: this.cwd,
20743
20851
  model: this.model,
20852
+ modelLabel: this.rawModel,
20744
20853
  timeoutSeconds: this.timeoutSeconds,
20745
20854
  promptRetries: this.promptRetries,
20746
20855
  permissionMode: opts.permissionMode,
@@ -20763,11 +20872,20 @@ class SpawnAcpClient {
20763
20872
  return null;
20764
20873
  }
20765
20874
  const { sessionId, recordId } = parseSessionIds(stdout);
20875
+ await applyReasoningEffort({
20876
+ effort: this.reasoningEffort,
20877
+ agentName,
20878
+ sessionName,
20879
+ cwd: this.cwd,
20880
+ storyId: this.storyId,
20881
+ spawn: (c) => this.trackedSpawn(c)
20882
+ });
20766
20883
  return new SpawnAcpSession({
20767
20884
  agentName,
20768
20885
  sessionName,
20769
20886
  cwd: this.cwd,
20770
20887
  model: this.model,
20888
+ modelLabel: this.rawModel,
20771
20889
  timeoutSeconds: this.timeoutSeconds,
20772
20890
  promptRetries: this.promptRetries,
20773
20891
  permissionMode,
@@ -20806,6 +20924,8 @@ var init_spawn_client = __esm(() => {
20806
20924
  init_logger2();
20807
20925
  init_bun_deps();
20808
20926
  init_env();
20927
+ init_model_spec();
20928
+ init_reasoning_effort();
20809
20929
  _spawnClientDeps = {
20810
20930
  spawn: typedSpawn,
20811
20931
  streamDrainTimeoutMs: ACPX_STREAM_DRAIN_TIMEOUT_MS
@@ -21075,8 +21195,23 @@ function buildRunInteractionHandler(options) {
21075
21195
  }
21076
21196
  };
21077
21197
  }
21198
+ function buildTurnResult(input) {
21199
+ const { lastResponse, totalTokenUsage, totalExactCostUsd, turnCount, interactions, timedOut, modelDef } = input;
21200
+ const output = timedOut ? "" : extractOutput(lastResponse);
21201
+ const estimatedCostUsd = totalTokenUsage.inputTokens > 0 || totalTokenUsage.outputTokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, modelDef.model) : 0;
21202
+ return {
21203
+ output,
21204
+ tokenUsage: totalTokenUsage,
21205
+ estimatedCostUsd,
21206
+ exactCostUsd: totalExactCostUsd,
21207
+ internalRoundTrips: turnCount,
21208
+ ...interactions.length > 0 ? { interactions } : {},
21209
+ timedOut
21210
+ };
21211
+ }
21078
21212
  var CONTEXT_TOOL_CALL_PATTERN;
21079
21213
  var init_adapter_output = __esm(() => {
21214
+ init_cost();
21080
21215
  CONTEXT_TOOL_CALL_PATTERN = /<nax_tool_call\s+name="([^"]+)">\s*([\s\S]*?)\s*<\/nax_tool_call>/i;
21081
21216
  });
21082
21217
 
@@ -21435,18 +21570,15 @@ class AcpAgentAdapter {
21435
21570
  if (lastResponse?.stopReason === "error") {
21436
21571
  throw new SessionTurnError(lastResponse.cancelled ? "Agent session ended with stop reason: error (externally cancelled)" : "Agent session ended with stop reason: error", lastResponse.cancelled === true, lastResponse.retryable === true);
21437
21572
  }
21438
- const output = extractOutput(lastResponse);
21439
- const tokenUsage = totalTokenUsage;
21440
- const estimatedCostUsd = totalTokenUsage.inputTokens > 0 || totalTokenUsage.outputTokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, modelDef.model) : 0;
21441
- const exactCostUsd = totalExactCostUsd;
21442
- return {
21443
- output,
21444
- tokenUsage,
21445
- estimatedCostUsd,
21446
- exactCostUsd,
21447
- internalRoundTrips: turnCount,
21448
- ...interactions.length > 0 ? { interactions } : {}
21449
- };
21573
+ return buildTurnResult({
21574
+ lastResponse,
21575
+ totalTokenUsage,
21576
+ totalExactCostUsd,
21577
+ turnCount,
21578
+ interactions,
21579
+ timedOut,
21580
+ modelDef
21581
+ });
21450
21582
  }
21451
21583
  async closeSession(handle) {
21452
21584
  const impl = handle;
@@ -21651,6 +21783,7 @@ var init_acp = __esm(() => {
21651
21783
  init_spawn_client();
21652
21784
  init_adapter_lifecycle();
21653
21785
  init_token_mapper();
21786
+ init_model_spec();
21654
21787
  });
21655
21788
 
21656
21789
  // src/agents/registry.ts
@@ -21927,6 +22060,108 @@ var init_default_strategy = __esm(() => {
21927
22060
  };
21928
22061
  });
21929
22062
 
22063
+ // src/agents/retry/hop-retry-policy.ts
22064
+ function trySameAgentRetry(result, state, deps) {
22065
+ const { staleRetryAttempts, timeoutRetryAttempts, adapterErrorRetries, currentRunOptions } = state;
22066
+ const { config: config2, requestRunOptions, signal } = deps;
22067
+ const isFailStale = result.adapterFailure?.outcome === "fail-stale";
22068
+ const maxStaleRetries = config2.agent?.idleWatchdog?.maxRetryAttempts ?? 3;
22069
+ if (isFailStale && result.adapterFailure?.retriable && staleRetryAttempts < maxStaleRetries) {
22070
+ const newAttempts = staleRetryAttempts + 1;
22071
+ return {
22072
+ outcome: "stale-retry",
22073
+ staleRetryAttempts: newAttempts,
22074
+ kind: { kind: "stale-retry", attempt: newAttempts },
22075
+ fallbackRecord: {
22076
+ outcome: result.adapterFailure?.outcome ?? "fail-stale",
22077
+ category: result.adapterFailure?.category ?? "availability",
22078
+ costUsd: result.estimatedCostUsd ?? 0,
22079
+ reason: result.adapterFailure?.reason
22080
+ }
22081
+ };
22082
+ }
22083
+ const isFailTimeout = result.adapterFailure?.outcome === "fail-timeout";
22084
+ if (isFailTimeout && result.adapterFailure?.retriable) {
22085
+ const timeoutConfig = extractTimeoutRetryConfig(config2);
22086
+ if (timeoutRetryShouldRetry(timeoutRetryAttempts, timeoutConfig)) {
22087
+ const newAttempts = timeoutRetryAttempts + 1;
22088
+ return {
22089
+ outcome: "timeout-retry",
22090
+ timeoutRetryAttempts: newAttempts,
22091
+ kind: { kind: "timeout-retry", attempt: newAttempts },
22092
+ currentRunOptions: resolveTimeoutRetryOptions(currentRunOptions, timeoutConfig, config2.execution),
22093
+ fallbackRecord: {
22094
+ outcome: result.adapterFailure?.outcome ?? "fail-timeout",
22095
+ category: result.adapterFailure?.category ?? "quality",
22096
+ costUsd: result.estimatedCostUsd ?? 0,
22097
+ reason: result.adapterFailure?.reason
22098
+ }
22099
+ };
22100
+ }
22101
+ }
22102
+ const isFailAdapterError = result.adapterFailure?.outcome === "fail-adapter-error";
22103
+ if (isFailAdapterError && !signal?.aborted) {
22104
+ const runConfig = requestRunOptions.config ?? config2;
22105
+ const maxAdapterRetries = result.adapterFailure?.retriable ? runConfig.execution?.sessionErrorRetryableMaxRetries ?? 3 : runConfig.execution?.sessionErrorMaxRetries ?? 1;
22106
+ if (adapterErrorRetries < maxAdapterRetries) {
22107
+ const newAttempts = adapterErrorRetries + 1;
22108
+ return {
22109
+ outcome: "adapter-error",
22110
+ adapterErrorRetries: newAttempts,
22111
+ kind: { kind: "stale-retry", attempt: newAttempts },
22112
+ fallbackRecord: {
22113
+ outcome: result.adapterFailure?.outcome ?? "fail-adapter-error",
22114
+ category: result.adapterFailure?.category ?? "availability",
22115
+ costUsd: result.estimatedCostUsd ?? 0,
22116
+ retriable: result.adapterFailure?.retriable ?? false,
22117
+ maxAttempts: maxAdapterRetries
22118
+ }
22119
+ };
22120
+ }
22121
+ }
22122
+ return null;
22123
+ }
22124
+ function extractTimeoutRetryConfig(config2) {
22125
+ const fromConfig = config2.agent?.timeoutRetry;
22126
+ return {
22127
+ maxAttempts: fromConfig?.maxAttempts ?? DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG.maxAttempts,
22128
+ budgetMultiplier: fromConfig?.budgetMultiplier ?? DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG.budgetMultiplier
22129
+ };
22130
+ }
22131
+ function resolveTimeoutRetryOptions(prev, timeoutConfig, executionConfig) {
22132
+ const budget = prev.timeoutSeconds ?? executionConfig?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds;
22133
+ return { ...prev, timeoutSeconds: budget * timeoutConfig.budgetMultiplier };
22134
+ }
22135
+ function timeoutRetryShouldRetry(attempts, config2) {
22136
+ return attempts < config2.maxAttempts;
22137
+ }
22138
+ function describeRetryLogEvent(retryDecision, storyId, agent) {
22139
+ const attempt = retryDecision.kind.attempt;
22140
+ if (retryDecision.outcome === "adapter-error") {
22141
+ return {
22142
+ recordFallback: false,
22143
+ level: "warn",
22144
+ message: "fail-adapter-error: same-agent retry with fresh session",
22145
+ fields: {
22146
+ storyId,
22147
+ attempt,
22148
+ maxAttempts: retryDecision.fallbackRecord.maxAttempts,
22149
+ retriable: retryDecision.fallbackRecord.retriable,
22150
+ agent
22151
+ }
22152
+ };
22153
+ }
22154
+ return {
22155
+ recordFallback: true,
22156
+ level: "info",
22157
+ message: retryDecision.outcome === "stale-retry" ? "fail-stale: immediate same-agent retry" : "fail-timeout: same-agent retry with reduced budget",
22158
+ fields: { storyId, attempt, agent, reason: retryDecision.fallbackRecord.reason }
22159
+ };
22160
+ }
22161
+ var init_hop_retry_policy = __esm(() => {
22162
+ init_config();
22163
+ });
22164
+
21930
22165
  // src/agents/manager.ts
21931
22166
  import { EventEmitter } from "events";
21932
22167
 
@@ -22033,7 +22268,7 @@ class AgentManager {
22033
22268
  shouldSwap(failure, hopsSoFar, hasBundle) {
22034
22269
  if (!failure)
22035
22270
  return false;
22036
- if (failure.outcome === "fail-aborted")
22271
+ if (failure.outcome === "fail-aborted" || failure.outcome === "fail-timeout")
22037
22272
  return false;
22038
22273
  const fallback = this._config.agent?.fallback;
22039
22274
  if (!fallback?.enabled)
@@ -22059,8 +22294,10 @@ class AgentManager {
22059
22294
  let hopsSoFar = 0;
22060
22295
  let rateLimitRetry = 0;
22061
22296
  let staleRetryAttempts = 0;
22297
+ let timeoutRetryAttempts = 0;
22062
22298
  let adapterErrorRetries = 0;
22063
22299
  let currentBundle = request.bundle;
22300
+ let currentRunOptions = request.runOptions;
22064
22301
  let currentHopKind = { kind: "primary" };
22065
22302
  let finalPrompt;
22066
22303
  const _opStartMs = Date.now();
@@ -22072,7 +22309,7 @@ class AgentManager {
22072
22309
  let result;
22073
22310
  let updatedBundle = currentBundle;
22074
22311
  if (request.executeHop) {
22075
- const hopOut = await request.executeHop(currentAgent, currentBundle, currentHopKind, request.runOptions);
22312
+ const hopOut = await request.executeHop(currentAgent, currentBundle, currentHopKind, currentRunOptions);
22076
22313
  result = hopOut.result;
22077
22314
  updatedBundle = hopOut.bundle ?? currentBundle;
22078
22315
  finalPrompt = hopOut.prompt ?? finalPrompt;
@@ -22089,7 +22326,7 @@ class AgentManager {
22089
22326
  _finalStatus = "error";
22090
22327
  return { result: unboundResult, fallbacks, finalBundle: currentBundle, finalPrompt };
22091
22328
  }
22092
- const rawHopOut = await this._runHop(currentAgent, request.runOptions);
22329
+ const rawHopOut = await this._runHop(currentAgent, currentRunOptions);
22093
22330
  const hopOut = "result" in rawHopOut && rawHopOut.result != null ? rawHopOut : { result: rawHopOut, prompt: undefined };
22094
22331
  result = hopOut.result;
22095
22332
  finalPrompt = hopOut.prompt ?? finalPrompt;
@@ -22100,51 +22337,49 @@ class AgentManager {
22100
22337
  return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22101
22338
  }
22102
22339
  const bundleForSwapCheck = updatedBundle ?? request.bundle;
22103
- if (request.noFallback) {
22104
- _finalStatus = "error";
22105
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22106
- }
22107
22340
  const isFailStale = result.adapterFailure?.outcome === "fail-stale";
22108
- const maxStaleRetries = this._config.agent?.idleWatchdog?.maxRetryAttempts ?? 3;
22109
- if (isFailStale && result.adapterFailure?.retriable && staleRetryAttempts < maxStaleRetries) {
22110
- staleRetryAttempts++;
22341
+ const retryState = {
22342
+ staleRetryAttempts,
22343
+ timeoutRetryAttempts,
22344
+ adapterErrorRetries,
22345
+ currentRunOptions
22346
+ };
22347
+ const retryDecision = trySameAgentRetry(result, retryState, {
22348
+ config: this._config,
22349
+ requestRunOptions: request.runOptions,
22350
+ signal: request.signal
22351
+ });
22352
+ if (retryDecision) {
22353
+ staleRetryAttempts = retryDecision.outcome === "stale-retry" ? retryDecision.staleRetryAttempts : staleRetryAttempts;
22354
+ timeoutRetryAttempts = retryDecision.outcome === "timeout-retry" ? retryDecision.timeoutRetryAttempts : timeoutRetryAttempts;
22355
+ adapterErrorRetries = retryDecision.outcome === "adapter-error" ? retryDecision.adapterErrorRetries : adapterErrorRetries;
22356
+ currentRunOptions = retryDecision.outcome === "timeout-retry" ? retryDecision.currentRunOptions : currentRunOptions;
22111
22357
  const retryHop = {
22112
22358
  storyId: request.runOptions.storyId,
22113
22359
  priorAgent: currentAgent,
22114
22360
  newAgent: currentAgent,
22115
- hop: staleRetryAttempts,
22116
- outcome: result.adapterFailure?.outcome ?? "fail-stale",
22117
- category: result.adapterFailure?.category ?? "availability",
22361
+ hop: retryDecision.kind.attempt,
22362
+ outcome: retryDecision.fallbackRecord.outcome,
22363
+ category: retryDecision.fallbackRecord.category,
22118
22364
  timestamp: new Date().toISOString(),
22119
- costUsd: result.estimatedCostUsd ?? 0
22365
+ costUsd: retryDecision.fallbackRecord.costUsd
22120
22366
  };
22121
- fallbacks.push(retryHop);
22122
- this._emitter.emit("onSwapAttempt", retryHop);
22123
- logger?.info("agent-manager", "fail-stale: immediate same-agent retry", {
22124
- storyId: request.runOptions.storyId,
22125
- attempt: staleRetryAttempts,
22126
- agent: currentAgent,
22127
- reason: result.adapterFailure?.reason
22128
- });
22129
- currentHopKind = { kind: "stale-retry", attempt: staleRetryAttempts };
22367
+ const logEvent = describeRetryLogEvent(retryDecision, request.runOptions.storyId, currentAgent);
22368
+ if (logEvent.recordFallback) {
22369
+ fallbacks.push(retryHop);
22370
+ this._emitter.emit("onSwapAttempt", retryHop);
22371
+ }
22372
+ if (logEvent.level === "warn") {
22373
+ logger?.warn("agent-manager", logEvent.message, logEvent.fields);
22374
+ } else {
22375
+ logger?.info("agent-manager", logEvent.message, logEvent.fields);
22376
+ }
22377
+ currentHopKind = retryDecision.kind;
22130
22378
  continue;
22131
22379
  }
22132
- const isFailAdapterError = result.adapterFailure?.outcome === "fail-adapter-error";
22133
- if (isFailAdapterError && !request.signal?.aborted) {
22134
- const runConfig = request.runOptions.config ?? this._config;
22135
- const maxAdapterRetries = result.adapterFailure?.retriable ? runConfig.execution?.sessionErrorRetryableMaxRetries ?? 3 : runConfig.execution?.sessionErrorMaxRetries ?? 1;
22136
- if (adapterErrorRetries < maxAdapterRetries) {
22137
- adapterErrorRetries++;
22138
- logger?.warn("agent-manager", "fail-adapter-error: same-agent retry with fresh session", {
22139
- storyId: request.runOptions.storyId,
22140
- attempt: adapterErrorRetries,
22141
- maxAttempts: maxAdapterRetries,
22142
- retriable: result.adapterFailure?.retriable ?? false,
22143
- agent: currentAgent
22144
- });
22145
- currentHopKind = { kind: "stale-retry", attempt: adapterErrorRetries };
22146
- continue;
22147
- }
22380
+ if (request.noFallback) {
22381
+ _finalStatus = "error";
22382
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22148
22383
  }
22149
22384
  const hasBundleForSwap = !!bundleForSwapCheck || isFailStale;
22150
22385
  if (!this.shouldSwap(result.adapterFailure, hopsSoFar, hasBundleForSwap)) {
@@ -22558,6 +22793,7 @@ var init_manager = __esm(() => {
22558
22793
  init_bun_deps();
22559
22794
  init_registry();
22560
22795
  init_default_strategy();
22796
+ init_hop_retry_policy();
22561
22797
  _agentManagerDeps = {
22562
22798
  sleep: (ms, signal) => cancellableDelay(ms, signal)
22563
22799
  };
@@ -22835,6 +23071,7 @@ var init_retry = __esm(() => {
22835
23071
  init_compose();
22836
23072
  init_parse_retry();
22837
23073
  init_tiered_parse_retry();
23074
+ init_hop_retry_policy();
22838
23075
  });
22839
23076
 
22840
23077
  // src/agents/index.ts
@@ -25288,7 +25525,7 @@ async function getGitRoot(workdir) {
25288
25525
  return null;
25289
25526
  }
25290
25527
  }
25291
- async function gitWithTimeout(args, workdir) {
25528
+ async function gitWithTimeout(args, workdir, timeoutMs = GIT_TIMEOUT_MS) {
25292
25529
  const proc = _gitDeps.spawn(["git", ...args], {
25293
25530
  cwd: workdir,
25294
25531
  stdout: "pipe",
@@ -25300,7 +25537,7 @@ async function gitWithTimeout(args, workdir) {
25300
25537
  try {
25301
25538
  proc.kill("SIGKILL");
25302
25539
  } catch {}
25303
- }, GIT_TIMEOUT_MS);
25540
+ }, timeoutMs);
25304
25541
  const exitCode = await proc.exited;
25305
25542
  clearTimeout(timerId);
25306
25543
  if (timedOut) {
@@ -25432,6 +25669,38 @@ async function captureOutputFiles(workdir, baseRef, scopePrefix) {
25432
25669
  return [];
25433
25670
  }
25434
25671
  }
25672
+ async function captureWorkingTreeChanges(workdir, baseRef, scopePrefix) {
25673
+ if (!baseRef)
25674
+ return [];
25675
+ const runDiff = async (args) => {
25676
+ const fullArgs = scopePrefix ? [...args, "--", `${scopePrefix}/`] : args;
25677
+ const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, TIMEOUT_RETRY_GIT_TIMEOUT_MS);
25678
+ if (exitCode !== 0)
25679
+ return [];
25680
+ return stdout.trim().split(`
25681
+ `).filter(Boolean);
25682
+ };
25683
+ try {
25684
+ const [committed, uncommitted, untracked] = await Promise.all([
25685
+ runDiff(["diff", "--name-only", `${baseRef}..HEAD`]),
25686
+ runDiff(["diff", "--name-only", "HEAD"]),
25687
+ runDiff(["ls-files", "--others", "--exclude-standard"])
25688
+ ]);
25689
+ const seen = new Set;
25690
+ const merged = [];
25691
+ for (const list of [committed, uncommitted, untracked]) {
25692
+ for (const file3 of list) {
25693
+ if (!seen.has(file3)) {
25694
+ seen.add(file3);
25695
+ merged.push(file3);
25696
+ }
25697
+ }
25698
+ }
25699
+ return merged;
25700
+ } catch {
25701
+ return [];
25702
+ }
25703
+ }
25435
25704
  async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25436
25705
  if (!baseRef)
25437
25706
  return "";
@@ -25454,7 +25723,7 @@ async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25454
25723
  return "";
25455
25724
  }
25456
25725
  }
25457
- var _gitDeps, GIT_TIMEOUT_MS = 1e4;
25726
+ var _gitDeps, GIT_TIMEOUT_MS = 1e4, TIMEOUT_RETRY_GIT_TIMEOUT_MS = 3000;
25458
25727
  var init_git = __esm(() => {
25459
25728
  init_logger2();
25460
25729
  init_bun_deps();
@@ -28697,81 +28966,12 @@ function generateHumanHaltSummary(prd) {
28697
28966
  `);
28698
28967
  }
28699
28968
 
28700
- // src/prd/verbatim-fidelity.ts
28701
- function normalizeWs(text) {
28702
- return text.replace(/\s+/g, " ").trim();
28703
- }
28704
- function stripBackticks(text) {
28705
- return text.replace(/`/g, "");
28706
- }
28707
- function canonical2(text) {
28708
- return normalizeWs(stripBackticks(text));
28709
- }
28710
- function leadingTagGroup(line) {
28711
- return line.match(LEADING_TAG_GROUP)?.[1] ?? null;
28712
- }
28713
- function isVerbatimBullet(line) {
28714
- const tags = leadingTagGroup(line);
28715
- return tags !== null && /\[verbatim\]/i.test(tags);
28716
- }
28717
- function isContinuation(line) {
28718
- if (line.trim().length === 0)
28719
- return false;
28720
- if (HEADING.test(line))
28721
- return false;
28722
- if (LIST_ITEM_START2.test(line))
28723
- return false;
28724
- return true;
28725
- }
28726
- function stripTagPrefix(block) {
28727
- return block.replace(LEADING_TAG_GROUP, "");
28728
- }
28729
- function extractVerbatimAcs(specContent) {
28730
- const lines = specContent.split(`
28731
- `);
28732
- const blocks = [];
28733
- for (let i = 0;i < lines.length; i++) {
28734
- if (!isVerbatimBullet(lines[i]))
28735
- continue;
28736
- const parts = [lines[i].trim()];
28737
- let j = i + 1;
28738
- while (j < lines.length && isContinuation(lines[j])) {
28739
- parts.push(lines[j].trim());
28740
- j += 1;
28741
- }
28742
- blocks.push(parts.join(" "));
28743
- i = j - 1;
28744
- }
28745
- return blocks;
28746
- }
28747
- function prdAcPayloads(prd) {
28748
- return (prd.userStories ?? []).flatMap((story) => (story.acceptanceCriteria ?? []).map(canonical2));
28749
- }
28750
- function findMissingVerbatimAcs(specContent, prd) {
28751
- const prdAcs = prdAcPayloads(prd);
28752
- const missing = [];
28753
- for (const block of extractVerbatimAcs(specContent)) {
28754
- const payload = canonical2(stripTagPrefix(block));
28755
- if (payload.length === 0)
28756
- continue;
28757
- if (!prdAcs.some((ac) => ac.includes(payload)))
28758
- missing.push(block);
28759
- }
28760
- return missing;
28761
- }
28762
- var LEADING_TAG_GROUP, LIST_ITEM_START2, HEADING;
28763
- var init_verbatim_fidelity = __esm(() => {
28764
- LEADING_TAG_GROUP = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
28765
- LIST_ITEM_START2 = /^\s*(?:[-*]|\d+\.)\s/;
28766
- HEADING = /^\s*#/;
28767
- });
28768
-
28769
28969
  // src/prd/spec-drift.ts
28770
- function leadingTagGroup2(ac) {
28771
- return ac.match(LEADING_TAG_GROUP2)?.[1] ?? null;
28970
+ function leadingTagGroup(ac) {
28971
+ return ac.match(LEADING_TAG_GROUP)?.[1] ?? null;
28772
28972
  }
28773
28973
  function hasDeprecatedTag(ac) {
28774
- const tags = leadingTagGroup2(ac);
28974
+ const tags = leadingTagGroup(ac);
28775
28975
  return tags !== null && DEPRECATED_TAG.test(tags);
28776
28976
  }
28777
28977
  function hasShellPattern(ac) {
@@ -28791,9 +28991,9 @@ function findSpecDriftViolations(prd) {
28791
28991
  }
28792
28992
  return violations;
28793
28993
  }
28794
- var LEADING_TAG_GROUP2, DEPRECATED_TAG, SHELL_PIPE, SHELL_WC, SHELL_GREP_FLAG;
28994
+ var LEADING_TAG_GROUP, DEPRECATED_TAG, SHELL_PIPE, SHELL_WC, SHELL_GREP_FLAG;
28795
28995
  var init_spec_drift = __esm(() => {
28796
- LEADING_TAG_GROUP2 = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
28996
+ LEADING_TAG_GROUP = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
28797
28997
  DEPRECATED_TAG = /\[(grep|file|verbatim)\]/i;
28798
28998
  SHELL_PIPE = /`[^`]*\b(grep|find|wc|awk|sed|sort|head|tail|xargs|cut|uniq)\b[^`]*\|[^`]*`/i;
28799
28999
  SHELL_WC = /`[^`]*\bwc\b[^`]*`/;
@@ -29221,9 +29421,7 @@ __export(exports_prd, {
29221
29421
  getContextFiles: () => getContextFiles,
29222
29422
  generateHumanHaltSummary: () => generateHumanHaltSummary,
29223
29423
  findSpecDriftViolations: () => findSpecDriftViolations,
29224
- findMissingVerbatimAcs: () => findMissingVerbatimAcs,
29225
29424
  findMissingOutOfScope: () => findMissingOutOfScope,
29226
- extractVerbatimAcs: () => extractVerbatimAcs,
29227
29425
  extractSpecOutOfScope: () => extractSpecOutOfScope,
29228
29426
  deriveNextStoryId: () => deriveNextStoryId,
29229
29427
  countStories: () => countStories,
@@ -29421,7 +29619,6 @@ var init_prd = __esm(() => {
29421
29619
  init_errors();
29422
29620
  init_json_file();
29423
29621
  init_out_of_scope();
29424
- init_verbatim_fidelity();
29425
29622
  init_spec_drift();
29426
29623
  init_out_of_scope();
29427
29624
  init_inject();
@@ -29878,6 +30075,13 @@ var init_plugin_loader = __esm(() => {
29878
30075
  });
29879
30076
 
29880
30077
  // src/context/engine/providers/plugin-cache.ts
30078
+ function withDisposeDeadline(p, deadlineMs) {
30079
+ let handle;
30080
+ const deadline = new Promise((resolve8) => {
30081
+ handle = setTimeout(resolve8, deadlineMs);
30082
+ });
30083
+ return Promise.race([p, deadline]).finally(() => clearTimeout(handle));
30084
+ }
29881
30085
  function stableCacheKey(configs, workdir) {
29882
30086
  const sorted = [...configs].sort((a, b) => a.module.localeCompare(b.module));
29883
30087
  return `${workdir}:${JSON.stringify(sorted)}`;
@@ -29908,31 +30112,32 @@ class PluginProviderCache {
29908
30112
  return;
29909
30113
  this.disposed = true;
29910
30114
  const logger = getLogger();
30115
+ const disposals = [];
29911
30116
  for (const providers of this.cache.values()) {
29912
30117
  for (const provider of providers) {
29913
30118
  const initialisable = provider;
29914
30119
  if (typeof initialisable.dispose !== "function")
29915
30120
  continue;
29916
- try {
29917
- await Promise.race([initialisable.dispose(), Bun.sleep(DISPOSE_TIMEOUT_MS)]);
29918
- } catch (err) {
30121
+ disposals.push(withDisposeDeadline(Promise.resolve().then(() => initialisable.dispose?.()).then(() => {}), _pluginCacheDeps.disposeTimeoutMs).catch((err) => {
29919
30122
  logger.warn("context-engine", "Plugin provider dispose() threw \u2014 continuing teardown", {
29920
30123
  providerId: provider.id,
29921
30124
  error: err instanceof Error ? err.message : String(err)
29922
30125
  });
29923
- }
30126
+ }));
29924
30127
  }
29925
30128
  }
30129
+ await Promise.all(disposals);
29926
30130
  this.cache.clear();
29927
30131
  }
29928
30132
  }
29929
- var _pluginCacheDeps, DISPOSE_TIMEOUT_MS = 5000;
30133
+ var DISPOSE_TIMEOUT_MS = 5000, _pluginCacheDeps;
29930
30134
  var init_plugin_cache = __esm(() => {
29931
30135
  init_errors();
29932
30136
  init_logger2();
29933
30137
  init_plugin_loader();
29934
30138
  _pluginCacheDeps = {
29935
- loadProviders: loadPluginProviders
30139
+ loadProviders: loadPluginProviders,
30140
+ disposeTimeoutMs: DISPOSE_TIMEOUT_MS
29936
30141
  };
29937
30142
  });
29938
30143
 
@@ -33450,7 +33655,7 @@ var init_acceptance_builder = __esm(() => {
33450
33655
  });
33451
33656
 
33452
33657
  // src/review/ac-quote-validator.ts
33453
- function normalizeWs2(s) {
33658
+ function normalizeWs(s) {
33454
33659
  return s.replace(/\s+/g, " ").trim();
33455
33660
  }
33456
33661
  function stripMarkdownInline(s) {
@@ -33485,8 +33690,8 @@ function validateAcQuote(finding, acceptanceCriteria) {
33485
33690
  if (typeof acIndex !== "number" || acIndex < 1 || acIndex > acceptanceCriteria.length) {
33486
33691
  return { valid: false, code: "ac_index_out_of_range" };
33487
33692
  }
33488
- const acText = normalizeWs2(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
33489
- const normalizedQuote = normalizeWs2(stripMarkdownInline(acQuote));
33693
+ const acText = normalizeWs(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
33694
+ const normalizedQuote = normalizeWs(stripMarkdownInline(acQuote));
33490
33695
  if (!acText.toLowerCase().includes(normalizedQuote.toLowerCase())) {
33491
33696
  return { valid: false, code: "ac_quote_not_substring" };
33492
33697
  }
@@ -33560,8 +33765,8 @@ function validateScopeQuote(finding, outOfScope) {
33560
33765
  if (typeof scopeIndex !== "number" || scopeIndex < 1 || scopeIndex > outOfScope.length) {
33561
33766
  return { valid: false, code: "scope_index_out_of_range" };
33562
33767
  }
33563
- const entry = normalizeWs2(stripMarkdownInline(outOfScope[scopeIndex - 1]));
33564
- const quote = normalizeWs2(stripMarkdownInline(scopeQuote));
33768
+ const entry = normalizeWs(stripMarkdownInline(outOfScope[scopeIndex - 1]));
33769
+ const quote = normalizeWs(stripMarkdownInline(scopeQuote));
33565
33770
  if (!entry.toLowerCase().includes(quote.toLowerCase())) {
33566
33771
  return { valid: false, code: "scope_quote_not_substring" };
33567
33772
  }
@@ -33780,13 +33985,31 @@ var init_adapters = __esm(() => {
33780
33985
  init_typecheck();
33781
33986
  });
33782
33987
 
33783
- // src/operations/verbatim-warn.ts
33784
- function warnOnDroppedVerbatimAcs(prd, specContent, featureName) {
33785
- const missing = findMissingVerbatimAcs(specContent, prd);
33786
- if (missing.length > 0) {
33787
- getSafeLogger()?.warn("plan", "[verbatim] spec acceptance criteria dropped from PRD \u2014 run spec-review --prd before executing", { featureName, missingCount: missing.length, missing });
33988
+ // src/operations/turn-failure-classification.ts
33989
+ function classifyEmptyOutputFailure(turn) {
33990
+ if (turn.adapterFailure)
33991
+ return turn.adapterFailure;
33992
+ if (turn.output && turn.output.trim().length > 0)
33993
+ return null;
33994
+ if (turn.timedOut) {
33995
+ return {
33996
+ category: "quality",
33997
+ outcome: "fail-timeout",
33998
+ retriable: true,
33999
+ message: "[callOp] agent timed out before producing output",
34000
+ reason: "wall-clock-timeout"
34001
+ };
33788
34002
  }
34003
+ return {
34004
+ category: "availability",
34005
+ outcome: "fail-stale",
34006
+ retriable: true,
34007
+ message: "[callOp] agent returned no output",
34008
+ reason: "empty-output"
34009
+ };
33789
34010
  }
34011
+
34012
+ // src/operations/plan-fidelity.ts
33790
34013
  function backfillOutOfScope(prd, specContent, featureName) {
33791
34014
  const missing = findMissingOutOfScope(specContent, prd);
33792
34015
  if (missing.length === 0)
@@ -33808,7 +34031,7 @@ function warnOnSpecDrift(prd, featureName) {
33808
34031
  });
33809
34032
  }
33810
34033
  }
33811
- var init_verbatim_warn = __esm(() => {
34034
+ var init_plan_fidelity = __esm(() => {
33812
34035
  init_logger2();
33813
34036
  init_prd();
33814
34037
  });
@@ -33820,7 +34043,7 @@ var init_plan = __esm(() => {
33820
34043
  init_config();
33821
34044
  init_schema2();
33822
34045
  init_prompts();
33823
- init_verbatim_warn();
34046
+ init_plan_fidelity();
33824
34047
  planInteractiveOp = {
33825
34048
  kind: "run",
33826
34049
  name: "plan-interactive",
@@ -33865,7 +34088,6 @@ ${outputFormat}`, overridable: false }
33865
34088
  verify: async (parsed, input, _ctx) => {
33866
34089
  if (!parsed.userStories || parsed.userStories.length === 0)
33867
34090
  return null;
33868
- warnOnDroppedVerbatimAcs(parsed, input.specContent, input.featureName);
33869
34091
  return backfillOutOfScope(parsed, input.specContent, input.featureName);
33870
34092
  },
33871
34093
  recover: async (input, ctx) => {
@@ -33951,20 +34173,6 @@ function validateRefinedPrd(prd) {
33951
34173
  validateRefinedStory(story);
33952
34174
  return prd;
33953
34175
  }
33954
- async function readMissingVerbatimAcs(input) {
33955
- const content = await _planRefineDeps.readFile(input.outputPath);
33956
- if (!content)
33957
- return [];
33958
- try {
33959
- const prd = validatePlanOutput(content, input.featureName, input.branchName);
33960
- return findMissingVerbatimAcs(input.specContent, prd);
33961
- } catch {
33962
- getSafeLogger()?.debug("plan", "Skipped [verbatim] self-heal \u2014 draft PRD not yet parseable", {
33963
- featureName: input.featureName
33964
- });
33965
- return [];
33966
- }
33967
- }
33968
34176
  async function readSpecDriftViolations(input) {
33969
34177
  const content = await _planRefineDeps.readFile(input.outputPath);
33970
34178
  if (!content)
@@ -34053,17 +34261,6 @@ async function normalizeCreatedContextFiles(prd, workdir, fileExists) {
34053
34261
  return prd;
34054
34262
  return { ...prd, userStories: results.map((r) => r.story) };
34055
34263
  }
34056
- function verbatimSelfHealStep(builder) {
34057
- return makeSelfHealStep({
34058
- detect: (input) => readMissingVerbatimAcs(input),
34059
- buildRepair: (missing, input) => builder.buildVerbatimRepair(missing, input.outputPath),
34060
- log: {
34061
- kind: "plan",
34062
- message: "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn",
34063
- meta: (input, missing) => ({ featureName: input.featureName, missingCount: missing.length })
34064
- }
34065
- });
34066
- }
34067
34264
  async function readMissingOutOfScope(input) {
34068
34265
  const content = await _planRefineDeps.readFile(input.outputPath);
34069
34266
  if (!content)
@@ -34110,8 +34307,8 @@ var init_plan_refine = __esm(() => {
34110
34307
  init_prd();
34111
34308
  init_schema2();
34112
34309
  init_prompts();
34310
+ init_plan_fidelity();
34113
34311
  init_self_heal();
34114
- init_verbatim_warn();
34115
34312
  _planRefineDeps = {
34116
34313
  readFile: async (path3) => {
34117
34314
  try {
@@ -34195,7 +34392,6 @@ ${outputFormat}`,
34195
34392
  estimatedCostUsd: (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0)
34196
34393
  };
34197
34394
  const steps = [
34198
- verbatimSelfHealStep(builder),
34199
34395
  outOfScopeSelfHealStep(builder),
34200
34396
  ...specGuard ? [specDriftSelfHealStep(builder)] : []
34201
34397
  ];
@@ -34206,7 +34402,6 @@ ${outputFormat}`,
34206
34402
  },
34207
34403
  verify: async (parsed, input, ctx) => {
34208
34404
  const validated = validateRefinedPrd(parsed);
34209
- warnOnDroppedVerbatimAcs(validated, input.specContent, input.featureName);
34210
34405
  if (ctx.config.plan.specGuard) {
34211
34406
  warnOnSpecDrift(validated, input.featureName);
34212
34407
  }
@@ -35638,6 +35833,11 @@ function categoryToFixTarget(category) {
35638
35833
  return "source";
35639
35834
  return category != null && BLOCKING_CATEGORIES.has(category) ? "source" : "test";
35640
35835
  }
35836
+ function resolveFixTarget({ base, file: file3, isTestFile: isTestFile3 }) {
35837
+ if (file3 && isTestFile3?.(file3))
35838
+ return "test";
35839
+ return base;
35840
+ }
35641
35841
  var init_category_fix_target = __esm(() => {
35642
35842
  init_ac_structural_counterfactual();
35643
35843
  });
@@ -35665,7 +35865,7 @@ function normalizeSeverity(sev) {
35665
35865
  return sev;
35666
35866
  return "info";
35667
35867
  }
35668
- function toAdversarialReviewFindings(findings) {
35868
+ function toAdversarialReviewFindings(findings, opts = {}) {
35669
35869
  return findings.map((f) => {
35670
35870
  const metaExtras = {};
35671
35871
  if (f.acQuote)
@@ -35686,7 +35886,7 @@ function toAdversarialReviewFindings(findings) {
35686
35886
  line: f.line,
35687
35887
  message: f.issue,
35688
35888
  suggestion: f.suggestion,
35689
- fixTarget: categoryToFixTarget(f.category),
35889
+ fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
35690
35890
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35691
35891
  };
35692
35892
  });
@@ -35750,7 +35950,7 @@ function downgradeToUnverifiable(finding) {
35750
35950
  severity: "unverifiable"
35751
35951
  };
35752
35952
  }
35753
- function llmFindingToFinding(f) {
35953
+ function llmFindingToFinding(f, opts = {}) {
35754
35954
  const metaExtras = {};
35755
35955
  if (f.verifiedBy)
35756
35956
  metaExtras.verifiedBy = f.verifiedBy;
@@ -35766,15 +35966,16 @@ function llmFindingToFinding(f) {
35766
35966
  line: f.line,
35767
35967
  message: f.issue,
35768
35968
  suggestion: f.suggestion ?? undefined,
35769
- fixTarget: "source",
35969
+ fixTarget: resolveFixTarget({ base: "source", file: f.file, isTestFile: opts.isTestFile }),
35770
35970
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35771
35971
  };
35772
35972
  }
35773
- function toReviewFindings(findings) {
35774
- return findings.map(llmFindingToFinding);
35973
+ function toReviewFindings(findings, opts = {}) {
35974
+ return findings.map((f) => llmFindingToFinding(f, opts));
35775
35975
  }
35776
35976
  var UNVERIFIED_FINDING_PATTERNS;
35777
35977
  var init_semantic_helpers = __esm(() => {
35978
+ init_category_fix_target();
35778
35979
  init_severity();
35779
35980
  UNVERIFIED_FINDING_PATTERNS = [
35780
35981
  "cannot verify",
@@ -35918,9 +36119,9 @@ function parseRequoteResponse(output) {
35918
36119
  const parsed = tryParseLLMJson(output);
35919
36120
  if (!isRecord(parsed))
35920
36121
  return null;
35921
- const canonical3 = extractCanonical(parsed);
35922
- if (canonical3)
35923
- return canonical3;
36122
+ const canonical2 = extractCanonical(parsed);
36123
+ if (canonical2)
36124
+ return canonical2;
35924
36125
  const findings = parsed.findings;
35925
36126
  if (!Array.isArray(findings) || findings.length !== 1)
35926
36127
  return null;
@@ -35967,6 +36168,10 @@ function withRepromptMarker(output, info) {
35967
36168
  return output;
35968
36169
  return JSON.stringify({ ...parsed, _repromptInfo: info });
35969
36170
  }
36171
+ function semanticTestFileMatch(input) {
36172
+ const patterns = input.resolvedTestPatterns?.regex ?? [];
36173
+ return (file3) => patterns.some((re) => re.test(file3));
36174
+ }
35970
36175
  function extractRepromptInfo(raw) {
35971
36176
  if (!raw || typeof raw !== "object")
35972
36177
  return;
@@ -36258,7 +36463,7 @@ var init_semantic_review = __esm(() => {
36258
36463
  ...parsed,
36259
36464
  passed,
36260
36465
  findings: accepted,
36261
- normalizedFindings: toReviewFindings(blocking),
36466
+ normalizedFindings: toReviewFindings(blocking, { isTestFile: semanticTestFileMatch(input) }),
36262
36467
  acDropped: dropped
36263
36468
  };
36264
36469
  }
@@ -36674,10 +36879,10 @@ var init_adversarial_review = __esm(() => {
36674
36879
  ...parsed,
36675
36880
  passed,
36676
36881
  findings: accepted,
36677
- normalizedFindings: toAdversarialReviewFindings(blocking),
36882
+ normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
36678
36883
  advisoryFindings: [
36679
- ...toAdversarialReviewFindings(advisory),
36680
- ...tagCoverageGap(toAdversarialReviewFindings(demoted))
36884
+ ...toAdversarialReviewFindings(advisory, { isTestFile: testFileMatch }),
36885
+ ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
36681
36886
  ],
36682
36887
  acDropped: dropped
36683
36888
  };
@@ -39063,14 +39268,9 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
39063
39268
  const pid = proc.pid;
39064
39269
  killProcessGroup(pid, "SIGTERM");
39065
39270
  let exitedDuringGrace = false;
39066
- await Promise.race([
39067
- proc.exited.then(() => {
39068
- exitedDuringGrace = true;
39069
- }),
39070
- new Promise((resolve11) => {
39071
- setTimeout(resolve11, gracePeriodMs);
39072
- })
39073
- ]);
39271
+ await raceWithDeadline(proc.exited.then(() => {
39272
+ exitedDuringGrace = true;
39273
+ }).catch(() => {}), gracePeriodMs);
39074
39274
  if (!exitedDuringGrace) {
39075
39275
  killProcessGroup(pid, "SIGKILL");
39076
39276
  }
@@ -39946,7 +40146,7 @@ async function runQualityCommand(opts) {
39946
40146
  let sigkillTimer;
39947
40147
  proc.exited.then(() => {
39948
40148
  exitedBeforeSigkill = true;
39949
- });
40149
+ }).catch(() => {});
39950
40150
  const killTimer = setTimeout(() => {
39951
40151
  timedOut = true;
39952
40152
  killProcessGroup(proc.pid, "SIGTERM");
@@ -41378,7 +41578,7 @@ var init_operations = __esm(() => {
41378
41578
  init_plan();
41379
41579
  init_plan_refine();
41380
41580
  init_self_heal();
41381
- init_verbatim_warn();
41581
+ init_plan_fidelity();
41382
41582
  init_decompose2();
41383
41583
  init_build_hop_callback();
41384
41584
  init_classify_route();
@@ -41495,11 +41695,15 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41495
41695
  const storyId = ctx.storyId;
41496
41696
  const packageDir = ctx.packageDir;
41497
41697
  let totalCostUsd = 0;
41698
+ const spentStrategies = new Set;
41699
+ let unresolvedDetail;
41700
+ const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
41498
41701
  for (;; ) {
41499
41702
  if (cycle.findings.length === 0 && cycle.verdict === undefined) {
41500
41703
  return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
41501
41704
  }
41502
- const active = selectActiveStrategies(cycle.strategies, cycle.findings, cycle.verdict);
41705
+ const selectable = cycle.strategies.filter((s) => !spentStrategies.has(s.name));
41706
+ const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
41503
41707
  if (active.length === 0) {
41504
41708
  const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
41505
41709
  logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
@@ -41508,14 +41712,15 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41508
41712
  cycleName,
41509
41713
  reason: "no-strategy",
41510
41714
  findingsCount: cycle.findings.length,
41511
- orphanSources
41715
+ orphanSources,
41716
+ ...spentStrategies.size > 0 ? { retiredStrategies: [...spentStrategies] } : {}
41512
41717
  });
41513
- return {
41718
+ return finish({
41514
41719
  iterations: cycle.iterations,
41515
41720
  finalFindings: cycle.findings,
41516
41721
  exitReason: "no-strategy",
41517
41722
  costUsd: totalCostUsd
41518
- };
41723
+ });
41519
41724
  }
41520
41725
  const uncappedActive = active.filter((s) => countStrategyAttempts(cycle.iterations, s.name) < s.maxAttempts);
41521
41726
  if (uncappedActive.length === 0) {
@@ -41527,13 +41732,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41527
41732
  reason: "max-attempts-per-strategy",
41528
41733
  exhaustedStrategy: exhaustedStrategy?.name
41529
41734
  });
41530
- return {
41735
+ return finish({
41531
41736
  iterations: cycle.iterations,
41532
41737
  finalFindings: cycle.findings,
41533
41738
  exitReason: "max-attempts-per-strategy",
41534
41739
  exhaustedStrategy: exhaustedStrategy?.name,
41535
41740
  costUsd: totalCostUsd
41536
- };
41741
+ });
41537
41742
  }
41538
41743
  const totalAttempts = countTotalAttempts(cycle.iterations);
41539
41744
  if (totalAttempts >= cycle.config.maxAttemptsTotal) {
@@ -41545,12 +41750,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41545
41750
  totalAttempts,
41546
41751
  maxAttemptsTotal: cycle.config.maxAttemptsTotal
41547
41752
  });
41548
- return {
41753
+ return finish({
41549
41754
  iterations: cycle.iterations,
41550
41755
  finalFindings: cycle.findings,
41551
41756
  exitReason: "max-attempts-total",
41552
41757
  costUsd: totalCostUsd
41553
- };
41758
+ });
41554
41759
  }
41555
41760
  for (const strategy of uncappedActive) {
41556
41761
  const bailReason = strategy.bailWhen?.(cycle.iterations) ?? null;
@@ -41563,13 +41768,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41563
41768
  strategyName: strategy.name,
41564
41769
  bailDetail: bailReason
41565
41770
  });
41566
- return {
41771
+ return finish({
41567
41772
  iterations: cycle.iterations,
41568
41773
  finalFindings: cycle.findings,
41569
41774
  exitReason: "bail-when",
41570
41775
  bailDetail: bailReason,
41571
41776
  costUsd: totalCostUsd
41572
- };
41777
+ });
41573
41778
  }
41574
41779
  }
41575
41780
  const group = selectExecutionGroup(uncappedActive);
@@ -41590,33 +41795,49 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41590
41795
  costUsd: extracted.costUsd
41591
41796
  });
41592
41797
  }
41593
- const unresolvedFa = fixesApplied.find((fa) => fa.unresolved);
41594
- if (unresolvedFa) {
41595
- const finishedAt2 = now();
41596
- cycle.iterations.push({
41597
- iterationNum: cycle.iterations.length + 1,
41598
- findingsBefore,
41599
- fixesApplied,
41600
- findingsAfter: cycle.findings,
41601
- outcome: "unchanged",
41602
- startedAt,
41603
- finishedAt: finishedAt2
41604
- });
41605
- logger?.info("findings.cycle", "cycle exited \u2014 agent gave up", {
41798
+ const unresolvedFas = fixesApplied.filter((fa) => fa.unresolved);
41799
+ if (unresolvedFas.length > 0) {
41800
+ const firstUnresolved = unresolvedFas[0];
41801
+ unresolvedDetail = firstUnresolved.unresolved;
41802
+ for (const fa of unresolvedFas)
41803
+ spentStrategies.add(fa.strategyName);
41804
+ const allGaveUp = unresolvedFas.length === fixesApplied.length;
41805
+ if (allGaveUp) {
41806
+ const finishedAt2 = now();
41807
+ cycle.iterations.push({
41808
+ iterationNum: cycle.iterations.length + 1,
41809
+ findingsBefore,
41810
+ fixesApplied,
41811
+ findingsAfter: cycle.findings,
41812
+ outcome: "unchanged",
41813
+ startedAt,
41814
+ finishedAt: finishedAt2
41815
+ });
41816
+ totalCostUsd += fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41817
+ logger?.info("findings.cycle", "cycle exited \u2014 agent gave up", {
41818
+ storyId,
41819
+ packageDir,
41820
+ cycleName,
41821
+ reason: "agent-gave-up",
41822
+ strategyName: firstUnresolved.strategyName,
41823
+ unresolvedDetail: firstUnresolved.unresolved
41824
+ });
41825
+ return finish({
41826
+ iterations: cycle.iterations,
41827
+ finalFindings: cycle.findings,
41828
+ exitReason: "agent-gave-up",
41829
+ unresolvedDetail: firstUnresolved.unresolved,
41830
+ costUsd: totalCostUsd
41831
+ });
41832
+ }
41833
+ logger?.info("findings.cycle", "strategy gave up \u2014 retired, continuing with co-run siblings", {
41606
41834
  storyId,
41607
41835
  packageDir,
41608
41836
  cycleName,
41609
- reason: "agent-gave-up",
41610
- strategyName: unresolvedFa.strategyName,
41611
- unresolvedDetail: unresolvedFa.unresolved
41837
+ strategyName: firstUnresolved.strategyName,
41838
+ unresolvedDetail: firstUnresolved.unresolved,
41839
+ ranWithoutGivingUp: fixesApplied.filter((fa) => !fa.unresolved).map((fa) => fa.strategyName)
41612
41840
  });
41613
- return {
41614
- iterations: cycle.iterations,
41615
- finalFindings: cycle.findings,
41616
- exitReason: "agent-gave-up",
41617
- unresolvedDetail: unresolvedFa.unresolved,
41618
- costUsd: totalCostUsd
41619
- };
41620
41841
  }
41621
41842
  const allExhausted = group.every((s) => {
41622
41843
  const prior = countStrategyAttempts(cycle.iterations, s.name);
@@ -41624,6 +41845,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41624
41845
  return prior + current >= s.maxAttempts;
41625
41846
  });
41626
41847
  if (allExhausted) {
41848
+ totalCostUsd += fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41627
41849
  let liteFindingsAfter;
41628
41850
  let liteShortCircuited = false;
41629
41851
  try {
@@ -41648,13 +41870,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41648
41870
  cycleName,
41649
41871
  error: errorMessage(err)
41650
41872
  });
41651
- return {
41873
+ return finish({
41652
41874
  iterations: cycle.iterations,
41653
41875
  finalFindings: cycle.findings,
41654
41876
  exitReason: "max-attempts-per-strategy",
41655
41877
  exhaustedStrategy: group[0]?.name,
41656
41878
  costUsd: totalCostUsd
41657
- };
41879
+ });
41658
41880
  }
41659
41881
  const outcome2 = classifyOutcome(findingsBefore, liteFindingsAfter);
41660
41882
  const finishedAt2 = now();
@@ -41675,18 +41897,16 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41675
41897
  cycleName,
41676
41898
  reason: "resolved"
41677
41899
  });
41678
- return {
41900
+ return finish({
41679
41901
  iterations: cycle.iterations,
41680
41902
  finalFindings: [],
41681
41903
  exitReason: "resolved",
41682
41904
  costUsd: totalCostUsd
41683
- };
41905
+ });
41684
41906
  }
41685
41907
  if (liteShortCircuited) {
41686
41908
  const companions = uncappedActive.filter((s) => !group.includes(s));
41687
41909
  if (companions.length > 0) {
41688
- const iterCostUsd = fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41689
- totalCostUsd += iterCostUsd;
41690
41910
  logger?.info("findings.cycle", "exclusive strategy exhausted \u2014 continuing to companion strategies", {
41691
41911
  storyId,
41692
41912
  packageDir,
@@ -41703,12 +41923,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41703
41923
  reason: "validate-short-circuit",
41704
41924
  liteFindingsAfterCount: liteFindingsAfter.length
41705
41925
  });
41706
- return {
41926
+ return finish({
41707
41927
  iterations: cycle.iterations,
41708
41928
  finalFindings: liteFindingsAfter,
41709
41929
  exitReason: "validate-short-circuit",
41710
41930
  costUsd: totalCostUsd
41711
- };
41931
+ });
41712
41932
  }
41713
41933
  logger?.info("findings.cycle", "cycle exited \u2014 strategy attempt cap reached (lite validate)", {
41714
41934
  storyId,
@@ -41718,13 +41938,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41718
41938
  exhaustedStrategy: group[0]?.name,
41719
41939
  liteFindingsAfterCount: liteFindingsAfter.length
41720
41940
  });
41721
- return {
41941
+ return finish({
41722
41942
  iterations: cycle.iterations,
41723
41943
  finalFindings: liteFindingsAfter,
41724
41944
  exitReason: "max-attempts-per-strategy",
41725
41945
  exhaustedStrategy: group[0]?.name,
41726
41946
  costUsd: totalCostUsd
41727
- };
41947
+ });
41728
41948
  }
41729
41949
  let findingsAfter;
41730
41950
  let validatorAttempt = 0;
@@ -41742,12 +41962,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41742
41962
  reason: "validator-error",
41743
41963
  error: errorMessage(err)
41744
41964
  });
41745
- return {
41965
+ return finish({
41746
41966
  iterations: cycle.iterations,
41747
41967
  finalFindings: cycle.findings,
41748
41968
  exitReason: "validator-error",
41749
41969
  costUsd: totalCostUsd
41750
- };
41970
+ });
41751
41971
  }
41752
41972
  logger?.warn("findings.cycle", "validator retry", {
41753
41973
  storyId,
@@ -42084,11 +42304,9 @@ function buildMeta(f, originalSeverity) {
42084
42304
  function findingCategory(f) {
42085
42305
  return "category" in f && f.category ? f.category : undefined;
42086
42306
  }
42087
- function deriveFixTargetForReviewFinding(category, source) {
42088
- if (source === "semantic-review" || source === "semantic-debate-review") {
42089
- return "source";
42090
- }
42091
- return categoryToFixTarget(category);
42307
+ function deriveFixTargetForReviewFinding(category, source, file3, isTestFile3) {
42308
+ const base = source === "semantic-review" || source === "semantic-debate-review" ? "source" : categoryToFixTarget(category);
42309
+ return resolveFixTarget({ base, file: file3, isTestFile: isTestFile3 });
42092
42310
  }
42093
42311
  function llmFindingToReviewFinding(f, opts = {}) {
42094
42312
  const category = findingCategory(f);
@@ -42105,7 +42323,7 @@ function llmFindingToReviewFinding(f, opts = {}) {
42105
42323
  result.category = category;
42106
42324
  if (source)
42107
42325
  result.source = source;
42108
- result.fixTarget = deriveFixTargetForReviewFinding(category, source);
42326
+ result.fixTarget = deriveFixTargetForReviewFinding(category, source, f.file, opts.isTestFile);
42109
42327
  const meta3 = buildMeta(f, f.severity !== narrowed ? f.severity : undefined);
42110
42328
  if (meta3)
42111
42329
  result.meta = meta3;
@@ -42275,7 +42493,7 @@ var package_default;
42275
42493
  var init_package = __esm(() => {
42276
42494
  package_default = {
42277
42495
  name: "@nathapp/nax",
42278
- version: "0.74.0",
42496
+ version: "0.75.1",
42279
42497
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42280
42498
  type: "module",
42281
42499
  bin: {
@@ -42378,8 +42596,8 @@ var init_version = __esm(() => {
42378
42596
  NAX_VERSION = package_default.version;
42379
42597
  NAX_COMMIT = (() => {
42380
42598
  try {
42381
- if (/^[0-9a-f]{6,10}$/.test("91bb7306"))
42382
- return "91bb7306";
42599
+ if (/^[0-9a-f]{6,10}$/.test("928c0354"))
42600
+ return "928c0354";
42383
42601
  } catch {}
42384
42602
  try {
42385
42603
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42785,12 +43003,12 @@ async function runAdversarialReview(opts) {
42785
43003
  } = classifyRecurrence(allFindings, priorAdversarialIterations ?? [], recurrenceCfg, testFileMatch, threshold);
42786
43004
  const advisoryFindings = [...advisoryOnly, ...demoted];
42787
43005
  const advisoryReviewFindings = [
42788
- ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review" }),
42789
- ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review" }))
43006
+ ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review", isTestFile: testFileMatch }),
43007
+ ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review", isTestFile: testFileMatch }))
42790
43008
  ];
42791
43009
  const advisoryFindingsAsFindings = [
42792
- ...toAdversarialReviewFindings(advisoryOnly),
42793
- ...tagCoverageGap(toAdversarialReviewFindings(demoted))
43010
+ ...toAdversarialReviewFindings(advisoryOnly, { isTestFile: testFileMatch }),
43011
+ ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
42794
43012
  ];
42795
43013
  const acDropped = opResult.acDropped ?? [];
42796
43014
  let diffFiles;
@@ -42870,7 +43088,7 @@ async function runAdversarialReview(opts) {
42870
43088
  blockingThreshold: threshold,
42871
43089
  result: {
42872
43090
  passed: false,
42873
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
43091
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
42874
43092
  },
42875
43093
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
42876
43094
  diffAvailable,
@@ -42887,7 +43105,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
42887
43105
  exitCode: 1,
42888
43106
  output,
42889
43107
  durationMs,
42890
- findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings) : undefined,
43108
+ findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings, { isTestFile: testFileMatch }) : undefined,
42891
43109
  advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
42892
43110
  cost: llmCost
42893
43111
  };
@@ -42895,7 +43113,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
42895
43113
  if (!opResult.passed && acDropped.length > 0) {
42896
43114
  const allHallucinated = acDropped.every((d) => d.code === "ac_quote_not_substring");
42897
43115
  if (allHallucinated) {
42898
- const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })));
43116
+ const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
42899
43117
  const existingAdvisory = advisoryFindings.length > 0 ? advisoryFindingsAsFindings : [];
42900
43118
  const allAdvisory = [...existingAdvisory, ...demotedFindings];
42901
43119
  logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
@@ -42983,7 +43201,7 @@ ${dropSummary}`,
42983
43201
  blockingThreshold: threshold,
42984
43202
  result: {
42985
43203
  passed: true,
42986
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
43204
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
42987
43205
  },
42988
43206
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
42989
43207
  diffAvailable,
@@ -43488,7 +43706,8 @@ async function runSemanticDebate(opts) {
43488
43706
  prompt,
43489
43707
  productionExcludePatterns,
43490
43708
  blockingThreshold,
43491
- createDebateRunner
43709
+ createDebateRunner,
43710
+ isTestFile: isTestFile3
43492
43711
  } = opts;
43493
43712
  const logger = getSafeLogger();
43494
43713
  const configuredStageConfig = naxConfig.debate?.stages.review;
@@ -43564,9 +43783,9 @@ async function runSemanticDebate(opts) {
43564
43783
  blockingThreshold: debateThreshold,
43565
43784
  result: {
43566
43785
  passed: false,
43567
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43786
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43568
43787
  },
43569
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43788
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43570
43789
  });
43571
43790
  return {
43572
43791
  check: "semantic",
@@ -43577,8 +43796,8 @@ async function runSemanticDebate(opts) {
43577
43796
 
43578
43797
  ${formatFindings2(debateBlocking)}`,
43579
43798
  durationMs,
43580
- findings: toReviewFindings(debateBlocking),
43581
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43799
+ findings: toReviewFindings(debateBlocking, { isTestFile: isTestFile3 }),
43800
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43582
43801
  cost: debateCost
43583
43802
  };
43584
43803
  }
@@ -43596,9 +43815,9 @@ ${formatFindings2(debateBlocking)}`,
43596
43815
  blockingThreshold: debateThreshold,
43597
43816
  result: {
43598
43817
  passed: true,
43599
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43818
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43600
43819
  },
43601
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43820
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43602
43821
  });
43603
43822
  return {
43604
43823
  check: "semantic",
@@ -43607,7 +43826,7 @@ ${formatFindings2(debateBlocking)}`,
43607
43826
  exitCode: 0,
43608
43827
  output: "Semantic review passed (debate, all findings were advisory \u2014 below blocking threshold)",
43609
43828
  durationMs,
43610
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43829
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43611
43830
  cost: debateCost
43612
43831
  };
43613
43832
  }
@@ -43622,9 +43841,9 @@ ${formatFindings2(debateBlocking)}`,
43622
43841
  blockingThreshold: debateThreshold,
43623
43842
  result: {
43624
43843
  passed: true,
43625
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43844
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43626
43845
  },
43627
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43846
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43628
43847
  });
43629
43848
  return {
43630
43849
  check: "semantic",
@@ -43633,7 +43852,7 @@ ${formatFindings2(debateBlocking)}`,
43633
43852
  exitCode: 0,
43634
43853
  output: "Semantic review passed",
43635
43854
  durationMs,
43636
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43855
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43637
43856
  cost: debateCost
43638
43857
  };
43639
43858
  }
@@ -43679,10 +43898,13 @@ async function runSemanticReview(opts) {
43679
43898
  contextBundle,
43680
43899
  projectDir,
43681
43900
  naxIgnoreIndex,
43682
- runtime
43901
+ runtime,
43902
+ resolvedTestPatterns
43683
43903
  } = opts;
43684
43904
  const startTime = Date.now();
43685
43905
  const logger = getSafeLogger();
43906
+ const testFilePatterns = resolvedTestPatterns?.regex ?? [];
43907
+ const testFileMatch = (file3) => testFilePatterns.some((re) => re.test(file3));
43686
43908
  if (featureName === undefined) {
43687
43909
  logger?.debug("semantic", "featureName missing \u2014 semantic session name will not include feature", {
43688
43910
  storyId: story.id
@@ -43815,6 +44037,7 @@ async function runSemanticReview(opts) {
43815
44037
  prompt,
43816
44038
  productionExcludePatterns: excludePatterns,
43817
44039
  blockingThreshold,
44040
+ isTestFile: testFileMatch,
43818
44041
  createDebateRunner: _semanticDeps.createDebateRunner
43819
44042
  });
43820
44043
  }
@@ -43974,9 +44197,9 @@ ${formatFindings2(blockingFindings)}`;
43974
44197
  blockingThreshold: threshold,
43975
44198
  result: {
43976
44199
  passed: false,
43977
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
44200
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
43978
44201
  },
43979
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44202
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
43980
44203
  });
43981
44204
  return {
43982
44205
  check: "semantic",
@@ -43985,8 +44208,8 @@ ${formatFindings2(blockingFindings)}`;
43985
44208
  exitCode: 1,
43986
44209
  output,
43987
44210
  durationMs,
43988
- findings: toReviewFindings(blockingFindings),
43989
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44211
+ findings: toReviewFindings(blockingFindings, { isTestFile: testFileMatch }),
44212
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
43990
44213
  cost: llmCost
43991
44214
  };
43992
44215
  }
@@ -44006,7 +44229,7 @@ ${formatFindings2(blockingFindings)}`;
44006
44229
  passed: false,
44007
44230
  blockingThreshold: threshold,
44008
44231
  result: { passed: false, findings: [] },
44009
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44232
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
44010
44233
  });
44011
44234
  return {
44012
44235
  check: "semantic",
@@ -44015,7 +44238,7 @@ ${formatFindings2(blockingFindings)}`;
44015
44238
  exitCode: 1,
44016
44239
  output: 'Semantic review failed: blocking finding(s) were dropped \u2014 acIndex was missing or out of range. The model emitted "passed: false" without valid AC attribution.',
44017
44240
  durationMs,
44018
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44241
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
44019
44242
  cost: llmCost
44020
44243
  };
44021
44244
  }
@@ -44032,9 +44255,9 @@ ${formatFindings2(blockingFindings)}`;
44032
44255
  blockingThreshold: threshold,
44033
44256
  result: {
44034
44257
  passed: true,
44035
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
44258
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
44036
44259
  },
44037
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44260
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
44038
44261
  });
44039
44262
  return {
44040
44263
  check: "semantic",
@@ -44043,7 +44266,7 @@ ${formatFindings2(blockingFindings)}`;
44043
44266
  exitCode: 0,
44044
44267
  output: allFindings.length === 0 ? "Semantic review passed" : "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
44045
44268
  durationMs,
44046
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44269
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
44047
44270
  cost: llmCost
44048
44271
  };
44049
44272
  }
@@ -45339,6 +45562,43 @@ var init_rectifier_builder = __esm(() => {
45339
45562
  ];
45340
45563
  });
45341
45564
 
45565
+ // src/prompts/builders/timeout-retry-builder.ts
45566
+ function formatDuration2(ms) {
45567
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
45568
+ const minutes = Math.floor(totalSeconds / 60);
45569
+ const seconds = totalSeconds % 60;
45570
+ if (minutes === 0)
45571
+ return `${seconds}s`;
45572
+ return `${minutes}m ${seconds}s`;
45573
+ }
45574
+ function timeoutRetry(input) {
45575
+ const { prompt, changedFiles, elapsedMs, attempt } = input;
45576
+ const duration3 = formatDuration2(elapsedMs);
45577
+ const attemptNumber = attempt + 1;
45578
+ if (changedFiles.length === 0) {
45579
+ return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}) with no file changes on disk.
45580
+ This is attempt ${attemptNumber} of the same story \u2014 the previous attempt left nothing behind, so the approach was wrong.
45581
+ Change your approach: pick a narrower scope, fewer file edits, or a different angle on the acceptance criteria.
45582
+
45583
+ ---
45584
+
45585
+ ${prompt}`;
45586
+ }
45587
+ const fileList = changedFiles.map((p) => `- ${p}`).join(`
45588
+ `);
45589
+ return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}), but left these files on disk:
45590
+
45591
+ ${fileList}
45592
+
45593
+ This is attempt ${attemptNumber} of the same story \u2014 continue from the existing state above.
45594
+ Read the files listed, pick up where the previous attempt stopped, and finish the story.
45595
+ Do NOT delete or revert the existing work; treat the working tree as the starting point.
45596
+
45597
+ ---
45598
+
45599
+ ${prompt}`;
45600
+ }
45601
+
45342
45602
  // src/prompts/builders/one-shot-builder.ts
45343
45603
  class OneShotPromptBuilder {
45344
45604
  acc = new SectionAccumulator;
@@ -45501,7 +45761,7 @@ No acceptance criterion may use a deprecated verification tag (\`[grep]\`, \`[fi
45501
45761
  Review the draft with a strict self-audit mindset. Re-read the codebase context and compare the PRD against it. Focus only on the issues below, then rewrite the PRD if needed.
45502
45762
 
45503
45763
  #### spec-ac-preservation
45504
- Enumerate every acceptance criterion the spec states. Confirm each one appears in some story's acceptanceCriteria \u2014 never drop a spec AC during this audit. If an AC looks unsupported by the current codebase, keep it: the story may be adding that capability. Any AC the spec tags \`[verbatim]\` MUST appear character-for-character in an acceptanceCriteria entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly. If a \`[verbatim]\` AC is missing or altered, restore it verbatim.
45764
+ Enumerate every acceptance criterion the spec states. Confirm each one appears in some story's acceptanceCriteria \u2014 never drop a spec AC during this audit. If an AC looks unsupported by the current codebase, keep it: the story may be adding that capability. An AC carrying a deprecated \`[grep]\`/\`[file]\`/\`[verbatim]\` tag is a file-content check, not a runtime behaviour \u2014 rewrite it as the behaviour it was meant to prove and drop the tag; that rewrite is required, not a dropped AC.
45505
45765
 
45506
45766
  #### ac-testable
45507
45767
  For each acceptance criterion, ask whether the assertion is observable through a return value, exception, log output, file content, or state change. If any AC is not directly testable, rewrite it so it is observable.
@@ -45553,23 +45813,6 @@ For each one:
45553
45813
  - Remove any shell-command patterns (\`grep -\`, \`wc\`, pipe \`|\` inside backticks). Express the same invariant as an assertion on the runtime value.
45554
45814
  - Do not remove or weaken acceptance criteria that are already correct.
45555
45815
 
45556
- Write the corrected PRD to this file path: ${outputFilePath}
45557
- Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
45558
- }
45559
- buildVerbatimRepair(missingAcs, outputFilePath) {
45560
- const list = missingAcs.map((ac) => `- ${ac}`).join(`
45561
- `);
45562
- return `Your revised PRD dropped or altered acceptance criteria the spec marked \`[verbatim]\`. These are load-bearing executable checks (greps, file-existence checks, regex/count assertions, or architectural invariants) and MUST survive character-for-character \u2014 paraphrasing destroys the verification mechanism.
45563
-
45564
- The following \`[verbatim]\` spec acceptance criteria are missing or altered in the PRD:
45565
-
45566
- ${list}
45567
-
45568
- For each one:
45569
- - Add it to the \`acceptanceCriteria\` array of the single most relevant user story.
45570
- - Preserve every backtick-quoted command, file path, regex, and count exactly as written in the spec. Do not paraphrase, retag, split, or move them into a description.
45571
- - Do not remove or weaken any acceptance criteria that are already correct.
45572
-
45573
45816
  Write the corrected PRD to this file path: ${outputFilePath}
45574
45817
  Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
45575
45818
  }
@@ -46310,10 +46553,17 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46310
46553
  hopBodyInput
46311
46554
  } = ctx;
46312
46555
  const stage = pipelineStage ?? "run";
46556
+ let preAttemptGitRefPromise;
46557
+ let priorHopStartedAt;
46313
46558
  return async (agentName, hopBundle, hopKind, resolvedRunOptions) => {
46314
46559
  const logger = getLogger();
46315
46560
  let workingBundle = hopBundle;
46316
46561
  let prompt = resolvedRunOptions.prompt;
46562
+ const elapsedSincePriorHop = priorHopStartedAt ? Date.now() - priorHopStartedAt : 0;
46563
+ priorHopStartedAt = Date.now();
46564
+ if (hopKind.kind === "primary" && !preAttemptGitRefPromise) {
46565
+ preAttemptGitRefPromise = _buildHopCallbackDeps.captureGitRef(workdir);
46566
+ }
46317
46567
  if (hopKind.kind === "swap" && hopBundle) {
46318
46568
  workingBundle = _buildHopCallbackDeps.rebuildForAgent(hopBundle, agentName, hopKind.failure, story.id);
46319
46569
  if (projectDir && featureName && workingBundle.manifest.rebuildInfo) {
@@ -46342,6 +46592,17 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46342
46592
  if (hopKind.kind === "swap" && sessionId) {
46343
46593
  sessionManager.handoff?.(sessionId, agentName, hopKind.failure.outcome);
46344
46594
  }
46595
+ if (hopKind.kind === "timeout-retry") {
46596
+ const preAttemptGitRef = preAttemptGitRefPromise ? await preAttemptGitRefPromise : undefined;
46597
+ const changedFiles = preAttemptGitRef ? await _buildHopCallbackDeps.captureWorkingTreeChanges(workdir, preAttemptGitRef) : [];
46598
+ const elapsedMs = elapsedSincePriorHop;
46599
+ prompt = _buildHopCallbackDeps.timeoutRetry({
46600
+ prompt: resolvedRunOptions.prompt,
46601
+ changedFiles,
46602
+ elapsedMs,
46603
+ attempt: hopKind.attempt
46604
+ });
46605
+ }
46345
46606
  const contextToolRuntime = workingBundle ? _buildHopCallbackDeps.createContextToolRuntime({
46346
46607
  bundle: workingBundle,
46347
46608
  story,
@@ -46400,6 +46661,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46400
46661
  signal: resolvedRunOptions.abortSignal
46401
46662
  });
46402
46663
  }
46664
+ let timedOut = false;
46403
46665
  try {
46404
46666
  const send = (turnPrompt) => agentManager.runAsSession(agentName, handle, turnPrompt, {
46405
46667
  storyId: story.id,
@@ -46417,9 +46679,12 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46417
46679
  ...maxInteractionTurns !== undefined ? { maxTurns: maxInteractionTurns } : {}
46418
46680
  });
46419
46681
  const turnResult = hopBody ? await hopBody(prompt, { send, input: hopBodyInput }) : await send(prompt);
46682
+ if (turnResult.timedOut)
46683
+ timedOut = true;
46420
46684
  return { result: turnResultToAgentResult(turnResult), bundle: workingBundle, prompt };
46421
46685
  } catch (err) {
46422
46686
  const sessionFailure = err instanceof SessionFailureError ? err.adapterFailure : undefined;
46687
+ timedOut = sessionFailure?.outcome === "fail-timeout";
46423
46688
  const turnError = err instanceof SessionTurnError ? err : undefined;
46424
46689
  const errMessage = err instanceof Error ? err.message : String(err);
46425
46690
  return {
@@ -46441,7 +46706,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46441
46706
  prompt
46442
46707
  };
46443
46708
  } finally {
46444
- if (hopKind.kind !== "stale-retry" && !resolvedRunOptions.keepOpen) {
46709
+ if (hopKind.kind !== "stale-retry" && (!resolvedRunOptions.keepOpen || timedOut)) {
46445
46710
  await sessionManager.closeSession(handle);
46446
46711
  }
46447
46712
  }
@@ -46456,10 +46721,14 @@ var init_build_hop_callback = __esm(() => {
46456
46721
  init_manifest_store();
46457
46722
  init_logger2();
46458
46723
  init_prompts();
46724
+ init_git();
46459
46725
  _buildHopCallbackDeps = {
46460
46726
  rebuildForAgent: (prior, newAgentId, failure, storyId) => new ContextOrchestrator([]).rebuildForAgent(prior, { newAgentId, failure, storyId }),
46461
46727
  writeRebuildManifest,
46462
- createContextToolRuntime
46728
+ createContextToolRuntime,
46729
+ captureGitRef,
46730
+ captureWorkingTreeChanges,
46731
+ timeoutRetry: (input) => timeoutRetry(input)
46463
46732
  };
46464
46733
  });
46465
46734
 
@@ -46664,17 +46933,10 @@ async function callOp(ctx, op, input) {
46664
46933
  effective = { ...turn, output: fileContent };
46665
46934
  }
46666
46935
  }
46667
- if (!effective.output?.trim() && !effective.adapterFailure) {
46668
- return {
46669
- ...effective,
46670
- adapterFailure: {
46671
- outcome: "fail-stale",
46672
- category: "availability",
46673
- retriable: true,
46674
- message: `[${op.name}] agent returned no output`,
46675
- reason: "empty-output"
46676
- }
46677
- };
46936
+ if (!effective.output?.trim()) {
46937
+ const failure = classifyEmptyOutputFailure(effective);
46938
+ if (failure)
46939
+ return { ...effective, adapterFailure: failure };
46678
46940
  }
46679
46941
  return effective;
46680
46942
  };
@@ -52249,11 +52511,142 @@ var init_cli = __esm(() => {
52249
52511
  CLIConfigSchema = exports_external.object({}).passthrough();
52250
52512
  });
52251
52513
 
52514
+ // src/interaction/plugins/telegram-format.ts
52515
+ function buildHeader(request) {
52516
+ const emoji3 = getStageEmoji(request.stage);
52517
+ let text = `${emoji3} *${request.stage.toUpperCase()}*
52518
+ `;
52519
+ text += `*Feature:* ${request.featureName}
52520
+ `;
52521
+ if (request.storyId) {
52522
+ text += `*Story:* ${request.storyId}
52523
+ `;
52524
+ }
52525
+ text += `
52526
+ `;
52527
+ return text;
52528
+ }
52529
+ function buildBody(request) {
52530
+ let text = `${sanitizeMarkdown(request.summary)}
52531
+ `;
52532
+ if (request.detail) {
52533
+ text += `
52534
+ ${sanitizeMarkdown(request.detail)}
52535
+ `;
52536
+ }
52537
+ if (request.options && request.options.length > 0) {
52538
+ text += `
52539
+ *Options:*
52540
+ `;
52541
+ for (const opt of request.options) {
52542
+ const desc = opt.description ? ` - ${sanitizeMarkdown(opt.description)}` : "";
52543
+ text += ` - ${opt.label}${desc}
52544
+ `;
52545
+ }
52546
+ }
52547
+ if (request.timeout) {
52548
+ const timeoutSec = Math.floor(request.timeout / 1000);
52549
+ text += `
52550
+ \u23F1 Timeout: ${timeoutSec}s | Fallback: ${request.fallback}`;
52551
+ }
52552
+ return text;
52553
+ }
52554
+ function sanitizeMarkdown(text) {
52555
+ return text.replace(/\\(?=[_*`\[])/g, "\\\\").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/\[/g, "\\[");
52556
+ }
52557
+ function splitText(text, maxChars) {
52558
+ if (text.length <= maxChars)
52559
+ return [text];
52560
+ const chunks = [];
52561
+ let remaining = text;
52562
+ while (remaining.length > maxChars) {
52563
+ const slice = remaining.slice(0, maxChars);
52564
+ const lastNewline = slice.lastIndexOf(`
52565
+ `);
52566
+ if (lastNewline > maxChars * 0.5) {
52567
+ chunks.push(remaining.slice(0, lastNewline));
52568
+ remaining = remaining.slice(lastNewline + 1);
52569
+ } else {
52570
+ chunks.push(slice);
52571
+ remaining = remaining.slice(maxChars);
52572
+ }
52573
+ }
52574
+ if (remaining.length > 0)
52575
+ chunks.push(remaining);
52576
+ return chunks;
52577
+ }
52578
+ function buildKeyboard(request) {
52579
+ switch (request.type) {
52580
+ case "confirm":
52581
+ return [
52582
+ [
52583
+ { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52584
+ { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52585
+ ],
52586
+ [
52587
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52588
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52589
+ ]
52590
+ ];
52591
+ case "choose": {
52592
+ if (!request.options || request.options.length === 0)
52593
+ return null;
52594
+ const rows = [];
52595
+ for (const opt of request.options) {
52596
+ rows.push([{ text: opt.label, callback_data: `${request.id}:choose:${opt.key}` }]);
52597
+ }
52598
+ rows.push([
52599
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52600
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52601
+ ]);
52602
+ return rows;
52603
+ }
52604
+ case "review":
52605
+ return [
52606
+ [
52607
+ { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52608
+ { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52609
+ ],
52610
+ [
52611
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52612
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52613
+ ]
52614
+ ];
52615
+ default:
52616
+ return null;
52617
+ }
52618
+ }
52619
+ function getStageEmoji(stage) {
52620
+ switch (stage) {
52621
+ case "pre-flight":
52622
+ return "\uD83D\uDE80";
52623
+ case "execution":
52624
+ return "\u2699\uFE0F";
52625
+ case "review":
52626
+ return "\uD83D\uDD0D";
52627
+ case "merge":
52628
+ return "\uD83D\uDD00";
52629
+ case "cost":
52630
+ return "\uD83D\uDCB0";
52631
+ default:
52632
+ return "\uD83D\uDCCC";
52633
+ }
52634
+ }
52635
+ var MAX_MESSAGE_CHARS = 4000;
52636
+
52252
52637
  // src/interaction/plugins/telegram.ts
52253
- var MAX_MESSAGE_CHARS = 4000, CALLBACK_API_TIMEOUT_MS = 4000, TelegramConfigSchema, TelegramInteractionPlugin;
52638
+ function normalizeChatId(raw) {
52639
+ const chatId = raw.trim();
52640
+ return { chatId, unmatchable: !NUMERIC_CHAT_ID.test(chatId) };
52641
+ }
52642
+ var _telegramPluginDeps, CALLBACK_API_TIMEOUT_MS = 4000, NUMERIC_CHAT_ID, TelegramConfigSchema, TelegramInteractionPlugin;
52254
52643
  var init_telegram = __esm(() => {
52255
52644
  init_zod();
52256
52645
  init_logger2();
52646
+ _telegramPluginDeps = {
52647
+ fetch: globalThis.fetch.bind(globalThis)
52648
+ };
52649
+ NUMERIC_CHAT_ID = /^-?\d+$/;
52257
52650
  TelegramConfigSchema = exports_external.object({
52258
52651
  botToken: exports_external.string().optional(),
52259
52652
  chatId: exports_external.string().optional()
@@ -52267,6 +52660,7 @@ var init_telegram = __esm(() => {
52267
52660
  lastUpdateId = 0;
52268
52661
  backoffMs = 1000;
52269
52662
  maxBackoffMs = 30000;
52663
+ static MAX_DRAIN_PAGES = 10;
52270
52664
  static INTERACTIVE_REQUEST_TYPES = new Set([
52271
52665
  "confirm",
52272
52666
  "choose",
@@ -52276,10 +52670,29 @@ var init_telegram = __esm(() => {
52276
52670
  async init(config2) {
52277
52671
  const cfg = TelegramConfigSchema.parse(config2);
52278
52672
  this.botToken = cfg.botToken ?? process.env.NAX_TELEGRAM_TOKEN ?? process.env.TELEGRAM_BOT_TOKEN ?? null;
52279
- this.chatId = cfg.chatId ?? process.env.NAX_TELEGRAM_CHAT_ID ?? null;
52673
+ const rawChatId = cfg.chatId ?? process.env.NAX_TELEGRAM_CHAT_ID ?? null;
52674
+ const normalized = rawChatId === null ? null : normalizeChatId(rawChatId);
52675
+ this.chatId = normalized?.chatId || null;
52280
52676
  if (!this.botToken || !this.chatId) {
52281
52677
  throw new Error("Telegram plugin requires botToken and chatId (env: NAX_TELEGRAM_TOKEN or TELEGRAM_BOT_TOKEN, NAX_TELEGRAM_CHAT_ID)");
52282
52678
  }
52679
+ if (normalized?.unmatchable) {
52680
+ this.logger?.warn("interaction", "Telegram chatId is not numeric \u2014 inbound updates cannot be matched, so interactive prompts will always fall back to their timeout. Use the numeric chat id (see getUpdates or @userinfobot).", { chatId: this.chatId });
52681
+ }
52682
+ }
52683
+ async drainBacklog() {
52684
+ for (let page = 0;page < TelegramInteractionPlugin.MAX_DRAIN_PAGES; page++) {
52685
+ const result = await this.fetchUpdates();
52686
+ if (!result.ok) {
52687
+ this.logger?.warn("interaction", "Telegram backlog drain failed \u2014 stale updates may be misread as a response");
52688
+ return;
52689
+ }
52690
+ if (result.rawCount === 0)
52691
+ return;
52692
+ }
52693
+ this.logger?.warn("interaction", "Telegram backlog drain hit page cap \u2014 stale updates may remain", {
52694
+ pages: TelegramInteractionPlugin.MAX_DRAIN_PAGES
52695
+ });
52283
52696
  }
52284
52697
  async destroy() {
52285
52698
  this.pendingMessages.clear();
@@ -52288,10 +52701,13 @@ var init_telegram = __esm(() => {
52288
52701
  if (!this.botToken || !this.chatId) {
52289
52702
  throw new Error("Telegram plugin not initialized");
52290
52703
  }
52291
- const header = this.buildHeader(request);
52292
- const keyboard = this.buildKeyboard(request);
52293
- const body = this.buildBody(request);
52294
- const chunks = this.splitText(body, MAX_MESSAGE_CHARS - header.length - 10);
52704
+ if (TelegramInteractionPlugin.INTERACTIVE_REQUEST_TYPES.has(request.type)) {
52705
+ await this.drainBacklog();
52706
+ }
52707
+ const header = buildHeader(request);
52708
+ const keyboard = buildKeyboard(request);
52709
+ const body = buildBody(request);
52710
+ const chunks = splitText(body, MAX_MESSAGE_CHARS - header.length - 10);
52295
52711
  try {
52296
52712
  const sentIds = [];
52297
52713
  for (let i = 0;i < chunks.length; i++) {
@@ -52299,7 +52715,7 @@ var init_telegram = __esm(() => {
52299
52715
  const partLabel = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
52300
52716
  const text = `${header}
52301
52717
  ${partLabel}${chunks[i]}`;
52302
- const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
52718
+ const response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
52303
52719
  method: "POST",
52304
52720
  headers: { "Content-Type": "application/json" },
52305
52721
  body: JSON.stringify({
@@ -52320,7 +52736,7 @@ ${partLabel}${chunks[i]}`;
52320
52736
  sentIds.push(data.result.message_id);
52321
52737
  }
52322
52738
  if (TelegramInteractionPlugin.INTERACTIVE_REQUEST_TYPES.has(request.type)) {
52323
- this.pendingMessages.set(request.id, sentIds);
52739
+ this.pendingMessages.set(request.id, { type: request.type, ids: sentIds });
52324
52740
  }
52325
52741
  } catch (err) {
52326
52742
  const msg = err instanceof Error ? err.message : String(err);
@@ -52380,140 +52796,25 @@ ${partLabel}${chunks[i]}`;
52380
52796
  await this.sendTimeoutMessage(requestId);
52381
52797
  this.pendingMessages.delete(requestId);
52382
52798
  }
52383
- buildHeader(request) {
52384
- const emoji3 = this.getStageEmoji(request.stage);
52385
- let text = `${emoji3} *${request.stage.toUpperCase()}*
52386
- `;
52387
- text += `*Feature:* ${request.featureName}
52388
- `;
52389
- if (request.storyId) {
52390
- text += `*Story:* ${request.storyId}
52391
- `;
52392
- }
52393
- text += `
52394
- `;
52395
- return text;
52396
- }
52397
- buildBody(request) {
52398
- let text = `${this.sanitizeMarkdown(request.summary)}
52399
- `;
52400
- if (request.detail) {
52401
- text += `
52402
- ${this.sanitizeMarkdown(request.detail)}
52403
- `;
52404
- }
52405
- if (request.options && request.options.length > 0) {
52406
- text += `
52407
- *Options:*
52408
- `;
52409
- for (const opt of request.options) {
52410
- const desc = opt.description ? ` - ${this.sanitizeMarkdown(opt.description)}` : "";
52411
- text += ` - ${opt.label}${desc}
52412
- `;
52413
- }
52414
- }
52415
- if (request.timeout) {
52416
- const timeoutSec = Math.floor(request.timeout / 1000);
52417
- text += `
52418
- \u23F1 Timeout: ${timeoutSec}s | Fallback: ${request.fallback}`;
52419
- }
52420
- return text;
52421
- }
52422
- sanitizeMarkdown(text) {
52423
- return text.replace(/\\(?=[_*`\[])/g, "\\\\").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/\[/g, "\\[");
52424
- }
52425
- splitText(text, maxChars) {
52426
- if (text.length <= maxChars)
52427
- return [text];
52428
- const chunks = [];
52429
- let remaining = text;
52430
- while (remaining.length > maxChars) {
52431
- const slice = remaining.slice(0, maxChars);
52432
- const lastNewline = slice.lastIndexOf(`
52433
- `);
52434
- if (lastNewline > maxChars * 0.5) {
52435
- chunks.push(remaining.slice(0, lastNewline));
52436
- remaining = remaining.slice(lastNewline + 1);
52437
- } else {
52438
- chunks.push(slice);
52439
- remaining = remaining.slice(maxChars);
52440
- }
52441
- }
52442
- if (remaining.length > 0)
52443
- chunks.push(remaining);
52444
- return chunks;
52445
- }
52446
- buildKeyboard(request) {
52447
- switch (request.type) {
52448
- case "confirm":
52449
- return [
52450
- [
52451
- { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52452
- { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52453
- ],
52454
- [
52455
- { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52456
- { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52457
- ]
52458
- ];
52459
- case "choose": {
52460
- if (!request.options || request.options.length === 0)
52461
- return null;
52462
- const rows = [];
52463
- for (const opt of request.options) {
52464
- rows.push([{ text: opt.label, callback_data: `${request.id}:choose:${opt.key}` }]);
52465
- }
52466
- rows.push([
52467
- { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52468
- { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52469
- ]);
52470
- return rows;
52471
- }
52472
- case "review":
52473
- return [
52474
- [
52475
- { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52476
- { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52477
- ],
52478
- [
52479
- { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52480
- { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52481
- ]
52482
- ];
52483
- default:
52484
- return null;
52485
- }
52486
- }
52487
- getStageEmoji(stage) {
52488
- switch (stage) {
52489
- case "pre-flight":
52490
- return "\uD83D\uDE80";
52491
- case "execution":
52492
- return "\u2699\uFE0F";
52493
- case "review":
52494
- return "\uD83D\uDD0D";
52495
- case "merge":
52496
- return "\uD83D\uDD00";
52497
- case "cost":
52498
- return "\uD83D\uDCB0";
52499
- default:
52500
- return "\uD83D\uDCCC";
52501
- }
52502
- }
52503
52799
  async getUpdates() {
52800
+ const result = await this.fetchUpdates();
52801
+ return result.updates;
52802
+ }
52803
+ async fetchUpdates() {
52504
52804
  if (!this.botToken)
52505
- return [];
52805
+ return { ok: true, updates: [], rawCount: 0 };
52506
52806
  try {
52507
52807
  const controller = new AbortController;
52508
52808
  const timer = setTimeout(() => controller.abort(), 8000);
52509
52809
  let response;
52510
52810
  try {
52511
- response = await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates`, {
52811
+ response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates`, {
52512
52812
  method: "POST",
52513
52813
  headers: { "Content-Type": "application/json" },
52514
52814
  body: JSON.stringify({
52515
52815
  offset: this.lastUpdateId + 1,
52516
- timeout: 1
52816
+ timeout: 1,
52817
+ limit: 100
52517
52818
  }),
52518
52819
  signal: controller.signal
52519
52820
  });
@@ -52528,17 +52829,31 @@ ${this.sanitizeMarkdown(request.detail)}
52528
52829
  if (!data.ok || !data.result) {
52529
52830
  throw new Error("Telegram API returned ok=false or missing result");
52530
52831
  }
52531
- const updates = data.result;
52532
- if (updates.length > 0) {
52533
- this.lastUpdateId = Math.max(...updates.map((u) => u.update_id));
52832
+ const raw = data.result;
52833
+ if (raw.length > 0) {
52834
+ this.lastUpdateId = Math.max(...raw.map((u) => u.update_id));
52835
+ }
52836
+ const updates = raw.filter((u) => this.isFromConfiguredChat(u));
52837
+ if (updates.length !== raw.length) {
52838
+ this.logger?.debug("interaction", "Telegram updates rejected -- not from the configured chat", {
52839
+ rejected: raw.length - updates.length
52840
+ });
52534
52841
  }
52535
52842
  this.backoffMs = 1000;
52536
- return updates;
52843
+ return { ok: true, updates, rawCount: raw.length };
52537
52844
  } catch (err) {
52538
52845
  this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
52539
- return [];
52846
+ this.logger?.debug("interaction", "Telegram getUpdates failed \u2014 retrying with backoff", {
52847
+ error: err instanceof Error ? err.message : String(err),
52848
+ backoffMs: this.backoffMs
52849
+ });
52850
+ return { ok: false, updates: [], rawCount: 0 };
52540
52851
  }
52541
52852
  }
52853
+ isFromConfiguredChat(update) {
52854
+ const chatId = update.callback_query?.message?.chat?.id ?? update.message?.chat?.id;
52855
+ return chatId !== undefined && String(chatId) === this.chatId;
52856
+ }
52542
52857
  parseUpdate(requestId, update) {
52543
52858
  if (update.callback_query) {
52544
52859
  const data = update.callback_query.data;
@@ -52558,11 +52873,11 @@ ${this.sanitizeMarkdown(request.detail)}
52558
52873
  };
52559
52874
  }
52560
52875
  if (update.message?.text) {
52561
- const messageIds = this.pendingMessages.get(requestId);
52562
- if (!messageIds)
52876
+ const pending = this.pendingMessages.get(requestId);
52877
+ if (!pending || pending.type !== "input")
52563
52878
  return null;
52564
52879
  const replyToId = update.message.reply_to_message?.message_id;
52565
- if (replyToId !== undefined && !messageIds.includes(replyToId))
52880
+ if (replyToId !== undefined && !pending.ids.includes(replyToId))
52566
52881
  return null;
52567
52882
  return {
52568
52883
  requestId,
@@ -52581,7 +52896,7 @@ ${this.sanitizeMarkdown(request.detail)}
52581
52896
  const controller = new AbortController;
52582
52897
  const timer = setTimeout(() => controller.abort(), CALLBACK_API_TIMEOUT_MS);
52583
52898
  try {
52584
- await fetch(`https://api.telegram.org/bot${this.botToken}/answerCallbackQuery`, {
52899
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/answerCallbackQuery`, {
52585
52900
  method: "POST",
52586
52901
  headers: { "Content-Type": "application/json" },
52587
52902
  body: JSON.stringify({
@@ -52601,7 +52916,7 @@ ${this.sanitizeMarkdown(request.detail)}
52601
52916
  const controller = new AbortController;
52602
52917
  const timer = setTimeout(() => controller.abort(), CALLBACK_API_TIMEOUT_MS);
52603
52918
  try {
52604
- await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageReplyMarkup`, {
52919
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageReplyMarkup`, {
52605
52920
  method: "POST",
52606
52921
  headers: { "Content-Type": "application/json" },
52607
52922
  body: JSON.stringify({
@@ -52617,14 +52932,14 @@ ${this.sanitizeMarkdown(request.detail)}
52617
52932
  } catch {}
52618
52933
  }
52619
52934
  async sendTimeoutMessage(requestId) {
52620
- const messageIds = this.pendingMessages.get(requestId);
52621
- if (!messageIds || !this.botToken || !this.chatId) {
52935
+ const pending = this.pendingMessages.get(requestId);
52936
+ if (!pending || !this.botToken || !this.chatId) {
52622
52937
  this.pendingMessages.delete(requestId);
52623
52938
  return;
52624
52939
  }
52625
- const lastId = messageIds[messageIds.length - 1];
52940
+ const lastId = pending.ids[pending.ids.length - 1];
52626
52941
  try {
52627
- await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
52942
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
52628
52943
  method: "POST",
52629
52944
  headers: { "Content-Type": "application/json" },
52630
52945
  body: JSON.stringify({
@@ -58708,7 +59023,8 @@ async function refreshReviewInputForDispatch(opName, input) {
58708
59023
  stat: fresh2.stat,
58709
59024
  diff: fresh2.diff,
58710
59025
  excludePatterns: fresh2.excludePatterns,
58711
- storyGitRef: fresh2.effectiveRef ?? semInput.storyGitRef
59026
+ storyGitRef: fresh2.effectiveRef ?? semInput.storyGitRef,
59027
+ resolvedTestPatterns: _refresh.resolvedTestPatterns
58712
59028
  };
58713
59029
  }
58714
59030
  const { _refresh: __, ...advInput } = input;
@@ -59755,6 +60071,7 @@ async function assemblePlanInputsFromCtx(ctx) {
59755
60071
  excludePatterns: prepared.excludePatterns,
59756
60072
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-semantic"),
59757
60073
  priorSemanticIterations: ctx.priorSemanticIterations,
60074
+ resolvedTestPatterns,
59758
60075
  blockingThreshold: ctx.config.review.blockingThreshold,
59759
60076
  _refresh: {
59760
60077
  projectDir: ctx.projectDir,
@@ -59792,6 +60109,7 @@ async function assemblePlanInputsFromCtx(ctx) {
59792
60109
  refExcludePatterns: prepared.refExcludePatterns,
59793
60110
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-adversarial"),
59794
60111
  priorAdversarialIterations: ctx.priorAdversarialIterations,
60112
+ resolvedTestPatterns,
59795
60113
  blockingThreshold: ctx.config.review.blockingThreshold,
59796
60114
  _refresh: {
59797
60115
  projectDir: ctx.projectDir,
@@ -62784,7 +63102,7 @@ var init_forge = __esm(() => {
62784
63102
  function buildTitle(ctx) {
62785
63103
  return `feat: ${ctx.feature}`;
62786
63104
  }
62787
- function formatDuration2(totalMs) {
63105
+ function formatDuration3(totalMs) {
62788
63106
  const clampedMs = Math.max(0, Math.round(totalMs));
62789
63107
  const totalSeconds = Math.floor(clampedMs / MS_PER_SECOND);
62790
63108
  const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
@@ -62800,7 +63118,7 @@ function buildSummaryLines(ctx) {
62800
63118
  "## Run summary",
62801
63119
  `- Feature: ${ctx.feature}`,
62802
63120
  `- Stories: ${passed} / ${failed} / ${skipped}`,
62803
- `- Duration: ${formatDuration2(ctx.totalDurationMs)}`,
63121
+ `- Duration: ${formatDuration3(ctx.totalDurationMs)}`,
62804
63122
  `- PRD: ${ctx.prdPath}`,
62805
63123
  ""
62806
63124
  ];
@@ -62821,7 +63139,7 @@ function buildStoryTable(stories) {
62821
63139
  lines.push("");
62822
63140
  return lines;
62823
63141
  }
62824
- function buildBody(ctx, template) {
63142
+ function buildBody2(ctx, template) {
62825
63143
  const blocks = [];
62826
63144
  blocks.push("> Auto-opened by nax \u2014 review pending. Run nax-finish before merge.");
62827
63145
  blocks.push("");
@@ -62996,7 +63314,7 @@ var init_auto_pr = __esm(() => {
62996
63314
  });
62997
63315
  const prCtx = toPrBodyContext(context);
62998
63316
  const title = buildTitle(prCtx);
62999
- const body = buildBody(prCtx, template);
63317
+ const body = buildBody2(prCtx, template);
63000
63318
  return await _autoPrDeps.openDraft(forge, { title, body, branch: context.branch, draft: cfg.draft }, { run: _autoPrDeps.run, readText: _autoPrDeps.readText }, context.workdir);
63001
63319
  } catch (err) {
63002
63320
  context.logger.warn("Auto-PR execute failed", { error: String(err) });
@@ -68780,7 +69098,7 @@ function extractQuoteTriples(reason) {
68780
69098
  }
68781
69099
  return triples;
68782
69100
  }
68783
- function normalizeWs3(s) {
69101
+ function normalizeWs2(s) {
68784
69102
  return s.replace(/\s+/g, " ").trim();
68785
69103
  }
68786
69104
  async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
@@ -68794,7 +69112,7 @@ async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
68794
69112
  const end = Math.min(lines.length, triple.line + CONTEXT_LINES);
68795
69113
  const window2 = lines.slice(start, end).join(`
68796
69114
  `);
68797
- return normalizeWs3(window2).toLowerCase().includes(normalizeWs3(triple.quote).toLowerCase());
69115
+ return normalizeWs2(window2).toLowerCase().includes(normalizeWs2(triple.quote).toLowerCase());
68798
69116
  }
68799
69117
  async function verifyEscalationQuotes(reason, workdir, storyId, deps = _quoteIntegrityDeps) {
68800
69118
  const triples = extractQuoteTriples(reason);
@@ -69792,6 +70110,12 @@ async function executeParallelBatch(stories, _projectRoot, config2, context, wor
69792
70110
  error: result.error
69793
70111
  });
69794
70112
  }
70113
+ }).catch((error48) => {
70114
+ results.failed.push({ story, error: errorMessage(error48) });
70115
+ logger?.error("parallel", "Story execution threw", {
70116
+ storyId: story.id,
70117
+ error: errorMessage(error48)
70118
+ });
69795
70119
  }).finally(() => {
69796
70120
  executing.delete(executePromise);
69797
70121
  });
@@ -69800,7 +70124,7 @@ async function executeParallelBatch(stories, _projectRoot, config2, context, wor
69800
70124
  await Promise.race(executing);
69801
70125
  }
69802
70126
  }
69803
- await Promise.all(executing);
70127
+ await Promise.allSettled(executing);
69804
70128
  return results;
69805
70129
  }
69806
70130
  var _parallelWorkerDeps;
@@ -102438,7 +102762,6 @@ class DebatePlanStrategy {
102438
102762
  });
102439
102763
  if (debateResult.outcome !== "failed" && debateResult.output) {
102440
102764
  const prd2 = validatePlanOutput(debateResult.output, ctx.options.feature, ctx.branchName);
102441
- warnOnDroppedVerbatimAcs(prd2, ctx.specContent, ctx.options.feature);
102442
102765
  const scoped2 = backfillOutOfScope(prd2, ctx.specContent, ctx.options.feature);
102443
102766
  const withProject2 = { ...scoped2, project: ctx.projectName };
102444
102767
  return _debatePlanDeps.writeOrRecoverPrd(ctx, withProject2);
@@ -103247,9 +103570,6 @@ var FIELD_DESCRIPTIONS = {
103247
103570
  "execution.regressionGate.timeoutSeconds": "Timeout for regression run in seconds",
103248
103571
  "execution.storyIsolation": 'Story isolation mode. "shared" (default): all stories run on the main branch. "worktree": each story runs in an isolated git worktree (.nax-wt/<storyId>/); passed stories merge into main, failed commits never reach main.',
103249
103572
  quality: "Quality gate configuration",
103250
- "quality.requireTypecheck": "Require typecheck to pass",
103251
- "quality.requireLint": "Require lint to pass",
103252
- "quality.requireTests": "Require tests to pass",
103253
103573
  "quality.commands": "Custom quality commands",
103254
103574
  "quality.commands.typecheck": "Custom typecheck command",
103255
103575
  "quality.commands.lint": "Custom lint command",
@@ -105478,7 +105798,7 @@ var DEFAULT_LIMIT = 20;
105478
105798
  var _runsCmdDeps = {
105479
105799
  getRunsDir
105480
105800
  };
105481
- function formatDuration3(ms) {
105801
+ function formatDuration4(ms) {
105482
105802
  if (ms <= 0)
105483
105803
  return "-";
105484
105804
  const minutes = Math.floor(ms / 60000);
@@ -105593,7 +105913,7 @@ async function runsCommand(options = {}) {
105593
105913
  pad3(row.feature, COL.feature),
105594
105914
  pad3(colored, COL.status + (colored.length - visibleLength(colored))),
105595
105915
  pad3(`${row.passed}/${row.total}`, COL.stories),
105596
- pad3(formatDuration3(row.durationMs), COL.duration),
105916
+ pad3(formatDuration4(row.durationMs), COL.duration),
105597
105917
  formatDate(row.registeredAt)
105598
105918
  ].join(" ");
105599
105919
  console.log(line);
@@ -112874,7 +113194,7 @@ function useAgentStreamEvents(bus) {
112874
113194
  if (!bus)
112875
113195
  return;
112876
113196
  const unsubscribe = bus.onAgentStream((event) => {
112877
- const next = new Map(activeCallsRef.current);
113197
+ const next = activeCallsRef.current;
112878
113198
  switch (event.kind) {
112879
113199
  case "agent.call_started": {
112880
113200
  next.set(event.callId, {
@@ -112958,10 +113278,13 @@ function useAgentStreamEvents(bus) {
112958
113278
  default:
112959
113279
  break;
112960
113280
  }
112961
- activeCallsRef.current = next;
112962
113281
  dirtyRef.current = true;
112963
113282
  });
112964
- return unsubscribe;
113283
+ return () => {
113284
+ unsubscribe();
113285
+ activeCallsRef.current.clear();
113286
+ lastTokensRef.current.clear();
113287
+ };
112965
113288
  }, [bus]);
112966
113289
  import_react31.useEffect(() => {
112967
113290
  const interval = setInterval(() => {