@nathapp/nax 0.65.0 → 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 +659 -94
  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();
@@ -40710,6 +40821,9 @@ var init_checks_cli = __esm(() => {
40710
40821
 
40711
40822
  // src/precheck/checks-system.ts
40712
40823
  import { existsSync as existsSync12, statSync as statSync4 } from "fs";
40824
+ function discoverCanonicalRuleRoots(workdir) {
40825
+ return [workdir];
40826
+ }
40713
40827
  async function checkDependenciesInstalled(workdir) {
40714
40828
  const depPaths = [
40715
40829
  { path: "node_modules" },
@@ -40787,7 +40901,47 @@ async function checkTypecheckCommand(config2) {
40787
40901
  message: `Typecheck command configured: ${typecheckCommand}`
40788
40902
  };
40789
40903
  }
40790
- var init_checks_system = () => {};
40904
+ async function checkCanonicalRulesLint(workdir) {
40905
+ const roots = _checkCanonicalRulesDeps.discoverRoots(workdir);
40906
+ let ruleCount = 0;
40907
+ try {
40908
+ for (const root of roots) {
40909
+ const rules = await _checkCanonicalRulesDeps.loadCanonicalRules(root);
40910
+ ruleCount += rules.length;
40911
+ }
40912
+ } catch (err) {
40913
+ if (err instanceof NeutralityLintError) {
40914
+ const first = err.violations[0];
40915
+ const detail = first ? `${first.file}:${first.lineNumber} (${first.ruleId})` : "unknown location";
40916
+ return {
40917
+ name: "canonical-rules-lint",
40918
+ tier: "blocker",
40919
+ passed: false,
40920
+ message: `Canonical rules lint failed (${err.violations.length} violation(s)): ${detail}`
40921
+ };
40922
+ }
40923
+ return {
40924
+ name: "canonical-rules-lint",
40925
+ tier: "blocker",
40926
+ passed: false,
40927
+ message: `Canonical rules lint failed: ${errorMessage(err)}`
40928
+ };
40929
+ }
40930
+ return {
40931
+ name: "canonical-rules-lint",
40932
+ tier: "blocker",
40933
+ passed: true,
40934
+ message: `Canonical rules lint passed (${ruleCount} file(s) across ${roots.length} root(s))`
40935
+ };
40936
+ }
40937
+ var _checkCanonicalRulesDeps;
40938
+ var init_checks_system = __esm(() => {
40939
+ init_canonical_loader();
40940
+ _checkCanonicalRulesDeps = {
40941
+ discoverRoots: discoverCanonicalRuleRoots,
40942
+ loadCanonicalRules
40943
+ };
40944
+ });
40791
40945
 
40792
40946
  // src/precheck/checks-blockers.ts
40793
40947
  var init_checks_blockers = __esm(() => {
@@ -41255,6 +41409,7 @@ function getLateEnvironmentBlockers(config2, workdir) {
41255
41409
  () => checkTestCommand(config2),
41256
41410
  () => checkLintCommand(config2),
41257
41411
  () => checkTypecheckCommand(config2),
41412
+ () => checkCanonicalRulesLint(workdir),
41258
41413
  () => checkGitUserConfigured(workdir)
41259
41414
  ];
41260
41415
  }
@@ -43373,7 +43528,7 @@ function collectAdversarialSourceChecks(ctx) {
43373
43528
  function buildAutofixStrategies(ctx, maxAttempts) {
43374
43529
  const implementer = {
43375
43530
  name: "autofix-implementer",
43376
- appliesTo: (f) => (f.fixTarget ?? "source") === "source",
43531
+ appliesTo: (f) => (f.fixTarget ?? "source") === "source" && f.category !== "prd_quote_mismatch",
43377
43532
  fixOp: implementerRectifyOp,
43378
43533
  maxAttempts,
43379
43534
  coRun: "co-run-sequential",
@@ -43381,16 +43536,22 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43381
43536
  failedChecks: collectFailedChecks(ctx),
43382
43537
  story: ctx.story
43383
43538
  }),
43384
- extractApplied: (output) => ({
43385
- summary: output.unresolvedReason ?? "",
43386
- unresolved: output.unresolvedReason
43387
- })
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
+ }
43388
43549
  };
43389
43550
  const testWriter = {
43390
43551
  name: "autofix-test-writer",
43391
43552
  appliesTo: (f) => f.fixTarget === "test" || (f.fixTarget ?? "source") === "source" && f.severity === "error" && f.source === "adversarial-review",
43392
43553
  fixOp: testWriterRectifyOp,
43393
- maxAttempts: 1,
43554
+ maxAttempts: 2,
43394
43555
  coRun: "co-run-sequential",
43395
43556
  buildInput: (findings, _prior, _cycleCtx) => {
43396
43557
  const hasSourceBug = findings.some((f) => (f.fixTarget ?? "source") === "source" && f.source === "adversarial-review");
@@ -43411,6 +43572,25 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43411
43572
  };
43412
43573
  return [testWriter, implementer];
43413
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
+ }
43414
43594
  function buildEscalationDigest(findings) {
43415
43595
  const byFile = new Map;
43416
43596
  for (const f of findings) {
@@ -43450,6 +43630,50 @@ async function writeShadowReport(ctx, result, initialFindingsCount) {
43450
43630
  });
43451
43631
  }
43452
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
+ }
43453
43677
  async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effectiveWorkdir) {
43454
43678
  const logger = getLogger();
43455
43679
  const storyId = ctx.story.id;
@@ -43473,7 +43697,18 @@ async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effecti
43473
43697
  },
43474
43698
  async validate(_cycleCtx) {
43475
43699
  await _autofixDeps.recheckReview(ctx);
43476
- 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;
43477
43712
  }
43478
43713
  };
