@nathapp/nax 0.82.2 → 0.82.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 +140 -24
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -2668,7 +2668,7 @@ var package_default;
2668
2668
  var init_package = __esm(() => {
2669
2669
  package_default = {
2670
2670
  name: "@nathapp/nax",
2671
- version: "0.82.2",
2671
+ version: "0.82.3",
2672
2672
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
2673
2673
  type: "module",
2674
2674
  bin: {
@@ -27035,6 +27035,7 @@ var init_schemas_execution = __esm(() => {
27035
27035
  }).default({ testWriter: "fast", verifier: "fast" }),
27036
27036
  verifierTimeoutSeconds: exports_external.number().int().min(60).max(7200).default(1800),
27037
27037
  testWriterAllowedPaths: exports_external.array(exports_external.string()).optional(),
27038
+ testWriterCommitHooks: exports_external.enum(["skip", "run"]).optional(),
27038
27039
  rollbackOnFailure: exports_external.boolean().optional(),
27039
27040
  greenfieldDetection: exports_external.boolean().optional()
27040
27041
  });
@@ -42044,6 +42045,7 @@ Workflow:
42044
42045
 
42045
42046
  Rules:
42046
42047
  - Stubs are NOT implementations. The implementer in the next session writes real logic.
42048
+ - Do not commit. When your session ends, nax commits the files you changed as the RED state.
42047
42049
  - Each test name describes ONE behavior. Use AC IDs in test names when available (e.g. \`it('AC4: throws Division by zero when b === 0')\`).
42048
42050
  - Assert on observable outputs.
42049
42051
  - ${frameworkHint}
@@ -42060,10 +42062,12 @@ Workflow:
42060
42062
  2. Break the work into small tasks before writing: treat each AC as one task and note the test name(s) you will write (success + boundary) and which file they belong in. This per-AC list is your checklist.
42061
42063
  3. Create test files in the location the project uses for tests (project context names it).
42062
42064
  4. For each AC: write at least one test for the success path AND at least one for a boundary/failure path (zero, empty, negative, missing, throws). ACs worded as "throws X" require a test asserting the throw.
42063
- 5. Run the new test files. Confirm every test fails with an ASSERTION failure \u2014 NOT an import error, compile error, or runtime crash before assertion. A test that errors before reaching its assertion does not prove the behavior is missing.
42065
+ 5. Run the new test files. Confirm every test fails with an ASSERTION failure, not an import error or a runtime crash before the assertion. A test that errors before reaching its assertion does not prove the behavior is missing.
42064
42066
 
42065
42067
  Rules:
42066
42068
  - Do NOT create or modify any source files. Read source for types/interfaces only.
42069
+ - A type-check error that exists only because the implementer has not yet added a field, parameter or export the acceptance criteria require is the expected RED state. Do not work around it with type casts, type-checker suppression comments, allow-list tags or throwaway type-probe scripts; type each test as the finished code will be.
42070
+ - Do not commit. When your session ends, nax commits the files you changed as the RED state.
42067
42071
  - Each test name describes ONE behavior; each test asserts ONE behavior. When the AC has a number or ID, prefix the test name (e.g. \`it('AC4: throws Division by zero when b === 0')\`).
42068
42072
  - Assert on observable outputs (return values, thrown errors, file contents, log output, boundary state). Do not assert on private helpers, internal call counts, or implementation-level mocks unless the AC requires it.
42069
42073
  - ${frameworkHint}
@@ -53377,8 +53381,8 @@ var init_version = __esm(() => {
53377
53381
  NAX_AI_VERSION = CATALOG_VERSION;
53378
53382
  NAX_COMMIT = (() => {
53379
53383
  try {
53380
- if (/^[0-9a-f]{6,10}$/.test("7c21771a"))
53381
- return "7c21771a";
53384
+ if (/^[0-9a-f]{6,10}$/.test("db5bc9ae"))
53385
+ return "db5bc9ae";
53382
53386
  } catch {}
53383
53387
  try {
53384
53388
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -73089,8 +73093,8 @@ async function getChangedFiles(workdir, fromRef = "HEAD") {
73089
73093
  { stdout: output, stderr, exitCode },
73090
73094
  { stdout: statusOutput, stderr: statusStderr, exitCode: statusExitCode }
73091
73095
  ] = await Promise.all([
73092
- runGitBounded(["diff", "--name-only", fromRef], workdir),
73093
- runGitBounded(["status", "--porcelain"], workdir)
73096
+ runGitBounded(["diff", "--name-only", "-z", fromRef], workdir),
73097
+ runGitBounded(["status", "--porcelain", "-z"], workdir)
73094
73098
  ]);
73095
73099
  if (exitCode !== 0) {
73096
73100
  throw new NaxError(`git diff --name-only ${fromRef} failed (exit ${exitCode}): ${stderr.trim()}`, "GIT_DIFF_FAILED", {
@@ -73106,10 +73110,8 @@ async function getChangedFiles(workdir, fromRef = "HEAD") {
73106
73110
  exitCode: statusExitCode
73107
73111
  });
73108
73112
  }
73109
- const diffFiles = output.trim().split(`
73110
- `).filter(Boolean);
73111
- const untrackedFiles = statusOutput.split(`
73112
- `).filter((line) => line.startsWith("??")).map((line) => line.slice(2).trim()).filter(Boolean);
73113
+ const diffFiles = output.split("\x00").filter(Boolean);
73114
+ const untrackedFiles = statusOutput.split("\x00").filter((line) => line.startsWith("??")).map((line) => line.slice(3)).filter(Boolean);
73113
73115
  return [...new Set([...diffFiles, ...untrackedFiles])];
73114
73116
  }
73115
73117
  async function getAddedLinesPerFile(workdir, fromRef = "HEAD") {
@@ -76465,20 +76467,7 @@ var init_write_test = __esm(() => {
76465
76467
  stage: "run",
76466
76468
  session: { role: "test-writer", lifetime: "warm" },
76467
76469
  config: tddConfigSelector,
76468
- tools: [
76469
- "Read",
76470
- "Glob",
76471
- "Grep",
76472
- "Write",
76473
- "Edit",
76474
- "Delete",
76475
- "Git",
76476
- "RunCommand",
76477
- "GitCommit",
76478
- "Exec",
76479
- "Bash",
76480
- "RequestCapability"
76481
- ],
76470
+ tools: ["Read", "Glob", "Grep", "Write", "Edit", "Delete", "Git", "RunCommand", "Exec", "Bash", "RequestCapability"],
76482
76471
  model: (_input, ctx) => ctx.config.tdd?.sessionTiers?.testWriter,
76483
76472
  keepOpen: (_input, ctx) => shouldKeepSessionOpen(ctx.config, "test-writer"),
76484
76473
  build(input, _ctx) {
@@ -79562,6 +79551,7 @@ var init_config_descriptions = __esm(() => {
79562
79551
  "tdd.sessionTiers.verifier": "Model tier for verifier session",
79563
79552
  "tdd.verifierTimeoutSeconds": "Wall-clock budget for one verifier turn in seconds (default: 1800). Its own knob, not execution.sessionTimeoutSeconds",
79564
79553
  "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
79554
+ "tdd.testWriterCommitHooks": 'Git hooks on the RED commit nax makes after the test-writer phase: "skip" (default, --no-verify) | "run"',
79565
79555
  "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
79566
79556
  "tdd.greenfieldDetection": "Force tdd-simple on projects with no test files",
79567
79557
  constitution: "Constitution settings (core rules and constraints)",
@@ -86408,12 +86398,108 @@ var init_cleanup = __esm(() => {
86408
86398
  };
86409
86399
  });
86410
86400
 
86401
+ // src/tdd/red-commit.ts
86402
+ function redCommitMessage(storyId) {
86403
+ return `chore(${storyId}): auto-commit after test-writer session (RED)`;
86404
+ }
86405
+ async function commitRedState(opts, deps = _redCommitDeps) {
86406
+ if (opts.dryRun)
86407
+ return { status: "skipped", reason: "dry-run" };
86408
+ try {
86409
+ return await commitFromRoot(opts, deps);
86410
+ } catch (err) {
86411
+ return { status: "failed", reason: errorMessage(err) };
86412
+ }
86413
+ }
86414
+ async function commitFromRoot(opts, deps) {
86415
+ const top = await deps.git(["rev-parse", "--show-toplevel"], opts.workdir, RED_COMMIT_GIT_TIMEOUT_MS);
86416
+ if (top.exitCode !== 0)
86417
+ return { status: "failed", reason: `git rev-parse failed: ${top.stderr.trim()}` };
86418
+ const gitRoot = top.stdout.trim();
86419
+ if (isBlocked2(gitRoot, opts))
86420
+ return { status: "skipped", reason: "blocked-worktree" };
86421
+ const changed = await deps.getChangedFiles(opts.workdir, opts.beforeRef);
86422
+ const files = await expandUntrackedDirs(gitRoot, changed, deps);
86423
+ const { kept } = await deps.partitionNaxOwnedPaths(gitRoot, files);
86424
+ if (kept.length === 0)
86425
+ return NOTHING;
86426
+ const addOpts = { pathspecs: kept, timeoutMs: RED_COMMIT_GIT_TIMEOUT_MS };
86427
+ const added = await gitlinkSafeAdd(deps.git, gitRoot, addOpts);
86428
+ if (added.exitCode !== 0)
86429
+ return { status: "failed", reason: `git add failed: ${added.stderr.trim()}` };
86430
+ const staged = await deps.git(["diff", "--cached", "--quiet", "--", ...kept], gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86431
+ if (staged.timedOut || staged.exitCode !== 0 && staged.exitCode !== 1) {
86432
+ return { status: "failed", reason: `git diff --cached failed: ${staged.stderr.trim()}` };
86433
+ }
86434
+ if (staged.exitCode === 0)
86435
+ return NOTHING;
86436
+ const noVerify = opts.hooks === "skip" ? ["--no-verify"] : [];
86437
+ const argv = ["commit", "--only", "-m", redCommitMessage(opts.storyId), ...noVerify, "--", ...kept];
86438
+ const committed = await deps.git(argv, gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86439
+ if (committed.exitCode !== 0) {
86440
+ const detail = committed.stderr.trim() || `exit ${committed.exitCode}`;
86441
+ return { status: "failed", reason: `git commit failed: ${detail}` };
86442
+ }
86443
+ return { status: "committed", files: kept, hooksSkipped: opts.hooks === "skip" };
86444
+ }
86445
+ function isBlocked2(gitRoot, opts) {
86446
+ if (!opts.blockedWorktrees?.size)
86447
+ return false;
86448
+ const root = realOrRaw(gitRoot);
86449
+ const blocked = [...opts.blockedWorktrees].filter((tree) => realOrRaw(tree) === root);
86450
+ if (blocked.length === 0)
86451
+ return false;
86452
+ const message = "Refusing to commit the RED state \u2014 working tree may still hold an unreverted mutation";
86453
+ getSafeLogger()?.error("tdd", message, {
86454
+ storyId: opts.storyId,
86455
+ workdir: opts.workdir,
86456
+ blocked,
86457
+ hint: "Check the mutation-check log for the file and line, restore it, then commit manually."
86458
+ });
86459
+ return true;
86460
+ }
86461
+ async function expandUntrackedDirs(gitRoot, paths, deps) {
86462
+ const out = [];
86463
+ for (const path11 of paths) {
86464
+ if (!path11.endsWith("/")) {
86465
+ out.push(path11);
86466
+ continue;
86467
+ }
86468
+ const args = ["ls-files", "--others", "--exclude-standard", "-z", "--", path11];
86469
+ const listed = await deps.git(args, gitRoot, RED_COMMIT_GIT_TIMEOUT_MS);
86470
+ if (listed.exitCode !== 0) {
86471
+ throw new NaxError(`git ls-files failed: ${listed.stderr.trim()}`, "GIT_LS_FILES_FAILED", {
86472
+ stage: "tdd-red-commit",
86473
+ path: path11
86474
+ });
86475
+ }
86476
+ out.push(...listed.stdout.split("\x00").filter(Boolean));
86477
+ }
86478
+ return out;
86479
+ }
86480
+ var RED_COMMIT_GIT_TIMEOUT_MS = 30000, _redCommitDeps, NOTHING;
86481
+ var init_red_commit = __esm(() => {
86482
+ init_errors();
86483
+ init_logger2();
86484
+ init_tools();
86485
+ init_git();
86486
+ init_realpath();
86487
+ init_isolation2();
86488
+ _redCommitDeps = {
86489
+ git: (args, cwd, timeoutMs) => gitWithTimeout(args, cwd, timeoutMs),
86490
+ getChangedFiles,
86491
+ partitionNaxOwnedPaths
86492
+ };
86493
+ NOTHING = { status: "skipped", reason: "nothing-to-commit" };
86494
+ });
86495
+
86411
86496
  // src/tdd/index.ts
86412
86497
  var init_tdd = __esm(() => {
86413
86498
  init_operations();
86414
86499
  init_test_runners();
86415
86500
  init_cleanup();
86416
86501
  init_isolation2();
86502
+ init_red_commit();
86417
86503
  init_rollback();
86418
86504
  init_verdict();
86419
86505
  });
@@ -87137,6 +87223,9 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
87137
87223
  }
87138
87224
  }
87139
87225
  }
87226
+ if (isTddPhase && opName === "test-writer" && !inRectification && beforeRef && outcome === "passed") {
87227
+ await commitTestWriterRedState(ctx, beforeRef);
87228
+ }
87140
87229
  return output;
87141
87230
  } catch (err) {
87142
87231
  const noDispatch = toNoDispatchCheckResult(opName, err, Date.now() - phaseStartedAt);
@@ -87237,6 +87326,32 @@ function derivePhaseOutcome(output) {
87237
87326
  return "skipped";
87238
87327
  return "failed";
87239
87328
  }
87329
+ async function commitTestWriterRedState(ctx, beforeRef) {
87330
+ const config2 = ctx.config ?? ctx.runtime.configLoader.current();
87331
+ const result = await _storyOrchestratorDeps.commitRedState({
87332
+ workdir: ctx.packageDir,
87333
+ beforeRef,
87334
+ storyId: ctx.storyId ?? "story",
87335
+ hooks: config2.tdd?.testWriterCommitHooks ?? "skip",
87336
+ dryRun: ctx.runtime.dryRun,
87337
+ blockedWorktrees: ctx.runtime.dirtyWorktrees
87338
+ });
87339
+ logRedCommit(ctx.storyId, result);
87340
+ }
87341
+ function logRedCommit(storyId, result) {
87342
+ const logger = getSafeLogger();
87343
+ if (result.status === "committed") {
87344
+ logger?.info("tdd", "RED state committed", {
87345
+ storyId,
87346
+ files: result.files.length,
87347
+ hooksSkipped: result.hooksSkipped
87348
+ });
87349
+ } else if (result.status === "skipped") {
87350
+ logger?.debug("tdd", "RED state commit skipped", { storyId, reason: result.reason });
87351
+ } else {
87352
+ logger?.warn("tdd", "RED state not committed", { storyId, reason: result.reason });
87353
+ }
87354
+ }
87240
87355
  function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
87241
87356
  if (!enabled)
87242
87357
  return strategies;
@@ -87283,6 +87398,7 @@ var init_run_phase = __esm(() => {
87283
87398
  callOp,
87284
87399
  runFixCycle,
87285
87400
  captureGitRef,
87401
+ commitRedState,
87286
87402
  cleanupVerdict,
87287
87403
  prepareSemanticReviewInput,
87288
87404
  prepareAdversarialReviewInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.82.2",
3
+ "version": "0.82.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {