@nathapp/nax 0.75.6 → 0.76.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 +966 -312
- package/flows/nax-finish/commit-message.ts +136 -0
- package/flows/nax-finish/flow-ctx.ts +95 -0
- package/flows/nax-finish/nax-finish.flow.ts +129 -30
- package/flows/nax-finish/review-prompts.ts +42 -4
- package/flows/nax-finish/steps/context.ts +38 -0
- package/flows/nax-finish/steps/git.ts +26 -3
- package/flows/nax-finish/steps/result.ts +99 -10
- package/flows/nax-finish/types.ts +42 -0
- package/package.json +1 -1
|
@@ -1,23 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Finish-audit artifacts.
|
|
3
|
+
*
|
|
4
|
+
* These live under nax's global per-project output directory —
|
|
5
|
+
* `~/.nax/<project>/finish-audit/<feature>/` — alongside `prompt-audit/` and
|
|
6
|
+
* `review-audit/`, not in the user's repo. Two reasons the repo was the wrong
|
|
7
|
+
* home: the artifact describes a *run*, not the source tree, so committing it
|
|
8
|
+
* and gitignoring it are both wrong answers; and a per-feature, per-run path
|
|
9
|
+
* makes the history queryable across runs, which a single overwritten
|
|
10
|
+
* `.nax/nax-finish-result.json` never was.
|
|
11
|
+
*
|
|
12
|
+
* The plugin supplies `auditDir` because it owns nax's path SSOT
|
|
13
|
+
* (`src/runtime/paths.ts`), which this module may not import — `flows/` is
|
|
14
|
+
* loaded by acpx, outside nax's own process. Absent, we fall back to a
|
|
15
|
+
* repo-local directory so a hand-run `acpx flow run` still records something.
|
|
16
|
+
*
|
|
17
|
+
* Two files per run:
|
|
18
|
+
* - `<runId>.jsonl` — one line per fix round, appended as it happens
|
|
19
|
+
* - `<runId>.result.json` — the terminal result the plugin reads back
|
|
20
|
+
*/
|
|
21
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import type { FinishInput, FinishResult, FinishRound } from "../types";
|
|
4
24
|
|
|
5
|
-
|
|
6
|
-
|
|
25
|
+
/** Used when the plugin supplied no run id (e.g. a hand-run `acpx flow run`). */
|
|
26
|
+
const FALLBACK_RUN_ID = "run";
|
|
27
|
+
|
|
28
|
+
type AuditTarget = Pick<FinishInput, "auditDir" | "workdir" | "feature" | "runId">;
|
|
29
|
+
|
|
30
|
+
export function resolveAuditDir(input: AuditTarget): string {
|
|
31
|
+
return input.auditDir ?? join(input.workdir, ".nax", "finish-audit", input.feature);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resultPath(input: AuditTarget): string {
|
|
35
|
+
return join(resolveAuditDir(input), `${input.runId || FALLBACK_RUN_ID}.result.json`);
|
|
7
36
|
}
|
|
8
37
|
|
|
9
|
-
export
|
|
38
|
+
export function roundsPath(input: AuditTarget): string {
|
|
39
|
+
return join(resolveAuditDir(input), `${input.runId || FALLBACK_RUN_ID}.jsonl`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const _resultDeps: {
|
|
43
|
+
writeText: (p: string, s: string) => Promise<void>;
|
|
44
|
+
appendText: (p: string, s: string) => Promise<void>;
|
|
45
|
+
readText: (p: string) => Promise<string | null>;
|
|
46
|
+
} = {
|
|
10
47
|
// node:fs, not Bun.write — this module runs inside acpx's Node process, where
|
|
11
48
|
// the `Bun` global does not exist (see the header of `../exec.ts`). The mkdir
|
|
12
49
|
// is not redundant: Bun.write creates missing parent directories implicitly,
|
|
13
|
-
// writeFile does not, and
|
|
14
|
-
//
|
|
50
|
+
// writeFile does not, and the audit directory now lives under `~/.nax/`,
|
|
51
|
+
// where for a project's first run nothing on the path exists yet.
|
|
15
52
|
writeText: async (p, s) => {
|
|
16
53
|
await mkdir(dirname(p), { recursive: true });
|
|
17
54
|
await writeFile(p, s, "utf8");
|
|
18
55
|
},
|
|
56
|
+
appendText: async (p, s) => {
|
|
57
|
+
await mkdir(dirname(p), { recursive: true });
|
|
58
|
+
await writeFile(p, s, { encoding: "utf8", flag: "a" });
|
|
59
|
+
},
|
|
60
|
+
readText: async (p) => {
|
|
61
|
+
try {
|
|
62
|
+
return await readFile(p, "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
19
67
|
};
|
|
20
68
|
|
|
21
|
-
|
|
22
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Append one fix round to the run's audit trail.
|
|
71
|
+
*
|
|
72
|
+
* Best-effort: an unwritable audit directory must not take the flow down
|
|
73
|
+
* mid-loop. The round is a record of work already done — losing the record is
|
|
74
|
+
* bad, losing the run that did the work is worse.
|
|
75
|
+
*/
|
|
76
|
+
export async function appendRound(input: AuditTarget, round: FinishRound): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
await _resultDeps.appendText(roundsPath(input), `${JSON.stringify(round)}\n`);
|
|
79
|
+
} catch {
|
|
80
|
+
// Intentionally swallowed — see the doc comment above.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read back every round recorded for this run, so a terminal result can embed them. */
|
|
85
|
+
export async function readRounds(input: AuditTarget): Promise<FinishRound[]> {
|
|
86
|
+
const raw = await _resultDeps.readText(roundsPath(input));
|
|
87
|
+
if (!raw) return [];
|
|
88
|
+
const rounds: FinishRound[] = [];
|
|
89
|
+
for (const line of raw.split("\n")) {
|
|
90
|
+
if (!line.trim()) continue;
|
|
91
|
+
try {
|
|
92
|
+
rounds.push(JSON.parse(line) as FinishRound);
|
|
93
|
+
} catch {
|
|
94
|
+
// A torn final line (killed mid-write) must not lose the rounds before it.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return rounds;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Write the terminal result, embedding every round this run recorded.
|
|
102
|
+
*
|
|
103
|
+
* Rounds are attached on *every* status, not just `escalated`: a finish that
|
|
104
|
+
* succeeded after four rounds is precisely the case worth auditing — it says
|
|
105
|
+
* the run's own review gates missed four defects — and it was the one case
|
|
106
|
+
* that previously recorded nothing at all.
|
|
107
|
+
*/
|
|
108
|
+
export async function writeResult(input: AuditTarget, result: FinishResult): Promise<void> {
|
|
109
|
+
const rounds = await readRounds(input);
|
|
110
|
+
const withRounds: FinishResult = rounds.length > 0 ? { ...result, rounds } : result;
|
|
111
|
+
await _resultDeps.writeText(resultPath(input), `${JSON.stringify(withRounds, null, 2)}\n`);
|
|
23
112
|
}
|
|
@@ -29,11 +29,46 @@ export interface FinishTimeouts {
|
|
|
29
29
|
acceptanceMs?: number;
|
|
30
30
|
gateMs?: number;
|
|
31
31
|
}
|
|
32
|
+
/** The four fix-and-reverify loops, in graph order. */
|
|
33
|
+
export type FinishPhase = "acceptance" | "spec" | "quality" | "gate";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One completed fix round, appended to the audit trail as it happens.
|
|
37
|
+
*
|
|
38
|
+
* Rounds are appended at `commit_<phase>` as they happen rather than
|
|
39
|
+
* reconstructed by a terminal node from `ctx.state.steps` (which does retain
|
|
40
|
+
* every step's output). Appending live is what makes the trail survive a flow
|
|
41
|
+
* that is killed or times out: no terminal node runs on those paths, and a
|
|
42
|
+
* finish that died mid-loop is exactly when the record of what it already
|
|
43
|
+
* changed on the branch matters most.
|
|
44
|
+
*/
|
|
45
|
+
export interface FinishRound {
|
|
46
|
+
ts: string;
|
|
47
|
+
phase: FinishPhase;
|
|
48
|
+
/** 1-based; the Nth time this phase's fix node has run. */
|
|
49
|
+
attempt: number;
|
|
50
|
+
/** True when the fix produced a commit; false when it changed nothing. */
|
|
51
|
+
committed: boolean;
|
|
52
|
+
/** Reviewer findings this round set out to fix (spec/quality phases). */
|
|
53
|
+
findings: Finding[];
|
|
54
|
+
/** Gate commands that were red this round (gate phase). */
|
|
55
|
+
failing?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
export interface FinishInput {
|
|
33
59
|
feature: string;
|
|
34
60
|
workdir: string;
|
|
35
61
|
branch: string;
|
|
36
62
|
prdPath: string;
|
|
63
|
+
/**
|
|
64
|
+
* Directory for this feature's finish-audit artifacts, e.g.
|
|
65
|
+
* `~/.nax/<project>/finish-audit/<feature>`. Supplied by the plugin, which
|
|
66
|
+
* owns nax's path SSOT (`src/runtime/paths.ts`) that this module may not
|
|
67
|
+
* import. Absent → the flow falls back to a repo-local directory.
|
|
68
|
+
*/
|
|
69
|
+
auditDir?: string;
|
|
70
|
+
/** Run id, used to name this run's audit files. Absent → "run". */
|
|
71
|
+
runId?: string;
|
|
37
72
|
/**
|
|
38
73
|
* True only when Telegram escalation is both enabled *and* credentialed, as
|
|
39
74
|
* determined by the plugin. When true the flow skips the PR/MR comment
|
|
@@ -62,6 +97,13 @@ export interface FinishResult {
|
|
|
62
97
|
* rather than lost.
|
|
63
98
|
*/
|
|
64
99
|
deliveryError?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Every fix round the flow ran, on *all* terminal statuses — not just
|
|
102
|
+
* escalations. A successful finish that took four rounds to get there is the
|
|
103
|
+
* case worth auditing (it says the run's own review gates missed four
|
|
104
|
+
* defects), and it was previously the one case that recorded nothing.
|
|
105
|
+
*/
|
|
106
|
+
rounds?: FinishRound[];
|
|
65
107
|
}
|
|
66
108
|
export interface RunResult {
|
|
67
109
|
exitCode: number;
|