43479
43714
  const result = await runFixCycle(cycle, cycleCtx, "autofix-v2");
@@ -44712,6 +44947,109 @@ var init_diff_utils = __esm(() => {
44712
44947
  DEFAULT_SUFFIX_STRIPPERS = [/\.(test|spec)\.(ts|js|tsx|jsx)$/, /_test\.go$/];
44713
44948
  });
44714
44949
 
44950
+ // src/review/finding-projection.ts
44951
+ function narrowSeverity(raw) {
44952
+ return SEVERITY_MAP[raw] ?? "info";
44953
+ }
44954
+ function slugLeadingTokens(text, tokenCount = RULE_ID_SLUG_TOKENS) {
44955
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter(Boolean).slice(0, tokenCount).join("-");
44956
+ }
44957
+ function deriveRuleId(category, issue2) {
44958
+ const prefix = category?.trim() ? category.trim() : "review";
44959
+ const slug = slugLeadingTokens(issue2) || "unspecified";
44960
+ return `${prefix}:${slug}`;
44961
+ }
44962
+ function joinMessage(issue2, suggestion) {
44963
+ const trimmedIssue = issue2.trim();
44964
+ const trimmedSuggestion = (suggestion ?? "").trim();
44965
+ if (trimmedIssue && trimmedSuggestion) {
44966
+ return `${trimmedIssue}
44967
+ \u2192 ${trimmedSuggestion}`;
44968
+ }
44969
+ return trimmedIssue;
44970
+ }
44971
+ function buildMeta(f, originalSeverity) {
44972
+ const meta3 = {};
44973
+ if (f.issue)
44974
+ meta3.issue = f.issue;
44975
+ if (f.suggestion)
44976
+ meta3.suggestion = f.suggestion;
44977
+ if (f.acQuote)
44978
+ meta3.acQuote = f.acQuote;
44979
+ if (typeof f.acIndex === "number" && f.acIndex >= 1)
44980
+ meta3.acIndex = f.acIndex;
44981
+ if ("acId" in f && f.acId)
44982
+ meta3.acId = f.acId;
44983
+ if ("verifiedBy" in f && f.verifiedBy)
44984
+ meta3.verifiedBy = f.verifiedBy;
44985
+ if (originalSeverity !== undefined)
44986
+ meta3.originalSeverity = originalSeverity;
44987
+ return Object.keys(meta3).length > 0 ? meta3 : undefined;
44988
+ }
44989
+ function findingCategory(f) {
44990
+ return "category" in f && f.category ? f.category : undefined;
44991
+ }
44992
+ function llmFindingToReviewFinding(f, opts = {}) {
44993
+ const category = findingCategory(f);
44994
+ const narrowed = narrowSeverity(f.severity);
44995
+ const result = {
44996
+ ruleId: deriveRuleId(category, f.issue),
44997
+ severity: narrowed,
44998
+ file: f.file,
44999
+ line: f.line,
45000
+ message: joinMessage(f.issue, f.suggestion)
45001
+ };
45002
+ if (category)
45003
+ result.category = category;
45004
+ if (opts.source)
45005
+ result.source = opts.source;
45006
+ const meta3 = buildMeta(f, f.severity !== narrowed ? f.severity : undefined);
45007
+ if (meta3)
45008
+ result.meta = meta3;
45009
+ return result;
45010
+ }
45011
+ function llmFindingsToReviewFindings(findings, opts = {}) {
45012
+ return findings.map((f) => llmFindingToReviewFinding(f, opts));
45013
+ }
45014
+ function findingToReviewFinding(f, opts = {}) {
45015
+ const narrowed = narrowSeverity(f.severity);
45016
+ const ruleId = f.rule?.trim() ? f.rule.trim() : deriveRuleId(f.category, f.message);
45017
+ const result = {
45018
+ ruleId,
45019
+ severity: narrowed,
45020
+ file: f.file ?? "",
45021
+ line: f.line ?? 0,
45022
+ message: f.message
45023
+ };
45024
+ if (f.category)
45025
+ result.category = f.category;
45026
+ const effectiveSource = opts.source ?? f.source;
45027
+ if (effectiveSource)
45028
+ result.source = effectiveSource;
45029
+ const meta3 = { ...f.meta ?? {} };
45030
+ if (f.suggestion)
45031
+ meta3.suggestion = f.suggestion;
45032
+ if (f.severity !== narrowed)
45033
+ meta3.originalSeverity = f.severity;
45034
+ if (Object.keys(meta3).length > 0)
45035
+ result.meta = meta3;
45036
+ return result;
45037
+ }
45038
+ function findingsToReviewFindings(findings, opts = {}) {
45039
+ return findings.map((f) => findingToReviewFinding(f, opts));
45040
+ }
45041
+ var SEVERITY_MAP, RULE_ID_SLUG_TOKENS = 6;
45042
+ var init_finding_projection = __esm(() => {
45043
+ SEVERITY_MAP = {
45044
+ critical: "critical",
45045
+ error: "error",
45046
+ warning: "warning",
45047
+ warn: "warning",
45048
+ info: "info",
45049
+ low: "low"
45050
+ };
45051
+ });
45052
+
44715
45053
  // src/review/adversarial.ts
44716
45054
  import { relative as relative11, sep as sep3 } from "path";
44717
45055
  function recordAdversarialAudit(opts) {
@@ -45002,8 +45340,11 @@ async function runAdversarialReview(opts) {
45002
45340
  failOpen: false,
45003
45341
  passed: false,
45004
45342
  blockingThreshold: threshold,
45005
- result: { passed: false, findings: parsed.findings },
45006
- advisoryFindings
45343
+ result: {
45344
+ passed: false,
45345
+ findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
45346
+ },
45347
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined
45007
45348
  });
45008
45349
  return {
45009
45350
  check: "adversarial",
@@ -45020,6 +45361,42 @@ ${formatFindings2(blockingFindings)}`,
45020
45361
  };
45021
45362
  }
45022
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
+ }
45023
45400
  const durationMs2 = Date.now() - startTime;
45024
45401
  logger?.info("review", "Adversarial review passed (all findings below blocking threshold)", {
45025
45402
  storyId: story.id,
@@ -45035,8 +45412,11 @@ ${formatFindings2(blockingFindings)}`,
45035
45412
  failOpen: false,
45036
45413
  passed: true,
45037
45414
  blockingThreshold: threshold,
45038
- result: { passed: true, findings: parsed.findings },
45039
- advisoryFindings
45415
+ result: {
45416
+ passed: true,
45417
+ findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
45418
+ },
45419
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined
45040
45420
  });
45041
45421
  return {
45042
45422
  check: "adversarial",
@@ -45063,8 +45443,11 @@ ${formatFindings2(blockingFindings)}`,
45063
45443
  failOpen: false,
45064
45444
  passed: parsed.passed,
45065
45445
  blockingThreshold: threshold,
45066
- result: { passed: parsed.passed, findings: parsed.findings },
45067
- advisoryFindings
45446
+ result: {
45447
+ passed: parsed.passed,
45448
+ findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
45449
+ },
45450
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined
45068
45451
  });
45069
45452
  return {
45070
45453
  check: "adversarial",
@@ -45089,6 +45472,7 @@ var init_adversarial = __esm(() => {
45089
45472
  init_ac_quote_validator();
45090
45473
  init_adversarial_helpers();
45091
45474
  init_diff_utils();
45475
+ init_finding_projection();
45092
45476
  init_review_audit();
45093
45477
  _adversarialDeps = {
45094
45478
  writeReviewAudit,
@@ -45481,7 +45865,10 @@ async function runSemanticDebate(opts) {
45481
45865
  parsed: true,
45482
45866
  passed: false,
45483
45867
  blockingThreshold,
45484
- result: { passed: false, findings }
45868
+ result: {
45869
+ passed: false,
45870
+ findings: findingsToReviewFindings(findings, { source: "semantic-debate-review" })
45871
+ }
45485
45872
  });
45486
45873
  return {
45487
45874
  check: "semantic",
@@ -45507,7 +45894,10 @@ ${findings.map((f) => `${f.rule ?? "semantic"}: ${f.message}`).join(`
45507
45894
  parsed: true,
45508
45895
  passed: true,
45509
45896
  blockingThreshold,
45510
- result: { passed: true, findings }
45897
+ result: {
45898
+ passed: true,
45899
+ findings: findingsToReviewFindings(findings, { source: "semantic-debate-review" })
45900
+ }
45511
45901
  });
45512
45902
  return {
45513
45903
  check: "semantic",
@@ -45567,8 +45957,11 @@ ${findings.map((f) => `${f.rule ?? "semantic"}: ${f.message}`).join(`
45567
45957
  parsed: true,
45568
45958
  passed: false,
45569
45959
  blockingThreshold: debateThreshold,
45570
- result: { passed: false, findings: debateFindings },
45571
- advisoryFindings: debateAdvisory
45960
+ result: {
45961
+ passed: false,
45962
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
45963
+ },
45964
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
45572
45965
  });
45573
45966
  return {
45574
45967
  check: "semantic",
@@ -45596,8 +45989,11 @@ ${formatFindings(debateBlocking)}`,
45596
45989
  parsed: true,
45597
45990
  passed: true,
45598
45991
  blockingThreshold: debateThreshold,
45599
- result: { passed: true, findings: debateFindings },
45600
- advisoryFindings: debateAdvisory
45992
+ result: {
45993
+ passed: true,
45994
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
45995
+ },
45996
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
45601
45997
  });
45602
45998
  return {
45603
45999
  check: "semantic",
@@ -45619,8 +46015,11 @@ ${formatFindings(debateBlocking)}`,
45619
46015
  parsed: true,
45620
46016
  passed: true,
45621
46017
  blockingThreshold: debateThreshold,
45622
- result: { passed: true, findings: debateFindings },
45623
- advisoryFindings: debateAdvisory
46018
+ result: {
46019
+ passed: true,
46020
+ findings: llmFindingsToReviewFindings(debateFindings, { source: "semantic-debate-review" })
46021
+ },
46022
+ advisoryFindings: debateAdvisory.length > 0 ? llmFindingsToReviewFindings(debateAdvisory, { source: "semantic-debate-review" }) : undefined
45624
46023
  });
45625
46024
  return {
45626
46025
  check: "semantic",
@@ -45636,6 +46035,7 @@ ${formatFindings(debateBlocking)}`,
45636
46035
  var init_semantic_debate = __esm(() => {
45637
46036
  init_logger2();
45638
46037
  init_ac_quote_validator();
46038
+ init_finding_projection();
45639
46039
  init_semantic_helpers();
45640
46040
  });
45641
46041
 
@@ -46036,8 +46436,11 @@ ${formatFindings(blockingFindings)}`;
46036
46436
  failOpen: false,
46037
46437
  passed: false,
46038
46438
  blockingThreshold: threshold,
46039
- result: { passed: false, findings: sanitizedParsed.findings },
46040
- advisoryFindings
46439
+ result: {
46440
+ passed: false,
46441
+ findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
46442
+ },
46443
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
46041
46444
  });
46042
46445
  return {
46043
46446
  check: "semantic",
@@ -46052,6 +46455,42 @@ ${formatFindings(blockingFindings)}`;
46052
46455
  };
46053
46456
  }
