@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.
Files changed (48) hide show
  1. package/dist/author.d.ts.map +1 -1
  2. package/dist/author.js +43 -0
  3. package/dist/author.js.map +1 -1
  4. package/dist/cli.js +86 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commit-plan.d.ts +40 -0
  7. package/dist/commit-plan.d.ts.map +1 -0
  8. package/dist/commit-plan.js +154 -0
  9. package/dist/commit-plan.js.map +1 -0
  10. package/dist/decision-ledger.d.ts +10 -0
  11. package/dist/decision-ledger.d.ts.map +1 -1
  12. package/dist/decision-ledger.js +7 -0
  13. package/dist/decision-ledger.js.map +1 -1
  14. package/dist/exit-codes.d.ts +16 -2
  15. package/dist/exit-codes.d.ts.map +1 -1
  16. package/dist/exit-codes.js +17 -2
  17. package/dist/exit-codes.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/install-skills.d.ts +1 -1
  23. package/dist/install-skills.d.ts.map +1 -1
  24. package/dist/install-skills.js +5 -1
  25. package/dist/install-skills.js.map +1 -1
  26. package/dist/manifest.d.ts +47 -13
  27. package/dist/manifest.d.ts.map +1 -1
  28. package/dist/manifest.js +80 -11
  29. package/dist/manifest.js.map +1 -1
  30. package/dist/preflight.d.ts +73 -0
  31. package/dist/preflight.d.ts.map +1 -0
  32. package/dist/preflight.js +225 -0
  33. package/dist/preflight.js.map +1 -0
  34. package/dist/publish.d.ts +36 -0
  35. package/dist/publish.d.ts.map +1 -1
  36. package/dist/publish.js +185 -0
  37. package/dist/publish.js.map +1 -1
  38. package/dist/run-program.d.ts +35 -1
  39. package/dist/run-program.d.ts.map +1 -1
  40. package/dist/run-program.js +224 -10
  41. package/dist/run-program.js.map +1 -1
  42. package/dist/skill-roots.d.ts +11 -3
  43. package/dist/skill-roots.d.ts.map +1 -1
  44. package/dist/skill-roots.js +59 -12
  45. package/dist/skill-roots.js.map +1 -1
  46. package/package.json +2 -2
  47. package/skills/backlog-capture/SKILL.md +182 -0
  48. 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 {
@@ -1 +1 @@
1
- {"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../src/publish.ts"],"names":[],"mappings":"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"}
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);