@nathapp/nax 0.70.0-canary.2 → 0.70.0-canary.4

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 +152 -38
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -21022,6 +21022,7 @@ class AcpAgentAdapter {
21022
21022
  const MAX_TURNS = opts.maxTurns ?? 10;
21023
21023
  let totalTokenUsage = { inputTokens: 0, outputTokens: 0 };
21024
21024
  let totalExactCostUsd;
21025
+ const interactions = [];
21025
21026
  let turnCount = 0;
21026
21027
  let lastResponse = null;
21027
21028
  let timedOut = false;
@@ -21106,6 +21107,7 @@ class AcpAgentAdapter {
21106
21107
  })
21107
21108
  ]);
21108
21109
  if (response) {
21110
+ interactions.push({ turnIndex: turnCount, question, reply: response.answer });
21109
21111
  currentPrompt = response.answer;
21110
21112
  continue;
21111
21113
  }
@@ -21136,7 +21138,8 @@ class AcpAgentAdapter {
21136
21138
  tokenUsage,
21137
21139
  estimatedCostUsd,
21138
21140
  exactCostUsd,
21139
- internalRoundTrips: turnCount
21141
+ internalRoundTrips: turnCount,
21142
+ ...interactions.length > 0 ? { interactions } : {}
21140
21143
  };
21141
21144
  }
21142
21145
  async closeSession(handle) {
@@ -22139,6 +22142,7 @@ class AgentManager {
22139
22142
  sessionId: handle.protocolIds?.sessionId ?? null,
22140
22143
  recordId: handle.protocolIds?.recordId ?? null
22141
22144
  },
22145
+ ...result.interactions?.length ? { interactions: result.interactions } : {},
22142
22146
  origin: "runAsSession",
22143
22147
  ...opts.callId !== undefined ? { callId: opts.callId } : {},
22144
22148
  ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
@@ -32088,6 +32092,19 @@ ${STEP3_SHARED_RULES}
32088
32092
  - **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
32089
32093
  - **Path anchor (CRITICAL \u2014 do NOT deviate)**: Write the test file to this exact path: \`${p.targetTestFilePath}\`. This path is intentional and computed by the orchestrator \u2014 do not change it based on what you observe in the project. In particular: if you see a \`.nax/features/\` directory at the repo root, that is for stories scoped to the repo root. When a story belongs to a specific package (e.g. \`packages/core\`), its acceptance test lives inside that package's \`.nax/features/\` directory so the test runner can resolve the package's imports correctly. The package root is 3 levels above the test file (\`../../../\` relative to the test file).
32090
32094
  - **Process cwd**: When spawning child processes to invoke a CLI or binary, set the working directory to the **package root** (\`join(import.meta.dir, "../../..")\`) as your default \u2014 unless your Step 2 exploration reveals the CLI uses a different working directory convention (e.g. reads config from \`~/.config/\`, or resolves paths relative to a flag value). Always check how the CLI resolves file paths before assuming.${implSection}`;
32095
+ }
32096
+ buildPathCorrection(targetTestFilePath) {
32097
+ return `The acceptance test file was NOT found at the required path. You likely wrote it to a different filename or directory (for example, by renaming a dotfile or replacing dashes with underscores).
32098
+
32099
+ Move (or re-write) the acceptance test you just created so it lives at EXACTLY this path:
32100
+ ${targetTestFilePath}
32101
+
32102
+ Requirements:
32103
+ - The file must be at that exact path \u2014 same directory and same filename, including any leading dot and dashes. Do NOT sanitize, rename, or relocate it.
32104
+ - Preserve the test content you already wrote. Do not regenerate, weaken, or stub the assertions.
32105
+ - If you wrote it somewhere else, delete the misplaced copy after moving it so only the canonical path remains.
32106
+
32107
+ After writing the file to the exact path above, reply with a brief confirmation only.`;
32091
32108
  }
32092
32109
  buildGeneratorFromSpecPrompt(p) {
32093
32110
  return `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${p.featureName}" feature.
@@ -34755,6 +34772,40 @@ ${outputFormat}`, overridable: false }
34755
34772
  };
34756
34773
  });
34757
34774
 
34775
+ // src/operations/self-heal.ts
34776
+ function makeSelfHealStep(spec) {
34777
+ return {
34778
+ async run(ctx) {
34779
+ const deviations = await spec.detect(ctx.input);
34780
+ if (deviations.length === 0)
34781
+ return null;
34782
+ if (spec.log) {
34783
+ getSafeLogger()?.info(spec.log.kind, spec.log.message, spec.log.meta?.(ctx.input, deviations) ?? {});
34784
+ }
34785
+ return ctx.send(spec.buildRepair(deviations, ctx.input));
34786
+ }
34787
+ };
34788
+ }
34789
+ async function runSelfHealChain(ctx, seed, steps) {
34790
+ let last = seed;
34791
+ let totalCost = seed.estimatedCostUsd ?? 0;
34792
+ for (const step of steps) {
34793
+ try {
34794
+ const turn = await step.run(ctx);
34795
+ if (turn) {
34796
+ totalCost += turn.estimatedCostUsd ?? 0;
34797
+ last = turn;
34798
+ }
34799
+ } catch (err) {
34800
+ getSafeLogger()?.warn("self-heal", "step threw \u2014 skipping", { error: errorMessage(err) });
34801
+ }
34802
+ }
34803
+ return { ...last, estimatedCostUsd: totalCost };
34804
+ }
34805
+ var init_self_heal = __esm(() => {
34806
+ init_logger2();
34807
+ });
34808
+
34758
34809
  // src/operations/plan-refine.ts
34759
34810
  import { join as join21 } from "path";
34760
34811
  function hasToken(text, tokens) {
@@ -34893,6 +34944,28 @@ async function normalizeCreatedContextFiles(prd, workdir, fileExists) {
34893
34944
  return prd;
34894
34945
  return { ...prd, userStories: results.map((r) => r.story) };
34895
34946
  }
34947
+ function verbatimSelfHealStep(builder) {
34948
+ return makeSelfHealStep({
34949
+ detect: (input) => readMissingVerbatimAcs(input),
34950
+ buildRepair: (missing, input) => builder.buildVerbatimRepair(missing, input.outputPath),
34951
+ log: {
34952
+ kind: "plan",
34953
+ message: "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn",
34954
+ meta: (input, missing) => ({ featureName: input.featureName, missingCount: missing.length })
34955
+ }
34956
+ });
34957
+ }
34958
+ function specDriftSelfHealStep(builder) {
34959
+ return makeSelfHealStep({
34960
+ detect: (input) => readSpecDriftViolations(input),
34961
+ buildRepair: (drifted, input) => builder.buildSpecDriftRepair(drifted, input.outputPath),
34962
+ log: {
34963
+ kind: "plan",
34964
+ message: "specGuard: spec-drift violations found \u2014 issuing one repair turn",
34965
+ meta: (input, drifted) => ({ featureName: input.featureName, violationCount: drifted.length })
34966
+ }
34967
+ });
34968
+ }
34896
34969
  var _planRefineDeps, NEGATIVE_PATH_TOKENS, planRefineOp;
34897
34970
  var init_plan_refine = __esm(() => {
34898
34971
  init_retry();
@@ -34902,6 +34975,7 @@ var init_plan_refine = __esm(() => {
34902
34975
  init_prd();
34903
34976
  init_schema2();
34904
34977
  init_prompts();
34978
+ init_self_heal();
34905
34979
  init_verbatim_warn();
34906
34980
  _planRefineDeps = {
34907
34981
  readFile: async (path3) => {
@@ -34981,31 +35055,15 @@ ${outputFormat}`,
34981
35055
  const specGuard = ctx.input.specGuard ?? false;
34982
35056
  const turn1 = await ctx.sendWithParseRetry(initialPrompt);
34983
35057
  const turn2 = await ctx.send(builder.buildRefineContinuation(ctx.input.outputPath, specGuard));
34984
- let totalCost = (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0);
34985
- let last = turn2;
34986
- const missing = await readMissingVerbatimAcs(ctx.input);
34987
- if (missing.length > 0) {
34988
- getSafeLogger()?.info("plan", "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn", {
34989
- featureName: ctx.input.featureName,
34990
- missingCount: missing.length
34991
- });
34992
- const turn3 = await ctx.send(builder.buildVerbatimRepair(missing, ctx.input.outputPath));
34993
- totalCost += turn3.estimatedCostUsd ?? 0;
34994
- last = turn3;
34995
- }
34996
- if (specGuard) {
34997
- const drifted = await readSpecDriftViolations(ctx.input);
34998
- if (drifted.length > 0) {
34999
- getSafeLogger()?.info("plan", "specGuard: spec-drift violations found \u2014 issuing one repair turn", {
35000
- featureName: ctx.input.featureName,
35001
- violationCount: drifted.length
35002
- });
35003
- const turn4 = await ctx.send(builder.buildSpecDriftRepair(drifted, ctx.input.outputPath));
35004
- totalCost += turn4.estimatedCostUsd ?? 0;
35005
- last = turn4;
35006
- }
35007
- }
35008
- return { ...last, estimatedCostUsd: totalCost };
35058
+ const seed = {
35059
+ ...turn2,
35060
+ estimatedCostUsd: (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0)
35061
+ };
35062
+ const steps = [
35063
+ verbatimSelfHealStep(builder),
35064
+ ...specGuard ? [specDriftSelfHealStep(builder)] : []
35065
+ ];
35066
+ return runSelfHealChain(ctx, seed, steps);
35009
35067
  },
35010
35068
  parse(output, input) {
35011
35069
  return validatePlanOutput(output, input.featureName, input.branchName);
@@ -36123,11 +36181,32 @@ function isStubTestContent(content) {
36123
36181
  }
36124
36182
 
36125
36183
  // src/operations/acceptance-generate.ts
36126
- var acceptanceGenerateOp;
36184
+ function pathCorrectionStep() {
36185
+ return makeSelfHealStep({
36186
+ detect: async (input) => await _acceptanceGenerateDeps.fileExists(input.targetTestFilePath) ? [] : [input.targetTestFilePath],
36187
+ buildRepair: (_deviations, input) => new AcceptancePromptBuilder().buildPathCorrection(input.targetTestFilePath),
36188
+ log: {
36189
+ kind: "acceptance",
36190
+ message: "Acceptance test not found at target path \u2014 issuing one corrective turn",
36191
+ meta: (input) => ({ targetTestFilePath: input.targetTestFilePath })
36192
+ }
36193
+ });
36194
+ }
36195
+ var _acceptanceGenerateDeps, acceptanceGenerateOp;
36127
36196
  var init_acceptance_generate = __esm(() => {
36128
36197
  init_generator();
36129
36198
  init_config();
36130
36199
  init_prompts();
36200
+ init_self_heal();
36201
+ _acceptanceGenerateDeps = {
36202
+ fileExists: async (path4) => {
36203
+ try {
36204
+ return await Bun.file(path4).exists();
36205
+ } catch {
36206
+ return false;
36207
+ }
36208
+ }
36209
+ };
36131
36210
  acceptanceGenerateOp = {
36132
36211
  kind: "run",
36133
36212
  name: "acceptance-generate",
@@ -36149,6 +36228,10 @@ var init_acceptance_generate = __esm(() => {
36149
36228
  task: { id: "task", content: prompt, overridable: false }
36150
36229
  };
36151
36230
  },
36231
+ async hopBody(initialPrompt, ctx) {
36232
+ const turn1 = await ctx.sendWithParseRetry(initialPrompt);
36233
+ return runSelfHealChain(ctx, turn1, [pathCorrectionStep()]);
36234
+ },
36152
36235
  parse(output, _input, _ctx) {
36153
36236
  return { testCode: extractTestCode(output) };
36154
36237
  },
@@ -40159,6 +40242,7 @@ var init_operations = __esm(() => {
40159
40242
  init_call();
40160
40243
  init_plan();
40161
40244
  init_plan_refine();
40245
+ init_self_heal();
40162
40246
  init_verbatim_warn();
40163
40247
  init_decompose2();
40164
40248
  init_build_hop_callback();
@@ -44729,11 +44813,22 @@ function buildTxtContent(entry) {
44729
44813
  "",
44730
44814
  "=== RESPONSE ===",
44731
44815
  "",
44732
- entry.response
44816
+ entry.response,
44817
+ ...buildInteractionLines(entry.interactions)
44733
44818
  ];
44734
44819
  return lines.join(`
44735
44820
  `);
44736
44821
  }
44822
+ function buildInteractionLines(interactions) {
44823
+ if (!interactions?.length)
44824
+ return [];
44825
+ const lines = ["", "=== INTERACTIONS ===", ""];
44826
+ for (const ix of interactions) {
44827
+ lines.push(`[turn ${ix.turnIndex}] Q: ${ix.question}`, ` A: ${ix.reply}`, "");
44828
+ }
44829
+ lines.pop();
44830
+ return lines;
44831
+ }
44737
44832
 
44738
44833
  class PromptAuditor {
44739
44834
  _queue = Promise.resolve();
@@ -45242,7 +45337,8 @@ function attachAuditSubscriber(bus, auditor, runId) {
45242
45337
  ...event.kind === "session-turn" && {
45243
45338
  sessionId: event.protocolIds.sessionId ?? null,
45244
45339
  recordId: event.protocolIds.recordId ?? null,
45245
- turn: event.turn
45340
+ turn: event.turn,
45341
+ ...event.interactions?.length ? { interactions: event.interactions } : {}
45246
45342
  }
45247
45343
  };
45248
45344
  auditor.record(entry);
@@ -55155,7 +55251,8 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
55155
55251
  break;
55156
55252
  }
55157
55253
  }
55158
- const validated = rectification.postValidate ? await rectification.postValidate(findings, _validateCtx) : findings;
55254
+ const postValidateFn = overrides?.postValidate ?? rectification.postValidate;
55255
+ const validated = postValidateFn ? await postValidateFn(findings, _validateCtx) : findings;
55159
55256
  return { findings: validated, shortCircuited };
55160
55257
  }
55161
55258
  };
@@ -55325,7 +55422,8 @@ class ExecutionPlan {
55325
55422
  strategies: this.state.nonBlockingFixStrategies ?? [],
55326
55423
  excludePhaseKinds: nonBlockingExcludePhases(),
55327
55424
  extraRevalidationKinds: nonBlockingExtraPhases(advCfg),
55328
- maxAttempts
55425
+ maxAttempts,
55426
+ postValidate: this.state.nonBlockingFixPostValidate
55329
55427
  })
55330
55428
  });
55331
55429
  }
@@ -55421,9 +55519,10 @@ class StoryOrchestratorBuilder {
55421
55519
  this.state.rectification = opts;
55422
55520
  return this;
55423
55521
  }
55424
- addNonBlockingFix(cfg, strategies) {
55522
+ addNonBlockingFix(cfg, strategies, postValidate) {
55425
55523
  this.state.nonBlockingFix = cfg;
55426
55524
  this.state.nonBlockingFixStrategies = strategies;
55525
+ this.state.nonBlockingFixPostValidate = postValidate;
55427
55526
  return this;
55428
55527
  }
55429
55528
  build(ctx, opts = {}) {
@@ -55553,10 +55652,10 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55553
55652
  if (inputs.adversarialReview) {
55554
55653
  builder.addAdversarialReview(inputs.adversarialReview);
55555
55654
  }
55655
+ const packageDir = join47(ctx.packageDir, story.workdir ?? "");
55656
+ const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
55556
55657
  if (shouldRunRectification(config2) && inputs.rectification) {
55557
55658
  const sink = makeDeclarationSink();
55558
- const packageDir = join47(ctx.packageDir, story.workdir ?? "");
55559
- const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
55560
55659
  const strategies = [];
55561
55660
  const pkgQuality = ctx.packageView.select(qualityConfigSelector).quality;
55562
55661
  if (pkgQuality?.commands?.lintFix || pkgQuality?.commands?.lintFixScoped) {
@@ -55612,7 +55711,22 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55612
55711
  }), makeAutofixTestWriterStrategy(story, config2, nbSink));
55613
55712
  }
55614
55713
  nbStrategies.push(makeFullSuiteRectifyStrategy(story, config2, nbSink));
55615
- builder.addNonBlockingFix(nbf, nbStrategies);
55714
+ const nbPostValidate = async (findings, _validateCtx) => {
55715
+ if (nbSink.testEdits.length === 0 && nbSink.mockHandoffs.length === 0)
55716
+ return findings;
55717
+ const pendingMock = nbSink.mockHandoffs.map((h) => ({
55718
+ reason: "mock_structure",
55719
+ file: h.files[0] ?? "",
55720
+ files: h.files,
55721
+ reasonDetail: h.reasonDetail
55722
+ }));
55723
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
55724
+ nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
55725
+ const allDeclarations = [...nbSink.testEdits, ...valid];
55726
+ nbSink.testEdits = [];
55727
+ return applyTestEditDeclarations(findings, allDeclarations, story, invalid);
55728
+ };
55729
+ builder.addNonBlockingFix(nbf, nbStrategies, nbPostValidate);
55616
55730
  }
55617
55731
  return builder.build(ctx, { isThreeSession });
55618
55732
  }
@@ -60211,7 +60325,7 @@ var package_default;
60211
60325
  var init_package = __esm(() => {
60212
60326
  package_default = {
60213
60327
  name: "@nathapp/nax",
60214
- version: "0.70.0-canary.2",
60328
+ version: "0.70.0-canary.4",
60215
60329
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60216
60330
  type: "module",
60217
60331
  bin: {
@@ -60306,8 +60420,8 @@ var init_version = __esm(() => {
60306
60420
  NAX_VERSION = package_default.version;
60307
60421
  NAX_COMMIT = (() => {
60308
60422
  try {
60309
- if (/^[0-9a-f]{6,10}$/.test("b070f4c1"))
60310
- return "b070f4c1";
60423
+ if (/^[0-9a-f]{6,10}$/.test("e2a854e7"))
60424
+ return "e2a854e7";
60311
60425
  } catch {}
60312
60426
  try {
60313
60427
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.70.0-canary.2",
3
+ "version": "0.70.0-canary.4",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {