@nathapp/nax 0.77.3 → 0.79.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.
- package/dist/nax.js +1737 -1054
- package/flows/nax-finish/flow-ctx.ts +25 -5
- package/flows/nax-finish/nax-finish.flow.ts +83 -148
- package/flows/nax-finish/review-prompts.ts +6 -3
- package/flows/nax-finish/steps/commit-round.ts +8 -8
- package/flows/nax-finish/steps/context.ts +63 -7
- package/flows/nax-finish/steps/gates.ts +183 -0
- package/flows/nax-finish/steps/index.ts +1 -0
- package/flows/nax-finish/types.ts +23 -9
- package/package.json +13 -4
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two gate nodes — `acceptance` and `quality_gates`.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `nax-finish.flow.ts`, which sits against the 600-line source
|
|
5
|
+
* cap. They belong together: both answer the same question ("did anything
|
|
6
|
+
* actually verify this tree?"), both enforce the same rule that **nothing ran
|
|
7
|
+
* is not a pass**, and both route on the shared `MAX_FIX_ATTEMPTS` cap.
|
|
8
|
+
*
|
|
9
|
+
* Keeping them out of the flow file also makes them callable in tests without
|
|
10
|
+
* reaching through `flow.nodes.*`.
|
|
11
|
+
*/
|
|
12
|
+
import { fixAttemptCount, inputOf, loadCtxOf } from "../flow-ctx";
|
|
13
|
+
import { MAX_FIX_ATTEMPTS } from "../verdict";
|
|
14
|
+
import { runAcceptanceGate } from "./acceptance";
|
|
15
|
+
import { type QualityCommands, loadQualityCommands, runQualityGates } from "./quality";
|
|
16
|
+
|
|
17
|
+
/** The slice of an acpx `FlowNodeContext` these nodes read. */
|
|
18
|
+
export interface GateNodeCtx {
|
|
19
|
+
input: unknown;
|
|
20
|
+
outputs: unknown;
|
|
21
|
+
state: { steps: { nodeId: string }[] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AcceptanceNodeOutput {
|
|
25
|
+
route: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
output: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface QualityGatesNodeOutput {
|
|
31
|
+
route: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
ran: string[];
|
|
34
|
+
failing: string[];
|
|
35
|
+
output: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The repo's explicit opt-out, as reported by `nax features resolve`. */
|
|
39
|
+
function acceptanceDisabled(ctx: GateNodeCtx): boolean {
|
|
40
|
+
return loadCtxOf(ctx).acceptanceStatus === "disabled";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Re-run the acceptance gate, routing on the shared fix-cap rules.
|
|
45
|
+
*
|
|
46
|
+
* "Nothing ran" is not a pass — the same rule `quality_gates` applies to an
|
|
47
|
+
* unconfigured repo. `nax features resolve` reports `groups: []` for `no-prd`,
|
|
48
|
+
* for `disabled`, **and** for an `ok` resolution whose PRD grouped to no
|
|
49
|
+
* package at all; it reports `exists: false` for a group whose test was
|
|
50
|
+
* expected at its canonical path but never generated. Treating any of those as
|
|
51
|
+
* green let the flow open a ready PR having verified nothing about the
|
|
52
|
+
* feature's own contract (#1398). Only `disabled` — the repo's explicit opt-out
|
|
53
|
+
* — skips cleanly.
|
|
54
|
+
*/
|
|
55
|
+
export async function acceptanceGateNode(ctx: GateNodeCtx): Promise<AcceptanceNodeOutput> {
|
|
56
|
+
const i = inputOf(ctx);
|
|
57
|
+
const { groups = [], acceptanceStatus } = loadCtxOf(ctx);
|
|
58
|
+
if (acceptanceStatus === "disabled") {
|
|
59
|
+
return { route: "proceed", output: "[acceptance] disabled in .nax/config.json — skipping" };
|
|
60
|
+
}
|
|
61
|
+
if (acceptanceStatus === "no-prd") {
|
|
62
|
+
return {
|
|
63
|
+
route: "escalate",
|
|
64
|
+
reason: `Acceptance targets could not be computed (status: no-prd) — nothing was verified for "${i.feature}".`,
|
|
65
|
+
output: "[acceptance] no prd.json resolved — acceptance targets unknown",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const r = await runAcceptanceGate(i.workdir, groups, { timeoutMs: i.timeouts?.acceptanceMs });
|
|
70
|
+
if (r.passed) {
|
|
71
|
+
// A real failure below routes to the fix loop, which is more actionable;
|
|
72
|
+
// the coverage hole is only reported once the runnable groups are green.
|
|
73
|
+
if (r.missing.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
route: "escalate",
|
|
76
|
+
reason: `Acceptance test never generated for: ${r.missing.join(", ")} — that package's contract is unverified.`,
|
|
77
|
+
output: r.output,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Passed, nothing missing, and nothing ran: the resolver produced no group
|
|
81
|
+
// to run at all. `status: "ok"` does NOT imply a target exists — it means
|
|
82
|
+
// the PRD loaded — so this is the one remaining way an empty gate reports
|
|
83
|
+
// green. Escalating matches what `runQualityGates` does for a repo with no
|
|
84
|
+
// configured commands: an LLM fix node cannot invent the missing target.
|
|
85
|
+
if (r.ran === 0) {
|
|
86
|
+
return {
|
|
87
|
+
route: "escalate",
|
|
88
|
+
reason: `No acceptance test target resolved for "${i.feature}" (status: ${acceptanceStatus ?? "unknown"}) — nothing verified its contract.`,
|
|
89
|
+
output: r.output,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { route: "proceed", output: r.output };
|
|
93
|
+
}
|
|
94
|
+
const attempts = fixAttemptCount(ctx, "fix_acceptance");
|
|
95
|
+
if (attempts >= MAX_FIX_ATTEMPTS) {
|
|
96
|
+
return {
|
|
97
|
+
route: "escalate",
|
|
98
|
+
reason: `Acceptance tests still failing after ${attempts} fix attempts.`,
|
|
99
|
+
output: r.output,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { route: "fix", output: r.output };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Acceptance is gate zero here, not just at the `acceptance` node.
|
|
107
|
+
*
|
|
108
|
+
* Both fix loops that run after it — quality review and this gate — edit code,
|
|
109
|
+
* and the repo-root `test` command does not cover the feature's acceptance
|
|
110
|
+
* tests: they live under `<pkg>/.nax/features/<f>/` and usually need their own
|
|
111
|
+
* runner config. Re-running them here is what makes "nothing reaches open_pr
|
|
112
|
+
* without the feature's own contract passing against the tree as it will ship"
|
|
113
|
+
* true on every path (#1398).
|
|
114
|
+
*
|
|
115
|
+
* Unconditional apart from the repo's own opt-out, though the common green path
|
|
116
|
+
* re-runs a gate that already passed: acceptance is the cheapest gate in the
|
|
117
|
+
* pipeline, and a conditional skip derived from step history would be a check
|
|
118
|
+
* that can be *wrong* — a silent false green, the failure mode this exists to
|
|
119
|
+
* prevent. The `disabled` skip is not such a derivation: it is the same
|
|
120
|
+
* resolver field the `acceptance` node already honours, and the two nodes
|
|
121
|
+
* disagreeing about who owns the opt-out is its own bug.
|
|
122
|
+
*
|
|
123
|
+
* `missing` is deliberately ignored: groups are resolved once at load_ctx, so a
|
|
124
|
+
* coverage hole was already escalated by the acceptance node and cannot appear
|
|
125
|
+
* here.
|
|
126
|
+
*/
|
|
127
|
+
async function reverifyAcceptance(ctx: GateNodeCtx): Promise<QualityGatesNodeOutput | null> {
|
|
128
|
+
if (acceptanceDisabled(ctx)) return null;
|
|
129
|
+
const i = inputOf(ctx);
|
|
130
|
+
const acc = await runAcceptanceGate(i.workdir, loadCtxOf(ctx).groups ?? [], {
|
|
131
|
+
timeoutMs: i.timeouts?.acceptanceMs,
|
|
132
|
+
});
|
|
133
|
+
if (acc.passed) return null;
|
|
134
|
+
// Short-circuit: the repo gates are re-run next round anyway, and skipping
|
|
135
|
+
// them keeps this out of the "nothing configured" branch below, which would
|
|
136
|
+
// otherwise misreport configured-but-skipped commands as absent.
|
|
137
|
+
const attempts = fixAttemptCount(ctx, "fix_gate");
|
|
138
|
+
const failing = ["acceptance"];
|
|
139
|
+
if (attempts >= MAX_FIX_ATTEMPTS) {
|
|
140
|
+
return {
|
|
141
|
+
route: "escalate",
|
|
142
|
+
reason: `A later fix broke the feature's own contract: acceptance still failing after ${attempts} fix attempts.`,
|
|
143
|
+
ran: [],
|
|
144
|
+
failing,
|
|
145
|
+
output: acc.output,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { route: "fix", ran: [], failing, output: acc.output };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function qualityGatesNode(ctx: GateNodeCtx): Promise<QualityGatesNodeOutput> {
|
|
152
|
+
const i = inputOf(ctx);
|
|
153
|
+
|
|
154
|
+
const accFailure = await reverifyAcceptance(ctx);
|
|
155
|
+
if (accFailure) return accFailure;
|
|
156
|
+
|
|
157
|
+
const cmds: QualityCommands = await loadQualityCommands(i.workdir);
|
|
158
|
+
const r = await runQualityGates(i.workdir, cmds, { timeoutMs: i.timeouts?.gateMs });
|
|
159
|
+
if (r.passed) return { route: "green", ran: r.ran, failing: r.failing, output: r.output };
|
|
160
|
+
// Nothing configured is not a pass — escalate immediately rather than open a
|
|
161
|
+
// "ready" PR having verified nothing. An LLM fix node cannot invent the
|
|
162
|
+
// repo's build/test commands.
|
|
163
|
+
if (r.ran.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
route: "escalate",
|
|
166
|
+
reason: "No quality.commands configured in .nax/config.json — nax-finish verified nothing.",
|
|
167
|
+
ran: r.ran,
|
|
168
|
+
failing: r.failing,
|
|
169
|
+
output: r.output,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const attempts = fixAttemptCount(ctx, "fix_gate");
|
|
173
|
+
if (attempts >= MAX_FIX_ATTEMPTS) {
|
|
174
|
+
return {
|
|
175
|
+
route: "escalate",
|
|
176
|
+
reason: `Quality gates still failing after ${attempts} fix attempts (${r.failing.join(", ")}).`,
|
|
177
|
+
ran: r.ran,
|
|
178
|
+
failing: r.failing,
|
|
179
|
+
output: r.output,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { route: "fix", ran: r.ran, failing: r.failing, output: r.output };
|
|
183
|
+
}
|
|
@@ -80,16 +80,17 @@ export type FinishRoundOutcome =
|
|
|
80
80
|
*/
|
|
81
81
|
| "no-reviewer"
|
|
82
82
|
/**
|
|
83
|
-
* A re-review was owed and deliberately skipped
|
|
84
|
-
* that touched test files exclusively (`gateCommitRoute` → `tests-only`).
|
|
83
|
+
* A re-review was owed and deliberately skipped.
|
|
85
84
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
85
|
+
* **No longer emitted.** It described the `gate` → `tests-only` route, which
|
|
86
|
+
* skipped `review_quality` as a cost tradeoff; #1510 closed that hole, so
|
|
87
|
+
* every committed gate fix is now re-reviewed and nothing writes this.
|
|
88
|
+
*
|
|
89
|
+
* Retained because the audit trail is read, not just written: a project that
|
|
90
|
+
* ran an earlier nax can hold rounds carrying this outcome, and dropping it
|
|
91
|
+
* from the union would make those unrenderable. Do not reuse the name for a
|
|
92
|
+
* new meaning — a reader hitting it in an old artifact must still be told
|
|
93
|
+
* what it meant when it was written.
|
|
93
94
|
*/
|
|
94
95
|
| "review-skipped";
|
|
95
96
|
|
|
@@ -106,6 +107,19 @@ export interface FinishRound {
|
|
|
106
107
|
findings: Finding[];
|
|
107
108
|
/** Gate commands that were red this round (gate phase). */
|
|
108
109
|
failing?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* The successor this round's commit routed to — `changed` / `tests-only` /
|
|
112
|
+
* `unchanged` for `gate`, `changed` / `unchanged` elsewhere.
|
|
113
|
+
*
|
|
114
|
+
* Recorded because `outcome` stopped carrying it. Until #1510 a tests-only
|
|
115
|
+
* gate fix was the only round writing `review-skipped`, so the outcome
|
|
116
|
+
* doubled as the classification; now every committed gate fix is reviewed
|
|
117
|
+
* and writes `no-reviewer`, which would leave "what did this fix touch?"
|
|
118
|
+
* unanswerable from the trail. That question is the input to deciding
|
|
119
|
+
* whether the re-review ever needs a cheaper, test-scoped form, so it has to
|
|
120
|
+
* survive the round it was computed in.
|
|
121
|
+
*/
|
|
122
|
+
route?: string;
|
|
109
123
|
/**
|
|
110
124
|
* `HEAD` SHA after this round's commit (set only when `committed`); absent
|
|
111
125
|
* on no-op rounds so a reader can distinguish "no commit" from "record lost".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "AI Coding Agent Orchestrator — loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,13 +39,22 @@
|
|
|
39
39
|
"test:e2e": "timeout -k 5s 180s bun test test/e2e/ --timeout=60000",
|
|
40
40
|
"test:coverage": "bun run scripts/check-coverage.ts",
|
|
41
41
|
"test:coverage:report": "bun run scripts/check-coverage.ts --report",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"check:test-sizes": "bun run scripts/check-test-sizes.ts",
|
|
42
|
+
"report:test-overlap": "bun run scripts/report-test-overlap.ts",
|
|
43
|
+
"report:dead-tests": "bun run scripts/report-dead-tests.ts",
|
|
45
44
|
"check:test-mocks": "bun scripts/check-inline-test-mocks.ts --strict",
|
|
46
45
|
"check:process-cwd": "bash scripts/check-process-cwd.sh",
|
|
47
46
|
"check:no-adapter-wrap": "bash scripts/check-no-adapter-wrap.sh",
|
|
48
47
|
"check:dispatch-context": "bash scripts/check-dispatch-context.sh",
|
|
48
|
+
"check:naxconfig-cast": "bash scripts/check-no-silent-naxconfig-cast.sh",
|
|
49
|
+
"check:runtime-cleanup": "bash scripts/check-runtime-cleanup.sh",
|
|
50
|
+
"check:scripts": "bash scripts/check-scripts.sh",
|
|
51
|
+
"check:adapter-no-config-import": "bash scripts/check-adapter-no-config-import.sh",
|
|
52
|
+
"check:test-typecheck": "bun run scripts/check-test-typecheck.ts",
|
|
53
|
+
"check:test-typecheck:update": "bun run scripts/check-test-typecheck.ts --update-baseline",
|
|
54
|
+
"check:test-as-unknown-as": "bun run scripts/check-test-as-unknown-as.ts",
|
|
55
|
+
"check:test-as-unknown-as:update": "bun run scripts/check-test-as-unknown-as.ts --update-baseline",
|
|
56
|
+
"check:gate-reachability": "bun run scripts/check-gate-reachability.ts",
|
|
57
|
+
"check:all": "bun run lint && bun run check:test-mocks && bun run check:process-cwd && bun run check:no-adapter-wrap && bun run check:scripts && bun run check:dispatch-context && bun run check:naxconfig-cast && bun run check:runtime-cleanup && bun run check:adapter-no-config-import && bun run check:test-typecheck && bun run check:test-as-unknown-as && bun run check:gate-reachability",
|
|
49
58
|
"prepublishOnly": "bun run build",
|
|
50
59
|
"test:full": "FULL=1 NAX_PRECHECK=1 bun test test/ --timeout=60000"
|
|
51
60
|
},
|