@ashlr/hub 3.5.1 → 3.8.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/CHANGELOG.md +165 -0
- package/README.md +3 -0
- package/dist/build-identity.json +2 -2
- package/dist/cli/activation.d.ts +41 -0
- package/dist/cli/activation.js +485 -0
- package/dist/cli/activation.js.map +1 -0
- package/dist/core/daemon/cutoff-checkpoint-scheduler.js +7 -0
- package/dist/core/daemon/cutoff-checkpoint-scheduler.js.map +1 -1
- package/dist/core/daemon/loop.d.ts +19 -0
- package/dist/core/daemon/loop.js +235 -5
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/overnight-status.d.ts +152 -0
- package/dist/core/daemon/overnight-status.js +275 -0
- package/dist/core/daemon/overnight-status.js.map +1 -0
- package/dist/core/daemon/post-merge-halt.d.ts +212 -0
- package/dist/core/daemon/post-merge-halt.js +503 -0
- package/dist/core/daemon/post-merge-halt.js.map +1 -0
- package/dist/core/daemon/run-window.d.ts +218 -0
- package/dist/core/daemon/run-window.js +420 -0
- package/dist/core/daemon/run-window.js.map +1 -0
- package/dist/core/fleet/generated-repair-resolution.d.ts +36 -0
- package/dist/core/fleet/generated-repair-resolution.js +245 -0
- package/dist/core/fleet/generated-repair-resolution.js.map +1 -0
- package/dist/core/local-runtime/llama/port.d.ts +17 -0
- package/dist/core/local-runtime/llama/port.js +47 -0
- package/dist/core/local-runtime/llama/port.js.map +1 -0
- package/dist/core/local-runtime/llama/proxy-host.d.ts +54 -0
- package/dist/core/local-runtime/llama/proxy-host.js +159 -0
- package/dist/core/local-runtime/llama/proxy-host.js.map +1 -0
- package/dist/core/local-runtime/llama/proxy-process.d.ts +148 -0
- package/dist/core/local-runtime/llama/proxy-process.js +250 -0
- package/dist/core/local-runtime/llama/proxy-process.js.map +1 -0
- package/dist/core/local-runtime/llama/proxy-record.d.ts +40 -0
- package/dist/core/local-runtime/llama/proxy-record.js +133 -0
- package/dist/core/local-runtime/llama/proxy-record.js.map +1 -0
- package/dist/core/local-runtime/llama/proxy-supervisor.d.ts +118 -0
- package/dist/core/local-runtime/llama/proxy-supervisor.js +511 -0
- package/dist/core/local-runtime/llama/proxy-supervisor.js.map +1 -0
- package/dist/core/policy/local-only.d.ts +142 -2
- package/dist/core/policy/local-only.js +204 -15
- package/dist/core/policy/local-only.js.map +1 -1
- package/dist/core/run/budget.d.ts +22 -8
- package/dist/core/run/budget.js +80 -12
- package/dist/core/run/budget.js.map +1 -1
- package/dist/core/run/sandboxed-engine.js +3 -3
- package/dist/core/run/sandboxed-engine.js.map +1 -1
- package/dist/core/universe/builtins/preparation/manifest.json +1 -1
- package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +61 -9
- package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +60 -8
- package/dist/core/verse/session-engine.d.ts +42 -1
- package/dist/core/verse/session-engine.js +79 -2
- package/dist/core/verse/session-engine.js.map +1 -1
- package/dist/core/web/public/next/assets/{App-BbVoUCIi.js → App-BsbJFA2j.js} +2 -2
- package/dist/core/web/public/next/assets/{App-BbVoUCIi.js.map → App-BsbJFA2j.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js → ApprovalsSection-2CCSoBTJ.js} +2 -2
- package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js.map → ApprovalsSection-2CCSoBTJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{AutonomySection-Cpk_oEhc.css → AutonomySection-CHjxJt2z.css} +1 -1
- package/dist/core/web/public/next/assets/AutonomySection-yeNNUPur.js +2 -0
- package/dist/core/web/public/next/assets/AutonomySection-yeNNUPur.js.map +1 -0
- package/dist/core/web/public/next/assets/ChatSection-BfnLe5dT.css +1 -0
- package/dist/core/web/public/next/assets/ChatSection-DwDDnCZQ.js +80 -0
- package/dist/core/web/public/next/assets/ChatSection-DwDDnCZQ.js.map +1 -0
- package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js → ConfirmDialog-C363ty_c.js} +2 -2
- package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js.map → ConfirmDialog-C363ty_c.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js → Dialog-nK15L1M0.js} +2 -2
- package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js.map → Dialog-nK15L1M0.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js → DiffViewer-D369eZe8.js} +2 -2
- package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js.map → DiffViewer-D369eZe8.js.map} +1 -1
- package/dist/core/web/public/next/assets/McpSection-D88QlDpG.css +1 -0
- package/dist/core/web/public/next/assets/McpSection-DVgiSYG1.js +2 -0
- package/dist/core/web/public/next/assets/McpSection-DVgiSYG1.js.map +1 -0
- package/dist/core/web/public/next/assets/Meter-CLcr-4wS.css +1 -0
- package/dist/core/web/public/next/assets/Meter-Cmo1pLbk.js +2 -0
- package/dist/core/web/public/next/assets/Meter-Cmo1pLbk.js.map +1 -0
- package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js → MutationTokenDialog-B9I4PAcp.js} +2 -2
- package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js.map → MutationTokenDialog-B9I4PAcp.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js → ResourcePoolConsoleApp-z6tJj3s9.js} +2 -2
- package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js.map → ResourcePoolConsoleApp-z6tJj3s9.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js → RouteErrorBoundary-OSajL45F.js} +2 -2
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js.map → RouteErrorBoundary-OSajL45F.js.map} +1 -1
- package/dist/core/web/public/next/assets/SettingsSection-B3YH2Npi.js +2 -0
- package/dist/core/web/public/next/assets/SettingsSection-B3YH2Npi.js.map +1 -0
- package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js → UniverseConsoleApp-wEKIzl1M.js} +2 -2
- package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js.map → UniverseConsoleApp-wEKIzl1M.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js → UniverseView-CQVpJqqI.js} +2 -2
- package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js.map → UniverseView-CQVpJqqI.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js → UsageSection-dToBThXe.js} +2 -2
- package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js.map → UsageSection-dToBThXe.js.map} +1 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-CMkCizvL.css +1 -0
- package/dist/core/web/public/next/assets/VerseConsoleApp-DHU32JCQ.js +3 -0
- package/dist/core/web/public/next/assets/VerseConsoleApp-DHU32JCQ.js.map +1 -0
- package/dist/core/web/public/next/assets/{charts-DSUpczxG.js → charts-BopViIha.js} +2 -2
- package/dist/core/web/public/next/assets/{charts-DSUpczxG.js.map → charts-BopViIha.js.map} +1 -1
- package/dist/core/web/public/next/assets/global-DMWakEhP.js +2 -0
- package/dist/core/web/public/next/assets/global-DMWakEhP.js.map +1 -0
- package/dist/core/web/public/next/assets/global-z-JfD3ow.css +1 -0
- package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js → icons-L06pQH6j.js} +2 -2
- package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js.map → icons-L06pQH6j.js.map} +1 -1
- package/dist/core/web/public/next/assets/{index-BMRIET7F.js → index-CXgVQHek.js} +3 -3
- package/dist/core/web/public/next/assets/{index-BMRIET7F.js.map → index-CXgVQHek.js.map} +1 -1
- package/dist/core/web/public/next/assets/{queries-AxnVa02h.js → queries-D3sSlan1.js} +2 -2
- package/dist/core/web/public/next/assets/{queries-AxnVa02h.js.map → queries-D3sSlan1.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js → use-guarded-action-BxfJ9E-n.js} +2 -2
- package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js.map → use-guarded-action-BxfJ9E-n.js.map} +1 -1
- package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js +2 -0
- package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js.map +1 -0
- package/dist/core/web/public/next/index.html +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/package.json +1 -1
- package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js +0 -2
- package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js.map +0 -1
- package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js +0 -80
- package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js.map +0 -1
- package/dist/core/web/public/next/assets/ChatSection-LQiAn6Eu.css +0 -1
- package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js +0 -2
- package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js.map +0 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js +0 -3
- package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js.map +0 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-D2E3mU9a.css +0 -1
- package/dist/core/web/public/next/assets/global-C4jfYHrj.js +0 -2
- package/dist/core/web/public/next/assets/global-C4jfYHrj.js.map +0 -1
- package/dist/core/web/public/next/assets/global-D463gG1X.css +0 -1
- package/dist/core/web/public/next/assets/workspace-model-cE4LkKpE.js +0 -2
- package/dist/core/web/public/next/assets/workspace-model-cE4LkKpE.js.map +0 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overnight-status.ts — the engine half of the `GET /api/verse/overnight`
|
|
3
|
+
* contract. The route (owned elsewhere, under src/web-ui / verse) reads this;
|
|
4
|
+
* this module never serves HTTP and never imports a web module.
|
|
5
|
+
*
|
|
6
|
+
* ── THE TRI-STATE RULE, WHICH IS THE WHOLE POINT ───────────────────────────
|
|
7
|
+
* Every field except `armed` is tri-state: `null` means "the engine did not
|
|
8
|
+
* say", and the client renders that differently from zero. So nothing here
|
|
9
|
+
* ever defaults an unknown to 0. `iterationsDone: null` is "no run has
|
|
10
|
+
* reported yet"; `iterationsDone: 0` is "a run is going and has finished no
|
|
11
|
+
* iteration". Those are different facts and the operator can tell them apart.
|
|
12
|
+
*
|
|
13
|
+
* ── WHERE `merged[]` COMES FROM ────────────────────────────────────────────
|
|
14
|
+
* From the repositories, not from a counter. `daemon/post-merge-halt` detects
|
|
15
|
+
* landings by comparing each ENROLLED repo's head across a tick, so every row
|
|
16
|
+
* carries a real commit OID and the real subject line. A count can drift from
|
|
17
|
+
* what actually landed; a commit OID cannot.
|
|
18
|
+
*
|
|
19
|
+
* ── WHERE `discarded[].reason` COMES FROM ──────────────────────────────────
|
|
20
|
+
* Whoever discarded it says why, in their own words, and this module passes
|
|
21
|
+
* the sentence through untouched. "post-merge suite failed on <repo>" and
|
|
22
|
+
* "typecheck failed" are answers; "rejected" is not. A quiet night has to be
|
|
23
|
+
* legible in the morning or the run window is not worth having.
|
|
24
|
+
*
|
|
25
|
+
* KNOWN GAP — `discarded[]` currently carries POST-merge halts only.
|
|
26
|
+
* Pre-merge discards already have specific per-proposal reasons: the pass
|
|
27
|
+
* returns them as `AutoMergePassResult.skipped[]`
|
|
28
|
+
* (`{ proposalId, check, reason }`, e.g. "verify-before-judge: known failed
|
|
29
|
+
* verification: typecheck") and as rejection details on `results[]`. They are
|
|
30
|
+
* lost on the way out: `autoMergeTickSummary` (daemon/loop.ts:1817) folds the
|
|
31
|
+
* pass result into `DaemonTick['autoMerge']` as COUNTS only, so by the time
|
|
32
|
+
* runDaemon can see it the reasons are gone.
|
|
33
|
+
*
|
|
34
|
+
* The fix, deliberately NOT made here because it edits a shared type while
|
|
35
|
+
* another agent is changing budget types in the same file:
|
|
36
|
+
* 1. add `skipped?: Array<{ proposalId: string; check: string; reason: string }>`
|
|
37
|
+
* to `DaemonTick['autoMerge']` (src/core/types.ts:4678-4692);
|
|
38
|
+
* 2. stop dropping it in `autoMergeTickSummary` (daemon/loop.ts:1817-1847),
|
|
39
|
+
* bounded to a sane number of entries so the tick record stays small;
|
|
40
|
+
* 3. in runDaemon's post-tick block, feed each entry to
|
|
41
|
+
* {@link recordOvernightDiscard} with `reason` = the pass's own sentence.
|
|
42
|
+
* That is the whole change; nothing else needs to move.
|
|
43
|
+
*
|
|
44
|
+
* `~/.ashlr/run-window/status.json`, written durably (exclusive tmp + atomic
|
|
45
|
+
* rename). Metadata only — never a diff, a token, or command output.
|
|
46
|
+
*
|
|
47
|
+
* No new runtime deps; node builtins only. Never throws out of a public API.
|
|
48
|
+
*/
|
|
49
|
+
import type { RunWindowStopRule } from './run-window.js';
|
|
50
|
+
import type { PostMergeGateResult } from './post-merge-halt.js';
|
|
51
|
+
export interface OvernightMergedItem {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly repo: string;
|
|
54
|
+
readonly title: string;
|
|
55
|
+
readonly at: string | null;
|
|
56
|
+
readonly commit: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface OvernightDiscardedItem {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly repo: string;
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly at: string | null;
|
|
63
|
+
/** A SPECIFIC sentence. Never a bare 'rejected'. */
|
|
64
|
+
readonly reason: string;
|
|
65
|
+
}
|
|
66
|
+
export interface OvernightRun {
|
|
67
|
+
readonly runId: string;
|
|
68
|
+
readonly startedAt: string | null;
|
|
69
|
+
readonly stopRule: RunWindowStopRule | null;
|
|
70
|
+
/** null means "did not say" — NOT zero. */
|
|
71
|
+
readonly iterationsDone: number | null;
|
|
72
|
+
readonly repo: string | null;
|
|
73
|
+
/** Free text in the engine's own words, rendered verbatim. */
|
|
74
|
+
readonly activity: string | null;
|
|
75
|
+
readonly merged: OvernightMergedItem[];
|
|
76
|
+
readonly discarded: OvernightDiscardedItem[];
|
|
77
|
+
}
|
|
78
|
+
export interface OvernightGate {
|
|
79
|
+
readonly tests: boolean;
|
|
80
|
+
readonly lint: boolean;
|
|
81
|
+
readonly typecheck: boolean;
|
|
82
|
+
readonly autoMerge: boolean | null;
|
|
83
|
+
readonly branch: string | null;
|
|
84
|
+
}
|
|
85
|
+
export interface OvernightStatus {
|
|
86
|
+
/** The one field that may never be absent. */
|
|
87
|
+
readonly armed: boolean;
|
|
88
|
+
readonly repos: number | null;
|
|
89
|
+
readonly gate: OvernightGate | null;
|
|
90
|
+
readonly run: OvernightRun | null;
|
|
91
|
+
}
|
|
92
|
+
export declare function overnightStatusDir(): string;
|
|
93
|
+
export declare function overnightStatusPath(): string;
|
|
94
|
+
/** The safe empty answer. Disarmed, and honest that it knows nothing else. */
|
|
95
|
+
export declare function emptyOvernightStatus(): OvernightStatus;
|
|
96
|
+
/**
|
|
97
|
+
* Read the current status. A missing or unreadable file answers
|
|
98
|
+
* {@link emptyOvernightStatus} — DISARMED. That is the fail-safe direction:
|
|
99
|
+
* an engine that cannot prove it armed a run must never claim it did.
|
|
100
|
+
*/
|
|
101
|
+
export declare function readOvernightStatus(): OvernightStatus;
|
|
102
|
+
/** Merge a partial update into the stored status. Never throws. */
|
|
103
|
+
export declare function updateOvernightStatus(patch: Partial<OvernightStatus>): OvernightStatus;
|
|
104
|
+
/**
|
|
105
|
+
* Arm a run. `disarm` stops the NEXT run, not this one — halting a LIVE run is
|
|
106
|
+
* the existing pause (`POST /api/verse/daemon {action:'pause'}`), and this
|
|
107
|
+
* module deliberately offers no second way to do it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function armOvernightRun(stopRule: RunWindowStopRule, opts?: {
|
|
110
|
+
repos?: number | null;
|
|
111
|
+
gate?: OvernightGate | null;
|
|
112
|
+
runId?: string;
|
|
113
|
+
now?: () => number;
|
|
114
|
+
}): OvernightStatus;
|
|
115
|
+
/** Disarm the NEXT run. Leaves the last run's record intact for the morning. */
|
|
116
|
+
export declare function disarmOvernightRun(): OvernightStatus;
|
|
117
|
+
/**
|
|
118
|
+
* Recover the proposal id a landed commit came from, so `merged[].id` is the
|
|
119
|
+
* id the inbox knows rather than an invented one. Returns null when the
|
|
120
|
+
* subject is not one the merge path writes — a hand-made commit is reported
|
|
121
|
+
* honestly as having no proposal id, never given a fabricated one.
|
|
122
|
+
*/
|
|
123
|
+
export declare function proposalIdFromCommitSubject(subject: string): string | null;
|
|
124
|
+
/**
|
|
125
|
+
* Fold one iteration's post-merge gate result into the live run record.
|
|
126
|
+
*
|
|
127
|
+
* A CLEAN landing appends to `merged[]`; a HALT appends to `discarded[]` with
|
|
128
|
+
* the gate's own specific sentence. Both come from repository truth.
|
|
129
|
+
*/
|
|
130
|
+
export declare function recordOvernightIteration(result: PostMergeGateResult, opts?: {
|
|
131
|
+
iterationsDone?: number;
|
|
132
|
+
activity?: string | null;
|
|
133
|
+
now?: () => number;
|
|
134
|
+
}): OvernightStatus;
|
|
135
|
+
/** Append a discard the engine decided outside the post-merge gate. */
|
|
136
|
+
export declare function recordOvernightDiscard(item: Omit<OvernightDiscardedItem, 'at'> & {
|
|
137
|
+
at?: string | null;
|
|
138
|
+
}, opts?: {
|
|
139
|
+
now?: () => number;
|
|
140
|
+
}): OvernightStatus;
|
|
141
|
+
/** Update the live "what is it doing right now" fields. Cheap; called per tick. */
|
|
142
|
+
export declare function recordOvernightActivity(patch: {
|
|
143
|
+
iterationsDone?: number | null;
|
|
144
|
+
repo?: string | null;
|
|
145
|
+
activity?: string | null;
|
|
146
|
+
}): OvernightStatus;
|
|
147
|
+
/**
|
|
148
|
+
* Mark the run concluded. The record is KEPT, not cleared: the morning's first
|
|
149
|
+
* question is "what happened last night", and a status that erases itself on
|
|
150
|
+
* exit cannot answer it.
|
|
151
|
+
*/
|
|
152
|
+
export declare function concludeOvernightRun(summary: string): OvernightStatus;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overnight-status.ts — the engine half of the `GET /api/verse/overnight`
|
|
3
|
+
* contract. The route (owned elsewhere, under src/web-ui / verse) reads this;
|
|
4
|
+
* this module never serves HTTP and never imports a web module.
|
|
5
|
+
*
|
|
6
|
+
* ── THE TRI-STATE RULE, WHICH IS THE WHOLE POINT ───────────────────────────
|
|
7
|
+
* Every field except `armed` is tri-state: `null` means "the engine did not
|
|
8
|
+
* say", and the client renders that differently from zero. So nothing here
|
|
9
|
+
* ever defaults an unknown to 0. `iterationsDone: null` is "no run has
|
|
10
|
+
* reported yet"; `iterationsDone: 0` is "a run is going and has finished no
|
|
11
|
+
* iteration". Those are different facts and the operator can tell them apart.
|
|
12
|
+
*
|
|
13
|
+
* ── WHERE `merged[]` COMES FROM ────────────────────────────────────────────
|
|
14
|
+
* From the repositories, not from a counter. `daemon/post-merge-halt` detects
|
|
15
|
+
* landings by comparing each ENROLLED repo's head across a tick, so every row
|
|
16
|
+
* carries a real commit OID and the real subject line. A count can drift from
|
|
17
|
+
* what actually landed; a commit OID cannot.
|
|
18
|
+
*
|
|
19
|
+
* ── WHERE `discarded[].reason` COMES FROM ──────────────────────────────────
|
|
20
|
+
* Whoever discarded it says why, in their own words, and this module passes
|
|
21
|
+
* the sentence through untouched. "post-merge suite failed on <repo>" and
|
|
22
|
+
* "typecheck failed" are answers; "rejected" is not. A quiet night has to be
|
|
23
|
+
* legible in the morning or the run window is not worth having.
|
|
24
|
+
*
|
|
25
|
+
* KNOWN GAP — `discarded[]` currently carries POST-merge halts only.
|
|
26
|
+
* Pre-merge discards already have specific per-proposal reasons: the pass
|
|
27
|
+
* returns them as `AutoMergePassResult.skipped[]`
|
|
28
|
+
* (`{ proposalId, check, reason }`, e.g. "verify-before-judge: known failed
|
|
29
|
+
* verification: typecheck") and as rejection details on `results[]`. They are
|
|
30
|
+
* lost on the way out: `autoMergeTickSummary` (daemon/loop.ts:1817) folds the
|
|
31
|
+
* pass result into `DaemonTick['autoMerge']` as COUNTS only, so by the time
|
|
32
|
+
* runDaemon can see it the reasons are gone.
|
|
33
|
+
*
|
|
34
|
+
* The fix, deliberately NOT made here because it edits a shared type while
|
|
35
|
+
* another agent is changing budget types in the same file:
|
|
36
|
+
* 1. add `skipped?: Array<{ proposalId: string; check: string; reason: string }>`
|
|
37
|
+
* to `DaemonTick['autoMerge']` (src/core/types.ts:4678-4692);
|
|
38
|
+
* 2. stop dropping it in `autoMergeTickSummary` (daemon/loop.ts:1817-1847),
|
|
39
|
+
* bounded to a sane number of entries so the tick record stays small;
|
|
40
|
+
* 3. in runDaemon's post-tick block, feed each entry to
|
|
41
|
+
* {@link recordOvernightDiscard} with `reason` = the pass's own sentence.
|
|
42
|
+
* That is the whole change; nothing else needs to move.
|
|
43
|
+
*
|
|
44
|
+
* `~/.ashlr/run-window/status.json`, written durably (exclusive tmp + atomic
|
|
45
|
+
* rename). Metadata only — never a diff, a token, or command output.
|
|
46
|
+
*
|
|
47
|
+
* No new runtime deps; node builtins only. Never throws out of a public API.
|
|
48
|
+
*/
|
|
49
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
50
|
+
import { homedir } from 'node:os';
|
|
51
|
+
import { join } from 'node:path';
|
|
52
|
+
import { randomUUID } from 'node:crypto';
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Storage
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
export function overnightStatusDir() {
|
|
57
|
+
return join(homedir(), '.ashlr', 'run-window');
|
|
58
|
+
}
|
|
59
|
+
export function overnightStatusPath() {
|
|
60
|
+
return join(overnightStatusDir(), 'status.json');
|
|
61
|
+
}
|
|
62
|
+
/** The safe empty answer. Disarmed, and honest that it knows nothing else. */
|
|
63
|
+
export function emptyOvernightStatus() {
|
|
64
|
+
return { armed: false, repos: null, gate: null, run: null };
|
|
65
|
+
}
|
|
66
|
+
function writeStatus(status) {
|
|
67
|
+
try {
|
|
68
|
+
const dir = overnightStatusDir();
|
|
69
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
const path = overnightStatusPath();
|
|
71
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
72
|
+
try {
|
|
73
|
+
writeFileSync(tmp, JSON.stringify(status, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
74
|
+
renameSync(tmp, path);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
try {
|
|
78
|
+
unlinkSync(tmp);
|
|
79
|
+
}
|
|
80
|
+
catch { /* the rename already consumed it */ }
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read the current status. A missing or unreadable file answers
|
|
91
|
+
* {@link emptyOvernightStatus} — DISARMED. That is the fail-safe direction:
|
|
92
|
+
* an engine that cannot prove it armed a run must never claim it did.
|
|
93
|
+
*/
|
|
94
|
+
export function readOvernightStatus() {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(overnightStatusPath(), 'utf8'));
|
|
97
|
+
if (parsed?.recordType !== 'daemon-overnight-status')
|
|
98
|
+
return emptyOvernightStatus();
|
|
99
|
+
return {
|
|
100
|
+
armed: parsed.armed === true,
|
|
101
|
+
repos: typeof parsed.repos === 'number' ? parsed.repos : null,
|
|
102
|
+
gate: parsed.gate ?? null,
|
|
103
|
+
run: parsed.run ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return emptyOvernightStatus();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Merge a partial update into the stored status. Never throws. */
|
|
111
|
+
export function updateOvernightStatus(patch) {
|
|
112
|
+
const current = readOvernightStatus();
|
|
113
|
+
const next = {
|
|
114
|
+
armed: patch.armed ?? current.armed,
|
|
115
|
+
repos: patch.repos !== undefined ? patch.repos : current.repos,
|
|
116
|
+
gate: patch.gate !== undefined ? patch.gate : current.gate,
|
|
117
|
+
run: patch.run !== undefined ? patch.run : current.run,
|
|
118
|
+
};
|
|
119
|
+
writeStatus({ recordType: 'daemon-overnight-status', ...next });
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Arm a run. `disarm` stops the NEXT run, not this one — halting a LIVE run is
|
|
124
|
+
* the existing pause (`POST /api/verse/daemon {action:'pause'}`), and this
|
|
125
|
+
* module deliberately offers no second way to do it.
|
|
126
|
+
*/
|
|
127
|
+
export function armOvernightRun(stopRule, opts = {}) {
|
|
128
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
129
|
+
return updateOvernightStatus({
|
|
130
|
+
armed: true,
|
|
131
|
+
repos: opts.repos ?? null,
|
|
132
|
+
gate: opts.gate ?? null,
|
|
133
|
+
run: {
|
|
134
|
+
runId: opts.runId ?? randomUUID(),
|
|
135
|
+
startedAt: new Date(nowMs).toISOString(),
|
|
136
|
+
stopRule,
|
|
137
|
+
// A freshly armed run HAS done zero iterations — that is a fact it can
|
|
138
|
+
// state, so it is 0 and not null.
|
|
139
|
+
iterationsDone: 0,
|
|
140
|
+
repo: null,
|
|
141
|
+
activity: 'armed; waiting for the first tick',
|
|
142
|
+
merged: [],
|
|
143
|
+
discarded: [],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Disarm the NEXT run. Leaves the last run's record intact for the morning. */
|
|
148
|
+
export function disarmOvernightRun() {
|
|
149
|
+
return updateOvernightStatus({ armed: false });
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Recording progress
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/** `ashlr: auto-merge proposal <id>` / `ashlr: merge proposal branch ashlr/merge/<id>`. */
|
|
155
|
+
const PROPOSAL_ID_PATTERNS = [
|
|
156
|
+
/^ashlr:\s*auto-merge proposal\s+(\S+)/,
|
|
157
|
+
/^ashlr:\s*merge proposal branch\s+ashlr\/merge\/(\S+)/,
|
|
158
|
+
];
|
|
159
|
+
/**
|
|
160
|
+
* Recover the proposal id a landed commit came from, so `merged[].id` is the
|
|
161
|
+
* id the inbox knows rather than an invented one. Returns null when the
|
|
162
|
+
* subject is not one the merge path writes — a hand-made commit is reported
|
|
163
|
+
* honestly as having no proposal id, never given a fabricated one.
|
|
164
|
+
*/
|
|
165
|
+
export function proposalIdFromCommitSubject(subject) {
|
|
166
|
+
for (const pattern of PROPOSAL_ID_PATTERNS) {
|
|
167
|
+
const match = pattern.exec(subject ?? '');
|
|
168
|
+
if (match?.[1])
|
|
169
|
+
return match[1];
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Fold one iteration's post-merge gate result into the live run record.
|
|
175
|
+
*
|
|
176
|
+
* A CLEAN landing appends to `merged[]`; a HALT appends to `discarded[]` with
|
|
177
|
+
* the gate's own specific sentence. Both come from repository truth.
|
|
178
|
+
*/
|
|
179
|
+
export function recordOvernightIteration(result, opts = {}) {
|
|
180
|
+
const current = readOvernightStatus();
|
|
181
|
+
if (!current.run)
|
|
182
|
+
return current;
|
|
183
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
184
|
+
const at = new Date(nowMs).toISOString();
|
|
185
|
+
const merged = [...current.run.merged];
|
|
186
|
+
const discarded = [...current.run.discarded];
|
|
187
|
+
if (!result.halt) {
|
|
188
|
+
for (const landing of result.landings) {
|
|
189
|
+
for (const commit of landing.commits) {
|
|
190
|
+
merged.push({
|
|
191
|
+
id: proposalIdFromCommitSubject(commit.subject) ?? commit.sha.slice(0, 12),
|
|
192
|
+
repo: landing.repo,
|
|
193
|
+
title: commit.subject,
|
|
194
|
+
at,
|
|
195
|
+
commit: commit.sha,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// A halted landing is not a merge that stuck — it is work the operator
|
|
202
|
+
// must back out, and it says exactly why and exactly how.
|
|
203
|
+
for (const landing of result.landings) {
|
|
204
|
+
const why = result.failures
|
|
205
|
+
.filter((f) => f.repo === landing.repo)
|
|
206
|
+
.map((f) => `${f.kind} ${f.detail}`)
|
|
207
|
+
.join('; ');
|
|
208
|
+
discarded.push({
|
|
209
|
+
id: landing.commits[0]
|
|
210
|
+
? proposalIdFromCommitSubject(landing.commits[0].subject) ?? landing.commits[0].sha.slice(0, 12)
|
|
211
|
+
: landing.afterHead.slice(0, 12),
|
|
212
|
+
repo: landing.repo,
|
|
213
|
+
title: landing.commits[0]?.subject ?? `landing ${landing.beforeHead.slice(0, 8)}..${landing.afterHead.slice(0, 8)}`,
|
|
214
|
+
at,
|
|
215
|
+
reason: result.verdict === 'regressed'
|
|
216
|
+
? `post-merge suite failed on ${landing.repo}: ${why || 'a required check went red after the merge'}. ` +
|
|
217
|
+
`Back it out with: ${landing.revertCommand}`
|
|
218
|
+
: `the merge on ${landing.repo} could not be verified: ${why || 'no required verify command could be run'}. ` +
|
|
219
|
+
`Back it out with: ${landing.revertCommand}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return updateOvernightStatus({
|
|
224
|
+
run: {
|
|
225
|
+
...current.run,
|
|
226
|
+
iterationsDone: opts.iterationsDone ?? current.run.iterationsDone,
|
|
227
|
+
repo: result.landings[0]?.repo ?? current.run.repo,
|
|
228
|
+
activity: opts.activity !== undefined ? opts.activity : current.run.activity,
|
|
229
|
+
merged,
|
|
230
|
+
discarded,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/** Append a discard the engine decided outside the post-merge gate. */
|
|
235
|
+
export function recordOvernightDiscard(item, opts = {}) {
|
|
236
|
+
const current = readOvernightStatus();
|
|
237
|
+
if (!current.run)
|
|
238
|
+
return current;
|
|
239
|
+
return updateOvernightStatus({
|
|
240
|
+
run: {
|
|
241
|
+
...current.run,
|
|
242
|
+
discarded: [...current.run.discarded, {
|
|
243
|
+
...item,
|
|
244
|
+
at: item.at ?? new Date((opts.now ?? Date.now)()).toISOString(),
|
|
245
|
+
}],
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Update the live "what is it doing right now" fields. Cheap; called per tick. */
|
|
250
|
+
export function recordOvernightActivity(patch) {
|
|
251
|
+
const current = readOvernightStatus();
|
|
252
|
+
if (!current.run)
|
|
253
|
+
return current;
|
|
254
|
+
return updateOvernightStatus({
|
|
255
|
+
run: {
|
|
256
|
+
...current.run,
|
|
257
|
+
iterationsDone: patch.iterationsDone !== undefined ? patch.iterationsDone : current.run.iterationsDone,
|
|
258
|
+
repo: patch.repo !== undefined ? patch.repo : current.run.repo,
|
|
259
|
+
activity: patch.activity !== undefined ? patch.activity : current.run.activity,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Mark the run concluded. The record is KEPT, not cleared: the morning's first
|
|
265
|
+
* question is "what happened last night", and a status that erases itself on
|
|
266
|
+
* exit cannot answer it.
|
|
267
|
+
*/
|
|
268
|
+
export function concludeOvernightRun(summary) {
|
|
269
|
+
const current = readOvernightStatus();
|
|
270
|
+
return updateOvernightStatus({
|
|
271
|
+
armed: false,
|
|
272
|
+
run: current.run ? { ...current.run, activity: summary } : null,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=overnight-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overnight-status.js","sourceRoot":"","sources":["../../../src/core/daemon/overnight-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* post-merge-halt.ts — the ONE gate that makes an unattended night survivable.
|
|
3
|
+
*
|
|
4
|
+
* ── WHAT WAS ALREADY THERE, AND WHAT WAS NOT ───────────────────────────────
|
|
5
|
+
* The PRE-merge gate is genuinely solid and this module does not duplicate a
|
|
6
|
+
* line of it. `fleet/automerge-pass` → `inbox/merge.autoMergeProposal` already
|
|
7
|
+
* runs the repo's OWN typecheck / tests / lint in a throwaway worktree branched
|
|
8
|
+
* off the default-branch head (`inbox/merge.verifyProposal`), with the verify
|
|
9
|
+
* commands detected from the BASE tree so a diff cannot rewrite which commands
|
|
10
|
+
* run; refuses any diff touching package.json / lockfiles / CI / Dockerfile /
|
|
11
|
+
* Makefile outright; caps scope (4 files / 150 lines) and risk; requires an
|
|
12
|
+
* independent frontier judge with an HMAC-signed attestation; calls
|
|
13
|
+
* `assertMayMutate` so only `listEnrolled()` repos are ever touched; and
|
|
14
|
+
* re-checks the kill switch at every write boundary. A proposal that cannot be
|
|
15
|
+
* verified is left PENDING or rejected — never merged — and every rejection is
|
|
16
|
+
* audited (`inbox:proposal-rejected`) with a decisions-ledger row. That is the
|
|
17
|
+
* pre-merge half of the contract, and it holds.
|
|
18
|
+
*
|
|
19
|
+
* What did NOT exist anywhere is the half that matters at 4am: NOTHING re-ran
|
|
20
|
+
* the suite AFTER a merge landed, and nothing could halt the run when it went
|
|
21
|
+
* red. The only post-merge machinery is observation-only by construction —
|
|
22
|
+
* `fleet/automerge-canary-observer` declares `authority: 'observation-only',
|
|
23
|
+
* enforceSupported: false` and merely writes an audit row, and the M189
|
|
24
|
+
* regression sentinel is default-OFF, needs two consecutive RED observations,
|
|
25
|
+
* and is explicitly "proposal-only — NEVER applies, merges, or pushes".
|
|
26
|
+
*
|
|
27
|
+
* So a regression merged at 01:00 was invisible to the gate, and iteration N+1
|
|
28
|
+
* was then built on top of it, and N+2 on top of that. By morning the bad
|
|
29
|
+
* commit is load-bearing and backing it out is archaeology. THIS module makes
|
|
30
|
+
* that impossible: the first red suite after a merge ENDS THE RUN. A bad night
|
|
31
|
+
* costs exactly one revert, which this module names precisely, instead of a
|
|
32
|
+
* tangled morning.
|
|
33
|
+
*
|
|
34
|
+
* ── HOW IT KNOWS A MERGE LANDED ────────────────────────────────────────────
|
|
35
|
+
* By reading the repos, not by trusting a return value. It snapshots each
|
|
36
|
+
* enrolled repo's HEAD before the tick and compares after. Anything that moved
|
|
37
|
+
* a default branch — `automerge-pass`, a future path, anything at all — is
|
|
38
|
+
* caught, because the evidence is the repository itself. A result object can be
|
|
39
|
+
* wrong about what it did; `git rev-parse HEAD` cannot.
|
|
40
|
+
*
|
|
41
|
+
* ── FAIL SAFE ──────────────────────────────────────────────────────────────
|
|
42
|
+
* A landing that cannot be verified HALTS. Not "continue and hope": if the
|
|
43
|
+
* repo declares no required verify command, or the command cannot be run, or
|
|
44
|
+
* git cannot be read, or THE WORKING TREE IS DIRTY, the run ends and says why.
|
|
45
|
+
* That is the same direction the pre-merge gate already fails
|
|
46
|
+
* (`allowWithoutVerification` defaults false) and the same direction
|
|
47
|
+
* `daemonPaused()` projects an unreadable sentinel.
|
|
48
|
+
*
|
|
49
|
+
* Those cases are reported as UNPROVABLE, never as a regression, and the
|
|
50
|
+
* distinction is deliberate. Not being overly critical governs how harshly a
|
|
51
|
+
* CHANGE is judged; it says nothing about whether the EVIDENCE is sound. A
|
|
52
|
+
* dirty tree means the post-merge run measured the operator's uncommitted
|
|
53
|
+
* edits instead of the merge — the gate is not being strict there, it is
|
|
54
|
+
* being honest that it proved nothing. That case matters more than the rest,
|
|
55
|
+
* because it is the only one where the gate would otherwise say GREEN and be
|
|
56
|
+
* wrong, and a false green is what the whole run window exists to prevent.
|
|
57
|
+
*
|
|
58
|
+
* ── FOLLOW-UP: THE MERGE COMMIT MESSAGE ────────────────────────────────────
|
|
59
|
+
* Each merge is already exactly one revertable commit, but its message names
|
|
60
|
+
* only the proposal: `ashlr: merge proposal branch ashlr/merge/<id>`
|
|
61
|
+
* (inbox/merge.ts:2843-2854) over `ashlr: auto-merge proposal <id>`
|
|
62
|
+
* (inbox/merge.ts:2690). It says WHAT, not WHY. Carrying the why into the
|
|
63
|
+
* commit means editing inbox/merge.ts, which 14 automerge suites pin; until
|
|
64
|
+
* that is worth doing, the why lives in the overnight status record
|
|
65
|
+
* (daemon/overnight-status.ts), keyed by the same proposal id, and in the
|
|
66
|
+
* halt record written below.
|
|
67
|
+
*
|
|
68
|
+
* ── HOW IT HALTS ───────────────────────────────────────────────────────────
|
|
69
|
+
* By PARKING, never by killing. `stopDaemon()` is `setKill(true)`, which writes
|
|
70
|
+
* the global `~/.ashlr/KILL` that `assertMayMutate` also reads — it would
|
|
71
|
+
* disable the operator's own MCP write tools on arrival. This module's halt is
|
|
72
|
+
* `pauseDaemon()` (`~/.ashlr/daemon.paused`, see daemon/pause.ts), which stops
|
|
73
|
+
* autonomous dispatch and nothing else, and which one click reverses.
|
|
74
|
+
*
|
|
75
|
+
* No new runtime deps; node builtins only. Never throws out of a public API.
|
|
76
|
+
*/
|
|
77
|
+
import type { AshlrConfig } from '../types.js';
|
|
78
|
+
import { detectVerifyCommands, runVerifyCommandAsync, type VerifyCommand } from '../run/verify-commands.js';
|
|
79
|
+
/** One enrolled repo's default-branch tip at a point in time. */
|
|
80
|
+
export interface RepoHeadSnapshot {
|
|
81
|
+
readonly repo: string;
|
|
82
|
+
/** 40-char OID, or null when the repo could not be read at all. */
|
|
83
|
+
readonly head: string | null;
|
|
84
|
+
/** Set when `head` is null, so an unreadable repo is never silently skipped. */
|
|
85
|
+
readonly unreadable?: string;
|
|
86
|
+
}
|
|
87
|
+
/** One commit that appeared on an enrolled repo between two snapshots. */
|
|
88
|
+
export interface LandedCommit {
|
|
89
|
+
readonly sha: string;
|
|
90
|
+
readonly subject: string;
|
|
91
|
+
/** True for a merge commit (2+ parents) — it reverts with `-m 1`. */
|
|
92
|
+
readonly isMerge: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** A repo whose head moved while the daemon was ticking. */
|
|
95
|
+
export interface PostMergeLanding {
|
|
96
|
+
readonly repo: string;
|
|
97
|
+
readonly beforeHead: string;
|
|
98
|
+
readonly afterHead: string;
|
|
99
|
+
readonly commits: LandedCommit[];
|
|
100
|
+
/** Exactly the command that backs this landing out. */
|
|
101
|
+
readonly revertCommand: string;
|
|
102
|
+
}
|
|
103
|
+
export type PostMergeVerdict =
|
|
104
|
+
/** No enrolled repo moved. The overwhelmingly common case; costs one git call per repo. */
|
|
105
|
+
'no-landing'
|
|
106
|
+
/** Something landed and the repo's own required suite is green on it. */
|
|
107
|
+
| 'clean'
|
|
108
|
+
/** Something landed and the repo's own required suite is RED on it. HALT. */
|
|
109
|
+
| 'regressed'
|
|
110
|
+
/** Something landed and could not be verified at all. HALT. */
|
|
111
|
+
| 'unverifiable';
|
|
112
|
+
export interface PostMergeFailure {
|
|
113
|
+
readonly repo: string;
|
|
114
|
+
/**
|
|
115
|
+
* A VerifyCommand kind means the check RAN and came back red — a genuine
|
|
116
|
+
* regression. 'detection' / 'harness' / 'git' mean the check could not be
|
|
117
|
+
* run or read at all, which is UNVERIFIABLE, not a regression. Both halt;
|
|
118
|
+
* the distinction is what the operator reads at 07:00 to know whether they
|
|
119
|
+
* are looking at a bad diff or a broken toolchain.
|
|
120
|
+
*/
|
|
121
|
+
readonly kind: VerifyCommand['kind'] | 'detection' | 'harness' | 'git' | 'dirty-tree';
|
|
122
|
+
readonly command: string;
|
|
123
|
+
readonly detail: string;
|
|
124
|
+
}
|
|
125
|
+
export interface PostMergeGateResult {
|
|
126
|
+
readonly verdict: PostMergeVerdict;
|
|
127
|
+
/** True exactly when the run must end. Never true for 'clean'/'no-landing'. */
|
|
128
|
+
readonly halt: boolean;
|
|
129
|
+
readonly landings: PostMergeLanding[];
|
|
130
|
+
readonly failures: PostMergeFailure[];
|
|
131
|
+
/** Commands actually executed, for the audit line. */
|
|
132
|
+
readonly ranCommands: number;
|
|
133
|
+
/** One line, safe for an audit summary. Metadata only, never command output. */
|
|
134
|
+
readonly detail: string;
|
|
135
|
+
/** Every revert needed to undo this iteration, in the order to run them. */
|
|
136
|
+
readonly revertPlan: string[];
|
|
137
|
+
readonly durationMs: number;
|
|
138
|
+
}
|
|
139
|
+
/** A halt, written durably so the morning does not begin with guesswork. */
|
|
140
|
+
export interface PostMergeHaltRecord {
|
|
141
|
+
readonly recordType: 'daemon-post-merge-halt';
|
|
142
|
+
readonly haltedAt: string;
|
|
143
|
+
readonly verdict: Exclude<PostMergeVerdict, 'clean' | 'no-landing'>;
|
|
144
|
+
readonly detail: string;
|
|
145
|
+
readonly landings: PostMergeLanding[];
|
|
146
|
+
readonly failures: PostMergeFailure[];
|
|
147
|
+
readonly revertPlan: string[];
|
|
148
|
+
}
|
|
149
|
+
export interface PostMergeGateSeams {
|
|
150
|
+
/** Enrolled repos. Defaults to `listEnrolled()` and is NEVER widened. */
|
|
151
|
+
readonly listEnrolledRepos?: () => string[];
|
|
152
|
+
readonly detect?: typeof detectVerifyCommands;
|
|
153
|
+
readonly run?: typeof runVerifyCommandAsync;
|
|
154
|
+
readonly now?: () => number;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read every ENROLLED repo's HEAD. Enrollment is the only source of repos —
|
|
158
|
+
* a repo that is not enrolled is never read, never verified, and can never
|
|
159
|
+
* appear in a landing or a halt.
|
|
160
|
+
*
|
|
161
|
+
* Never throws; an unreadable repo is recorded as such, not dropped.
|
|
162
|
+
*/
|
|
163
|
+
export declare function snapshotEnrolledHeads(seams?: PostMergeGateSeams): RepoHeadSnapshot[];
|
|
164
|
+
/**
|
|
165
|
+
* Which enrolled repos moved between two snapshots.
|
|
166
|
+
*
|
|
167
|
+
* A repo present in `before` but unreadable in `after` (or vice versa) is NOT
|
|
168
|
+
* treated as "nothing happened" — it is reported as a landing with an empty
|
|
169
|
+
* commit list so the caller fails safe rather than assuming quiet.
|
|
170
|
+
*/
|
|
171
|
+
export declare function detectLandings(before: RepoHeadSnapshot[], after: RepoHeadSnapshot[]): {
|
|
172
|
+
landings: PostMergeLanding[];
|
|
173
|
+
unreadable: PostMergeFailure[];
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* The exact command that backs a landing out.
|
|
177
|
+
*
|
|
178
|
+
* A single merge commit — which is what `inbox/merge.mergeLocally` produces,
|
|
179
|
+
* a two-parent `commit-tree` against the verified base — reverts with
|
|
180
|
+
* `git revert -m 1 <oid>`. Several commits revert newest-first. A landing whose
|
|
181
|
+
* commits could not be listed falls back to resetting the branch to the exact
|
|
182
|
+
* OID it had before the iteration, which is always correct for a local-only
|
|
183
|
+
* merge that was never pushed (and `inbox/merge` never pushes: host auto-merge
|
|
184
|
+
* is disabled and returns a handoff instead).
|
|
185
|
+
*/
|
|
186
|
+
export declare function revertCommandFor(beforeHead: string, afterHead: string, commits: LandedCommit[]): string;
|
|
187
|
+
/**
|
|
188
|
+
* Re-run each landed-on repo's OWN required verify commands against the MERGED
|
|
189
|
+
* head, and decide whether the run may continue.
|
|
190
|
+
*
|
|
191
|
+
* Fast in the common case: when no enrolled repo moved, this is one
|
|
192
|
+
* `git rev-parse` per repo and returns `no-landing` without running anything.
|
|
193
|
+
* The expensive path only runs when something actually merged, which is exactly
|
|
194
|
+
* when it is worth paying for.
|
|
195
|
+
*
|
|
196
|
+
* `halt` is true for `regressed` and `unverifiable` and for nothing else.
|
|
197
|
+
* Never throws.
|
|
198
|
+
*/
|
|
199
|
+
export declare function runPostMergeGate(before: RepoHeadSnapshot[], after: RepoHeadSnapshot[], cfg: AshlrConfig, seams?: PostMergeGateSeams & {
|
|
200
|
+
signal?: AbortSignal;
|
|
201
|
+
}): Promise<PostMergeGateResult>;
|
|
202
|
+
export declare function postMergeHaltDir(): string;
|
|
203
|
+
/**
|
|
204
|
+
* Persist the halt durably (exclusive tmp + atomic rename) and audit it.
|
|
205
|
+
* Returns the record path, or null when it could not be written — a write
|
|
206
|
+
* failure must never itself throw out of the loop's shutdown path.
|
|
207
|
+
*/
|
|
208
|
+
export declare function recordPostMergeHalt(result: PostMergeGateResult, opts?: {
|
|
209
|
+
now?: () => number;
|
|
210
|
+
}): string | null;
|
|
211
|
+
/** Read halt records back, newest first. Best-effort; never throws. */
|
|
212
|
+
export declare function readPostMergeHalts(limit?: number): PostMergeHaltRecord[];
|