@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.
- package/dist/nax.js +140 -24
- 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.
|
|
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
|
|
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("
|
|
53381
|
-
return "
|
|
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.
|
|
73110
|
-
|
|
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,
|