@nathapp/nax 0.81.0-canary.1 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +225 -269
  2. package/package.json +3 -1
package/dist/nax.js CHANGED
@@ -17148,8 +17148,7 @@ var init_schemas_infra = __esm(() => {
17148
17148
  agents: AgentRoutingConfigSchema.default({ enabled: true, strategy: "off", profiles: [] })
17149
17149
  });
17150
17150
  OptimizerConfigSchema = exports_external.object({
17151
- enabled: exports_external.boolean(),
17152
- strategy: exports_external.enum(["rule-based", "llm", "noop"]).optional()
17151
+ enabled: exports_external.boolean()
17153
17152
  });
17154
17153
  PluginConfigEntrySchema = exports_external.object({
17155
17154
  module: exports_external.string().min(1, "plugin.module must be non-empty"),
@@ -18840,6 +18839,21 @@ function _applyRemovedRoutingKeysShim(conf, warn = defaultConfigWarn) {
18840
18839
  }
18841
18840
  return newRouting === routing ? conf : { ...conf, routing: newRouting };
18842
18841
  }
18842
+ function _applyRemovedOptimizerKeysShim(conf, warn = defaultConfigWarn) {
18843
+ const optimizer = conf.optimizer;
18844
+ if (!optimizer || typeof optimizer !== "object")
18845
+ return conf;
18846
+ const REMOVED_OPTIMIZER_KEYS = ["strategy", "strategies"];
18847
+ let newOptimizer = optimizer;
18848
+ for (const key of REMOVED_OPTIMIZER_KEYS) {
18849
+ if (key in newOptimizer) {
18850
+ warn(`optimizer.${key} was removed along with the rule-based optimizer and has no effect. Remove it from your config; set optimizer.enabled and provide a plugin optimizer instead.`);
18851
+ const { [key]: _removed, ...rest } = newOptimizer;
18852
+ newOptimizer = rest;
18853
+ }
18854
+ }
18855
+ return newOptimizer === optimizer ? conf : { ...conf, optimizer: newOptimizer };
18856
+ }
18843
18857
  function _applyRemovedWorktreeInheritShim(conf, warn = defaultConfigWarn) {
18844
18858
  const execution = conf.execution;
18845
18859
  const worktreeDependencies = execution?.worktreeDependencies;
@@ -18968,7 +18982,17 @@ function _applyFinishAutoFlowShim(conf, warn = defaultConfigWarn) {
18968
18982
  function applyConfigCompatShims(conf, logger, dedupe) {
18969
18983
  const log = dedupe.wrapLogger(logger);
18970
18984
  const warn = dedupe.warn;
18971
- return _applyFinishAutoFlowShim(_applyRemovedWorktreeInheritShim(_applyLegacyReviewExecutionShim(_applyRemovedRoutingKeysShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(conf, log), log), warn), warn), warn), warn), warn), warn), warn);
18985
+ let out = migrateLegacyTestPattern(conf, log);
18986
+ out = migrateLegacyReviewModelKey(out, log);
18987
+ out = applyRemovedStrategyCompat(out, warn);
18988
+ out = applyBatchModeCompat(out, warn);
18989
+ out = applyRoutingRetryDeprecationWarning(out, warn);
18990
+ out = _applyRemovedRoutingKeysShim(out, warn);
18991
+ out = _applyLegacyReviewExecutionShim(out, warn);
18992
+ out = _applyRemovedWorktreeInheritShim(out, warn);
18993
+ out = _applyFinishAutoFlowShim(out, warn);
18994
+ out = _applyRemovedOptimizerKeysShim(out, warn);
18995
+ return out;
18972
18996
  }
18973
18997
  var SECURITY_SENSITIVE_KEY_PATHS, REMOVED_FINISH_KEYS;
18974
18998
  var init_compat_shims = __esm(() => {
@@ -29969,186 +29993,20 @@ class NoopOptimizer {
29969
29993
  }
29970
29994
  var init_noop_optimizer = () => {};
29971
29995
 
29972
- // src/optimizer/rule-based.optimizer.ts
29973
- class RuleBasedOptimizer {
29974
- name = "rule-based";
29975
- async optimize(input) {
29976
- const originalTokens = estimateTokens2(input.prompt);
29977
- const appliedRules = [];
29978
- let optimized = input.prompt;
29979
- const config2 = {
29980
- ...DEFAULT_CONFIG2,
29981
- ...input.config.optimizer?.strategies?.["rule-based"]
29982
- };
29983
- if (config2.stripWhitespace) {
29984
- const before = optimized;
29985
- optimized = this.stripWhitespace(optimized);
29986
- if (optimized !== before) {
29987
- appliedRules.push("stripWhitespace");
29988
- }
29989
- }
29990
- if (config2.compactCriteria) {
29991
- const before = optimized;
29992
- optimized = this.compactCriteria(optimized);
29993
- if (optimized !== before) {
29994
- appliedRules.push("compactCriteria");
29995
- }
29996
- }
29997
- if (config2.deduplicateContext && input.contextMarkdown) {
29998
- const before = optimized;
29999
- optimized = this.deduplicateContext(optimized, input.contextMarkdown);
30000
- if (optimized !== before) {
30001
- appliedRules.push("deduplicateContext");
30002
- }
30003
- }
30004
- if (config2.maxPromptTokens) {
30005
- const currentTokens = estimateTokens2(optimized);
30006
- if (currentTokens > config2.maxPromptTokens) {
30007
- optimized = this.trimToMaxTokens(optimized, config2.maxPromptTokens);
30008
- appliedRules.push("maxPromptTokens");
30009
- }
30010
- }
30011
- const optimizedTokens = estimateTokens2(optimized);
30012
- const savings = originalTokens > 0 ? (originalTokens - optimizedTokens) / originalTokens : 0;
30013
- return {
30014
- prompt: optimized,
30015
- originalTokens,
30016
- optimizedTokens,
30017
- savings,
30018
- appliedRules
30019
- };
30020
- }
30021
- stripWhitespace(prompt) {
30022
- return prompt.split(`
30023
- `).map((line) => line.trimEnd()).join(`
30024
- `).replace(/\n{3,}/g, `
30025
-
30026
- `);
30027
- }
30028
- compactCriteria(prompt) {
30029
- return prompt.replace(/The user should be able to /gi, "").replace(/The system must /gi, "").replace(/The system should /gi, "").replace(/When the /gi, "").replace(/When a /gi, "").replace(/it should validate all fields/gi, "validate all fields").replace(/display an error message/gi, "show error").replace(/error message/gi, "error");
30030
- }
30031
- deduplicateContext(prompt, contextMarkdown) {
30032
- const contextSectionMatch = prompt.match(/# Context\n([\s\S]*?)(?=\n#|$)/i);
30033
- if (!contextSectionMatch) {
30034
- return prompt;
30035
- }
30036
- const contextSection = contextSectionMatch[1];
30037
- const contextLines = contextSection.split(`
30038
- `);
30039
- const dedupedLines = contextLines.filter((line) => {
30040
- const trimmed = line.trim();
30041
- if (!trimmed)
30042
- return true;
30043
- return !contextMarkdown.includes(trimmed);
30044
- });
30045
- if (dedupedLines.length < contextLines.length) {
30046
- const newContextSection = dedupedLines.join(`
30047
- `);
30048
- return prompt.replace(contextSectionMatch[0], `# Context
30049
- ${newContextSection}`);
30050
- }
30051
- return prompt;
30052
- }
30053
- trimToMaxTokens(prompt, maxTokens) {
30054
- const currentTokens = estimateTokens2(prompt);
30055
- if (currentTokens <= maxTokens) {
30056
- return prompt;
30057
- }
30058
- const sections = this.extractSections(prompt);
30059
- const targetChars = maxTokens * 4;
30060
- const trimmedMessage = `
30061
- ... (context trimmed)`;
30062
- let result = "";
30063
- let remainingChars = targetChars;
30064
- if (sections.task) {
30065
- result += sections.task;
30066
- remainingChars -= sections.task.length;
30067
- }
30068
- if (sections.acceptanceCriteria) {
30069
- result += sections.acceptanceCriteria;
30070
- remainingChars -= sections.acceptanceCriteria.length;
30071
- }
30072
- const trimmable = sections.context ?? sections.other;
30073
- if (trimmable && remainingChars > 0) {
30074
- const reserveForMessage = trimmable.length > remainingChars ? trimmedMessage.length : 0;
30075
- const maxContextChars = Math.max(0, remainingChars - reserveForMessage);
30076
- const trimmedContext = trimmable.substring(0, maxContextChars);
30077
- result += trimmedContext;
30078
- if (trimmedContext.length < trimmable.length) {
30079
- result += trimmedMessage;
30080
- }
30081
- }
30082
- return result.trim() ? result : prompt;
30083
- }
30084
- extractSections(prompt) {
30085
- const sections = {};
30086
- const taskMatch = prompt.match(/# Task\n([\s\S]*?)(?=\n#|$)/i);
30087
- if (taskMatch) {
30088
- sections.task = taskMatch[0];
30089
- }
30090
- const contextMatch = prompt.match(/# Context\n([\s\S]*?)(?=\n#|$)/i);
30091
- if (contextMatch) {
30092
- sections.context = contextMatch[0];
30093
- }
30094
- const acMatch = prompt.match(/# Acceptance Criteria\n([\s\S]*?)(?=\n#|$)/i);
30095
- if (acMatch) {
30096
- sections.acceptanceCriteria = acMatch[0];
30097
- }
30098
- let other = prompt;
30099
- if (sections.task) {
30100
- other = other.replace(sections.task, "");
30101
- }
30102
- if (sections.context) {
30103
- other = other.replace(sections.context, "");
30104
- }
30105
- if (sections.acceptanceCriteria) {
30106
- other = other.replace(sections.acceptanceCriteria, "");
30107
- }
30108
- if (other.trim()) {
30109
- sections.other = other;
30110
- }
30111
- return sections;
30112
- }
30113
- }
30114
- var DEFAULT_CONFIG2;
30115
- var init_rule_based_optimizer = __esm(() => {
30116
- DEFAULT_CONFIG2 = {
30117
- stripWhitespace: true,
30118
- compactCriteria: true,
30119
- deduplicateContext: true,
30120
- maxPromptTokens: 8000
30121
- };
30122
- });
30123
-
30124
29996
  // src/optimizer/index.ts
30125
29997
  function resolveOptimizer(config2, pluginRegistry) {
30126
29998
  if (!config2.optimizer?.enabled) {
30127
29999
  return new NoopOptimizer;
30128
30000
  }
30129
- if (pluginRegistry) {
30130
- const pluginOptimizers = pluginRegistry.getOptimizers();
30131
- if (pluginOptimizers.length > 0) {
30132
- return pluginOptimizers[0];
30133
- }
30134
- }
30135
- const strategy = config2.optimizer.strategy ?? "noop";
30136
- switch (strategy) {
30137
- case "rule-based":
30138
- return new RuleBasedOptimizer;
30139
- case "noop":
30140
- return new NoopOptimizer;
30141
- default:
30142
- getSafeLogger()?.warn("optimizer", `Unknown optimizer strategy '${strategy}', using noop`);
30143
- return new NoopOptimizer;
30001
+ const pluginOptimizers = pluginRegistry?.getOptimizers() ?? [];
30002
+ if (pluginOptimizers.length > 0) {
30003
+ return pluginOptimizers[0];
30144
30004
  }
30005
+ return new NoopOptimizer;
30145
30006
  }
30146
30007
  var init_optimizer = __esm(() => {
30147
30008
  init_noop_optimizer();
30148
- init_rule_based_optimizer();
30149
- init_logger2();
30150
30009
  init_noop_optimizer();
30151
- init_rule_based_optimizer();
30152
30010
  });
30153
30011
 
30154
30012
  // src/context/fragments/store.ts
@@ -33625,24 +33483,23 @@ async function saveRunMetrics(outputDir, runMetrics) {
33625
33483
  cacheCreationInputTokens: totalCacheCreationInputTokens
33626
33484
  })
33627
33485
  } : runMetrics;
33628
- const allMetrics = await withPathFileLock(metricsPath, async () => {
33486
+ await withPathFileLock(metricsPath, async () => {
33629
33487
  const existing = await loadJsonFile(metricsPath, "metrics");
33630
- const base = Array.isArray(existing) ? existing : [];
33631
- base.push(finalMetrics);
33632
- return base;
33488
+ const allMetrics = Array.isArray(existing) ? existing : [];
33489
+ allMetrics.push(finalMetrics);
33490
+ const isTruncating = allMetrics.length > MAX_RETAINED_RUNS;
33491
+ const cappedMetrics = isTruncating ? allMetrics.slice(allMetrics.length - MAX_RETAINED_RUNS) : allMetrics;
33492
+ if (isTruncating && !hasWarnedAboutRunTruncation) {
33493
+ hasWarnedAboutRunTruncation = true;
33494
+ const droppedCount = allMetrics.length - MAX_RETAINED_RUNS;
33495
+ getLogger().warn("metrics", "Run-history cap reached \u2014 oldest run-entries dropped from metrics.json; aggregate metrics now cover only the retained window", {
33496
+ droppedCount,
33497
+ maxRetainedRuns: MAX_RETAINED_RUNS,
33498
+ metricsPath
33499
+ });
33500
+ }
33501
+ await saveJsonFile(metricsPath, cappedMetrics, "metrics");
33633
33502
  });
33634
- const isTruncating = allMetrics.length > MAX_RETAINED_RUNS;
33635
- const cappedMetrics = isTruncating ? allMetrics.slice(allMetrics.length - MAX_RETAINED_RUNS) : allMetrics;
33636
- if (isTruncating && !hasWarnedAboutRunTruncation) {
33637
- hasWarnedAboutRunTruncation = true;
33638
- const droppedCount = allMetrics.length - MAX_RETAINED_RUNS;
33639
- getLogger().warn("metrics", "Run-history cap reached \u2014 oldest run-entries dropped from metrics.json; aggregate metrics now cover only the retained window", {
33640
- droppedCount,
33641
- maxRetainedRuns: MAX_RETAINED_RUNS,
33642
- metricsPath
33643
- });
33644
- }
33645
- await saveJsonFile(metricsPath, cappedMetrics, "metrics");
33646
33503
  }
33647
33504
  async function loadRunMetrics(outputDir) {
33648
33505
  const metricsPath = metricsPathFor(outputDir);
@@ -48370,7 +48227,35 @@ async function exists(workdir, rel) {
48370
48227
  return false;
48371
48228
  }
48372
48229
  }
48373
- async function auditGaps(report, workdir) {
48230
+ function isNoise(rel) {
48231
+ const segments = rel.split("/");
48232
+ if (segments.includes(".nax"))
48233
+ return true;
48234
+ const name = segments[segments.length - 1];
48235
+ if (name.endsWith(".lock") || name === "package-lock.json" || name === "pnpm-lock.yaml")
48236
+ return true;
48237
+ if (/(^|\/)(dist|build|\.next|\.turbo|__pycache__)(\/|$)/.test(rel))
48238
+ return true;
48239
+ if (/\.generated\./.test(name))
48240
+ return true;
48241
+ return false;
48242
+ }
48243
+ function walkPathToken(line) {
48244
+ const token = line.trim().split(/\s+/)[0];
48245
+ return token.length > 0 ? token : null;
48246
+ }
48247
+ async function changedFiles(workdir, range) {
48248
+ try {
48249
+ const { stdout, exitCode } = await gitWithTimeout(["diff", "--name-only", `${range.base}...${range.head}`], workdir);
48250
+ if (exitCode !== 0)
48251
+ return [];
48252
+ return stdout.split(`
48253
+ `).map((s) => s.trim()).filter((s) => s.length > 0 && !isNoise(s));
48254
+ } catch {
48255
+ return [];
48256
+ }
48257
+ }
48258
+ async function auditGaps(report, workdir, range, phase = "spec") {
48374
48259
  const gaps = [];
48375
48260
  const touchpoints = report.touchpoints ?? [];
48376
48261
  if (!report.sawTouchpointsSection || touchpoints.length === 0) {
@@ -48378,13 +48263,29 @@ async function auditGaps(report, workdir) {
48378
48263
  } else if (!touchpoints.some((t) => t.path === "none")) {
48379
48264
  const checked = touchpoints.slice(0, MAX_CHECKED);
48380
48265
  const found = await Promise.all(checked.map((t) => exists(workdir, t.path)));
48381
- if (!found.some(Boolean)) {
48266
+ const foundCount = found.filter(Boolean).length;
48267
+ if (foundCount * 2 <= checked.length) {
48382
48268
  gaps.push(`touchpoint path does not exist in the repo (checked: ${checked.map((t) => t.path).join(", ")}) \u2014 list files you actually opened`);
48383
48269
  }
48384
48270
  }
48385
48271
  if (!report.sawWalkSection || (report.walk ?? []).length === 0) {
48386
48272
  gaps.push("no `## WALK` section: the per-AC (spec) or per-function (quality) enumeration is required");
48387
48273
  }
48274
+ if (phase === "quality" && range) {
48275
+ const required2 = await changedFiles(workdir, range);
48276
+ if (required2.length > 0) {
48277
+ const walked = new Set;
48278
+ for (const line of report.walk ?? []) {
48279
+ const token = walkPathToken(line);
48280
+ if (token && required2.includes(token))
48281
+ walked.add(token);
48282
+ }
48283
+ const unwalked = required2.filter((file3) => !walked.has(file3));
48284
+ if (unwalked.length > 0) {
48285
+ gaps.push(`unwalked changed files not named in \`## WALK\` (${unwalked.length}): ${unwalked.join(", ")} \u2014 walk every changed file`);
48286
+ }
48287
+ }
48288
+ }
48388
48289
  return gaps;
48389
48290
  }
48390
48291
  async function validateDispositions(workdir, dispositions) {
@@ -48396,7 +48297,9 @@ async function validateDispositions(workdir, dispositions) {
48396
48297
  }));
48397
48298
  }
48398
48299
  var MAX_CHECKED = 20;
48399
- var init_audit_gaps = () => {};
48300
+ var init_audit_gaps = __esm(() => {
48301
+ init_git();
48302
+ });
48400
48303
 
48401
48304
  // src/finish/review/parse.ts
48402
48305
  function parseTouchpoint(text) {
@@ -48429,13 +48332,15 @@ function parseReviewReport(text) {
48429
48332
  let section = "findings";
48430
48333
  let current = null;
48431
48334
  let lastField = null;
48335
+ const normalized = text.replace(GLUED_HEADING, `$1
48336
+ $2`);
48432
48337
  const flush = () => {
48433
48338
  if (current)
48434
48339
  report.findings.push(current);
48435
48340
  current = null;
48436
48341
  lastField = null;
48437
48342
  };
48438
- for (const line of text.split(`
48343
+ for (const line of normalized.split(`
48439
48344
  `)) {
48440
48345
  const heading = HEADING.exec(line);
48441
48346
  if (heading) {
@@ -48514,9 +48419,10 @@ function parseDispositions(text) {
48514
48419
  }
48515
48420
  return out;
48516
48421
  }
48517
- var HEADING, BLOCK, FIELD, NO_FINDINGS, BULLET, DISPOSITION, EVIDENCE;
48422
+ var HEADING, GLUED_HEADING, BLOCK, FIELD, NO_FINDINGS, BULLET, DISPOSITION, EVIDENCE;
48518
48423
  var init_parse5 = __esm(() => {
48519
48424
  HEADING = /^\s*#{1,6}\s*(TOUCHPOINTS|WALK|FINDINGS|DISPOSITIONS)\s*:?\s*$/i;
48425
+ GLUED_HEADING = /([^\s#])(#{1,6}[ \t]*(?:TOUCHPOINTS|WALK|FINDINGS|DISPOSITIONS)[ \t]*:?)(?=[ \t]*(?:\r?\n|$))/gi;
48520
48426
  BLOCK = /^\s*\[(CRITICAL|HIGH|MEDIUM|LOW)\]\s+(.+?)\s*$/;
48521
48427
  FIELD = /^\s*(Problem|Fix|Judgment)\s*:\s*(.*)$/i;
48522
48428
  NO_FINDINGS = /^\s*no findings\.?\s*$/i;
@@ -48678,12 +48584,20 @@ test code \u2014 and judge each changed function on its own merits.
48678
48584
 
48679
48585
  ## Forcing function \u2014 enumerate before you conclude
48680
48586
 
48681
- Before reporting, walk **every function/method the diff adds or changes** and
48682
- write yourself a one-line verdict for each: *earns its place* or *concern: \u2026*.
48683
- This enumeration is a thinking tool \u2014 it does not go in the final report, only
48684
- the resulting findings do. Skipping it is how real maintainability issues get
48685
- missed: an agent that pattern-matches a few obvious smells and stops will always
48686
- under-report. Look at each changed function deliberately.
48587
+ Before reporting, walk **every function/method the diff adds or changes \u2014 in
48588
+ test files as well as production ones** \u2014 and write yourself a one-line verdict
48589
+ for each: *earns its place* or *concern: \u2026*. Skipping it is how real
48590
+ maintainability issues get missed: an agent that pattern-matches a few obvious
48591
+ smells and stops will always under-report. Look at each changed function
48592
+ deliberately.
48593
+
48594
+ This per-function walk is **private scratch work** \u2014 it stays in your reasoning,
48595
+ you do not transcribe it into the reply. What you emit, in the \`## WALK\` section
48596
+ the reply contract requires, is **one line per changed file**, carrying that
48597
+ file's verdict (\`path \u2014 earns its place|concern: <one clause>\`). The
48598
+ per-function walk is what finds the defects; the per-file WALK is the
48599
+ checkable evidence that you walked the diff. A reply without the \`## WALK\`
48600
+ section is treated as an incomplete review and sent back.
48687
48601
 
48688
48602
  ## What to look for
48689
48603
 
@@ -48874,7 +48788,7 @@ reads to judge an integration-shaped defect.)
48874
48788
 
48875
48789
  // src/finish/review/prompt.ts
48876
48790
  function outputContract(phase) {
48877
- const walk = phase === "spec" ? "one line per AC in the spec: `AC-3 Covered|Partial|Missing \u2014 <one clause>`" : "one line per function or method the diff adds or changes: `path.ts:name \u2014 earns its place|concern: <one clause>`";
48791
+ const walk = phase === "spec" ? "one line per AC in the spec: `AC-3 Covered|Partial|Missing \u2014 <one clause>`" : "one line per file the diff adds or changes: `path.ts \u2014 earns its place|concern: <one clause>`";
48878
48792
  return `# Reply contract \u2014 your reply must be these three sections, in this order
48879
48793
 
48880
48794
  ## TOUCHPOINTS
@@ -48908,10 +48822,11 @@ function buildReviewPrompt(phase, args) {
48908
48822
  `)
48909
48823
  ] : [];
48910
48824
  if (!args.since) {
48825
+ const specNotice = phase === "spec" && args.specPath ? [`The spec/requirements source is: ${args.specPath}. Read it in full.`] : [];
48911
48826
  return [
48912
48827
  ...gapNotice,
48913
48828
  `You are the ${phase.toUpperCase()} reviewer for a completed feature.`,
48914
- `The spec/requirements source is: ${args.specPath}. Read it in full.`,
48829
+ ...specNotice,
48915
48830
  `Fetch and review the diff: \`git diff ${args.base}...HEAD\` (also \`--name-only\` for the file list).`,
48916
48831
  WORKER_PROTOCOL_MECHANICS,
48917
48832
  dims,
@@ -48921,8 +48836,7 @@ function buildReviewPrompt(phase, args) {
48921
48836
 
48922
48837
  `);
48923
48838
  }
48924
- return [
48925
- ...gapNotice,
48839
+ const reReviewBody = [
48926
48840
  `You are the ${phase.toUpperCase()} reviewer for a completed feature, continuing a review you already started.`,
48927
48841
  `On your previous pass over \`git diff ${args.base}...HEAD\` you raised the findings below, and they have since been fixed and committed. Everything else in that diff you already judged acceptable \u2014 do not re-derive a verdict on it.`,
48928
48842
  `Your findings from the previous pass:
@@ -48932,14 +48846,19 @@ ${JSON.stringify(args.priorFindings ?? [], null, 2)}`,
48932
48846
  "1. **Resolved?** Does the fix actually resolve each finding above? A finding that was papered over (assertion weakened, test deleted, check disabled) is NOT resolved \u2014 re-raise it.",
48933
48847
  "2. **Broken?** Did the fix introduce a new problem, in the changed lines or in the unchanged code they now call into?",
48934
48848
  "",
48935
- `Read whatever files you need \u2014 the spec is at ${args.specPath} and the whole repo is available. Scope means *what you judge*, not *what you may read*.`
48849
+ ...phase === "spec" && args.specPath ? [
48850
+ `Read whatever files you need \u2014 the spec is at ${args.specPath} and the whole repo is available. Scope means *what you judge*, not *what you may read*.`
48851
+ ] : [
48852
+ "Read whatever files you need \u2014 the whole repo is available. Scope means *what you judge*, not *what you may read*."
48853
+ ]
48936
48854
  ].join(`
48937
48855
  `),
48938
48856
  WORKER_PROTOCOL_MECHANICS,
48939
48857
  dims,
48940
48858
  CLASSIFIER,
48941
48859
  outputContract(phase)
48942
- ].join(`
48860
+ ];
48861
+ return [...gapNotice, ...reReviewBody].join(`
48943
48862
 
48944
48863
  `);
48945
48864
  }
@@ -49087,7 +49006,10 @@ var init_finish_review = __esm(() => {
49087
49006
  exhaustedFallback: () => EMPTY_REVIEW_REPORT
49088
49007
  }),
49089
49008
  async verify(parsed, input, _verifyCtx) {
49090
- return { ...parsed, gaps: await auditGaps(parsed, input.workdir) };
49009
+ return {
49010
+ ...parsed,
49011
+ gaps: await auditGaps(parsed, input.workdir, { base: input.base, head: "HEAD" }, input.phase)
49012
+ };
49091
49013
  }
49092
49014
  };
49093
49015
  });
@@ -50719,7 +50641,7 @@ var package_default;
50719
50641
  var init_package = __esm(() => {
50720
50642
  package_default = {
50721
50643
  name: "@nathapp/nax",
50722
- version: "0.81.0-canary.1",
50644
+ version: "0.81.0",
50723
50645
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
50724
50646
  type: "module",
50725
50647
  bin: {
@@ -50761,6 +50683,8 @@ var init_package = __esm(() => {
50761
50683
  "test:e2e": "timeout -k 5s 180s bun test test/e2e/ --timeout=60000",
50762
50684
  "test:coverage": "bun run scripts/check-coverage.ts",
50763
50685
  "test:coverage:report": "bun run scripts/check-coverage.ts --report",
50686
+ "test:coverage:update": "bun run scripts/check-coverage.ts --update-baseline",
50687
+ "test:coverage:list": "bun run scripts/check-coverage.ts --list",
50764
50688
  "report:test-overlap": "bun run scripts/report-test-overlap.ts",
50765
50689
  "report:dead-tests": "bun run scripts/report-dead-tests.ts",
50766
50690
  "check:test-mocks": "bun scripts/check-inline-test-mocks.ts --strict",
@@ -50834,8 +50758,8 @@ var init_version = __esm(() => {
50834
50758
  NAX_VERSION = package_default.version;
50835
50759
  NAX_COMMIT = (() => {
50836
50760
  try {
50837
- if (/^[0-9a-f]{6,10}$/.test("ac91a9a7"))
50838
- return "ac91a9a7";
50761
+ if (/^[0-9a-f]{6,10}$/.test("a2cc47be"))
50762
+ return "a2cc47be";
50839
50763
  } catch {}
50840
50764
  try {
50841
50765
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -53582,10 +53506,10 @@ function formatDuration2(ms) {
53582
53506
  return `${minutes}m ${seconds}s`;
53583
53507
  }
53584
53508
  function timeoutRetry(input) {
53585
- const { prompt, changedFiles, elapsedMs, attempt } = input;
53509
+ const { prompt, changedFiles: changedFiles2, elapsedMs, attempt } = input;
53586
53510
  const duration3 = formatDuration2(elapsedMs);
53587
53511
  const attemptNumber = attempt + 1;
53588
- if (changedFiles.length === 0) {
53512
+ if (changedFiles2.length === 0) {
53589
53513
  return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}) with no file changes on disk.
53590
53514
  This is attempt ${attemptNumber} of the same story \u2014 the previous attempt left nothing behind, so the approach was wrong.
53591
53515
  Change your approach: pick a narrower scope, fewer file edits, or a different angle on the acceptance criteria.
@@ -53594,7 +53518,7 @@ Change your approach: pick a narrower scope, fewer file edits, or a different an
53594
53518
 
53595
53519
  ${prompt}`;
53596
53520
  }
53597
- const fileList = changedFiles.map((p) => `- ${p}`).join(`
53521
+ const fileList = changedFiles2.map((p) => `- ${p}`).join(`
53598
53522
  `);
53599
53523
  return `The previous attempt hit a timeout after ${elapsedMs}ms (${duration3}), but left these files on disk:
53600
53524
 
@@ -54606,11 +54530,11 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
54606
54530
  }
54607
54531
  if (hopKind.kind === "timeout-retry") {
54608
54532
  const preAttemptGitRef = preAttemptGitRefPromise ? await preAttemptGitRefPromise : undefined;
54609
- const changedFiles = preAttemptGitRef ? await _buildHopCallbackDeps.captureWorkingTreeChanges(workdir, preAttemptGitRef) : [];
54533
+ const changedFiles2 = preAttemptGitRef ? await _buildHopCallbackDeps.captureWorkingTreeChanges(workdir, preAttemptGitRef) : [];
54610
54534
  const elapsedMs = elapsedSincePriorHop;
54611
54535
  prompt = _buildHopCallbackDeps.timeoutRetry({
54612
54536
  prompt: resolvedRunOptions.prompt,
54613
- changedFiles,
54537
+ changedFiles: changedFiles2,
54614
54538
  elapsedMs,
54615
54539
  attempt: hopKind.attempt
54616
54540
  });
@@ -62252,7 +62176,7 @@ async function checkGitignoreCoversNax(workdir) {
62252
62176
  "**/.nax-acceptance*",
62253
62177
  "**/_nax_acceptance_test.py",
62254
62178
  "**/_nax_suggested_test.py",
62255
- `**/${PROJECT_FEATURES_DIR}/*/`
62179
+ `${PROJECT_FEATURES_DIR}/*/fragments/`
62256
62180
  ];
62257
62181
  const missing = patterns.filter((pattern) => !content.includes(pattern));
62258
62182
  const passed = missing.length === 0;
@@ -65650,8 +65574,8 @@ var init_completion = __esm(() => {
65650
65574
  }
65651
65575
  if (fragmentsEnabled) {
65652
65576
  try {
65653
- const changedFiles = [...await _completionDeps.getDiffFilePaths(ctx.workdir, ctx.storyGitRef)];
65654
- const body = _completionDeps.renderFragmentBody(ctx.story.id, ctx.story.title, ctx.story.acceptanceCriteria, changedFiles);
65577
+ const changedFiles2 = [...await _completionDeps.getDiffFilePaths(ctx.workdir, ctx.storyGitRef)];
65578
+ const body = _completionDeps.renderFragmentBody(ctx.story.id, ctx.story.title, ctx.story.acceptanceCriteria, changedFiles2);
65655
65579
  const maxTokens = ctx.config.context.v2.fragments.maxTokens;
65656
65580
  await _completionDeps.writeFragment(ctx.projectDir, featureId, ctx.story.id, body, maxTokens);
65657
65581
  } catch (err) {
@@ -66046,7 +65970,7 @@ async function acquireLock(workdir) {
66046
65970
  }
66047
65971
  const tombstonePath = `${lockPath}.stale.${process.pid}.${Date.now()}`;
66048
65972
  try {
66049
- await rename2(lockPath, tombstonePath);
65973
+ await _lockDeps.rename(lockPath, tombstonePath);
66050
65974
  } catch (renameError) {
66051
65975
  if (renameError.code === "ENOENT") {
66052
65976
  return false;
@@ -66111,8 +66035,12 @@ async function releaseLock(workdir) {
66111
66035
  }
66112
66036
  }
66113
66037
  }
66038
+ var _lockDeps;
66114
66039
  var init_lock = __esm(() => {
66115
66040
  init_logger2();
66041
+ _lockDeps = {
66042
+ rename: rename2
66043
+ };
66116
66044
  });
66117
66045
 
66118
66046
  // src/execution/helpers.ts
@@ -70664,11 +70592,11 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
70664
70592
  const repoRoot = acceptanceContext.projectDir ?? workdir;
70665
70593
  const packageDir = acceptanceContext.story.workdir && acceptanceContext.projectDir ? path15.join(acceptanceContext.projectDir, acceptanceContext.story.workdir) : undefined;
70666
70594
  const ignoreMatchers = acceptanceContext.naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
70667
- const changedFiles = filterNaxInternalPaths(changedFilesRaw, ignoreMatchers);
70595
+ const changedFiles2 = filterNaxInternalPaths(changedFilesRaw, ignoreMatchers);
70668
70596
  const MAX_BYTES = 51200;
70669
70597
  let totalBytes = 0;
70670
70598
  const entries = [];
70671
- for (const file3 of changedFiles) {
70599
+ for (const file3 of changedFiles2) {
70672
70600
  if (totalBytes >= MAX_BYTES)
70673
70601
  break;
70674
70602
  const filePath = path15.join(workdir, file3);
@@ -75017,12 +74945,12 @@ async function runDeferredReview(workdir, _reviewConfig, plugins, runStartRef, n
75017
74945
  }
75018
74946
  const changedFilesRaw = await getChangedFilesForDeferred(workdir, runStartRef);
75019
74947
  const ignoreMatchers = naxIgnoreIndex?.getMatchers() ?? await resolveNaxIgnorePatterns(workdir);
75020
- const changedFiles = filterNaxInternalPaths(changedFilesRaw, ignoreMatchers);
74948
+ const changedFiles2 = filterNaxInternalPaths(changedFilesRaw, ignoreMatchers);
75021
74949
  const reviewerResults = [];
75022
74950
  let anyFailed = false;
75023
74951
  for (const reviewer of reviewers) {
75024
74952
  try {
75025
- const result = await reviewer.check(workdir, changedFiles);
74953
+ const result = await reviewer.check(workdir, changedFiles2);
75026
74954
  reviewerResults.push({
75027
74955
  name: reviewer.name,
75028
74956
  passed: result.passed,
@@ -75043,7 +74971,7 @@ async function runDeferredReview(workdir, _reviewConfig, plugins, runStartRef, n
75043
74971
  anyFailed = true;
75044
74972
  }
75045
74973
  }
75046
- return { runStartRef, changedFiles, reviewerResults, anyFailed };
74974
+ return { runStartRef, changedFiles: changedFiles2, reviewerResults, anyFailed };
75047
74975
  }
75048
74976
  var _deferredReviewDeps;
75049
74977
  var init_deferred_review = __esm(() => {
@@ -75682,7 +75610,7 @@ ${missing.join(`
75682
75610
  `);
75683
75611
  return { created: false, added: [...missing] };
75684
75612
  }
75685
- var NAX_GITIGNORE_ENTRIES, NAX_NAXIGNORE_ENTRIES, NAX_NAXIGNORE_HEADER = `# nax - paths excluded from context, review
75613
+ var FEATURE_RUN_ARTIFACTS, NAX_GITIGNORE_ENTRIES, NAX_NAXIGNORE_ENTRIES, NAX_NAXIGNORE_HEADER = `# nax - paths excluded from context, review
75686
75614
  # and verification scanning.
75687
75615
  # gitignore syntax. Also honoured per-package.
75688
75616
 
@@ -75695,24 +75623,32 @@ var NAX_GITIGNORE_ENTRIES, NAX_NAXIGNORE_ENTRIES, NAX_NAXIGNORE_HEADER = `# nax
75695
75623
  `;
75696
75624
  var init_gitignore = __esm(() => {
75697
75625
  init_config();
75626
+ FEATURE_RUN_ARTIFACTS = [
75627
+ "runs/",
75628
+ "plan/",
75629
+ "sessions/",
75630
+ "stories/",
75631
+ "fragments/",
75632
+ "interactions/",
75633
+ "semantic-verdicts/",
75634
+ "status.json",
75635
+ "checkpoint.jsonl",
75636
+ "progress.txt",
75637
+ "acp-sessions.json",
75638
+ "acceptance-refined.json",
75639
+ "*.bak"
75640
+ ].map((artifact) => `**/${PROJECT_FEATURES_DIR}/*/${artifact}`);
75698
75641
  NAX_GITIGNORE_ENTRIES = [
75699
75642
  ".nax-verifier-verdict.json",
75700
75643
  "nax.lock",
75701
75644
  ".nax/**/runs/",
75702
75645
  ".nax/metrics.json",
75703
- `${PROJECT_FEATURES_DIR}/*/status.json`,
75704
- `${PROJECT_FEATURES_DIR}/*/plan/`,
75705
- `${PROJECT_FEATURES_DIR}/*/fragments/`,
75706
- `${PROJECT_FEATURES_DIR}/*/acp-sessions.json`,
75707
- `${PROJECT_FEATURES_DIR}/*/interactions/`,
75708
- `${PROJECT_FEATURES_DIR}/*/progress.txt`,
75709
- `${PROJECT_FEATURES_DIR}/*/acceptance-refined.json`,
75646
+ ...FEATURE_RUN_ARTIFACTS,
75710
75647
  ".nax-pids",
75711
75648
  ".nax-wt/",
75712
75649
  "**/.nax-acceptance*",
75713
75650
  "**/_nax_acceptance_test.py",
75714
75651
  "**/_nax_suggested_test.py",
75715
- `**/${PROJECT_FEATURES_DIR}/*/`,
75716
75652
  ".nax/prompt-audit/",
75717
75653
  ".nax/finish-audit/",
75718
75654
  ".nax/mutation-journal/"
@@ -83694,6 +83630,7 @@ __export(exports_execution, {
83694
83630
  _postRunDeps: () => _postRunDeps,
83695
83631
  _pidRegistryDeps: () => _pidRegistryDeps,
83696
83632
  _newPackageSetupDeps: () => _newPackageSetupDeps,
83633
+ _lockDeps: () => _lockDeps,
83697
83634
  StoryOrchestratorBuilder: () => StoryOrchestratorBuilder,
83698
83635
  StatusWriter: () => StatusWriter,
83699
83636
  STRICT_VERDICT_PHASE_NAMES: () => STRICT_VERDICT_PHASE_NAMES,
@@ -85115,8 +85052,7 @@ var init_config_descriptions = __esm(() => {
85115
85052
  "context.autoDetect.maxFiles": "Max files to auto-detect",
85116
85053
  "context.autoDetect.traceImports": "Trace imports to find related files",
85117
85054
  optimizer: "Prompt optimizer configuration",
85118
- "optimizer.enabled": "Enable prompt optimizer",
85119
- "optimizer.strategy": "Optimization strategy: rule-based | llm | noop",
85055
+ "optimizer.enabled": "Enable prompt optimizer (pass-through unless a plugin provides one)",
85120
85056
  plugins: "Plugin configurations",
85121
85057
  hooks: "Hooks configuration",
85122
85058
  "hooks.skipGlobal": "Skip loading global hooks",
@@ -117545,6 +117481,7 @@ import { join as join106 } from "path";
117545
117481
  // src/commands/logs-formatter.ts
117546
117482
  init_source();
117547
117483
  init_formatter();
117484
+ init_bun_deps();
117548
117485
  import { readdirSync as readdirSync8 } from "fs";
117549
117486
  import { join as join105 } from "path";
117550
117487
 
@@ -117731,13 +117668,57 @@ async function displayLogs(filePath, options) {
117731
117668
  }
117732
117669
  }
117733
117670
  }
117734
- async function followLogs(filePath, options) {
117671
+ async function defaultReadRange(filePath, start) {
117672
+ return Bun.file(filePath).slice(start).text();
117673
+ }
117674
+ async function defaultSize(filePath) {
117675
+ return (await Bun.file(filePath).stat()).size;
117676
+ }
117677
+ var DEFAULT_FOLLOW_LOGS_DEPS = {
117678
+ emit: (line) => console.log(line),
117679
+ sleep: (ms, signal) => cancellableDelay(ms, signal),
117680
+ readRange: defaultReadRange,
117681
+ size: defaultSize
117682
+ };
117683
+ async function followLogs(filePath, options, opts) {
117684
+ const deps = { ...DEFAULT_FOLLOW_LOGS_DEPS, ...opts?._deps };
117685
+ const signal = opts?.signal;
117735
117686
  const mode = options.json ? "json" : "normal";
117736
- const file3 = Bun.file(filePath);
117737
- const content = await file3.text();
117738
- const lines = content.trim().split(`
117687
+ if (signal?.aborted)
117688
+ return "cancelled";
117689
+ let lastOffset = await consumeRange(filePath, 0, deps, options, mode);
117690
+ while (true) {
117691
+ if (signal?.aborted)
117692
+ return "cancelled";
117693
+ try {
117694
+ await deps.sleep(500, signal);
117695
+ } catch (err) {
117696
+ if (signal?.aborted)
117697
+ return "cancelled";
117698
+ throw err;
117699
+ }
117700
+ let currentSize;
117701
+ try {
117702
+ currentSize = await deps.size(filePath);
117703
+ } catch {
117704
+ return "cancelled";
117705
+ }
117706
+ if (currentSize > lastOffset) {
117707
+ lastOffset = await consumeRange(filePath, lastOffset, deps, options, mode);
117708
+ } else if (currentSize < lastOffset) {
117709
+ lastOffset = currentSize;
117710
+ }
117711
+ }
117712
+ }
117713
+ async function consumeRange(filePath, start, deps, options, mode) {
117714
+ const chunk = await deps.readRange(filePath, start);
117715
+ if (!chunk)
117716
+ return start;
117717
+ const lastNewline = chunk.lastIndexOf(`
117739
117718
  `);
117740
- for (const line of lines) {
117719
+ const complete = lastNewline === -1 ? "" : chunk.slice(0, lastNewline + 1);
117720
+ for (const line of complete.split(`
117721
+ `)) {
117741
117722
  if (!line.trim())
117742
117723
  continue;
117743
117724
  try {
@@ -117747,36 +117728,11 @@ async function followLogs(filePath, options) {
117747
117728
  }
117748
117729
  const formatted = formatLogEntry(entry, { mode, useColor: true });
117749
117730
  if (formatted.shouldDisplay && formatted.output) {
117750
- console.log(formatted.output);
117731
+ deps.emit(formatted.output);
117751
117732
  }
117752
117733
  } catch {}
117753
117734
  }
117754
- let lastSize = (await Bun.file(filePath).stat()).size;
117755
- while (true) {
117756
- await Bun.sleep(500);
117757
- const currentSize = (await Bun.file(filePath).stat()).size;
117758
- if (currentSize > lastSize) {
117759
- const newFile = Bun.file(filePath);
117760
- const newContent = await newFile.text();
117761
- const newLines = newContent.slice(lastSize).trim().split(`
117762
- `);
117763
- for (const line of newLines) {
117764
- if (!line.trim())
117765
- continue;
117766
- try {
117767
- const entry = JSON.parse(line);
117768
- if (!shouldDisplayEntry(entry, options)) {
117769
- continue;
117770
- }
117771
- const formatted = formatLogEntry(entry, { mode, useColor: true });
117772
- if (formatted.shouldDisplay && formatted.output) {
117773
- console.log(formatted.output);
117774
- }
117775
- } catch {}
117776
- }
117777
- lastSize = currentSize;
117778
- }
117779
- }
117735
+ return start + Buffer.byteLength(complete, "utf8");
117780
117736
  }
117781
117737
  function shouldDisplayEntry(entry, options) {
117782
117738
  if (options.story && entry.storyId !== options.story) {
@@ -117800,7 +117756,7 @@ async function logsCommand(options) {
117800
117756
  return;
117801
117757
  }
117802
117758
  if (options.follow) {
117803
- await followLogs(runFile2, options);
117759
+ await followLogs(runFile2, options, { signal: options.signal });
117804
117760
  } else {
117805
117761
  await displayLogs(runFile2, options);
117806
117762
  }
@@ -117823,7 +117779,7 @@ async function logsCommand(options) {
117823
117779
  throw new Error("No runs found for this feature");
117824
117780
  }
117825
117781
  if (options.follow) {
117826
- await followLogs(runFile, options);
117782
+ await followLogs(runFile, options, { signal: options.signal });
117827
117783
  return;
117828
117784
  }
117829
117785
  await displayLogs(runFile, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.81.0-canary.1",
3
+ "version": "0.81.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,8 @@
42
42
  "test:e2e": "timeout -k 5s 180s bun test test/e2e/ --timeout=60000",
43
43
  "test:coverage": "bun run scripts/check-coverage.ts",
44
44
  "test:coverage:report": "bun run scripts/check-coverage.ts --report",
45
+ "test:coverage:update": "bun run scripts/check-coverage.ts --update-baseline",
46
+ "test:coverage:list": "bun run scripts/check-coverage.ts --list",
45
47
  "report:test-overlap": "bun run scripts/report-test-overlap.ts",
46
48
  "report:dead-tests": "bun run scripts/report-dead-tests.ts",
47
49
  "check:test-mocks": "bun scripts/check-inline-test-mocks.ts --strict",