@nathapp/nax 0.65.1 → 0.65.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +424 -65
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -19957,7 +19957,9 @@ var init_schemas_execution = __esm(() => {
19957
19957
  test: exports_external.string().optional(),
19958
19958
  testScoped: exports_external.string().optional(),
19959
19959
  lintFix: exports_external.string().optional(),
19960
+ lintFixScoped: exports_external.string().optional(),
19960
19961
  formatFix: exports_external.string().optional(),
19962
+ formatFixScoped: exports_external.string().optional(),
19961
19963
  build: exports_external.string().optional()
19962
19964
  }).default({}),
19963
19965
  lintOutput: exports_external.object({
@@ -20045,7 +20047,7 @@ var init_schemas_execution = __esm(() => {
20045
20047
  });
20046
20048
 
20047
20049
  // src/config/schemas-infra.ts
20048
- var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentFallbackConfigSchema, AgentIdleWatchdogConfigSchema, AgentAcpConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, CuratorThresholdsSchema, CuratorConfigSchema;
20050
+ var PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, LlmRoutingConfigSchema, 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;
20049
20051
  var init_schemas_infra = __esm(() => {
20050
20052
  init_zod();
20051
20053
  init_schemas_model();
@@ -20147,6 +20149,14 @@ var init_schemas_infra = __esm(() => {
20147
20149
  onQualityFailure: exports_external.boolean().default(false),
20148
20150
  rebuildContext: exports_external.boolean().default(true)
20149
20151
  });
20152
+ DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG = {
20153
+ enabled: true,
20154
+ mode: "warn-then-cancel",
20155
+ idleTimeoutSeconds: 900,
20156
+ activityKinds: ["message_update", "thinking_update", "usage_update"],
20157
+ cancelGraceSeconds: 10,
20158
+ maxRetryAttempts: 3
20159
+ };
20150
20160
  AgentIdleWatchdogConfigSchema = exports_external.object({
20151
20161
  enabled: exports_external.boolean().default(true),
20152
20162
  mode: exports_external.enum(["off", "observe", "warn-then-cancel", "cancel"]).default("warn-then-cancel"),
@@ -20173,7 +20183,7 @@ var init_schemas_infra = __esm(() => {
20173
20183
  rebuildContext: true
20174
20184
  }),
20175
20185
  acp: AgentAcpConfigSchema.default({ promptRetries: 0 }),
20176
- idleWatchdog: AgentIdleWatchdogConfigSchema.optional()
20186
+ idleWatchdog: AgentIdleWatchdogConfigSchema.default(DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG)
20177
20187
  });
20178
20188
  PrecheckConfigSchema = exports_external.object({
20179
20189
  storySizeGate: StorySizeGateConfigSchema
@@ -20255,7 +20265,9 @@ var init_schemas_review = __esm(() => {
20255
20265
  test: exports_external.string().optional(),
20256
20266
  build: exports_external.string().optional(),
20257
20267
  lintFix: exports_external.string().optional(),
20258
- formatFix: exports_external.string().optional()
20268
+ lintFixScoped: exports_external.string().optional(),
20269
+ formatFix: exports_external.string().optional(),
20270
+ formatFixScoped: exports_external.string().optional()
20259
20271
  }),
20260
20272
  pluginMode: exports_external.enum(["per-story", "deferred"]).default("per-story"),
20261
20273
  audit: exports_external.object({ enabled: exports_external.boolean().default(false) }).default({ enabled: false }),
@@ -20546,7 +20558,8 @@ var init_schemas3 = __esm(() => {
20546
20558
  maxInteractionTurns: 20,
20547
20559
  promptAudit: { enabled: false },
20548
20560
  fallback: { enabled: false, map: {}, maxHopsPerStory: 2, onQualityFailure: false, rebuildContext: true },
20549
- acp: { promptRetries: 0 }
20561
+ acp: { promptRetries: 0 },
20562
+ idleWatchdog: DEFAULT_AGENT_IDLE_WATCHDOG_CONFIG
20550
20563
  }),
20551
20564
  precheck: PrecheckConfigSchema.optional().default({
20552
20565
  storySizeGate: {
@@ -20716,6 +20729,15 @@ function mergePackageConfig(root, packageOverride) {
20716
20729
  ...packageOverride.quality?.commands?.lintFix !== undefined && {
20717
20730
  lintFix: packageOverride.quality.commands.lintFix
20718
20731
  },
20732
+ ...packageOverride.quality?.commands?.lintFixScoped !== undefined && {
20733
+ lintFixScoped: packageOverride.quality.commands.lintFixScoped
20734
+ },
20735
+ ...packageOverride.quality?.commands?.formatFix !== undefined && {
20736
+ formatFix: packageOverride.quality.commands.formatFix
20737
+ },
20738
+ ...packageOverride.quality?.commands?.formatFixScoped !== undefined && {
20739
+ formatFixScoped: packageOverride.quality.commands.formatFixScoped
20740
+ },
20719
20741
  ...packageOverride.quality?.commands?.typecheck !== undefined && {
20720
20742
  typecheck: packageOverride.quality.commands.typecheck
20721
20743
  },
@@ -29412,10 +29434,14 @@ For each acceptance criterion, verify the current codebase implements it correct
29412
29434
  - The \`verifiedBy.observed\` field MUST be a **verbatim** 1-3 line code excerpt copy-pasted from the file \u2014 not a description. Paste the actual source lines (or a substring of them) that prove your claim. A description like "function X does not check Y" is not a verifiable observation; quote the lines that demonstrate the omission instead. If you cannot quote an exact excerpt that proves your point, downgrade the finding to "unverifiable".
29413
29435
 
29414
29436
  **AC-grounding rule \u2014 required for every "error" finding:**
29415
- - Every "error" finding MUST include \`acQuote\`: a verbatim substring of one AC bullet that names or constrains the exact file, function, or symbol you are flagging.
29437
+ - Every "error" finding MUST include \`acQuote\`: a verbatim substring of one AC bullet that names or constrains the exact **symbol** you are flagging \u2014 not merely the file the symbol lives in.
29416
29438
  - Include \`acIndex\` (1-based) indicating which AC bullet you are quoting.
29417
- - If no AC bullet mentions the file, function, or symbol being flagged, the finding is out-of-scope. Emit it as \`severity: "info"\` instead \u2014 it cannot block the story.
29418
- - Copy \`acQuote\` directly from the Acceptance Criteria text \u2014 do not paraphrase.
29439
+ - Copy \`acQuote\` directly from the Acceptance Criteria \u2014 including any backticks, asterisks, or punctuation. Do not paraphrase, strip formatting, or rewrite.
29440
+
29441
+ **The "AC names the file but not the symbol" trap (most common failure mode):**
29442
+ If the AC bullet mentions a file or component but the **specific symbol you are flagging** (the function, class, interface, type, or convention) is not named in that bullet, the AC does **not** constrain your finding. Emit it as \`severity: "info"\` \u2014 not \`"error"\`. **Convention / coding-standard violations almost always belong as \`"info"\`** unless an AC specifically names the convention or the symbol it concerns.
29443
+
29444
+ If you cannot find an AC that names the **specific symbol** in your finding, downgrade to \`"info"\`. A finding dropped by the validator as ungrounded is worse than one correctly classified as advisory.
29419
29445
 
29420
29446
  Flag issues only when you have confirmed:
29421
29447
  1. An AC is not implemented or partially implemented (verified by reading the actual files)
@@ -29663,10 +29689,22 @@ Severity guide:
29663
29689
  \`passed\` may be \`true\` with findings if all findings are \`"info"\` or \`"unverifiable"\`.
29664
29690
 
29665
29691
  **AC-grounding rule \u2014 required for every "error" finding:**
29666
- - \`acQuote\` must be a verbatim substring of one AC bullet (from the Acceptance Criteria above) that names or constrains the exact file, function, or symbol you are flagging.
29692
+ - \`acQuote\` must be a verbatim substring of one AC bullet (from the Acceptance Criteria above) that names or constrains the exact **symbol** you are flagging \u2014 not merely the file the symbol lives in.
29667
29693
  - \`acIndex\` is the 1-based position of that AC bullet in the list.
29668
- - If no AC bullet mentions the file, function, or symbol being flagged, emit the finding as \`"info"\` instead \u2014 it cannot block the story.
29669
- - Do not paraphrase the AC text \u2014 copy it exactly.`;
29694
+ - Copy \`acQuote\` **exactly** from the AC text, including any backticks, asterisks, or punctuation. Do not paraphrase, strip formatting, or rewrite.
29695
+
29696
+ **The "AC names the file but not the symbol" trap (most common failure mode):**
29697
+ If the AC bullet mentions a file or component but the **specific symbol you are flagging** (the function, class, interface, type, or convention) is not named in that bullet, the AC does **not** constrain your finding. Emit it as \`"info"\` \u2014 not \`"error"\`.
29698
+
29699
+ Worked example:
29700
+ - AC#1 reads: \`\`\`\`AstIndexService.indexCommit() is called by the code_commit outbox handler\`\`\`\`
29701
+ - You found that \`code-commit-outbox-handler.ts\` defines a custom \`ExtendedPrismaClient\` interface, violating a project convention rule.
29702
+ - WRONG: severity \`"error"\`, \`acQuote: "AstIndexService.indexCommit() is called by the code_commit outbox handler"\`, \`acIndex: 1\`. \u2014 AC#1 is about *who calls indexCommit*; it says nothing about Prisma typing. Picking it because the file is named is mis-grounding.
29703
+ - RIGHT: severity \`"info"\`, no \`acQuote\`. The convention violation is real, but no AC constrains \`ExtendedPrismaClient\`, so it cannot block the story.
29704
+
29705
+ **Convention / coding-standard violations almost always belong as \`"info"\`** unless an AC specifically names the convention or the symbol it concerns.
29706
+
29707
+ If you cannot find an AC that names the **specific symbol** in your finding, downgrade to \`"info"\`. A finding dropped by the validator is worse than one correctly classified as advisory.`;
29670
29708
  var init_adversarial_review_builder = () => {};
29671
29709
 
29672
29710
  // src/prompts/builders/acceptance-builder-helpers.ts
@@ -33670,12 +33708,79 @@ var init_rectify = __esm(() => {
33670
33708
  init_prompts();
33671
33709
  });
33672
33710
 
33711
+ // src/operations/test-edit-declaration.ts
33712
+ function readBlockField(block, key) {
33713
+ const re = new RegExp(`^${key}:\\s*(.+)$`, "m");
33714
+ const m = block.match(re);
33715
+ if (!m?.[1])
33716
+ return null;
33717
+ return m[1].trim();
33718
+ }
33719
+ function unwrapQuotes(s) {
33720
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"'))
33721
+ return s.slice(1, -1);
33722
+ return s;
33723
+ }
33724
+ function parseTestEditDeclarations(output) {
33725
+ const result = [];
33726
+ const blocks = output.split(/\n\s*\n/);
33727
+ for (const block of blocks) {
33728
+ const reasonMatch = block.match(REASON_RE);
33729
+ if (!reasonMatch?.[1])
33730
+ continue;
33731
+ const reason = reasonMatch[1];
33732
+ if (reason === "prd_contract") {
33733
+ const file3 = readBlockField(block, "FILE");
33734
+ const prdQuote = readBlockField(block, "PRD_QUOTE");
33735
+ const testBefore = readBlockField(block, "TEST_BEFORE");
33736
+ const testAfter = readBlockField(block, "TEST_AFTER");
33737
+ if (!file3 || !prdQuote || !testBefore || !testAfter)
33738
+ continue;
33739
+ result.push({
33740
+ reason,
33741
+ file: file3,
33742
+ prdQuote: unwrapQuotes(prdQuote),
33743
+ testBefore,
33744
+ testAfter
33745
+ });
33746
+ } else if (reason === "lint_only") {
33747
+ const file3 = readBlockField(block, "FILE");
33748
+ const finding = readBlockField(block, "FINDING");
33749
+ if (!file3 || !finding)
33750
+ continue;
33751
+ result.push({ reason, file: file3, finding });
33752
+ } else if (reason === "sibling_scope") {
33753
+ const file3 = readBlockField(block, "SIBLING_FILE");
33754
+ const finding = readBlockField(block, "FINDING");
33755
+ if (!file3 || !finding)
33756
+ continue;
33757
+ result.push({ reason, file: file3, finding });
33758
+ }
33759
+ }
33760
+ return result;
33761
+ }
33762
+ function normaliseWs(s) {
33763
+ return s.replace(/\s+/g, " ").replace(/\s*([(),<>])\s*/g, "$1").replace(/\s*:\s*/g, ": ").trim();
33764
+ }
33765
+ function validatePrdQuote(prdQuote, story) {
33766
+ if (!prdQuote.trim())
33767
+ return false;
33768
+ const needle = normaliseWs(prdQuote);
33769
+ const haystack = normaliseWs([story.description, ...story.acceptanceCriteria].join(" "));
33770
+ return haystack.includes(needle);
33771
+ }
33772
+ var REASON_RE;
33773
+ var init_test_edit_declaration = __esm(() => {
33774
+ REASON_RE = /^TEST_EDIT_REASON:\s*(prd_contract|lint_only|sibling_scope)\s*$/m;
33775
+ });
33776
+
33673
33777
  // src/operations/autofix-implementer.ts
33674
33778
  var implementerRectifyOp;
33675
33779
  var init_autofix_implementer = __esm(() => {
33676
33780
  init_config();
33677
33781
  init_logger2();
33678
33782
  init_prompts();
33783
+ init_test_edit_declaration();
33679
33784
  implementerRectifyOp = {
33680
33785
  kind: "run",
33681
33786
  name: "autofix-implementer",
@@ -33690,15 +33795,20 @@ var init_autofix_implementer = __esm(() => {
33690
33795
  };
33691
33796
  },
33692
33797
  parse(output, input, _ctx) {
33693
- const match = output.match(/^UNRESOLVED:\s*(.+)$/ms);
33694
- const editReasonMatch = output.match(/TEST_EDIT_REASON:\s*(\w+)/);
33695
- if (editReasonMatch) {
33798
+ const unresolvedMatch = output.match(/^UNRESOLVED:\s*(.+)$/m);
33799
+ const declarations = parseTestEditDeclarations(output);
33800
+ for (const d of declarations) {
33696
33801
  getSafeLogger()?.info("autofix", "test_edit_declared", {
33697
33802
  storyId: input.story.id,
33698
- reason: editReasonMatch[1]
33803
+ reason: d.reason,
33804
+ file: d.file
33699
33805
  });
33700
33806
  }
33701
- return { applied: true, ...match ? { unresolvedReason: match[1]?.trim() } : {} };
33807
+ return {
33808
+ applied: true,
33809
+ testEditDeclarations: declarations,
33810
+ ...unresolvedMatch ? { unresolvedReason: unresolvedMatch[1]?.trim() } : {}
33811
+ };
33702
33812
  }
33703
33813
  };
33704
33814
  });
@@ -35142,6 +35252,7 @@ var init_operations = __esm(() => {
35142
35252
  init_adversarial_review();
35143
35253
  init_rectify();
35144
35254
  init_autofix_implementer();
35255
+ init_test_edit_declaration();
35145
35256
  init_autofix_test_writer();
35146
35257
  init_debate_propose();
35147
35258
  init_debate_rebut();
@@ -43417,7 +43528,7 @@ function collectAdversarialSourceChecks(ctx) {
43417
43528
  function buildAutofixStrategies(ctx, maxAttempts) {
43418
43529
  const implementer = {
43419
43530
  name: "autofix-implementer",
43420
- appliesTo: (f) => (f.fixTarget ?? "source") === "source",
43531
+ appliesTo: (f) => (f.fixTarget ?? "source") === "source" && f.category !== "prd_quote_mismatch",
43421
43532
  fixOp: implementerRectifyOp,
43422
43533
  maxAttempts,
43423
43534
  coRun: "co-run-sequential",
@@ -43425,16 +43536,22 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43425
43536
  failedChecks: collectFailedChecks(ctx),
43426
43537
  story: ctx.story
43427
43538
  }),
43428
- extractApplied: (output) => ({
43429
- summary: output.unresolvedReason ?? "",
43430
- unresolved: output.unresolvedReason
43431
- })
43539
+ extractApplied: (output) => {
43540
+ const decls = output.testEditDeclarations ?? [];
43541
+ if (decls.length > 0) {
43542
+ ctx.testEditDeclarations = [...ctx.testEditDeclarations ?? [], ...decls];
43543
+ }
43544
+ return {
43545
+ summary: output.unresolvedReason ?? "",
43546
+ unresolved: output.unresolvedReason
43547
+ };
43548
+ }
43432
43549
  };
43433
43550
  const testWriter = {
43434
43551
  name: "autofix-test-writer",
43435
43552
  appliesTo: (f) => f.fixTarget === "test" || (f.fixTarget ?? "source") === "source" && f.severity === "error" && f.source === "adversarial-review",
43436
43553
  fixOp: testWriterRectifyOp,
43437
- maxAttempts: 1,
43554
+ maxAttempts: 2,
43438
43555
  coRun: "co-run-sequential",
43439
43556
  buildInput: (findings, _prior, _cycleCtx) => {
43440
43557
  const hasSourceBug = findings.some((f) => (f.fixTarget ?? "source") === "source" && f.source === "adversarial-review");
@@ -43455,6 +43572,25 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43455
43572
  };
43456
43573
  return [testWriter, implementer];
43457
43574
  }
43575
+ function autofixCapacityExhausted(ctx) {
43576
+ const findings = collectCurrentFindings(ctx);
43577
+ if (findings.length === 0)
43578
+ return false;
43579
+ const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 3;
43580
+ const maxTotal = ctx.config.quality.autofix?.maxTotalAttempts ?? 12;
43581
+ const prior = ctx.autofixPriorIterations ?? [];
43582
+ const totalUsed = prior.reduce((sum, iter) => sum + iter.fixesApplied.length, 0);
43583
+ if (totalUsed >= maxTotal)
43584
+ return true;
43585
+ const strategies = buildAutofixStrategies(ctx, maxAttempts);
43586
+ const active = strategies.filter((s) => findings.some((f) => s.appliesTo(f)));
43587
+ if (active.length === 0)
43588
+ return true;
43589
+ return active.some((s) => {
43590
+ const used = prior.reduce((sum, iter) => sum + iter.fixesApplied.filter((fa) => fa.strategyName === s.name).length, 0);
43591
+ return used >= s.maxAttempts;
43592
+ });
43593
+ }
43458
43594
  function buildEscalationDigest(findings) {
43459
43595
  const byFile = new Map;
43460
43596
  for (const f of findings) {
@@ -43494,6 +43630,50 @@ async function writeShadowReport(ctx, result, initialFindingsCount) {
43494
43630
  });
43495
43631
  }
43496
43632
  }
43633
+ function applyTestEditDeclarations(findings, declarations, story) {
43634
+ if (declarations.length === 0)
43635
+ return findings;
43636
+ const out = [...findings];
43637
+ const originalLength = findings.length;
43638
+ const reTaggedKeys = new Set;
43639
+ for (const decl of declarations) {
43640
+ if (decl.reason !== "prd_contract")
43641
+ continue;
43642
+ if (!validatePrdQuote(decl.prdQuote ?? "", story)) {
43643
+ out.push({
43644
+ source: "adversarial-review",
43645
+ severity: "warning",
43646
+ category: "prd_quote_mismatch",
43647
+ message: `Implementer declared TEST_EDIT_REASON: prd_contract with PRD_QUOTE not found in story description or AC text: ${decl.prdQuote}`,
43648
+ file: decl.file,
43649
+ fixTarget: "source"
43650
+ });
43651
+ continue;
43652
+ }
43653
+ for (let i = 0;i < originalLength; i++) {
43654
+ if (reTaggedKeys.has(i))
43655
+ continue;
43656
+ if (out[i].file !== decl.file)
43657
+ continue;
43658
+ if ((out[i].fixTarget ?? "source") === "test")
43659
+ continue;
43660
+ out[i] = {
43661
+ ...out[i],
43662
+ fixTarget: "test",
43663
+ meta: {
43664
+ ...out[i].meta ?? {},
43665
+ prdContractDeclaration: {
43666
+ prdQuote: decl.prdQuote,
43667
+ testBefore: decl.testBefore,
43668
+ testAfter: decl.testAfter
43669
+ }
43670
+ }
43671
+ };
43672
+ reTaggedKeys.add(i);
43673
+ }
43674
+ }
43675
+ return out;
43676
+ }
43497
43677
  async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effectiveWorkdir) {
43498
43678
  const logger = getLogger();
43499
43679
  const storyId = ctx.story.id;
@@ -43517,7 +43697,18 @@ async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effecti
43517
43697
  },
43518
43698
  async validate(_cycleCtx) {
43519
43699
  await _autofixDeps.recheckReview(ctx);
43520
- return collectCurrentFindings(ctx);
43700
+ const fresh = collectCurrentFindings(ctx);
43701
+ const pending = ctx.testEditDeclarations ?? [];
43702
+ if (pending.length === 0)
43703
+ return fresh;
43704
+ const retagged = applyTestEditDeclarations(fresh, pending, ctx.story);
43705
+ ctx.testEditDeclarations = [];
43706
+ logger.info("autofix-cycle", "applied test-edit declarations", {
43707
+ storyId: ctx.story.id,
43708
+ declarationCount: pending.length,
43709
+ reTaggedCount: retagged.filter((f) => f.fixTarget === "test").length - fresh.filter((f) => f.fixTarget === "test").length
43710
+ });
43711
+ return retagged;
43521
43712
  }
43522
43713
  };
43523
43714
  const result = await runFixCycle(cycle, cycleCtx, "autofix-v2");
@@ -45170,6 +45361,42 @@ ${formatFindings2(blockingFindings)}`,
45170
45361
  };
45171
45362
  }
45172
45363
  if (!parsed.passed && blockingFindings.length === 0) {
45364
+ if (acDropped.length > 0) {
45365
+ const durationMs3 = Date.now() - startTime;
45366
+ logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
45367
+ storyId: story.id,
45368
+ durationMs: durationMs3,
45369
+ droppedCount: acDropped.length,
45370
+ dropCodes: acDropped.map((d) => d.code)
45371
+ });
45372
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
45373
+ `);
45374
+ recordAdversarialAudit({
45375
+ runtime,
45376
+ workdir,
45377
+ projectDir,
45378
+ storyId: story.id,
45379
+ featureName,
45380
+ parsed: true,
45381
+ failOpen: false,
45382
+ passed: false,
45383
+ blockingThreshold: threshold,
45384
+ result: { passed: false, findings: [] },
45385
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined
45386
+ });
45387
+ return {
45388
+ check: "adversarial",
45389
+ success: false,
45390
+ command: "",
45391
+ exitCode: 1,
45392
+ output: `Adversarial review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Either re-classify these as "info" upstream or extend the ACs. Drops:
45393
+
45394
+ ${dropSummary}`,
45395
+ durationMs: durationMs3,
45396
+ advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
45397
+ cost: llmCost
45398
+ };
45399
+ }
45173
45400
  const durationMs2 = Date.now() - startTime;
45174
45401
  logger?.info("review", "Adversarial review passed (all findings below blocking threshold)", {
45175
45402
  storyId: story.id,
@@ -46228,6 +46455,42 @@ ${formatFindings(blockingFindings)}`;
46228
46455
  };
46229
46456
  }
46230
46457
  if (!sanitizedParsed.passed && blockingFindings.length === 0) {
46458
+ if (acDropped.length > 0) {
46459
+ const durationMs3 = Date.now() - startTime;
46460
+ logger?.warn("review", "Semantic review fail-closed: blocking findings dropped as ungrounded", {
46461
+ storyId: story.id,
46462
+ durationMs: durationMs3,
46463
+ droppedCount: acDropped.length,
46464
+ dropCodes: acDropped.map((d) => d.code)
46465
+ });
46466
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
46467
+ `);
46468
+ recordSemanticAudit({
46469
+ runtime,
46470
+ workdir,
46471
+ projectDir,
46472
+ storyId: story.id,
46473
+ featureName,
46474
+ parsed: true,
46475
+ failOpen: false,
46476
+ passed: false,
46477
+ blockingThreshold: threshold,
46478
+ result: { passed: false, findings: [] },
46479
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
46480
+ });
46481
+ return {
46482
+ check: "semantic",
46483
+ success: false,
46484
+ command: "",
46485
+ exitCode: 1,
46486
+ output: `Semantic review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Either re-classify these as "info" upstream or extend the ACs. Drops:
46487
+
46488
+ ${dropSummary}`,
46489
+ durationMs: durationMs3,
46490
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
46491
+ cost: llmCost
46492
+ };
46493
+ }
46231
46494
  const durationMs2 = Date.now() - startTime;
46232
46495
  logger?.info("review", "Semantic review passed (all findings below blocking threshold)", {
46233
46496
  storyId: story.id,
@@ -47315,12 +47578,115 @@ async function recheckReview(ctx) {
47315
47578
  return false;
47316
47579
  return ctx.reviewResult?.success === true;
47317
47580
  }
47581
+ async function runMechanicalFixes(ctx, failedCheckNames) {
47582
+ const commands = resolveMechanicalFixCommands(ctx, failedCheckNames);
47583
+ for (const resolved of commands) {
47584
+ if (resolved.skipped)
47585
+ continue;
47586
+ pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: resolved.command });
47587
+ const result = await _autofixDeps.runQualityCommand({
47588
+ commandName: resolved.commandName,
47589
+ command: resolved.command,
47590
+ workdir: ctx.workdir,
47591
+ storyId: ctx.story.id
47592
+ });
47593
+ logMechanicalFixResult(ctx, resolved, result.exitCode);
47594
+ }
47595
+ }
47596
+ function resolveMechanicalFixCommands(ctx, failedCheckNames) {
47597
+ if (!failedCheckNames.has("lint"))
47598
+ return [];
47599
+ const scopeFiles = collectLintScopeFiles(ctx.reviewResult?.checks ?? []);
47600
+ return [resolveFixCommand(ctx, "lintFix", scopeFiles), resolveFixCommand(ctx, "formatFix", scopeFiles)].filter((cmd) => cmd !== undefined);
47601
+ }
47602
+ function resolveFixCommand(ctx, commandName, scopeFiles) {
47603
+ const broad = resolveBroadFixCommand(ctx, commandName);
47604
+ const template = resolveScopedFixTemplate(ctx, commandName);
47605
+ if (!broad && !template)
47606
+ return;
47607
+ if (!scopeFiles)
47608
+ return warnAndUseFullFix(ctx, commandName, broad, "missing_lint_scope");
47609
+ if (scopeFiles.length === 0)
47610
+ return logEmptyFixScope(ctx, commandName, broad ?? template ?? "");
47611
+ if (template) {
47612
+ return {
47613
+ commandName,
47614
+ command: template.replaceAll("{{files}}", scopeFiles.map(shellQuotePath2).join(" ")),
47615
+ scoped: true
47616
+ };
47617
+ }
47618
+ if (!broad)
47619
+ return;
47620
+ const derived = deriveScopedFixCommand(broad, scopeFiles);
47621
+ if (derived)
47622
+ return { commandName, command: derived, scoped: true };
47623
+ return warnAndUseFullFix(ctx, commandName, broad, "unsupported_scoped_command_shape");
47624
+ }
47625
+ function collectLintScopeFiles(checks3) {
47626
+ const lintChecks = checks3.filter((check2) => check2.check === "lint" && !check2.success);
47627
+ if (lintChecks.length === 0)
47628
+ return;
47629
+ if (lintChecks.some((check2) => !check2.lintScope))
47630
+ return;
47631
+ if (lintChecks.some((check2) => check2.lintScope?.status === "degraded"))
47632
+ return;
47633
+ const files = lintChecks.flatMap((check2) => check2.lintScope?.packageGroups.flatMap((group) => group.files) ?? []);
47634
+ return [...new Set(files)];
47635
+ }
47636
+ function resolveBroadFixCommand(ctx, commandName) {
47637
+ return commandName === "lintFix" ? ctx.config.quality.commands.lintFix ?? ctx.config.review.commands.lintFix : ctx.config.quality.commands.formatFix ?? ctx.config.review.commands.formatFix;
47638
+ }
47639
+ function hasMechanicalFixCommand(ctx) {
47640
+ return ["lintFix", "formatFix"].some((name) => resolveBroadFixCommand(ctx, name) ?? resolveScopedFixTemplate(ctx, name));
47641
+ }
47642
+ function resolveScopedFixTemplate(ctx, commandName) {
47643
+ return commandName === "lintFix" ? ctx.config.review.commands.lintFixScoped ?? ctx.config.quality.commands.lintFixScoped : ctx.config.review.commands.formatFixScoped ?? ctx.config.quality.commands.formatFixScoped;
47644
+ }
47645
+ function deriveScopedFixCommand(command, files) {
47646
+ const trimmed = command.trim();
47647
+ const supportedTools = ["eslint", "biome", "ruff", "flake8", "prettier"];
47648
+ const isSupported = supportedTools.some((tool) => trimmed === tool || trimmed.startsWith(`${tool} `)) || supportedTools.some((tool) => trimmed.startsWith(`bunx ${tool}`));
47649
+ if (!isSupported)
47650
+ return;
47651
+ return `${command} ${files.map(shellQuotePath2).join(" ")}`;
47652
+ }
47653
+ function shellQuotePath2(path9) {
47654
+ return `'${path9.replaceAll("'", "'\\''")}'`;
47655
+ }
47656
+ function logEmptyFixScope(ctx, commandName, command) {
47657
+ getLogger().info("autofix", `${toScopeLogPrefix(commandName)}_scope_empty`, { storyId: ctx.story.id });
47658
+ return { commandName, command, scoped: true, skipped: true };
47659
+ }
47660
+ function warnAndUseFullFix(ctx, commandName, command, reason) {
47661
+ getLogger().warn("autofix", `${toScopeLogPrefix(commandName)}_scope_degraded`, { storyId: ctx.story.id, reason });
47662
+ if (!command)
47663
+ return { commandName, command: "", scoped: false, skipped: true };
47664
+ return { commandName, command, scoped: false };
47665
+ }
47666
+ function toScopeLogPrefix(commandName) {
47667
+ return commandName === "lintFix" ? "lint_fix" : "format_fix";
47668
+ }
47669
+ function logMechanicalFixResult(ctx, resolved, exitCode) {
47670
+ const logger = getLogger();
47671
+ logger.debug("autofix", `${resolved.commandName} exit=${exitCode}`, {
47672
+ storyId: ctx.story.id,
47673
+ command: resolved.command,
47674
+ scoped: resolved.scoped
47675
+ });
47676
+ if (exitCode !== 0) {
47677
+ logger.warn("autofix", `${resolved.commandName} command failed \u2014 may not have fixed all issues`, {
47678
+ storyId: ctx.story.id,
47679
+ exitCode
47680
+ });
47681
+ }
47682
+ }
47318
47683
  var NON_FIXABLE_BY_RECTIFICATION, autofixStage, _autofixDeps;
47319
47684
  var init_autofix = __esm(() => {
47320
47685
  init_logger2();
47321
47686
  init_quality();
47322
47687
  init_event_bus();
47323
47688
  init_autofix_agent();
47689
+ init_autofix_cycle();
47324
47690
  init_autofix_scope_split();
47325
47691
  init_autofix_test_writer2();
47326
47692
  NON_FIXABLE_BY_RECTIFICATION = new Set(["git-clean"]);
@@ -47345,8 +47711,7 @@ var init_autofix = __esm(() => {
47345
47711
  if (!reviewResult || reviewResult.success) {
47346
47712
  return { action: "continue" };
47347
47713
  }
47348
- const lintFixCmd = ctx.config.quality.commands.lintFix ?? ctx.config.review.commands.lintFix;
47349
- const formatFixCmd = ctx.config.quality.commands.formatFix ?? ctx.config.review.commands.formatFix;
47714
+ ctx.autofixAttempt = (ctx.autofixAttempt ?? 0) + 1;
47350
47715
  const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
47351
47716
  const hasLintFailure = failedCheckNames.has("lint");
47352
47717
  const totalFindingCount = (reviewResult.checks ?? []).reduce((n, c) => n + (c.findings?.length ?? 0), 0);
@@ -47367,42 +47732,8 @@ var init_autofix = __esm(() => {
47367
47732
  failedChecks: [...failedCheckNames],
47368
47733
  workdir: ctx.workdir
47369
47734
  });
47370
- if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
47371
- if (lintFixCmd) {
47372
- pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
47373
- const lintResult = await _autofixDeps.runQualityCommand({
47374
- commandName: "lintFix",
47375
- command: lintFixCmd,
47376
- workdir: ctx.workdir,
47377
- storyId: ctx.story.id
47378
- });
47379
- logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
47380
- if (lintResult.exitCode !== 0) {
47381
- logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
47382
- storyId: ctx.story.id,
47383
- exitCode: lintResult.exitCode
47384
- });
47385
- }
47386
- }
47387
- if (formatFixCmd) {
47388
- pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
47389
- const fmtResult = await _autofixDeps.runQualityCommand({
47390
- commandName: "formatFix",
47391
- command: formatFixCmd,
47392
- workdir: ctx.workdir,
47393
- storyId: ctx.story.id
47394
- });
47395
- logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
47396
- storyId: ctx.story.id,
47397
- command: formatFixCmd
47398
- });
47399
- if (fmtResult.exitCode !== 0) {
47400
- logger.warn("autofix", "formatFix command failed \u2014 may not have fixed all issues", {
47401
- storyId: ctx.story.id,
47402
- exitCode: fmtResult.exitCode
47403
- });
47404
- }
47405
- }
47735
+ if (hasLintFailure && hasMechanicalFixCommand(ctx)) {
47736
+ await runMechanicalFixes(ctx, failedCheckNames);
47406
47737
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
47407
47738
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
47408
47739
  if (recheckPassed) {
@@ -47445,7 +47776,7 @@ var init_autofix = __esm(() => {
47445
47776
  cost: agentCost,
47446
47777
  unresolvedReason,
47447
47778
  escalationDigest
47448
- } = await _autofixDeps.runAgentRectification(ctx, lintFixCmd, formatFixCmd, ctx.workdir);
47779
+ } = await _autofixDeps.runAgentRectification(ctx, resolveBroadFixCommand(ctx, "lintFix"), resolveBroadFixCommand(ctx, "formatFix"), ctx.workdir);
47449
47780
  if (!agentFixed && unresolvedReason) {
47450
47781
  if (ctx.mechanicalFailedOnly) {
47451
47782
  logger.warn("autofix", "Mechanical-only failure unfixable \u2014 proceeding (LLM review passed)", {
@@ -47480,7 +47811,8 @@ var init_autofix = __esm(() => {
47480
47811
  const totalUsed = ctx.autofixAttempt ?? 0;
47481
47812
  const currentlyFailing = new Set((ctx.reviewResult?.checks ?? []).filter((c) => !c.success || c.failOpen).map((c) => c.check));
47482
47813
  const nowPassing = [...failedCheckNames].filter((c) => !currentlyFailing.has(c));
47483
- if (nowPassing.length > 0 && totalUsed < maxTotal) {
47814
+ const capacityExhausted = autofixCapacityExhausted(ctx);
47815
+ if (nowPassing.length > 0 && totalUsed < maxTotal && !capacityExhausted) {
47484
47816
  ctx.retrySkipChecks = new Set([...ctx.retrySkipChecks ?? [], ...nowPassing]);
47485
47817
  logger.info("autofix", "Partial progress \u2014 retrying review with updated skip list", {
47486
47818
  storyId: ctx.story.id,
@@ -47490,6 +47822,13 @@ var init_autofix = __esm(() => {
47490
47822
  });
47491
47823
  return { action: "retry", fromStage: "review", cost: agentCost };
47492
47824
  }
47825
+ if (nowPassing.length > 0 && capacityExhausted) {
47826
+ logger.info("autofix", "Partial progress \u2014 but autofix capacity exhausted; escalating instead of retrying review", {
47827
+ storyId: ctx.story.id,
47828
+ nowPassing,
47829
+ remaining: [...currentlyFailing]
47830
+ });
47831
+ }
47493
47832
  logger.warn("autofix", "Autofix exhausted \u2014 escalating", { storyId: ctx.story.id });
47494
47833
  return {
47495
47834
  action: "escalate",
@@ -54286,7 +54625,7 @@ var package_default;
54286
54625
  var init_package = __esm(() => {
54287
54626
  package_default = {
54288
54627
  name: "@nathapp/nax",
54289
- version: "0.65.1",
54628
+ version: "0.65.2",
54290
54629
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
54291
54630
  type: "module",
54292
54631
  bin: {
@@ -54372,8 +54711,8 @@ var init_version = __esm(() => {
54372
54711
  NAX_VERSION = package_default.version;
54373
54712
  NAX_COMMIT = (() => {
54374
54713
  try {
54375
- if (/^[0-9a-f]{6,10}$/.test("3aa1ff6b"))
54376
- return "3aa1ff6b";
54714
+ if (/^[0-9a-f]{6,10}$/.test("99828ef9"))
54715
+ return "99828ef9";
54377
54716
  } catch {}
54378
54717
  try {
54379
54718
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -90724,6 +91063,7 @@ __export(exports_curator, {
90724
91063
  _curatorCmdDeps: () => _curatorCmdDeps
90725
91064
  });
90726
91065
  import { readdirSync as readdirSync10 } from "fs";
91066
+ import { unlink as unlink4 } from "fs/promises";
90727
91067
  import { basename as basename16, join as join79 } from "path";
90728
91068
  function getProjectKey(config2, projectDir) {
90729
91069
  return config2.name?.trim() || basename16(projectDir);
@@ -90994,6 +91334,16 @@ async function curatorGc(options) {
90994
91334
  `)}
90995
91335
  `;
90996
91336
  await _curatorCmdDeps.writeFile(rollupPath, newContent);
91337
+ const projectKey = getProjectKey(config2, resolved.projectDir);
91338
+ const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
91339
+ const perRunsDir = join79(outputDir, "runs");
91340
+ for (const runId of uniqueRunIds) {
91341
+ if (!keepSet.has(runId)) {
91342
+ const runDir = join79(perRunsDir, runId);
91343
+ await _curatorCmdDeps.removeFile(join79(runDir, "observations.jsonl"));
91344
+ await _curatorCmdDeps.removeFile(join79(runDir, "curator-proposals.md"));
91345
+ }
91346
+ }
90997
91347
  console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
90998
91348
  }
90999
91349
  var _curatorCmdDeps, DEFAULT_KEEP = 50;
@@ -91017,6 +91367,11 @@ var init_curator2 = __esm(() => {
91017
91367
  const prev = await existing.exists() ? await existing.text() : "";
91018
91368
  await Bun.write(p, prev + content);
91019
91369
  },
91370
+ removeFile: async (p) => {
91371
+ try {
91372
+ await unlink4(p);
91373
+ } catch {}
91374
+ },
91020
91375
  openInEditor: async (filePath) => {
91021
91376
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
91022
91377
  const proc = Bun.spawnSync([editor, filePath], { stdio: ["inherit", "inherit", "inherit"] });
@@ -92516,6 +92871,8 @@ var FIELD_DESCRIPTIONS = {
92516
92871
  "quality.commands.typecheck": "Custom typecheck command",
92517
92872
  "quality.commands.lint": "Custom lint command",
92518
92873
  "quality.commands.lintScoped": "Scoped lint command template for story-owned files (supports {{files}})",
92874
+ "quality.commands.lintFixScoped": "Scoped lint fix command template for story-owned files (supports {{files}})",
92875
+ "quality.commands.formatFixScoped": "Scoped format fix command template for story-owned files (supports {{files}})",
92519
92876
  "quality.commands.test": "Custom test command",
92520
92877
  "quality.commands.build": "Custom build command",
92521
92878
  "quality.forceExit": "Append --forceExit to test command (prevents hangs)",
@@ -92549,6 +92906,8 @@ var FIELD_DESCRIPTIONS = {
92549
92906
  "review.commands.typecheck": "Custom typecheck command for review",
92550
92907
  "review.commands.lint": "Custom lint command for review",
92551
92908
  "review.commands.lintScoped": "Scoped lint command template for review (supports {{files}})",
92909
+ "review.commands.lintFixScoped": "Scoped lint fix command template for review (supports {{files}})",
92910
+ "review.commands.formatFixScoped": "Scoped format fix command template for review (supports {{files}})",
92552
92911
  "review.commands.test": "Custom test command for review",
92553
92912
  "review.commands.build": "Custom build command for review",
92554
92913
  "review.semantic": "Semantic review configuration (code quality analysis)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.65.1",
3
+ "version": "0.65.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {