instar 1.3.149 → 1.3.151

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.149",
3
+ "version": "1.3.151",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-31T07:33:04.801Z",
5
- "instarVersion": "1.3.149",
4
+ "generatedAt": "2026-05-31T08:16:51.978Z",
5
+ "instarVersion": "1.3.151",
6
6
  "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
14
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
15
15
  "since": "2025-01-01"
16
16
  },
17
17
  "hook:dangerous-command-guard": {
@@ -20,7 +20,7 @@
20
20
  "domain": "safety",
21
21
  "sourcePath": "src/core/PostUpdateMigrator.ts",
22
22
  "installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
23
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
23
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
24
24
  "since": "2025-01-01"
25
25
  },
26
26
  "hook:grounding-before-messaging": {
@@ -29,7 +29,7 @@
29
29
  "domain": "safety",
30
30
  "sourcePath": "src/core/PostUpdateMigrator.ts",
31
31
  "installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
32
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
32
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
33
33
  "since": "2025-01-01"
34
34
  },
35
35
  "hook:compaction-recovery": {
@@ -38,7 +38,7 @@
38
38
  "domain": "identity",
39
39
  "sourcePath": "src/core/PostUpdateMigrator.ts",
40
40
  "installedPath": ".instar/hooks/instar/compaction-recovery.sh",
41
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
41
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
42
42
  "since": "2025-01-01"
43
43
  },
44
44
  "hook:external-operation-gate": {
@@ -47,7 +47,7 @@
47
47
  "domain": "safety",
48
48
  "sourcePath": "src/core/PostUpdateMigrator.ts",
49
49
  "installedPath": ".instar/hooks/instar/external-operation-gate.js",
50
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
50
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
51
51
  "since": "2025-01-01"
52
52
  },
53
53
  "hook:deferral-detector": {
@@ -56,7 +56,7 @@
56
56
  "domain": "safety",
57
57
  "sourcePath": "src/core/PostUpdateMigrator.ts",
58
58
  "installedPath": ".instar/hooks/instar/deferral-detector.js",
59
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
59
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:post-action-reflection": {
@@ -65,7 +65,7 @@
65
65
  "domain": "evolution",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
68
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
68
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:external-communication-guard": {
@@ -74,7 +74,7 @@
74
74
  "domain": "safety",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
77
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
77
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:scope-coherence-collector": {
@@ -83,7 +83,7 @@
83
83
  "domain": "coherence",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
86
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
86
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-checkpoint": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
95
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
95
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:free-text-guard": {
@@ -101,7 +101,7 @@
101
101
  "domain": "safety",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
104
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
104
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:claim-intercept": {
@@ -110,7 +110,7 @@
110
110
  "domain": "coherence",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
113
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
113
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept-response": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
122
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
122
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:stop-gate-router": {
@@ -128,7 +128,7 @@
128
128
  "domain": "safety",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
131
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
131
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:auto-approve-permissions": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
140
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
140
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "job:health-check": {
@@ -1489,7 +1489,7 @@
1489
1489
  "type": "subsystem",
1490
1490
  "domain": "updates",
1491
1491
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1492
- "contentHash": "09e2c3f8c4a1f7d579224923247e788bf1b90a79ecbc629c4598179cf534ffa2",
1492
+ "contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
1493
1493
  "since": "2025-01-01"
1494
1494
  },
1495
1495
  "subsystem:scheduler": {
@@ -1545,7 +1545,7 @@
1545
1545
  "type": "subsystem",
1546
1546
  "domain": "coordination",
1547
1547
  "sourcePath": "src/core/MultiMachineCoordinator.ts",
1548
- "contentHash": "44aa63dff721e15ad680db4c1d552abfc9e97e610498b6752496c222f27f323b",
1548
+ "contentHash": "1dc3fb11c8ddea5cfe5f02561636cd54c1661d0e9da04f54607ad7679f027ea6",
1549
1549
  "since": "2025-01-01"
1550
1550
  },
1551
1551
  "subsystem:backup-manager": {
@@ -0,0 +1,54 @@
1
+ ---
2
+ review-convergence: complete
3
+ approved: true
4
+ approved-by: echo (standing 12h deploy mandate, topic 13435; live-found driving Codey)
5
+ ---
6
+
7
+ # Upgrade Guide — vNEXT
8
+
9
+ <!-- bump: patch -->
10
+
11
+ ## What Changed — the scope-check Stop hook no longer confuses Codex
12
+
13
+ When an agent finishes a turn, a "scope-coherence checkpoint" Stop hook runs and, on
14
+ the normal allow path, used to print a tiny `{decision:approve}` message. Claude
15
+ treats that as allow, but Codex's Stop-hook contract is stricter: print
16
+ `{decision:block,...}` to interrupt, print **nothing** to allow — an explicit
17
+ approve message is rejected as "invalid stop hook JSON output". So on every Codex
18
+ session completion that hook reported an error (the reply was already sent, so
19
+ nothing actually broke, but the error was noisy and wrong). Found live while
20
+ test-driving the Codex agent.
21
+
22
+ The fix changes every allow path to emit nothing (empty stdout = allow), which is
23
+ byte-equivalent for Claude and removes the error for Codex. The interrupt (block)
24
+ path is unchanged. The hook is regenerated from source on every update, so all
25
+ agents get the fix automatically.
26
+
27
+ ## Summary of New Capabilities
28
+
29
+ - `scope-coherence-checkpoint` Stop hook is now Codex-stdout-safe: allow paths emit
30
+ empty stdout (instead of `{decision:approve}`); the block/checkpoint path is
31
+ unchanged. Sibling of the earlier autonomous-stop-hook Codex fix.
32
+
33
+ ## What to Tell Your User
34
+
35
+ If you run a Codex-based agent, it will stop logging a spurious "invalid stop hook"
36
+ error every time it finishes a turn. Nothing actually broke before (replies still
37
+ went out), and Claude agents are unaffected — this just makes the scope-check hook
38
+ speak Codex's dialect correctly. It applies automatically on the next update.
39
+
40
+ ## Evidence
41
+
42
+ - Live: driving the Codex agent over Telegram, it completed a task + sent its reply,
43
+ then its session reported "Stop hook (failed) — invalid stop hook JSON output".
44
+ - Audited the 5-hook Stop chain: only scope-coherence-checkpoint writes on the allow
45
+ path; the others write only on block (codex-safe), and the autonomous hook was
46
+ fixed earlier.
47
+ - Unit: `tests/unit/scope-coherence-reentry.test.ts` — allow paths now exit 0 with
48
+ empty stdout; the generated hook contains no `{decision:approve}` and keeps
49
+ `{decision:block}`.
50
+ - Regression: PostUpdateMigrator-buildStopHook + migration-parity-hooks +
51
+ installCodexHooks — 28 tests pass.
52
+ - `tsc --noEmit` + `npm run lint` clean.
53
+ - Spec: `docs/specs/codex-scope-coherence-stdout-safe.md` (+ `.eli16.md`).
54
+ - Side-effects: `upgrades/side-effects/codex-scope-coherence-stdout-safe.md`.
@@ -0,0 +1,65 @@
1
+ ---
2
+ review-convergence: complete
3
+ approved: true
4
+ approved-by: echo (standing 12h deploy mandate, topic 13481; multi-machine live-transfer cascade)
5
+ ---
6
+
7
+ # Instar Upgrade Guide — vNEXT
8
+
9
+ <!-- bump: patch -->
10
+
11
+ ## What Changed — a silent standby machine now correctly recognizes the awake machine as in charge
12
+
13
+ Two narrow fixes for the multi-machine live session-transfer path (the one that
14
+ moves a conversation from one machine to another when you say "move this to the
15
+ Mac mini"). Both were found by running the transfer live between a laptop and a
16
+ Mac mini and tracing why the handoff was rejected.
17
+
18
+ 1. **Signing-key fallback.** Each machine signs its mesh messages with a private
19
+ key file. The loader looked only for the current filename; a machine whose key
20
+ was saved under the older filename threw an error at boot, which silently
21
+ aborted its lease-coordination setup — so it never learned which machine was in
22
+ charge. The loader now also checks the legacy filename before giving up,
23
+ matching the fallback the lifeline loader already had.
24
+
25
+ 2. **Silent standby stops crowning itself.** A machine configured as a silent
26
+ standby (it never answers messages on its own) was still trying to claim the
27
+ "I'm in charge" lease at startup. On a machine without the shared git-backed
28
+ coordination medium, that produced a split-brain where both machines thought
29
+ they were in charge and the handoff was refused. A silent standby now only
30
+ watches the lease and never claims one, so the awake machine always wins and the
31
+ handoff is accepted.
32
+
33
+ ## Summary of New Capabilities
34
+
35
+ - `MachineIdentity.loadSigningKey()` falls back to the legacy `signing-private.pem`
36
+ when the canonical `signing-key.pem` is absent (rethrows if neither exists).
37
+ - `MultiMachineCoordinator.isLeaseObserveOnly` (driven by
38
+ `multiMachine.telegramPolling === false`): a silent standby skips lease
39
+ acquire/renew in both `initializeLease` and `tickLease`, only reconciling its
40
+ role to the observed lease — removing the git-less split-brain epoch leapfrog.
41
+
42
+ ## What to Tell Your User
43
+
44
+ If you run your agent on more than one machine, moving a live conversation from one
45
+ machine to another is now more reliable. A quiet backup machine will correctly
46
+ treat your main machine as the one in charge instead of fighting it for control,
47
+ and a machine whose security key was saved under an older filename will no longer
48
+ fail to join the handoff. Nothing to configure — it applies on the next update.
49
+
50
+ ## Evidence
51
+
52
+ - Both bugs were found live on the two-machine setup (laptop plus Mac mini) by
53
+ instrumenting the lease broadcast or observe path: the mini threw ENOENT on the
54
+ canonical key at boot so its lease coordinator never attached, and once that was
55
+ fixed it rejected the laptop's lease as below its own self-issued epoch floor.
56
+ - Unit, `tests/unit/machine-identity.test.ts`: loadSigningKey falls back to the
57
+ legacy name and still throws when neither file exists.
58
+ - Unit, `tests/unit/multi-machine-coordinator.test.ts`: a silent standby never
59
+ acquires or renews (across initializeLease and tickLease); a normal machine does
60
+ acquire; the observe-only flag reflects telegramPolling false.
61
+ - 109 related unit tests pass (machine-identity, multi-machine-coordinator,
62
+ LeaseCoordinator, mesh-signing-key-resolution, multimachine-syncstatus);
63
+ tsc --noEmit clean.
64
+ - Spec, `docs/specs/silent-standby-lease.md` plus the .eli16.md sibling.
65
+ - Side-effects, `upgrades/side-effects/silent-standby-lease.md`.
@@ -0,0 +1,42 @@
1
+ # Side-effects — scope-coherence-checkpoint codex stdout-safe
2
+
3
+ ## 1. What files/state does this touch at runtime?
4
+ Only the generated `.instar/hooks/instar/scope-coherence-checkpoint.js` (rewritten
5
+ by the always-overwrite built-in-hook migration). At Stop time the hook reads its
6
+ state file + (when deep) fetches active-job context — unchanged. The only change is
7
+ what it writes to stdout on the allow paths (now nothing).
8
+
9
+ ## 2. Does it change any functional behavior?
10
+ - **Claude:** none. Empty stdout == approve in Claude Code, identical to the prior
11
+ explicit `{decision:'approve'}`. The scope-checkpoint still blocks (prompts) when
12
+ deep-implementation + cooldown + min-age conditions are met.
13
+ - **codex:** the allow paths no longer emit `{decision:'approve'}`, so codex stops
14
+ reporting "invalid stop hook JSON output" on session completion. The block path is
15
+ unchanged (codex accepts `{decision:'block',...}`).
16
+
17
+ ## 3. What happens on failure / weird input?
18
+ The catch-all error path now allows (empty stdout) instead of emitting approve-JSON
19
+ — same allow outcome, codex-safe. Malformed stdin still falls through to allow.
20
+
21
+ ## 4. Migration parity — do existing agents get it?
22
+ Yes, automatically. The hook is in the always-overwrite built-in-hook set, so every
23
+ agent regenerates it from the fixed `getScopeCoherenceCheckpointHook()` on its next
24
+ update (incl. instar-codey). No marker/sniff needed.
25
+
26
+ ## 5. Could it spam / flood / burn resources?
27
+ No — it REDUCES output (writes less). The hook runs only at Stop, same as before.
28
+
29
+ ## 6. Rollback / off-switch?
30
+ Revert the PR; the next update regenerates the prior hook. No residual state, no
31
+ flag. Reverting restores `{decision:'approve'}` (Claude unaffected; codex error
32
+ returns).
33
+
34
+ ## 7. Concurrency / ordering?
35
+ None new. Single short-lived hook process per Stop event, unchanged.
36
+
37
+ ## Blast radius
38
+ Small + behavior-preserving on Claude, error-fixing on codex. One generator method
39
+ in PostUpdateMigrator (six allow-path stdout writes removed; block path kept) + test
40
+ updates. Mirrors the #604 codex-stdout convention. The broader approve-JSON class
41
+ (other hooks + additionalContext signal hooks) is explicitly out of scope, tracked
42
+ under #32 / ledger issue codex-stop-chain-multi-json-invalid.
@@ -0,0 +1,115 @@
1
+ # Side-Effects Review — Silent-standby git-less lease (observe-only + legacy-key fallback)
2
+
3
+ **Version / slug:** `silent-standby-lease`
4
+ **Date:** `2026-05-31`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Two narrowly-scoped fixes for bugs #4 + #5 of the multi-machine live-transfer
11
+ cascade:
12
+
13
+ 1. `MachineIdentity.loadSigningKey()` — on ENOENT for the canonical
14
+ `signing-key.pem`, fall back to the legacy `signing-private.pem` if present,
15
+ else rethrow. (The mini's key was under the legacy name; the lease setup threw
16
+ at boot, so the `LeaseCoordinator` never attached and the mini never resolved a
17
+ holder.)
18
+ 2. `MultiMachineCoordinator` — new `isLeaseObserveOnly` getter
19
+ (`multiMachine.telegramPolling === false`). A silent standby skips
20
+ `acquireIfEligible`/`renew` in both `initializeLease` and `tickLease`; it only
21
+ reconciles its role to whatever lease it observes. It never holds its own lease,
22
+ so with the git-less `LocalLeaseStore` (no shared CAS) it can't leapfrog the
23
+ primary's epoch and reject the primary as `below-git-floor`.
24
+
25
+ ## Decision-point inventory
26
+
27
+ - **loadSigningKey fallback branch** — ENOENT on canonical → try legacy name →
28
+ exists? read it : rethrow original. Both sides covered by unit tests.
29
+ - **isLeaseObserveOnly gate** — true (telegramPolling:false) → observe-only path
30
+ (no acquire/renew); false/undefined → unchanged acquire+renew path. Both sides
31
+ covered by unit tests (silent standby never acquires; normal machine does).
32
+
33
+ ## 1. Over-block
34
+
35
+ **What legitimate inputs does this change reject?** A silent standby
36
+ (`telegramPolling:false`) no longer acquires a lease at all — so it can never
37
+ become awake on its own. That is the INTENT: a muted standby that became "awake but
38
+ not serving" is the incoherent state we are removing. A machine that should be able
39
+ to take over sets `telegramPolling:true` (or unsets it), which is the unchanged
40
+ acquire path. The key fallback rejects nothing — it only ADDS a second lookup
41
+ location before the same rethrow.
42
+
43
+ ## 2. Under-block
44
+
45
+ **What does this still miss?** It does not add auto-failover for a silent standby
46
+ (out of scope — failover for it is a deliberate un-mute). It does not add a shared
47
+ CAS to `LocalLeaseStore`; the split-brain is avoided by not acquiring rather than
48
+ by making git-less acquisition safe (correct for the one-awake-machine model; the
49
+ active-active pool, which needs real shared CAS, remains git-backed). The key
50
+ fallback covers only the one known legacy name (`signing-private.pem`), matching
51
+ the lifeline loader's existing fallback — not an open-ended search.
52
+
53
+ ## 3. Level-of-abstraction fit
54
+
55
+ **Right layer?** Yes. The key fallback lives in the single `loadSigningKey()`
56
+ loader that every lease/transport consumer already calls (one chokepoint). The
57
+ observe-only gate lives in `MultiMachineCoordinator`'s two lease entry points
58
+ (`initializeLease`, `tickLease`) — the only places acquisition is decided — derived
59
+ from the same `telegramPolling` flag that already designates a silent standby
60
+ elsewhere. No new config surface, no duplicated rule.
61
+
62
+ ## 4. Signal vs authority compliance
63
+
64
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
65
+
66
+ No blocking authority added. The observe-only gate REMOVES an action (acquisition)
67
+ for a configured-silent machine; it never blocks a message or an operation. The key
68
+ fallback is a pure read-path widen with an unchanged terminal rethrow. Neither path
69
+ gates any user-facing or outbound behavior.
70
+
71
+ ## 5. Interactions
72
+
73
+ `loadSigningKey` feeds `HttpLeaseTransport` (lease broadcast/observe) and
74
+ machineAuth signing; widening it only makes a previously-throwing legacy-keyed
75
+ machine succeed — no consumer sees a different value, only a present-vs-throw
76
+ difference. The observe-only gate interacts with `reconcileRoleToLease` (still
77
+ called every tick, now inside each branch) and with `LeaseCoordinator.effectiveView`
78
+ (a standby with an empty `LocalLeaseStore` folds the primary's observed broadcast
79
+ cleanly). Idempotent: both paths produce the same result on repeated boots/ticks.
80
+
81
+ ## 6. External surfaces
82
+
83
+ No HTTP routes, no config defaults, no notifications, no Telegram. `/health`'s
84
+ `multiMachine.syncStatus.leaseHolder` will now correctly resolve to the primary on a
85
+ silent standby (the visible, intended effect). Pure code.
86
+
87
+ ## 7. Rollback cost
88
+
89
+ Low. Revert the PR: `loadSigningKey` returns to canonical-only (re-bricks a
90
+ legacy-keyed machine's lease setup) and the coordinator returns to always-acquire
91
+ (re-introduces the split-brain on git-less standbys). No schema, no migration, no
92
+ persisted state created by this change. The one deploy-time action (clear the mini's
93
+ stale self-issued `lease-local.json`) is a one-shot cleanup, not a rollback cost.
94
+
95
+ ## Conclusion
96
+
97
+ Minimal, additive-or-removing-an-action change with both decision sides unit-tested,
98
+ no new authority, no external surface beyond a corrected `/health` field, and a cheap
99
+ revert. It closes the final two layers blocking the live two-machine transfer.
100
+
101
+ ## Second-pass review (if required)
102
+
103
+ Not required — no new blocking authority, no destructive op, both decision branches
104
+ covered by tests, reversible. (Live two-machine transfer proof is the Tier-3 gate
105
+ that follows deploy.)
106
+
107
+ ## Evidence pointers
108
+
109
+ - `tests/unit/machine-identity.test.ts` — loadSigningKey legacy fallback + still-throws.
110
+ - `tests/unit/multi-machine-coordinator.test.ts` — observe-only never acquires/renews;
111
+ normal machine acquires; `isLeaseObserveOnly` reflects the flag.
112
+ - 109 related unit tests green (machine-identity, coordinator, LeaseCoordinator,
113
+ mesh-signing-key-resolution, multimachine-syncstatus); `tsc --noEmit` clean.
114
+ - Spec: `docs/specs/silent-standby-lease.md` (+ `.eli16.md`).
115
+ - `upgrades/NEXT.md` — upgrade guide.