46054
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
+ }
46055
46494
  const durationMs2 = Date.now() - startTime;
46056
46495
  logger?.info("review", "Semantic review passed (all findings below blocking threshold)", {
46057
46496
  storyId: story.id,
@@ -46067,8 +46506,11 @@ ${formatFindings(blockingFindings)}`;
46067
46506
  failOpen: false,
46068
46507
  passed: true,
46069
46508
  blockingThreshold: threshold,
46070
- result: { passed: true, findings: sanitizedParsed.findings },
46071
- advisoryFindings
46509
+ result: {
46510
+ passed: true,
46511
+ findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
46512
+ },
46513
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
46072
46514
  });
46073
46515
  return {
46074
46516
  check: "semantic",
@@ -46095,8 +46537,11 @@ ${formatFindings(blockingFindings)}`;
46095
46537
  failOpen: false,
46096
46538
  passed: sanitizedParsed.passed,
46097
46539
  blockingThreshold: threshold,
46098
- result: { passed: sanitizedParsed.passed, findings: sanitizedParsed.findings },
46099
- advisoryFindings
46540
+ result: {
46541
+ passed: sanitizedParsed.passed,
46542
+ findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
46543
+ },
46544
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
46100
46545
  });
46101
46546
  return {
46102
46547
  check: "semantic",
@@ -46122,6 +46567,7 @@ var init_semantic = __esm(() => {
46122
46567
  init_test_runners();
46123
46568
  init_ac_quote_validator();
46124
46569
  init_diff_utils();
46570
+ init_finding_projection();
46125
46571
  init_review_audit();
46126
46572
  init_semantic_debate();
46127
46573
  init_semantic_evidence();
@@ -47132,12 +47578,115 @@ async function recheckReview(ctx) {
47132
47578
  return false;
47133
47579
  return ctx.reviewResult?.success === true;
47134
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
+ }
47135
47683
  var NON_FIXABLE_BY_RECTIFICATION, autofixStage, _autofixDeps;
47136
47684
  var init_autofix = __esm(() => {
47137
47685
  init_logger2();
47138
47686
  init_quality();
47139
47687
  init_event_bus();
47140
47688
  init_autofix_agent();
47689
+ init_autofix_cycle();
47141
47690
  init_autofix_scope_split();
47142
47691
  init_autofix_test_writer2();
47143
47692
  NON_FIXABLE_BY_RECTIFICATION = new Set(["git-clean"]);
@@ -47162,8 +47711,7 @@ var init_autofix = __esm(() => {
47162
47711
  if (!reviewResult || reviewResult.success) {
47163
47712
  return { action: "continue" };
47164
47713
  }
47165
- const lintFixCmd = ctx.config.quality.commands.lintFix ?? ctx.config.review.commands.lintFix;
47166
- const formatFixCmd = ctx.config.quality.commands.formatFix ?? ctx.config.review.commands.formatFix;
47714
+ ctx.autofixAttempt = (ctx.autofixAttempt ?? 0) + 1;
47167
47715
  const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
47168
47716
  const hasLintFailure = failedCheckNames.has("lint");
47169
47717
  const totalFindingCount = (reviewResult.checks ?? []).reduce((n, c) => n + (c.findings?.length ?? 0), 0);
@@ -47184,42 +47732,8 @@ var init_autofix = __esm(() => {
47184
47732
  failedChecks: [...failedCheckNames],
47185
47733
  workdir: ctx.workdir
47186
47734
  });
47187
- if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
47188
- if (lintFixCmd) {
47189
- pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
47190
- const lintResult = await _autofixDeps.runQualityCommand({
47191
- commandName: "lintFix",
47192
- command: lintFixCmd,
47193
- workdir: ctx.workdir,
47194
- storyId: ctx.story.id
47195
- });
47196
- logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
47197
- if (lintResult.exitCode !== 0) {
47198
- logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
47199
- storyId: ctx.story.id,
47200
- exitCode: lintResult.exitCode
47201
- });
47202
- }
47203
- }
47204
- if (formatFixCmd) {
47205
- pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
47206
- const fmtResult = await _autofixDeps.runQualityCommand({
47207
- commandName: "formatFix",
47208
- command: formatFixCmd,
47209
- workdir: ctx.workdir,
47210
- storyId: ctx.story.id
47211
- });
47212
- logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
47213
- storyId: ctx.story.id,
47214
- command: formatFixCmd
47215
- });
47216
- if (fmtResult.exitCode !== 0) {
47217
- logger.warn("autofix", "formatFix command failed \u2014 may not have fixed all issues", {
47218
- storyId: ctx.story.id,
47219
- exitCode: fmtResult.exitCode
47220
- });
47221
- }
47222
- }
47735
+ if (hasLintFailure && hasMechanicalFixCommand(ctx)) {
47736
+ await runMechanicalFixes(ctx, failedCheckNames);
47223
47737
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
47224
47738
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
47225
47739
  if (recheckPassed) {
@@ -47262,7 +47776,7 @@ var init_autofix = __esm(() => {
47262
47776
  cost: agentCost,
47263
47777
  unresolvedReason,
47264
47778
  escalationDigest
47265
- } = await _autofixDeps.runAgentRectification(ctx, lintFixCmd, formatFixCmd, ctx.workdir);
47779
+ } = await _autofixDeps.runAgentRectification(ctx, resolveBroadFixCommand(ctx, "lintFix"), resolveBroadFixCommand(ctx, "formatFix"), ctx.workdir);
47266
47780
  if (!agentFixed && unresolvedReason) {
47267
47781
  if (ctx.mechanicalFailedOnly) {
47268
47782
  logger.warn("autofix", "Mechanical-only failure unfixable \u2014 proceeding (LLM review passed)", {
@@ -47297,7 +47811,8 @@ var init_autofix = __esm(() => {
47297
47811
  const totalUsed = ctx.autofixAttempt ?? 0;
47298
47812
  const currentlyFailing = new Set((ctx.reviewResult?.checks ?? []).filter((c) => !c.success || c.failOpen).map((c) => c.check));
47299
47813
  const nowPassing = [...failedCheckNames].filter((c) => !currentlyFailing.has(c));
47300
- if (nowPassing.length > 0 && totalUsed < maxTotal) {
47814
+ const capacityExhausted = autofixCapacityExhausted(ctx);
47815
+ if (nowPassing.length > 0 && totalUsed < maxTotal && !capacityExhausted) {
47301
47816
  ctx.retrySkipChecks = new Set([...ctx.retrySkipChecks ?? [], ...nowPassing]);
47302
47817
  logger.info("autofix", "Partial progress \u2014 retrying review with updated skip list", {
47303
47818
  storyId: ctx.story.id,
@@ -47307,6 +47822,13 @@ var init_autofix = __esm(() => {
47307
47822
  });
47308
47823
  return { action: "retry", fromStage: "review", cost: agentCost };
47309
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
+ }
47310
47832
  logger.warn("autofix", "Autofix exhausted \u2014 escalating", { storyId: ctx.story.id });
47311
47833
  return {
47312
47834
  action: "escalate",
@@ -52583,7 +53105,18 @@ async function collectFromMetrics(context) {
52583
53105
  return observations;
52584
53106
  }
52585
53107
  function findingRuleId(finding) {
52586
- return stringValue(finding.rule ?? finding.ruleId ?? finding.checkId ?? finding.category, "unknown");
53108
+ return stringValue(finding.ruleId ?? finding.rule ?? finding.checkId ?? finding.category, "unknown");
53109
+ }
53110
+ function findingMessage(finding) {
53111
+ const message = stringValue(finding.message);
53112
+ if (message)
53113
+ return message;
53114
+ const issue2 = stringValue(finding.issue);
53115
+ const suggestion = stringValue(finding.suggestion);
53116
+ if (issue2 && suggestion)
53117
+ return `${issue2}
53118
+ \u2192 ${suggestion}`;
53119
+ return issue2;
52587
53120
  }
52588
53121
  async function collectFromReviewAudit(context) {
52589
53122
  const observations = [];
@@ -52619,7 +53152,7 @@ async function collectFromReviewAudit(context) {
52619
53152
  severity: stringValue(finding.severity, "info"),
52620
53153
  file: stringValue(finding.file),
52621
53154
  line: numberValue(finding.line, 0),
52622
- message: stringValue(finding.message)
53155
+ message: findingMessage(finding)
52623
53156
  }
52624
53157
  };
52625
53158
  observations.push(obs);
@@ -52893,31 +53426,43 @@ function mergeThresholds(thresholds) {
52893
53426
  function uniqueStoryIds(storyIds) {
52894
53427
  return [...new Set(storyIds)];
52895
53428
  }
53429
+ function firstLine2(message) {
53430
+ return message.split(`
53431
+ `)[0] ?? message;
53432
+ }
52896
53433
  function h1RepeatedReviewFinding(observations, threshold) {
52897
53434
  const findings = observations.filter((o) => o.kind === "review-finding");
52898
- const byRuleId = new Map;
53435
+ const groups = new Map;
52899
53436
  for (const obs of findings) {
52900
53437
  const ruleId = obs.payload.ruleId;
52901
- const existing = byRuleId.get(ruleId);
53438
+ const message = obs.payload.message;
53439
+ const existing = groups.get(ruleId);
52902
53440
  if (existing) {
52903
- existing.push(obs.storyId);
53441
+ existing.storyIds.push(obs.storyId);
53442
+ const sampleKey = firstLine2(message);
53443
+ if (existing.samples.length < 2 && sampleKey && !existing.samples.includes(sampleKey)) {
53444
+ existing.samples.push(sampleKey);
53445
+ }
52904
53446
  } else {
52905
- byRuleId.set(ruleId, [obs.storyId]);
53447
+ const sampleKey = firstLine2(message);
53448
+ groups.set(ruleId, { storyIds: [obs.storyId], samples: sampleKey ? [sampleKey] : [] });
52906
53449
  }
52907
53450
  }
52908
53451
  const proposals = [];
52909
- for (const [ruleId, storyIds] of byRuleId.entries()) {
53452
+ for (const [ruleId, { storyIds, samples }] of groups.entries()) {
52910
53453
  if (storyIds.length < threshold)
52911
53454
  continue;
52912
53455
  const count = storyIds.length;
52913
53456
  const severity = count >= 4 ? "HIGH" : "MED";
52914
53457
  const unique = uniqueStoryIds(storyIds);
53458
+ const sampleSection = samples.length > 0 ? `
53459
+ Examples: ${samples.join(" | ")}` : "";
52915
53460
  proposals.push({
52916
53461
  id: "H1",
52917
53462
  severity,
52918
53463
  target: { canonicalFile: ".nax/rules/curator-suggestions.md", action: "add" },
52919
53464
  description: `Repeated review finding: ${ruleId} appeared ${count}x across stories`,
52920
- evidence: `Rule ${ruleId} fired ${count}\xD7 in stories: ${unique.join(", ")}`,
53465
+ evidence: `Rule ${ruleId} fired ${count}\xD7 in stories: ${unique.join(", ")}${sampleSection}`,
52921
53466
  sourceKinds: ["review-finding"],
52922
53467
  storyIds: unique
52923
53468
  });
@@ -54080,7 +54625,7 @@ var package_default;
54080
54625
  var init_package = __esm(() => {
54081
54626
  package_default = {
54082
54627
  name: "@nathapp/nax",
54083
- version: "0.65.0",
54628
+ version: "0.65.2",
54084
54629
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
54085
54630
  type: "module",
54086
54631
  bin: {
@@ -54166,8 +54711,8 @@ var init_version = __esm(() => {
54166
54711
  NAX_VERSION = package_default.version;
54167
54712
  NAX_COMMIT = (() => {
54168
54713
  try {
54169
- if (/^[0-9a-f]{6,10}$/.test("c067a82c"))
54170
- return "c067a82c";
54714
+ if (/^[0-9a-f]{6,10}$/.test("99828ef9"))
54715
+ return "99828ef9";
54171
54716
  } catch {}
54172
54717
  try {
54173
54718
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -90518,6 +91063,7 @@ __export(exports_curator, {
90518
91063
  _curatorCmdDeps: () => _curatorCmdDeps
90519
91064
  });
90520
91065
  import { readdirSync as readdirSync10 } from "fs";
91066
+ import { unlink as unlink4 } from "fs/promises";
90521
91067
  import { basename as basename16, join as join79 } from "path";
90522
91068
  function getProjectKey(config2, projectDir) {
90523
91069
  return config2.name?.trim() || basename16(projectDir);
@@ -90788,6 +91334,16 @@ async function curatorGc(options) {
90788
91334
  `)}
90789
91335
  `;
90790
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
+ }
90791
91347
  console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
90792
91348
  }
90793
91349
  var _curatorCmdDeps, DEFAULT_KEEP = 50;
@@ -90811,6 +91367,11 @@ var init_curator2 = __esm(() => {
90811
91367
  const prev = await existing.exists() ? await existing.text() : "";
90812
91368
  await Bun.write(p, prev + content);
90813
91369
  },
91370
+ removeFile: async (p) => {
91371
+ try {
91372
+ await unlink4(p);
91373
+ } catch {}
91374
+ },
90814
91375
  openInEditor: async (filePath) => {
90815
91376
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
90816
91377
  const proc = Bun.spawnSync([editor, filePath], { stdio: ["inherit", "inherit", "inherit"] });
@@ -92310,6 +92871,8 @@ var FIELD_DESCRIPTIONS = {
92310
92871
  "quality.commands.typecheck": "Custom typecheck command",
92311
92872
  "quality.commands.lint": "Custom lint command",
92312
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}})",
92313
92876
  "quality.commands.test": "Custom test command",
92314
92877
  "quality.commands.build": "Custom build command",
92315
92878
  "quality.forceExit": "Append --forceExit to test command (prevents hangs)",
@@ -92343,6 +92906,8 @@ var FIELD_DESCRIPTIONS = {
92343
92906
  "review.commands.typecheck": "Custom typecheck command for review",
92344
92907
  "review.commands.lint": "Custom lint command for review",
92345
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}})",
92346
92911
  "review.commands.test": "Custom test command for review",
92347
92912
  "review.commands.build": "Custom build command for review",
92348
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.0",
3
+ "version": "0.65.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {