instar 1.3.511 → 1.3.512

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 (69) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +67 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +24 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/CoherenceJournal.d.ts +31 -1
  8. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  9. package/dist/core/CoherenceJournal.js +49 -2
  10. package/dist/core/CoherenceJournal.js.map +1 -1
  11. package/dist/core/JournalSyncApplier.d.ts.map +1 -1
  12. package/dist/core/JournalSyncApplier.js +18 -0
  13. package/dist/core/JournalSyncApplier.js.map +1 -1
  14. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  15. package/dist/core/PostUpdateMigrator.js +44 -0
  16. package/dist/core/PostUpdateMigrator.js.map +1 -1
  17. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  18. package/dist/core/devGatedFeatures.js +5 -0
  19. package/dist/core/devGatedFeatures.js.map +1 -1
  20. package/dist/core/types.d.ts +27 -0
  21. package/dist/core/types.d.ts.map +1 -1
  22. package/dist/core/types.js.map +1 -1
  23. package/dist/monitoring/GreenPrAutoMerger.d.ts +188 -0
  24. package/dist/monitoring/GreenPrAutoMerger.d.ts.map +1 -0
  25. package/dist/monitoring/GreenPrAutoMerger.js +393 -0
  26. package/dist/monitoring/GreenPrAutoMerger.js.map +1 -0
  27. package/dist/monitoring/GuardLatchStore.d.ts +134 -0
  28. package/dist/monitoring/GuardLatchStore.d.ts.map +1 -0
  29. package/dist/monitoring/GuardLatchStore.js +255 -0
  30. package/dist/monitoring/GuardLatchStore.js.map +1 -0
  31. package/dist/monitoring/MergeRunner.d.ts +126 -0
  32. package/dist/monitoring/MergeRunner.d.ts.map +1 -0
  33. package/dist/monitoring/MergeRunner.js +296 -0
  34. package/dist/monitoring/MergeRunner.js.map +1 -0
  35. package/dist/monitoring/floorDriftCanary.d.ts +88 -0
  36. package/dist/monitoring/floorDriftCanary.d.ts.map +1 -0
  37. package/dist/monitoring/floorDriftCanary.js +89 -0
  38. package/dist/monitoring/floorDriftCanary.js.map +1 -0
  39. package/dist/monitoring/greenPrAutomergeWiring.d.ts +73 -0
  40. package/dist/monitoring/greenPrAutomergeWiring.d.ts.map +1 -0
  41. package/dist/monitoring/greenPrAutomergeWiring.js +273 -0
  42. package/dist/monitoring/greenPrAutomergeWiring.js.map +1 -0
  43. package/dist/monitoring/greenPrLogic.d.ts +121 -0
  44. package/dist/monitoring/greenPrLogic.d.ts.map +1 -0
  45. package/dist/monitoring/greenPrLogic.js +198 -0
  46. package/dist/monitoring/greenPrLogic.js.map +1 -0
  47. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  48. package/dist/monitoring/guardManifest.js +12 -0
  49. package/dist/monitoring/guardManifest.js.map +1 -1
  50. package/dist/server/AgentServer.d.ts +2 -0
  51. package/dist/server/AgentServer.d.ts.map +1 -1
  52. package/dist/server/AgentServer.js +2 -0
  53. package/dist/server/AgentServer.js.map +1 -1
  54. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  55. package/dist/server/CapabilityIndex.js +16 -0
  56. package/dist/server/CapabilityIndex.js.map +1 -1
  57. package/dist/server/routes.d.ts +5 -0
  58. package/dist/server/routes.d.ts.map +1 -1
  59. package/dist/server/routes.js +112 -1
  60. package/dist/server/routes.js.map +1 -1
  61. package/dist/server/stopGate.d.ts +45 -0
  62. package/dist/server/stopGate.d.ts.map +1 -1
  63. package/dist/server/stopGate.js +74 -0
  64. package/dist/server/stopGate.js.map +1 -1
  65. package/package.json +1 -1
  66. package/scripts/safe-merge.mjs +449 -59
  67. package/src/data/builtin-manifest.json +64 -64
  68. package/upgrades/1.3.512.md +65 -0
  69. package/upgrades/side-effects/green-pr-automerge-enforcement.md +171 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * GuardLatchStore — pool-visible kill-switch + marker latches for guarded
3
+ * autonomous authorities (green-pr-automerge-enforcement R9/R7).
4
+ *
5
+ * Two independent gate families decide whether the GreenPrAutoMerger may act:
6
+ * - `rollback` — set by POST /green-pr-automerge/rollback, cleared by
7
+ * POST /green-pr-automerge/enable (PIN-gated re-arm)
8
+ * - `emergency-pause` — set by the MessageSentinel emergency stop, cleared by
9
+ * its own resume path
10
+ * BOTH must be open for a merge. A third family, `pool-armed`, is a posture
11
+ * MARKER (not a gate): its presence makes a lease holder whose local config is
12
+ * disabled grade `diverged-from-default` instead of healthy dark-default.
13
+ *
14
+ * Why a store and not a flag (round-2 finding): execution follows the lease, so
15
+ * a machine-LOCAL latch would silently resurrect the watcher on a lease move.
16
+ * Each transition is therefore written BOTH to a durable local file (authoritative
17
+ * for this machine's own writes, present regardless of the journal feature flag)
18
+ * AND to the replicated `guard-latch` coherence-journal kind, so a peer that
19
+ * takes the lease reads the same disabled state.
20
+ *
21
+ * ABSORBING disable (round-4): a `rollback` set always wins ordering conflicts
22
+ * regardless of epoch — "a stale-epoch standby's STOP can never be out-ordered
23
+ * by an earlier /enable from a higher epoch." This is achieved structurally, not
24
+ * by clock comparison: every `set` mints a FRESH globally-unique latchId
25
+ * (`<machineId>:<seq>`), and `/enable` clears ONLY the specific latchId(s) it
26
+ * names. A new STOP therefore introduces a latchId no prior enable could have
27
+ * named, so it can never be pre-cleared. Ordering within a single latchId is by
28
+ * (epoch, seq), NEVER wall-clock.
29
+ *
30
+ * arrive-disabled-on-unreadable (round-3): if the merged peer view cannot be
31
+ * read, `isMergeAllowed()` returns false — absence of evidence is not "armed."
32
+ */
33
+ import type { CoherenceJournal, GuardLatchAction } from '../core/CoherenceJournal.js';
34
+ /** A single latch transition (own or peer), as merged for resolution. */
35
+ export interface GuardLatchEntry {
36
+ machine: string;
37
+ latchKind: string;
38
+ latchId: string;
39
+ action: GuardLatchAction;
40
+ epoch: number;
41
+ seq: number;
42
+ reason?: string;
43
+ ts?: string;
44
+ }
45
+ export type MergeGateReason = 'allowed' | 'rollback' | 'emergency-pause' | 'unreadable-peers';
46
+ export interface MergeGateVerdict {
47
+ allowed: boolean;
48
+ reason: MergeGateReason;
49
+ /** Active latch ids per blocking family (for the status surface / audit). */
50
+ activeLatchIds: Record<string, string[]>;
51
+ }
52
+ /** Latch family names. */
53
+ export declare const ROLLBACK_FAMILY = "rollback";
54
+ export declare const EMERGENCY_PAUSE_FAMILY = "emergency-pause";
55
+ export declare const POOL_ARMED_FAMILY = "pool-armed";
56
+ /** The fixed, pool-shared latchId for the single pool-armed marker. */
57
+ export declare const POOL_MARKER_ID = "pool";
58
+ export interface GuardLatchStoreConfig {
59
+ /** Absolute path to the agent's `.instar/` directory. */
60
+ stateDir: string;
61
+ /** Stable id for THIS machine. */
62
+ machineId: string;
63
+ /** Replication writer (optional — single-machine installs pass none). */
64
+ journal?: Pick<CoherenceJournal, 'emitGuardLatch'>;
65
+ /** Current lease epoch (monotonic; stamped on every transition). */
66
+ leaseEpoch: () => number;
67
+ /**
68
+ * Merged peer view: the latest guard-latch transition per (machine, kind, id)
69
+ * from replicated peer streams. Throwing signals UNREADABLE → arrive-disabled.
70
+ * Single-machine installs pass `() => []`.
71
+ */
72
+ readPeerEntries?: () => GuardLatchEntry[];
73
+ now?: () => Date;
74
+ logger?: (msg: string) => void;
75
+ }
76
+ export declare class GuardLatchStore {
77
+ private readonly stateDir;
78
+ private readonly machineId;
79
+ private readonly journal?;
80
+ private readonly leaseEpoch;
81
+ private readonly readPeerEntries;
82
+ private readonly now;
83
+ private readonly logger?;
84
+ private local;
85
+ constructor(cfg: GuardLatchStoreConfig);
86
+ /**
87
+ * Set (mint) a latch in `latchKind`. Returns the fresh, globally-unique
88
+ * latchId. Use for the absorbing families (rollback, emergency-pause): every
89
+ * call introduces a NEW latchId that no prior clear could have named.
90
+ */
91
+ set(latchKind: string, reason?: string): string;
92
+ /** Clear the SPECIFIC named latchId(s) in `latchKind` (the /enable semantics). */
93
+ clear(latchKind: string, latchIds: string[], reason?: string): void;
94
+ /** Set/clear a FIXED-id marker (pool-armed): last-(epoch,seq)-wins, not absorbing. */
95
+ setMarker(latchKind: string, latchId: string, on: boolean, reason?: string): void;
96
+ private record;
97
+ /**
98
+ * Resolve a family to the winning action per latchId across own + peers.
99
+ * Within one latchId, the highest (epoch, seq) transition wins. Distinct
100
+ * latchIds are independent — that is what makes `set` absorbing for the
101
+ * fresh-id families.
102
+ * @throws if the peer view is unreadable (caller maps to arrive-disabled).
103
+ */
104
+ resolveFamily(latchKind: string): Map<string, GuardLatchEntry>;
105
+ /** Active (currently-set) latch ids in a family. */
106
+ activeLatchIds(latchKind: string): string[];
107
+ /**
108
+ * The dual-latch gate (R9): both rollback and emergency-pause must be open.
109
+ * An unreadable peer view arrives DISABLED (absence of evidence ≠ armed).
110
+ */
111
+ isMergeAllowed(): MergeGateVerdict;
112
+ markPoolArmed(): void;
113
+ /** PIN-gated disarm: superseding entry, grades back to healthy dark-default. */
114
+ markPoolDisarmed(): void;
115
+ /**
116
+ * Is this pool deliberately armed (per the replicated marker)? Used by guard
117
+ * posture to grade a local-disabled + pool-armed machine `diverged-from-default`.
118
+ * Unreadable peers → false (we never INVENT a divergence alarm on a read error).
119
+ */
120
+ isPoolArmed(): boolean;
121
+ /** Snapshot for the status route + posture grading. Never throws. */
122
+ snapshot(): {
123
+ rollback: string[];
124
+ emergencyPause: string[];
125
+ poolArmed: boolean;
126
+ mergeAllowed: boolean;
127
+ reason: MergeGateReason;
128
+ };
129
+ private filePath;
130
+ private load;
131
+ private persist;
132
+ private log;
133
+ }
134
+ //# sourceMappingURL=GuardLatchStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GuardLatchStore.d.ts","sourceRoot":"","sources":["../../src/monitoring/GuardLatchStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,255 @@
1
+ /**
2
+ * GuardLatchStore — pool-visible kill-switch + marker latches for guarded
3
+ * autonomous authorities (green-pr-automerge-enforcement R9/R7).
4
+ *
5
+ * Two independent gate families decide whether the GreenPrAutoMerger may act:
6
+ * - `rollback` — set by POST /green-pr-automerge/rollback, cleared by
7
+ * POST /green-pr-automerge/enable (PIN-gated re-arm)
8
+ * - `emergency-pause` — set by the MessageSentinel emergency stop, cleared by
9
+ * its own resume path
10
+ * BOTH must be open for a merge. A third family, `pool-armed`, is a posture
11
+ * MARKER (not a gate): its presence makes a lease holder whose local config is
12
+ * disabled grade `diverged-from-default` instead of healthy dark-default.
13
+ *
14
+ * Why a store and not a flag (round-2 finding): execution follows the lease, so
15
+ * a machine-LOCAL latch would silently resurrect the watcher on a lease move.
16
+ * Each transition is therefore written BOTH to a durable local file (authoritative
17
+ * for this machine's own writes, present regardless of the journal feature flag)
18
+ * AND to the replicated `guard-latch` coherence-journal kind, so a peer that
19
+ * takes the lease reads the same disabled state.
20
+ *
21
+ * ABSORBING disable (round-4): a `rollback` set always wins ordering conflicts
22
+ * regardless of epoch — "a stale-epoch standby's STOP can never be out-ordered
23
+ * by an earlier /enable from a higher epoch." This is achieved structurally, not
24
+ * by clock comparison: every `set` mints a FRESH globally-unique latchId
25
+ * (`<machineId>:<seq>`), and `/enable` clears ONLY the specific latchId(s) it
26
+ * names. A new STOP therefore introduces a latchId no prior enable could have
27
+ * named, so it can never be pre-cleared. Ordering within a single latchId is by
28
+ * (epoch, seq), NEVER wall-clock.
29
+ *
30
+ * arrive-disabled-on-unreadable (round-3): if the merged peer view cannot be
31
+ * read, `isMergeAllowed()` returns false — absence of evidence is not "armed."
32
+ */
33
+ import fs from 'node:fs';
34
+ import path from 'node:path';
35
+ /** Latch family names. */
36
+ export const ROLLBACK_FAMILY = 'rollback';
37
+ export const EMERGENCY_PAUSE_FAMILY = 'emergency-pause';
38
+ export const POOL_ARMED_FAMILY = 'pool-armed';
39
+ /** The fixed, pool-shared latchId for the single pool-armed marker. */
40
+ export const POOL_MARKER_ID = 'pool';
41
+ export class GuardLatchStore {
42
+ stateDir;
43
+ machineId;
44
+ journal;
45
+ leaseEpoch;
46
+ readPeerEntries;
47
+ now;
48
+ logger;
49
+ local;
50
+ constructor(cfg) {
51
+ this.stateDir = cfg.stateDir;
52
+ this.machineId = cfg.machineId;
53
+ this.journal = cfg.journal;
54
+ this.leaseEpoch = cfg.leaseEpoch;
55
+ this.readPeerEntries = cfg.readPeerEntries ?? (() => []);
56
+ this.now = cfg.now ?? (() => new Date());
57
+ this.logger = cfg.logger;
58
+ this.local = this.load();
59
+ }
60
+ // ---- mutation -----------------------------------------------------------
61
+ /**
62
+ * Set (mint) a latch in `latchKind`. Returns the fresh, globally-unique
63
+ * latchId. Use for the absorbing families (rollback, emergency-pause): every
64
+ * call introduces a NEW latchId that no prior clear could have named.
65
+ */
66
+ set(latchKind, reason) {
67
+ const seq = ++this.local.ownSeq;
68
+ const latchId = `${this.machineId}:${seq}`;
69
+ this.record(latchKind, latchId, 'set', seq, reason);
70
+ return latchId;
71
+ }
72
+ /** Clear the SPECIFIC named latchId(s) in `latchKind` (the /enable semantics). */
73
+ clear(latchKind, latchIds, reason) {
74
+ for (const latchId of latchIds) {
75
+ const seq = ++this.local.ownSeq;
76
+ this.record(latchKind, latchId, 'clear', seq, reason);
77
+ }
78
+ }
79
+ /** Set/clear a FIXED-id marker (pool-armed): last-(epoch,seq)-wins, not absorbing. */
80
+ setMarker(latchKind, latchId, on, reason) {
81
+ const seq = ++this.local.ownSeq;
82
+ this.record(latchKind, latchId, on ? 'set' : 'clear', seq, reason);
83
+ }
84
+ record(latchKind, latchId, action, seq, reason) {
85
+ const epoch = safeEpoch(this.leaseEpoch);
86
+ const ts = this.now().toISOString();
87
+ const fam = this.local.transitions[latchKind] ?? (this.local.transitions[latchKind] = {});
88
+ fam[latchId] = { action, epoch, seq, ...(reason ? { reason: reason.slice(0, 80) } : {}), ts };
89
+ // Durable local write FIRST (safety never waits on the journal/network).
90
+ this.persist();
91
+ // Then replicate (best-effort; non-blocking inside the journal).
92
+ try {
93
+ this.journal?.emitGuardLatch({ latchKind, latchId, action, epoch, seq, ...(reason ? { reason: reason.slice(0, 80) } : {}) });
94
+ }
95
+ catch (e) { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
96
+ this.log(`guard-latch journal emit failed (swallowed): ${e?.message}`);
97
+ }
98
+ }
99
+ // ---- resolution ---------------------------------------------------------
100
+ /**
101
+ * Resolve a family to the winning action per latchId across own + peers.
102
+ * Within one latchId, the highest (epoch, seq) transition wins. Distinct
103
+ * latchIds are independent — that is what makes `set` absorbing for the
104
+ * fresh-id families.
105
+ * @throws if the peer view is unreadable (caller maps to arrive-disabled).
106
+ */
107
+ resolveFamily(latchKind) {
108
+ const winners = new Map();
109
+ const consider = (e) => {
110
+ if (e.latchKind !== latchKind)
111
+ return;
112
+ const cur = winners.get(e.latchId);
113
+ if (!cur || dominates(e, cur))
114
+ winners.set(e.latchId, e);
115
+ };
116
+ // Own transitions.
117
+ const ownFam = this.local.transitions[latchKind] ?? {};
118
+ for (const [latchId, t] of Object.entries(ownFam)) {
119
+ consider({ machine: this.machineId, latchKind, latchId, action: t.action, epoch: t.epoch, seq: t.seq, reason: t.reason, ts: t.ts });
120
+ }
121
+ // Peer transitions (throws → unreadable).
122
+ for (const e of this.readPeerEntries())
123
+ consider(e);
124
+ return winners;
125
+ }
126
+ /** Active (currently-set) latch ids in a family. */
127
+ activeLatchIds(latchKind) {
128
+ const out = [];
129
+ for (const [latchId, e] of this.resolveFamily(latchKind)) {
130
+ if (e.action === 'set')
131
+ out.push(latchId);
132
+ }
133
+ return out;
134
+ }
135
+ /**
136
+ * The dual-latch gate (R9): both rollback and emergency-pause must be open.
137
+ * An unreadable peer view arrives DISABLED (absence of evidence ≠ armed).
138
+ */
139
+ isMergeAllowed() {
140
+ let rollback;
141
+ let pause;
142
+ try {
143
+ rollback = this.activeLatchIds(ROLLBACK_FAMILY);
144
+ pause = this.activeLatchIds(EMERGENCY_PAUSE_FAMILY);
145
+ }
146
+ catch (e) { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
147
+ this.log(`guard-latch peer view unreadable — arriving DISABLED: ${e?.message}`);
148
+ return { allowed: false, reason: 'unreadable-peers', activeLatchIds: {} };
149
+ }
150
+ if (rollback.length > 0) {
151
+ return { allowed: false, reason: 'rollback', activeLatchIds: { [ROLLBACK_FAMILY]: rollback } };
152
+ }
153
+ if (pause.length > 0) {
154
+ return { allowed: false, reason: 'emergency-pause', activeLatchIds: { [EMERGENCY_PAUSE_FAMILY]: pause } };
155
+ }
156
+ return { allowed: true, reason: 'allowed', activeLatchIds: {} };
157
+ }
158
+ // ---- pool-armed marker (R7) --------------------------------------------
159
+ markPoolArmed() {
160
+ this.setMarker(POOL_ARMED_FAMILY, POOL_MARKER_ID, true, 'armed');
161
+ }
162
+ /** PIN-gated disarm: superseding entry, grades back to healthy dark-default. */
163
+ markPoolDisarmed() {
164
+ this.setMarker(POOL_ARMED_FAMILY, POOL_MARKER_ID, false, 'disarmed');
165
+ }
166
+ /**
167
+ * Is this pool deliberately armed (per the replicated marker)? Used by guard
168
+ * posture to grade a local-disabled + pool-armed machine `diverged-from-default`.
169
+ * Unreadable peers → false (we never INVENT a divergence alarm on a read error).
170
+ */
171
+ isPoolArmed() {
172
+ try {
173
+ const winner = this.resolveFamily(POOL_ARMED_FAMILY).get(POOL_MARKER_ID);
174
+ return winner?.action === 'set';
175
+ }
176
+ catch { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
177
+ return false;
178
+ }
179
+ }
180
+ // ---- status -------------------------------------------------------------
181
+ /** Snapshot for the status route + posture grading. Never throws. */
182
+ snapshot() {
183
+ const verdict = this.isMergeAllowed();
184
+ let rollback = [];
185
+ let pause = [];
186
+ try {
187
+ rollback = this.activeLatchIds(ROLLBACK_FAMILY);
188
+ pause = this.activeLatchIds(EMERGENCY_PAUSE_FAMILY);
189
+ }
190
+ catch { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */ /* unreadable — leave empty; verdict already reflects it */ }
191
+ return {
192
+ rollback,
193
+ emergencyPause: pause,
194
+ poolArmed: this.isPoolArmed(),
195
+ mergeAllowed: verdict.allowed,
196
+ reason: verdict.reason,
197
+ };
198
+ }
199
+ // ---- durable local file -------------------------------------------------
200
+ filePath() {
201
+ return path.join(this.stateDir, 'state', 'green-pr-automerge-latches.json');
202
+ }
203
+ load() {
204
+ const fresh = { machineId: this.machineId, ownSeq: 0, transitions: {} };
205
+ try {
206
+ const raw = fs.readFileSync(this.filePath(), 'utf-8');
207
+ const obj = JSON.parse(raw);
208
+ if (obj && typeof obj.ownSeq === 'number' && obj.transitions && typeof obj.transitions === 'object') {
209
+ return { machineId: this.machineId, ownSeq: obj.ownSeq, transitions: obj.transitions };
210
+ }
211
+ }
212
+ catch (e) { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
213
+ if (e?.code !== 'ENOENT') {
214
+ this.log(`guard-latch local file unreadable, starting fresh: ${e?.message}`);
215
+ }
216
+ }
217
+ return fresh;
218
+ }
219
+ persist() {
220
+ const p = this.filePath();
221
+ const dir = path.dirname(p);
222
+ try {
223
+ if (!fs.existsSync(dir))
224
+ fs.mkdirSync(dir, { recursive: true });
225
+ const tmp = p + '.tmp';
226
+ fs.writeFileSync(tmp, JSON.stringify(this.local, null, 2), { mode: 0o600 });
227
+ fs.renameSync(tmp, p);
228
+ }
229
+ catch (e) { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
230
+ // A failed durable write is a SAFETY problem for a STOP (the latch might
231
+ // not survive a restart). We surface it loudly but do not throw into the
232
+ // route — the journal emit + the in-memory state still gate this process.
233
+ this.log(`guard-latch durable write FAILED: ${e?.message}`);
234
+ }
235
+ }
236
+ log(msg) {
237
+ this.logger?.(`[guard-latch] ${msg}`);
238
+ }
239
+ }
240
+ /** (epoch, seq) lexicographic domination — NEVER wall-clock. */
241
+ function dominates(a, b) {
242
+ if (a.epoch !== b.epoch)
243
+ return a.epoch > b.epoch;
244
+ return a.seq > b.seq;
245
+ }
246
+ function safeEpoch(fn) {
247
+ try {
248
+ const e = fn();
249
+ return Number.isFinite(e) ? e : 0;
250
+ }
251
+ catch { /* @silent-fallback-ok: green-pr-automerge fail-safe — skip/refuse, never over-merge; safe-merge is the act-time authority */
252
+ return 0;
253
+ }
254
+ }
255
+ //# sourceMappingURL=GuardLatchStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GuardLatchStore.js","sourceRoot":"","sources":["../../src/monitoring/GuardLatchStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * MergeRunner — the act-path engine for the GreenPrAutoMerger (Step 4 of
3
+ * green-pr-automerge-enforcement §3.1/R5). Owns everything that touches a real
4
+ * process:
5
+ *
6
+ * - probeContract(): runs `safe-merge --capabilities`, pins the script's
7
+ * content hash + absolute path for the attempt (round-3 mid-run-swap defense),
8
+ * and re-verifies that hash IMMEDIATELY before exec (round-5: the spawn runs
9
+ * the verified bytes).
10
+ * - run(): writes a DURABLE two-phase in-flight record BEFORE the spawn (intent
11
+ * first, pid/pgid patched in after), spawns safe-merge in its OWN process
12
+ * group, hard-kills the group at mergeTimeoutMs + grace, parses the classified
13
+ * `safe-merge-result:` line, and confirms a `merged` outcome INDEPENDENTLY
14
+ * (B10). The record is cleared on classification.
15
+ * - reapOrphan(): at boot/warm-up, reaps a surviving orphan from a crash —
16
+ * verifies the recorded pid/pgid identity via a unique attempt token before
17
+ * signalling (pid-reuse safe), handles the dead-leader/live-group corner
18
+ * (orphan-reap-incomplete), and re-verifies PR state via gh. A pid-less
19
+ * in-flight record is treated as attempt-of-unknown-outcome (re-verify,
20
+ * never assume not-spawned).
21
+ *
22
+ * The guarantee survives the server's own death: the watcher's merges trigger
23
+ * releases → auto-update restarts, so a restart mid-attempt is a NORMAL
24
+ * condition. The durable record + own-process-group + shutdown group-kill +
25
+ * warm-up reap close the double-attempt / wedged-orphan classes.
26
+ *
27
+ * All process + fs + gh I/O is behind injected seams so the runner is testable
28
+ * without spawning anything.
29
+ *
30
+ * RULE 3.1 RATIONALE
31
+ * Criticality: high — this code drives an autonomous merge to main.
32
+ * Frequency: per-merge-attempt (at most one per ~10-minute tick).
33
+ * Stability: stable — parses safe-merge's OWN structured contract line
34
+ * (`safe-merge-result: {...}`), not human gh output; and
35
+ * `gh ... --json` structured responses, never regex over prose.
36
+ * Fallback: every parse failure is caught and resolves to a NON-merged
37
+ * outcome (fail-toward-skip); a `merged` outcome is never trusted
38
+ * without an INDEPENDENT `gh pr view` confirmation (B10).
39
+ * Verdict: deterministic; the act-time authority is safe-merge, which
40
+ * re-verifies, so a misread here can only refuse, never over-merge.
41
+ */
42
+ import type { MergeAttempt, MergeRunResult, MergeRunner } from './GreenPrAutoMerger.js';
43
+ /** A spawned child's observable result (seam over child_process). */
44
+ export interface SpawnOutcome {
45
+ stdout: string;
46
+ stderr: string;
47
+ status: number | null;
48
+ signal: string | null;
49
+ /** Whether the deadline timer fired and the group was killed. */
50
+ deadlineKilled: boolean;
51
+ pid: number | null;
52
+ }
53
+ export interface SpawnArgs {
54
+ command: string;
55
+ args: string[];
56
+ env: NodeJS.ProcessEnv;
57
+ deadlineMs: number;
58
+ /** Called once the child has a pid, so the runner can patch the durable record. */
59
+ onPid?: (pid: number) => void;
60
+ }
61
+ export interface MergeRunnerConfig {
62
+ stateDir: string;
63
+ repo: string;
64
+ /** Absolute path to scripts/safe-merge.mjs. */
65
+ safeMergePath: string;
66
+ mergeTimeoutMs: number;
67
+ mergeKillGraceMs: number;
68
+ expectedContractVersion: number;
69
+ /** node executable (default process.execPath). */
70
+ nodePath?: string;
71
+ }
72
+ export interface MergeRunnerDeps {
73
+ /** Spawn a child in its OWN process group; resolve when it exits or is killed. */
74
+ spawn?: (a: SpawnArgs) => Promise<SpawnOutcome>;
75
+ /** Independent merge confirmation (gh pr view --json state,mergedAt). */
76
+ confirmMerged: (pr: number, repo: string) => Promise<boolean>;
77
+ /** Re-verify a PR's live state (for orphan reap). 'MERGED'|'CLOSED'|'OPEN'|'UNKNOWN'. */
78
+ prState: (pr: number, repo: string) => Promise<string>;
79
+ /** Is a process group still alive? (process.kill(pgid, 0) wrapper.) */
80
+ isAlive?: (pid: number) => boolean;
81
+ /** Kill a process group (SIGKILL the negative pid). */
82
+ killGroup?: (pgid: number) => void;
83
+ now?: () => number;
84
+ logger?: (msg: string) => void;
85
+ }
86
+ /** The durable in-flight record (state/green-pr-automerge-inflight.json). */
87
+ export interface InFlightRecord {
88
+ pr: number;
89
+ headRefOid: string;
90
+ repo: string;
91
+ attemptToken: string;
92
+ startedAt: number;
93
+ /** Patched in AFTER the spawn (phase 2). Null until then. */
94
+ pid: number | null;
95
+ pgid: number | null;
96
+ }
97
+ export declare class DefaultMergeRunner implements MergeRunner {
98
+ private readonly cfg;
99
+ private readonly deps;
100
+ private readonly now;
101
+ /** Pinned per-attempt: the probed script's hash + path. */
102
+ private pinnedHash;
103
+ constructor(cfg: MergeRunnerConfig, deps: MergeRunnerDeps);
104
+ probeContract(): Promise<{
105
+ ok: boolean;
106
+ version?: number;
107
+ }>;
108
+ private hashScript;
109
+ run(attempt: MergeAttempt): Promise<MergeRunResult>;
110
+ reapOrphan(): Promise<{
111
+ reaped: boolean;
112
+ outcome?: string;
113
+ }>;
114
+ private reverifyAndClear;
115
+ /** Best-effort identity check: does the live pid's argv carry our token? */
116
+ private verifyAttemptToken;
117
+ private inFlightPath;
118
+ private writeInFlight;
119
+ private readInFlight;
120
+ private clearInFlight;
121
+ private spawn;
122
+ private log;
123
+ }
124
+ /** Parse safe-merge's classified `safe-merge-result: {...}` line from stdout. */
125
+ export declare function parseResultLine(stdout: string): string | null;
126
+ //# sourceMappingURL=MergeRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MergeRunner.d.ts","sourceRoot":"","sources":["../../src/monitoring/MergeRunner.ts"],"names":[],"mappings":"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"}