@nathapp/nax 0.65.1 → 0.65.3

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 +551 -88
  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
@@ -32468,7 +32506,8 @@ function acceptanceTestFilename(language) {
32468
32506
  }
32469
32507
  }
32470
32508
  function resolveAcceptanceTestFile(language, testPathConfig) {
32471
- return testPathConfig ?? acceptanceTestFilename(language);
32509
+ const candidate = testPathConfig ?? acceptanceTestFilename(language);
32510
+ return sanitizeTestFileName(candidate, "acceptance.testPath");
32472
32511
  }
32473
32512
  function resolveAcceptanceFeatureTestPath(featureDir, testPathConfig, language) {
32474
32513
  return path3.join(featureDir, resolveAcceptanceTestFile(language, testPathConfig));
@@ -32522,7 +32561,21 @@ function suggestedTestFilename(language) {
32522
32561
  }
32523
32562
  }
32524
32563
  function resolveSuggestedTestFile(language, testPathConfig) {
32525
- return testPathConfig ?? suggestedTestFilename(language);
32564
+ const candidate = testPathConfig ?? suggestedTestFilename(language);
32565
+ return sanitizeTestFileName(candidate, "acceptance.suggestedTestPath");
32566
+ }
32567
+ function sanitizeTestFileName(value, fieldName) {
32568
+ const filename = value.trim();
32569
+ if (filename.length === 0) {
32570
+ throw new Error(`${fieldName} must be non-empty`);
32571
+ }
32572
+ if (filename.includes("/") || filename.includes("\\")) {
32573
+ throw new Error(`${fieldName} must be a filename, not a path: ${filename}`);
32574
+ }
32575
+ if (filename.includes("..")) {
32576
+ throw new Error(`${fieldName} cannot contain '..': ${filename}`);
32577
+ }
32578
+ return filename;
32526
32579
  }
32527
32580
  function resolveSuggestedPackageFeatureTestPath(packageDir, featureName, testPathConfig, language) {
32528
32581
  return path3.join(packageDir, ".nax", "features", featureName, resolveSuggestedTestFile(language, testPathConfig));
@@ -33670,12 +33723,79 @@ var init_rectify = __esm(() => {
33670
33723
  init_prompts();
33671
33724
  });
33672
33725
 
33726
+ // src/operations/test-edit-declaration.ts
33727
+ function readBlockField(block, key) {
33728
+ const re = new RegExp(`^${key}:\\s*(.+)$`, "m");
33729
+ const m = block.match(re);
33730
+ if (!m?.[1])
33731
+ return null;
33732
+ return m[1].trim();
33733
+ }
33734
+ function unwrapQuotes(s) {
33735
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"'))
33736
+ return s.slice(1, -1);
33737
+ return s;
33738
+ }
33739
+ function parseTestEditDeclarations(output) {
33740
+ const result = [];
33741
+ const blocks = output.split(/\n\s*\n/);
33742
+ for (const block of blocks) {
33743
+ const reasonMatch = block.match(REASON_RE);
33744
+ if (!reasonMatch?.[1])
33745
+ continue;
33746
+ const reason = reasonMatch[1];
33747
+ if (reason === "prd_contract") {
33748
+ const file3 = readBlockField(block, "FILE");
33749
+ const prdQuote = readBlockField(block, "PRD_QUOTE");
33750
+ const testBefore = readBlockField(block, "TEST_BEFORE");
33751
+ const testAfter = readBlockField(block, "TEST_AFTER");
33752
+ if (!file3 || !prdQuote || !testBefore || !testAfter)
33753
+ continue;
33754
+ result.push({
33755
+ reason,
33756
+ file: file3,
33757
+ prdQuote: unwrapQuotes(prdQuote),
33758
+ testBefore,
33759
+ testAfter
33760
+ });
33761
+ } else if (reason === "lint_only") {
33762
+ const file3 = readBlockField(block, "FILE");
33763
+ const finding = readBlockField(block, "FINDING");
33764
+ if (!file3 || !finding)
33765
+ continue;
33766
+ result.push({ reason, file: file3, finding });
33767
+ } else if (reason === "sibling_scope") {
33768
+ const file3 = readBlockField(block, "SIBLING_FILE");
33769
+ const finding = readBlockField(block, "FINDING");
33770
+ if (!file3 || !finding)
33771
+ continue;
33772
+ result.push({ reason, file: file3, finding });
33773
+ }
33774
+ }
33775
+ return result;
33776
+ }
33777
+ function normaliseWs(s) {
33778
+ return s.replace(/\s+/g, " ").replace(/\s*([(),<>])\s*/g, "$1").replace(/\s*:\s*/g, ": ").trim();
33779
+ }
33780
+ function validatePrdQuote(prdQuote, story) {
33781
+ if (!prdQuote.trim())
33782
+ return false;
33783
+ const needle = normaliseWs(prdQuote);
33784
+ const haystack = normaliseWs([story.description, ...story.acceptanceCriteria].join(" "));
33785
+ return haystack.includes(needle);
33786
+ }
33787
+ var REASON_RE;
33788
+ var init_test_edit_declaration = __esm(() => {
33789
+ REASON_RE = /^TEST_EDIT_REASON:\s*(prd_contract|lint_only|sibling_scope)\s*$/m;
33790
+ });
33791
+
33673
33792
  // src/operations/autofix-implementer.ts
33674
33793
  var implementerRectifyOp;
33675
33794
  var init_autofix_implementer = __esm(() => {
33676
33795
  init_config();
33677
33796
  init_logger2();
33678
33797
  init_prompts();
33798
+ init_test_edit_declaration();
33679
33799
  implementerRectifyOp = {
33680
33800
  kind: "run",
33681
33801
  name: "autofix-implementer",
@@ -33690,15 +33810,20 @@ var init_autofix_implementer = __esm(() => {
33690
33810
  };
33691
33811
  },
33692
33812
  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) {
33813
+ const unresolvedMatch = output.match(/^UNRESOLVED:\s*(.+)$/m);
33814
+ const declarations = parseTestEditDeclarations(output);
33815
+ for (const d of declarations) {
33696
33816
  getSafeLogger()?.info("autofix", "test_edit_declared", {
33697
33817
  storyId: input.story.id,
33698
- reason: editReasonMatch[1]
33818
+ reason: d.reason,
33819
+ file: d.file
33699
33820
  });
33700
33821
  }
33701
- return { applied: true, ...match ? { unresolvedReason: match[1]?.trim() } : {} };
33822
+ return {
33823
+ applied: true,
33824
+ testEditDeclarations: declarations,
33825
+ ...unresolvedMatch ? { unresolvedReason: unresolvedMatch[1]?.trim() } : {}
33826
+ };
33702
33827
  }
33703
33828
  };
33704
33829
  });
@@ -33805,6 +33930,20 @@ function killProcessGroup(pid, signal) {
33805
33930
 
33806
33931
  // src/quality/runner.ts
33807
33932
  var {spawn: spawn2 } = globalThis.Bun;
33933
+ function createDrainDeadline(deadlineMs) {
33934
+ let timeoutId;
33935
+ const promise2 = new Promise((resolve11) => {
33936
+ timeoutId = setTimeout(() => resolve11(""), deadlineMs);
33937
+ });
33938
+ return {
33939
+ promise: promise2,
33940
+ cancel: () => {
33941
+ if (timeoutId !== undefined) {
33942
+ clearTimeout(timeoutId);
33943
+ }
33944
+ }
33945
+ };
33946
+ }
33808
33947
  async function runQualityCommand(opts) {
33809
33948
  const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
33810
33949
  const startTime = Date.now();
@@ -33837,11 +33976,22 @@ async function runQualityCommand(opts) {
33837
33976
  }
33838
33977
  }, SIGKILL_GRACE_PERIOD_MS);
33839
33978
  }, timeoutMs);
33840
- const [exitCode, stdout, stderr] = await Promise.all([
33841
- proc.exited,
33842
- new Response(proc.stdout).text(),
33843
- new Response(proc.stderr).text()
33844
- ]);
33979
+ const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
33980
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
33981
+ const exitCode = await proc.exited;
33982
+ const [stdout, stderr] = timedOut ? await (async () => {
33983
+ const stdoutDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
33984
+ const stderrDrain = createDrainDeadline(STREAM_DRAIN_TIMEOUT_MS);
33985
+ try {
33986
+ return await Promise.all([
33987
+ Promise.race([stdoutPromise, stdoutDrain.promise]),
33988
+ Promise.race([stderrPromise, stderrDrain.promise])
33989
+ ]);
33990
+ } finally {
33991
+ stdoutDrain.cancel();
33992
+ stderrDrain.cancel();
33993
+ }
33994
+ })() : await Promise.all([stdoutPromise, stderrPromise]);
33845
33995
  clearTimeout(killTimer);
33846
33996
  if (sigkillTimer !== undefined) {
33847
33997
  clearTimeout(sigkillTimer);
@@ -33893,7 +34043,7 @@ async function runQualityCommand(opts) {
33893
34043
  };
33894
34044
  }
33895
34045
  }
33896
- var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, _qualityRunnerDeps;
34046
+ var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS = 5000, STREAM_DRAIN_TIMEOUT_MS = 2000, _qualityRunnerDeps;
33897
34047
  var init_runner = __esm(() => {
33898
34048
  init_logger2();
33899
34049
  _qualityRunnerDeps = {
@@ -34258,6 +34408,8 @@ function buildSmartTestCommand(testFiles, baseCommand) {
34258
34408
  if (testFiles.length === 0) {
34259
34409
  return baseCommand;
34260
34410
  }
34411
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
34412
+ const quotedTestFiles = testFiles.map(shellQuote);
34261
34413
  const parts = baseCommand.trim().split(/\s+/);
34262
34414
  let lastPathIndex = -1;
34263
34415
  for (let i = parts.length - 1;i >= 0; i--) {
@@ -34267,11 +34419,11 @@ function buildSmartTestCommand(testFiles, baseCommand) {
34267
34419
  }
34268
34420
  }
34269
34421
  if (lastPathIndex === -1) {
34270
- return `${baseCommand} ${testFiles.join(" ")}`;
34422
+ return `${baseCommand} ${quotedTestFiles.join(" ")}`;
34271
34423
  }
34272
34424
  const beforePath = parts.slice(0, lastPathIndex);
34273
34425
  const afterPath = parts.slice(lastPathIndex + 1);
34274
- const newParts = [...beforePath, ...testFiles, ...afterPath];
34426
+ const newParts = [...beforePath, ...quotedTestFiles, ...afterPath];
34275
34427
  return newParts.join(" ");
34276
34428
  }
34277
34429
  async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileRegex = [], naxIgnoreIndex, repoRoot) {
@@ -34358,7 +34510,8 @@ function coerceSmartRunner(val) {
34358
34510
  }
34359
34511
  function buildScopedCommand(testFiles, baseCommand, testScopedTemplate) {
34360
34512
  if (testScopedTemplate) {
34361
- return testScopedTemplate.replace("{{files}}", testFiles.join(" "));
34513
+ const quotedFiles = testFiles.map((file3) => `'${file3.replaceAll("'", "'\\''")}'`);
34514
+ return testScopedTemplate.replace("{{files}}", quotedFiles.join(" "));
34362
34515
  }
34363
34516
  return _scopedDeps.buildSmartTestCommand(testFiles, baseCommand);
34364
34517
  }
@@ -35142,6 +35295,7 @@ var init_operations = __esm(() => {
35142
35295
  init_adversarial_review();
35143
35296
  init_rectify();
35144
35297
  init_autofix_implementer();
35298
+ init_test_edit_declaration();
35145
35299
  init_autofix_test_writer();
35146
35300
  init_debate_propose();
35147
35301
  init_debate_rebut();
@@ -35151,6 +35305,23 @@ var init_operations = __esm(() => {
35151
35305
  init_auto_approve();
35152
35306
  });
35153
35307
 
35308
+ // src/utils/feature-name.ts
35309
+ function validateFeatureName(feature) {
35310
+ if (!feature || feature.trim() === "") {
35311
+ throw new Error("Feature name must be non-empty");
35312
+ }
35313
+ if (feature.includes("/") || feature.includes("\\")) {
35314
+ throw new Error(`Feature name must be a single path segment: ${feature}`);
35315
+ }
35316
+ if (feature.includes("..")) {
35317
+ throw new Error(`Feature name cannot contain '..': ${feature}`);
35318
+ }
35319
+ const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
35320
+ if (!validPattern.test(feature)) {
35321
+ throw new Error(`Feature name contains invalid characters: ${feature}`);
35322
+ }
35323
+ }
35324
+
35154
35325
  // src/cli/plan-helpers.ts
35155
35326
  import { createInterface } from "readline";
35156
35327
  function createCliInteractionBridge() {
@@ -39313,7 +39484,7 @@ class SessionManager {
39313
39484
  _pidRegistry;
39314
39485
  _watchdogControllerRegistry;
39315
39486
  _onStreamActivity;
39316
- _watchdogCancelledCalls = new Set;
39487
+ _watchdogCancelledCallsBySession = new Map;
39317
39488
  _agentStreamUnsubscribe;
39318
39489
  constructor(opts) {
39319
39490
  this._getAdapter = opts?.getAdapter ?? (() => {
@@ -39343,22 +39514,26 @@ class SessionManager {
39343
39514
  this._agentStreamUnsubscribe = opts.agentStreamEvents.onAgentStream((event) => {
39344
39515
  if (event.kind === "agent.call_ended") {
39345
39516
  this._watchdogControllerRegistry?.delete(event.callId);
39346
- this._watchdogCancelledCalls.delete(event.callId);
39347
39517
  }
39348
39518
  });
39349
39519
  }
39350
39520
  }
39351
- _buildOnActiveCall() {
39521
+ _buildOnActiveCall(sessionName) {
39352
39522
  const registry2 = this._watchdogControllerRegistry;
39353
39523
  if (!registry2)
39354
39524
  return;
39355
39525
  return (callId, cancel) => {
39356
39526
  registry2.set(callId, async () => {
39357
- this._watchdogCancelledCalls.add(callId);
39527
+ const cancelledCalls = this._watchdogCancelledCallsBySession.get(sessionName) ?? new Set;
39528
+ cancelledCalls.add(callId);
39529
+ this._watchdogCancelledCallsBySession.set(sessionName, cancelledCalls);
39358
39530
  await cancel();
39359
39531
  });
39360
39532
  };
39361
39533
  }
39534
+ _clearWatchdogCancelledCalls(sessionName) {
39535
+ this._watchdogCancelledCallsBySession.delete(sessionName);
39536
+ }
39362
39537
  _persistDescriptor(descriptor) {
39363
39538
  if (!descriptor.scratchDir)
39364
39539
  return;
@@ -39581,7 +39756,7 @@ class SessionManager {
39581
39756
  onSessionEstablished: opts.onSessionEstablished,
39582
39757
  signal: opts.signal,
39583
39758
  resume,
39584
- onActiveCall: this._buildOnActiveCall(),
39759
+ onActiveCall: this._buildOnActiveCall(name),
39585
39760
  onStreamActivity: this._onStreamActivity
39586
39761
  });
39587
39762
  this._liveHandles.set(name, handle);
@@ -39668,8 +39843,7 @@ class SessionManager {
39668
39843
  return { ...result, protocolIds: result.protocolIds ?? handle.protocolIds };
39669
39844
  } catch (err) {
39670
39845
  if (err instanceof SessionTurnError && err.cancelled) {
39671
- const wasWatchdog = this._watchdogCancelledCalls.size > 0;
39672
- this._watchdogCancelledCalls.clear();
39846
+ const wasWatchdog = (this._watchdogCancelledCallsBySession.get(handle.id)?.size ?? 0) > 0;
39673
39847
  if (wasWatchdog) {
39674
39848
  throw new SessionFailureError("idle watchdog cancelled session \u2014 no stream activity", {
39675
39849
  category: "availability",
@@ -39688,6 +39862,7 @@ class SessionManager {
39688
39862
  }
39689
39863
  throw err;
39690
39864
  } finally {
39865
+ this._clearWatchdogCancelledCalls(handle.id);
39691
39866
  this._busySessions.delete(handle.id);
39692
39867
  }
39693
39868
  }
@@ -40367,8 +40542,10 @@ import { basename as basename5, join as join28 } from "path";
40367
40542
  function createRuntime(config2, workdir, opts) {
40368
40543
  const runId = crypto.randomUUID();
40369
40544
  const controller = new AbortController;
40545
+ let parentAbortHandler;
40370
40546
  if (opts?.parentSignal) {
40371
- opts.parentSignal.addEventListener("abort", () => controller.abort(opts.parentSignal?.reason), { once: true });
40547
+ parentAbortHandler = () => controller.abort(opts.parentSignal?.reason);
40548
+ opts.parentSignal.addEventListener("abort", parentAbortHandler, { once: true });
40372
40549
  }
40373
40550
  const configLoader = createConfigLoader(config2);
40374
40551
  const dispatchEvents = new DispatchEventBus;
@@ -40469,6 +40646,9 @@ function createRuntime(config2, workdir, opts) {
40469
40646
  offReviewAudit();
40470
40647
  offAgentStreamLogging();
40471
40648
  offWatchdog();
40649
+ if (opts?.parentSignal && parentAbortHandler) {
40650
+ opts.parentSignal.removeEventListener("abort", parentAbortHandler);
40651
+ }
40472
40652
  const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
40473
40653
  for (const r of results) {
40474
40654
  if (r.status === "rejected") {
@@ -42188,6 +42368,11 @@ Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, co
42188
42368
  }
42189
42369
  let featureDir;
42190
42370
  if (feature) {
42371
+ try {
42372
+ validateFeatureName(feature);
42373
+ } catch (error48) {
42374
+ throw new NaxError(error48.message, "FEATURE_INVALID", { feature });
42375
+ }
42191
42376
  const featuresDir = join32(naxDir, "features");
42192
42377
  featureDir = join32(featuresDir, feature);
42193
42378
  if (!existsSync16(featureDir)) {
@@ -43417,7 +43602,7 @@ function collectAdversarialSourceChecks(ctx) {
43417
43602
  function buildAutofixStrategies(ctx, maxAttempts) {
43418
43603
  const implementer = {
43419
43604
  name: "autofix-implementer",
43420
- appliesTo: (f) => (f.fixTarget ?? "source") === "source",
43605
+ appliesTo: (f) => (f.fixTarget ?? "source") === "source" && f.category !== "prd_quote_mismatch",
43421
43606
  fixOp: implementerRectifyOp,
43422
43607
  maxAttempts,
43423
43608
  coRun: "co-run-sequential",
@@ -43425,16 +43610,22 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43425
43610
  failedChecks: collectFailedChecks(ctx),
43426
43611
  story: ctx.story
43427
43612
  }),
43428
- extractApplied: (output) => ({
43429
- summary: output.unresolvedReason ?? "",
43430
- unresolved: output.unresolvedReason
43431
- })
43613
+ extractApplied: (output) => {
43614
+ const decls = output.testEditDeclarations ?? [];
43615
+ if (decls.length > 0) {
43616
+ ctx.testEditDeclarations = [...ctx.testEditDeclarations ?? [], ...decls];
43617
+ }
43618
+ return {
43619
+ summary: output.unresolvedReason ?? "",
43620
+ unresolved: output.unresolvedReason
43621
+ };
43622
+ }
43432
43623
  };
43433
43624
  const testWriter = {
43434
43625
  name: "autofix-test-writer",
43435
43626
  appliesTo: (f) => f.fixTarget === "test" || (f.fixTarget ?? "source") === "source" && f.severity === "error" && f.source === "adversarial-review",
43436
43627
  fixOp: testWriterRectifyOp,
43437
- maxAttempts: 1,
43628
+ maxAttempts: 2,
43438
43629
  coRun: "co-run-sequential",
43439
43630
  buildInput: (findings, _prior, _cycleCtx) => {
43440
43631
  const hasSourceBug = findings.some((f) => (f.fixTarget ?? "source") === "source" && f.source === "adversarial-review");
@@ -43455,6 +43646,25 @@ function buildAutofixStrategies(ctx, maxAttempts) {
43455
43646
  };
43456
43647
  return [testWriter, implementer];
43457
43648
  }
43649
+ function autofixCapacityExhausted(ctx) {
43650
+ const findings = collectCurrentFindings(ctx);
43651
+ if (findings.length === 0)
43652
+ return false;
43653
+ const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 3;
43654
+ const maxTotal = ctx.config.quality.autofix?.maxTotalAttempts ?? 12;
43655
+ const prior = ctx.autofixPriorIterations ?? [];
43656
+ const totalUsed = prior.reduce((sum, iter) => sum + iter.fixesApplied.length, 0);
43657
+ if (totalUsed >= maxTotal)
43658
+ return true;
43659
+ const strategies = buildAutofixStrategies(ctx, maxAttempts);
43660
+ const active = strategies.filter((s) => findings.some((f) => s.appliesTo(f)));
43661
+ if (active.length === 0)
43662
+ return true;
43663
+ return active.some((s) => {
43664
+ const used = prior.reduce((sum, iter) => sum + iter.fixesApplied.filter((fa) => fa.strategyName === s.name).length, 0);
43665
+ return used >= s.maxAttempts;
43666
+ });
43667
+ }
43458
43668
  function buildEscalationDigest(findings) {
43459
43669
  const byFile = new Map;
43460
43670
  for (const f of findings) {
@@ -43494,6 +43704,50 @@ async function writeShadowReport(ctx, result, initialFindingsCount) {
43494
43704
  });
43495
43705
  }
43496
43706
  }
43707
+ function applyTestEditDeclarations(findings, declarations, story) {
43708
+ if (declarations.length === 0)
43709
+ return findings;
43710
+ const out = [...findings];
43711
+ const originalLength = findings.length;
43712
+ const reTaggedKeys = new Set;
43713
+ for (const decl of declarations) {
43714
+ if (decl.reason !== "prd_contract")
43715
+ continue;
43716
+ if (!validatePrdQuote(decl.prdQuote ?? "", story)) {
43717
+ out.push({
43718
+ source: "adversarial-review",
43719
+ severity: "warning",
43720
+ category: "prd_quote_mismatch",
43721
+ message: `Implementer declared TEST_EDIT_REASON: prd_contract with PRD_QUOTE not found in story description or AC text: ${decl.prdQuote}`,
43722
+ file: decl.file,
43723
+ fixTarget: "source"
43724
+ });
43725
+ continue;
43726
+ }
43727
+ for (let i = 0;i < originalLength; i++) {
43728
+ if (reTaggedKeys.has(i))
43729
+ continue;
43730
+ if (out[i].file !== decl.file)
43731
+ continue;
43732
+ if ((out[i].fixTarget ?? "source") === "test")
43733
+ continue;
43734
+ out[i] = {
43735
+ ...out[i],
43736
+ fixTarget: "test",
43737
+ meta: {
43738
+ ...out[i].meta ?? {},
43739
+ prdContractDeclaration: {
43740
+ prdQuote: decl.prdQuote,
43741
+ testBefore: decl.testBefore,
43742
+ testAfter: decl.testAfter
43743
+ }
43744
+ }
43745
+ };
43746
+ reTaggedKeys.add(i);
43747
+ }
43748
+ }
43749
+ return out;
43750
+ }
43497
43751
  async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effectiveWorkdir) {
43498
43752
  const logger = getLogger();
43499
43753
  const storyId = ctx.story.id;
@@ -43517,7 +43771,18 @@ async function runAgentRectificationV2(ctx, _lintFixCmd, _formatFixCmd, _effecti
43517
43771
  },
43518
43772
  async validate(_cycleCtx) {
43519
43773
  await _autofixDeps.recheckReview(ctx);
43520
- return collectCurrentFindings(ctx);
43774
+ const fresh = collectCurrentFindings(ctx);
43775
+ const pending = ctx.testEditDeclarations ?? [];
43776
+ if (pending.length === 0)
43777
+ return fresh;
43778
+ const retagged = applyTestEditDeclarations(fresh, pending, ctx.story);
43779
+ ctx.testEditDeclarations = [];
43780
+ logger.info("autofix-cycle", "applied test-edit declarations", {
43781
+ storyId: ctx.story.id,
43782
+ declarationCount: pending.length,
43783
+ reTaggedCount: retagged.filter((f) => f.fixTarget === "test").length - fresh.filter((f) => f.fixTarget === "test").length
43784
+ });
43785
+ return retagged;
43521
43786
  }
43522
43787
  };
43523
43788
  const result = await runFixCycle(cycle, cycleCtx, "autofix-v2");
@@ -45170,6 +45435,42 @@ ${formatFindings2(blockingFindings)}`,
45170
45435
  };
45171
45436
  }
45172
45437
  if (!parsed.passed && blockingFindings.length === 0) {
45438
+ if (acDropped.length > 0) {
45439
+ const durationMs3 = Date.now() - startTime;
45440
+ logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
45441
+ storyId: story.id,
45442
+ durationMs: durationMs3,
45443
+ droppedCount: acDropped.length,
45444
+ dropCodes: acDropped.map((d) => d.code)
45445
+ });
45446
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
45447
+ `);
45448
+ recordAdversarialAudit({
45449
+ runtime,
45450
+ workdir,
45451
+ projectDir,
45452
+ storyId: story.id,
45453
+ featureName,
45454
+ parsed: true,
45455
+ failOpen: false,
45456
+ passed: false,
45457
+ blockingThreshold: threshold,
45458
+ result: { passed: false, findings: [] },
45459
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined
45460
+ });
45461
+ return {
45462
+ check: "adversarial",
45463
+ success: false,
45464
+ command: "",
45465
+ exitCode: 1,
45466
+ 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:
45467
+
45468
+ ${dropSummary}`,
45469
+ durationMs: durationMs3,
45470
+ advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
45471
+ cost: llmCost
45472
+ };
45473
+ }
45173
45474
  const durationMs2 = Date.now() - startTime;
45174
45475
  logger?.info("review", "Adversarial review passed (all findings below blocking threshold)", {
45175
45476
  storyId: story.id,
@@ -46228,6 +46529,42 @@ ${formatFindings(blockingFindings)}`;
46228
46529
  };
46229
46530
  }
46230
46531
  if (!sanitizedParsed.passed && blockingFindings.length === 0) {
46532
+ if (acDropped.length > 0) {
46533
+ const durationMs3 = Date.now() - startTime;
46534
+ logger?.warn("review", "Semantic review fail-closed: blocking findings dropped as ungrounded", {
46535
+ storyId: story.id,
46536
+ durationMs: durationMs3,
46537
+ droppedCount: acDropped.length,
46538
+ dropCodes: acDropped.map((d) => d.code)
46539
+ });
46540
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
46541
+ `);
46542
+ recordSemanticAudit({
46543
+ runtime,
46544
+ workdir,
46545
+ projectDir,
46546
+ storyId: story.id,
46547
+ featureName,
46548
+ parsed: true,
46549
+ failOpen: false,
46550
+ passed: false,
46551
+ blockingThreshold: threshold,
46552
+ result: { passed: false, findings: [] },
46553
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
46554
+ });
46555
+ return {
46556
+ check: "semantic",
46557
+ success: false,
46558
+ command: "",
46559
+ exitCode: 1,
46560
+ 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:
46561
+
46562
+ ${dropSummary}`,
46563
+ durationMs: durationMs3,
46564
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
46565
+ cost: llmCost
46566
+ };
46567
+ }
46231
46568
  const durationMs2 = Date.now() - startTime;
46232
46569
  logger?.info("review", "Semantic review passed (all findings below blocking threshold)", {
46233
46570
  storyId: story.id,
@@ -47315,12 +47652,115 @@ async function recheckReview(ctx) {
47315
47652
  return false;
47316
47653
  return ctx.reviewResult?.success === true;
47317
47654
  }
47655
+ async function runMechanicalFixes(ctx, failedCheckNames) {
47656
+ const commands = resolveMechanicalFixCommands(ctx, failedCheckNames);
47657
+ for (const resolved of commands) {
47658
+ if (resolved.skipped)
47659
+ continue;
47660
+ pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: resolved.command });
47661
+ const result = await _autofixDeps.runQualityCommand({
47662
+ commandName: resolved.commandName,
47663
+ command: resolved.command,
47664
+ workdir: ctx.workdir,
47665
+ storyId: ctx.story.id
47666
+ });
47667
+ logMechanicalFixResult(ctx, resolved, result.exitCode);
47668
+ }
47669
+ }
47670
+ function resolveMechanicalFixCommands(ctx, failedCheckNames) {
47671
+ if (!failedCheckNames.has("lint"))
47672
+ return [];
47673
+ const scopeFiles = collectLintScopeFiles(ctx.reviewResult?.checks ?? []);
47674
+ return [resolveFixCommand(ctx, "lintFix", scopeFiles), resolveFixCommand(ctx, "formatFix", scopeFiles)].filter((cmd) => cmd !== undefined);
47675
+ }
47676
+ function resolveFixCommand(ctx, commandName, scopeFiles) {
47677
+ const broad = resolveBroadFixCommand(ctx, commandName);
47678
+ const template = resolveScopedFixTemplate(ctx, commandName);
47679
+ if (!broad && !template)
47680
+ return;
47681
+ if (!scopeFiles)
47682
+ return warnAndUseFullFix(ctx, commandName, broad, "missing_lint_scope");
47683
+ if (scopeFiles.length === 0)
47684
+ return logEmptyFixScope(ctx, commandName, broad ?? template ?? "");
47685
+ if (template) {
47686
+ return {
47687
+ commandName,
47688
+ command: template.replaceAll("{{files}}", scopeFiles.map(shellQuotePath2).join(" ")),
47689
+ scoped: true
47690
+ };
47691
+ }
47692
+ if (!broad)
47693
+ return;
47694
+ const derived = deriveScopedFixCommand(broad, scopeFiles);
47695
+ if (derived)
47696
+ return { commandName, command: derived, scoped: true };
47697
+ return warnAndUseFullFix(ctx, commandName, broad, "unsupported_scoped_command_shape");
47698
+ }
47699
+ function collectLintScopeFiles(checks3) {
47700
+ const lintChecks = checks3.filter((check2) => check2.check === "lint" && !check2.success);
47701
+ if (lintChecks.length === 0)
47702
+ return;
47703
+ if (lintChecks.some((check2) => !check2.lintScope))
47704
+ return;
47705
+ if (lintChecks.some((check2) => check2.lintScope?.status === "degraded"))
47706
+ return;
47707
+ const files = lintChecks.flatMap((check2) => check2.lintScope?.packageGroups.flatMap((group) => group.files) ?? []);
47708
+ return [...new Set(files)];
47709
+ }
47710
+ function resolveBroadFixCommand(ctx, commandName) {
47711
+ return commandName === "lintFix" ? ctx.config.quality.commands.lintFix ?? ctx.config.review.commands.lintFix : ctx.config.quality.commands.formatFix ?? ctx.config.review.commands.formatFix;
47712
+ }
47713
+ function hasMechanicalFixCommand(ctx) {
47714
+ return ["lintFix", "formatFix"].some((name) => resolveBroadFixCommand(ctx, name) ?? resolveScopedFixTemplate(ctx, name));
47715
+ }
47716
+ function resolveScopedFixTemplate(ctx, commandName) {
47717
+ return commandName === "lintFix" ? ctx.config.review.commands.lintFixScoped ?? ctx.config.quality.commands.lintFixScoped : ctx.config.review.commands.formatFixScoped ?? ctx.config.quality.commands.formatFixScoped;
47718
+ }
47719
+ function deriveScopedFixCommand(command, files) {
47720
+ const trimmed = command.trim();
47721
+ const supportedTools = ["eslint", "biome", "ruff", "flake8", "prettier"];
47722
+ const isSupported = supportedTools.some((tool) => trimmed === tool || trimmed.startsWith(`${tool} `)) || supportedTools.some((tool) => trimmed.startsWith(`bunx ${tool}`));
47723
+ if (!isSupported)
47724
+ return;
47725
+ return `${command} ${files.map(shellQuotePath2).join(" ")}`;
47726
+ }
47727
+ function shellQuotePath2(path9) {
47728
+ return `'${path9.replaceAll("'", "'\\''")}'`;
47729
+ }
47730
+ function logEmptyFixScope(ctx, commandName, command) {
47731
+ getLogger().info("autofix", `${toScopeLogPrefix(commandName)}_scope_empty`, { storyId: ctx.story.id });
47732
+ return { commandName, command, scoped: true, skipped: true };
47733
+ }
47734
+ function warnAndUseFullFix(ctx, commandName, command, reason) {
47735
+ getLogger().warn("autofix", `${toScopeLogPrefix(commandName)}_scope_degraded`, { storyId: ctx.story.id, reason });
47736
+ if (!command)
47737
+ return { commandName, command: "", scoped: false, skipped: true };
47738
+ return { commandName, command, scoped: false };
47739
+ }
47740
+ function toScopeLogPrefix(commandName) {
47741
+ return commandName === "lintFix" ? "lint_fix" : "format_fix";
47742
+ }
47743
+ function logMechanicalFixResult(ctx, resolved, exitCode) {
47744
+ const logger = getLogger();
47745
+ logger.debug("autofix", `${resolved.commandName} exit=${exitCode}`, {
47746
+ storyId: ctx.story.id,
47747
+ command: resolved.command,
47748
+ scoped: resolved.scoped
47749
+ });
47750
+ if (exitCode !== 0) {
47751
+ logger.warn("autofix", `${resolved.commandName} command failed \u2014 may not have fixed all issues`, {
47752
+ storyId: ctx.story.id,
47753
+ exitCode
47754
+ });
47755
+ }
47756
+ }
47318
47757
  var NON_FIXABLE_BY_RECTIFICATION, autofixStage, _autofixDeps;
47319
47758
  var init_autofix = __esm(() => {
47320
47759
  init_logger2();
47321
47760
  init_quality();
47322
47761
  init_event_bus();
47323
47762
  init_autofix_agent();
47763
+ init_autofix_cycle();
47324
47764
  init_autofix_scope_split();
47325
47765
  init_autofix_test_writer2();
47326
47766
  NON_FIXABLE_BY_RECTIFICATION = new Set(["git-clean"]);
@@ -47345,8 +47785,7 @@ var init_autofix = __esm(() => {
47345
47785
  if (!reviewResult || reviewResult.success) {
47346
47786
  return { action: "continue" };
47347
47787
  }
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;
47788
+ ctx.autofixAttempt = (ctx.autofixAttempt ?? 0) + 1;
47350
47789
  const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
47351
47790
  const hasLintFailure = failedCheckNames.has("lint");
47352
47791
  const totalFindingCount = (reviewResult.checks ?? []).reduce((n, c) => n + (c.findings?.length ?? 0), 0);
@@ -47367,42 +47806,8 @@ var init_autofix = __esm(() => {
47367
47806
  failedChecks: [...failedCheckNames],
47368
47807
  workdir: ctx.workdir
47369
47808
  });
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
- }
47809
+ if (hasLintFailure && hasMechanicalFixCommand(ctx)) {
47810
+ await runMechanicalFixes(ctx, failedCheckNames);
47406
47811
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
47407
47812
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
47408
47813
  if (recheckPassed) {
@@ -47445,7 +47850,7 @@ var init_autofix = __esm(() => {
47445
47850
  cost: agentCost,
47446
47851
  unresolvedReason,
47447
47852
  escalationDigest
47448
- } = await _autofixDeps.runAgentRectification(ctx, lintFixCmd, formatFixCmd, ctx.workdir);
47853
+ } = await _autofixDeps.runAgentRectification(ctx, resolveBroadFixCommand(ctx, "lintFix"), resolveBroadFixCommand(ctx, "formatFix"), ctx.workdir);
47449
47854
  if (!agentFixed && unresolvedReason) {
47450
47855
  if (ctx.mechanicalFailedOnly) {
47451
47856
  logger.warn("autofix", "Mechanical-only failure unfixable \u2014 proceeding (LLM review passed)", {
@@ -47480,7 +47885,8 @@ var init_autofix = __esm(() => {
47480
47885
  const totalUsed = ctx.autofixAttempt ?? 0;
47481
47886
  const currentlyFailing = new Set((ctx.reviewResult?.checks ?? []).filter((c) => !c.success || c.failOpen).map((c) => c.check));
47482
47887
  const nowPassing = [...failedCheckNames].filter((c) => !currentlyFailing.has(c));
47483
- if (nowPassing.length > 0 && totalUsed < maxTotal) {
47888
+ const capacityExhausted = autofixCapacityExhausted(ctx);
47889
+ if (nowPassing.length > 0 && totalUsed < maxTotal && !capacityExhausted) {
47484
47890
  ctx.retrySkipChecks = new Set([...ctx.retrySkipChecks ?? [], ...nowPassing]);
47485
47891
  logger.info("autofix", "Partial progress \u2014 retrying review with updated skip list", {
47486
47892
  storyId: ctx.story.id,
@@ -47490,6 +47896,13 @@ var init_autofix = __esm(() => {
47490
47896
  });
47491
47897
  return { action: "retry", fromStage: "review", cost: agentCost };
47492
47898
  }
47899
+ if (nowPassing.length > 0 && capacityExhausted) {
47900
+ logger.info("autofix", "Partial progress \u2014 but autofix capacity exhausted; escalating instead of retrying review", {
47901
+ storyId: ctx.story.id,
47902
+ nowPassing,
47903
+ remaining: [...currentlyFailing]
47904
+ });
47905
+ }
47493
47906
  logger.warn("autofix", "Autofix exhausted \u2014 escalating", { storyId: ctx.story.id });
47494
47907
  return {
47495
47908
  action: "escalate",
@@ -54120,6 +54533,20 @@ var init_command_argv = __esm(() => {
54120
54533
 
54121
54534
  // src/hooks/runner.ts
54122
54535
  import { join as join67 } from "path";
54536
+ function createDrainDeadline2(deadlineMs) {
54537
+ let timeoutId;
54538
+ const promise2 = new Promise((resolve16) => {
54539
+ timeoutId = setTimeout(() => resolve16(""), deadlineMs);
54540
+ });
54541
+ return {
54542
+ promise: promise2,
54543
+ cancel: () => {
54544
+ if (timeoutId !== undefined) {
54545
+ clearTimeout(timeoutId);
54546
+ }
54547
+ }
54548
+ };
54549
+ }
54123
54550
  async function loadHooksConfig(projectDir, globalDir) {
54124
54551
  let globalHooks = { hooks: {} };
54125
54552
  let projectHooks = { hooks: {} };
@@ -54222,15 +54649,30 @@ async function executeHook(hookDef, ctx, workdir) {
54222
54649
  stderr: "pipe",
54223
54650
  env: buildAllowedEnv({ env: env2 })
54224
54651
  });
54652
+ let timedOut = false;
54225
54653
  const timeoutId = setTimeout(() => {
54654
+ timedOut = true;
54226
54655
  killProcessGroup(proc.pid, "SIGTERM");
54227
54656
  }, timeout);
54657
+ const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
54658
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
54228
54659
  const exitCode = await proc.exited;
54229
54660
  clearTimeout(timeoutId);
54230
- const stdout = await new Response(proc.stdout).text();
54231
- const stderr = await new Response(proc.stderr).text();
54661
+ const [stdout, stderr] = timedOut ? await (async () => {
54662
+ const stdoutDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
54663
+ const stderrDrain = createDrainDeadline2(STREAM_DRAIN_TIMEOUT_MS2);
54664
+ try {
54665
+ return await Promise.all([
54666
+ Promise.race([stdoutPromise, stdoutDrain.promise]),
54667
+ Promise.race([stderrPromise, stderrDrain.promise])
54668
+ ]);
54669
+ } finally {
54670
+ stdoutDrain.cancel();
54671
+ stderrDrain.cancel();
54672
+ }
54673
+ })() : await Promise.all([stdoutPromise, stderrPromise]);
54232
54674
  const output = (stdout + stderr).trim();
54233
- if (exitCode !== 0 && output === "") {
54675
+ if (timedOut) {
54234
54676
  return {
54235
54677
  success: false,
54236
54678
  output: `Hook timed out after ${timeout}ms`
@@ -54268,7 +54710,7 @@ async function fireHook(config2, event, ctx, workdir) {
54268
54710
  }
54269
54711
  }
54270
54712
  }
54271
- var DEFAULT_TIMEOUT = 5000;
54713
+ var DEFAULT_TIMEOUT = 5000, STREAM_DRAIN_TIMEOUT_MS2 = 2000;
54272
54714
  var init_runner5 = __esm(() => {
54273
54715
  init_env();
54274
54716
  init_logger2();
@@ -54286,7 +54728,7 @@ var package_default;
54286
54728
  var init_package = __esm(() => {
54287
54729
  package_default = {
54288
54730
  name: "@nathapp/nax",
54289
- version: "0.65.1",
54731
+ version: "0.65.3",
54290
54732
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
54291
54733
  type: "module",
54292
54734
  bin: {
@@ -54372,8 +54814,8 @@ var init_version = __esm(() => {
54372
54814
  NAX_VERSION = package_default.version;
54373
54815
  NAX_COMMIT = (() => {
54374
54816
  try {
54375
- if (/^[0-9a-f]{6,10}$/.test("3aa1ff6b"))
54376
- return "3aa1ff6b";
54817
+ if (/^[0-9a-f]{6,10}$/.test("9ff2ea7d"))
54818
+ return "9ff2ea7d";
54377
54819
  } catch {}
54378
54820
  try {
54379
54821
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -90724,6 +91166,7 @@ __export(exports_curator, {
90724
91166
  _curatorCmdDeps: () => _curatorCmdDeps
90725
91167
  });
90726
91168
  import { readdirSync as readdirSync10 } from "fs";
91169
+ import { unlink as unlink4 } from "fs/promises";
90727
91170
  import { basename as basename16, join as join79 } from "path";
90728
91171
  function getProjectKey(config2, projectDir) {
90729
91172
  return config2.name?.trim() || basename16(projectDir);
@@ -90994,6 +91437,16 @@ async function curatorGc(options) {
90994
91437
  `)}
90995
91438
  `;
90996
91439
  await _curatorCmdDeps.writeFile(rollupPath, newContent);
91440
+ const projectKey = getProjectKey(config2, resolved.projectDir);
91441
+ const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
91442
+ const perRunsDir = join79(outputDir, "runs");
91443
+ for (const runId of uniqueRunIds) {
91444
+ if (!keepSet.has(runId)) {
91445
+ const runDir = join79(perRunsDir, runId);
91446
+ await _curatorCmdDeps.removeFile(join79(runDir, "observations.jsonl"));
91447
+ await _curatorCmdDeps.removeFile(join79(runDir, "curator-proposals.md"));
91448
+ }
91449
+ }
90997
91450
  console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
90998
91451
  }
90999
91452
  var _curatorCmdDeps, DEFAULT_KEEP = 50;
@@ -91017,6 +91470,11 @@ var init_curator2 = __esm(() => {
91017
91470
  const prev = await existing.exists() ? await existing.text() : "";
91018
91471
  await Bun.write(p, prev + content);
91019
91472
  },
91473
+ removeFile: async (p) => {
91474
+ try {
91475
+ await unlink4(p);
91476
+ } catch {}
91477
+ },
91020
91478
  openInEditor: async (filePath) => {
91021
91479
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
91022
91480
  const proc = Bun.spawnSync([editor, filePath], { stdio: ["inherit", "inherit", "inherit"] });
@@ -91067,6 +91525,7 @@ async function planCommand(workdir, config2, options) {
91067
91525
  if (!existsSync15(naxDir)) {
91068
91526
  throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
91069
91527
  }
91528
+ validateFeatureName(options.feature);
91070
91529
  const logger = getLogger();
91071
91530
  logger?.info("plan", "Reading spec", { from: options.from });
91072
91531
  const specContent = await _planDeps.readFile(options.from);
@@ -92516,6 +92975,8 @@ var FIELD_DESCRIPTIONS = {
92516
92975
  "quality.commands.typecheck": "Custom typecheck command",
92517
92976
  "quality.commands.lint": "Custom lint command",
92518
92977
  "quality.commands.lintScoped": "Scoped lint command template for story-owned files (supports {{files}})",
92978
+ "quality.commands.lintFixScoped": "Scoped lint fix command template for story-owned files (supports {{files}})",
92979
+ "quality.commands.formatFixScoped": "Scoped format fix command template for story-owned files (supports {{files}})",
92519
92980
  "quality.commands.test": "Custom test command",
92520
92981
  "quality.commands.build": "Custom build command",
92521
92982
  "quality.forceExit": "Append --forceExit to test command (prevents hangs)",
@@ -92549,6 +93010,8 @@ var FIELD_DESCRIPTIONS = {
92549
93010
  "review.commands.typecheck": "Custom typecheck command for review",
92550
93011
  "review.commands.lint": "Custom lint command for review",
92551
93012
  "review.commands.lintScoped": "Scoped lint command template for review (supports {{files}})",
93013
+ "review.commands.lintFixScoped": "Scoped lint fix command template for review (supports {{files}})",
93014
+ "review.commands.formatFixScoped": "Scoped format fix command template for review (supports {{files}})",
92552
93015
  "review.commands.test": "Custom test command for review",
92553
93016
  "review.commands.build": "Custom build command for review",
92554
93017
  "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.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {