@nathapp/nax 0.74.0 → 0.75.0

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 +847 -587
  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,
@@ -21075,8 +21133,23 @@ function buildRunInteractionHandler(options) {
21075
21133
  }
21076
21134
  };
21077
21135
  }
21136
+ function buildTurnResult(input) {
21137
+ const { lastResponse, totalTokenUsage, totalExactCostUsd, turnCount, interactions, timedOut, modelDef } = input;
21138
+ const output = timedOut ? "" : extractOutput(lastResponse);
21139
+ const estimatedCostUsd = totalTokenUsage.inputTokens > 0 || totalTokenUsage.outputTokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, modelDef.model) : 0;
21140
+ return {
21141
+ output,
21142
+ tokenUsage: totalTokenUsage,
21143
+ estimatedCostUsd,
21144
+ exactCostUsd: totalExactCostUsd,
21145
+ internalRoundTrips: turnCount,
21146
+ ...interactions.length > 0 ? { interactions } : {},
21147
+ timedOut
21148
+ };
21149
+ }
21078
21150
  var CONTEXT_TOOL_CALL_PATTERN;
21079
21151
  var init_adapter_output = __esm(() => {
21152
+ init_cost();
21080
21153
  CONTEXT_TOOL_CALL_PATTERN = /<nax_tool_call\s+name="([^"]+)">\s*([\s\S]*?)\s*<\/nax_tool_call>/i;
21081
21154
  });
21082
21155
 
@@ -21435,18 +21508,15 @@ class AcpAgentAdapter {
21435
21508
  if (lastResponse?.stopReason === "error") {
21436
21509
  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
21510
  }
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
- };
21511
+ return buildTurnResult({
21512
+ lastResponse,
21513
+ totalTokenUsage,
21514
+ totalExactCostUsd,
21515
+ turnCount,
21516
+ interactions,
21517
+ timedOut,
21518
+ modelDef
21519
+ });
21450
21520
  }
21451
21521
  async closeSession(handle) {
21452
21522
  const impl = handle;
@@ -21927,6 +21997,108 @@ var init_default_strategy = __esm(() => {
21927
21997
  };
21928
21998
  });
21929
21999
 
22000
+ // src/agents/retry/hop-retry-policy.ts
22001
+ function trySameAgentRetry(result, state, deps) {
22002
+ const { staleRetryAttempts, timeoutRetryAttempts, adapterErrorRetries, currentRunOptions } = state;
22003
+ const { config: config2, requestRunOptions, signal } = deps;
22004
+ const isFailStale = result.adapterFailure?.outcome === "fail-stale";
22005
+ const maxStaleRetries = config2.agent?.idleWatchdog?.maxRetryAttempts ?? 3;
22006
+ if (isFailStale && result.adapterFailure?.retriable && staleRetryAttempts < maxStaleRetries) {
22007
+ const newAttempts = staleRetryAttempts + 1;
22008
+ return {
22009
+ outcome: "stale-retry",
22010
+ staleRetryAttempts: newAttempts,
22011
+ kind: { kind: "stale-retry", attempt: newAttempts },
22012
+ fallbackRecord: {
22013
+ outcome: result.adapterFailure?.outcome ?? "fail-stale",
22014
+ category: result.adapterFailure?.category ?? "availability",
22015
+ costUsd: result.estimatedCostUsd ?? 0,
22016
+ reason: result.adapterFailure?.reason
22017
+ }
22018
+ };
22019
+ }
22020
+ const isFailTimeout = result.adapterFailure?.outcome === "fail-timeout";
22021
+ if (isFailTimeout && result.adapterFailure?.retriable) {
22022
+ const timeoutConfig = extractTimeoutRetryConfig(config2);
22023
+ if (timeoutRetryShouldRetry(timeoutRetryAttempts, timeoutConfig)) {
22024
+ const newAttempts = timeoutRetryAttempts + 1;
22025
+ return {
22026
+ outcome: "timeout-retry",
22027
+ timeoutRetryAttempts: newAttempts,
22028
+ kind: { kind: "timeout-retry", attempt: newAttempts },
22029
+ currentRunOptions: resolveTimeoutRetryOptions(currentRunOptions, timeoutConfig, config2.execution),
22030
+ fallbackRecord: {
22031
+ outcome: result.adapterFailure?.outcome ?? "fail-timeout",
22032
+ category: result.adapterFailure?.category ?? "quality",
22033
+ costUsd: result.estimatedCostUsd ?? 0,
22034
+ reason: result.adapterFailure?.reason
22035
+ }
22036
+ };
22037
+ }
22038
+ }
22039
+ const isFailAdapterError = result.adapterFailure?.outcome === "fail-adapter-error";
22040
+ if (isFailAdapterError && !signal?.aborted) {
22041
+ const runConfig = requestRunOptions.config ?? config2;
22042
+ const maxAdapterRetries = result.adapterFailure?.retriable ? runConfig.execution?.sessionErrorRetryableMaxRetries ?? 3 : runConfig.execution?.sessionErrorMaxRetries ?? 1;
22043
+ if (adapterErrorRetries < maxAdapterRetries) {
22044
+ const newAttempts = adapterErrorRetries + 1;
22045
+ return {
22046
+ outcome: "adapter-error",
22047
+ adapterErrorRetries: newAttempts,
22048
+ kind: { kind: "stale-retry", attempt: newAttempts },
22049
+ fallbackRecord: {
22050
+ outcome: result.adapterFailure?.outcome ?? "fail-adapter-error",
22051
+ category: result.adapterFailure?.category ?? "availability",
22052
+ costUsd: result.estimatedCostUsd ?? 0,
22053
+ retriable: result.adapterFailure?.retriable ?? false,
22054
+ maxAttempts: maxAdapterRetries
22055
+ }
22056
+ };
22057
+ }
22058
+ }
22059
+ return null;
22060
+ }
22061
+ function extractTimeoutRetryConfig(config2) {
22062
+ const fromConfig = config2.agent?.timeoutRetry;
22063
+ return {
22064
+ maxAttempts: fromConfig?.maxAttempts ?? DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG.maxAttempts,
22065
+ budgetMultiplier: fromConfig?.budgetMultiplier ?? DEFAULT_AGENT_TIMEOUT_RETRY_CONFIG.budgetMultiplier
22066
+ };
22067
+ }
22068
+ function resolveTimeoutRetryOptions(prev, timeoutConfig, executionConfig) {
22069
+ const budget = prev.timeoutSeconds ?? executionConfig?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds;
22070
+ return { ...prev, timeoutSeconds: budget * timeoutConfig.budgetMultiplier };
22071
+ }
22072
+ function timeoutRetryShouldRetry(attempts, config2) {
22073
+ return attempts < config2.maxAttempts;
22074
+ }
22075
+ function describeRetryLogEvent(retryDecision, storyId, agent) {
22076
+ const attempt = retryDecision.kind.attempt;
22077
+ if (retryDecision.outcome === "adapter-error") {
22078
+ return {
22079
+ recordFallback: false,
22080
+ level: "warn",
22081
+ message: "fail-adapter-error: same-agent retry with fresh session",
22082
+ fields: {
22083
+ storyId,
22084
+ attempt,
22085
+ maxAttempts: retryDecision.fallbackRecord.maxAttempts,
22086
+ retriable: retryDecision.fallbackRecord.retriable,
22087
+ agent
22088
+ }
22089
+ };
22090
+ }
22091
+ return {
22092
+ recordFallback: true,
22093
+ level: "info",
22094
+ message: retryDecision.outcome === "stale-retry" ? "fail-stale: immediate same-agent retry" : "fail-timeout: same-agent retry with reduced budget",
22095
+ fields: { storyId, attempt, agent, reason: retryDecision.fallbackRecord.reason }
22096
+ };
22097
+ }
22098
+ var init_hop_retry_policy = __esm(() => {
22099
+ init_config();
22100
+ });
22101
+
21930
22102
  // src/agents/manager.ts
21931
22103
  import { EventEmitter } from "events";
21932
22104
 
@@ -22033,7 +22205,7 @@ class AgentManager {
22033
22205
  shouldSwap(failure, hopsSoFar, hasBundle) {
22034
22206
  if (!failure)
22035
22207
  return false;
22036
- if (failure.outcome === "fail-aborted")
22208
+ if (failure.outcome === "fail-aborted" || failure.outcome === "fail-timeout")
22037
22209
  return false;
22038
22210
  const fallback = this._config.agent?.fallback;
22039
22211
  if (!fallback?.enabled)
@@ -22059,8 +22231,10 @@ class AgentManager {
22059
22231
  let hopsSoFar = 0;
22060
22232
  let rateLimitRetry = 0;
22061
22233
  let staleRetryAttempts = 0;
22234
+ let timeoutRetryAttempts = 0;
22062
22235
  let adapterErrorRetries = 0;
22063
22236
  let currentBundle = request.bundle;
22237
+ let currentRunOptions = request.runOptions;
22064
22238
  let currentHopKind = { kind: "primary" };
22065
22239
  let finalPrompt;
22066
22240
  const _opStartMs = Date.now();
@@ -22072,7 +22246,7 @@ class AgentManager {
22072
22246
  let result;
22073
22247
  let updatedBundle = currentBundle;
22074
22248
  if (request.executeHop) {
22075
- const hopOut = await request.executeHop(currentAgent, currentBundle, currentHopKind, request.runOptions);
22249
+ const hopOut = await request.executeHop(currentAgent, currentBundle, currentHopKind, currentRunOptions);
22076
22250
  result = hopOut.result;
22077
22251
  updatedBundle = hopOut.bundle ?? currentBundle;
22078
22252
  finalPrompt = hopOut.prompt ?? finalPrompt;
@@ -22089,7 +22263,7 @@ class AgentManager {
22089
22263
  _finalStatus = "error";
22090
22264
  return { result: unboundResult, fallbacks, finalBundle: currentBundle, finalPrompt };
22091
22265
  }
22092
- const rawHopOut = await this._runHop(currentAgent, request.runOptions);
22266
+ const rawHopOut = await this._runHop(currentAgent, currentRunOptions);
22093
22267
  const hopOut = "result" in rawHopOut && rawHopOut.result != null ? rawHopOut : { result: rawHopOut, prompt: undefined };
22094
22268
  result = hopOut.result;
22095
22269
  finalPrompt = hopOut.prompt ?? finalPrompt;
@@ -22100,51 +22274,49 @@ class AgentManager {
22100
22274
  return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22101
22275
  }
22102
22276
  const bundleForSwapCheck = updatedBundle ?? request.bundle;
22103
- if (request.noFallback) {
22104
- _finalStatus = "error";
22105
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22106
- }
22107
22277
  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++;
22278
+ const retryState = {
22279
+ staleRetryAttempts,
22280
+ timeoutRetryAttempts,
22281
+ adapterErrorRetries,
22282
+ currentRunOptions
22283
+ };
22284
+ const retryDecision = trySameAgentRetry(result, retryState, {
22285
+ config: this._config,
22286
+ requestRunOptions: request.runOptions,
22287
+ signal: request.signal
22288
+ });
22289
+ if (retryDecision) {
22290
+ staleRetryAttempts = retryDecision.outcome === "stale-retry" ? retryDecision.staleRetryAttempts : staleRetryAttempts;
22291
+ timeoutRetryAttempts = retryDecision.outcome === "timeout-retry" ? retryDecision.timeoutRetryAttempts : timeoutRetryAttempts;
22292
+ adapterErrorRetries = retryDecision.outcome === "adapter-error" ? retryDecision.adapterErrorRetries : adapterErrorRetries;
22293
+ currentRunOptions = retryDecision.outcome === "timeout-retry" ? retryDecision.currentRunOptions : currentRunOptions;
22111
22294
  const retryHop = {
22112
22295
  storyId: request.runOptions.storyId,
22113
22296
  priorAgent: currentAgent,
22114
22297
  newAgent: currentAgent,
22115
- hop: staleRetryAttempts,
22116
- outcome: result.adapterFailure?.outcome ?? "fail-stale",
22117
- category: result.adapterFailure?.category ?? "availability",
22298
+ hop: retryDecision.kind.attempt,
22299
+ outcome: retryDecision.fallbackRecord.outcome,
22300
+ category: retryDecision.fallbackRecord.category,
22118
22301
  timestamp: new Date().toISOString(),
22119
- costUsd: result.estimatedCostUsd ?? 0
22302
+ costUsd: retryDecision.fallbackRecord.costUsd
22120
22303
  };
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 };
22304
+ const logEvent = describeRetryLogEvent(retryDecision, request.runOptions.storyId, currentAgent);
22305
+ if (logEvent.recordFallback) {
22306
+ fallbacks.push(retryHop);
22307
+ this._emitter.emit("onSwapAttempt", retryHop);
22308
+ }
22309
+ if (logEvent.level === "warn") {
22310
+ logger?.warn("agent-manager", logEvent.message, logEvent.fields);
22311
+ } else {
22312
+ logger?.info("agent-manager", logEvent.message, logEvent.fields);
22313
+ }
22314
+ currentHopKind = retryDecision.kind;
22130
22315
  continue;
22131
22316
  }
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
- }
22317
+ if (request.noFallback) {
22318
+ _finalStatus = "error";
22319
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
22148
22320
  }
22149
22321
  const hasBundleForSwap = !!bundleForSwapCheck || isFailStale;
22150
22322
  if (!this.shouldSwap(result.adapterFailure, hopsSoFar, hasBundleForSwap)) {
@@ -22558,6 +22730,7 @@ var init_manager = __esm(() => {
22558
22730
  init_bun_deps();
22559
22731
  init_registry();
22560
22732
  init_default_strategy();
22733
+ init_hop_retry_policy();
22561
22734
  _agentManagerDeps = {
22562
22735
  sleep: (ms, signal) => cancellableDelay(ms, signal)
22563
22736
  };
@@ -22835,6 +23008,7 @@ var init_retry = __esm(() => {
22835
23008
  init_compose();
22836
23009
  init_parse_retry();
22837
23010
  init_tiered_parse_retry();
23011
+ init_hop_retry_policy();
22838
23012
  });
22839
23013
 
22840
23014
  // src/agents/index.ts
@@ -25288,7 +25462,7 @@ async function getGitRoot(workdir) {
25288
25462
  return null;
25289
25463
  }
25290
25464
  }
25291
- async function gitWithTimeout(args, workdir) {
25465
+ async function gitWithTimeout(args, workdir, timeoutMs = GIT_TIMEOUT_MS) {
25292
25466
  const proc = _gitDeps.spawn(["git", ...args], {
25293
25467
  cwd: workdir,
25294
25468
  stdout: "pipe",
@@ -25300,7 +25474,7 @@ async function gitWithTimeout(args, workdir) {
25300
25474
  try {
25301
25475
  proc.kill("SIGKILL");
25302
25476
  } catch {}
25303
- }, GIT_TIMEOUT_MS);
25477
+ }, timeoutMs);
25304
25478
  const exitCode = await proc.exited;
25305
25479
  clearTimeout(timerId);
25306
25480
  if (timedOut) {
@@ -25432,6 +25606,38 @@ async function captureOutputFiles(workdir, baseRef, scopePrefix) {
25432
25606
  return [];
25433
25607
  }
25434
25608
  }
25609
+ async function captureWorkingTreeChanges(workdir, baseRef, scopePrefix) {
25610
+ if (!baseRef)
25611
+ return [];
25612
+ const runDiff = async (args) => {
25613
+ const fullArgs = scopePrefix ? [...args, "--", `${scopePrefix}/`] : args;
25614
+ const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, TIMEOUT_RETRY_GIT_TIMEOUT_MS);
25615
+ if (exitCode !== 0)
25616
+ return [];
25617
+ return stdout.trim().split(`
25618
+ `).filter(Boolean);
25619
+ };
25620
+ try {
25621
+ const [committed, uncommitted, untracked] = await Promise.all([
25622
+ runDiff(["diff", "--name-only", `${baseRef}..HEAD`]),
25623
+ runDiff(["diff", "--name-only", "HEAD"]),
25624
+ runDiff(["ls-files", "--others", "--exclude-standard"])
25625
+ ]);
25626
+ const seen = new Set;
25627
+ const merged = [];
25628
+ for (const list of [committed, uncommitted, untracked]) {
25629
+ for (const file3 of list) {
25630
+ if (!seen.has(file3)) {
25631
+ seen.add(file3);
25632
+ merged.push(file3);
25633
+ }
25634
+ }
25635
+ }
25636
+ return merged;
25637
+ } catch {
25638
+ return [];
25639
+ }
25640
+ }
25435
25641
  async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25436
25642
  if (!baseRef)
25437
25643
  return "";
@@ -25454,7 +25660,7 @@ async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25454
25660
  return "";
25455
25661
  }
25456
25662
  }
25457
- var _gitDeps, GIT_TIMEOUT_MS = 1e4;
25663
+ var _gitDeps, GIT_TIMEOUT_MS = 1e4, TIMEOUT_RETRY_GIT_TIMEOUT_MS = 3000;
25458
25664
  var init_git = __esm(() => {
25459
25665
  init_logger2();
25460
25666
  init_bun_deps();
@@ -28697,81 +28903,12 @@ function generateHumanHaltSummary(prd) {
28697
28903
  `);
28698
28904
  }
28699
28905
 
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
28906
  // src/prd/spec-drift.ts
28770
- function leadingTagGroup2(ac) {
28771
- return ac.match(LEADING_TAG_GROUP2)?.[1] ?? null;
28907
+ function leadingTagGroup(ac) {
28908
+ return ac.match(LEADING_TAG_GROUP)?.[1] ?? null;
28772
28909
  }
28773
28910
  function hasDeprecatedTag(ac) {
28774
- const tags = leadingTagGroup2(ac);
28911
+ const tags = leadingTagGroup(ac);
28775
28912
  return tags !== null && DEPRECATED_TAG.test(tags);
28776
28913
  }
28777
28914
  function hasShellPattern(ac) {
@@ -28791,9 +28928,9 @@ function findSpecDriftViolations(prd) {
28791
28928
  }
28792
28929
  return violations;
28793
28930
  }
28794
- var LEADING_TAG_GROUP2, DEPRECATED_TAG, SHELL_PIPE, SHELL_WC, SHELL_GREP_FLAG;
28931
+ var LEADING_TAG_GROUP, DEPRECATED_TAG, SHELL_PIPE, SHELL_WC, SHELL_GREP_FLAG;
28795
28932
  var init_spec_drift = __esm(() => {
28796
- LEADING_TAG_GROUP2 = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
28933
+ LEADING_TAG_GROUP = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
28797
28934
  DEPRECATED_TAG = /\[(grep|file|verbatim)\]/i;
28798
28935
  SHELL_PIPE = /`[^`]*\b(grep|find|wc|awk|sed|sort|head|tail|xargs|cut|uniq)\b[^`]*\|[^`]*`/i;
28799
28936
  SHELL_WC = /`[^`]*\bwc\b[^`]*`/;
@@ -29221,9 +29358,7 @@ __export(exports_prd, {
29221
29358
  getContextFiles: () => getContextFiles,
29222
29359
  generateHumanHaltSummary: () => generateHumanHaltSummary,
29223
29360
  findSpecDriftViolations: () => findSpecDriftViolations,
29224
- findMissingVerbatimAcs: () => findMissingVerbatimAcs,
29225
29361
  findMissingOutOfScope: () => findMissingOutOfScope,
29226
- extractVerbatimAcs: () => extractVerbatimAcs,
29227
29362
  extractSpecOutOfScope: () => extractSpecOutOfScope,
29228
29363
  deriveNextStoryId: () => deriveNextStoryId,
29229
29364
  countStories: () => countStories,
@@ -29421,7 +29556,6 @@ var init_prd = __esm(() => {
29421
29556
  init_errors();
29422
29557
  init_json_file();
29423
29558
  init_out_of_scope();
29424
- init_verbatim_fidelity();
29425
29559
  init_spec_drift();
29426
29560
  init_out_of_scope();
29427
29561
  init_inject();
@@ -29878,6 +30012,13 @@ var init_plugin_loader = __esm(() => {
29878
30012
  });
29879
30013
 
29880
30014
  // src/context/engine/providers/plugin-cache.ts
30015
+ function withDisposeDeadline(p, deadlineMs) {
30016
+ let handle;
30017
+ const deadline = new Promise((resolve8) => {
30018
+ handle = setTimeout(resolve8, deadlineMs);
30019
+ });
30020
+ return Promise.race([p, deadline]).finally(() => clearTimeout(handle));
30021
+ }
29881
30022
  function stableCacheKey(configs, workdir) {
29882
30023
  const sorted = [...configs].sort((a, b) => a.module.localeCompare(b.module));
29883
30024
  return `${workdir}:${JSON.stringify(sorted)}`;
@@ -29908,31 +30049,32 @@ class PluginProviderCache {
29908
30049
  return;
29909
30050
  this.disposed = true;
29910
30051
  const logger = getLogger();
30052
+ const disposals = [];
29911
30053
  for (const providers of this.cache.values()) {
29912
30054
  for (const provider of providers) {
29913
30055
  const initialisable = provider;
29914
30056
  if (typeof initialisable.dispose !== "function")
29915
30057
  continue;
29916
- try {
29917
- await Promise.race([initialisable.dispose(), Bun.sleep(DISPOSE_TIMEOUT_MS)]);
29918
- } catch (err) {
30058
+ disposals.push(withDisposeDeadline(Promise.resolve().then(() => initialisable.dispose?.()).then(() => {}), _pluginCacheDeps.disposeTimeoutMs).catch((err) => {
29919
30059
  logger.warn("context-engine", "Plugin provider dispose() threw \u2014 continuing teardown", {
29920
30060
  providerId: provider.id,
29921
30061
  error: err instanceof Error ? err.message : String(err)
29922
30062
  });
29923
- }
30063
+ }));
29924
30064
  }
29925
30065
  }
30066
+ await Promise.all(disposals);
29926
30067
  this.cache.clear();
29927
30068
  }
29928
30069
  }
29929
- var _pluginCacheDeps, DISPOSE_TIMEOUT_MS = 5000;
30070
+ var DISPOSE_TIMEOUT_MS = 5000, _pluginCacheDeps;
29930
30071
  var init_plugin_cache = __esm(() => {
29931
30072
  init_errors();
29932
30073
  init_logger2();
29933
30074
  init_plugin_loader();
29934
30075
  _pluginCacheDeps = {
29935
- loadProviders: loadPluginProviders
30076
+ loadProviders: loadPluginProviders,
30077
+ disposeTimeoutMs: DISPOSE_TIMEOUT_MS
29936
30078
  };
29937
30079
  });
29938
30080
 
@@ -33450,7 +33592,7 @@ var init_acceptance_builder = __esm(() => {
33450
33592
  });
33451
33593
 
33452
33594
  // src/review/ac-quote-validator.ts
33453
- function normalizeWs2(s) {
33595
+ function normalizeWs(s) {
33454
33596
  return s.replace(/\s+/g, " ").trim();
33455
33597
  }
33456
33598
  function stripMarkdownInline(s) {
@@ -33485,8 +33627,8 @@ function validateAcQuote(finding, acceptanceCriteria) {
33485
33627
  if (typeof acIndex !== "number" || acIndex < 1 || acIndex > acceptanceCriteria.length) {
33486
33628
  return { valid: false, code: "ac_index_out_of_range" };
33487
33629
  }
33488
- const acText = normalizeWs2(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
33489
- const normalizedQuote = normalizeWs2(stripMarkdownInline(acQuote));
33630
+ const acText = normalizeWs(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
33631
+ const normalizedQuote = normalizeWs(stripMarkdownInline(acQuote));
33490
33632
  if (!acText.toLowerCase().includes(normalizedQuote.toLowerCase())) {
33491
33633
  return { valid: false, code: "ac_quote_not_substring" };
33492
33634
  }
@@ -33560,8 +33702,8 @@ function validateScopeQuote(finding, outOfScope) {
33560
33702
  if (typeof scopeIndex !== "number" || scopeIndex < 1 || scopeIndex > outOfScope.length) {
33561
33703
  return { valid: false, code: "scope_index_out_of_range" };
33562
33704
  }
33563
- const entry = normalizeWs2(stripMarkdownInline(outOfScope[scopeIndex - 1]));
33564
- const quote = normalizeWs2(stripMarkdownInline(scopeQuote));
33705
+ const entry = normalizeWs(stripMarkdownInline(outOfScope[scopeIndex - 1]));
33706
+ const quote = normalizeWs(stripMarkdownInline(scopeQuote));
33565
33707
  if (!entry.toLowerCase().includes(quote.toLowerCase())) {
33566
33708
  return { valid: false, code: "scope_quote_not_substring" };
33567
33709
  }
@@ -33780,13 +33922,31 @@ var init_adapters = __esm(() => {
33780
33922
  init_typecheck();
33781
33923
  });
33782
33924
 
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 });
33925
+ // src/operations/turn-failure-classification.ts
33926
+ function classifyEmptyOutputFailure(turn) {
33927
+ if (turn.adapterFailure)
33928
+ return turn.adapterFailure;
33929
+ if (turn.output && turn.output.trim().length > 0)
33930
+ return null;
33931
+ if (turn.timedOut) {
33932
+ return {
33933
+ category: "quality",
33934
+ outcome: "fail-timeout",
33935
+ retriable: true,
33936
+ message: "[callOp] agent timed out before producing output",
33937
+ reason: "wall-clock-timeout"
33938
+ };
33788
33939
  }
33940
+ return {
33941
+ category: "availability",
33942
+ outcome: "fail-stale",
33943
+ retriable: true,
33944
+ message: "[callOp] agent returned no output",
33945
+ reason: "empty-output"
33946
+ };
33789
33947
  }
33948
+
33949
+ // src/operations/plan-fidelity.ts
33790
33950
  function backfillOutOfScope(prd, specContent, featureName) {
33791
33951
  const missing = findMissingOutOfScope(specContent, prd);
33792
33952
  if (missing.length === 0)
@@ -33808,7 +33968,7 @@ function warnOnSpecDrift(prd, featureName) {
33808
33968
  });
33809
33969
  }
33810
33970
  }
33811
- var init_verbatim_warn = __esm(() => {
33971
+ var init_plan_fidelity = __esm(() => {
33812
33972
  init_logger2();
33813
33973
  init_prd();
33814
33974
  });
@@ -33820,7 +33980,7 @@ var init_plan = __esm(() => {
33820
33980
  init_config();
33821
33981
  init_schema2();
33822
33982
  init_prompts();
33823
- init_verbatim_warn();
33983
+ init_plan_fidelity();
33824
33984
  planInteractiveOp = {
33825
33985
  kind: "run",
33826
33986
  name: "plan-interactive",
@@ -33865,7 +34025,6 @@ ${outputFormat}`, overridable: false }
33865
34025
  verify: async (parsed, input, _ctx) => {
33866
34026
  if (!parsed.userStories || parsed.userStories.length === 0)
33867
34027
  return null;
33868
- warnOnDroppedVerbatimAcs(parsed, input.specContent, input.featureName);
33869
34028
  return backfillOutOfScope(parsed, input.specContent, input.featureName);
33870
34029
  },
33871
34030
  recover: async (input, ctx) => {
@@ -33951,20 +34110,6 @@ function validateRefinedPrd(prd) {
33951
34110
  validateRefinedStory(story);
33952
34111
  return prd;
33953
34112
  }
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
34113
  async function readSpecDriftViolations(input) {
33969
34114
  const content = await _planRefineDeps.readFile(input.outputPath);
33970
34115
  if (!content)
@@ -34053,17 +34198,6 @@ async function normalizeCreatedContextFiles(prd, workdir, fileExists) {
34053
34198
  return prd;
34054
34199
  return { ...prd, userStories: results.map((r) => r.story) };
34055
34200
  }
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
34201
  async function readMissingOutOfScope(input) {
34068
34202
  const content = await _planRefineDeps.readFile(input.outputPath);
34069
34203
  if (!content)
@@ -34110,8 +34244,8 @@ var init_plan_refine = __esm(() => {
34110
34244
  init_prd();
34111
34245
  init_schema2();
34112
34246
  init_prompts();
34247
+ init_plan_fidelity();
34113
34248
  init_self_heal();
34114
- init_verbatim_warn();
34115
34249
  _planRefineDeps = {
34116
34250
  readFile: async (path3) => {
34117
34251
  try {
@@ -34195,7 +34329,6 @@ ${outputFormat}`,
34195
34329
  estimatedCostUsd: (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0)
34196
34330
  };
34197
34331
  const steps = [
34198
- verbatimSelfHealStep(builder),
34199
34332
  outOfScopeSelfHealStep(builder),
34200
34333
  ...specGuard ? [specDriftSelfHealStep(builder)] : []
34201
34334
  ];
@@ -34206,7 +34339,6 @@ ${outputFormat}`,
34206
34339
  },
34207
34340
  verify: async (parsed, input, ctx) => {
34208
34341
  const validated = validateRefinedPrd(parsed);
34209
- warnOnDroppedVerbatimAcs(validated, input.specContent, input.featureName);
34210
34342
  if (ctx.config.plan.specGuard) {
34211
34343
  warnOnSpecDrift(validated, input.featureName);
34212
34344
  }
@@ -35638,6 +35770,11 @@ function categoryToFixTarget(category) {
35638
35770
  return "source";
35639
35771
  return category != null && BLOCKING_CATEGORIES.has(category) ? "source" : "test";
35640
35772
  }
35773
+ function resolveFixTarget({ base, file: file3, isTestFile: isTestFile3 }) {
35774
+ if (file3 && isTestFile3?.(file3))
35775
+ return "test";
35776
+ return base;
35777
+ }
35641
35778
  var init_category_fix_target = __esm(() => {
35642
35779
  init_ac_structural_counterfactual();
35643
35780
  });
@@ -35665,7 +35802,7 @@ function normalizeSeverity(sev) {
35665
35802
  return sev;
35666
35803
  return "info";
35667
35804
  }
35668
- function toAdversarialReviewFindings(findings) {
35805
+ function toAdversarialReviewFindings(findings, opts = {}) {
35669
35806
  return findings.map((f) => {
35670
35807
  const metaExtras = {};
35671
35808
  if (f.acQuote)
@@ -35686,7 +35823,7 @@ function toAdversarialReviewFindings(findings) {
35686
35823
  line: f.line,
35687
35824
  message: f.issue,
35688
35825
  suggestion: f.suggestion,
35689
- fixTarget: categoryToFixTarget(f.category),
35826
+ fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
35690
35827
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35691
35828
  };
35692
35829
  });
@@ -35750,7 +35887,7 @@ function downgradeToUnverifiable(finding) {
35750
35887
  severity: "unverifiable"
35751
35888
  };
35752
35889
  }
35753
- function llmFindingToFinding(f) {
35890
+ function llmFindingToFinding(f, opts = {}) {
35754
35891
  const metaExtras = {};
35755
35892
  if (f.verifiedBy)
35756
35893
  metaExtras.verifiedBy = f.verifiedBy;
@@ -35766,15 +35903,16 @@ function llmFindingToFinding(f) {
35766
35903
  line: f.line,
35767
35904
  message: f.issue,
35768
35905
  suggestion: f.suggestion ?? undefined,
35769
- fixTarget: "source",
35906
+ fixTarget: resolveFixTarget({ base: "source", file: f.file, isTestFile: opts.isTestFile }),
35770
35907
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35771
35908
  };
35772
35909
  }
35773
- function toReviewFindings(findings) {
35774
- return findings.map(llmFindingToFinding);
35910
+ function toReviewFindings(findings, opts = {}) {
35911
+ return findings.map((f) => llmFindingToFinding(f, opts));
35775
35912
  }
35776
35913
  var UNVERIFIED_FINDING_PATTERNS;
35777
35914
  var init_semantic_helpers = __esm(() => {
35915
+ init_category_fix_target();
35778
35916
  init_severity();
35779
35917
  UNVERIFIED_FINDING_PATTERNS = [
35780
35918
  "cannot verify",
@@ -35918,9 +36056,9 @@ function parseRequoteResponse(output) {
35918
36056
  const parsed = tryParseLLMJson(output);
35919
36057
  if (!isRecord(parsed))
35920
36058
  return null;
35921
- const canonical3 = extractCanonical(parsed);
35922
- if (canonical3)
35923
- return canonical3;
36059
+ const canonical2 = extractCanonical(parsed);
36060
+ if (canonical2)
36061
+ return canonical2;
35924
36062
  const findings = parsed.findings;
35925
36063
  if (!Array.isArray(findings) || findings.length !== 1)
35926
36064
  return null;
@@ -35967,6 +36105,10 @@ function withRepromptMarker(output, info) {
35967
36105
  return output;
35968
36106
  return JSON.stringify({ ...parsed, _repromptInfo: info });
35969
36107
  }
36108
+ function semanticTestFileMatch(input) {
36109
+ const patterns = input.resolvedTestPatterns?.regex ?? [];
36110
+ return (file3) => patterns.some((re) => re.test(file3));
36111
+ }
35970
36112
  function extractRepromptInfo(raw) {
35971
36113
  if (!raw || typeof raw !== "object")
35972
36114
  return;
@@ -36258,7 +36400,7 @@ var init_semantic_review = __esm(() => {
36258
36400
  ...parsed,
36259
36401
  passed,
36260
36402
  findings: accepted,
36261
- normalizedFindings: toReviewFindings(blocking),
36403
+ normalizedFindings: toReviewFindings(blocking, { isTestFile: semanticTestFileMatch(input) }),
36262
36404
  acDropped: dropped
36263
36405
  };
36264
36406
  }
@@ -36674,10 +36816,10 @@ var init_adversarial_review = __esm(() => {
36674
36816
  ...parsed,
36675
36817
  passed,
36676
36818
  findings: accepted,
36677
- normalizedFindings: toAdversarialReviewFindings(blocking),
36819
+ normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
36678
36820
  advisoryFindings: [
36679
- ...toAdversarialReviewFindings(advisory),
36680
- ...tagCoverageGap(toAdversarialReviewFindings(demoted))
36821
+ ...toAdversarialReviewFindings(advisory, { isTestFile: testFileMatch }),
36822
+ ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
36681
36823
  ],
36682
36824
  acDropped: dropped
36683
36825
  };
@@ -39063,14 +39205,9 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
39063
39205
  const pid = proc.pid;
39064
39206
  killProcessGroup(pid, "SIGTERM");
39065
39207
  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
- ]);
39208
+ await raceWithDeadline(proc.exited.then(() => {
39209
+ exitedDuringGrace = true;
39210
+ }).catch(() => {}), gracePeriodMs);
39074
39211
  if (!exitedDuringGrace) {
39075
39212
  killProcessGroup(pid, "SIGKILL");
39076
39213
  }
@@ -39946,7 +40083,7 @@ async function runQualityCommand(opts) {
39946
40083
  let sigkillTimer;
39947
40084
  proc.exited.then(() => {
39948
40085
  exitedBeforeSigkill = true;
39949
- });
40086
+ }).catch(() => {});
39950
40087
  const killTimer = setTimeout(() => {
39951
40088
  timedOut = true;
39952
40089
  killProcessGroup(proc.pid, "SIGTERM");
@@ -41378,7 +41515,7 @@ var init_operations = __esm(() => {
41378
41515
  init_plan();
41379
41516
  init_plan_refine();
41380
41517
  init_self_heal();
41381
- init_verbatim_warn();
41518
+ init_plan_fidelity();
41382
41519
  init_decompose2();
41383
41520
  init_build_hop_callback();
41384
41521
  init_classify_route();
@@ -41495,11 +41632,15 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41495
41632
  const storyId = ctx.storyId;
41496
41633
  const packageDir = ctx.packageDir;
41497
41634
  let totalCostUsd = 0;
41635
+ const spentStrategies = new Set;
41636
+ let unresolvedDetail;
41637
+ const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
41498
41638
  for (;; ) {
41499
41639
  if (cycle.findings.length === 0 && cycle.verdict === undefined) {
41500
41640
  return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
41501
41641
  }
41502
- const active = selectActiveStrategies(cycle.strategies, cycle.findings, cycle.verdict);
41642
+ const selectable = cycle.strategies.filter((s) => !spentStrategies.has(s.name));
41643
+ const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
41503
41644
  if (active.length === 0) {
41504
41645
  const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
41505
41646
  logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
@@ -41508,14 +41649,15 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41508
41649
  cycleName,
41509
41650
  reason: "no-strategy",
41510
41651
  findingsCount: cycle.findings.length,
41511
- orphanSources
41652
+ orphanSources,
41653
+ ...spentStrategies.size > 0 ? { retiredStrategies: [...spentStrategies] } : {}
41512
41654
  });
41513
- return {
41655
+ return finish({
41514
41656
  iterations: cycle.iterations,
41515
41657
  finalFindings: cycle.findings,
41516
41658
  exitReason: "no-strategy",
41517
41659
  costUsd: totalCostUsd
41518
- };
41660
+ });
41519
41661
  }
41520
41662
  const uncappedActive = active.filter((s) => countStrategyAttempts(cycle.iterations, s.name) < s.maxAttempts);
41521
41663
  if (uncappedActive.length === 0) {
@@ -41527,13 +41669,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41527
41669
  reason: "max-attempts-per-strategy",
41528
41670
  exhaustedStrategy: exhaustedStrategy?.name
41529
41671
  });
41530
- return {
41672
+ return finish({
41531
41673
  iterations: cycle.iterations,
41532
41674
  finalFindings: cycle.findings,
41533
41675
  exitReason: "max-attempts-per-strategy",
41534
41676
  exhaustedStrategy: exhaustedStrategy?.name,
41535
41677
  costUsd: totalCostUsd
41536
- };
41678
+ });
41537
41679
  }
41538
41680
  const totalAttempts = countTotalAttempts(cycle.iterations);
41539
41681
  if (totalAttempts >= cycle.config.maxAttemptsTotal) {
@@ -41545,12 +41687,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41545
41687
  totalAttempts,
41546
41688
  maxAttemptsTotal: cycle.config.maxAttemptsTotal
41547
41689
  });
41548
- return {
41690
+ return finish({
41549
41691
  iterations: cycle.iterations,
41550
41692
  finalFindings: cycle.findings,
41551
41693
  exitReason: "max-attempts-total",
41552
41694
  costUsd: totalCostUsd
41553
- };
41695
+ });
41554
41696
  }
41555
41697
  for (const strategy of uncappedActive) {
41556
41698
  const bailReason = strategy.bailWhen?.(cycle.iterations) ?? null;
@@ -41563,13 +41705,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41563
41705
  strategyName: strategy.name,
41564
41706
  bailDetail: bailReason
41565
41707
  });
41566
- return {
41708
+ return finish({
41567
41709
  iterations: cycle.iterations,
41568
41710
  finalFindings: cycle.findings,
41569
41711
  exitReason: "bail-when",
41570
41712
  bailDetail: bailReason,
41571
41713
  costUsd: totalCostUsd
41572
- };
41714
+ });
41573
41715
  }
41574
41716
  }
41575
41717
  const group = selectExecutionGroup(uncappedActive);
@@ -41590,33 +41732,49 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41590
41732
  costUsd: extracted.costUsd
41591
41733
  });
41592
41734
  }
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", {
41735
+ const unresolvedFas = fixesApplied.filter((fa) => fa.unresolved);
41736
+ if (unresolvedFas.length > 0) {
41737
+ const firstUnresolved = unresolvedFas[0];
41738
+ unresolvedDetail = firstUnresolved.unresolved;
41739
+ for (const fa of unresolvedFas)
41740
+ spentStrategies.add(fa.strategyName);
41741
+ const allGaveUp = unresolvedFas.length === fixesApplied.length;
41742
+ if (allGaveUp) {
41743
+ const finishedAt2 = now();
41744
+ cycle.iterations.push({
41745
+ iterationNum: cycle.iterations.length + 1,
41746
+ findingsBefore,
41747
+ fixesApplied,
41748
+ findingsAfter: cycle.findings,
41749
+ outcome: "unchanged",
41750
+ startedAt,
41751
+ finishedAt: finishedAt2
41752
+ });
41753
+ totalCostUsd += fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41754
+ logger?.info("findings.cycle", "cycle exited \u2014 agent gave up", {
41755
+ storyId,
41756
+ packageDir,
41757
+ cycleName,
41758
+ reason: "agent-gave-up",
41759
+ strategyName: firstUnresolved.strategyName,
41760
+ unresolvedDetail: firstUnresolved.unresolved
41761
+ });
41762
+ return finish({
41763
+ iterations: cycle.iterations,
41764
+ finalFindings: cycle.findings,
41765
+ exitReason: "agent-gave-up",
41766
+ unresolvedDetail: firstUnresolved.unresolved,
41767
+ costUsd: totalCostUsd
41768
+ });
41769
+ }
41770
+ logger?.info("findings.cycle", "strategy gave up \u2014 retired, continuing with co-run siblings", {
41606
41771
  storyId,
41607
41772
  packageDir,
41608
41773
  cycleName,
41609
- reason: "agent-gave-up",
41610
- strategyName: unresolvedFa.strategyName,
41611
- unresolvedDetail: unresolvedFa.unresolved
41774
+ strategyName: firstUnresolved.strategyName,
41775
+ unresolvedDetail: firstUnresolved.unresolved,
41776
+ ranWithoutGivingUp: fixesApplied.filter((fa) => !fa.unresolved).map((fa) => fa.strategyName)
41612
41777
  });
41613
- return {
41614
- iterations: cycle.iterations,
41615
- finalFindings: cycle.findings,
41616
- exitReason: "agent-gave-up",
41617
- unresolvedDetail: unresolvedFa.unresolved,
41618
- costUsd: totalCostUsd
41619
- };
41620
41778
  }
41621
41779
  const allExhausted = group.every((s) => {
41622
41780
  const prior = countStrategyAttempts(cycle.iterations, s.name);
@@ -41624,6 +41782,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41624
41782
  return prior + current >= s.maxAttempts;
41625
41783
  });
41626
41784
  if (allExhausted) {
41785
+ totalCostUsd += fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41627
41786
  let liteFindingsAfter;
41628
41787
  let liteShortCircuited = false;
41629
41788
  try {
@@ -41648,13 +41807,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41648
41807
  cycleName,
41649
41808
  error: errorMessage(err)
41650
41809
  });
41651
- return {
41810
+ return finish({
41652
41811
  iterations: cycle.iterations,
41653
41812
  finalFindings: cycle.findings,
41654
41813
  exitReason: "max-attempts-per-strategy",
41655
41814
  exhaustedStrategy: group[0]?.name,
41656
41815
  costUsd: totalCostUsd
41657
- };
41816
+ });
41658
41817
  }
41659
41818
  const outcome2 = classifyOutcome(findingsBefore, liteFindingsAfter);
41660
41819
  const finishedAt2 = now();
@@ -41675,18 +41834,16 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41675
41834
  cycleName,
41676
41835
  reason: "resolved"
41677
41836
  });
41678
- return {
41837
+ return finish({
41679
41838
  iterations: cycle.iterations,
41680
41839
  finalFindings: [],
41681
41840
  exitReason: "resolved",
41682
41841
  costUsd: totalCostUsd
41683
- };
41842
+ });
41684
41843
  }
41685
41844
  if (liteShortCircuited) {
41686
41845
  const companions = uncappedActive.filter((s) => !group.includes(s));
41687
41846
  if (companions.length > 0) {
41688
- const iterCostUsd = fixesApplied.reduce((sum, fa) => sum + (fa.costUsd ?? 0), 0);
41689
- totalCostUsd += iterCostUsd;
41690
41847
  logger?.info("findings.cycle", "exclusive strategy exhausted \u2014 continuing to companion strategies", {
41691
41848
  storyId,
41692
41849
  packageDir,
@@ -41703,12 +41860,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41703
41860
  reason: "validate-short-circuit",
41704
41861
  liteFindingsAfterCount: liteFindingsAfter.length
41705
41862
  });
41706
- return {
41863
+ return finish({
41707
41864
  iterations: cycle.iterations,
41708
41865
  finalFindings: liteFindingsAfter,
41709
41866
  exitReason: "validate-short-circuit",
41710
41867
  costUsd: totalCostUsd
41711
- };
41868
+ });
41712
41869
  }
41713
41870
  logger?.info("findings.cycle", "cycle exited \u2014 strategy attempt cap reached (lite validate)", {
41714
41871
  storyId,
@@ -41718,13 +41875,13 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41718
41875
  exhaustedStrategy: group[0]?.name,
41719
41876
  liteFindingsAfterCount: liteFindingsAfter.length
41720
41877
  });
41721
- return {
41878
+ return finish({
41722
41879
  iterations: cycle.iterations,
41723
41880
  finalFindings: liteFindingsAfter,
41724
41881
  exitReason: "max-attempts-per-strategy",
41725
41882
  exhaustedStrategy: group[0]?.name,
41726
41883
  costUsd: totalCostUsd
41727
- };
41884
+ });
41728
41885
  }
41729
41886
  let findingsAfter;
41730
41887
  let validatorAttempt = 0;
@@ -41742,12 +41899,12 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41742
41899
  reason: "validator-error",
41743
41900
  error: errorMessage(err)
41744
41901
  });
41745
- return {
41902
+ return finish({
41746
41903
  iterations: cycle.iterations,
41747
41904
  finalFindings: cycle.findings,
41748
41905
  exitReason: "validator-error",
41749
41906
  costUsd: totalCostUsd
41750
- };
41907
+ });
41751
41908
  }
41752
41909
  logger?.warn("findings.cycle", "validator retry", {
41753
41910
  storyId,
@@ -42084,11 +42241,9 @@ function buildMeta(f, originalSeverity) {
42084
42241
  function findingCategory(f) {
42085
42242
  return "category" in f && f.category ? f.category : undefined;
42086
42243
  }
42087
- function deriveFixTargetForReviewFinding(category, source) {
42088
- if (source === "semantic-review" || source === "semantic-debate-review") {
42089
- return "source";
42090
- }
42091
- return categoryToFixTarget(category);
42244
+ function deriveFixTargetForReviewFinding(category, source, file3, isTestFile3) {
42245
+ const base = source === "semantic-review" || source === "semantic-debate-review" ? "source" : categoryToFixTarget(category);
42246
+ return resolveFixTarget({ base, file: file3, isTestFile: isTestFile3 });
42092
42247
  }
42093
42248
  function llmFindingToReviewFinding(f, opts = {}) {
42094
42249
  const category = findingCategory(f);
@@ -42105,7 +42260,7 @@ function llmFindingToReviewFinding(f, opts = {}) {
42105
42260
  result.category = category;
42106
42261
  if (source)
42107
42262
  result.source = source;
42108
- result.fixTarget = deriveFixTargetForReviewFinding(category, source);
42263
+ result.fixTarget = deriveFixTargetForReviewFinding(category, source, f.file, opts.isTestFile);
42109
42264
  const meta3 = buildMeta(f, f.severity !== narrowed ? f.severity : undefined);
42110
42265
  if (meta3)
42111
42266
  result.meta = meta3;
@@ -42275,7 +42430,7 @@ var package_default;
42275
42430
  var init_package = __esm(() => {
42276
42431
  package_default = {
42277
42432
  name: "@nathapp/nax",
42278
- version: "0.74.0",
42433
+ version: "0.75.0",
42279
42434
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42280
42435
  type: "module",
42281
42436
  bin: {
@@ -42378,8 +42533,8 @@ var init_version = __esm(() => {
42378
42533
  NAX_VERSION = package_default.version;
42379
42534
  NAX_COMMIT = (() => {
42380
42535
  try {
42381
- if (/^[0-9a-f]{6,10}$/.test("91bb7306"))
42382
- return "91bb7306";
42536
+ if (/^[0-9a-f]{6,10}$/.test("05b2a123"))
42537
+ return "05b2a123";
42383
42538
  } catch {}
42384
42539
  try {
42385
42540
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42785,12 +42940,12 @@ async function runAdversarialReview(opts) {
42785
42940
  } = classifyRecurrence(allFindings, priorAdversarialIterations ?? [], recurrenceCfg, testFileMatch, threshold);
42786
42941
  const advisoryFindings = [...advisoryOnly, ...demoted];
42787
42942
  const advisoryReviewFindings = [
42788
- ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review" }),
42789
- ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review" }))
42943
+ ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review", isTestFile: testFileMatch }),
42944
+ ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review", isTestFile: testFileMatch }))
42790
42945
  ];
42791
42946
  const advisoryFindingsAsFindings = [
42792
- ...toAdversarialReviewFindings(advisoryOnly),
42793
- ...tagCoverageGap(toAdversarialReviewFindings(demoted))
42947
+ ...toAdversarialReviewFindings(advisoryOnly, { isTestFile: testFileMatch }),
42948
+ ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
42794
42949
  ];
42795
42950
  const acDropped = opResult.acDropped ?? [];
42796
42951
  let diffFiles;
@@ -42870,7 +43025,7 @@ async function runAdversarialReview(opts) {
42870
43025
  blockingThreshold: threshold,
42871
43026
  result: {
42872
43027
  passed: false,
42873
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
43028
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
42874
43029
  },
42875
43030
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
42876
43031
  diffAvailable,
@@ -42887,7 +43042,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
42887
43042
  exitCode: 1,
42888
43043
  output,
42889
43044
  durationMs,
42890
- findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings) : undefined,
43045
+ findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings, { isTestFile: testFileMatch }) : undefined,
42891
43046
  advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
42892
43047
  cost: llmCost
42893
43048
  };
@@ -42895,7 +43050,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
42895
43050
  if (!opResult.passed && acDropped.length > 0) {
42896
43051
  const allHallucinated = acDropped.every((d) => d.code === "ac_quote_not_substring");
42897
43052
  if (allHallucinated) {
42898
- const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })));
43053
+ const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
42899
43054
  const existingAdvisory = advisoryFindings.length > 0 ? advisoryFindingsAsFindings : [];
42900
43055
  const allAdvisory = [...existingAdvisory, ...demotedFindings];
42901
43056
  logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
@@ -42983,7 +43138,7 @@ ${dropSummary}`,
42983
43138
  blockingThreshold: threshold,
42984
43139
  result: {
42985
43140
  passed: true,
42986
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
43141
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
42987
43142
  },
42988
43143
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
42989
43144
  diffAvailable,
@@ -43488,7 +43643,8 @@ async function runSemanticDebate(opts) {
43488
43643
  prompt,
43489
43644
  productionExcludePatterns,
43490
43645
  blockingThreshold,
43491
- createDebateRunner
43646
+ createDebateRunner,
43647
+ isTestFile: isTestFile3
43492
43648
  } = opts;
43493
43649
  const logger = getSafeLogger();
43494
43650
  const configuredStageConfig = naxConfig.debate?.stages.review;
@@ -43564,9 +43720,9 @@ async function runSemanticDebate(opts) {
43564
43720
  blockingThreshold: debateThreshold,
43565
43721
  result: {
43566
43722
  passed: false,
43567
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43723
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43568
43724
  },
43569
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43725
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43570
43726
  });
43571
43727
  return {
43572
43728
  check: "semantic",
@@ -43577,8 +43733,8 @@ async function runSemanticDebate(opts) {
43577
43733
 
43578
43734
  ${formatFindings2(debateBlocking)}`,
43579
43735
  durationMs,
43580
- findings: toReviewFindings(debateBlocking),
43581
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43736
+ findings: toReviewFindings(debateBlocking, { isTestFile: isTestFile3 }),
43737
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43582
43738
  cost: debateCost
43583
43739
  };
43584
43740
  }
@@ -43596,9 +43752,9 @@ ${formatFindings2(debateBlocking)}`,
43596
43752
  blockingThreshold: debateThreshold,
43597
43753
  result: {
43598
43754
  passed: true,
43599
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43755
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43600
43756
  },
43601
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43757
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43602
43758
  });
43603
43759
  return {
43604
43760
  check: "semantic",
@@ -43607,7 +43763,7 @@ ${formatFindings2(debateBlocking)}`,
43607
43763
  exitCode: 0,
43608
43764
  output: "Semantic review passed (debate, all findings were advisory \u2014 below blocking threshold)",
43609
43765
  durationMs,
43610
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43766
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43611
43767
  cost: debateCost
43612
43768
  };
43613
43769
  }
@@ -43622,9 +43778,9 @@ ${formatFindings2(debateBlocking)}`,
43622
43778
  blockingThreshold: debateThreshold,
43623
43779
  result: {
43624
43780
  passed: true,
43625
- findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
43781
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review", isTestFile: isTestFile3 })
43626
43782
  },
43627
- advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
43783
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review", isTestFile: isTestFile3 }) : undefined
43628
43784
  });
43629
43785
  return {
43630
43786
  check: "semantic",
@@ -43633,7 +43789,7 @@ ${formatFindings2(debateBlocking)}`,
43633
43789
  exitCode: 0,
43634
43790
  output: "Semantic review passed",
43635
43791
  durationMs,
43636
- advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory) : undefined,
43792
+ advisoryFindings: debateAdvisory.length > 0 ? toReviewFindings(debateAdvisory, { isTestFile: isTestFile3 }) : undefined,
43637
43793
  cost: debateCost
43638
43794
  };
43639
43795
  }
@@ -43679,10 +43835,13 @@ async function runSemanticReview(opts) {
43679
43835
  contextBundle,
43680
43836
  projectDir,
43681
43837
  naxIgnoreIndex,
43682
- runtime
43838
+ runtime,
43839
+ resolvedTestPatterns
43683
43840
  } = opts;
43684
43841
  const startTime = Date.now();
43685
43842
  const logger = getSafeLogger();
43843
+ const testFilePatterns = resolvedTestPatterns?.regex ?? [];
43844
+ const testFileMatch = (file3) => testFilePatterns.some((re) => re.test(file3));
43686
43845
  if (featureName === undefined) {
43687
43846
  logger?.debug("semantic", "featureName missing \u2014 semantic session name will not include feature", {
43688
43847
  storyId: story.id
@@ -43815,6 +43974,7 @@ async function runSemanticReview(opts) {
43815
43974
  prompt,
43816
43975
  productionExcludePatterns: excludePatterns,
43817
43976
  blockingThreshold,
43977
+ isTestFile: testFileMatch,
43818
43978
  createDebateRunner: _semanticDeps.createDebateRunner
43819
43979
  });
43820
43980
  }
@@ -43974,9 +44134,9 @@ ${formatFindings2(blockingFindings)}`;
43974
44134
  blockingThreshold: threshold,
43975
44135
  result: {
43976
44136
  passed: false,
43977
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
44137
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
43978
44138
  },
43979
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44139
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
43980
44140
  });
43981
44141
  return {
43982
44142
  check: "semantic",
@@ -43985,8 +44145,8 @@ ${formatFindings2(blockingFindings)}`;
43985
44145
  exitCode: 1,
43986
44146
  output,
43987
44147
  durationMs,
43988
- findings: toReviewFindings(blockingFindings),
43989
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44148
+ findings: toReviewFindings(blockingFindings, { isTestFile: testFileMatch }),
44149
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
43990
44150
  cost: llmCost
43991
44151
  };
43992
44152
  }
@@ -44006,7 +44166,7 @@ ${formatFindings2(blockingFindings)}`;
44006
44166
  passed: false,
44007
44167
  blockingThreshold: threshold,
44008
44168
  result: { passed: false, findings: [] },
44009
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44169
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
44010
44170
  });
44011
44171
  return {
44012
44172
  check: "semantic",
@@ -44015,7 +44175,7 @@ ${formatFindings2(blockingFindings)}`;
44015
44175
  exitCode: 1,
44016
44176
  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
44177
  durationMs,
44018
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44178
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
44019
44179
  cost: llmCost
44020
44180
  };
44021
44181
  }
@@ -44032,9 +44192,9 @@ ${formatFindings2(blockingFindings)}`;
44032
44192
  blockingThreshold: threshold,
44033
44193
  result: {
44034
44194
  passed: true,
44035
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
44195
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
44036
44196
  },
44037
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
44197
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
44038
44198
  });
44039
44199
  return {
44040
44200
  check: "semantic",
@@ -44043,7 +44203,7 @@ ${formatFindings2(blockingFindings)}`;
44043
44203
  exitCode: 0,
44044
44204
  output: allFindings.length === 0 ? "Semantic review passed" : "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
44045
44205
  durationMs,
44046
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
44206
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
44047
44207
  cost: llmCost
44048
44208
  };
44049
44209
  }
@@ -45339,6 +45499,43 @@ var init_rectifier_builder = __esm(() => {
45339
45499
  ];
45340
45500
  });
45341
45501
 
45502
+ // src/prompts/builders/timeout-retry-builder.ts
45503
+ function formatDuration2(ms) {
45504
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
45505
+ const minutes = Math.floor(totalSeconds / 60);
45506
+ const seconds = totalSeconds % 60;
45507
+ if (minutes === 0)
45508
+ return `${seconds}s`;
45509
+ return `${minutes}m ${seconds}s`;
45510
+ }
45511
+ function timeoutRetry(input) {
45512
+ const { prompt, changedFiles, elapsedMs, attempt } = input;
45513
+ const duration3 = formatDuration2(elapsedMs);
45514
+ const attemptNumber = attempt + 1;
45515
+ if (changedFiles.length === 0) {
45516
+ return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}) with no file changes on disk.
45517
+ This is attempt ${attemptNumber} of the same story \u2014 the previous attempt left nothing behind, so the approach was wrong.
45518
+ Change your approach: pick a narrower scope, fewer file edits, or a different angle on the acceptance criteria.
45519
+
45520
+ ---
45521
+
45522
+ ${prompt}`;
45523
+ }
45524
+ const fileList = changedFiles.map((p) => `- ${p}`).join(`
45525
+ `);
45526
+ return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}), but left these files on disk:
45527
+
45528
+ ${fileList}
45529
+
45530
+ This is attempt ${attemptNumber} of the same story \u2014 continue from the existing state above.
45531
+ Read the files listed, pick up where the previous attempt stopped, and finish the story.
45532
+ Do NOT delete or revert the existing work; treat the working tree as the starting point.
45533
+
45534
+ ---
45535
+
45536
+ ${prompt}`;
45537
+ }
45538
+
45342
45539
  // src/prompts/builders/one-shot-builder.ts
45343
45540
  class OneShotPromptBuilder {
45344
45541
  acc = new SectionAccumulator;
@@ -45501,7 +45698,7 @@ No acceptance criterion may use a deprecated verification tag (\`[grep]\`, \`[fi
45501
45698
  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
45699
 
45503
45700
  #### 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.
45701
+ 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
45702
 
45506
45703
  #### ac-testable
45507
45704
  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 +45750,6 @@ For each one:
45553
45750
  - Remove any shell-command patterns (\`grep -\`, \`wc\`, pipe \`|\` inside backticks). Express the same invariant as an assertion on the runtime value.
45554
45751
  - Do not remove or weaken acceptance criteria that are already correct.
45555
45752
 
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
45753
  Write the corrected PRD to this file path: ${outputFilePath}
45574
45754
  Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
45575
45755
  }
@@ -46310,10 +46490,17 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46310
46490
  hopBodyInput
46311
46491
  } = ctx;
46312
46492
  const stage = pipelineStage ?? "run";
46493
+ let preAttemptGitRefPromise;
46494
+ let priorHopStartedAt;
46313
46495
  return async (agentName, hopBundle, hopKind, resolvedRunOptions) => {
46314
46496
  const logger = getLogger();
46315
46497
  let workingBundle = hopBundle;
46316
46498
  let prompt = resolvedRunOptions.prompt;
46499
+ const elapsedSincePriorHop = priorHopStartedAt ? Date.now() - priorHopStartedAt : 0;
46500
+ priorHopStartedAt = Date.now();
46501
+ if (hopKind.kind === "primary" && !preAttemptGitRefPromise) {
46502
+ preAttemptGitRefPromise = _buildHopCallbackDeps.captureGitRef(workdir);
46503
+ }
46317
46504
  if (hopKind.kind === "swap" && hopBundle) {
46318
46505
  workingBundle = _buildHopCallbackDeps.rebuildForAgent(hopBundle, agentName, hopKind.failure, story.id);
46319
46506
  if (projectDir && featureName && workingBundle.manifest.rebuildInfo) {
@@ -46342,6 +46529,17 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46342
46529
  if (hopKind.kind === "swap" && sessionId) {
46343
46530
  sessionManager.handoff?.(sessionId, agentName, hopKind.failure.outcome);
46344
46531
  }
46532
+ if (hopKind.kind === "timeout-retry") {
46533
+ const preAttemptGitRef = preAttemptGitRefPromise ? await preAttemptGitRefPromise : undefined;
46534
+ const changedFiles = preAttemptGitRef ? await _buildHopCallbackDeps.captureWorkingTreeChanges(workdir, preAttemptGitRef) : [];
46535
+ const elapsedMs = elapsedSincePriorHop;
46536
+ prompt = _buildHopCallbackDeps.timeoutRetry({
46537
+ prompt: resolvedRunOptions.prompt,
46538
+ changedFiles,
46539
+ elapsedMs,
46540
+ attempt: hopKind.attempt
46541
+ });
46542
+ }
46345
46543
  const contextToolRuntime = workingBundle ? _buildHopCallbackDeps.createContextToolRuntime({
46346
46544
  bundle: workingBundle,
46347
46545
  story,
@@ -46400,6 +46598,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46400
46598
  signal: resolvedRunOptions.abortSignal
46401
46599
  });
46402
46600
  }
46601
+ let timedOut = false;
46403
46602
  try {
46404
46603
  const send = (turnPrompt) => agentManager.runAsSession(agentName, handle, turnPrompt, {
46405
46604
  storyId: story.id,
@@ -46417,9 +46616,12 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46417
46616
  ...maxInteractionTurns !== undefined ? { maxTurns: maxInteractionTurns } : {}
46418
46617
  });
46419
46618
  const turnResult = hopBody ? await hopBody(prompt, { send, input: hopBodyInput }) : await send(prompt);
46619
+ if (turnResult.timedOut)
46620
+ timedOut = true;
46420
46621
  return { result: turnResultToAgentResult(turnResult), bundle: workingBundle, prompt };
46421
46622
  } catch (err) {
46422
46623
  const sessionFailure = err instanceof SessionFailureError ? err.adapterFailure : undefined;
46624
+ timedOut = sessionFailure?.outcome === "fail-timeout";
46423
46625
  const turnError = err instanceof SessionTurnError ? err : undefined;
46424
46626
  const errMessage = err instanceof Error ? err.message : String(err);
46425
46627
  return {
@@ -46441,7 +46643,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46441
46643
  prompt
46442
46644
  };
46443
46645
  } finally {
46444
- if (hopKind.kind !== "stale-retry" && !resolvedRunOptions.keepOpen) {
46646
+ if (hopKind.kind !== "stale-retry" && (!resolvedRunOptions.keepOpen || timedOut)) {
46445
46647
  await sessionManager.closeSession(handle);
46446
46648
  }
46447
46649
  }
@@ -46456,10 +46658,14 @@ var init_build_hop_callback = __esm(() => {
46456
46658
  init_manifest_store();
46457
46659
  init_logger2();
46458
46660
  init_prompts();
46661
+ init_git();
46459
46662
  _buildHopCallbackDeps = {
46460
46663
  rebuildForAgent: (prior, newAgentId, failure, storyId) => new ContextOrchestrator([]).rebuildForAgent(prior, { newAgentId, failure, storyId }),
46461
46664
  writeRebuildManifest,
46462
- createContextToolRuntime
46665
+ createContextToolRuntime,
46666
+ captureGitRef,
46667
+ captureWorkingTreeChanges,
46668
+ timeoutRetry: (input) => timeoutRetry(input)
46463
46669
  };
46464
46670
  });
46465
46671
 
@@ -46664,17 +46870,10 @@ async function callOp(ctx, op, input) {
46664
46870
  effective = { ...turn, output: fileContent };
46665
46871
  }
46666
46872
  }
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
- };
46873
+ if (!effective.output?.trim()) {
46874
+ const failure = classifyEmptyOutputFailure(effective);
46875
+ if (failure)
46876
+ return { ...effective, adapterFailure: failure };
46678
46877
  }
46679
46878
  return effective;
46680
46879
  };
@@ -52249,11 +52448,142 @@ var init_cli = __esm(() => {
52249
52448
  CLIConfigSchema = exports_external.object({}).passthrough();
52250
52449
  });
52251
52450
 
52451
+ // src/interaction/plugins/telegram-format.ts
52452
+ function buildHeader(request) {
52453
+ const emoji3 = getStageEmoji(request.stage);
52454
+ let text = `${emoji3} *${request.stage.toUpperCase()}*
52455
+ `;
52456
+ text += `*Feature:* ${request.featureName}
52457
+ `;
52458
+ if (request.storyId) {
52459
+ text += `*Story:* ${request.storyId}
52460
+ `;
52461
+ }
52462
+ text += `
52463
+ `;
52464
+ return text;
52465
+ }
52466
+ function buildBody(request) {
52467
+ let text = `${sanitizeMarkdown(request.summary)}
52468
+ `;
52469
+ if (request.detail) {
52470
+ text += `
52471
+ ${sanitizeMarkdown(request.detail)}
52472
+ `;
52473
+ }
52474
+ if (request.options && request.options.length > 0) {
52475
+ text += `
52476
+ *Options:*
52477
+ `;
52478
+ for (const opt of request.options) {
52479
+ const desc = opt.description ? ` - ${sanitizeMarkdown(opt.description)}` : "";
52480
+ text += ` - ${opt.label}${desc}
52481
+ `;
52482
+ }
52483
+ }
52484
+ if (request.timeout) {
52485
+ const timeoutSec = Math.floor(request.timeout / 1000);
52486
+ text += `
52487
+ \u23F1 Timeout: ${timeoutSec}s | Fallback: ${request.fallback}`;
52488
+ }
52489
+ return text;
52490
+ }
52491
+ function sanitizeMarkdown(text) {
52492
+ return text.replace(/\\(?=[_*`\[])/g, "\\\\").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/\[/g, "\\[");
52493
+ }
52494
+ function splitText(text, maxChars) {
52495
+ if (text.length <= maxChars)
52496
+ return [text];
52497
+ const chunks = [];
52498
+ let remaining = text;
52499
+ while (remaining.length > maxChars) {
52500
+ const slice = remaining.slice(0, maxChars);
52501
+ const lastNewline = slice.lastIndexOf(`
52502
+ `);
52503
+ if (lastNewline > maxChars * 0.5) {
52504
+ chunks.push(remaining.slice(0, lastNewline));
52505
+ remaining = remaining.slice(lastNewline + 1);
52506
+ } else {
52507
+ chunks.push(slice);
52508
+ remaining = remaining.slice(maxChars);
52509
+ }
52510
+ }
52511
+ if (remaining.length > 0)
52512
+ chunks.push(remaining);
52513
+ return chunks;
52514
+ }
52515
+ function buildKeyboard(request) {
52516
+ switch (request.type) {
52517
+ case "confirm":
52518
+ return [
52519
+ [
52520
+ { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52521
+ { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52522
+ ],
52523
+ [
52524
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52525
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52526
+ ]
52527
+ ];
52528
+ case "choose": {
52529
+ if (!request.options || request.options.length === 0)
52530
+ return null;
52531
+ const rows = [];
52532
+ for (const opt of request.options) {
52533
+ rows.push([{ text: opt.label, callback_data: `${request.id}:choose:${opt.key}` }]);
52534
+ }
52535
+ rows.push([
52536
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52537
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52538
+ ]);
52539
+ return rows;
52540
+ }
52541
+ case "review":
52542
+ return [
52543
+ [
52544
+ { text: "\u2705 Approve", callback_data: `${request.id}:approve` },
52545
+ { text: "\u274C Reject", callback_data: `${request.id}:reject` }
52546
+ ],
52547
+ [
52548
+ { text: "\u23ED Skip", callback_data: `${request.id}:skip` },
52549
+ { text: "\uD83D\uDED1 Abort", callback_data: `${request.id}:abort` }
52550
+ ]
52551
+ ];
52552
+ default:
52553
+ return null;
52554
+ }
52555
+ }
52556
+ function getStageEmoji(stage) {
52557
+ switch (stage) {
52558
+ case "pre-flight":
52559
+ return "\uD83D\uDE80";
52560
+ case "execution":
52561
+ return "\u2699\uFE0F";
52562
+ case "review":
52563
+ return "\uD83D\uDD0D";
52564
+ case "merge":
52565
+ return "\uD83D\uDD00";
52566
+ case "cost":
52567
+ return "\uD83D\uDCB0";
52568
+ default:
52569
+ return "\uD83D\uDCCC";
52570
+ }
52571
+ }
52572
+ var MAX_MESSAGE_CHARS = 4000;
52573
+
52252
52574
  // src/interaction/plugins/telegram.ts
52253
- var MAX_MESSAGE_CHARS = 4000, CALLBACK_API_TIMEOUT_MS = 4000, TelegramConfigSchema, TelegramInteractionPlugin;
52575
+ function normalizeChatId(raw) {
52576
+ const chatId = raw.trim();
52577
+ return { chatId, unmatchable: !NUMERIC_CHAT_ID.test(chatId) };
52578
+ }
52579
+ var _telegramPluginDeps, CALLBACK_API_TIMEOUT_MS = 4000, NUMERIC_CHAT_ID, TelegramConfigSchema, TelegramInteractionPlugin;
52254
52580
  var init_telegram = __esm(() => {
52255
52581
  init_zod();
52256
52582
  init_logger2();
52583
+ _telegramPluginDeps = {
52584
+ fetch: globalThis.fetch.bind(globalThis)
52585
+ };
52586
+ NUMERIC_CHAT_ID = /^-?\d+$/;
52257
52587
  TelegramConfigSchema = exports_external.object({
52258
52588
  botToken: exports_external.string().optional(),
52259
52589
  chatId: exports_external.string().optional()
@@ -52267,6 +52597,7 @@ var init_telegram = __esm(() => {
52267
52597
  lastUpdateId = 0;
52268
52598
  backoffMs = 1000;
52269
52599
  maxBackoffMs = 30000;
52600
+ static MAX_DRAIN_PAGES = 10;
52270
52601
  static INTERACTIVE_REQUEST_TYPES = new Set([
52271
52602
  "confirm",
52272
52603
  "choose",
@@ -52276,10 +52607,29 @@ var init_telegram = __esm(() => {
52276
52607
  async init(config2) {
52277
52608
  const cfg = TelegramConfigSchema.parse(config2);
52278
52609
  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;
52610
+ const rawChatId = cfg.chatId ?? process.env.NAX_TELEGRAM_CHAT_ID ?? null;
52611
+ const normalized = rawChatId === null ? null : normalizeChatId(rawChatId);
52612
+ this.chatId = normalized?.chatId || null;
52280
52613
  if (!this.botToken || !this.chatId) {
52281
52614
  throw new Error("Telegram plugin requires botToken and chatId (env: NAX_TELEGRAM_TOKEN or TELEGRAM_BOT_TOKEN, NAX_TELEGRAM_CHAT_ID)");
52282
52615
  }
52616
+ if (normalized?.unmatchable) {
52617
+ 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 });
52618
+ }
52619
+ }
52620
+ async drainBacklog() {
52621
+ for (let page = 0;page < TelegramInteractionPlugin.MAX_DRAIN_PAGES; page++) {
52622
+ const result = await this.fetchUpdates();
52623
+ if (!result.ok) {
52624
+ this.logger?.warn("interaction", "Telegram backlog drain failed \u2014 stale updates may be misread as a response");
52625
+ return;
52626
+ }
52627
+ if (result.rawCount === 0)
52628
+ return;
52629
+ }
52630
+ this.logger?.warn("interaction", "Telegram backlog drain hit page cap \u2014 stale updates may remain", {
52631
+ pages: TelegramInteractionPlugin.MAX_DRAIN_PAGES
52632
+ });
52283
52633
  }
52284
52634
  async destroy() {
52285
52635
  this.pendingMessages.clear();
@@ -52288,10 +52638,13 @@ var init_telegram = __esm(() => {
52288
52638
  if (!this.botToken || !this.chatId) {
52289
52639
  throw new Error("Telegram plugin not initialized");
52290
52640
  }
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);
52641
+ if (TelegramInteractionPlugin.INTERACTIVE_REQUEST_TYPES.has(request.type)) {
52642
+ await this.drainBacklog();
52643
+ }
52644
+ const header = buildHeader(request);
52645
+ const keyboard = buildKeyboard(request);
52646
+ const body = buildBody(request);
52647
+ const chunks = splitText(body, MAX_MESSAGE_CHARS - header.length - 10);
52295
52648
  try {
52296
52649
  const sentIds = [];
52297
52650
  for (let i = 0;i < chunks.length; i++) {
@@ -52299,7 +52652,7 @@ var init_telegram = __esm(() => {
52299
52652
  const partLabel = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
52300
52653
  const text = `${header}
52301
52654
  ${partLabel}${chunks[i]}`;
52302
- const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
52655
+ const response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
52303
52656
  method: "POST",
52304
52657
  headers: { "Content-Type": "application/json" },
52305
52658
  body: JSON.stringify({
@@ -52320,7 +52673,7 @@ ${partLabel}${chunks[i]}`;
52320
52673
  sentIds.push(data.result.message_id);
52321
52674
  }
52322
52675
  if (TelegramInteractionPlugin.INTERACTIVE_REQUEST_TYPES.has(request.type)) {
52323
- this.pendingMessages.set(request.id, sentIds);
52676
+ this.pendingMessages.set(request.id, { type: request.type, ids: sentIds });
52324
52677
  }
52325
52678
  } catch (err) {
52326
52679
  const msg = err instanceof Error ? err.message : String(err);
@@ -52380,140 +52733,25 @@ ${partLabel}${chunks[i]}`;
52380
52733
  await this.sendTimeoutMessage(requestId);
52381
52734
  this.pendingMessages.delete(requestId);
52382
52735
  }
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
52736
  async getUpdates() {
52737
+ const result = await this.fetchUpdates();
52738
+ return result.updates;
52739
+ }
52740
+ async fetchUpdates() {
52504
52741
  if (!this.botToken)
52505
- return [];
52742
+ return { ok: true, updates: [], rawCount: 0 };
52506
52743
  try {
52507
52744
  const controller = new AbortController;
52508
52745
  const timer = setTimeout(() => controller.abort(), 8000);
52509
52746
  let response;
52510
52747
  try {
52511
- response = await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates`, {
52748
+ response = await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates`, {
52512
52749
  method: "POST",
52513
52750
  headers: { "Content-Type": "application/json" },
52514
52751
  body: JSON.stringify({
52515
52752
  offset: this.lastUpdateId + 1,
52516
- timeout: 1
52753
+ timeout: 1,
52754
+ limit: 100
52517
52755
  }),
52518
52756
  signal: controller.signal
52519
52757
  });
@@ -52528,17 +52766,31 @@ ${this.sanitizeMarkdown(request.detail)}
52528
52766
  if (!data.ok || !data.result) {
52529
52767
  throw new Error("Telegram API returned ok=false or missing result");
52530
52768
  }
52531
- const updates = data.result;
52532
- if (updates.length > 0) {
52533
- this.lastUpdateId = Math.max(...updates.map((u) => u.update_id));
52769
+ const raw = data.result;
52770
+ if (raw.length > 0) {
52771
+ this.lastUpdateId = Math.max(...raw.map((u) => u.update_id));
52772
+ }
52773
+ const updates = raw.filter((u) => this.isFromConfiguredChat(u));
52774
+ if (updates.length !== raw.length) {
52775
+ this.logger?.debug("interaction", "Telegram updates rejected -- not from the configured chat", {
52776
+ rejected: raw.length - updates.length
52777
+ });
52534
52778
  }
52535
52779
  this.backoffMs = 1000;
52536
- return updates;
52780
+ return { ok: true, updates, rawCount: raw.length };
52537
52781
  } catch (err) {
52538
52782
  this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
52539
- return [];
52783
+ this.logger?.debug("interaction", "Telegram getUpdates failed \u2014 retrying with backoff", {
52784
+ error: err instanceof Error ? err.message : String(err),
52785
+ backoffMs: this.backoffMs
52786
+ });
52787
+ return { ok: false, updates: [], rawCount: 0 };
52540
52788
  }
52541
52789
  }
52790
+ isFromConfiguredChat(update) {
52791
+ const chatId = update.callback_query?.message?.chat?.id ?? update.message?.chat?.id;
52792
+ return chatId !== undefined && String(chatId) === this.chatId;
52793
+ }
52542
52794
  parseUpdate(requestId, update) {
52543
52795
  if (update.callback_query) {
52544
52796
  const data = update.callback_query.data;
@@ -52558,11 +52810,11 @@ ${this.sanitizeMarkdown(request.detail)}
52558
52810
  };
52559
52811
  }
52560
52812
  if (update.message?.text) {
52561
- const messageIds = this.pendingMessages.get(requestId);
52562
- if (!messageIds)
52813
+ const pending = this.pendingMessages.get(requestId);
52814
+ if (!pending || pending.type !== "input")
52563
52815
  return null;
52564
52816
  const replyToId = update.message.reply_to_message?.message_id;
52565
- if (replyToId !== undefined && !messageIds.includes(replyToId))
52817
+ if (replyToId !== undefined && !pending.ids.includes(replyToId))
52566
52818
  return null;
52567
52819
  return {
52568
52820
  requestId,
@@ -52581,7 +52833,7 @@ ${this.sanitizeMarkdown(request.detail)}
52581
52833
  const controller = new AbortController;
52582
52834
  const timer = setTimeout(() => controller.abort(), CALLBACK_API_TIMEOUT_MS);
52583
52835
  try {
52584
- await fetch(`https://api.telegram.org/bot${this.botToken}/answerCallbackQuery`, {
52836
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/answerCallbackQuery`, {
52585
52837
  method: "POST",
52586
52838
  headers: { "Content-Type": "application/json" },
52587
52839
  body: JSON.stringify({
@@ -52601,7 +52853,7 @@ ${this.sanitizeMarkdown(request.detail)}
52601
52853
  const controller = new AbortController;
52602
52854
  const timer = setTimeout(() => controller.abort(), CALLBACK_API_TIMEOUT_MS);
52603
52855
  try {
52604
- await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageReplyMarkup`, {
52856
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageReplyMarkup`, {
52605
52857
  method: "POST",
52606
52858
  headers: { "Content-Type": "application/json" },
52607
52859
  body: JSON.stringify({
@@ -52617,14 +52869,14 @@ ${this.sanitizeMarkdown(request.detail)}
52617
52869
  } catch {}
52618
52870
  }
52619
52871
  async sendTimeoutMessage(requestId) {
52620
- const messageIds = this.pendingMessages.get(requestId);
52621
- if (!messageIds || !this.botToken || !this.chatId) {
52872
+ const pending = this.pendingMessages.get(requestId);
52873
+ if (!pending || !this.botToken || !this.chatId) {
52622
52874
  this.pendingMessages.delete(requestId);
52623
52875
  return;
52624
52876
  }
52625
- const lastId = messageIds[messageIds.length - 1];
52877
+ const lastId = pending.ids[pending.ids.length - 1];
52626
52878
  try {
52627
- await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
52879
+ await _telegramPluginDeps.fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
52628
52880
  method: "POST",
52629
52881
  headers: { "Content-Type": "application/json" },
52630
52882
  body: JSON.stringify({
@@ -58708,7 +58960,8 @@ async function refreshReviewInputForDispatch(opName, input) {
58708
58960
  stat: fresh2.stat,
58709
58961
  diff: fresh2.diff,
58710
58962
  excludePatterns: fresh2.excludePatterns,
58711
- storyGitRef: fresh2.effectiveRef ?? semInput.storyGitRef
58963
+ storyGitRef: fresh2.effectiveRef ?? semInput.storyGitRef,
58964
+ resolvedTestPatterns: _refresh.resolvedTestPatterns
58712
58965
  };
58713
58966
  }
58714
58967
  const { _refresh: __, ...advInput } = input;
@@ -59755,6 +60008,7 @@ async function assemblePlanInputsFromCtx(ctx) {
59755
60008
  excludePatterns: prepared.excludePatterns,
59756
60009
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-semantic"),
59757
60010
  priorSemanticIterations: ctx.priorSemanticIterations,
60011
+ resolvedTestPatterns,
59758
60012
  blockingThreshold: ctx.config.review.blockingThreshold,
59759
60013
  _refresh: {
59760
60014
  projectDir: ctx.projectDir,
@@ -59792,6 +60046,7 @@ async function assemblePlanInputsFromCtx(ctx) {
59792
60046
  refExcludePatterns: prepared.refExcludePatterns,
59793
60047
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-adversarial"),
59794
60048
  priorAdversarialIterations: ctx.priorAdversarialIterations,
60049
+ resolvedTestPatterns,
59795
60050
  blockingThreshold: ctx.config.review.blockingThreshold,
59796
60051
  _refresh: {
59797
60052
  projectDir: ctx.projectDir,
@@ -62784,7 +63039,7 @@ var init_forge = __esm(() => {
62784
63039
  function buildTitle(ctx) {
62785
63040
  return `feat: ${ctx.feature}`;
62786
63041
  }
62787
- function formatDuration2(totalMs) {
63042
+ function formatDuration3(totalMs) {
62788
63043
  const clampedMs = Math.max(0, Math.round(totalMs));
62789
63044
  const totalSeconds = Math.floor(clampedMs / MS_PER_SECOND);
62790
63045
  const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
@@ -62800,7 +63055,7 @@ function buildSummaryLines(ctx) {
62800
63055
  "## Run summary",
62801
63056
  `- Feature: ${ctx.feature}`,
62802
63057
  `- Stories: ${passed} / ${failed} / ${skipped}`,
62803
- `- Duration: ${formatDuration2(ctx.totalDurationMs)}`,
63058
+ `- Duration: ${formatDuration3(ctx.totalDurationMs)}`,
62804
63059
  `- PRD: ${ctx.prdPath}`,
62805
63060
  ""
62806
63061
  ];
@@ -62821,7 +63076,7 @@ function buildStoryTable(stories) {
62821
63076
  lines.push("");
62822
63077
  return lines;
62823
63078
  }
62824
- function buildBody(ctx, template) {
63079
+ function buildBody2(ctx, template) {
62825
63080
  const blocks = [];
62826
63081
  blocks.push("> Auto-opened by nax \u2014 review pending. Run nax-finish before merge.");
62827
63082
  blocks.push("");
@@ -62996,7 +63251,7 @@ var init_auto_pr = __esm(() => {
62996
63251
  });
62997
63252
  const prCtx = toPrBodyContext(context);
62998
63253
  const title = buildTitle(prCtx);
62999
- const body = buildBody(prCtx, template);
63254
+ const body = buildBody2(prCtx, template);
63000
63255
  return await _autoPrDeps.openDraft(forge, { title, body, branch: context.branch, draft: cfg.draft }, { run: _autoPrDeps.run, readText: _autoPrDeps.readText }, context.workdir);
63001
63256
  } catch (err) {
63002
63257
  context.logger.warn("Auto-PR execute failed", { error: String(err) });
@@ -68780,7 +69035,7 @@ function extractQuoteTriples(reason) {
68780
69035
  }
68781
69036
  return triples;
68782
69037
  }
68783
- function normalizeWs3(s) {
69038
+ function normalizeWs2(s) {
68784
69039
  return s.replace(/\s+/g, " ").trim();
68785
69040
  }
68786
69041
  async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
@@ -68794,7 +69049,7 @@ async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
68794
69049
  const end = Math.min(lines.length, triple.line + CONTEXT_LINES);
68795
69050
  const window2 = lines.slice(start, end).join(`
68796
69051
  `);
68797
- return normalizeWs3(window2).toLowerCase().includes(normalizeWs3(triple.quote).toLowerCase());
69052
+ return normalizeWs2(window2).toLowerCase().includes(normalizeWs2(triple.quote).toLowerCase());
68798
69053
  }
68799
69054
  async function verifyEscalationQuotes(reason, workdir, storyId, deps = _quoteIntegrityDeps) {
68800
69055
  const triples = extractQuoteTriples(reason);
@@ -69792,6 +70047,12 @@ async function executeParallelBatch(stories, _projectRoot, config2, context, wor
69792
70047
  error: result.error
69793
70048
  });
69794
70049
  }
70050
+ }).catch((error48) => {
70051
+ results.failed.push({ story, error: errorMessage(error48) });
70052
+ logger?.error("parallel", "Story execution threw", {
70053
+ storyId: story.id,
70054
+ error: errorMessage(error48)
70055
+ });
69795
70056
  }).finally(() => {
69796
70057
  executing.delete(executePromise);
69797
70058
  });
@@ -69800,7 +70061,7 @@ async function executeParallelBatch(stories, _projectRoot, config2, context, wor
69800
70061
  await Promise.race(executing);
69801
70062
  }
69802
70063
  }
69803
- await Promise.all(executing);
70064
+ await Promise.allSettled(executing);
69804
70065
  return results;
69805
70066
  }
69806
70067
  var _parallelWorkerDeps;
@@ -102438,7 +102699,6 @@ class DebatePlanStrategy {
102438
102699
  });
102439
102700
  if (debateResult.outcome !== "failed" && debateResult.output) {
102440
102701
  const prd2 = validatePlanOutput(debateResult.output, ctx.options.feature, ctx.branchName);
102441
- warnOnDroppedVerbatimAcs(prd2, ctx.specContent, ctx.options.feature);
102442
102702
  const scoped2 = backfillOutOfScope(prd2, ctx.specContent, ctx.options.feature);
102443
102703
  const withProject2 = { ...scoped2, project: ctx.projectName };
102444
102704
  return _debatePlanDeps.writeOrRecoverPrd(ctx, withProject2);
@@ -103247,9 +103507,6 @@ var FIELD_DESCRIPTIONS = {
103247
103507
  "execution.regressionGate.timeoutSeconds": "Timeout for regression run in seconds",
103248
103508
  "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
103509
  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
103510
  "quality.commands": "Custom quality commands",
103254
103511
  "quality.commands.typecheck": "Custom typecheck command",
103255
103512
  "quality.commands.lint": "Custom lint command",
@@ -105478,7 +105735,7 @@ var DEFAULT_LIMIT = 20;
105478
105735
  var _runsCmdDeps = {
105479
105736
  getRunsDir
105480
105737
  };
105481
- function formatDuration3(ms) {
105738
+ function formatDuration4(ms) {
105482
105739
  if (ms <= 0)
105483
105740
  return "-";
105484
105741
  const minutes = Math.floor(ms / 60000);
@@ -105593,7 +105850,7 @@ async function runsCommand(options = {}) {
105593
105850
  pad3(row.feature, COL.feature),
105594
105851
  pad3(colored, COL.status + (colored.length - visibleLength(colored))),
105595
105852
  pad3(`${row.passed}/${row.total}`, COL.stories),
105596
- pad3(formatDuration3(row.durationMs), COL.duration),
105853
+ pad3(formatDuration4(row.durationMs), COL.duration),
105597
105854
  formatDate(row.registeredAt)
105598
105855
  ].join(" ");
105599
105856
  console.log(line);
@@ -112874,7 +113131,7 @@ function useAgentStreamEvents(bus) {
112874
113131
  if (!bus)
112875
113132
  return;
112876
113133
  const unsubscribe = bus.onAgentStream((event) => {
112877
- const next = new Map(activeCallsRef.current);
113134
+ const next = activeCallsRef.current;
112878
113135
  switch (event.kind) {
112879
113136
  case "agent.call_started": {
112880
113137
  next.set(event.callId, {
@@ -112958,10 +113215,13 @@ function useAgentStreamEvents(bus) {
112958
113215
  default:
112959
113216
  break;
112960
113217
  }
112961
- activeCallsRef.current = next;
112962
113218
  dirtyRef.current = true;
112963
113219
  });
112964
- return unsubscribe;
113220
+ return () => {
113221
+ unsubscribe();
113222
+ activeCallsRef.current.clear();
113223
+ lastTokensRef.current.clear();
113224
+ };
112965
113225
  }, [bus]);
112966
113226
  import_react31.useEffect(() => {
112967
113227
  const interval = setInterval(() => {