instar 1.3.510 → 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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +67 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +24 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CoherenceJournal.d.ts +31 -1
- package/dist/core/CoherenceJournal.d.ts.map +1 -1
- package/dist/core/CoherenceJournal.js +49 -2
- package/dist/core/CoherenceJournal.js.map +1 -1
- package/dist/core/JournalSyncApplier.d.ts.map +1 -1
- package/dist/core/JournalSyncApplier.js +18 -0
- package/dist/core/JournalSyncApplier.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +63 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +5 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.d.ts +188 -0
- package/dist/monitoring/GreenPrAutoMerger.d.ts.map +1 -0
- package/dist/monitoring/GreenPrAutoMerger.js +393 -0
- package/dist/monitoring/GreenPrAutoMerger.js.map +1 -0
- package/dist/monitoring/GuardLatchStore.d.ts +134 -0
- package/dist/monitoring/GuardLatchStore.d.ts.map +1 -0
- package/dist/monitoring/GuardLatchStore.js +255 -0
- package/dist/monitoring/GuardLatchStore.js.map +1 -0
- package/dist/monitoring/MergeRunner.d.ts +126 -0
- package/dist/monitoring/MergeRunner.d.ts.map +1 -0
- package/dist/monitoring/MergeRunner.js +296 -0
- package/dist/monitoring/MergeRunner.js.map +1 -0
- package/dist/monitoring/floorDriftCanary.d.ts +88 -0
- package/dist/monitoring/floorDriftCanary.d.ts.map +1 -0
- package/dist/monitoring/floorDriftCanary.js +89 -0
- package/dist/monitoring/floorDriftCanary.js.map +1 -0
- package/dist/monitoring/greenPrAutomergeWiring.d.ts +73 -0
- package/dist/monitoring/greenPrAutomergeWiring.d.ts.map +1 -0
- package/dist/monitoring/greenPrAutomergeWiring.js +273 -0
- package/dist/monitoring/greenPrAutomergeWiring.js.map +1 -0
- package/dist/monitoring/greenPrLogic.d.ts +121 -0
- package/dist/monitoring/greenPrLogic.d.ts.map +1 -0
- package/dist/monitoring/greenPrLogic.js +198 -0
- package/dist/monitoring/greenPrLogic.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +12 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +16 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +251 -8
- package/dist/server/routes.js.map +1 -1
- package/dist/server/stopGate.d.ts +45 -0
- package/dist/server/stopGate.d.ts.map +1 -1
- package/dist/server/stopGate.js +74 -0
- package/dist/server/stopGate.js.map +1 -1
- package/package.json +1 -1
- package/scripts/safe-merge.mjs +449 -59
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.511.md +46 -0
- package/upgrades/1.3.512.md +65 -0
- package/upgrades/side-effects/green-pr-automerge-enforcement.md +171 -0
- package/upgrades/side-effects/multi-machine-seamlessness-ws43-jobs-pool-read.md +88 -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AA0B7B,0BAA0B;AAC1B,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,iBAAiB,CAAC;AACxD,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAC9C,uEAAuE;AACvE,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AAoCrC,MAAM,OAAO,eAAe;IACT,QAAQ,CAAS;IACjB,SAAS,CAAS;IAClB,OAAO,CAA4C;IACnD,UAAU,CAAe;IACzB,eAAe,CAA0B;IACzC,GAAG,CAAa;IAChB,MAAM,CAAyB;IAExC,KAAK,CAAY;IAEzB,YAAY,GAA0B;QACpC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,GAAG,CAAC,eAAe,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,4EAA4E;IAE5E;;;;OAIG;IACH,GAAG,CAAC,SAAiB,EAAE,MAAe;QACpC,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAChC,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,SAAiB,EAAE,QAAkB,EAAE,MAAe;QAC1D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,sFAAsF;IACtF,SAAS,CAAC,SAAiB,EAAE,OAAe,EAAE,EAAW,EAAE,MAAe;QACxE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACrE,CAAC;IAEO,MAAM,CAAC,SAAiB,EAAE,OAAe,EAAE,MAAwB,EAAE,GAAW,EAAE,MAAe;QACvG,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1F,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;QAC9F,yEAAyE;QACzE,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,CAAC;YACH,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/H,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,6HAA6H;YACzI,IAAI,CAAC,GAAG,CAAC,gDAAiD,CAAW,EAAE,OAAO,EAAE,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;IAED,4EAA4E;IAE5E;;;;;;OAMG;IACH,aAAa,CAAC,SAAiB;QAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;QACnD,MAAM,QAAQ,GAAG,CAAC,CAAkB,EAAE,EAAE;YACtC,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO;YACtC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACnC,IAAI,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC3D,CAAC,CAAC;QACF,mBAAmB;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QACvD,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAClD,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtI,CAAC;QACD,0CAA0C;QAC1C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,eAAe,EAAE;YAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,oDAAoD;IACpD,cAAc,CAAC,SAAiB;QAC9B,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK;gBAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,IAAI,QAAkB,CAAC;QACvB,IAAI,KAAe,CAAC;QACpB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;YAChD,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,6HAA6H;YACzI,IAAI,CAAC,GAAG,CAAC,yDAA0D,CAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3F,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;QAC5E,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC;QACjG,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,EAAE,CAAC,sBAAsB,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAC5G,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC;IAClE,CAAC;IAED,2EAA2E;IAE3E,aAAa;QACX,IAAI,CAAC,SAAS,CAAC,iBAAiB,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,gFAAgF;IAChF,gBAAgB;QACd,IAAI,CAAC,SAAS,CAAC,iBAAiB,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IACvE,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACzE,OAAO,MAAM,EAAE,MAAM,KAAK,KAAK,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC,CAAC,6HAA6H;YACrI,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,4EAA4E;IAE5E,qEAAqE;IACrE,QAAQ;QACN,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACtC,IAAI,QAAQ,GAAa,EAAE,CAAC;QAC5B,IAAI,KAAK,GAAa,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;YAChD,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC,CAAC,6HAA6H,CAAC,2DAA2D,CAAC,CAAC;QACrM,OAAO;YACL,QAAQ;YACR,cAAc,EAAE,KAAK;YACrB,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE;YAC7B,YAAY,EAAE,OAAO,CAAC,OAAO;YAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC;IACJ,CAAC;IAED,4EAA4E;IAEpE,QAAQ;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,iCAAiC,CAAC,CAAC;IAC9E,CAAC;IAEO,IAAI;QACV,MAAM,KAAK,GAAc,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;QACnF,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;YACtD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;YAClD,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;gBACpG,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,WAAuC,EAAE,CAAC;YACrH,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,6HAA6H;YACzI,IAAK,CAA2B,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACpD,IAAI,CAAC,GAAG,CAAC,sDAAuD,CAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YAC1F,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,OAAO;QACb,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAChE,MAAM,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC;YACvB,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5E,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,6HAA6H;YACzI,yEAAyE;YACzE,yEAAyE;YACzE,0EAA0E;YAC1E,IAAI,CAAC,GAAG,CAAC,qCAAsC,CAAW,EAAE,OAAO,EAAE,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAEO,GAAG,CAAC,GAAW;QACrB,IAAI,CAAC,MAAM,EAAE,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;IACxC,CAAC;CACF;AAED,gEAAgE;AAChE,SAAS,SAAS,CAAC,CAAkB,EAAE,CAAkB;IACvD,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;IAClD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AACvB,CAAC;AAED,SAAS,SAAS,CAAC,EAAgB;IACjC,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC;QACf,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC,CAAC,6HAA6H;QACrI,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC"}
|
|
@@ -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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"}
|