@nathapp/nax 0.54.6 → 0.54.8

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 +108 -55
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17982,7 +17982,8 @@ var init_schemas3 = __esm(() => {
17982
17982
  SemanticReviewConfigSchema = exports_external.object({
17983
17983
  modelTier: ModelTierSchema.default("balanced"),
17984
17984
  rules: exports_external.array(exports_external.string()).default([]),
17985
- timeoutMs: exports_external.number().int().positive().default(600000)
17985
+ timeoutMs: exports_external.number().int().positive().default(600000),
17986
+ excludePatterns: exports_external.array(exports_external.string()).default([":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"])
17986
17987
  });
17987
17988
  ReviewConfigSchema = exports_external.object({
17988
17989
  enabled: exports_external.boolean(),
@@ -18278,7 +18279,8 @@ var init_defaults = __esm(() => {
18278
18279
  semantic: {
18279
18280
  modelTier: "balanced",
18280
18281
  rules: [],
18281
- timeoutMs: 600000
18282
+ timeoutMs: 600000,
18283
+ excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
18282
18284
  }
18283
18285
  },
18284
18286
  plan: {
@@ -19927,14 +19929,13 @@ function extractQuestion(output) {
19927
19929
  const text = output.trim();
19928
19930
  if (!text)
19929
19931
  return null;
19930
- const sentences = text.split(/(?<=[.!?])\s+/);
19931
- const questionSentences = sentences.filter((s) => s.trim().endsWith("?"));
19932
- if (questionSentences.length > 0) {
19933
- const q = questionSentences[questionSentences.length - 1].trim();
19934
- if (q.length > 10)
19935
- return q;
19936
- }
19937
- const lower = text.toLowerCase();
19932
+ const lines = text.split(`
19933
+ `).filter((l) => l.trim().length > 0);
19934
+ const lastLine = lines.at(-1)?.trim() ?? "";
19935
+ if (lastLine.endsWith("?") && lastLine.length > 10) {
19936
+ return lastLine;
19937
+ }
19938
+ const lower = lastLine.toLowerCase();
19938
19939
  const markers = [
19939
19940
  "please confirm",
19940
19941
  "please specify",
@@ -19946,7 +19947,7 @@ function extractQuestion(output) {
19946
19947
  ];
19947
19948
  for (const marker of markers) {
19948
19949
  if (lower.includes(marker)) {
19949
- return text.slice(-200).trim();
19950
+ return lastLine;
19950
19951
  }
19951
19952
  }
19952
19953
  return null;
@@ -20133,7 +20134,19 @@ class AcpAgentAdapter {
20133
20134
  };
20134
20135
  }
20135
20136
  async complete(prompt, _options) {
20136
- const model = _options?.model ?? "default";
20137
+ let model = _options?.model;
20138
+ if (!model && _options?.modelTier && _options?.config?.models) {
20139
+ const tier = _options.modelTier;
20140
+ const { resolveModel: resolveModel2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
20141
+ const models = _options.config.models;
20142
+ const entry = models[tier] ?? models.balanced;
20143
+ if (entry) {
20144
+ try {
20145
+ model = resolveModel2(entry).model;
20146
+ } catch {}
20147
+ }
20148
+ }
20149
+ model ??= "default";
20137
20150
  const timeoutMs = _options?.timeoutMs ?? 120000;
20138
20151
  const permissionMode = resolvePermissions(_options?.config, "complete").mode;
20139
20152
  const workdir = _options?.workdir;
@@ -22352,7 +22365,7 @@ var package_default;
22352
22365
  var init_package = __esm(() => {
22353
22366
  package_default = {
22354
22367
  name: "@nathapp/nax",
22355
- version: "0.54.6",
22368
+ version: "0.54.8",
22356
22369
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22357
22370
  type: "module",
22358
22371
  bin: {
@@ -22429,8 +22442,8 @@ var init_version = __esm(() => {
22429
22442
  NAX_VERSION = package_default.version;
22430
22443
  NAX_COMMIT = (() => {
22431
22444
  try {
22432
- if (/^[0-9a-f]{6,10}$/.test("955ab31"))
22433
- return "955ab31";
22445
+ if (/^[0-9a-f]{6,10}$/.test("6f97ec3"))
22446
+ return "6f97ec3";
22434
22447
  } catch {}
22435
22448
  try {
22436
22449
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22478,7 +22491,7 @@ function buildInteractionBridge(chain, context, timeoutMs = DEFAULT_INTERACTION_
22478
22491
  }
22479
22492
  var QUESTION_PATTERNS, DEFAULT_INTERACTION_TIMEOUT_MS = 120000;
22480
22493
  var init_bridge_builder = __esm(() => {
22481
- QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
22494
+ QUESTION_PATTERNS = [/\?\s*$/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
22482
22495
  });
22483
22496
 
22484
22497
  // src/interaction/chain.ts
@@ -24853,9 +24866,13 @@ var init_language_commands = __esm(() => {
24853
24866
 
24854
24867
  // src/review/semantic.ts
24855
24868
  var {spawn: spawn2 } = globalThis.Bun;
24856
- async function collectDiff(workdir, storyGitRef) {
24869
+ async function collectDiff(workdir, storyGitRef, excludePatterns) {
24870
+ const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`];
24871
+ if (excludePatterns.length > 0) {
24872
+ cmd.push("--", ".", ...excludePatterns);
24873
+ }
24857
24874
  const proc = _semanticDeps.spawn({
24858
- cmd: ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`],
24875
+ cmd,
24859
24876
  cwd: workdir,
24860
24877
  stdout: "pipe",
24861
24878
  stderr: "pipe"
@@ -24870,26 +24887,44 @@ async function collectDiff(workdir, storyGitRef) {
24870
24887
  }
24871
24888
  return stdout;
24872
24889
  }
24873
- function truncateDiff(diff) {
24890
+ async function collectDiffStat(workdir, storyGitRef) {
24891
+ const proc = _semanticDeps.spawn({
24892
+ cmd: ["git", "diff", "--stat", `${storyGitRef}..HEAD`],
24893
+ cwd: workdir,
24894
+ stdout: "pipe",
24895
+ stderr: "pipe"
24896
+ });
24897
+ const [exitCode, stdout] = await Promise.all([
24898
+ proc.exited,
24899
+ new Response(proc.stdout).text(),
24900
+ new Response(proc.stderr).text()
24901
+ ]);
24902
+ return exitCode === 0 ? stdout.trim() : "";
24903
+ }
24904
+ function truncateDiff(diff, stat) {
24874
24905
  if (diff.length <= DIFF_CAP_BYTES) {
24875
24906
  return diff;
24876
24907
  }
24877
24908
  const truncated = diff.slice(0, DIFF_CAP_BYTES);
24878
- const fileCount = (truncated.match(/^diff --git/gm) ?? []).length;
24879
- return `${truncated}
24880
- ... (truncated, showing first ${fileCount} files)`;
24909
+ const visibleFiles = (truncated.match(/^diff --git/gm) ?? []).length;
24910
+ const totalFiles = (diff.match(/^diff --git/gm) ?? []).length;
24911
+ const statPreamble = stat ? `## File Summary (all changed files)
24912
+ ${stat}
24913
+
24914
+ ## Diff (truncated \u2014 ${visibleFiles}/${totalFiles} files shown)
24915
+ ` : "";
24916
+ return `${statPreamble}${truncated}
24917
+ ... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
24881
24918
  }
24882
24919
  function buildPrompt(story, semanticConfig, diff) {
24883
24920
  const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
24884
- `);
24885
- const defaultRulesText = DEFAULT_RULES.map((r, i) => `${i + 1}. ${r}`).join(`
24886
24921
  `);
24887
24922
  const customRulesSection = semanticConfig.rules.length > 0 ? `
24888
- ## Custom Rules
24923
+ ## Additional Review Rules
24889
24924
  ${semanticConfig.rules.map((r, i) => `${i + 1}. ${r}`).join(`
24890
24925
  `)}
24891
24926
  ` : "";
24892
- return `You are a code reviewer. Review the following git diff against the story requirements and rules.
24927
+ return `You are a semantic code reviewer. Your job is to verify that the implementation satisfies the story's acceptance criteria (ACs). You are NOT a linter or style checker \u2014 lint, typecheck, and convention checks are handled separately.
24893
24928
 
24894
24929
  ## Story: ${story.title}
24895
24930
 
@@ -24898,27 +24933,29 @@ ${story.description}
24898
24933
 
24899
24934
  ### Acceptance Criteria
24900
24935
  ${acList}
24901
-
24902
- ## Review Rules
24903
-
24904
- ### Default Rules
24905
- ${defaultRulesText}
24906
24936
  ${customRulesSection}
24907
- ## Git Diff
24937
+ ## Git Diff (production code only \u2014 test files excluded)
24908
24938
 
24909
24939
  \`\`\`diff
24910
24940
  ${diff}\`\`\`
24911
24941
 
24912
24942
  ## Instructions
24913
24943
 
24914
- Respond with JSON only. No markdown fences around the JSON response itself.
24915
- Format:
24944
+ For each acceptance criterion, verify the diff implements it correctly. Flag issues only when:
24945
+ 1. An AC is not implemented or partially implemented
24946
+ 2. The implementation contradicts what the AC specifies
24947
+ 3. New code has dead paths that will never execute (stubs, noops, unreachable branches)
24948
+ 4. New code is not wired into callers/exports (written but never used)
24949
+
24950
+ Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.
24951
+
24952
+ Respond in JSON format:
24916
24953
  {
24917
24954
  "passed": boolean,
24918
24955
  "findings": [
24919
24956
  {
24920
24957
  "severity": "error" | "warn" | "info",
24921
- "file": "path/to/file.ts",
24958
+ "file": "path/to/file",
24922
24959
  "line": 42,
24923
24960
  "issue": "description of the issue",
24924
24961
  "suggestion": "how to fix it"
@@ -24926,7 +24963,7 @@ Format:
24926
24963
  ]
24927
24964
  }
24928
24965
 
24929
- If the implementation looks correct, respond with { "passed": true, "findings": [] }.`;
24966
+ If all ACs are correctly implemented, respond with { "passed": true, "findings": [] }.`;
24930
24967
  }
24931
24968
  function parseLLMResponse(raw) {
24932
24969
  try {
@@ -24969,7 +25006,7 @@ function toReviewFindings(findings) {
24969
25006
  source: "semantic-review"
24970
25007
  }));
24971
25008
  }
24972
- async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver) {
25009
+ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig) {
24973
25010
  const startTime = Date.now();
24974
25011
  const logger = getSafeLogger();
24975
25012
  if (!storyGitRef) {
@@ -24982,9 +25019,15 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
24982
25019
  durationMs: Date.now() - startTime
24983
25020
  };
24984
25021
  }
24985
- logger?.info("review", "Running semantic check", { storyId: story.id, modelTier: semanticConfig.modelTier });
24986
- const rawDiff = await collectDiff(workdir, storyGitRef);
24987
- const diff = truncateDiff(rawDiff);
25022
+ logger?.info("review", "Running semantic check", {
25023
+ storyId: story.id,
25024
+ modelTier: semanticConfig.modelTier,
25025
+ configProvided: !!naxConfig
25026
+ });
25027
+ const rawDiff = await collectDiff(workdir, storyGitRef, semanticConfig.excludePatterns);
25028
+ const needsTruncation = rawDiff.length > DIFF_CAP_BYTES;
25029
+ const stat = needsTruncation ? await collectDiffStat(workdir, storyGitRef) : undefined;
25030
+ const diff = truncateDiff(rawDiff, stat);
24988
25031
  const agent = modelResolver(semanticConfig.modelTier);
24989
25032
  if (!agent) {
24990
25033
  logger?.warn("semantic", "No agent available for semantic review \u2014 skipping", {
@@ -25005,7 +25048,9 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
25005
25048
  rawResponse = await agent.complete(prompt, {
25006
25049
  sessionName: `nax-semantic-${story.id}`,
25007
25050
  workdir,
25008
- timeoutMs: semanticConfig.timeoutMs
25051
+ timeoutMs: semanticConfig.timeoutMs,
25052
+ modelTier: semanticConfig.modelTier,
25053
+ config: naxConfig
25009
25054
  });
25010
25055
  } catch (err) {
25011
25056
  logger?.warn("semantic", "LLM call failed \u2014 fail-open", { cause: String(err) });
@@ -25020,6 +25065,20 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
25020
25065
  }
25021
25066
  const parsed = parseLLMResponse(rawResponse);
25022
25067
  if (!parsed) {
25068
+ const looksLikeFail = /"passed"\s*:\s*false/.test(rawResponse);
25069
+ if (looksLikeFail) {
25070
+ logger?.warn("semantic", "LLM returned truncated JSON with passed:false \u2014 treating as failure", {
25071
+ rawResponse: rawResponse.slice(0, 200)
25072
+ });
25073
+ return {
25074
+ check: "semantic",
25075
+ success: false,
25076
+ command: "",
25077
+ exitCode: 1,
25078
+ output: "semantic review: LLM response truncated but indicated failure (passed:false found in partial response)",
25079
+ durationMs: Date.now() - startTime
25080
+ };
25081
+ }
25023
25082
  logger?.warn("semantic", "LLM returned invalid JSON \u2014 fail-open", { rawResponse: rawResponse.slice(0, 200) });
25024
25083
  return {
25025
25084
  check: "semantic",
@@ -25072,19 +25131,12 @@ ${formatFindings(parsed.findings)}`;
25072
25131
  durationMs
25073
25132
  };
25074
25133
  }
25075
- var _semanticDeps, DIFF_CAP_BYTES = 12288, DEFAULT_RULES;
25134
+ var _semanticDeps, DIFF_CAP_BYTES = 51200;
25076
25135
  var init_semantic = __esm(() => {
25077
25136
  init_logger2();
25078
25137
  _semanticDeps = {
25079
25138
  spawn: spawn2
25080
25139
  };
25081
- DEFAULT_RULES = [
25082
- "No stubs or noops left in production code paths",
25083
- "No placeholder values (TODO, FIXME, hardcoded dummy data)",
25084
- "No unrelated changes outside the story scope",
25085
- "All new code is properly wired into callers and exports",
25086
- "No silent error swallowing (catch blocks that discard errors without logging)"
25087
- ];
25088
25140
  });
25089
25141
 
25090
25142
  // src/review/runner.ts
@@ -25231,7 +25283,7 @@ async function getUncommittedFilesImpl(workdir) {
25231
25283
  return [];
25232
25284
  }
25233
25285
  }
25234
- async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver) {
25286
+ async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig) {
25235
25287
  const startTime = Date.now();
25236
25288
  const logger = getSafeLogger();
25237
25289
  const checks3 = [];
@@ -25281,9 +25333,10 @@ Stage and commit these files before running review.`
25281
25333
  const semanticCfg = config2.semantic ?? {
25282
25334
  modelTier: "balanced",
25283
25335
  rules: [],
25284
- timeoutMs: 600000
25336
+ timeoutMs: 600000,
25337
+ excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
25285
25338
  };
25286
- const result2 = await _reviewSemanticDeps.runSemanticReview(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null));
25339
+ const result2 = await _reviewSemanticDeps.runSemanticReview(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null), naxConfig);
25287
25340
  checks3.push(result2);
25288
25341
  if (!result2.success && !firstFailure) {
25289
25342
  firstFailure = `${checkName} failed`;
@@ -25365,9 +25418,9 @@ async function getChangedFiles(workdir, baseRef) {
25365
25418
  }
25366
25419
 
25367
25420
  class ReviewOrchestrator {
25368
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver) {
25421
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig) {
25369
25422
  const logger = getSafeLogger();
25370
- const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver);
25423
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig);
25371
25424
  if (!builtIn.success) {
25372
25425
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
25373
25426
  }
@@ -25466,7 +25519,7 @@ var init_review = __esm(() => {
25466
25519
  title: ctx.story.title,
25467
25520
  description: ctx.story.description,
25468
25521
  acceptanceCriteria: ctx.story.acceptanceCriteria
25469
- }, modelResolver);
25522
+ }, modelResolver, ctx.config);
25470
25523
  ctx.reviewResult = result.builtIn;
25471
25524
  if (!result.success) {
25472
25525
  const pluginFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.6",
3
+ "version": "0.54.8",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {