@ashlr/hub 3.5.1 → 3.7.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 (110) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +3 -0
  3. package/dist/build-identity.json +2 -2
  4. package/dist/cli/activation.d.ts +41 -0
  5. package/dist/cli/activation.js +485 -0
  6. package/dist/cli/activation.js.map +1 -0
  7. package/dist/core/daemon/cutoff-checkpoint-scheduler.js +7 -0
  8. package/dist/core/daemon/cutoff-checkpoint-scheduler.js.map +1 -1
  9. package/dist/core/daemon/loop.d.ts +19 -0
  10. package/dist/core/daemon/loop.js +235 -5
  11. package/dist/core/daemon/loop.js.map +1 -1
  12. package/dist/core/daemon/overnight-status.d.ts +152 -0
  13. package/dist/core/daemon/overnight-status.js +275 -0
  14. package/dist/core/daemon/overnight-status.js.map +1 -0
  15. package/dist/core/daemon/post-merge-halt.d.ts +212 -0
  16. package/dist/core/daemon/post-merge-halt.js +503 -0
  17. package/dist/core/daemon/post-merge-halt.js.map +1 -0
  18. package/dist/core/daemon/run-window.d.ts +218 -0
  19. package/dist/core/daemon/run-window.js +420 -0
  20. package/dist/core/daemon/run-window.js.map +1 -0
  21. package/dist/core/fleet/generated-repair-resolution.d.ts +36 -0
  22. package/dist/core/fleet/generated-repair-resolution.js +245 -0
  23. package/dist/core/fleet/generated-repair-resolution.js.map +1 -0
  24. package/dist/core/local-runtime/llama/port.d.ts +17 -0
  25. package/dist/core/local-runtime/llama/port.js +47 -0
  26. package/dist/core/local-runtime/llama/port.js.map +1 -0
  27. package/dist/core/local-runtime/llama/proxy-host.d.ts +54 -0
  28. package/dist/core/local-runtime/llama/proxy-host.js +159 -0
  29. package/dist/core/local-runtime/llama/proxy-host.js.map +1 -0
  30. package/dist/core/local-runtime/llama/proxy-process.d.ts +148 -0
  31. package/dist/core/local-runtime/llama/proxy-process.js +250 -0
  32. package/dist/core/local-runtime/llama/proxy-process.js.map +1 -0
  33. package/dist/core/local-runtime/llama/proxy-record.d.ts +40 -0
  34. package/dist/core/local-runtime/llama/proxy-record.js +133 -0
  35. package/dist/core/local-runtime/llama/proxy-record.js.map +1 -0
  36. package/dist/core/local-runtime/llama/proxy-supervisor.d.ts +118 -0
  37. package/dist/core/local-runtime/llama/proxy-supervisor.js +511 -0
  38. package/dist/core/local-runtime/llama/proxy-supervisor.js.map +1 -0
  39. package/dist/core/web/public/next/assets/{App-BbVoUCIi.js → App-vtaHB9mp.js} +2 -2
  40. package/dist/core/web/public/next/assets/{App-BbVoUCIi.js.map → App-vtaHB9mp.js.map} +1 -1
  41. package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js → ApprovalsSection-B-bFJ0I9.js} +2 -2
  42. package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js.map → ApprovalsSection-B-bFJ0I9.js.map} +1 -1
  43. package/dist/core/web/public/next/assets/{AutonomySection-Cpk_oEhc.css → AutonomySection-CHjxJt2z.css} +1 -1
  44. package/dist/core/web/public/next/assets/AutonomySection-DoqmsCjw.js +2 -0
  45. package/dist/core/web/public/next/assets/AutonomySection-DoqmsCjw.js.map +1 -0
  46. package/dist/core/web/public/next/assets/ChatSection-BfnLe5dT.css +1 -0
  47. package/dist/core/web/public/next/assets/ChatSection-vNPIJffd.js +80 -0
  48. package/dist/core/web/public/next/assets/ChatSection-vNPIJffd.js.map +1 -0
  49. package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js → ConfirmDialog-Dd2APQhB.js} +2 -2
  50. package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js.map → ConfirmDialog-Dd2APQhB.js.map} +1 -1
  51. package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js → Dialog-CPM9JxBc.js} +2 -2
  52. package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js.map → Dialog-CPM9JxBc.js.map} +1 -1
  53. package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js → DiffViewer-B84qVef9.js} +2 -2
  54. package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js.map → DiffViewer-B84qVef9.js.map} +1 -1
  55. package/dist/core/web/public/next/assets/McpSection-CEUpV_5X.js +2 -0
  56. package/dist/core/web/public/next/assets/McpSection-CEUpV_5X.js.map +1 -0
  57. package/dist/core/web/public/next/assets/McpSection-D88QlDpG.css +1 -0
  58. package/dist/core/web/public/next/assets/Meter--h3MmZS-.js +2 -0
  59. package/dist/core/web/public/next/assets/Meter--h3MmZS-.js.map +1 -0
  60. package/dist/core/web/public/next/assets/Meter-CLcr-4wS.css +1 -0
  61. package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js → MutationTokenDialog-DA8t97XT.js} +2 -2
  62. package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js.map → MutationTokenDialog-DA8t97XT.js.map} +1 -1
  63. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js → ResourcePoolConsoleApp-Bq7Vyud0.js} +2 -2
  64. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js.map → ResourcePoolConsoleApp-Bq7Vyud0.js.map} +1 -1
  65. package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js → RouteErrorBoundary-Badio_uO.js} +2 -2
  66. package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js.map → RouteErrorBoundary-Badio_uO.js.map} +1 -1
  67. package/dist/core/web/public/next/assets/SettingsSection-BwrpQlpP.js +2 -0
  68. package/dist/core/web/public/next/assets/SettingsSection-BwrpQlpP.js.map +1 -0
  69. package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js → UniverseConsoleApp-_FSohAdN.js} +2 -2
  70. package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js.map → UniverseConsoleApp-_FSohAdN.js.map} +1 -1
  71. package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js → UniverseView-DqcXbed4.js} +2 -2
  72. package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js.map → UniverseView-DqcXbed4.js.map} +1 -1
  73. package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js → UsageSection-Bq6Daaby.js} +2 -2
  74. package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js.map → UsageSection-Bq6Daaby.js.map} +1 -1
  75. package/dist/core/web/public/next/assets/VerseConsoleApp-CMkCizvL.css +1 -0
  76. package/dist/core/web/public/next/assets/VerseConsoleApp-CsIQv9id.js +3 -0
  77. package/dist/core/web/public/next/assets/VerseConsoleApp-CsIQv9id.js.map +1 -0
  78. package/dist/core/web/public/next/assets/{charts-DSUpczxG.js → charts-CTZJ-JrH.js} +2 -2
  79. package/dist/core/web/public/next/assets/{charts-DSUpczxG.js.map → charts-CTZJ-JrH.js.map} +1 -1
  80. package/dist/core/web/public/next/assets/global-D6bTxIRn.js +2 -0
  81. package/dist/core/web/public/next/assets/global-D6bTxIRn.js.map +1 -0
  82. package/dist/core/web/public/next/assets/global-z-JfD3ow.css +1 -0
  83. package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js → icons-D9vlf85u.js} +2 -2
  84. package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js.map → icons-D9vlf85u.js.map} +1 -1
  85. package/dist/core/web/public/next/assets/{index-BMRIET7F.js → index-CZamKRUg.js} +3 -3
  86. package/dist/core/web/public/next/assets/{index-BMRIET7F.js.map → index-CZamKRUg.js.map} +1 -1
  87. package/dist/core/web/public/next/assets/{queries-AxnVa02h.js → queries-B8eBnFox.js} +2 -2
  88. package/dist/core/web/public/next/assets/{queries-AxnVa02h.js.map → queries-B8eBnFox.js.map} +1 -1
  89. package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js → use-guarded-action-bzrVVAYq.js} +2 -2
  90. package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js.map → use-guarded-action-bzrVVAYq.js.map} +1 -1
  91. package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js +2 -0
  92. package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js.map +1 -0
  93. package/dist/core/web/public/next/index.html +1 -1
  94. package/dist/release-dependency-inventory.json +1 -1
  95. package/package.json +1 -1
  96. package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js +0 -2
  97. package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js.map +0 -1
  98. package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js +0 -80
  99. package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js.map +0 -1
  100. package/dist/core/web/public/next/assets/ChatSection-LQiAn6Eu.css +0 -1
  101. package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js +0 -2
  102. package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js.map +0 -1
  103. package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js +0 -3
  104. package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js.map +0 -1
  105. package/dist/core/web/public/next/assets/VerseConsoleApp-D2E3mU9a.css +0 -1
  106. package/dist/core/web/public/next/assets/global-C4jfYHrj.js +0 -2
  107. package/dist/core/web/public/next/assets/global-C4jfYHrj.js.map +0 -1
  108. package/dist/core/web/public/next/assets/global-D463gG1X.css +0 -1
  109. package/dist/core/web/public/next/assets/workspace-model-cE4LkKpE.js +0 -2
  110. 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[];