@wildorder/nightshift 0.11.1 → 0.13.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/author.d.ts.map +1 -1
- package/dist/author.js +43 -0
- package/dist/author.js.map +1 -1
- package/dist/cli.js +86 -3
- package/dist/cli.js.map +1 -1
- package/dist/commit-plan.d.ts +40 -0
- package/dist/commit-plan.d.ts.map +1 -0
- package/dist/commit-plan.js +154 -0
- package/dist/commit-plan.js.map +1 -0
- package/dist/decision-ledger.d.ts +10 -0
- package/dist/decision-ledger.d.ts.map +1 -1
- package/dist/decision-ledger.js +7 -0
- package/dist/decision-ledger.js.map +1 -1
- package/dist/exit-codes.d.ts +16 -2
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +17 -2
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/install-skills.d.ts +1 -1
- package/dist/install-skills.d.ts.map +1 -1
- package/dist/install-skills.js +5 -1
- package/dist/install-skills.js.map +1 -1
- package/dist/manifest.d.ts +47 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +80 -11
- package/dist/manifest.js.map +1 -1
- package/dist/preflight.d.ts +73 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +225 -0
- package/dist/preflight.js.map +1 -0
- package/dist/publish.d.ts +36 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +185 -0
- package/dist/publish.js.map +1 -1
- package/dist/run-program.d.ts +35 -1
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +224 -10
- package/dist/run-program.js.map +1 -1
- package/dist/skill-roots.d.ts +11 -3
- package/dist/skill-roots.d.ts.map +1 -1
- package/dist/skill-roots.js +59 -12
- package/dist/skill-roots.js.map +1 -1
- package/package.json +2 -2
- package/skills/backlog-capture/SKILL.md +182 -0
- package/skills/plan-program/SKILL.md +221 -5
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LedgerEvent } from "./decision-ledger.js";
|
|
2
|
+
import type { ProgramManifest } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The preflight stage: at run start, before baseline verification and any
|
|
5
|
+
* agent spawn, executes every *pending* prerequisite's `verifyCommand` as a
|
|
6
|
+
* deterministic, bounded subprocess. Exit zero flips it to `satisfied` (in
|
|
7
|
+
* memory — the caller decides when to persist); anything else — nonzero
|
|
8
|
+
* exit, spawn failure, timeout — uniformly means unmet, with a captured
|
|
9
|
+
* reason, and never throws out of this module. See
|
|
10
|
+
* `tasks/human-prerequisites/ws-02-*.md` for the full design.
|
|
11
|
+
*/
|
|
12
|
+
/** The wall-clock ceiling on a single prerequisite check. A deterministic
|
|
13
|
+
* check that has not answered in this long is treated as unmet, and the
|
|
14
|
+
* process tree is killed so it can never outlive the run. A constant, never
|
|
15
|
+
* a config key — the charter keeps tuning knobs off the config surface. */
|
|
16
|
+
export declare const PREREQUISITE_TIMEOUT_MS = 60000;
|
|
17
|
+
export interface PrerequisiteRun {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
output: string;
|
|
20
|
+
/** True when the process tree was killed for exceeding the timeout. */
|
|
21
|
+
timedOut: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type PrerequisiteRunner = (command: string, cwd: string) => Promise<PrerequisiteRun>;
|
|
24
|
+
/**
|
|
25
|
+
* Builds a bounded, process-tree-killing `PrerequisiteRunner`. Fixing the
|
|
26
|
+
* timeout inside a `(command, cwd)` runner would leave a timeout test no
|
|
27
|
+
* choice but to wait out the real production ceiling; this factory keeps the
|
|
28
|
+
* ceiling a test seam while {@link defaultPrerequisiteRunner} wires the
|
|
29
|
+
* production constant once.
|
|
30
|
+
*/
|
|
31
|
+
export declare function makePrerequisiteRunner(timeoutMs?: number): PrerequisiteRunner;
|
|
32
|
+
/** The production runner: the 60 s ceiling, wired once. */
|
|
33
|
+
export declare const defaultPrerequisiteRunner: PrerequisiteRunner;
|
|
34
|
+
export interface PrerequisiteCheck {
|
|
35
|
+
id: string;
|
|
36
|
+
met: boolean;
|
|
37
|
+
/** Captured reason when unmet — always non-empty so the report never
|
|
38
|
+
* renders a blank line. The tailed verify output on a nonzero exit; on a
|
|
39
|
+
* nonzero exit that emitted no output, a fallback naming the exit code;
|
|
40
|
+
* the spawn error message (or a generic notice) on a spawn failure; a
|
|
41
|
+
* notice naming the ceiling on a timeout. Absent only when met. */
|
|
42
|
+
reason?: string;
|
|
43
|
+
/** True when this run's preflight flipped it pending → satisfied. False
|
|
44
|
+
* for an already-satisfied prerequisite (not re-run) and for an unmet
|
|
45
|
+
* one. */
|
|
46
|
+
newlySatisfied: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PreflightResult {
|
|
49
|
+
checks: PrerequisiteCheck[];
|
|
50
|
+
/** Ledger events for the prerequisites this run newly satisfied. The
|
|
51
|
+
* caller appends these once its own baseline verification admits the run
|
|
52
|
+
* — see run-program.ts's staged-persistence wiring. */
|
|
53
|
+
events: LedgerEvent[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs every pending prerequisite's `verifyCommand` once. Mutates
|
|
57
|
+
* `manifest.prerequisites` in place on a met check (pending → satisfied);
|
|
58
|
+
* the caller decides when (and whether) to persist that mutation to disk. An
|
|
59
|
+
* already-`satisfied` prerequisite is never re-run — settled facts stay
|
|
60
|
+
* settled (SC-06). No `verifyCommand` outcome can throw out of this
|
|
61
|
+
* function or halt the run (SC-03): a thrown spawn failure is caught here.
|
|
62
|
+
*/
|
|
63
|
+
export declare function runPreflight(args: {
|
|
64
|
+
manifest: ProgramManifest;
|
|
65
|
+
cwd: string;
|
|
66
|
+
runner: PrerequisiteRunner;
|
|
67
|
+
now: () => Date;
|
|
68
|
+
/** The run's identity, stamped on each `prerequisite-verified` event. */
|
|
69
|
+
runId: string;
|
|
70
|
+
runStartCommit: string | undefined;
|
|
71
|
+
log: (line: string) => void;
|
|
72
|
+
}): Promise<PreflightResult>;
|
|
73
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD;;;;;;;;GAQG;AAEH;;;4EAG4E;AAC5E,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAe9C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,KACR,OAAO,CAAC,eAAe,CAAC,CAAC;AAiD9B;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,GAAE,MAAgC,GAC1C,kBAAkB,CAqEpB;AAED,2DAA2D;AAC3D,eAAO,MAAM,yBAAyB,EAAE,kBACd,CAAC;AAE3B,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,OAAO,CAAC;IACb;;;;wEAIoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;eAEW;IACX,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B;;4DAEwD;IACxD,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,eAAe,CAAC,CA0E3B"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { tail } from "./agent-runner.js";
|
|
3
|
+
/**
|
|
4
|
+
* The preflight stage: at run start, before baseline verification and any
|
|
5
|
+
* agent spawn, executes every *pending* prerequisite's `verifyCommand` as a
|
|
6
|
+
* deterministic, bounded subprocess. Exit zero flips it to `satisfied` (in
|
|
7
|
+
* memory — the caller decides when to persist); anything else — nonzero
|
|
8
|
+
* exit, spawn failure, timeout — uniformly means unmet, with a captured
|
|
9
|
+
* reason, and never throws out of this module. See
|
|
10
|
+
* `tasks/human-prerequisites/ws-02-*.md` for the full design.
|
|
11
|
+
*/
|
|
12
|
+
/** The wall-clock ceiling on a single prerequisite check. A deterministic
|
|
13
|
+
* check that has not answered in this long is treated as unmet, and the
|
|
14
|
+
* process tree is killed so it can never outlive the run. A constant, never
|
|
15
|
+
* a config key — the charter keeps tuning knobs off the config surface. */
|
|
16
|
+
export const PREREQUISITE_TIMEOUT_MS = 60_000;
|
|
17
|
+
// In-memory output is kept to a bounded tail, mirroring agent-runner.ts's
|
|
18
|
+
// own OUTPUT_TAIL_LIMIT — this module intentionally does not import that
|
|
19
|
+
// private constant.
|
|
20
|
+
const OUTPUT_TAIL_LIMIT = 200_000;
|
|
21
|
+
/** How long a timed-out check waits, past its own ceiling, for the kill
|
|
22
|
+
* request to take effect before giving up on `close` and settling anyway.
|
|
23
|
+
* A kill (`taskkill`, a signalled process group) has no completion callback
|
|
24
|
+
* to await, so without this bound a slow or failed kill would turn a
|
|
25
|
+
* *bounded* check into an unbounded one — this is the backstop that keeps
|
|
26
|
+
* the promise settling promptly regardless. */
|
|
27
|
+
const KILL_GRACE_MS = 3_000;
|
|
28
|
+
/**
|
|
29
|
+
* Kills a spawned shell line's whole process tree, not just the shell
|
|
30
|
+
* process `spawn` returned — a `verifyCommand` may fork descendants (node,
|
|
31
|
+
* curl, a helper script), and `child.kill()` alone would orphan them: on
|
|
32
|
+
* Windows in particular, killing only the returned `cmd.exe` process leaves
|
|
33
|
+
* its own child (say, `node`) running and still holding the inherited stdio
|
|
34
|
+
* pipe handles, so `close` never fires until that orphan exits on its own —
|
|
35
|
+
* confirmed empirically (see the git history of this function's tests): a
|
|
36
|
+
* `child.kill()` issued *before* `taskkill /T` gets a chance to walk the
|
|
37
|
+
* still-live tree kills the wrapper first, orphans the descendant, and
|
|
38
|
+
* taskkill then reports "not found" for a pid that is already gone —
|
|
39
|
+
* reproducing exactly the unbounded-wait failure this function exists to
|
|
40
|
+
* prevent. So the whole tree is killed as a single unit, by pid, while the
|
|
41
|
+
* tree is still intact — never a separate signal to the immediate child
|
|
42
|
+
* first.
|
|
43
|
+
*
|
|
44
|
+
* POSIX: the child was spawned `detached: true`, making it its own
|
|
45
|
+
* process-group leader; signalling the negative pid signals the whole group
|
|
46
|
+
* atomically. `ESRCH` (the group already exited) is swallowed — best effort.
|
|
47
|
+
*
|
|
48
|
+
* Windows: there is no process-group signal, so `taskkill /T /F` is used,
|
|
49
|
+
* which walks the live process tree from `pid` and terminates it forcibly in
|
|
50
|
+
* one call. Fire-and-forget as far as this function is concerned — its
|
|
51
|
+
* caller (the grace timer in {@link makePrerequisiteRunner}) is what bounds
|
|
52
|
+
* the wait, since an external kill request has no completion callback of its
|
|
53
|
+
* own to await; a spawn failure for `taskkill` itself (e.g. an unusual PATH)
|
|
54
|
+
* is swallowed the same way.
|
|
55
|
+
*/
|
|
56
|
+
function killProcessTree(pid) {
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
});
|
|
62
|
+
killer.on("error", () => {
|
|
63
|
+
// taskkill itself failed to spawn — the grace timer still bounds the
|
|
64
|
+
// wait regardless.
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
process.kill(-pid, "SIGKILL");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Already exited, or never became a group leader — best effort.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds a bounded, process-tree-killing `PrerequisiteRunner`. Fixing the
|
|
77
|
+
* timeout inside a `(command, cwd)` runner would leave a timeout test no
|
|
78
|
+
* choice but to wait out the real production ceiling; this factory keeps the
|
|
79
|
+
* ceiling a test seam while {@link defaultPrerequisiteRunner} wires the
|
|
80
|
+
* production constant once.
|
|
81
|
+
*/
|
|
82
|
+
export function makePrerequisiteRunner(timeoutMs = PREREQUISITE_TIMEOUT_MS) {
|
|
83
|
+
return (command, cwd) => new Promise((resolvePromise, rejectPromise) => {
|
|
84
|
+
// shell: true, exactly like defaultVerifyRunner — a verifyCommand is a
|
|
85
|
+
// whole shell line the human/planner authored, run under cmd /c or
|
|
86
|
+
// /bin/sh -c. detached: true on POSIX makes the child a process-group
|
|
87
|
+
// leader so killProcessTree can signal the whole tree; Windows has no
|
|
88
|
+
// such option and uses taskkill /T instead.
|
|
89
|
+
const child = spawn(command, [], {
|
|
90
|
+
cwd,
|
|
91
|
+
shell: true,
|
|
92
|
+
windowsHide: true,
|
|
93
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
94
|
+
...(process.platform === "win32" ? {} : { detached: true }),
|
|
95
|
+
});
|
|
96
|
+
let buffered = "";
|
|
97
|
+
const push = (chunk) => {
|
|
98
|
+
buffered = (buffered + chunk).slice(-OUTPUT_TAIL_LIMIT);
|
|
99
|
+
};
|
|
100
|
+
child.stdout?.setEncoding("utf8");
|
|
101
|
+
child.stderr?.setEncoding("utf8");
|
|
102
|
+
child.stdout?.on("data", push);
|
|
103
|
+
child.stderr?.on("data", push);
|
|
104
|
+
// Never reads stdin, and ends it immediately — the same headless
|
|
105
|
+
// posture as defaultVerifyRunner (agent-runner.ts).
|
|
106
|
+
child.stdin?.end();
|
|
107
|
+
let settled = false;
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
let graceTimer;
|
|
110
|
+
const settle = (result) => {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
settled = true;
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
clearTimeout(graceTimer);
|
|
116
|
+
resolvePromise(result);
|
|
117
|
+
};
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
timedOut = true;
|
|
120
|
+
if (child.pid !== undefined)
|
|
121
|
+
killProcessTree(child.pid);
|
|
122
|
+
// The kill above is a request, not a confirmation — `taskkill` and a
|
|
123
|
+
// signalled process group have no completion callback this runner
|
|
124
|
+
// can await, and a slow or failed kill must never turn a *bounded*
|
|
125
|
+
// check into an unbounded one (SC-03/SC-11: the run always
|
|
126
|
+
// terminates). `close` still wins the race in the overwhelmingly
|
|
127
|
+
// common case where the kill lands promptly; this grace window is
|
|
128
|
+
// strictly a backstop for the rest.
|
|
129
|
+
graceTimer = setTimeout(() => {
|
|
130
|
+
settle({ exitCode: 1, output: buffered, timedOut: true });
|
|
131
|
+
}, KILL_GRACE_MS);
|
|
132
|
+
graceTimer.unref();
|
|
133
|
+
}, timeoutMs);
|
|
134
|
+
timer.unref();
|
|
135
|
+
child.on("error", (error) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
clearTimeout(graceTimer);
|
|
141
|
+
rejectPromise(error);
|
|
142
|
+
});
|
|
143
|
+
child.on("close", (code) => {
|
|
144
|
+
settle({ exitCode: code ?? 1, output: buffered, timedOut });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** The production runner: the 60 s ceiling, wired once. */
|
|
149
|
+
export const defaultPrerequisiteRunner = makePrerequisiteRunner();
|
|
150
|
+
/**
|
|
151
|
+
* Runs every pending prerequisite's `verifyCommand` once. Mutates
|
|
152
|
+
* `manifest.prerequisites` in place on a met check (pending → satisfied);
|
|
153
|
+
* the caller decides when (and whether) to persist that mutation to disk. An
|
|
154
|
+
* already-`satisfied` prerequisite is never re-run — settled facts stay
|
|
155
|
+
* settled (SC-06). No `verifyCommand` outcome can throw out of this
|
|
156
|
+
* function or halt the run (SC-03): a thrown spawn failure is caught here.
|
|
157
|
+
*/
|
|
158
|
+
export async function runPreflight(args) {
|
|
159
|
+
const { manifest, cwd, runner, now, runId, runStartCommit, log } = args;
|
|
160
|
+
const checks = [];
|
|
161
|
+
const events = [];
|
|
162
|
+
for (const prerequisite of manifest.prerequisites) {
|
|
163
|
+
if (prerequisite.status === "satisfied") {
|
|
164
|
+
checks.push({ id: prerequisite.id, met: true, newlySatisfied: false });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let run;
|
|
168
|
+
try {
|
|
169
|
+
run = await runner(prerequisite.verifyCommand, cwd);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error?.message?.trim();
|
|
173
|
+
const reason = message && message !== ""
|
|
174
|
+
? message
|
|
175
|
+
: "the verify command could not be started";
|
|
176
|
+
checks.push({
|
|
177
|
+
id: prerequisite.id,
|
|
178
|
+
met: false,
|
|
179
|
+
reason,
|
|
180
|
+
newlySatisfied: false,
|
|
181
|
+
});
|
|
182
|
+
log(`preflight: ${prerequisite.id} unmet — ${reason}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (run.timedOut) {
|
|
186
|
+
const reason = `the verify command did not finish within ` +
|
|
187
|
+
`${PREREQUISITE_TIMEOUT_MS}ms and was terminated`;
|
|
188
|
+
checks.push({
|
|
189
|
+
id: prerequisite.id,
|
|
190
|
+
met: false,
|
|
191
|
+
reason,
|
|
192
|
+
newlySatisfied: false,
|
|
193
|
+
});
|
|
194
|
+
log(`preflight: ${prerequisite.id} unmet — ${reason}`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (run.exitCode !== 0) {
|
|
198
|
+
const tailed = tail(run.output, 1500).trim();
|
|
199
|
+
const reason = tailed !== ""
|
|
200
|
+
? tailed
|
|
201
|
+
: `the verify command exited ${run.exitCode} with no output`;
|
|
202
|
+
checks.push({
|
|
203
|
+
id: prerequisite.id,
|
|
204
|
+
met: false,
|
|
205
|
+
reason,
|
|
206
|
+
newlySatisfied: false,
|
|
207
|
+
});
|
|
208
|
+
log(`preflight: ${prerequisite.id} unmet — ${reason}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
prerequisite.status = "satisfied";
|
|
212
|
+
checks.push({ id: prerequisite.id, met: true, newlySatisfied: true });
|
|
213
|
+
events.push({
|
|
214
|
+
kind: "prerequisite-verified",
|
|
215
|
+
at: now().toISOString(),
|
|
216
|
+
id: prerequisite.id,
|
|
217
|
+
verifyCommand: prerequisite.verifyCommand,
|
|
218
|
+
runId,
|
|
219
|
+
...(runStartCommit === undefined ? {} : { commit: runStartCommit }),
|
|
220
|
+
});
|
|
221
|
+
log(`preflight: ${prerequisite.id} satisfied`);
|
|
222
|
+
}
|
|
223
|
+
return { checks, events };
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"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"}
|
package/dist/publish.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type AgentRunner } from "./agent-runner.js";
|
|
2
|
+
import { type NightshiftConfig } from "./config.js";
|
|
1
3
|
/**
|
|
2
4
|
* The handover from a finished run to a reviewable pull request: pushes the
|
|
3
5
|
* program branch, opens or updates exactly one draft PR through `gh`,
|
|
@@ -65,6 +67,32 @@ export interface PublishGit {
|
|
|
65
67
|
/** Is the branch ahead of base? `git rev-list --count <base>..<branch>`. */
|
|
66
68
|
commitsAhead(cwd: string, base: string, branch: string): Promise<number>;
|
|
67
69
|
push(cwd: string, remote: string, branch: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Fetches `branch` from `remote` so a subsequent merge sees its current
|
|
72
|
+
* tip, not whatever this checkout last saw at clone/checkout time.
|
|
73
|
+
*/
|
|
74
|
+
fetchBranch(cwd: string, remote: string, branch: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Merges `ref` into the checked-out branch without committing
|
|
77
|
+
* (`git merge --no-commit --no-ff`). `"up-to-date"` means `ref` was
|
|
78
|
+
* already an ancestor of HEAD and nothing happened; `"merged"` means it
|
|
79
|
+
* merged cleanly and is staged, waiting for `commitMerge`; `"conflict"`
|
|
80
|
+
* means it left unmerged paths and the merge is still in progress.
|
|
81
|
+
*/
|
|
82
|
+
mergeNoCommit(cwd: string, ref: string): Promise<{
|
|
83
|
+
status: "up-to-date";
|
|
84
|
+
} | {
|
|
85
|
+
status: "merged";
|
|
86
|
+
} | {
|
|
87
|
+
status: "conflict";
|
|
88
|
+
paths: string[];
|
|
89
|
+
}>;
|
|
90
|
+
/** Aborts an in-progress merge, restoring the pre-merge working tree. */
|
|
91
|
+
abortMerge(cwd: string): Promise<void>;
|
|
92
|
+
/** Resolves one unmerged path to the checked-out branch's own side, and stages it. */
|
|
93
|
+
checkoutOurs(cwd: string, path: string): Promise<void>;
|
|
94
|
+
/** Commits a merge staged by `mergeNoCommit`/`checkoutOurs`. */
|
|
95
|
+
commitMerge(cwd: string, message: string): Promise<void>;
|
|
68
96
|
}
|
|
69
97
|
export declare const defaultPublishGit: PublishGit;
|
|
70
98
|
export declare const defaultGhClient: GhClient;
|
|
@@ -75,6 +103,14 @@ export interface PublishOptions {
|
|
|
75
103
|
gh?: GhClient;
|
|
76
104
|
/** Injected git boundary; default execFile-backed. */
|
|
77
105
|
git?: PublishGit;
|
|
106
|
+
/**
|
|
107
|
+
* Loaded internally via `loadConfig` when omitted, exactly like `run` and
|
|
108
|
+
* `author` load it in `cli.ts` — publish has no dedicated action of its
|
|
109
|
+
* own to load it first. Test seam only; production never needs to pass it.
|
|
110
|
+
*/
|
|
111
|
+
config?: NightshiftConfig;
|
|
112
|
+
/** Passed straight through to the pre-publish as-built refresh; default reviewer runner. */
|
|
113
|
+
agentRunner?: AgentRunner;
|
|
78
114
|
log?: (line: string) => void;
|
|
79
115
|
}
|
|
80
116
|
export interface PublishResult {
|
package/dist/publish.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../src/publish.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../src/publish.ts"],"names":[],"mappings":"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"}
|
package/dist/publish.js
CHANGED
|
@@ -4,11 +4,14 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { sanitizedEnvironment } from "./agent-runner.js";
|
|
7
|
+
import { refreshAsBuilt } from "./as-built.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
7
9
|
import { readDecisionLedger } from "./decision-ledger.js";
|
|
8
10
|
import { escalatedRecords, renderRecord, } from "./decision-view.js";
|
|
9
11
|
import { loadManifest } from "./manifest.js";
|
|
10
12
|
import { detectDefaultBranch, programBranchName } from "./program-branch.js";
|
|
11
13
|
import { runReportPath } from "./report-path.js";
|
|
14
|
+
import { AS_BUILT_PATH } from "./whole-program-review.js";
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
13
16
|
/**
|
|
14
17
|
* The handover from a finished run to a reviewable pull request: pushes the
|
|
@@ -81,6 +84,59 @@ export const defaultPublishGit = {
|
|
|
81
84
|
throw new Error(`git push -u ${remote} ${branch} failed:\n${stderr.trim()}`, { cause: error });
|
|
82
85
|
}
|
|
83
86
|
},
|
|
87
|
+
async fetchBranch(cwd, remote, branch) {
|
|
88
|
+
try {
|
|
89
|
+
await execFileAsync("git", ["fetch", remote, branch], { cwd });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const stderr = String(error.stderr ?? error.message);
|
|
93
|
+
throw new Error(`git fetch ${remote} ${branch} failed:\n${stderr.trim()}`, { cause: error });
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async mergeNoCommit(cwd, ref) {
|
|
97
|
+
try {
|
|
98
|
+
await execFileAsync("git", ["merge", "--no-commit", "--no-ff", ref], { cwd });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const { stdout: unmerged } = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
102
|
+
const paths = unmerged.split(/\r?\n/u).filter((line) => line.trim() !== "");
|
|
103
|
+
if (paths.length === 0) {
|
|
104
|
+
// Merge failed for a reason other than a content conflict (a dirty
|
|
105
|
+
// tree, a mid-merge repository, ...) — not ours to resolve.
|
|
106
|
+
const stderr = String(error.stderr ?? error.message);
|
|
107
|
+
throw new Error(`git merge --no-ff ${ref} failed:\n${stderr.trim()}`, {
|
|
108
|
+
cause: error,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { status: "conflict", paths };
|
|
112
|
+
}
|
|
113
|
+
// `--no-commit` still leaves a real merge (one that was not a no-op)
|
|
114
|
+
// staged rather than committed; `MERGE_HEAD` is git's own record of
|
|
115
|
+
// that, and is absent exactly when there was nothing to merge.
|
|
116
|
+
try {
|
|
117
|
+
await execFileAsync("git", ["rev-parse", "--verify", "-q", "MERGE_HEAD"], { cwd });
|
|
118
|
+
return { status: "merged" };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return { status: "up-to-date" };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async abortMerge(cwd) {
|
|
125
|
+
await execFileAsync("git", ["merge", "--abort"], { cwd });
|
|
126
|
+
},
|
|
127
|
+
async checkoutOurs(cwd, path) {
|
|
128
|
+
await execFileAsync("git", ["checkout", "--ours", "--", path], { cwd });
|
|
129
|
+
await execFileAsync("git", ["add", "--", path], { cwd });
|
|
130
|
+
},
|
|
131
|
+
async commitMerge(cwd, message) {
|
|
132
|
+
try {
|
|
133
|
+
await execFileAsync("git", ["commit", "-m", message], { cwd });
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const stderr = String(error.stderr ?? error.message);
|
|
137
|
+
throw new Error(`git commit (merge) failed:\n${stderr.trim()}`, { cause: error });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
84
140
|
};
|
|
85
141
|
/**
|
|
86
142
|
* Spawns `gh` through the same primitives the rest of the codebase spawns
|
|
@@ -491,6 +547,122 @@ async function reconcileComments(gh, cwd, prNumber, desired) {
|
|
|
491
547
|
}
|
|
492
548
|
return { escalationComments, continuationComments };
|
|
493
549
|
}
|
|
550
|
+
// ---- pre-publish as-built sync -------------------------------------------
|
|
551
|
+
/**
|
|
552
|
+
* `docs/as-built.md` is regenerated wholesale, not patched, so once the base
|
|
553
|
+
* branch and the program branch have each regenerated it independently, a
|
|
554
|
+
* GitHub merge sees two unrelated full rewrites of the same file and
|
|
555
|
+
* conflicts almost every time content differs, even though nothing about the
|
|
556
|
+
* change is actually incompatible. Merging the base into the branch first —
|
|
557
|
+
* so the branch's copy of the file descends from the base's — turns that
|
|
558
|
+
* into an ordinary fast-forward-compatible history and the PR stops
|
|
559
|
+
* conflicting on it.
|
|
560
|
+
*
|
|
561
|
+
* This step is entirely best-effort and never turns a publish that would
|
|
562
|
+
* have succeeded before this existed into one that fails: any git failure
|
|
563
|
+
* along the way — fetch, merge, or commit — is logged and the merge is left
|
|
564
|
+
* alone (aborted, when one was left in progress) rather than surfaced as a
|
|
565
|
+
* publish failure. A merge that conflicts on files other than
|
|
566
|
+
* `docs/as-built.md` is not this function's to resolve at all; it aborts and
|
|
567
|
+
* leaves the branch exactly as publish found it, the same as if this step
|
|
568
|
+
* did not exist.
|
|
569
|
+
*/
|
|
570
|
+
async function syncAsBuiltWithBase(args) {
|
|
571
|
+
const { root, git, base, branch, programId, config, agentRunner, log } = args;
|
|
572
|
+
const baseRef = `origin/${base}`;
|
|
573
|
+
try {
|
|
574
|
+
await git.fetchBranch(root, "origin", base);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
log(`pre-publish as-built sync: could not fetch \`${base}\`, skipping — ` +
|
|
578
|
+
error.message);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
let mergeResult;
|
|
582
|
+
try {
|
|
583
|
+
mergeResult = await git.mergeNoCommit(root, baseRef);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
// A failure here is not necessarily "never started" — abort defensively
|
|
587
|
+
// so a merge git did leave in progress (a shape `mergeNoCommit`'s own
|
|
588
|
+
// conflict detection did not recognize) never lingers in the working
|
|
589
|
+
// tree after this function returns.
|
|
590
|
+
try {
|
|
591
|
+
await git.abortMerge(root);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Best-effort only; nothing was left staged if there was no merge to
|
|
595
|
+
// abort, and the original failure below is what matters.
|
|
596
|
+
}
|
|
597
|
+
log(`pre-publish as-built sync: could not merge \`${baseRef}\` into ` +
|
|
598
|
+
`\`${branch}\`, skipping — ${error.message}`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (mergeResult.status === "up-to-date")
|
|
602
|
+
return;
|
|
603
|
+
if (mergeResult.status === "conflict") {
|
|
604
|
+
const other = mergeResult.paths.filter((path) => path !== AS_BUILT_PATH);
|
|
605
|
+
if (other.length > 0) {
|
|
606
|
+
await git.abortMerge(root);
|
|
607
|
+
log(`pre-publish as-built sync: merging \`${baseRef}\` into \`${branch}\` ` +
|
|
608
|
+
`conflicts outside \`${AS_BUILT_PATH}\` (${other.join(", ")}); left ` +
|
|
609
|
+
`the branch as publish found it. Merge \`${base}\` into \`${branch}\` ` +
|
|
610
|
+
"yourself, resolve the conflicts, and re-run publish.");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
await git.checkoutOurs(root, AS_BUILT_PATH);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
await git.abortMerge(root);
|
|
618
|
+
log(`pre-publish as-built sync: could not resolve the \`${AS_BUILT_PATH}\` ` +
|
|
619
|
+
`conflict, skipping — ${error.message}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
await git.commitMerge(root, `nightshift(${programId}): merge ${base} before publish`);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
try {
|
|
628
|
+
await git.abortMerge(root);
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Best-effort cleanup only; the commit failure below is what matters.
|
|
632
|
+
}
|
|
633
|
+
log(`pre-publish as-built sync: could not commit the merge of \`${baseRef}\`, ` +
|
|
634
|
+
`skipping — ${error.message}`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
log(`pre-publish as-built sync: merged \`${baseRef}\` into \`${branch}\``);
|
|
638
|
+
try {
|
|
639
|
+
const result = await refreshAsBuilt({
|
|
640
|
+
cwd: root,
|
|
641
|
+
programId,
|
|
642
|
+
config,
|
|
643
|
+
log,
|
|
644
|
+
...(agentRunner === undefined ? {} : { agentRunner }),
|
|
645
|
+
});
|
|
646
|
+
log(`pre-publish as-built sync: as-built refresh ${result.outcome.status}` +
|
|
647
|
+
(result.commit ? ` (${result.commit})` : ""));
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
log(`pre-publish as-built sync: could not refresh \`${AS_BUILT_PATH}\` after ` +
|
|
651
|
+
`merging \`${base}\` — ${error.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function resolvePublishConfig(root, provided, log) {
|
|
655
|
+
if (provided !== undefined)
|
|
656
|
+
return provided;
|
|
657
|
+
try {
|
|
658
|
+
return (await loadConfig(root)).config;
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
log("pre-publish as-built sync: could not load nightshift.config.json, " +
|
|
662
|
+
`skipping — ${error.message}`);
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
494
666
|
// ---- publish ------------------------------------------------------------
|
|
495
667
|
export async function publish(options) {
|
|
496
668
|
const root = resolve(options.cwd);
|
|
@@ -526,6 +698,19 @@ export async function publish(options) {
|
|
|
526
698
|
throw new Error(`Program ${options.programId} has no commits on \`${branch}\` beyond ` +
|
|
527
699
|
`\`${base}\`; there is nothing to publish.`);
|
|
528
700
|
}
|
|
701
|
+
const config = await resolvePublishConfig(root, options.config, log);
|
|
702
|
+
if (config !== undefined) {
|
|
703
|
+
await syncAsBuiltWithBase({
|
|
704
|
+
root,
|
|
705
|
+
git,
|
|
706
|
+
base,
|
|
707
|
+
branch,
|
|
708
|
+
programId: options.programId,
|
|
709
|
+
config,
|
|
710
|
+
agentRunner: options.agentRunner,
|
|
711
|
+
log,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
529
714
|
await git.push(root, "origin", branch);
|
|
530
715
|
log(`pushed ${branch} to origin`);
|
|
531
716
|
const chunks = splitReportIntoChunks(report);
|