instar 0.28.102 → 0.28.103

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 (247) hide show
  1. package/.claude/hooks/instar/before-prompt-recall.js +90 -0
  2. package/dist/cli.js +28 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +14 -1
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/jobMigrate.d.ts +46 -0
  9. package/dist/commands/jobMigrate.d.ts.map +1 -0
  10. package/dist/commands/jobMigrate.js +292 -0
  11. package/dist/commands/jobMigrate.js.map +1 -0
  12. package/dist/commands/server.d.ts.map +1 -1
  13. package/dist/commands/server.js +88 -42
  14. package/dist/commands/server.js.map +1 -1
  15. package/dist/core/BackupManager.d.ts +27 -1
  16. package/dist/core/BackupManager.d.ts.map +1 -1
  17. package/dist/core/BackupManager.js +56 -1
  18. package/dist/core/BackupManager.js.map +1 -1
  19. package/dist/core/GitStateManager.d.ts +8 -0
  20. package/dist/core/GitStateManager.d.ts.map +1 -1
  21. package/dist/core/GitStateManager.js +31 -1
  22. package/dist/core/GitStateManager.js.map +1 -1
  23. package/dist/core/MigratorStepEngine.d.ts +170 -0
  24. package/dist/core/MigratorStepEngine.d.ts.map +1 -0
  25. package/dist/core/MigratorStepEngine.js +352 -0
  26. package/dist/core/MigratorStepEngine.js.map +1 -0
  27. package/dist/core/PostUpdateMigrator.d.ts +76 -0
  28. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  29. package/dist/core/PostUpdateMigrator.js +252 -1
  30. package/dist/core/PostUpdateMigrator.js.map +1 -1
  31. package/dist/core/PreCompactionFlush.d.ts +101 -0
  32. package/dist/core/PreCompactionFlush.d.ts.map +1 -0
  33. package/dist/core/PreCompactionFlush.js +282 -0
  34. package/dist/core/PreCompactionFlush.js.map +1 -0
  35. package/dist/core/PromptBuildRecall.d.ts +97 -0
  36. package/dist/core/PromptBuildRecall.d.ts.map +1 -0
  37. package/dist/core/PromptBuildRecall.js +152 -0
  38. package/dist/core/PromptBuildRecall.js.map +1 -0
  39. package/dist/core/selectIntelligenceProvider.d.ts +41 -0
  40. package/dist/core/selectIntelligenceProvider.d.ts.map +1 -0
  41. package/dist/core/selectIntelligenceProvider.js +89 -0
  42. package/dist/core/selectIntelligenceProvider.js.map +1 -0
  43. package/dist/lifeline/ServerSupervisor.d.ts +183 -0
  44. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  45. package/dist/lifeline/ServerSupervisor.js +443 -0
  46. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  47. package/dist/memory/NativeModuleHealer.d.ts +89 -0
  48. package/dist/memory/NativeModuleHealer.d.ts.map +1 -1
  49. package/dist/memory/NativeModuleHealer.js +371 -1
  50. package/dist/memory/NativeModuleHealer.js.map +1 -1
  51. package/dist/memory/SemanticMemory.d.ts +131 -0
  52. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  53. package/dist/memory/SemanticMemory.js +315 -0
  54. package/dist/memory/SemanticMemory.js.map +1 -1
  55. package/dist/messaging/DeliveryRetryManager.d.ts +60 -0
  56. package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -1
  57. package/dist/messaging/DeliveryRetryManager.js +104 -0
  58. package/dist/messaging/DeliveryRetryManager.js.map +1 -1
  59. package/dist/monitoring/DegradationReporter.d.ts +164 -0
  60. package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
  61. package/dist/monitoring/DegradationReporter.js +309 -1
  62. package/dist/monitoring/DegradationReporter.js.map +1 -1
  63. package/dist/monitoring/ErrorCodeExtractor.d.ts +69 -0
  64. package/dist/monitoring/ErrorCodeExtractor.d.ts.map +1 -0
  65. package/dist/monitoring/ErrorCodeExtractor.js +131 -0
  66. package/dist/monitoring/ErrorCodeExtractor.js.map +1 -0
  67. package/dist/monitoring/PromptGate.d.ts +34 -0
  68. package/dist/monitoring/PromptGate.d.ts.map +1 -1
  69. package/dist/monitoring/PromptGate.js +81 -1
  70. package/dist/monitoring/PromptGate.js.map +1 -1
  71. package/dist/monitoring/Redactor.d.ts +58 -0
  72. package/dist/monitoring/Redactor.d.ts.map +1 -0
  73. package/dist/monitoring/Redactor.js +143 -0
  74. package/dist/monitoring/Redactor.js.map +1 -0
  75. package/dist/monitoring/probes/LifelineProbe.d.ts +7 -0
  76. package/dist/monitoring/probes/LifelineProbe.d.ts.map +1 -1
  77. package/dist/monitoring/probes/LifelineProbe.js +7 -0
  78. package/dist/monitoring/probes/LifelineProbe.js.map +1 -1
  79. package/dist/monitoring/probes/__shared.d.ts +50 -0
  80. package/dist/monitoring/probes/__shared.d.ts.map +1 -0
  81. package/dist/monitoring/probes/__shared.js +51 -0
  82. package/dist/monitoring/probes/__shared.js.map +1 -0
  83. package/dist/remediation/IntentJournal.d.ts +64 -0
  84. package/dist/remediation/IntentJournal.d.ts.map +1 -0
  85. package/dist/remediation/IntentJournal.js +118 -0
  86. package/dist/remediation/IntentJournal.js.map +1 -0
  87. package/dist/remediation/MachineLock.d.ts +137 -0
  88. package/dist/remediation/MachineLock.d.ts.map +1 -0
  89. package/dist/remediation/MachineLock.js +314 -0
  90. package/dist/remediation/MachineLock.js.map +1 -0
  91. package/dist/remediation/PrimaryAggregatorLease.d.ts +205 -0
  92. package/dist/remediation/PrimaryAggregatorLease.d.ts.map +1 -0
  93. package/dist/remediation/PrimaryAggregatorLease.js +450 -0
  94. package/dist/remediation/PrimaryAggregatorLease.js.map +1 -0
  95. package/dist/remediation/RemediationContext.d.ts +63 -0
  96. package/dist/remediation/RemediationContext.d.ts.map +1 -0
  97. package/dist/remediation/RemediationContext.js +109 -0
  98. package/dist/remediation/RemediationContext.js.map +1 -0
  99. package/dist/remediation/RemediationKeyVault.d.ts +123 -0
  100. package/dist/remediation/RemediationKeyVault.d.ts.map +1 -0
  101. package/dist/remediation/RemediationKeyVault.js +564 -0
  102. package/dist/remediation/RemediationKeyVault.js.map +1 -0
  103. package/dist/remediation/Remediator.d.ts +288 -0
  104. package/dist/remediation/Remediator.d.ts.map +1 -0
  105. package/dist/remediation/Remediator.js +520 -0
  106. package/dist/remediation/Remediator.js.map +1 -0
  107. package/dist/remediation/RemediatorBootstrap.d.ts +141 -0
  108. package/dist/remediation/RemediatorBootstrap.d.ts.map +1 -0
  109. package/dist/remediation/RemediatorBootstrap.js +255 -0
  110. package/dist/remediation/RemediatorBootstrap.js.map +1 -0
  111. package/dist/remediation/TrustElevationSource.d.ts +153 -0
  112. package/dist/remediation/TrustElevationSource.d.ts.map +1 -0
  113. package/dist/remediation/TrustElevationSource.js +208 -0
  114. package/dist/remediation/TrustElevationSource.js.map +1 -0
  115. package/dist/remediation/audit/AuditProjection.d.ts +37 -0
  116. package/dist/remediation/audit/AuditProjection.d.ts.map +1 -0
  117. package/dist/remediation/audit/AuditProjection.js +118 -0
  118. package/dist/remediation/audit/AuditProjection.js.map +1 -0
  119. package/dist/remediation/audit/AuditWriter.d.ts +79 -0
  120. package/dist/remediation/audit/AuditWriter.d.ts.map +1 -0
  121. package/dist/remediation/audit/AuditWriter.js +132 -0
  122. package/dist/remediation/audit/AuditWriter.js.map +1 -0
  123. package/dist/remediation/channels/CliApprovalChannel.d.ts +37 -0
  124. package/dist/remediation/channels/CliApprovalChannel.d.ts.map +1 -0
  125. package/dist/remediation/channels/CliApprovalChannel.js +45 -0
  126. package/dist/remediation/channels/CliApprovalChannel.js.map +1 -0
  127. package/dist/remediation/channels/TelegramApprovalChannel.d.ts +37 -0
  128. package/dist/remediation/channels/TelegramApprovalChannel.d.ts.map +1 -0
  129. package/dist/remediation/channels/TelegramApprovalChannel.js +45 -0
  130. package/dist/remediation/channels/TelegramApprovalChannel.js.map +1 -0
  131. package/dist/remediation/runbooks/db-corruption.d.ts +86 -0
  132. package/dist/remediation/runbooks/db-corruption.d.ts.map +1 -0
  133. package/dist/remediation/runbooks/db-corruption.js +190 -0
  134. package/dist/remediation/runbooks/db-corruption.js.map +1 -0
  135. package/dist/remediation/runbooks/messaging-delivery-failed.d.ts +108 -0
  136. package/dist/remediation/runbooks/messaging-delivery-failed.d.ts.map +1 -0
  137. package/dist/remediation/runbooks/messaging-delivery-failed.js +172 -0
  138. package/dist/remediation/runbooks/messaging-delivery-failed.js.map +1 -0
  139. package/dist/remediation/runbooks/node-abi-mismatch.d.ts +70 -0
  140. package/dist/remediation/runbooks/node-abi-mismatch.d.ts.map +1 -0
  141. package/dist/remediation/runbooks/node-abi-mismatch.js +216 -0
  142. package/dist/remediation/runbooks/node-abi-mismatch.js.map +1 -0
  143. package/dist/remediation/runbooks/supervisor-preflight.d.ts +114 -0
  144. package/dist/remediation/runbooks/supervisor-preflight.d.ts.map +1 -0
  145. package/dist/remediation/runbooks/supervisor-preflight.js +299 -0
  146. package/dist/remediation/runbooks/supervisor-preflight.js.map +1 -0
  147. package/dist/scheduler/AgentMdAtomicSave.d.ts +85 -0
  148. package/dist/scheduler/AgentMdAtomicSave.d.ts.map +1 -0
  149. package/dist/scheduler/AgentMdAtomicSave.js +169 -0
  150. package/dist/scheduler/AgentMdAtomicSave.js.map +1 -0
  151. package/dist/scheduler/AgentMdReconcile.d.ts +62 -0
  152. package/dist/scheduler/AgentMdReconcile.d.ts.map +1 -0
  153. package/dist/scheduler/AgentMdReconcile.js +212 -0
  154. package/dist/scheduler/AgentMdReconcile.js.map +1 -0
  155. package/dist/scheduler/DisabledBodyDrift.d.ts +70 -0
  156. package/dist/scheduler/DisabledBodyDrift.d.ts.map +1 -0
  157. package/dist/scheduler/DisabledBodyDrift.js +149 -0
  158. package/dist/scheduler/DisabledBodyDrift.js.map +1 -0
  159. package/dist/scheduler/InstallBuiltinJobs.d.ts +53 -0
  160. package/dist/scheduler/InstallBuiltinJobs.d.ts.map +1 -0
  161. package/dist/scheduler/InstallBuiltinJobs.js +212 -0
  162. package/dist/scheduler/InstallBuiltinJobs.js.map +1 -0
  163. package/dist/scheduler/JobLoader.d.ts.map +1 -1
  164. package/dist/scheduler/JobLoader.js +48 -0
  165. package/dist/scheduler/JobLoader.js.map +1 -1
  166. package/dist/scheduler/MigrationInvariants.d.ts +62 -0
  167. package/dist/scheduler/MigrationInvariants.d.ts.map +1 -0
  168. package/dist/scheduler/MigrationInvariants.js +215 -0
  169. package/dist/scheduler/MigrationInvariants.js.map +1 -0
  170. package/dist/scheduler/MigrationLedger.d.ts +84 -0
  171. package/dist/scheduler/MigrationLedger.d.ts.map +1 -0
  172. package/dist/scheduler/MigrationLedger.js +114 -0
  173. package/dist/scheduler/MigrationLedger.js.map +1 -0
  174. package/dist/server/fileRoutes.d.ts.map +1 -1
  175. package/dist/server/fileRoutes.js +7 -0
  176. package/dist/server/fileRoutes.js.map +1 -1
  177. package/dist/server/routes.d.ts.map +1 -1
  178. package/dist/server/routes.js +141 -0
  179. package/dist/server/routes.js.map +1 -1
  180. package/package.json +3 -2
  181. package/scripts/classify-default-drift.mjs +238 -0
  182. package/scripts/eli16-overview-check.mjs +85 -0
  183. package/scripts/instar-dev-precommit.js +60 -1
  184. package/scripts/lint-degradation-emit-sites.js +116 -0
  185. package/scripts/lint-no-direct-destructive.js +12 -0
  186. package/scripts/protect-migration-guarantee.js +61 -0
  187. package/scripts/regen-default-job-templates.mjs +111 -0
  188. package/scripts/verify-runbook-pr-signature.js +366 -0
  189. package/src/data/builtin-manifest.json +93 -93
  190. package/src/scaffold/bootstrap.ts +138 -0
  191. package/src/scaffold/keys/instar-release-pub.pem +3 -0
  192. package/src/scaffold/templates/jobs/instar/.gitkeep +0 -0
  193. package/src/scaffold/templates/jobs/instar/commitment-detection.md +28 -0
  194. package/src/scaffold/templates/jobs/instar/evolution-overdue-check.md +28 -0
  195. package/src/scaffold/templates/jobs/instar/evolution-proposal-evaluate.md +30 -0
  196. package/src/scaffold/templates/jobs/instar/evolution-proposal-implement.md +25 -0
  197. package/src/scaffold/templates/jobs/instar/health-check.md +14 -0
  198. package/src/scaffold/templates/jobs/instar/identity-review.md +42 -0
  199. package/src/scaffold/templates/jobs/instar/insight-harvest.md +31 -0
  200. package/src/scaffold/templates/jobs/instar/overseer-development.md +23 -0
  201. package/src/scaffold/templates/jobs/instar/overseer-guardian.md +28 -0
  202. package/src/scaffold/templates/jobs/instar/overseer-infrastructure.md +26 -0
  203. package/src/scaffold/templates/jobs/instar/overseer-learning.md +28 -0
  204. package/src/scaffold/templates/jobs/instar/overseer-maintenance.md +27 -0
  205. package/src/scaffold/templates/jobs/instar/reflection-trigger.md +40 -0
  206. package/src/scaffold/templates/jobs/instar/relationship-maintenance.md +16 -0
  207. package/src/scaffold/templates.ts +1616 -0
  208. package/upgrades/0.28.102.md +6 -7
  209. package/upgrades/0.28.103.md +471 -0
  210. package/upgrades/side-effects/a47-primary-aggregator-lease.md +82 -0
  211. package/upgrades/side-effects/agentmd-atomic-save.md +66 -0
  212. package/upgrades/side-effects/agentmd-loader-200jobs-perf-benchmark.md +50 -0
  213. package/upgrades/side-effects/agentmd-reconcile-boot-lifecycle.md +66 -0
  214. package/upgrades/side-effects/api-safety-guard.md +80 -0
  215. package/upgrades/side-effects/c1-runbook-pr-gate.md +83 -0
  216. package/upgrades/side-effects/disabled-body-drift-detection.md +59 -0
  217. package/upgrades/side-effects/drift-classifier-release-time.md +71 -0
  218. package/upgrades/side-effects/eli16-overview-required-gate.md +77 -0
  219. package/upgrades/side-effects/f1-remediation-key-vault.md +195 -0
  220. package/upgrades/side-effects/f2-redactor-errorcode-extractor.md +130 -0
  221. package/upgrades/side-effects/f3-degradation-reporter-shim.md +105 -0
  222. package/upgrades/side-effects/f4-machine-locks-intent-journal-audit.md +117 -0
  223. package/upgrades/side-effects/f5-trust-elevation-source.md +258 -0
  224. package/upgrades/side-effects/f6-supervisor-handshake.md +234 -0
  225. package/upgrades/side-effects/f7-post-update-migrator-atomic-step.md +129 -0
  226. package/upgrades/side-effects/f8-remediator-skeleton.md +153 -0
  227. package/upgrades/side-effects/f8-rest-enforcement.md +72 -0
  228. package/upgrades/side-effects/file-viewer-jobs-instar-never-editable.md +41 -0
  229. package/upgrades/side-effects/frontmatter-interpolation-breakout-test.md +48 -0
  230. package/upgrades/side-effects/jobs-as-agentmd-phase-2.md +78 -0
  231. package/upgrades/side-effects/jobs-as-agentmd-phase-3.md +76 -0
  232. package/upgrades/side-effects/jobs-as-agentmd-phase-4-endpoints.md +62 -0
  233. package/upgrades/side-effects/jobs-as-agentmd-phase-5.md +67 -0
  234. package/upgrades/side-effects/jobs-as-agentmd-phase-6.md +53 -0
  235. package/upgrades/side-effects/jobs-endpoints-auth-verification.md +52 -0
  236. package/upgrades/side-effects/migration-guarantee-suite.md +61 -0
  237. package/upgrades/side-effects/migration-runtime-invariant-gate.md +59 -0
  238. package/upgrades/side-effects/migration-telemetry-invariant-8.md +68 -0
  239. package/upgrades/side-effects/npm-pack-templates-smoke.md +51 -0
  240. package/upgrades/side-effects/openclaw-import-before-prompt-build.md +84 -0
  241. package/upgrades/side-effects/openclaw-import-pre-compaction-flush.md +85 -0
  242. package/upgrades/side-effects/prompt-gate-no-prompt-cache.md +258 -0
  243. package/upgrades/side-effects/tier2-degradation-reporter-live-wire.md +146 -0
  244. package/upgrades/side-effects/w1-node-abi-mismatch-runbook.md +154 -0
  245. package/upgrades/side-effects/w2-supervisor-preflight-runbook.md +176 -0
  246. package/upgrades/side-effects/w3-messaging-delivery-failed-runbook.md +185 -0
  247. package/upgrades/side-effects/w4-db-corruption-runbook.md +114 -0
@@ -0,0 +1,258 @@
1
+ # Side-Effects Review — F-5: TrustElevationSource + AutonomyProfileLevel wiring
2
+
3
+ **Version / slug:** `f5-trust-elevation-source`
4
+ **Date:** `2026-05-13`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Tier-2 foundation module from the Self-Healing Remediator v2 spec (per
11
+ `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` "Trust elevation policy"
12
+ section + amendments A11, A22, A25, A41, A53, A57, A59). Three new modules
13
+ under `src/remediation/`:
14
+
15
+ - `src/remediation/TrustElevationSource.ts` — authoritative gate for
16
+ runbook lifecycle transitions. Implements the asymmetric trust-elevation
17
+ table from the spec: pessimistic-quarantine always-allowed, collaborative
18
+ trust minimum for upward transitions, 48h fresh-trace + 1-week history
19
+ for registered→live, two-distinct-kind-channel rule for essential
20
+ un-quarantine (A53), and source-only transitions for
21
+ proposal→registered, live→deprecated, deprecated→removed.
22
+ - `src/remediation/channels/TelegramApprovalChannel.ts` — F-5 stub
23
+ implementation of `TrustedApprovalChannel`. Exposes the shape the real
24
+ Telegram-countersignature verification (A41) will plug into, with a
25
+ deterministic seeded-map for test fixtures.
26
+ - `src/remediation/channels/CliApprovalChannel.ts` — F-5 stub
27
+ implementation of `TrustedApprovalChannel` for the
28
+ `instar doctor confirm-unquarantine` signed-CLI second-factor path
29
+ (A53 option 1). Same seeded-map shape.
30
+
31
+ This PR ships the policy module ONLY. No surface (dispatcher F-8,
32
+ un-quarantine endpoint A25, dashboard toggle) wires into the source in
33
+ this PR. F-5 is consumed by F-8's runbook-lifecycle wiring and by the
34
+ Tier-2 `POST /remediation/unquarantine/:runbookId` endpoint in
35
+ subsequent PRs.
36
+
37
+ The change is foundational infrastructure: it adds capability surface, it
38
+ does not remove or replace any existing decision point.
39
+
40
+ ## Decision-point inventory
41
+
42
+ - `TrustElevationSource.canTransition()` — **add** — gates whether a
43
+ runbook lifecycle transition is permitted given (a) the current
44
+ autonomy profile, (b) supplied dry-run context, (c) approvals collected
45
+ via configured channels. Returns `{allowed, reason}`; never mutates
46
+ registry state.
47
+ - `TrustElevationSource.requireSecondChannel()` — **add** — gates whether
48
+ the configured channels are sufficient for an essential-runbook
49
+ un-quarantine (A53's "real second factor" rule, distinct-kind).
50
+ - `TelegramApprovalChannel.verifyApproval()` — **add** — stub: returns
51
+ approval for seeded entries only. Real Telegram countersignature
52
+ verification (A41) is a Tier-2 follow-up.
53
+ - `CliApprovalChannel.verifyApproval()` — **add** — stub: returns
54
+ confirmation for seeded entries only. Real `instar doctor
55
+ confirm-unquarantine` signed-CLI integration is a Tier-2 follow-up.
56
+
57
+ All decision points operate on caller-supplied policy inputs
58
+ (autonomy profile, dry-run trace age + history span, essential bit,
59
+ channel approvals). They do not contain content classification,
60
+ heuristics, or "guess intent from text" logic.
61
+
62
+ ---
63
+
64
+ ## 1. Over-block
65
+
66
+ **What legitimate inputs does this change reject that it shouldn't?**
67
+
68
+ - `canTransition('proposal-to-registered', ...)` ALWAYS refuses. By
69
+ design — the spec deliberately offers no programmatic path. A human
70
+ must run a `/instar-dev` commit + spec-converge approval. The source
71
+ cannot become a backdoor.
72
+ - `canTransition('live-to-deprecated' | 'deprecated-to-removed', ...)`
73
+ ALWAYS refuses. Same reason: source change only.
74
+ - `registered-to-live` refuses without `dryRunTraceAge` /
75
+ `dryRunHistoryDays`. The dispatcher is expected to supply both from
76
+ the audit projection (F-4); a caller that omits them is a programmer
77
+ error. Refusing with an explicit `missing-...` reason is preferable to
78
+ silently approving.
79
+ - Below-`collaborative` profile refuses ALL upward transitions even
80
+ when channels approve. Spec-mandated. The profile is the user-set
81
+ policy on how much authority the agent gets; it sits ABOVE channel
82
+ approvals. A user explicitly setting `cautious` SHOULD see the
83
+ agent refuse to un-quarantine even if approval comes through.
84
+
85
+ ---
86
+
87
+ ## 2. Under-block
88
+
89
+ **What failure modes does this still miss?**
90
+
91
+ - **Stub channels are not real.** F-5 ships the abstraction with
92
+ deterministic seeded-map verification. The real verification (A41
93
+ Telegram-countersignature with proposalId-binding + user_id principal +
94
+ message-id watermark; A53 CLI signing key) lives in the channel
95
+ implementations that ship later. A misconfigured agent that wires a
96
+ stub into production would silently approve nothing; the dispatcher
97
+ surfaces this as `no-matching-...-countersignature` audit-logged refusal.
98
+ - **Channel-kind collision.** The A53 "different-kind" rule keys on the
99
+ channel's `kind` string. Two implementations using the same `kind`
100
+ (e.g., two different Telegram bots) would falsely satisfy the
101
+ two-channel rule if the source counted instances. The implementation
102
+ counts distinct `kind` strings, so this is closed at the source. But
103
+ a channel-implementer that picks a clashing `kind` would create a
104
+ silent collapse — channel-implementer review is the mitigation.
105
+ - **Replay across transitions.** F-5 forwards the channel's `messageId`
106
+ unchanged. The channel implementation MUST enforce the A41 watermark
107
+ `(proposalId, messageId)` tuple; F-5 does not duplicate that check.
108
+ This is the right layer — the source decides "did enough channels
109
+ approve" given verified results; the channel decides "is this
110
+ approval real and non-replayed."
111
+ - **Race between concurrent un-quarantine attempts.** F-5 evaluates
112
+ channel approvals in `Promise.all`. Two concurrent dispatcher calls
113
+ for the same runbook would each issue a separate `verifyApproval`. The
114
+ channel implementation's watermark catches replay; the dispatcher's
115
+ per-runbook lock (F-4 `MachineLock`) prevents two un-quarantines from
116
+ taking effect. F-5 trusts those upstream gates.
117
+ - **Trust-profile race.** If `AutonomyProfileManager.setProfile()` flips
118
+ during a `canTransition` call, the source captured `profile` in the
119
+ constructor. The dispatcher is expected to construct a fresh
120
+ `TrustElevationSource` per dispatch (or re-read profile from the
121
+ manager). F-5 does not subscribe to profile changes.
122
+
123
+ ---
124
+
125
+ ## 3. Level-of-abstraction fit
126
+
127
+ **Is this at the right layer?**
128
+
129
+ Yes. F-5 is a **policy** module:
130
+
131
+ - It decides "is this transition permitted right now given my
132
+ inputs"; it does not mutate state, dispatch runbooks, write
133
+ registry, or talk to the network.
134
+ - The dispatcher (F-8) and the un-quarantine endpoint (A25) are the
135
+ enforcement points: they call `canTransition`, get a verdict, and
136
+ either proceed or audit-log + refuse.
137
+ - The channels are **mechanism**: they verify cryptographic /
138
+ protocol-specific approval proofs and return a boolean.
139
+
140
+ Three-layer split (mechanism → policy → enforcement) is the spec-
141
+ mandated layering. F-5 sits in the middle layer alone — channels and
142
+ enforcement live elsewhere.
143
+
144
+ The `AutonomyProfileLevel` type is re-exported from
145
+ `src/core/types.ts` (canonical owner). F-5 does not duplicate or
146
+ redefine the type; it imports the same enum the rest of the codebase
147
+ uses via `AutonomyProfileManager`.
148
+
149
+ ---
150
+
151
+ ## 4. Signal vs authority compliance
152
+
153
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
154
+
155
+ **Does this change hold blocking authority with brittle logic?**
156
+
157
+ - [x] Yes — but the logic is a smart gate operating on cryptographic /
158
+ policy primitives.
159
+ - [ ] ⚠️ Yes, with brittle logic — STOP.
160
+
161
+ The "blocks" in this change are:
162
+
163
+ 1. Profile-level comparison against a numeric ordering (`cautious=0,
164
+ supervised=1, collaborative=2, autonomous=3`). Deterministic.
165
+ 2. Trace-age numeric comparison against `FRESH_TRACE_MAX_AGE_MS`.
166
+ Deterministic.
167
+ 3. History-span numeric comparison against `MIN_DRY_RUN_HISTORY_DAYS`.
168
+ Deterministic.
169
+ 4. Channel-approval boolean count + distinct-kind set size.
170
+ Deterministic, sourced from channel verifications.
171
+ 5. Transition enum exhaustive switch. Deterministic.
172
+
173
+ None of these are "string match on free-text"-class brittle filters.
174
+ The signal-vs-authority anti-pattern is heuristic detectors holding
175
+ blocking power; F-5's gates are policy primitives operating on
176
+ authoritative inputs. The smart gate (the dispatcher / un-quarantine
177
+ endpoint) is the consumer that decides *whether to attempt* a
178
+ transition; F-5 only encodes the trust-elevation table.
179
+
180
+ The CHANNEL implementations are where signal-vs-authority matters most.
181
+ F-5's stubs are deterministic seeded-map lookups; the real Telegram /
182
+ CLI channels MUST enforce cryptographic verification (HMAC, signature,
183
+ watermark) — not string match on user reply text. The
184
+ `TrustedApprovalChannel` interface returns a `principalUserId` so the
185
+ A41 user_id-principal binding is preserved end-to-end.
186
+
187
+ ---
188
+
189
+ ## 5. Interactions
190
+
191
+ **Does this interact with existing checks, recovery paths, or
192
+ infrastructure?**
193
+
194
+ - **Shadowing:** `src/core/TrustElevationTracker.ts` exists already.
195
+ Despite the similar name, that module tracks dashboard-driven trust
196
+ elevation events at the autonomy-profile layer. F-5 is the
197
+ remediator-specific runbook-lifecycle policy module. They share the
198
+ `AutonomyProfileLevel` type but no decision-point overlap; the
199
+ tracker is the SOURCE of the profile that F-5 READS.
200
+ - **Race with adjacent cleanup:** None. F-5 has no on-disk state, no
201
+ background timers, no event listeners. Its only mutable state is the
202
+ channel list captured at construction.
203
+ - **Double-fire:** `canTransition` is pure with respect to F-5 state
204
+ (no caching, no memo). Channels are responsible for their own
205
+ idempotency / replay defence. The dispatcher's `MachineLock`
206
+ serialises actual state mutations.
207
+ - **Interaction with AutonomyProfileManager.** F-5 captures the
208
+ profile at construction. The dispatcher SHOULD construct a fresh
209
+ `TrustElevationSource` per dispatch (or re-derive). This is
210
+ documented in the module header.
211
+
212
+ ---
213
+
214
+ ## 6. External surfaces
215
+
216
+ **Does this change anything visible to other agents, other users,
217
+ other systems?**
218
+
219
+ - No on-disk state. F-5 is in-memory only.
220
+ - No HTTP routes. The A25 `POST /remediation/unquarantine/:runbookId`
221
+ endpoint that consumes F-5 ships in a follow-up PR.
222
+ - No Telegram surfaces. The Telegram channel implementation is a stub;
223
+ the real Telegram-countersignature wiring ships in a follow-up.
224
+ - No dashboard tabs.
225
+ - New public types: `RunbookTransition`, `TrustedApprovalChannel`,
226
+ `TrustedApprovalVerifyInput`, `TrustedApprovalVerifyResult`,
227
+ `CanTransitionContext`, `CanTransitionResult`,
228
+ `TrustElevationSourceOpts`. All under `src/remediation/`.
229
+ - New configuration surface: `remediation.approvalChannel.primary`
230
+ (per A59) is NOT yet read by F-5; the channel array is supplied
231
+ explicitly by the constructing caller. Config wiring lives at the
232
+ point of dispatcher construction, in a follow-up PR.
233
+
234
+ ---
235
+
236
+ ## 7. Rollback cost
237
+
238
+ **If this turns out wrong in production, what's the back-out?**
239
+
240
+ Trivially low. No consumers of these modules ship in this PR — the
241
+ exported symbols are new and not yet imported anywhere in `src/`. A
242
+ `git revert` removes them with zero state-migration cost. No on-disk
243
+ files are created.
244
+
245
+ If a future consumer-PR exposes a bug in the policy logic, the back-out
246
+ path is "revert the consumer PR" — F-5 in isolation has no runtime
247
+ effect.
248
+
249
+ ---
250
+
251
+ ## Reviewer concurrence (Phase 5)
252
+
253
+ Not required. This change touches no block/allow surface for messaging,
254
+ no session lifecycle, no coherence/sentinel/gate authority. It is
255
+ foundational policy infrastructure with no live consumers in this PR.
256
+ The dispatcher (F-8 Tier-2) and the un-quarantine endpoint (A25
257
+ follow-up) are the enforcement points; their PRs will require their own
258
+ side-effects reviews when wiring lands.
@@ -0,0 +1,234 @@
1
+ # Side-Effects Review — F-6 ServerSupervisor handshake
2
+
3
+ **Version / slug:** `f6-supervisor-handshake`
4
+ **Date:** 2026-05-13
5
+ **Author:** echo (instar-developing agent)
6
+ **Second-pass reviewer:** not required
7
+
8
+ ## Summary of the change
9
+
10
+ Extends `src/lifeline/ServerSupervisor.ts` with the Remediator ↔ Supervisor
11
+ handshake described in `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md`
12
+ (§A15 partial-upgrade rule, carried forward from v1 §Supervisor
13
+ coordination) and `docs/specs/SELF-HEALING-REMEDIATOR-V3-CONSOLIDATED-SPEC.md`
14
+ (§3 state-file taxonomy: `supervisor-handshake.json`, HMAC-extended
15
+ `restart-requested.json`; §9 Tier-2 sequencing). Adds:
16
+
17
+ - `ServerSupervisor.HANDSHAKE_PROTOCOL_VERSION = 1` (static).
18
+ - `RestartRequestedPayload`, `RestartRequestedReply`, `RegisteredRemediator`
19
+ public types.
20
+ - `ServerSupervisor.registerRemediator(remediator)` — writes
21
+ `<stateDir>/state/supervisor-handshake.json` (`{version,
22
+ supervisorBuildId, writtenAt}`).
23
+ - `ServerSupervisor.handleRestartRequested(payload)` — verifies HMAC,
24
+ handshake-version, staleness (5-minute window), and blastRadius
25
+ (`process | machine` only — `fleet` is refused for Tier-2). On accept,
26
+ tracks the request and initiates a `performGracefulRestart` cycle.
27
+ - `notifyPendingRemediatorRequestsOnHealthy()` — fires
28
+ `remediator.onRestartComplete({requestId})` once the next healthy tick
29
+ lands after a serverRestarting → healthy transition.
30
+ - `canonicalRestartRequestedBody(payload)` exported helper —
31
+ deterministic byte serialization used as HMAC input on both sides.
32
+
33
+ The existing `private preflightSelfHeal()` is unchanged; W-2 will wrap
34
+ it. The existing `restart-requested.json` flag path is unchanged (the
35
+ in-process handshake is the canonical F-6 surface; the file-based flag
36
+ remains for AutoUpdater and is co-owned per v3 §3).
37
+
38
+ Files touched: `src/lifeline/ServerSupervisor.ts`,
39
+ `tests/unit/ServerSupervisor-handshake.test.ts` (new, 9 tests),
40
+ `upgrades/NEXT.md`, this artifact.
41
+
42
+ ## Decision-point inventory
43
+
44
+ - `handleRestartRequested` — **add** — verifies signed restart requests.
45
+ This holds blocking authority: a rejection refuses to restart the
46
+ server. Each rejection branch is a deterministic check on
47
+ cryptographic state (HMAC, monotonic clock, declared protocol
48
+ version, declared blast radius), NOT a heuristic message classifier.
49
+ See section 4 for signal-vs-authority analysis.
50
+ - `registerRemediator` — **add** — idempotent registration; no
51
+ authority. Side-effects writes `supervisor-handshake.json`.
52
+ - `notifyPendingRemediatorRequestsOnHealthy` — **add** — fires
53
+ callbacks. Idempotent (entries deleted after first call). No
54
+ authority.
55
+
56
+ ---
57
+
58
+ ## 1. Over-block
59
+
60
+ **What legitimate inputs does this change reject that it shouldn't?**
61
+
62
+ - `blastRadius: 'fleet'` is rejected. Tier-2's ServerSupervisor only
63
+ handles `process` and `machine` restarts. A fleet-wide restart is the
64
+ coordination-protocol surface's responsibility, not the supervisor's.
65
+ Refusal is by design and matches v3 §9 Tier-3 deferral; Remediator
66
+ must not issue fleet-scope restart-requested in Tier-2 anyway.
67
+ - Clock skew > 5 minutes (in either direction) rejects valid requests.
68
+ This is the documented staleness window per the F-6 spec; widening it
69
+ would weaken replay defense. Operators with persistent clock skew
70
+ receive `stale: ageMs=…` in the reject reason; the Remediator
71
+ surfaces this in audit + alerts via the higher-level dispatch path.
72
+ - Handshake version mismatch is exact-equality only. A future v2
73
+ protocol must be negotiated by bumping `HANDSHAKE_PROTOCOL_VERSION`
74
+ on both sides; legacy Remediators speaking v1 will be rejected. This
75
+ is the A15 contract — partial-upgrade DoS is prevented by refusing,
76
+ not by best-effort interpolation.
77
+
78
+ ## 2. Under-block
79
+
80
+ **What failure modes does this still miss?**
81
+
82
+ - The `monotonicTs` field is informational; the HMAC covers it but the
83
+ supervisor does NOT enforce monotonic ordering across requests. A
84
+ per-Remediator monotonic counter would close the residual replay
85
+ window inside the 5-minute staleness budget. F-8 (rest) owns the
86
+ per-surface monotonic counter (A42); F-6 deliberately punts.
87
+ - The supervisor accepts only the registered Remediator's leaf key. If
88
+ the Remediator's RemediationKeyVault is rotated mid-flight, an
89
+ in-flight request signed with the old leaf will fail HMAC
90
+ verification. This is expected — the overlap window is owned at the
91
+ vault rotation layer (per F-1's `rotateContext` contract).
92
+ - `RegisteredRemediator.getCapabilityLeafKey()` is called per
93
+ verification — the supervisor does not cache. This is intentional
94
+ (lets the Remediator rotate without re-registration) but pushes a
95
+ small constant cost onto each call. Negligible at expected throughput
96
+ (≤ 1 restart-requested per process per ~24h per A7 cross-process cap).
97
+ - File-based `restart-requested.json` path is unchanged in this PR.
98
+ Adding HMAC to the file shape is described in v3 §9 ("HMAC required on
99
+ ANY plannedRestart:true post-F-6"); the in-process handshake is the
100
+ Tier-2 canonical surface and file-shape HMAC migration tracks under
101
+ the AutoUpdater path, not F-6 proper. NEXT.md documents the gap.
102
+
103
+ ## 3. Level-of-abstraction fit
104
+
105
+ **Is this at the right layer?**
106
+
107
+ Yes. F-6 lives in `src/lifeline/ServerSupervisor.ts` because the
108
+ supervisor is the only authority that can request a tmux-session
109
+ restart safely. The handshake protocol surface is a thin layer on top
110
+ of the supervisor's existing `performGracefulRestart()`; it does NOT
111
+ re-implement restart logic, doesn't reach into runbook internals, and
112
+ doesn't speak any domain vocabulary above
113
+ `{requestId, runbookId, attemptId, blastRadius}`.
114
+
115
+ The `canonicalRestartRequestedBody()` exported helper deliberately
116
+ lives at the bottom of the same file so the wire format has one
117
+ canonical owner — both the supervisor's verify path and the
118
+ Remediator's sign path (in F-8 rest) call the same function. Cross-file
119
+ divergence is the most common HMAC-protocol bug; co-locating prevents
120
+ it.
121
+
122
+ ## 4. Signal vs authority compliance
123
+
124
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
125
+
126
+ **Does this change hold blocking authority with brittle logic?**
127
+
128
+ - [ ] No — this change produces a signal consumed by an existing smart gate.
129
+ - [ ] No — this change has no block/allow surface.
130
+ - [x] Yes — but the logic is a smart gate with full conversational context.
131
+ - [ ] Yes, with brittle logic — STOP.
132
+
133
+ `handleRestartRequested` is a blocking gate. Each rejection condition
134
+ is a deterministic check on cryptographic + structural state, not a
135
+ heuristic message classifier:
136
+
137
+ 1. HMAC verification via `crypto.timingSafeEqual` — pure crypto.
138
+ 2. Handshake-version equality — integer compare.
139
+ 3. `requestedAt` staleness — wall-clock arithmetic on a signed field.
140
+ 4. `blastRadius` allowlist — string-set membership on a signed field.
141
+ 5. Required-field shape check — typeof guards.
142
+
143
+ Every gated field is HMAC-covered by `canonicalRestartRequestedBody()`,
144
+ so an attacker who shifts any field invalidates the signature. The
145
+ gate has the FULL context needed to decide — the canonical body IS the
146
+ context — and the decision is reproducible. This matches the
147
+ "signal-vs-authority" contract: the gate that holds blocking authority
148
+ has the full structured payload, not a free-text excerpt.
149
+
150
+ ## 5. Interactions
151
+
152
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
153
+
154
+ - **`performGracefulRestart`:** `handleRestartRequested` calls the
155
+ existing method on accept. Maintenance wait is pre-set so existing
156
+ `handleUnhealthy`/serverDown suppression behaves as it does for
157
+ AutoUpdater-driven restarts.
158
+ - **`restart-requested.json` file path:** Untouched. Both the file path
159
+ (AutoUpdater) and the in-process path (Remediator) trigger the same
160
+ `performGracefulRestart`. The two paths are independent and cannot
161
+ race destructively — `performGracefulRestart` is idempotent at the
162
+ tmux-session level.
163
+ - **Pending-request notification:** Wired into the existing healthy-
164
+ tick branch of the health check loop. Idempotent (clears the map
165
+ after firing). If `onRestartComplete` throws, the supervisor logs and
166
+ continues — a faulty Remediator callback cannot wedge the supervisor.
167
+ - **`debug-restart-request.json`:** Unrelated path; doctor-session
168
+ HMAC uses a different secret (`setDoctorSessionSecret`) and a
169
+ different file. No interaction.
170
+ - **State-file: `supervisor-handshake.json`:** Newly written by
171
+ `registerRemediator` + `setSupervisorBuildId`. Per v3 §3 it is NOT
172
+ backed up and NOT git-synced. The path matches v3 §3 verbatim.
173
+ - **`RemediationKeyVault` (F-1):** F-6 takes the leaf key from the
174
+ registered Remediator via callback, NOT directly from the vault.
175
+ This keeps the supervisor decoupled from F-1's API surface; the
176
+ Remediator (which owns the vault lifecycle) injects the key. If F-1
177
+ rotates the context, the next registration / next call sees the new
178
+ key.
179
+
180
+ ## 6. External surfaces
181
+
182
+ **Does this change anything visible to other agents, other users, other systems?**
183
+
184
+ - **State file:** `<stateDir>/state/supervisor-handshake.json` (new).
185
+ JSON `{version, supervisorBuildId, writtenAt}`. World-readable by
186
+ default (same as sibling files in the same dir). No secrets in the
187
+ body.
188
+ - **Public types:** `RestartRequestedPayload`,
189
+ `RestartRequestedReply`, `RegisteredRemediator`,
190
+ `canonicalRestartRequestedBody()` exported from
191
+ `src/lifeline/ServerSupervisor.ts`. These are stable API surfaces
192
+ that F-8-rest will consume; bump `HANDSHAKE_PROTOCOL_VERSION` on any
193
+ wire-format change.
194
+ - **Events:** No new EventEmitter events. The existing
195
+ `serverRestarting` event still fires from `performGracefulRestart`.
196
+ `onRestartComplete` is delivered via callback, not event, because it
197
+ is a per-request notification with a `requestId` correlator (events
198
+ fan out; callbacks correlate).
199
+ - **No telegram, slack, or external API calls** added.
200
+
201
+ ## 7. Rollback cost
202
+
203
+ **If this turns out wrong in production, what's the back-out?**
204
+
205
+ `git revert` on the F-6 PR removes the surface. No data migration:
206
+
207
+ - `supervisor-handshake.json` is regenerated on next registration; if
208
+ no consumer registers (post-revert state), the file is stale but inert.
209
+ - No keychain or vault state is touched. F-1 vault is untouched.
210
+ - No restart-requested.json or other state-file shape changes (file
211
+ shape additions are deferred to a follow-up per §2 above).
212
+ - The F-6 surface is currently uncalled in production code (no
213
+ Remediator instance calls `registerRemediator` on main yet). Revert
214
+ is safe at any point before the wrapper-PR series ships.
215
+
216
+ The A15 7-day age rule is what bounds the rollback risk for downstream
217
+ wrappers: a wrapper that depends on F-6 must wait until F-6 has been
218
+ on main + auto-updated for 7 days before merging, so a revert window
219
+ of < 7 days has zero downstream consumers to break.
220
+
221
+ ---
222
+
223
+ ## Spec anchor
224
+
225
+ This change implements:
226
+ - **v2 §A15** (partial-upgrade window — supervisor handshake lag rule),
227
+ - **v2 carried-forward §Supervisor coordination** (HMAC-signed
228
+ restart-requested),
229
+ - **v2 §A20** (key segregation — the supervisor verifies HMACs against
230
+ the capability-context leaf, not the legacy `~/.instar/agent.key`),
231
+ - **v3 §3** (state-file taxonomy — `supervisor-handshake.json`,
232
+ `restart-requested.json` co-ownership),
233
+ - **v3 §9** (Tier-2 sequencing — F-6 unblocks W-2..W-4 after the 7-day
234
+ age requirement).
@@ -0,0 +1,129 @@
1
+ # Side-Effects Review — F-7: PostUpdateMigrator atomic-step + announceOnce primitives (Tier-2)
2
+
3
+ **Version / slug:** `f7-post-update-migrator-atomic-step`
4
+ **Date:** `2026-05-13`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Ships F-7 from the Self-Healing Remediator v2 spec (`docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` §R1 Upgrade invariants + §A35 backup/sync wiring + §A50 hook-shape corrections + §A57 Tier-2 phase manifest).
11
+
12
+ Two new primitives plus the A35 hook-shape changes:
13
+
14
+ 1. **`MigratorStepEngine`** — atomic, idempotent, run-once-per-version migration steps. Steps are registered by name + version. The engine writes a ledger at `<stateDir>/migrator-steps-completed.json` keyed by `<version>:<step-name>`. A step failure records `outcome: 'failed'` and does NOT roll back prior steps or block subsequent steps — each step is self-contained.
15
+
16
+ 2. **`AnnouncementManager`** — `announceOnce(id, message, channel)` primitive. Emits a message exactly once per id, then never again. Ledger at `<stateDir>/announcements-shown.json`. Used for surfacing migration completions and structural-change notices without spamming the user across restarts.
17
+
18
+ 3. **A35 hook-shape — `GitStateManager.DEFAULT_GITIGNORE` extension.** Const literal now contains five remediation runtime path globs (`remediation/system-reviewer-state-*.json`, `remediation/inbox-*.jsonl`, `remediation/audit-projection-*.jsonl`, `remediation/cross-process-attempts-*.jsonl`, `remediation/llm-raw-*.jsonl`). Exported as `REMEDIATION_GITIGNORE_ENTRIES` for the F-7 atomic step to use as the source-of-truth list.
19
+
20
+ 4. **A35 hook-shape — `BackupManager` exclusion list with feature-flag gate.** New const `REMEDIATION_EXCLUDED_PATH_PREFIXES` (exported) — the five remediation path prefixes. New optional constructor arg `isRemediationEnabled?: () => boolean` parallels the existing `isIntegratedBeingEnabled` gate. When the gate is ON, the prefixes drop any user-added `includeFiles` entry that begins with a remediation prefix. When OFF (or absent), the prefixes are inactive — strictly additive, no back-compat break.
21
+
22
+ Files touched:
23
+ - `src/core/MigratorStepEngine.ts` (add)
24
+ - `src/core/PostUpdateMigrator.ts` (modify — add `registerStep` + `runPendingSteps` + lazy engine field; existing constructor + all 15 existing migration methods unchanged)
25
+ - `src/core/GitStateManager.ts` (modify — extend `DEFAULT_GITIGNORE` const literal; export `REMEDIATION_GITIGNORE_ENTRIES`)
26
+ - `src/core/BackupManager.ts` (modify — add `REMEDIATION_EXCLUDED_PATH_PREFIXES` const + export; add 5th optional constructor arg `isRemediationEnabled`; gate logic in `resolveIncludedFiles`)
27
+ - `tests/unit/PostUpdateMigrator-atomicStep.test.ts` (add — 8 cases)
28
+ - `tests/unit/AnnouncementManager.test.ts` (add — 5 cases)
29
+ - `tests/unit/PostUpdateMigrator-a35-remediationPaths.test.ts` (add — 5 cases)
30
+ - `upgrades/NEXT.md` (modify — preserves all existing entries; adds A35 + announceOnce entries)
31
+
32
+ ## Decision-point inventory
33
+
34
+ - `PostUpdateMigrator.registerStep()` — **add** — delegates to lazy `MigratorStepEngine`. Existing 15 `migrate*` methods + `migrate()` orchestrator untouched.
35
+ - `PostUpdateMigrator.runPendingSteps()` — **add** — async; returns `{ steps: [{name, outcome, details}] }`. Never throws.
36
+ - `MigratorStepEngine.registerStep()` — rejects duplicate names, missing version/run.
37
+ - `MigratorStepEngine.runPendingSteps()` — three skip reasons: `already-recorded:<outcome>`, `future-version:<step.version> > <toVersion>`, and explicit `'skipped'` from the step itself. Failed steps record `outcome: 'failed'` and DO NOT retry (operator must clear the ledger entry to retry — surfaces the failure in the report).
38
+ - `MigratorStepEngine.writeLedger()` — atomic temp-file → fsync → rename, `0600` perms.
39
+ - `AnnouncementManager.announceOnce()` — three-input validation (id, message, channel); records BEFORE sink-invoke so a flaky sink cannot cause duplicate emission. Sink throws are swallowed (ledger entry stands).
40
+ - `GitStateManager.DEFAULT_GITIGNORE` const literal — **modify** — append remediation block. Backwards-compatible: previously private const is now exported (no internal call sites broken).
41
+ - `BackupManager` constructor — **modify** — append 5th optional argument. All four existing call sites pass ≤4 args and continue to work.
42
+ - `BackupManager.resolveIncludedFiles()` — **modify** — adds gated exclusion loop. When gate is OFF (the default — every existing caller), behavior is identical to pre-PR.
43
+
44
+ No new HTTP routes. No new daemon processes. No new file types beyond two JSON ledgers in `<stateDir>`.
45
+
46
+ ---
47
+
48
+ ## 1. Over-block
49
+
50
+ **What legitimate inputs does this change reject that it shouldn't?**
51
+
52
+ - **A future-version step is silently skipped.** Intended. The ledger is NOT written when the step's version > `toVersion`, so the next upgrade boundary that crosses the threshold runs the step.
53
+ - **A failed step is NOT auto-retried on the next `runPendingSteps()` invocation.** This could be seen as over-blocking — a transient failure (disk full, network blip) gets recorded as `failed` and stays that way. Trade-off accepted because the alternative (unbounded retry on every update) lets a broken step DoS every release. Operator-facing mitigation: the failed entry surfaces in the migration report; clearing the ledger key allows retry.
54
+ - **The `BackupManager` remediation-prefix gate, when ON, drops includeFiles entries whose paths begin with a remediation prefix.** A user who manually adds `.instar/remediation/system-reviewer-state-mine.json` to `config.backup.includeFiles` will see it silently dropped. Mitigated by: (a) the gate defaults to OFF until remediation is enabled; (b) the prefixes are documented in code + spec; (c) this is the explicit spec requirement (A14 "Backed up? no" rows).
55
+
56
+ No unintended over-blocks.
57
+
58
+ ---
59
+
60
+ ## 2. Under-block
61
+
62
+ **What failure modes does this still miss?**
63
+
64
+ - **No cross-machine coordination of step execution.** Two machines updating to the same version in parallel each run their own steps independently. The ledger is per-machine. This is the correct model for per-machine state (gitignore lives in `<stateDir>/.gitignore` which is per-machine via the `.instar` git repo), but a future cross-machine migration would need additional coordination.
65
+ - **Step `run()` is not deadline-enforced.** A hung step could block subsequent steps. The engine relies on step authors to be well-behaved. Real-world steps are file I/O against `<stateDir>` and complete in <50ms; a step that calls external APIs should add its own timeout. Worst case: the migrator hangs, the next update boundary sees the same situation; the user can `kill` the process and the ledger remains consistent.
66
+ - **The `announceOnce` sink does not retry.** If a sink fails (e.g. Telegram offline), the announcement is recorded as shown but never delivered. This is the deliberate signal-vs-authority separation: the brittle sink layer does not have authority over the ledger. A future enhancement could add a separate delivery queue; F-7 keeps the primitive minimal.
67
+ - **Ledger corruption is treated as empty.** If `migrator-steps-completed.json` becomes corrupt (truncated, bad JSON), the engine falls back to an empty ledger. This is intentional — the alternative (refuse to migrate) would be worse. Cost: a corrupt ledger causes one round of step re-runs; each step is idempotent by spec.
68
+
69
+ ---
70
+
71
+ ## 3. Level-of-abstraction fit
72
+
73
+ **Is this at the right layer?**
74
+
75
+ Yes. The two primitives sit at the `src/core/` layer — the same layer as `PostUpdateMigrator`, `GitStateManager`, `BackupManager`. They compose on top of `node:fs` only; no dependency on the Remediator (Tier-2 surfaces will register their own steps via `migrator.registerStep(...)` after import). The A35 const-literal extensions sit where the const lives — `GitStateManager` for the gitignore, `BackupManager` for the exclusion prefixes. No new plugin/register API was introduced per A50's explicit ruling.
76
+
77
+ The `PostUpdateMigrator` class is the right owner for the public-facing `registerStep`/`runPendingSteps` methods even though the engine logic lives in `MigratorStepEngine` — the existing class is already the documented entry point for "thing that runs on update boundary". Tier-2/Tier-3 surfaces (S-2, S-3) will reach for `PostUpdateMigrator` first.
78
+
79
+ ---
80
+
81
+ ## 4. Signal-vs-authority compliance
82
+
83
+ The brittle/low-context layers (file I/O ledger reads, sink invocations) are signal layers. The higher-context layer (the `runPendingSteps` orchestrator) is the authority. Specifically:
84
+
85
+ - A corrupt ledger does NOT block migration — the ledger is treated as a signal ("here's what we believe happened"), and the engine recovers by treating it as empty. Authority over "should this step run" lives in the in-memory step list + the version comparison.
86
+ - A failing sink does NOT block `announceOnce` from recording the announcement — sink delivery is signal; the ledger record is authority over "have we already attempted to surface this".
87
+ - A failing step does NOT block other steps — the per-step try/catch isolates signals; the engine retains authority over the run sequence.
88
+
89
+ This matches the §A50 / signal-vs-authority principle in MEMORY.md.
90
+
91
+ ---
92
+
93
+ ## 5. Interactions with existing surfaces
94
+
95
+ - **`PostUpdateMigrator.migrate()` is untouched.** Existing callers (`src/update/UpdateService.ts`, `src/commands/init.ts`, etc.) continue to work — they call `migrate()` and never touch the new methods.
96
+ - **`GitStateManager.init()`** — writes `DEFAULT_GITIGNORE` only when no `.gitignore` exists. Existing repos with `.gitignore` already on disk are unaffected. New repos get the remediation entries pre-included.
97
+ - **`BackupManager` callers** — 14 existing call sites pass at most 4 constructor args. The new 5th arg is optional + defaults to no-op behavior. Verified by running the full `backup-manager.test.ts` (44 cases) and `BackupManager-sharedState.test.ts` (3 cases) — all pass unchanged.
98
+ - **`upgrades/NEXT.md`** — only appended to (preserving the W-1, F-8, F-3, F-4, F-1..F-2 entries already on main).
99
+
100
+ ---
101
+
102
+ ## 6. Rollback cost
103
+
104
+ Low. The two new modules (`MigratorStepEngine.ts`, the new methods on `PostUpdateMigrator.ts`) are dormant unless something calls `registerStep` — and nothing calls it in this PR. Tier-2 surfaces will be the first consumers. Reverting requires:
105
+
106
+ 1. `git revert` the PR. No data migration. No format changes. Existing `migrate()` behavior is untouched.
107
+ 2. If any agent has already created `migrator-steps-completed.json` or `announcements-shown.json` files, they become unused leftovers — no harm. They can be deleted at leisure.
108
+
109
+ The A35 const-literal changes are inline edits to existing files; revert restores the prior literal. The `BackupManager`'s 5th constructor arg is optional and unused by every current caller, so removing it has zero blast radius.
110
+
111
+ ---
112
+
113
+ ## Tests
114
+
115
+ - `tests/unit/PostUpdateMigrator-atomicStep.test.ts` — 8 cases covering: step runs once and records completion; step is skipped on subsequent runs; failed step records failure and doesn't block other steps; future-version step skipped without ledger entry; state persists across instances; semver compare correctness.
116
+ - `tests/unit/AnnouncementManager.test.ts` — 5 cases covering: first-true / subsequent-false; independent ids; persistence across instances; sink-throw does not cause re-emission; input validation.
117
+ - `tests/unit/PostUpdateMigrator-a35-remediationPaths.test.ts` — 5 cases covering: `DEFAULT_GITIGNORE` contains all five remediation entries; `REMEDIATION_EXCLUDED_PATH_PREFIXES` exported correctly; gate ON drops remediation entries; gate OFF preserves them; absent callback = back-compat (gate OFF).
118
+
119
+ All 18 new tests pass. All 76 pre-existing `PostUpdateMigrator-*` tests pass unchanged. All 47 `BackupManager*` tests pass unchanged. TypeScript clean (`tsc --noEmit`).
120
+
121
+ ---
122
+
123
+ ## Gate trace
124
+
125
+ - side-effects review: this document
126
+ - NEXT.md: amended, preserving every prior entry
127
+ - spec citation: `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` §R1 + §A35 + §A50 + §A57
128
+ - worktree: `/tmp/instar-f7-post-update-migrator` (branch `f7-post-update-migrator`)
129
+ - no bypass; no `--no-verify`