instar 1.3.658 → 1.3.660

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 (37) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +52 -0
  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 +14 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/PostUpdateMigrator.d.ts +30 -0
  8. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  9. package/dist/core/PostUpdateMigrator.js +69 -0
  10. package/dist/core/PostUpdateMigrator.js.map +1 -1
  11. package/dist/core/SessionOwnershipRegistry.d.ts +14 -0
  12. package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -1
  13. package/dist/core/SessionOwnershipRegistry.js +9 -0
  14. package/dist/core/SessionOwnershipRegistry.js.map +1 -1
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +6 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/core/types.d.ts +22 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/core/types.js.map +1 -1
  21. package/dist/monitoring/StrandedTopicSentinel.d.ts +102 -0
  22. package/dist/monitoring/StrandedTopicSentinel.d.ts.map +1 -0
  23. package/dist/monitoring/StrandedTopicSentinel.js +216 -0
  24. package/dist/monitoring/StrandedTopicSentinel.js.map +1 -0
  25. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  26. package/dist/monitoring/guardManifest.js +17 -0
  27. package/dist/monitoring/guardManifest.js.map +1 -1
  28. package/dist/monitoring/strandedTopicDecision.d.ts +96 -0
  29. package/dist/monitoring/strandedTopicDecision.d.ts.map +1 -0
  30. package/dist/monitoring/strandedTopicDecision.js +160 -0
  31. package/dist/monitoring/strandedTopicDecision.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/data/builtin-manifest.json +19 -19
  34. package/upgrades/1.3.659.md +29 -0
  35. package/upgrades/1.3.660.md +27 -0
  36. package/upgrades/side-effects/stranded-inbound-self-heal.md +80 -0
  37. package/upgrades/side-effects/subagent-permission-allow-rules.md +109 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * strandedTopicDecision — the PURE, unit-testable decision core of the
3
+ * StrandedTopicSentinel (spec docs/specs/stranded-inbound-self-heal.md).
4
+ *
5
+ * A topic is STRANDED when its durable ownership record names a machine that is
6
+ * online-by-heartbeat but, persistently across rich beats, cannot serve the
7
+ * topic's channel — quota-walled (`quotaState.blocked === true`, channel-
8
+ * independent) OR adapter-disconnected (`machineServesChannel(...) === 'no'`).
9
+ * Inbound for that topic routes to the owner that cannot serve it, so it is
10
+ * silently dead while outbound still flows from a healthy lease-holder.
11
+ *
12
+ * This module computes the strand verdict ONLY. It mutates nothing: no
13
+ * ownership CAS, no pin write, no session kill. Fail-closed on EVERY
14
+ * uncertainty (missing field, stale beat, underivable scope, pool view
15
+ * unavailable) — an uncertainty can NEVER manufacture a strand, it routes to
16
+ * SKIP. The quota arm carries detection on its own when the adapter arm skips.
17
+ *
18
+ * Signal-vs-authority: pure read + a verdict object. No authority to misuse.
19
+ */
20
+ import { machineServesChannel, } from '../core/machineServesChannel.js';
21
+ /** Is this owner's rich beat genuine (present + fresh)? */
22
+ function richBeatAgeMs(cap, now) {
23
+ if (!cap.routerReceivedAt)
24
+ return undefined;
25
+ const t = Date.parse(cap.routerReceivedAt);
26
+ if (!Number.isFinite(t))
27
+ return undefined;
28
+ return now - t;
29
+ }
30
+ /**
31
+ * The pure strand evaluation for one tick. Synchronous, LLM-free, no I/O.
32
+ *
33
+ * Predicate per `active`, non-self-owned record:
34
+ * owner online
35
+ * AND ( quota arm: quotaState.blocked === true [channel-independent]
36
+ * OR adapter arm: machineServesChannel(servesChannels, scope) === 'no'
37
+ * ['unknown'/undefined-scope ⇒ SKIP that arm] )
38
+ * AND the condition has held ≥2 consecutive rich beats spanning ≥ dwellMs.
39
+ *
40
+ * Early no-op: < 2 machines OR !holdsLease ⇒ empty (and the map is dropped).
41
+ */
42
+ export function evaluateStrandedTopics(input) {
43
+ const { records, capacities, selfMachineId, holdsLease, prevStrandedSince, now, cfg } = input;
44
+ // Early no-op gates (spec step 1). A single-machine agent can't strand on a
45
+ // peer; a non-lease-holder is not the sole actor. Drop the dwell map so a
46
+ // demotion/scale-down doesn't carry stale anchors.
47
+ if (capacities.length < 2 || !holdsLease) {
48
+ return { strandedSet: [], cantAssessCount: 0, nextStrandedSince: {} };
49
+ }
50
+ const capById = new Map();
51
+ for (const c of capacities)
52
+ capById.set(c.machineId, c);
53
+ const strandedSet = [];
54
+ const nextStrandedSince = {};
55
+ let cantAssessCount = 0;
56
+ for (const rec of records) {
57
+ // Only active, peer-owned records are candidates.
58
+ if (rec.status !== 'active')
59
+ continue;
60
+ if (rec.ownerMachineId === selfMachineId)
61
+ continue;
62
+ const owner = capById.get(rec.ownerMachineId);
63
+ // Unknown owner / not in the pool view → can't assess → skip (fail-closed).
64
+ if (!owner)
65
+ continue;
66
+ // A dead owner is the existing Case C's job; this sentinel only covers the
67
+ // online-but-unable gap.
68
+ if (!owner.online)
69
+ continue;
70
+ // Require a genuine rich beat: present + fresh. A stale/undecodable beat
71
+ // fails closed (skip) and does NOT count toward the dwell.
72
+ const beatAgeMs = richBeatAgeMs(owner, now);
73
+ if (beatAgeMs === undefined || beatAgeMs > cfg.freshnessBoundMs)
74
+ continue;
75
+ // ── Arm (a): quota — channel-independent, the dominant incident case. ──
76
+ const quotaArm = owner.quotaState?.blocked === true;
77
+ // ── Arm (b): adapter — best-effort, channel-specific. ──
78
+ let adapterArm = false;
79
+ // A missing servesChannels on an online owner is the anti-blind-spot case:
80
+ // we cannot evaluate the adapter arm. If the quota arm ALSO can't fire
81
+ // (quotaState absent/undecided), this owner is genuinely unassessable.
82
+ const servesPresent = owner.servesChannels !== undefined;
83
+ let armBlind = false;
84
+ if (input.resolveScope) {
85
+ const scope = input.resolveScope(rec.sessionKey);
86
+ if (scope === undefined) {
87
+ // Underivable scope ⇒ SKIP this arm (not a strand).
88
+ }
89
+ else {
90
+ const serve = machineServesChannel(owner.servesChannels, scope);
91
+ if (serve === 'no')
92
+ adapterArm = true;
93
+ else if (serve === 'unknown') {
94
+ // 'unknown' (sparse/absent servesChannels) ⇒ SKIP this arm.
95
+ if (!servesPresent)
96
+ armBlind = true;
97
+ }
98
+ // 'yes' ⇒ owner can serve on this arm → not stranded here.
99
+ }
100
+ }
101
+ const unableToServe = quotaArm || adapterArm;
102
+ if (!unableToServe) {
103
+ // Anti-blind-spot: an online owner we could NOT assess at all — quota
104
+ // undecided AND the adapter arm blinded by a missing servesChannels —
105
+ // increments the can't-assess count (spec step 4). A topic with a usable
106
+ // 'yes' or a derivable scope is genuinely assessed (not blind).
107
+ const quotaUndecided = owner.quotaState === undefined;
108
+ if (quotaUndecided && armBlind)
109
+ cantAssessCount++;
110
+ continue;
111
+ }
112
+ // ── Persistence (dwell) — anchor on the FIRST qualifying beat. ──
113
+ const prevSince = prevStrandedSince[rec.sessionKey];
114
+ const strandedSince = typeof prevSince === 'number' ? prevSince : now;
115
+ nextStrandedSince[rec.sessionKey] = strandedSince;
116
+ // The condition must have held ≥ dwellMs (which, given the ≥1 prior beat that
117
+ // set the anchor, spans ≥2 consecutive rich beats). A first-beat strand has
118
+ // dwell 0 < dwellMs → recorded in the map but NOT yet emitted.
119
+ if (now - strandedSince < cfg.dwellMs)
120
+ continue;
121
+ const reason = quotaArm ? 'quota' : 'adapter';
122
+ strandedSet.push({
123
+ sessionKey: rec.sessionKey,
124
+ ownerMachineId: rec.ownerMachineId,
125
+ reason,
126
+ strandedSince,
127
+ ownerBeatAgeMs: beatAgeMs,
128
+ servablePeerExists: servablePeerExists(rec, capacities, selfMachineId, input.resolveScope),
129
+ });
130
+ }
131
+ return { strandedSet, cantAssessCount, nextStrandedSince };
132
+ }
133
+ /**
134
+ * Whether SOME online machine could serve the topic's channel — narrowly "online
135
+ * AND not quota-blocked AND machineServesChannel(...) === 'yes'" (the same
136
+ * PlacementExecutor filter, so detector + placement agree). Deliberately NOT a
137
+ * "safe failover target": it vets no pin policy, lease/router readiness, secrets,
138
+ * or session limits — v1 never fails over, so this only tells the operator
139
+ * "somewhere could serve this" vs "nowhere can (fleet-wide wall)". When the scope
140
+ * can't be derived, a not-quota-blocked online peer counts (quota-arm semantics).
141
+ */
142
+ function servablePeerExists(rec, capacities, selfMachineId, resolveScope) {
143
+ const scope = resolveScope?.(rec.sessionKey);
144
+ for (const c of capacities) {
145
+ if (c.machineId === rec.ownerMachineId)
146
+ continue;
147
+ if (!c.online)
148
+ continue;
149
+ if (c.quotaState?.blocked === true)
150
+ continue;
151
+ if (scope === undefined) {
152
+ // No channel scope → quota-arm semantics: a healthy peer suffices.
153
+ return true;
154
+ }
155
+ if (machineServesChannel(c.servesChannels, scope) === 'yes')
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+ //# sourceMappingURL=strandedTopicDecision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strandedTopicDecision.js","sourceRoot":"","sources":["../../src/monitoring/strandedTopicDecision.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.658",
3
+ "version": "1.3.660",
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-06-24T23:28:12.769Z",
5
- "instarVersion": "1.3.658",
4
+ "generatedAt": "2026-06-25T07:17:11.166Z",
5
+ "instarVersion": "1.3.660",
6
6
  "entryCount": 202,
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
14
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
23
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
32
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
41
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
50
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
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": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
59
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:self-stop-guard": {
@@ -65,7 +65,7 @@
65
65
  "domain": "coherence",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/self-stop-guard.js",
68
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
68
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:post-action-reflection": {
@@ -74,7 +74,7 @@
74
74
  "domain": "evolution",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
77
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
77
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:external-communication-guard": {
@@ -83,7 +83,7 @@
83
83
  "domain": "safety",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
86
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
86
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-collector": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
95
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
95
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:scope-coherence-checkpoint": {
@@ -101,7 +101,7 @@
101
101
  "domain": "coherence",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
104
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
104
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:free-text-guard": {
@@ -110,7 +110,7 @@
110
110
  "domain": "safety",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
113
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
113
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
122
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
122
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:claim-intercept-response": {
@@ -128,7 +128,7 @@
128
128
  "domain": "coherence",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
131
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
131
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:stop-gate-router": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
140
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
140
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "hook:auto-approve-permissions": {
@@ -146,7 +146,7 @@
146
146
  "domain": "safety",
147
147
  "sourcePath": "src/core/PostUpdateMigrator.ts",
148
148
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
149
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
149
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
150
150
  "since": "2025-01-01"
151
151
  },
152
152
  "job:health-check": {
@@ -1562,7 +1562,7 @@
1562
1562
  "type": "subsystem",
1563
1563
  "domain": "updates",
1564
1564
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1565
- "contentHash": "25307559192c0fd8e51ea087b3ba9ee4473424a330518b8899ce06bb4cdcdb80",
1565
+ "contentHash": "4c5fa1d050d577392ce8e682974c6d495df23727ff7622a0ed01ff8fd6663c48",
1566
1566
  "since": "2025-01-01"
1567
1567
  },
1568
1568
  "subsystem:scheduler": {
@@ -0,0 +1,29 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ A new pure-signal monitoring sentinel, `StrandedTopicSentinel`, ships behind a DARK dev-gate (`monitoring.strandedTopicSentinel.enabled`, omitted from ConfigDefaults ⇒ live on a development agent, dark on the fleet). It closes a real, proven gap: a Telegram/Slack topic's durable ownership record can name a machine that is **online-by-heartbeat but cannot serve** — quota-walled (`quotaState.blocked`) or adapter-disconnected (`servesChannels` omits the topic's channel) — while a healthy machine holds the lease. Inbound for that topic routes to the owner that can't answer, so it is **silently dead** (outbound still flows from the healthy machine — the "my replies send but the user's messages never arrive" split). The existing `OwnershipReconciler` only force-claims a *provably-DEAD* owner (offline ≥180s) and only iterates *pinned* topics, so a walled-but-online owner defers forever and an unpinned strand is never even considered.
9
+
10
+ The sentinel scans the in-memory ownership cache against the replicated machine-pool view each tick (default 60s), applies a fail-closed predicate with a persistence gate (the unable-to-serve condition must hold ≥2 rich beats over ≥`dwellMs`, default 30s, so a transient quota/adapter blip can't trip it), and raises ONE aggregated `agent-health` attention item per (owner-machine, stranding-window) — never one-per-topic — plus a separate LOW "can't-assess" item if a heartbeat/schema regression blinds it. It is **lease-holder-only** (so peers don't double-report), a **strict no-op on a single-machine agent**, registers in the GuardRegistry (`GET /guards`), and **MUTATES NOTHING** — no ownership CAS, no pin write, no session kill.
11
+
12
+ This is the deliberately-safe HALF of the fix. Spec-convergence review (3 rounds) established that the *instinctive* auto-failover is unsafe with today's primitives — there is no per-topic remote-liveness signal, the reachability signal is self-reported and up to `failoverThresholdMs` stale, and there's no hysteresis — so a wrong failover would yank a live conversation mid-reply, which is strictly worse than the bug. The auto-failover is therefore deferred to a tracked v2 (`CMT-1786`) whose prerequisites are each named in the spec.
13
+
14
+ ## What to Tell Your User
15
+
16
+ Nothing visible day-to-day — it's off everywhere except the development machine until it's soaked. The eventual benefit: a conversation whose messages are silently going to a machine that can't answer them gets caught within about a minute, instead of staying invisible until you notice you're being ignored.
17
+
18
+ ## Summary of New Capabilities
19
+
20
+ - `monitoring.strandedTopicSentinel.enabled` (dev-gated dark flag): a pure-signal sentinel that detects an online-but-unable-to-serve owner stranding a topic's inbound and raises one aggregated `agent-health` attention item; registered on `GET /guards`.
21
+ - `evaluateStrandedTopics(...)` (pure helper): the unit-testable, fail-closed strand decision (quota arm + best-effort adapter arm + dwell persistence + reconciliation).
22
+ - Deferred (CMT-1786): the auto-failover that actually re-points a stranded topic to a healthy server, plus its named prerequisites (per-topic remote liveness signal, hysteresis/cooldown, claim-time re-assertion, atomic CAS+pin, reason-stamped nonce, OwnershipReconciler unification) and a live userbot harness for real-Telegram UX regression testing.
23
+
24
+ ## Evidence
25
+
26
+ - **Reproduction (the live incident, 2026-06-24):** 17 of 25 topics were owned by an offline/quota-walled Mac Mini; every one had silently-dead Telegram inbound; every code review and CI gate was green the whole time; the wedge surfaced only when the operator reported missing messages. `GET /pool/placement?topic=N` showed `owner=Mini, pendingReplacement:true` — the signal was present and unmonitored. The only recovery was a human hand-editing `.instar/ownership/local/<topicId>.json`.
27
+ - **Before → after:** before, this class of breakage was invisible to all automated checks until a user got bitten. After (dev-gated), the moment a topic's owner is persistently online-but-unable-to-serve, ONE attention item fires within ~`dwellMs + tickMs` (≈ a minute or two on a healthy heartbeat cadence) naming the stranded topics, the walled owner + reason, whether a servable peer exists, and the signal's staleness. Flag off (the fleet) is byte-identical to today (no sentinel).
28
+ - **Safety verified:** the sentinel is pure signal — it raises an attention item and writes nothing. The fail-closed predicate (missing field / stale beat / underivable scope / pool view unavailable ⇒ SKIP) means an uncertainty can never manufacture a strand; the lease-holder-sole-actor + single-machine no-op prevent duplicate items; the aggregated item rides the existing `AttentionTopicGuard` flood ceiling.
29
+ - **Tests:** 29 unit tests on the pure decision core (both sides of every boundary — quota arm, three-valued adapter arm, persistence/dwell, fail-closed skips, `strandedSince` reconciliation, can't-assess counting, servable-peer, single-machine/non-lease-holder no-op, sync/LLM-free invariant) + GuardRegistry registration + a `/guards`-posture integration check. `tsc --noEmit` clean. Multi-angle spec review (6 internal reviewers + GPT-5.5 cross-model, 3 rounds) drove the design from an unsafe auto-failover to this safe detector and caught a critical predicate bug (the three-valued `machineServesChannel` would have made a `!fn(...)` detector never fire).
@@ -0,0 +1,27 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed the "session paused" hang: an autonomous session would freeze indefinitely the first time a helper sub-agent (spawned via the Task/Agent tool) ran a shell command. Root cause: a sub-agent does NOT inherit the parent session's `--dangerously-skip-permissions` MODE — confirmed against Claude Code docs, it inherits only the permission RULES from `.claude/settings.json`. With no allow-rules configured, the sub-agent's first `Bash` call surfaced the interactive "This command requires approval" dialog, and in an unattended run there was no human to answer it, so the session sat modal-blocked forever (indistinguishable from "paused"). It affected the whole fleet because every agent shipped with an empty permissions block.
9
+
10
+ `PostUpdateMigrator.ensurePermissionAllowRules()` now runs inside `migrateSettings()` and adds a `permissions.allow` list for the built-in tools a sub-agent uses (`Bash`, `Read`, `Edit`, `Write`, `Glob`, `Grep`, `Task`, `NotebookEdit`, `WebFetch`, `WebSearch`, `TodoWrite`). Sub-agents inherit permission rules, so the allow-rule is the structural lever that reliably applies — unlike the existing `PermissionRequest` auto-approve hook, which does not reliably fire for sub-agent calls. The migration is idempotent (adds only missing tool names), never touches the operator's `deny`/`ask` lists, and deliberately excludes MCP tools (`mcp__*`) so external/network operations keep their external-operation-gate approval posture. Because it rides `migrateSettings()`, every existing agent picks it up on its next update and every new agent gets it at init (Migration Parity).
11
+
12
+ Safety is unchanged: the allow-rules only skip the duplicative human-in-the-loop prompt. The PreToolUse guard chain (`dangerous-command-guard`, `external-operation-gate`, `external-communication-guard`, `self-stop-guard`, …) still runs on every tool call and can still block — and the same `migrateSettings()` pass that adds the allow-rules also wires that guard chain (via `ensureInstarBashPreToolUseHooks`, before the allow-rules), so the guards and the allow-rules always arrive together.
13
+
14
+ ## What to Tell Your User
15
+
16
+ If your agent ever seemed to "pause" mid-task during an unattended/autonomous run — silently stuck and only resuming when you messaged it — this is a common cause: a helper it spawned hit an approval prompt nobody was there to answer. After this update (and a session restart so the new settings load), helper tasks no longer stall on those prompts, so autonomous runs keep moving. Your safety guards are unchanged — every command still passes the same pre-action checks; the only thing removed is the approval pop-up that had no one to answer it.
17
+
18
+ ## Summary of New Capabilities
19
+
20
+ - `PostUpdateMigrator.ensurePermissionAllowRules()` — adds inherited `permissions.allow` rules for sub-agent built-in tools to every agent's `.claude/settings.json` via the migration path (new agents at init, existing agents on update). Idempotent, non-clobbering of operator `deny`/`ask`, and scoped to local tools (MCP stays gated by the external-operation-gate).
21
+
22
+ ## Evidence
23
+
24
+ - **Reproduction (2026-06-24):** an unattended autonomous run sat silent for ~68 min; a screenshot showed the session modal-blocked on a sub-agent's `Bash` "This command requires approval" dialog. Confirmed the same class on a second agent (AI Guy). Settings inspection showed the `permissions` block was entirely absent (no allow-rules for sub-agents to inherit).
25
+ - **Mechanism confirmed:** Claude Code docs — sub-agents inherit permission RULES but not the permission MODE; PreToolUse hooks run BEFORE permission-rule evaluation and a hook exiting 2 blocks the call even for an allowed tool (so allow-rules cannot weaken the guards).
26
+ - **Tests:** `tests/unit/PostUpdateMigrator-permissionAllowRules.test.ts` — 6 tests: adds all sub-agent tools when absent; includes Bash; does NOT blanket-allow MCP; preserves operator allow entries with no duplicates; never touches deny/ask; idempotent on second pass. All passing. Adjacent `PostUpdateMigrator-cleanupPeriodDays.test.ts` (same migrateSettings path) still green. `npm run build` green.
27
+ - **Second-pass review:** independent reviewer concurred; verified the safety argument against the docs and confirmed the guard chain is wired in the same migration pass, before the allow-rules.
@@ -0,0 +1,80 @@
1
+ # Side-Effects Review — StrandedTopicSentinel (online-but-unable-to-serve inbound detector)
2
+
3
+ **Version / slug:** `stranded-inbound-self-heal`
4
+ **Date:** `2026-06-24`
5
+ **Author:** Echo (autonomous)
6
+ **Second-pass reviewer:** not-required (Tier 2; dev-gated, pure-signal monitoring sentinel — no mutation, no fleet runtime path, no operator surface)
7
+
8
+ ## Summary of the change
9
+
10
+ Adds a new dark-gated, pure-signal monitoring sentinel, `StrandedTopicSentinel`, that detects a Telegram/Slack topic whose durable ownership record names a machine that is **online-by-heartbeat but cannot serve** (quota-walled `quotaState.blocked`, or adapter-disconnected `servesChannels` omits the topic's channel) while a healthy machine holds the lease — so inbound for that topic is silently dead. It raises ONE aggregated `agent-health` attention item per (owner-machine, stranding-window) and a separate LOW "can't-assess" item if a heartbeat/schema regression blinds it. It **MUTATES NOTHING** (no ownership CAS, no pin write, no session kill); its sole output is an advisory attention item. The instinctive auto-failover was deferred to a tracked v2 (CMT-1786) because spec-convergence review proved it unsafe with today's primitives (no per-topic remote liveness, self-reported stale reachability, no hysteresis) — a wrong failover drops a live conversation, strictly worse than the bug.
11
+
12
+ Files added:
13
+ - `src/monitoring/strandedTopicDecision.ts` — the PURE, unit-testable decision core (`evaluateStrandedTopics`): fail-closed predicate (quota arm + best-effort adapter arm), dwell persistence, `strandedSince` reconciliation, `servablePeerExists`.
14
+ - `src/monitoring/StrandedTopicSentinel.ts` — the sentinel (tick loop, per-owner stranding-window discipline, attention emission, `guardStatus`).
15
+ - `tests/unit/stranded-topic-sentinel.test.ts` (29), `tests/integration/stranded-topic-guard-posture.test.ts` (4), `tests/e2e/stranded-topic-guards-lifecycle.test.ts` (2).
16
+ - `docs/specs/stranded-inbound-self-heal.md` (+ `.eli16.md` + convergence report), `upgrades/next/stranded-inbound-self-heal.md`.
17
+
18
+ Files modified:
19
+ - `src/core/types.ts` — `MonitoringConfig.strandedTopicSentinel?` config type.
20
+ - `src/config/ConfigDefaults.ts` — the `strandedTopicSentinel` defaults block (tickMs/dwellMs/freshnessBoundMs/clearAfterTicks); `enabled` OMITTED (dev-gate decides).
21
+ - `src/core/devGatedFeatures.ts` — `DEV_GATED_FEATURES` entry (pure-signal justification).
22
+ - `src/monitoring/guardManifest.ts` — `GUARD_MANIFEST` entry (`expectRuntime`, `expectedTickMs`).
23
+ - `src/core/SessionOwnershipRegistry.ts` — added `all()` (delegates to `store.all?.()`; reads empty for a store lacking it) so the registry the server holds can be scanned.
24
+ - `src/commands/server.ts` — wires the sentinel (late, after pool boot; `selfMachineId` via a lazy getter to avoid the #1190 boot-ordering null-capture; `raiseAttention` → `telegram.createAttentionItem`; GuardRegistry registration).
25
+ - `tests/unit/lint-dev-agent-dark-gate.test.ts` — hand-shifted the golden line-map keys +14 (my ConfigDefaults block added 14 lines, NO new `enabled:` literal).
26
+
27
+ ## Decision-point inventory
28
+
29
+ - **Added**: the strand predicate (`evaluateStrandedTopics`) — a READ-ONLY verdict, not a gate. It decides whether to RAISE an attention item; it authorizes no mutation. Fail-closed on every uncertainty.
30
+ - **Added**: the lease-holder-sole-actor + single-machine early-no-op gates — decide WHETHER THIS MACHINE evaluates at all (so peers don't double-report). Observe-only.
31
+ - No agent-to-user or ownership mutation decision point is added. This is pure-signal monitoring.
32
+
33
+ ## 1. Over-block
34
+
35
+ None — the change blocks nothing. It is observe-only. The only "false positive" risk is a spurious attention item, mitigated by: ≥2-rich-beat dwell persistence (kills transient blips), missing-field/stale-beat/underivable-scope ⇒ SKIP (fail-closed — an uncertainty can never manufacture a strand), and lease-holder-only emission (no duplicate items across machines). A spurious item is cheap and rides the existing `AttentionTopicGuard` flood ceiling.
36
+
37
+ ## 2. Under-block
38
+
39
+ By design, v1 only DETECTS — it does not remediate; the operator/agent acts on the alert (manual remediation documented in the spec; auto-failover is the tracked v2). The adapter arm is best-effort: for Telegram the channel scope is shared adapter config so the adapter arm rarely fires (the quota arm carries the Telegram case — the actual incident); the adapter arm's real value is Slack per-workspace. A dead (offline) owner is intentionally NOT covered (that is the existing OwnershipReconciler Case C's job) — this sentinel covers only the online-but-unable gap.
40
+
41
+ ## 3. Level-of-abstraction fit
42
+
43
+ Right layer: a `src/monitoring/` sentinel, mirroring the established pattern (ContextWedgeSentinel/ResumeQueue) — deps-injected, `lastTickAt` liveness, GuardRegistry registration, dark-gate via DEV_GATED_FEATURES. The pure decision is extracted to a separate module so it is unit-testable without tmux/HTTP.
44
+
45
+ ## 4. Signal vs authority compliance
46
+
47
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md). FULLY compliant — this is the textbook SIGNAL side. The sentinel holds NO authority: it raises an advisory attention item and writes nothing else (no CAS, no pin, no kill, no direct user message). The deliberate review-driven retreat from a mutating design to a pure detector is exactly the standard's prescription (the dangerous mutation is deferred until the primitives that make it safe exist).
48
+
49
+ ## 5. Interactions
50
+
51
+ - **OwnershipReconciler (Case C):** disjoint by trigger — Case C covers offline-DEAD+pinned owners; this covers online-but-UNABLE owners (which Case C explicitly defers as `deferredNoEvidence`). No shared writer (this sentinel writes nothing).
52
+ - **AttentionTopicGuard:** the aggregated item rides the existing flood ceiling; NORMAL/LOW priority never bypasses it.
53
+ - **GuardRegistry / GET /guards:** registers so a silently-disabled instance is visible (the exact failure class this feature exists to surface).
54
+ - **Host spawn cap / event loop:** the tick is synchronous, LLM-free, acquires NO spawn-cap slot (asserted by test), and does NO synchronous peer probe — it reads the in-memory ownership cache + replicated heartbeat view only.
55
+
56
+ ## 6. External surfaces
57
+
58
+ The only external-visible effect is an `agent-health` attention item (and a LOW can't-assess item) when a strand is detected — calm, aggregated, deduped, flood-ceiling-bounded, with signal-staleness disclosed in the text. No new HTTP route, no dashboard change, no message to a user channel.
59
+
60
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
61
+
62
+ No operator surface — not applicable. This change touches no dashboard renderer/markup, approval page, or grant/revoke/secret-drop form. Its only operator-facing output is a plain-language attention item whose text leads with the situation ("inbound for topics X is going to <machine>, which can't serve them; <machine> can"), de-emphasizes identifiers, and discloses signal staleness.
63
+
64
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
65
+
66
+ **machine-local BY DESIGN.** The detection is per-machine observability over the REPLICATED heartbeat machine-pool view + the local in-memory ownership cache; it writes no durable state, so there is nothing to replicate or strand on a topic transfer. Exactly ONE machine reports per strand: the lease-holder is the sole actor (`syncStatus.holdsLease`), so peers stay observe-only and the user hears one voice. A single-machine agent is a strict no-op (`machines().length < 2` early return — nothing can be stranded on a peer that doesn't exist).
67
+
68
+ ## 8. Rollback cost
69
+
70
+ Trivial and instant. The feature ships dark on the fleet (`monitoring.strandedTopicSentinel.enabled` omitted ⇒ resolves dark off a dev agent); flag-off is byte-identical to today (no sentinel constructed). On a dev agent, set `monitoring.strandedTopicSentinel.enabled: false` to force-dark. No durable state is written, so there is nothing to clean up on back-out.
71
+
72
+ ## Conclusion
73
+
74
+ A safe, dark-gated, pure-signal detector that makes a real, proven, previously-invisible cross-machine inbound wedge loud within a bounded window — the deliberately-safe half of the fix, with the dangerous auto-failover deferred behind named, tracked prerequisites (CMT-1786). All three test tiers green; the design passed 3 spec-convergence rounds (6 internal reviewers + GPT-5.5 cross-model).
75
+
76
+ ## Evidence pointers
77
+
78
+ - Spec + convergence report: `docs/specs/stranded-inbound-self-heal.md`, `docs/specs/reports/stranded-inbound-self-heal-convergence.md`.
79
+ - Live incident (the reproduction): 2026-06-24, 17 of 25 topics stranded on a quota-walled Mac Mini; inbound silently dead; surfaced only when the operator reported missing messages.
80
+ - Tests: unit (29) `tests/unit/stranded-topic-sentinel.test.ts`; integration (4) `tests/integration/stranded-topic-guard-posture.test.ts`; e2e (2) `tests/e2e/stranded-topic-guards-lifecycle.test.ts`.
@@ -0,0 +1,109 @@
1
+ # Side-Effects Review — Subagent permission allow-rules (the "session paused" fix)
2
+
3
+ **Version / slug:** `subagent-permission-allow-rules`
4
+ **Date:** `2026-06-24`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `general-purpose reviewer subagent (Phase 5 — touches permission allow/deny surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds `PostUpdateMigrator.ensurePermissionAllowRules()` and wires it into `migrateSettings()` so every agent's `.claude/settings.json` gains a `permissions.allow` list for the built-in tools a Task/Agent-spawned sub-agent uses (`Bash`, `Read`, `Edit`, `Write`, `Glob`, `Grep`, `Task`, `NotebookEdit`, `WebFetch`, `WebSearch`, `TodoWrite`). Root cause: a sub-agent does NOT inherit the parent session's `--dangerously-skip-permissions` MODE — only the permission RULES from settings.json. With no allow-rules, a sub-agent's first Bash call surfaces the interactive approval dialog and an unattended autonomous session freezes on it forever (the "session paused" bug; reproduced on this agent and AI Guy). Files: `src/core/PostUpdateMigrator.ts` (new method + one call site in `migrateSettings`), `tests/unit/PostUpdateMigrator-permissionAllowRules.test.ts` (6 tests).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `migrateSettings() → ensurePermissionAllowRules()` — **add** — populates `permissions.allow` for subagent tools, set-if-missing per tool name; idempotent; never touches `deny`/`ask`.
15
+ - The interactive permission PROMPT for sub-agent local-tool calls — **remove** (only the human-in-the-loop prompt; the programmatic PreToolUse guards are untouched).
16
+
17
+ ---
18
+
19
+ ## 1. Over-block
20
+
21
+ **What legitimate inputs does this change reject that it shouldn't?**
22
+
23
+ No new block surface — this change only ADDS allow-rules, it never adds a deny. It cannot reject anything that worked before; the worst case direction is "allows too much," covered under §2. Over-block not applicable.
24
+
25
+ ---
26
+
27
+ ## 2. Under-block
28
+
29
+ **What failure modes does this still miss / does it allow too much?**
30
+
31
+ The allow-rules skip the interactive prompt for the listed local tools, so a human is no longer asked before a sub-agent runs e.g. an arbitrary Bash command. This is intentional and acceptable because the real safety is the PreToolUse guard chain (`dangerous-command-guard.sh`, `external-operation-gate.js`, `external-communication-guard.js`, `self-stop-guard.js`) which runs on EVERY tool call regardless of allow-rules — the prompt was duplicative friction, not the protective layer. Deliberately NOT covered: MCP tools (`mcp__*`) are left out of the allow list so external/network operations keep their external-operation-gate plan/approval posture. A sub-agent calling an MCP tool could still prompt — but MCP calls are not the wedge this fixes, and broadening to MCP would weaken the external-op gate's intended approval step.
32
+
33
+ ---
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ **Is this at the right layer?**
38
+
39
+ Yes. `permissions.allow` in settings.json is the exact Claude Code mechanism that sub-agents inherit (per docs), so the fix lives at the layer that actually controls the behavior. The alternative — the `PermissionRequest` auto-approve hook — sits at a layer that does not reliably reach sub-agent calls, which is why it failed. Putting the rule in `migrateSettings()` (rather than only `init`) is the correct layer for Migration Parity: it reaches both new agents (init → refreshHooksAndSettings → migrateSettings) and the existing fleet (update → migrateSettings) through one code path, mirroring the established `cleanupPeriodDays` migration.
40
+
41
+ ---
42
+
43
+ ## 4. Signal vs authority compliance
44
+
45
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
46
+
47
+ - [x] No — this change has no block/allow surface that adds brittle blocking authority. It REMOVES a human-in-the-loop prompt while leaving every existing smart/programmatic gate (the PreToolUse guards) fully in force.
48
+
49
+ This change adds zero new decision authority. It does not inspect command content, classify, or gate — it declares static allow-rules so the existing PreToolUse authorities are the sole deciders. There is no brittle detector holding block power here.
50
+
51
+ ---
52
+
53
+ ## 5. Interactions
54
+
55
+ - **Shadowing:** none. Allow-rules and the PreToolUse hooks are orthogonal — an allow-rule skips the prompt; the hooks still fire and can still block (exit 2 / deny). The allow-rule cannot shadow a guard.
56
+ - **Double-fire:** the `PermissionRequest` auto-approve hook (`ensurePermissionAutoApprove`) remains in place as defense-in-depth. With allow-rules present, the prompt typically never arises, so the hook is a redundant backstop, not a conflicting one — both pushing toward "allow" is harmless.
57
+ - **Races:** none. `migrateSettings()` is a synchronous, idempotent file patch run at init/update; it shares no runtime state with live sessions. The new rules take effect only on the next session start (settings load once at spawn).
58
+ - **Feedback loops:** none.
59
+
60
+ ---
61
+
62
+ ## 6. External surfaces
63
+
64
+ - Other agents on the machine: unaffected at runtime — this only changes a file each agent reads at its own session start.
65
+ - Install base: every existing agent gains the allow-rules on its next update; new agents at init. This is the intended fleet-wide fix.
66
+ - External systems: none. No Telegram/Slack/GitHub/Cloudflare surface changes.
67
+ - Persistent state: writes `permissions.allow` into each agent's local `.claude/settings.json`. Idempotent, additive, reversible.
68
+ - **Operator surface (Mobile-Complete Operator Actions):** no operator-facing action added or changed — this is internal config plumbing. Not applicable.
69
+
70
+ ---
71
+
72
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
73
+
74
+ No operator surface — this change touches no dashboard renderer, approval page, or grant/revoke/secret-drop form. Not applicable.
75
+
76
+ ---
77
+
78
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
79
+
80
+ **machine-local BY DESIGN.** `.claude/settings.json` is a per-machine file that Claude Code reads from local disk at session start; the permission rules must exist on whichever machine actually spawns the session. There is no cross-machine state to replicate — each machine's `PostUpdateMigrator` runs the same idempotent migration locally on its own update/init, so the fleet converges to identical allow-rules without any replication path. It emits no user-facing notices (no one-voice concern), holds no durable cross-topic state (nothing strands on topic transfer), and generates no URLs.
81
+
82
+ ---
83
+
84
+ ## 8. Rollback cost
85
+
86
+ Pure additive config migration. Back-out: revert the code change and ship as the next patch — new/updated agents simply stop gaining the allow-rules. Already-migrated agents keep a `permissions.allow` block in their settings.json; it is harmless (it only ever skipped a prompt the operator didn't want in an unattended run) and an operator can delete it by hand if desired. No data migration, no agent-state repair, no user-visible regression during the rollback window.
87
+
88
+ ---
89
+
90
+ ## Conclusion
91
+
92
+ The review produced no design changes. The fix is minimal, additive, idempotent, and scoped to the exact mechanism (inherited permission rules) that controls sub-agent prompting. It preserves all real safety (the PreToolUse guard chain) and only removes the human-in-the-loop prompt that was freezing unattended autonomous runs. Clear to ship pending second-pass concurrence.
93
+
94
+ ---
95
+
96
+ ## Second-pass review (if required)
97
+
98
+ **Reviewer:** general-purpose reviewer subagent
99
+ **Independent read of the artifact: concur**
100
+
101
+ Concur with the review. The reviewer independently verified, against Claude Code docs, that PreToolUse hooks run BEFORE permission-rule evaluation and a hook exiting 2 blocks the call even when the tool is in `permissions.allow` — the docs describe exactly this pattern (Bash in allow + a PreToolUse guard) as the recommended architecture, so the safety argument holds. Key corroboration: the SAME `migrateSettings()` pass calls `ensureInstarBashPreToolUseHooks()` (which registers the full `INSTAR_BASH_PRETOOLUSE_HOOKS` guard chain) BEFORE `ensurePermissionAllowRules()`, so any agent that gains the allow-rules gains the blocking guards in the same migration — no race, no window where allow-rules exist without the guards. Idempotency, non-clobbering of operator deny/ask, mcp__* exclusion, and the absence of any shared-array mutation were all confirmed. Non-blocking note: the allow list is a fixed in-code array; a future new built-in subagent tool won't be covered until the list is updated — benign failure direction (it would prompt, not over-allow).
102
+
103
+ ---
104
+
105
+ ## Evidence pointers
106
+
107
+ - `tests/unit/PostUpdateMigrator-permissionAllowRules.test.ts` — 6 tests: adds all subagent tools when absent; includes Bash; does NOT blanket-allow MCP; preserves operator allow entries + no dupes; never touches deny/ask; idempotent on second pass. All passing.
108
+ - Adjacent regression: `tests/unit/PostUpdateMigrator-cleanupPeriodDays.test.ts` still green (same `migrateSettings` path).
109
+ - `npm run build` green on the worktree.