instar 1.3.761 → 1.3.763

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 (134) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +92 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +28 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/FileClassifier.d.ts.map +1 -1
  8. package/dist/core/FileClassifier.js +5 -0
  9. package/dist/core/FileClassifier.js.map +1 -1
  10. package/dist/core/PostUpdateMigrator.d.ts +12 -0
  11. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  12. package/dist/core/PostUpdateMigrator.js +54 -0
  13. package/dist/core/PostUpdateMigrator.js.map +1 -1
  14. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  15. package/dist/core/WriteDomainRegistry.js +16 -0
  16. package/dist/core/WriteDomainRegistry.js.map +1 -1
  17. package/dist/core/componentCategories.d.ts.map +1 -1
  18. package/dist/core/componentCategories.js +1 -0
  19. package/dist/core/componentCategories.js.map +1 -1
  20. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  21. package/dist/core/devGatedFeatures.js +6 -0
  22. package/dist/core/devGatedFeatures.js.map +1 -1
  23. package/dist/core/types.d.ts +50 -0
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/core/types.js.map +1 -1
  26. package/dist/data/llmBenchCoverage.d.ts.map +1 -1
  27. package/dist/data/llmBenchCoverage.js +18 -0
  28. package/dist/data/llmBenchCoverage.js.map +1 -1
  29. package/dist/monitoring/ExternalHogArmMarker.d.ts +65 -0
  30. package/dist/monitoring/ExternalHogArmMarker.d.ts.map +1 -0
  31. package/dist/monitoring/ExternalHogArmMarker.js +0 -0
  32. package/dist/monitoring/ExternalHogArmMarker.js.map +1 -0
  33. package/dist/monitoring/ExternalHogArmStore.d.ts +46 -0
  34. package/dist/monitoring/ExternalHogArmStore.d.ts.map +1 -0
  35. package/dist/monitoring/ExternalHogArmStore.js +124 -0
  36. package/dist/monitoring/ExternalHogArmStore.js.map +1 -0
  37. package/dist/monitoring/ExternalHogClassifier.d.ts +62 -0
  38. package/dist/monitoring/ExternalHogClassifier.d.ts.map +1 -0
  39. package/dist/monitoring/ExternalHogClassifier.js +0 -0
  40. package/dist/monitoring/ExternalHogClassifier.js.map +1 -0
  41. package/dist/monitoring/ExternalHogClassifierPrompt.d.ts +29 -0
  42. package/dist/monitoring/ExternalHogClassifierPrompt.d.ts.map +1 -0
  43. package/dist/monitoring/ExternalHogClassifierPrompt.js +70 -0
  44. package/dist/monitoring/ExternalHogClassifierPrompt.js.map +1 -0
  45. package/dist/monitoring/ExternalHogCpuDelta.d.ts +50 -0
  46. package/dist/monitoring/ExternalHogCpuDelta.d.ts.map +1 -0
  47. package/dist/monitoring/ExternalHogCpuDelta.js +79 -0
  48. package/dist/monitoring/ExternalHogCpuDelta.js.map +1 -0
  49. package/dist/monitoring/ExternalHogFactBuilder.d.ts +71 -0
  50. package/dist/monitoring/ExternalHogFactBuilder.d.ts.map +1 -0
  51. package/dist/monitoring/ExternalHogFactBuilder.js +111 -0
  52. package/dist/monitoring/ExternalHogFactBuilder.js.map +1 -0
  53. package/dist/monitoring/ExternalHogFloor.d.ts +87 -0
  54. package/dist/monitoring/ExternalHogFloor.d.ts.map +1 -0
  55. package/dist/monitoring/ExternalHogFloor.js +123 -0
  56. package/dist/monitoring/ExternalHogFloor.js.map +1 -0
  57. package/dist/monitoring/ExternalHogGuardStatus.d.ts +29 -0
  58. package/dist/monitoring/ExternalHogGuardStatus.d.ts.map +1 -0
  59. package/dist/monitoring/ExternalHogGuardStatus.js +27 -0
  60. package/dist/monitoring/ExternalHogGuardStatus.js.map +1 -0
  61. package/dist/monitoring/ExternalHogKillFunnel.d.ts +77 -0
  62. package/dist/monitoring/ExternalHogKillFunnel.d.ts.map +1 -0
  63. package/dist/monitoring/ExternalHogKillFunnel.js +72 -0
  64. package/dist/monitoring/ExternalHogKillFunnel.js.map +1 -0
  65. package/dist/monitoring/ExternalHogKillLedger.d.ts +76 -0
  66. package/dist/monitoring/ExternalHogKillLedger.d.ts.map +1 -0
  67. package/dist/monitoring/ExternalHogKillLedger.js +84 -0
  68. package/dist/monitoring/ExternalHogKillLedger.js.map +1 -0
  69. package/dist/monitoring/ExternalHogNoticeCoalescer.d.ts +46 -0
  70. package/dist/monitoring/ExternalHogNoticeCoalescer.d.ts.map +1 -0
  71. package/dist/monitoring/ExternalHogNoticeCoalescer.js +67 -0
  72. package/dist/monitoring/ExternalHogNoticeCoalescer.js.map +1 -0
  73. package/dist/monitoring/ExternalHogOwnership.d.ts +48 -0
  74. package/dist/monitoring/ExternalHogOwnership.d.ts.map +1 -0
  75. package/dist/monitoring/ExternalHogOwnership.js +60 -0
  76. package/dist/monitoring/ExternalHogOwnership.js.map +1 -0
  77. package/dist/monitoring/ExternalHogProcTable.d.ts +37 -0
  78. package/dist/monitoring/ExternalHogProcTable.d.ts.map +1 -0
  79. package/dist/monitoring/ExternalHogProcTable.js +65 -0
  80. package/dist/monitoring/ExternalHogProcTable.js.map +1 -0
  81. package/dist/monitoring/ExternalHogRealAdapters.d.ts +91 -0
  82. package/dist/monitoring/ExternalHogRealAdapters.d.ts.map +1 -0
  83. package/dist/monitoring/ExternalHogRealAdapters.js +247 -0
  84. package/dist/monitoring/ExternalHogRealAdapters.js.map +1 -0
  85. package/dist/monitoring/ExternalHogSampler.d.ts +62 -0
  86. package/dist/monitoring/ExternalHogSampler.d.ts.map +1 -0
  87. package/dist/monitoring/ExternalHogSampler.js +0 -0
  88. package/dist/monitoring/ExternalHogSampler.js.map +1 -0
  89. package/dist/monitoring/ExternalHogScanTick.d.ts +89 -0
  90. package/dist/monitoring/ExternalHogScanTick.d.ts.map +1 -0
  91. package/dist/monitoring/ExternalHogScanTick.js +126 -0
  92. package/dist/monitoring/ExternalHogScanTick.js.map +1 -0
  93. package/dist/monitoring/ExternalHogSentinel.d.ts +127 -0
  94. package/dist/monitoring/ExternalHogSentinel.d.ts.map +1 -0
  95. package/dist/monitoring/ExternalHogSentinel.js +168 -0
  96. package/dist/monitoring/ExternalHogSentinel.js.map +1 -0
  97. package/dist/monitoring/ExternalHogServerPrimitives.d.ts +50 -0
  98. package/dist/monitoring/ExternalHogServerPrimitives.d.ts.map +1 -0
  99. package/dist/monitoring/ExternalHogServerPrimitives.js +93 -0
  100. package/dist/monitoring/ExternalHogServerPrimitives.js.map +1 -0
  101. package/dist/monitoring/ExternalHogSustained.d.ts +49 -0
  102. package/dist/monitoring/ExternalHogSustained.d.ts.map +1 -0
  103. package/dist/monitoring/ExternalHogSustained.js +62 -0
  104. package/dist/monitoring/ExternalHogSustained.js.map +1 -0
  105. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  106. package/dist/monitoring/guardManifest.js +24 -0
  107. package/dist/monitoring/guardManifest.js.map +1 -1
  108. package/dist/scaffold/templates.d.ts.map +1 -1
  109. package/dist/scaffold/templates.js +2 -1
  110. package/dist/scaffold/templates.js.map +1 -1
  111. package/dist/server/AgentServer.d.ts +3 -0
  112. package/dist/server/AgentServer.d.ts.map +1 -1
  113. package/dist/server/AgentServer.js +1 -0
  114. package/dist/server/AgentServer.js.map +1 -1
  115. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  116. package/dist/server/CapabilityIndex.js +1 -0
  117. package/dist/server/CapabilityIndex.js.map +1 -1
  118. package/dist/server/routes.d.ts +3 -0
  119. package/dist/server/routes.d.ts.map +1 -1
  120. package/dist/server/routes.js +64 -0
  121. package/dist/server/routes.js.map +1 -1
  122. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  123. package/dist/testing/selfActionRegistry.js +51 -0
  124. package/dist/testing/selfActionRegistry.js.map +1 -1
  125. package/package.json +1 -1
  126. package/scripts/lint-scrape-fixture-realness.js +14 -0
  127. package/scripts/model-registry-freshness.manifest.json +2 -2
  128. package/src/data/builtin-manifest.json +64 -64
  129. package/src/data/llmBenchCoverage.ts +18 -0
  130. package/src/scaffold/templates.ts +2 -1
  131. package/upgrades/1.3.762.md +29 -0
  132. package/upgrades/1.3.763.md +23 -0
  133. package/upgrades/side-effects/doorway-model-registry-inc5.md +118 -0
  134. package/upgrades/side-effects/external-hog-sentinel.md +832 -0
@@ -0,0 +1,832 @@
1
+ # Side-Effects Review — External-Hog Zombie Auto-Kill Sentinel
2
+
3
+ Spec: `docs/specs/external-hog-zombie-autokill-sentinel.md` (CMT-1901, converged 11 rounds, approved by operator 2026-07-03).
4
+ Build branch: `echo/external-hog-sentinel` off `JKHeadley/main` @ v1.3.748.
5
+ Tier: **2** (safety-critical, irreversible action — process kill).
6
+
7
+ ## Phase 1 — Principle check (signal vs authority)
8
+
9
+ **Does this change involve a decision point?** YES — it gates an irreversible action
10
+ (killing an external OS process).
11
+
12
+ **Compliance:** The design is signal-vs-authority-COMPLIANT and this was the single
13
+ most-reviewed axis across 11 rounds. Split of authority:
14
+ - The **mechanical safety floor** is a VETO-ONLY hard-invariant guard on an
15
+ irreversible action — the allowed "brittle blocker" class (a hard invariant guarding
16
+ an irreversible action, per `docs/signal-vs-authority.md`). It can only ever BLOCK a
17
+ kill (downgrade to alert); it can never trigger one.
18
+ - The **LLM classifier** holds the judgment authority (the smart gate) — kill/leave/
19
+ alert — fed the deterministically-computed facts. Its authority is purely
20
+ SUBTRACTIVE: `kill executes iff floor_pass && classifier === 'kill'`.
21
+ - The **observability floor** is a signal producer: every confirmed sustained hog that
22
+ is not killed is surfaced; the model cannot suppress it.
23
+
24
+ This is the correct shape: brittle deterministic logic produces hard vetoes + signals;
25
+ the intelligent full-context gate makes the judgment call. Adding blocking authority to
26
+ a brittle check is exactly what the design AVOIDS — the floor never authorizes, only
27
+ refuses.
28
+
29
+ ## Phase 2 — Plan (build location + interactions)
30
+
31
+ - **Build location (re-grounded):** FRESH worktree off `JKHeadley/main` @ **v1.3.748**
32
+ (the agent-home checkout was 243 commits behind — building here would be stale). git
33
+ remote = JKHeadley; per-worktree identity set by `instar worktree create`.
34
+ - **Re-grounding against master (all confirmed present):** `resolveOwningSession(pid,
35
+ tree, tmuxPaneMap, maxHops)` (McpProcessReaper.ts:111); `parseProcTimeToSeconds`
36
+ (SessionManager.ts:201) — the load-bearing parser to register in `SCRAPE_PARSERS`;
37
+ `devGatedFeatures.ts` with `credentialRepointing` (the Posture-A precedent) + the
38
+ sibling destructive killers `sessionReaper`/`agentWorktreeReaper`/`mcpProcessReaper`
39
+ (DARK_GATE_EXCLUSIONS); `migrateDevGateTeethStrip` (PostUpdateMigrator.ts:1353,
40
+ "Allowlist is HARDCODED"). `zombie-classify.json` benchmark brought into the branch
41
+ (was not in master).
42
+ - **Interactions:** de-conflict with McpProcessReaper + SessionManager (exclude their
43
+ pids from discovery + kill), suppress OrphanProcessReaper's legacy external-report for
44
+ in-lane pids; ride the shared LlmQueue background lane + hostSpawnCap; wire into
45
+ guard-posture + the Guard-Posture Tripwire.
46
+ - **Rollback path:** ships dev-gated dark-on-fleet + `dryRun:true` (kills nothing);
47
+ live killing requires the PIN arm route. Back-out = disarm route (writes dryRun:true +
48
+ invalidates the marker) or config `enabled:false`; no data migration; feature removal
49
+ leaves inert leftover state (sampler snapshot, kill ledger, audit log).
50
+
51
+ ## Phase 4 — Side-effects review (per-question)
52
+
53
+ _(Filled in progressively as slices land; this baseline commit is the spec + benchmark
54
+ + plan artifacts brought into the branch — no runtime `src/` change yet.)_
55
+
56
+ 1. **Over-block:** N/A for the baseline; the runtime design over-blocks toward NOT
57
+ killing (every uncertainty → alert-never-kill), which is the safe direction.
58
+ 2. **Under-block:** the narrow v1 allowlist deliberately does NOT auto-kill non-exthost
59
+ hogs (they are surfaced, not killed) — an accepted, evidence-gated scope.
60
+ 3. **Level-of-abstraction fit:** owns its own uid-scoped host-process discovery because
61
+ the existing OrphanProcessReaper is structurally blind to the target class
62
+ (framework-needle pre-filter). Correct layer.
63
+ 4. **Signal vs authority:** compliant (Phase 1).
64
+ 5. **Interactions:** de-conflicted (Phase 2).
65
+ 6. **External surfaces:** new `GET/POST /external-hog*` routes (Bearer/PIN-authed);
66
+ coalesced Telegram notice on the deterministic delivery path; guard-posture row.
67
+ 7. **Multi-machine posture:** machine-local BY DESIGN (`hardware-bound-resource` — a host
68
+ OS process is bound to one kernel); cross-machine visibility via each machine's own
69
+ `GET /external-hog` + pool-scope `/guards`. Confirmed correct by review.
70
+ 8. **Rollback cost:** low — disarm route / config flip; no migration.
71
+
72
+ ## Slice log
73
+
74
+ ### Slice 2 — config schema + dev-gate registration
75
+ Files: `src/core/types.ts` (the `externalHogSentinel?` interface), `src/config/ConfigDefaults.ts`
76
+ (defaults — `enabled` OMITTED, `dryRun: true`, all kill-gate knobs), `src/core/devGatedFeatures.ts`
77
+ (DEV_GATED_FEATURES entry with the credentialRepointing-style justification),
78
+ `tests/unit/external-hog-sentinel-config.test.ts`.
79
+ - **Side effects:** NONE at runtime — this slice adds a DORMANT config block + a registry
80
+ entry. No code consumes it yet, so no behavior changes on any agent. It resolves the
81
+ §7/§8 obligation that `enabled` be omitted (dev-gate) + registered in DEV_GATED_FEATURES
82
+ (the #1001 wiring guarantee — auto-covered by `devGatedFeatures-wiring.test.ts`, 151 pass).
83
+ - **Over/under-block:** N/A (no decision logic yet).
84
+ - **Signal vs authority:** N/A (config only).
85
+ - **Multi-machine:** config is machine-local; the feature's posture is `hardware-bound-resource`.
86
+ - **Tests:** 6 focused (dev-gate live/dark, dryRun canary on both, enabled-omitted, all
87
+ defaults, the `maxClassificationsPerScan < hostSpawnCap` invariant) + the 151-test wiring
88
+ suite. Typecheck clean.
89
+
90
+ ### Slice 3 — deterministic safety floor (the veto-only kill envelope)
91
+ Files: `src/monitoring/ExternalHogFloor.ts` (the `ExternalHogFacts` type, the code-defined
92
+ `EXTERNAL_HOG_ALLOWLIST` + `matchAllowlistClass`, and `evaluateKillFloor` — the pure
93
+ veto-only predicate), `tests/unit/external-hog-floor.test.ts` (24 tests).
94
+ - **Over-block:** the floor fails CLOSED — any unknown invariant (unknown uid, missing
95
+ field) → NOT permitted → alert. It "over-blocks" toward NOT killing, which is the correct
96
+ safe direction for an irreversible action. It does not reject any legitimate KILL that a
97
+ real orphaned in-envelope zombie would need (the anchor case permits).
98
+ - **Under-block:** the floor is a NECESSARY, not sufficient, condition — it never authorizes
99
+ a kill on its own (the caller must ALSO have `classifier==='kill'`). It cannot under-block
100
+ a kill because it only ever removes kills.
101
+ - **Level-of-abstraction fit:** pure function over a normalized fact set; the discovery/
102
+ sampler layer computes the facts, the floor evaluates them, the caller ANDs with the model
103
+ verdict. Correct separation.
104
+ - **Signal vs authority:** COMPLIANT — this is the hard-invariant guard on an irreversible
105
+ action (the allowed brittle-blocker class). It holds VETO authority only; the model holds
106
+ the judgment. It can only BLOCK a kill, never trigger one (structurally: the return type is
107
+ permit-or-veto, and the only `permitted:true` path requires every invariant to pass).
108
+ - **Interactions:** the allowlist is code-defined (not config), so no runtime widening;
109
+ instar-own exclusion is checked first (defense-in-depth vs the discovery-layer exclusion).
110
+ - **Multi-machine:** pure logic, no state — N/A.
111
+ - **External surfaces:** none — no I/O, no routes, no messages. Dormant until a runtime
112
+ consumer wires it (later slices).
113
+ - **Rollback:** delete the module; nothing consumes it yet.
114
+ - **Tests:** 24 — allowlist match (name+token, attacker-name inert, anchored regex), the
115
+ anchor permits, EVERY invariant load-bearing (instar-own, other-uid, root-euid, unknown-uid,
116
+ root-daemon, launchctl, live-parent, not-sustained, outside-allowlist ×2), and the 8
117
+ zombie-classify cases as floor fixtures (permits only the exthost-kill case). Typecheck clean.
118
+
119
+ ### Slice 4 — CPU-delta signal core (monotonic clock, fail-closed)
120
+ Files: `src/monitoring/ExternalHogCpuDelta.ts` (`monotonicNowMs`, `computeCoreEquivalents`,
121
+ `meetsThreshold`, the `CPU_DELTA_UNKNOWN` sentinel), `tests/unit/external-hog-cpu-delta.test.ts`
122
+ (12 tests).
123
+ - **What it is:** the pure `Δcputime / Δwall` core-equivalents computation feeding the
124
+ `sustainedHighCpu` floor invariant. Δwall is a MONOTONIC clock (sleep-paused, NTP-immune).
125
+ - **Over/under-block:** it produces a SIGNAL, not a decision — it never kills. Its only
126
+ safety obligation is to never produce a FALSE "sustained hog" (which the floor would then
127
+ treat as a passing invariant). It fails CLOSED: non-positive Δwall, implausibly-large Δwall
128
+ (a sleep slipped through), a decreasing counter (pid reuse), or any non-finite input →
129
+ `CPU_DELTA_UNKNOWN`, and `meetsThreshold(UNKNOWN, …) === false` — so an unknown reading is
130
+ NEVER a confirmed hog. It cannot over-report an idle process as sustained.
131
+ - **Signal vs authority:** signal-only; feeds the deterministic floor. Compliant.
132
+ - **Multi-machine:** pure logic, no state — N/A.
133
+ - **External surfaces:** none — no I/O, no subprocess (the actual `ps` sampling is a later
134
+ slice; this is only the delta math over samples).
135
+ - **Rollback:** delete the module; nothing consumes it yet.
136
+ - **Tests:** 12 — core-equiv math (2 cores / idle / 1 core), fail-closed on every implausible
137
+ interval (backward/same-instant Δwall, 3h-sleep Δwall, decreasing counter, NaN/Infinity),
138
+ jitter tolerance within the factor, `meetsThreshold` (UNKNOWN never a hog), monotonicity.
139
+
140
+ ### Slice 5 — armed-marker gate (the doubly-held "can this kill" logic)
141
+ Files: `src/monitoring/ExternalHogArmMarker.ts` (`classContentHash`, `isMarkerValid`,
142
+ `classIsArmed`, `canKillLive`), `tests/unit/external-hog-arm-marker.test.ts` (16 tests).
143
+ - **What it is:** the second key holding a LIVE kill (beyond enabled && !dryRun) — a valid
144
+ PIN-written marker with the arm-epoch lifecycle + the per-class content-hash arm-scope.
145
+ - **Signal vs authority:** pure authorization predicate; never kills. Fails CLOSED on any
146
+ missing/invalid input (no marker, non-finite epoch, dryRun:true, disarmed).
147
+ - **Security properties (round-9 review, now in code):** (1) armEpoch > lastDisarmEpoch — a
148
+ disarm can never be silently un-done; `disarm→config dryRun:false→restart` boots UNARMED;
149
+ config.dryRun:false is NEVER a positive arm. (2) per-class content-hash — new/broadened
150
+ class → not armed → alert-only until PIN re-arm; unrelated class add doesn't disarm others.
151
+ - **External surfaces:** none yet (the arm/disarm ROUTES that write the marker are a later
152
+ slice); this is the pure predicate they'll consume. Multi-machine: pure logic.
153
+ - **Rollback:** delete the module; nothing consumes it yet.
154
+ - **Tests:** 16 — content-hash determinism/change-sensitivity/order, epoch validity + the
155
+ disarm-restart-bypass-closed, per-class armed/new/broadened/unrelated, canKillLive doubly-
156
+ held (dryRun-never-kills, bare-config-flip-never-arms, enabled:false, unarmed class).
157
+
158
+ ### Slice 6 — P19 loop brakes (kill-ledger + respawn breaker + in-flight set)
159
+ Files: `src/monitoring/ExternalHogKillLedger.ts` (`recordKill`, `isBreakerTripped`,
160
+ `killCountInWindow`, `shouldEvictInFlight`, `isInFlight`), `tests/unit/external-hog-kill-ledger.test.ts` (14 tests).
161
+ - **What it is:** the pure state machines that STOP a kill-respawn loop (#863). After K kills
162
+ of the same respawn-surviving key in a rolling window → breaker trips (stop killing +
163
+ surface). A VOLATILE key falls back to a CLASS-level breaker (a per-volatile-key count could
164
+ never accumulate). The in-flight set stops re-killing a SIGTERM'd pid; eviction on confirmed
165
+ exit or the TTL.
166
+ - **Safe direction:** for a LOOP BRAKE the safe failure is TRIP (stop killing) — a non-finite
167
+ window/now returns tripped rather than risk an unbounded loop. The breaker shields same-key
168
+ hogs from KILL only, never from the §4 observability floor.
169
+ - **Bounded:** `recordKill` prunes past the retention bound so the durable ledger can't grow
170
+ without limit. In-flight eviction (confirmed-exit OR ttl, ~3×sigtermGrace) prevents a leak
171
+ and prevents re-killing a mid-write LS early.
172
+ - **External surfaces:** none yet (durable persistence + the actual kill are the caller's);
173
+ pure logic. Multi-machine: machine-local (the kill ledger is per-machine).
174
+ - **Rollback:** delete the module; nothing consumes it yet.
175
+ - **Tests:** 14 — append+prune, per-key breaker (trip at K, ignore out-of-window, a decoy
176
+ can't shield another key), volatile→class fallback, safe-trip on bad window inputs, in-flight
177
+ detect + pid-reuse distinction + TTL/confirmed-exit/non-finite eviction.
178
+
179
+ ### Slice 7 — instar-own exclusion (ancestry walk, start-time-aware, own-root fallback)
180
+ Files: `src/monitoring/ExternalHogOwnership.ts` (`isInstarOwned` over a ProcTree snapshot),
181
+ `tests/unit/external-hog-ownership.test.ts` (11 tests).
182
+ - **What it is:** the pure ancestry walk that EXCLUDES a candidate whose chain reaches a
183
+ start-time-verified instar-owned pid (tmux pane OR own-root), so own busy build children
184
+ (vitest/tsc) aren't flagged as hogs. Extends resolveOwningSession with per-hop start-time.
185
+ - **Dangerous direction:** a false "owned" would HIDE a real external hog. That requires a
186
+ start-time MATCH on an owned pid, so an external hog can't fake it. INCLUDE-on-uncertainty
187
+ (anti-evasion): an unresolvable edge / cycle / hop-bound → NOT owned (candidate stays);
188
+ instar-own KILL protection is carried by reparent-to-pid-1 + the §4 allowlist floor, not
189
+ this walk — so INCLUDE-on-uncertainty costs only observability noise, never a wrong kill.
190
+ - **Signal vs authority:** produces a candidacy signal; never kills. Multi-machine: pure.
191
+ - **External surfaces:** none yet (the ps read that builds the tree + the owned-pid set are a
192
+ later I/O slice). Rollback: delete; nothing consumes it yet.
193
+ - **Tests:** 11 — direct/deep/self owned; genuine orphan (ppid 1) not owned; start-time
194
+ defeats pid reuse (reused number ≠ owned, matching start-time excludes); unresolvable edge
195
+ / cycle / hop-bound → not owned; invalid inputs → not owned.
196
+
197
+ ### Slice 8 — P17 notice coalescer (notification bounding)
198
+ Files: `src/monitoring/ExternalHogNoticeCoalescer.ts` (`coalesceNotices`),
199
+ `tests/unit/external-hog-notice-coalescer.test.ts` (9 tests).
200
+ - **What it is:** the pure P17 selection logic — one coalescing chokepoint over all notice
201
+ classes (kill / decider-unavailable / floor-veto-downgrade / hog-left-alive) with
202
+ per-signature dedup, a per-window budget, severity ordering on exhaustion, and live KILLS
203
+ always piercing the budget. It NEVER kills — it selects which NOTICES to emit vs drop.
204
+ - **Signal vs authority:** notification bounding, NOT a kill/block decision. The safety-
205
+ critical second-pass (kill logic) does not apply; the risk is bounded (worst case: a dropped
206
+ LOW-severity notice — a kill notice can never be dropped, and dedup prevents a flood).
207
+ - **Multi-machine:** pure, machine-local (notices are per-machine). External surfaces: none
208
+ yet (the actual delivery + window state are the caller's).
209
+ - **Rollback:** delete; nothing consumes it yet.
210
+ - **Tests:** 9 — dedup (in-batch, vs-window, different-class-not-deduped), budget + severity
211
+ ordering (keeps highest severity, reports dropped-by-class), kills-always-pierce, robustness
212
+ (zero/negative/NaN budget, malformed notice ignored, empty batch).
213
+
214
+ ### Slice 9 — ps whole-table parser + realness fixture (load-bearing, SCRAPE_PARSERS)
215
+ Files: `src/monitoring/ExternalHogProcTable.ts` (`parseProcTable`),
216
+ `tests/unit/external-hog-proc-table.test.ts` (5 tests), the captured fixture
217
+ `tests/fixtures/captured/ps-proc-table/{table.txt,table.meta.json}`, and the SCRAPE_PARSERS
218
+ registration in `scripts/lint-scrape-fixture-realness.js`.
219
+ - **What it is:** the whole-table `ps -o pid=,ppid=,uid=,lstart=,time=,comm=` parse into rows
220
+ (pid/ppid/uid/startTime/cputimeSeconds/comm) the sampler uses. LOAD-BEARING for kill
221
+ eligibility (the CPU-delta pivots on `time=`), so REGISTERED in SCRAPE_PARSERS with a
222
+ captured realness fixture (§Testing F1, resolves the round-9 lessons-aware blocker).
223
+ - **Dangerous direction:** a parse bug that OVER-reports cputime → a false sustained hog. The
224
+ fixture proves the parser survives the real structural bytes (dd- day-prefix, embedded-space
225
+ lstart + comm, <defunct>, malformed short) and fails CLOSED: unidentifiable pid/ppid/uid →
226
+ row SKIPPED; malformed `time=` → cputimeSeconds undefined → CPU-delta UNKNOWN → alert-never-kill.
227
+ - **Signal vs authority:** parsing only; feeds the deterministic CPU-delta. Multi-machine: pure.
228
+ - **External surfaces:** the parser is pure (the actual `ps` spawn is the sampler slice).
229
+ parseProcTimeToSeconds stays a non-blocking register-or-justify note (a field parser, not a
230
+ whole-output parser — exercised transitively through this fixture).
231
+ - **Rollback:** delete the module + fixture + registration entry.
232
+ - **Tests:** 5 — the byte-for-byte fixture parse (7 rows, 1 malformed skipped; dd- anchor =
233
+ 106920s; comm-with-spaces preserved; <defunct>), the time= realness (dd- day-prefix), and
234
+ fail-closed (malformed time → undefined, non-numeric pid → skip, non-string → []).
235
+
236
+ ### Slice 10 — stage-1 candidacy state machine (pure)
237
+ Files: `src/monitoring/ExternalHogSampler.ts` (`advanceSampler`, `isSamplerDead`),
238
+ `tests/unit/external-hog-sampler.test.ts` (13 tests).
239
+ - **What it is:** the pure stage-1 candidacy computation — from two successive ps snapshots it
240
+ selects external (own-uid, not instar-owned) processes whose cross-tick Δcputime/Δwall
241
+ crosses the threshold. Integrates the CPU-delta + ownership walk + parsed rows. Rebuilds the
242
+ identity map each tick (bounded). A liveness heartbeat advances only on a plausible parse.
243
+ - **Dangerous direction:** a false CANDIDATE (an idle process flagged as a hog). Guarded: uses
244
+ the DELTA not lifetime average (emergent hog caught, idle not); different-uid / instar-owned /
245
+ unknown-cputime / first-sight → never a candidate; UNKNOWN delta → excluded.
246
+ - **Signal vs authority:** produces a candidacy signal; never kills. The actual ps spawn + wall
247
+ clock live in the thin I/O worker shell (later); this is pure. Multi-machine: pure.
248
+ - **Heartbeat:** advances on any plausible parse (≥1 row) regardless of candidate count (idle
249
+ machine not sampler-dead); a failed/empty parse does NOT advance (→ eventually sampler-dead)
250
+ and keeps the previous baseline (a transient hiccup doesn't lose the delta baseline).
251
+ - **Rollback:** delete; the sentinel class (later) consumes it.
252
+ - **Tests:** 13 — baseline/candidate/emergent-hog/low-cpu; exclusions (uid, instar-own, unknown
253
+ cputime); heartbeat (advances on zero-candidate plausible parse, not on empty/non-finite);
254
+ isSamplerDead (null=not-dead, fresh/stale, non-finite-now=not-dead).
255
+
256
+ ### Slice 11 — classifier orchestration (pure: verdict parse, cap-select, cache)
257
+ Files: `src/monitoring/ExternalHogClassifier.ts` (`parseClassifierVerdict`,
258
+ `selectForClassification`, the TTL+LRU `VerdictCache`), `tests/unit/external-hog-classifier.test.ts` (13 tests).
259
+ - **What it is:** the pure orchestration around the model call (the actual LlmQueue call is the
260
+ sentinel-class adapter). Verdict parse (bounded enum, fail-safe), worst-CPU-first selection
261
+ under maxClassificationsPerScan, and the identity-tuple verdict cache.
262
+ - **Dangerous direction:** `parseClassifierVerdict` returning 'kill' when it shouldn't. Guarded:
263
+ ONLY an exact `kill`/`leave`/`alert` (bare or `{action}`) parses; anything else → null →
264
+ decider-unavailable → ALERT, never kill. NEVER extracts a pid/target from output.
265
+ - **Cap-select:** worst-CPU-first so a decoy flood can't starve the real hog; deterministic
266
+ non-attacker tie-break; non-positive cap → classify none.
267
+ - **Cache:** keyed on the FULL identity tuple (pid+start-time+command-hash) so a reused pid
268
+ can't inherit a prior `kill`; TTL+LRU; advisory (the §4 kill-time re-check is authoritative;
269
+ a non-finite now/ttl → miss).
270
+ - **Signal vs authority:** the model decides WITHIN the floor; this is the pure plumbing. Rollback:
271
+ delete; the sentinel class consumes it. Multi-machine: pure.
272
+ - **Tests:** 13 — verdict parse (enum, JSON, unparseable→null, no-pid-extraction), worst-CPU-first
273
+ select (decoy-flood can't starve, tie-break, non-positive cap), cache (TTL, reused-pid-no-inherit,
274
+ LRU eviction, non-finite→miss).
275
+
276
+ ### Slice 12 — kill funnel (the hardened SIGTERM→SIGKILL sequence, the ONLY signal path)
277
+ Files: `src/monitoring/ExternalHogKillFunnel.ts` (`runKillFunnel`),
278
+ `tests/unit/external-hog-kill-funnel.test.ts` (10 tests).
279
+ - **What it is:** the ONLY place a real signal is sent. The watch-only guarantee is BY
280
+ CONSTRUCTION: unless canKillLive (enabled && !dryRun && a valid PIN marker for this class) at
281
+ BOTH re-check points, NO signal is sent (returns `would-kill`). All I/O injected → fully
282
+ testable without killing anything. Sequence: pre-SIGTERM arm-gate + Stage-B floor re-check →
283
+ SIGTERM → grace → exited? → pre-SIGKILL re-check (disarm/identity/floor mid-grace aborts) →
284
+ fd-skip defer (bounded) → SIGKILL.
285
+ - **Dangerous direction:** sending SIGKILL without full authorization. Guarded: canKillLive at
286
+ entry (would-kill/no-signal in watch-only) AND re-checked before SIGKILL; a disarm/identity-
287
+ change/floor-veto mid-grace aborts the escalation (the graceful SIGTERM already sent is not
288
+ forced to SIGKILL); class re-matched at kill time; fd-write defers (capped).
289
+ - **Signal vs authority:** executes the floor+model decision; never decides. Multi-machine:
290
+ machine-local (a host process). Rollback: delete; the sentinel class consumes it.
291
+ - **Tests:** 10 — dryRun/not-armed/disarmed → would-kill NO SIGNAL; floor-veto/gone → aborted NO
292
+ SIGNAL; sigterm-exit (SIGTERM only); full kill (SIGTERM+SIGKILL); disarm-mid-grace → aborted
293
+ (SIGTERM only, no SIGKILL); fd-write defer (SIGTERM only); defer-cap-exhausted → SIGKILL.
294
+
295
+ ### Slice 13 — scan-tick orchestrator (composes all modules; the feature is ALIVE)
296
+ Files: `src/monitoring/ExternalHogScanTick.ts` (`runScanTick`),
297
+ `tests/unit/external-hog-scan-tick.test.ts` (6 tests, end-to-end over injected I/O).
298
+ - **What it is:** the orchestrator tying every reviewed module into ONE scan tick: discovery
299
+ (sampler) → worst-CPU-first classify under the cap → floor → P19 breaker → kill funnel → P17
300
+ coalesced notices. All I/O injected (read ps, ownership, classify, funnel deps, clock,
301
+ deliver) → the whole tick is end-to-end testable without a real ps/model/signal.
302
+ - **Dangerous direction:** driving the funnel toward a kill it shouldn't. Guarded: a kill is
303
+ attempted ONLY when verdict==='kill' && floor.permitted && breaker-not-tripped; and even then
304
+ the funnel is watch-only unless armed (so the orchestrator can never cause a signal the funnel
305
+ wouldn't). Every non-killed sustained hog (leave / veto / decider-unavailable / breaker /
306
+ would-kill / over-cap) is SURFACED (§4 observability floor — the model can't silence a hog).
307
+ - **Watch-only rides through:** in the shipped dryRun state the funnel returns would-kill → no
308
+ signal → the tick produces would-kill records + observability notices, kills NOTHING.
309
+ - **Signal vs authority:** pure control flow composing the reviewed modules. Multi-machine:
310
+ machine-local. Rollback: delete; the server wiring (later) consumes it.
311
+ - **Tests:** 6 — watch-only hog→would-kill NO SIGNAL + surfaced; armed hog→killed + kill notice;
312
+ model-leave→alert-only+surfaced; decider-unavailable→notice; floor-veto→alert-only+notice;
313
+ idle→nothing.
314
+
315
+ ### Slice 14 — guard-posture status (pure §8 honesty rule)
316
+ Files: `src/monitoring/ExternalHogGuardStatus.ts` (`externalHogEffectiveState`),
317
+ `tests/unit/external-hog-guard-status.test.ts` (5 tests).
318
+ - **What it is:** the pure mapping of live state → a `GuardEffectiveState`. Enforces the §8
319
+ honesty rule: `on-confirmed` ONLY when actually kill-capable (enabled && !dryRun &&
320
+ marker-valid); the reachable config.dryRun:false + marker-absent state reads `on-dry-run`
321
+ (armed-pending mapped to on-dry-run in v1), never on-confirmed; a dead sampler → `on-stale`.
322
+ - **Signal vs authority:** a STATUS signal — it never kills or gates. Not kill-decision logic,
323
+ so the safety-critical second-pass does not apply; the honesty risk (a false on-confirmed) is
324
+ covered by the exhaustive branch tests. Multi-machine: pure (per-machine posture).
325
+ - **Rollback:** delete; the guard-posture wiring (server slice) consumes it.
326
+ - **Tests:** 5 — off/on-confirmed/honesty-on-dry-run/dryRun-soak/sampler-dead-on-stale.
327
+
328
+ ### Slice 15 — the composition shell (`ExternalHogSentinel`)
329
+ Files: `src/monitoring/ExternalHogSentinel.ts` (`ExternalHogSentinel`, `buildProcTree`),
330
+ `src/monitoring/ExternalHogScanTick.ts` (additive: `ScanOutcome` now carries `ledgerKey`/`classId`),
331
+ `tests/unit/external-hog-sentinel.test.ts` (5 tests).
332
+ - **What it is:** the ADAPTER LAYER that turns the reviewed pure modules into a live, tickable
333
+ monitor. It adds NO kill decision — every tick delegates the whole decision to the reviewed
334
+ `runScanTick`. Its only jobs: (a) hold cross-tick state the pure orchestrator cannot (sampler
335
+ baseline + kill ledger + per-signature deferral count); (b) bridge the async real reads (ps
336
+ spawn / owned-pid resolve) into the sync closures the orchestrator expects, by reading a
337
+ snapshot BEFORE the tick and closing over it; (c) persist the per-signature deferral count so
338
+ `maxKillDeferrals` actually bounds ACROSS ticks (proven behaviorally: an open-workspace-file
339
+ hog defers each scan under the cap, then proceeds to SIGKILL at the cap); (d) deliver notices
340
+ every tick; (e) expose the honest §8 `status()`.
341
+ - **The orchestrator amendment:** `ScanOutcome` gained `ledgerKey`+`classId` (additive — better
342
+ audit + lets the shell persist deferral counts without re-deriving identity). Not a logic
343
+ change; all 7 scan-tick tests stay green.
344
+ - **Watch-only ride-through:** in the shipped dryRun state a tick produces would-kill records +
345
+ §4 observability notices and signals NOTHING (test: `on-dry-run`, zero signals).
346
+ - **Fail-safe:** a read failure degrades to an empty tick (sampler heartbeat does not advance →
347
+ eventually on-stale); `auditTick` is best-effort (a write failure never breaks a tick).
348
+ - **Over-block / under-block:** none new — the shell cannot widen a kill (it only forwards to the
349
+ funnel, which is watch-only + floor-vetoed + arm-gated). The one risk it introduces is
350
+ UNBOUNDED memory in the deferral map; bounded by `deferralMapMax` (default 128, oldest-pruned)
351
+ AND by terminal-clear (a resolved/killed/gone signature is deleted).
352
+ - **Signal vs authority:** pure forwarding — the shell holds NO authority the modules below don't.
353
+ - **Multi-machine:** machine-local BY DESIGN — a process hog + its ps table + owned pids are
354
+ physical to ONE host's process table; the kill is a `process.kill` on THIS machine. `physical-
355
+ credential-locality`-class locality (a host's live process table is hardware/OS-bound). Posture
356
+ per §7 in the spec. No cross-machine state.
357
+ - **Rollback:** delete the file; the AgentServer construction (next slice) is what wires it in.
358
+ - **Tests:** 5 — buildProcTree, watch-only ride-through + delivery-every-tick + on-dry-run,
359
+ armed→SIGKILL + on-confirmed, cross-tick deferral→SIGKILL-at-cap, sampler-dead→on-stale.
360
+
361
+ ### Slice 16 — the N-window sustained-CPU confirmation (§1 anti-spike)
362
+ Files: `src/monitoring/ExternalHogSustained.ts` (`advanceSustained`/`isSustained`/`candidateSignature`),
363
+ `src/monitoring/ExternalHogScanTick.ts` (orchestrator amendment: ScanState.sustained,
364
+ ScanOpts.sustainedSampleCount, advance-after-candidacy, AND into sustainedHighCpu),
365
+ `src/monitoring/ExternalHogSentinel.ts` (state init gains sustained),
366
+ `tests/unit/external-hog-sustained.test.ts` (9), scan-tick +1 anti-spike test, sentinel opts.
367
+ - **What it is + why:** the spec §1 requires sustainedHighCpu to be an N-window confirmation
368
+ (`sustainedSampleCount:3`) — a kill must NOT fire on a single-window CPU spike (a compile, a GC
369
+ pause). The sampler only produces SINGLE-window candidates; this tracker holds the per-signature
370
+ CONSECUTIVE-window streak and the orchestrator sets sustainedHighCpu authoritatively =
371
+ (fact-builder single-window read) AND (streak ≥ N). A one-window spike (streak < N) is forced to
372
+ sustainedHighCpu:false → the floor's HARD VETO downgrades it to alert — never a kill.
373
+ - **Where it lives (architecture):** the streak is stage-2 decision state that ONLY the
374
+ orchestrator can coordinate — it alone has both the full tick candidate set (to advance streaks)
375
+ AND the per-candidate fact call (to apply the result). So it is threaded through ScanState, not
376
+ hidden in the adapter. Advanced ONCE per tick right after candidacy.
377
+ - **Safe direction everywhere:** absence resets the streak (strict consecutive — a one-window dip
378
+ from ps quantization re-accumulates rather than shortening the path to a kill); a failed/empty
379
+ parse resets EVERY streak (fail toward not-sustained); a bad N (≤0/non-finite) → isSustained
380
+ false (a misconfigured N can never let a spike qualify). Bounded: the next streak map is rebuilt
381
+ ONLY from this tick's candidates (≤ live candidate count).
382
+ - **Over-block:** a genuine hog that dips below threshold for one noisy window is delayed N more
383
+ windows (≈90s at defaults) — an acceptable, deliberately-conservative delay, never a missed kill
384
+ (it re-qualifies). **Under-block:** none — the gate only ever ADDS a precondition to a kill.
385
+ - **Signal vs authority:** pure predicate feeding the floor's veto; holds no authority itself.
386
+ - **Multi-machine:** machine-local BY DESIGN (a host's process CPU history is physical to one
387
+ machine; `physical-credential-locality`-class). No cross-machine state.
388
+ - **Rollback:** revert the amendment (sustainedHighCpu falls back to the single-window read) +
389
+ delete the tracker. **Tests:** 9 tracker + 1 orchestrator anti-spike (single-window+N=2 → veto,
390
+ no kill); all 8 scan-tick + 6 sentinel stay green (N=1 preserves single-window behavior there).
391
+
392
+ ### Slice 17 — the armed-marker persistence store (the live-kill authorization file)
393
+ Files: `src/monitoring/ExternalHogArmStore.ts` (`loadArmState`/`armStore`/`disarmStore`),
394
+ `tests/unit/external-hog-arm-store.test.ts` (9 tests).
395
+ - **What it is:** the durable 0600 file behind the reviewed marker VALIDATORS — the thing the PIN
396
+ arm route writes, the disarm route bumps, and the poll loop reads. Upholds the two load-bearing
397
+ properties by how it mutates epochs: ARM raises armEpoch strictly above both prior armEpoch AND
398
+ lastDisarmEpoch (a fresh arm always wins); DISARM raises lastDisarmEpoch ≥ current armEpoch (the
399
+ marker becomes invalid — a disarm can NEVER be silently un-done; returning to live-kill always
400
+ needs a fresh PIN arm).
401
+ - **Fail-closed reads:** missing / unparseable / wrong-shape / non-string-hash / corrupt-epoch →
402
+ `{marker:null, lastDisarmEpoch:0}` (marker null → not armed regardless of any epoch). No corrupt
403
+ shape yields a marker `canKillLive` accepts. `coerceMarker` rebuilds an allowlisted object +
404
+ strictly string-checks every snapshot hash (no prototype-pollution).
405
+ - **Durable atomic writes:** tmp → fsync → rename (0600). The fsync closes the Phase-5 residual —
406
+ a power loss right after a DISARM must not revert to armed (the one safety-critical direction).
407
+ - **Over/under-block:** none — it never decides a kill; it only persists the authorization the PIN
408
+ route grants. **Signal vs authority:** the PIN route is the authority; this is its durable record.
409
+ - **Multi-machine:** machine-local BY DESIGN — the arm is a physical, per-machine operator consent
410
+ to kill on THIS host's process table (`physical-credential-locality`-class); it MUST NOT
411
+ replicate (a PIN arm on one machine can never authorize a kill on another). No cross-machine state.
412
+ - **Rollback:** delete the file + module; with no marker the feature is watch-only regardless.
413
+ - **Tests:** 9 — fresh/corrupt/wrong-shape fail-closed, arm authorizes exactly the consented class,
414
+ new/broadened class not armed, 0600 mode, disarm invalidates + monotonic + idempotent, and
415
+ reboot-boots-unarmed (live config alone never re-arms).
416
+
417
+ ### Slice 18 — the deterministic fact + identity builder (stage-2 derivation)
418
+ Files: `src/monitoring/ExternalHogFactBuilder.ts` (`buildFacts`/`buildIdentity`/`deriveOwnerAppRunning`/
419
+ `parseParentPid`/`lstartToEpochMs`), `src/monitoring/ExternalHogFloor.ts` (DOC-ONLY: fixed an
420
+ INVERTED comment on `ownerAppRunning`), `tests/unit/external-hog-fact-builder.test.ts` (18 tests).
421
+ - **What it is:** the PURE stage-2 derivation that turns a candidate's proc row + FULL argv (fetched
422
+ off-loop by the adapter) into the `ExternalHogFacts` the floor evaluates and the identity
423
+ (classId/commandHash/ledgerKey) the ledger + funnel key on. No process spawn, no clock — the
424
+ impure edges (ps -o args=, launchctl, geteuid) are passed IN, so every derivation is testable.
425
+ - **The load-bearing derivation — `ownerAppRunning` polarity (§ round-6/round-8):** `true` = the
426
+ specific `--parentPid` owner is alive OR cannot be established → floor VETO; `false` = that
427
+ parent is dead (start-time-verified) → kill-eligible. Round-8 reused-pid rule: parent-pid absent
428
+ → dead; parent-pid present but its lstart is LATER than the child's → pid reused, real parent
429
+ dead; parent older-or-equal (or start-times un-orderable) → assume live → veto. EVERY
430
+ un-establishable branch fails toward `true` (veto). Validated by running the built facts through
431
+ the REAL floor (a genuine orphaned exthost PERMITS; root/launchctl/non-sustained VETO).
432
+ - **The lstart ordering parse:** the proc-table parser keeps `startTime` an OPAQUE equality token
433
+ by design; this slice adds a CONFINED, fail-safe `lstartToEpochMs` used ONLY for the one
434
+ reused-pid ordering comparison (parse ambiguity → null → conservative veto). Does not change the
435
+ parser's opaque-token contract.
436
+ - **Floor doc-bug fix (doc-only, no logic change):** the `ownerAppRunning` field comment in
437
+ ExternalHogFloor.ts read "parent is dead → true", the INVERSE of the floor's own step-5 logic
438
+ (`ownerAppRunning` true → veto). Corrected to match the authoritative logic + the spec.
439
+ - **Identity:** `buildIdentity` is allowlist-gated (outside the code-defined allowlist → null → not
440
+ kill-eligible, surfaced not killed); the command-hash STRIPS the volatile `--parentPid` so the
441
+ P19 breaker counts respawns of the SAME command (proven: hash stable across a changed parentPid).
442
+ - **Over/under-block:** over-vetoes on uncertainty (the safe direction); the only under-block risk
443
+ (a reused-then-immediately-hog parent) is the documented identity limit, fail-safe. **Signal vs
444
+ authority:** pure derivation feeding the floor; holds no authority. **Multi-machine:**
445
+ machine-local BY DESIGN (a process tree + argv are physical to one host; `physical-credential-
446
+ locality`). **Rollback:** delete the module (the real adapter's factsFor would inline it).
447
+ - **Tests:** 18 — parentPid parse, lstart ordering + fail-safe null, all 5 ownerAppRunning
448
+ branches, floor-validated permit/veto for orphan/root/launchctl/non-sustained, allowlist-gated +
449
+ stable-hash identity.
450
+
451
+ ### Slice 19 — the classifier prompt builder (injection-hardening boundary)
452
+ Files: `src/monitoring/ExternalHogClassifierPrompt.ts` (`buildClassifierPrompt`),
453
+ `tests/unit/external-hog-classifier-prompt.test.ts` (6 tests).
454
+ - **What it is:** the PURE composer of the kill/leave/alert prompt fed to the zombie-classify
455
+ model. Carries the envelope-wrapped DERIVED facts (matched class, ownerAppRunning,
456
+ sustainedHighCpu, launchctl-label, same-uid boolean) + the attacker-controllable name/argv
457
+ wrapped as explicit untrusted data, and demands a strict `{"action":"kill|leave|alert"}` verdict.
458
+ - **Two spec §5 security properties enforced here:** (1) the raw (pid, start-time, command-hash)
459
+ IDENTITY TUPLE is NEVER in the prompt (round-8 — denies an injection payload a concrete target
460
+ to name); (2) the name/argv are wrapped in a `<untrusted-process-data>` envelope with a
461
+ treat-as-data instruction, and the embedded values are delimiter-stripped (a process forging the
462
+ close-tag can't break out) + length-clamped (no unbounded prompt growth). Unit-asserted.
463
+ - **Signal vs authority — why no second-pass:** the prompt holds NO kill authority. The model's
464
+ verdict is SUBTRACTIVE (it can only SPARE an in-envelope process; the floor's two-key AND means
465
+ a kill still needs `floor.permitted`), so a prompt bug (or even a fully-successful injection) can
466
+ only cause a false-LEAVE / false-ALERT or a kill WITHIN the allowlist envelope the attacker
467
+ crafted — NEVER a wrong-kill outside the floor. This is effectiveness, not kill-safety; the
468
+ security properties are unit-asserted. (Consistent with the coalescer slice — not-kill-logic.)
469
+ - **Over/under-block:** a poor prompt lowers effectiveness (more false-leaves), never safety.
470
+ **Multi-machine:** pure string builder, no state. **Rollback:** delete; the adapter would inline
471
+ a prompt. **Tests:** 6 — derived facts present, strict verdict demanded, identity-tuple excluded,
472
+ untrusted envelope + forge-resistance + length clamp.
473
+
474
+ ### Slice 20 — the real-I/O adapter factory (the glue that makes it run)
475
+ Files: `src/monitoring/ExternalHogRealAdapters.ts` (`createExternalHogAdapters` + pure lsof/launchctl
476
+ parsers), `src/monitoring/ExternalHogKillFunnel.ts` + `ExternalHogScanTick.ts` + `ExternalHogSentinel.ts`
477
+ (async-ified factsFor / reReadFacts / hasOpenWritableWorkspaceFile), `src/monitoring/ExternalHogFloor.ts`
478
+ (additive `classRuleSources` export), `tests/unit/external-hog-real-adapters.test.ts` (13 tests).
479
+ - **What it is:** the impure edge binding the reviewed pure modules to the real OS. Every raw
480
+ side-effect (spawn ps/launchctl/lsof off-loop, process.kill, the model call, raiseAttention,
481
+ clock, arm-file read) is a single INJECTED primitive → the wiring is unit-testable with fakes and
482
+ NO real process is spawned/signalled in a test. Holds NO kill decision — it READS the OS to
483
+ produce facts and EXECUTES the signals the reviewed funnel decides on.
484
+ - **Async-ification (kill-path files):** factsFor (orchestrator) + reReadFacts/hasOpenWritableWorkspaceFile
485
+ (funnel) genuinely need async I/O (a live ps+argv / lsof read). Widened to accept a Promise +
486
+ `await` — NO decision change; every existing sync test fake still works via await; all 25
487
+ prior funnel/scan-tick/sentinel tests stay green.
488
+ - **The §4.5 kill-time CPU RE-CONFIRM (a gap I caught + fixed mid-build):** reReadFacts must not
489
+ assume the candidate is still a hog. Added a `cpuCoresOver(pid, windowMs)` micro-probe primitive;
490
+ reReadFacts sets sustainedHighCpu from a FRESH sample — a below-threshold OR null reading →
491
+ sustainedHighCpu:false → the floor re-check ABORTS the kill (the "went idle since classify" guard).
492
+ Tested (still-pinning → true; idle → false; null → false).
493
+ - **classRuleSources (floor, additive):** the SINGLE source of truth for a class's content-hash,
494
+ so the arm route's snapshot and the funnel's `currentClassContentHash` always agree (tested).
495
+ - **Fail-safe directions:** ps fail → empty table (→ on-stale); argv null → skip candidate; lsof
496
+ error → DEFER (bounded); launchctl error → empty set (floor-bounded, feature keeps working);
497
+ cpu null → not-sustained (abort); attention delivery best-effort (never throws into a tick).
498
+ - **Signal vs authority:** pure forwarding — no authority beyond the modules it wires.
499
+ **Multi-machine:** machine-local BY DESIGN (spawns/signals THIS host's processes). **Rollback:**
500
+ delete the factory; the server construction (next slice) is what instantiates it.
501
+ - **Tests:** 13 — pure lsof/launchctl parsers, readProcTable/ownedRefs/factsFor/identityFor/classify
502
+ wiring, armStatus composition, deliverNotices, and the §4.5 CPU re-confirm + arm-scope hash agreement.
503
+
504
+ ### Slice 21 — migration parity (existing agents get the dev-gated config on update)
505
+ Files: `src/core/PostUpdateMigrator.ts` (`migrateConfigExternalHogSentinelDevGate` + its registration
506
+ in `migrateConfig`), `tests/unit/PostUpdateMigrator-externalHogSentinel.test.ts` (7 tests).
507
+ - **What it is:** the Migration Parity Standard for the config. On update, an EXISTING agent gets the
508
+ `monitoring.externalHogSentinel` DARK defaults block (dryRun:true + kill-gate knobs, `enabled`
509
+ OMITTED) via ConfigDefaults + applyDefaults add-missing — no add-migration needed. The migrator
510
+ handles the ONE case applyDefaults can't: it STRIPS a default-shaped `enabled:false` (the #1001
511
+ force-dark mechanism that would dark even a dev agent), so `resolveDevAgentGate` resolves it
512
+ live-on-dev / dark-fleet. Mirrors the credentialRepointing / playwrightRegistry precedents exactly.
513
+ - **Never clobbers:** an explicit `enabled:true` (an operator fleet-flip) is PRESERVED; the `dryRun`
514
+ canary is left untouched. Existence-checked + idempotent.
515
+ - **Over/under-block:** none — it only removes a default-shaped literal that would misconfigure the
516
+ gate. **Signal vs authority:** a config migration, no runtime decision. **Multi-machine:**
517
+ machine-local config edit (each machine migrates its own config). **Rollback:** the strip is
518
+ idempotent; re-adding `enabled:false` to config restores the old shape.
519
+ - **Second-pass not-required:** a config-defaults migration (not kill-logic), purely additive,
520
+ mirroring two reviewed dev-gate precedents; the pure strip predicate + the through-migrateConfig
521
+ behavior are unit-asserted. **Tests:** 7 — strip predicate (3), block-install / strip / no-clobber
522
+ / idempotent through the real migrateConfig (4).
523
+ - **CLAUDE.md agent-awareness (Agent Awareness Standard) is a SEPARATE upcoming slice** (generateClaudeMd
524
+ + migrateClaudeMd). <!-- tracked: CMT-1901 -->
525
+
526
+ ### Slice 22 — the HTTP routes (status + PIN-gated arm + disarm)
527
+ Files: `src/server/routes.ts` (RouteContext.externalHogSentinel + GET /external-hog, POST
528
+ /external-hog/arm, POST /external-hog/disarm), `tests/integration/external-hog-routes.test.ts` (6).
529
+ - **What it is:** the operator surface. GET /external-hog (Bearer, read-only) returns
530
+ sentinel.status() + the durable arm state (503 when the sentinel is dark). POST /external-hog/arm
531
+ (PIN-gated via checkMandatePin — a Bearer token cannot arm a real kill; Know Your Principal)
532
+ builds the per-class content-hash snapshot from EXTERNAL_HOG_ALLOWLIST via classRuleSources +
533
+ classContentHash and calls armStore. POST /external-hog/disarm (Bearer — the SAFE direction, no
534
+ PIN) calls disarmStore.
535
+ - **Why PIN on arm but not disarm:** arming turns ON an irreversible action (a real kill) → minimum
536
+ rung 1 (PIN). Disarming returns to watch-only (the safe direction) → Bearer is sufficient.
537
+ - **Snapshot agreement:** the route arms every allowlist class with the SAME hash the funnel's
538
+ currentClassContentHash re-checks (both derive from classRuleSources) — so a legitimately-armed
539
+ class is recognized, and a matcher change forces a re-arm. Thin wiring over the reviewed
540
+ armStore/disarmStore epoch machinery.
541
+ - **Over/under-block:** GET emits only the arm METADATA (armEpoch, armedAt, class NAMES) — never a
542
+ secret. **Signal vs authority:** the PIN + the arm-store epochs are the authority; the route is
543
+ the surface. **Multi-machine:** machine-local BY DESIGN (a PIN arm authorizes a kill on THIS
544
+ host's process table; the marker must not replicate). **Rollback:** the routes 503/no-op when the
545
+ sentinel is unwired.
546
+ - **Tests:** 6 (Tier-2 integration over the real HTTP pipeline) — 503-when-dark, status shape,
547
+ arm-403-without-PIN, arm-403-wrong-PIN, arm-200-mints-epoch-1, and the full arm→disarm→re-arm
548
+ (epoch 2) lifecycle through the routes + the durable marker file.
549
+
550
+ ### Slice 23 — the server primitives (the §4.5 CPU probe + the primitive factory)
551
+ Files: `src/monitoring/ExternalHogServerPrimitives.ts` (`makeCpuCoresOver` + `createExternalHogServerPrimitives`
552
+ + `parseTmuxPanePids`), `tests/unit/external-hog-server-primitives.test.ts` (7 tests).
553
+ - **What it is:** builds the real-OS ExternalHogPrimitives the server injects into the factory —
554
+ kept OUT of the giant server command so the one non-trivial composed primitive (the §4.5 kill-time
555
+ CPU micro-probe) is unit-testable. Thin over injected low-level deps (async exec, process.kill,
556
+ monotonic clock, sleep, intelligence.evaluate, raiseAttention, resolved config, loadArmState).
557
+ - **The §4.5 probe (`makeCpuCoresOver`, kill-GATING):** samples a pid's cumulative cputime TWICE over
558
+ windowMs (monotonic Δwall) and resolves core-equivalents via the REVIEWED computeCoreEquivalents
559
+ (whose symmetric small/large-Δwall guards — the slice-4 fix — already block ps-quantization
560
+ false-highs). Fail-safe to null (→ the reviewed reReadFacts aborts the kill) on: pid gone, ps
561
+ unreadable/unparseable, pid-REUSED mid-window (startTime changed), or an UNKNOWN delta.
562
+ - **Over-block / under-block:** the false-HIGH direction (a non-hog reading as sustained → wrong
563
+ kill) is guarded by computeCoreEquivalents + tested (idle → ~0 cores, not high); the false-low
564
+ direction just spares (safe). **Signal vs authority:** a measurement primitive — no authority.
565
+ **Multi-machine:** machine-local (samples THIS host's processes). **Rollback:** delete; the
566
+ construction slice would inline the primitives.
567
+ - **Tests:** 7 — still-pinning → ~2 cores, idle → ~0, vanished → null, pid-reused → null, exec-throw
568
+ → null, tmux-pane parse, and the factory assembly (config/loadArm/serverPid/tmux pass-through).
569
+
570
+ ### Slice 24 — server construction (make it alive) + guard registration
571
+ Files: `src/commands/server.ts` (construct primitives → adapters → sentinel; guardRegistry.register;
572
+ the dev-gate-gated interval; pass to AgentServer), `src/server/AgentServer.ts` (the externalHogSentinel
573
+ option → ctx), `src/monitoring/guardManifest.ts` (expectRuntime false→true), `src/monitoring/
574
+ ExternalHogSentinel.ts` (guardRuntimeStatus getter), `tests/e2e/external-hog-routes-alive.test.ts` (4).
575
+ - **What it is:** the production composition root. Resolves the developmentAgent gate
576
+ (resolveDevAgentGate) → live-on-dev / dark-fleet; builds the real primitives (execFileAsync
577
+ ps/launchctl/lsof w/ 15s timeout + 8MB cap, process.kill, sharedIntelligence.evaluate,
578
+ telegram.createAttentionItem, resolved config, loadArmState, the §4.5 probe); creates the adapters
579
+ + the ExternalHogSentinel; ALWAYS registers the /guards runtime getter (honest posture even when
580
+ dark); and starts the scan interval ONLY when the dev-gate resolves enabled (unref'd so it never
581
+ holds the process open). Construction is TRY-guarded → a failure disables the feature this boot,
582
+ never crashes the server.
583
+ - **The interval is dev-gate-gated:** when dark (fleet), no interval → NO tick → NO kill, ever. The
584
+ dryRun canary flows to the sentinel's config() so even a live-on-dev tick is watch-only until a
585
+ PIN arm. `guardRuntimeStatus().lastTickAt` = the sampler heartbeat, so /guards reads on-stale when
586
+ blind (never a false on-confirmed).
587
+ - **guardManifest expectRuntime false→true:** now that the getter is registered at boot, /guards
588
+ expects the runtime report (an un-registered expectRuntime would manufacture a phantom `missing`).
589
+ - **Over/under-block:** none new — the construction only WIRES reviewed pieces; the kill decision is
590
+ entirely below it. **Signal vs authority:** composition, no authority. **Multi-machine:**
591
+ machine-local BY DESIGN (each machine constructs its own sentinel over ITS process table; nothing
592
+ replicates). **Rollback:** the try-guard + the dev-gate + the omitted config `enabled` mean the
593
+ feature is dark-by-default; removing the construction block reverts cleanly.
594
+ - **Tests:** 4 (Tier-3 E2E "feature is alive" over the real AgentServer) — GET 200 with a live status
595
+ (a benign tick advanced the sampler → on-dry-run), the arm→disarm→re-arm epoch-2 lifecycle
596
+ end-to-end, dark → 503, and auth (Bearer required, arm needs the PIN not just Bearer).
597
+
598
+ ### Slice 25 — CLAUDE.md agent-awareness (Agent Awareness Standard)
599
+ Files: `src/core/PostUpdateMigrator.ts` (`EXTERNAL_HOG_CLAUDEMD_SECTION` + the migrateClaudeMd
600
+ append), `src/scaffold/templates.ts` (import + concat into generateClaudeMd), the migration test
601
+ (+3 CLAUDE.md tests).
602
+ - **What it is:** the Agent Awareness Standard — an agent that doesn't know about a capability
603
+ effectively doesn't have it. Adds a CLAUDE.md section teaching the agent the GET /external-hog
604
+ status route, the PIN-gated arm + Bearer disarm routes, the two-key `floor_pass &&
605
+ classifier==='kill'` rule, the watch-only/PIN-arm posture, and the proactive triggers ("what's
606
+ pinning my CPU?", "why did an editor helper get killed?", "why is it only watching?").
607
+ - **Migration parity (existing agents):** migrateClaudeMd appends the section content-sniffed on
608
+ 'External-Hog Zombie Auto-Kill Sentinel' (idempotent, preserves prior content). generateClaudeMd
609
+ (templates.ts) emits the SAME section so fresh installs get it too (parity between the two sources).
610
+ - **The PIN honesty rule is IN the section:** "NEVER ask the user to paste the PIN into chat — point
611
+ them at the dashboard" (Operators-act-in-taps).
612
+ - **Not runtime behavior:** documentation/awareness only. **Second-pass not-required** (no decision
613
+ logic). **Rollback:** the content-sniff guard makes re-running a no-op; removing the section
614
+ function + the two callsites reverts. **Tests:** +4 — the section is added with the key routes +
615
+ posture + the two-key rule; idempotent + content-preserving; and the generateClaudeMd template
616
+ emits it (fresh-install parity).
617
+
618
+ ### Slice 26 — CI ratchet fixes (route-prefix classification + bench-coverage decision)
619
+ Files: `src/server/CapabilityIndex.ts` (INTERNAL_PREFIXES += external-hog), `src/data/llmBenchCoverage.ts`
620
+ (ExternalHogClassifier bench decision). Two registry ratchets CI caught (the only 2 red tests among
621
+ 7448; every other test + all my local suites were green):
622
+ - **capabilities-discoverability:** the `/external-hog` route prefix was registered in routes.ts but
623
+ not classified. Added it to `INTERNAL_PREFIXES` — agent-read operational observability (status) +
624
+ a PIN-gated operator action (arm/disarm), like `/green-pr-automerge`; dev-gated dark, 503 on the
625
+ fleet, surfaced via the CLAUDE.md agent-awareness section (not a user-invokable capability).
626
+ - **llm-bench-coverage-ratchet:** `ExternalHogClassifier` (a new sentinel-category LLM component)
627
+ needed a bench-coverage decision. Added `{ task: 'zombie-classify' }` — the operator-approved
628
+ benchmark that IS this classifier's bench task (measures false-leave/false-alert EFFECTIVENESS;
629
+ kill-SAFETY is carried entirely by the deterministic floor, so it never gates a kill). The task
630
+ battery lives in the research tree; neither ratchet checks the task file on disk.
631
+ - **Over/under-block:** none — registry/ratchet entries. **Signal vs authority:** no runtime
632
+ decision. **Rollback:** remove the two entries. **Tests:** both ratchet suites green (151 tests).
633
+
634
+ ## Phase 5 — Second-pass review
635
+
636
+ ### Slice 16 Phase-5 verdict — defect found + fixed → CONCUR
637
+ An independent reviewer read all six kill-path files and traced A–E:
638
+ - **A (false-sustained):** handled — `candidateSignature` is byte-identical to the sampler's
639
+ `idKey`, the streak accrues to the right process, `startTime` defeats pid-reuse, duplicates
640
+ dedupe.
641
+ - **B (AND-composition) — REAL DEFECT FOUND + FIXED:** `rawFacts.sustainedHighCpu && sustained`
642
+ could LAUNDER a degraded truthy non-boolean (`1`) into boolean `true`, defeating the floor's
643
+ round-11 `typeof !== 'boolean' → field-unknown` veto in the kill-PERMITTING direction. FIXED to
644
+ the reviewer's exact prescription: `rawFacts.sustainedHighCpu === true ? sustained :
645
+ rawFacts.sustainedHighCpu` — the N-window gate applies ONLY to a genuine boolean `true`; every
646
+ other value (false / 1 / undefined / null) is PRESERVED verbatim so the floor still vetoes it.
647
+ Added a regression test (a `sustainedHighCpu:1` fact + full streak → NO kill, alert-only,
648
+ floor-veto-downgrade). Reviewer RE-CONFIRMED: "Category B is fully closed; the regression test
649
+ pins it. Preserving `false` is identical to forcing it — introduces nothing."
650
+ - **C/D/E:** handled — bad-N fail-closed, empty-parse reset, bounded map, correct state threading,
651
+ and the funnel's live re-read composes subtractively (no gate bypass).
652
+
653
+ Verdict: **Concur with the review** (after the category-B fix).
654
+
655
+ ### Slice 17 Phase-5 verdict — CONCUR
656
+ An independent reviewer traced all five risks against the validators and found no safety defect:
657
+ no corrupt/crafted/torn-write path yields a marker `canKillLive` accepts.
658
+ - **A disarm durability:** `lastDisarmEpoch = max(cur, markerEpoch)` reads the CURRENT marker epoch
659
+ → however high armEpoch, the marker reads invalid; a failed rename leaves the OLD state and
660
+ throws (never a silent partial disarm).
661
+ - **B fail-closed:** any corrupt/NaN/negative/missing lastDisarmEpoch collapses the read to
662
+ not-armed.
663
+ - **C monotonicity:** `max(prior, lastDisarm)+1` is strictly above both; 2^53 float precision fails
664
+ SAFE (won't arm).
665
+ - **D atomicity:** tmp+rename atomic; readers only open the main file.
666
+ - **E laundering:** coerceMarker rebuilds an allowlisted object, rejects non-string snapshot values;
667
+ `__proto__` keys can't pollute.
668
+ - **Residual (non-blocking, now CLOSED):** the reviewer noted writeState lacked fsync (a power loss
669
+ after a disarm could revert to a PREVIOUSLY-authorized arm — never new/widened). CLOSED by adding
670
+ the content fsync (tmp → fsync → rename) — strictly more durable, no behavior change.
671
+
672
+ Verdict: **Concur with the review.**
673
+
674
+ ### Slice 18 Phase-5 verdict — CONCUR
675
+ An independent reviewer read the builder + the floor + ownership + the proc-table, and found no
676
+ wrong-permit after an adversarial walk:
677
+ - **A polarity:** correct — `deriveOwnerAppRunning` returns `false` (kill-eligible) ONLY when the
678
+ parent-pid is absent from the tree OR its occupant started strictly after the child (reused pid);
679
+ every uncertain/owner-alive branch returns `true` → floor step-5 vetoes. The floor comment now
680
+ matches the logic exactly.
681
+ - **B ordering:** sound — a real parent's start is necessarily ≤ its child's, so `parentMs > childMs
682
+ → dead` is valid; both lstarts parse through the same Date.parse in the same TZ (offset cancels);
683
+ same-second ties → veto (safe); unparseable → veto.
684
+ - **Tree-completeness (where a wrong-permit could hide):** verified — buildProcTree uses the FULL
685
+ ps output with NO cpu filter and macOS ps lists all same-uid processes, so a live parent is
686
+ always present → branch-2 "absent" means genuinely dead.
687
+ - **C/D/E:** field derivations veto-leaning; `--parentPid`-strip hash collisions only shift breaker
688
+ counts (never the envelope); same-uid + allowlist + sustained + orphaned conjunction bounds an
689
+ argv-attacker to killing a zombie-shaped process they themselves crafted.
690
+
691
+ Verdict: **Concur with the review.**
692
+
693
+ ### Slice 20 Phase-5 verdict — CONCUR
694
+ An independent reviewer read the factory + funnel + orchestrator + arm-marker + floor + the two
695
+ fail-safe dependency modules, and found no safety defect yielding a wrong-KILL or a skipped abort:
696
+ - **A async:** awaiting changes no logic — both funnel re-checks survive; every awaited path can
697
+ only ABORT, never introduce a signal; the residual check-then-signal pid-reuse gap is inherent
698
+ POSIX (identical to the sync version), bounded by the startTime-verified stillAlive.
699
+ - **B §4.5 CPU re-confirm:** `stillHog = cores!==null && finite && cores>=threshold`; null/NaN/below
700
+ → false → floor `not-sustained-hog` veto. No non-hog reaches true through the glue. Strictly
701
+ veto-STRENGTHENING.
702
+ - **C launchctl-empty:** contained — killing a real launchd job ADDITIONALLY requires same-non-root-
703
+ uid + orphaned + sustained + the editor-exthost allowlist match (an unreachable conjunction for a
704
+ managed daemon; the allowlist is the true containment).
705
+ - **D:** pid-reuse → startTime mismatch → null → abort; stale lastOwned → isInstarProcess:false can't
706
+ open a kill (an instar process won't match the exthost regex — ownership isn't the kill-protection).
707
+ - **E arm-scope:** both sides derive from classRuleSources; a matcher change → new hash → re-arm
708
+ required; snapshot keyed by classId (no cross-class arming); any divergence fails toward no-kill.
709
+ - **F:** deliverNotices / classify only raise attention / call the model.
710
+
711
+ Verdict: **Concur with the review.**
712
+
713
+ ### Slice 22 Phase-5 verdict — CONCUR
714
+ An independent reviewer traced the load-bearing property (a live kill needs enabled && !dryRun &&
715
+ isMarkerValid && classIsArmed; the marker is written ONLY by armStore, whose sole caller is the
716
+ PIN-gated arm route — grep-confirmed) and found no safety defect:
717
+ - **A PIN gate:** checkMandatePin is the FIRST statement; every failure path (no dashboardPin→503,
718
+ rate-limit→429, missing/non-string/wrong pin→403 timing-safe) sends a response + returns false
719
+ BEFORE any write. Bearer-only / `{}` / PIN-in-wrong-field all yield pin===undefined → 403; even
720
+ absent body-parsing, `req.body ?? {}` fails closed to 403.
721
+ - **B snapshot:** arm's classContentHash(classRuleSources(id)) is the IDENTICAL call the funnel's
722
+ currentClassContentHash makes, same classId key — byte-identical, every allowlist class included.
723
+ - **C disarm:** no PIN (safe); lastDisarmEpoch = max(cur, markerEpoch) ≥ armEpoch invalidates; atomic
724
+ + fsync write; fs error → 500, never a 200-without-write.
725
+ - **D leak:** returns only epoch/timestamps/class-ids — no token/PIN/secret (armedBy not even surfaced).
726
+ - **E monotonicity:** max(prior, lastDisarm)+1 mints strictly-higher, finite-guarded — stale markers
727
+ can't re-authorize.
728
+
729
+ Verdict: **Concur with the review.**
730
+
731
+ ### Slice 23 Phase-5 verdict — CONCUR
732
+ An independent reviewer traced the false-high direction adversarially and found no exploitable
733
+ false-high on an idle process:
734
+ - **A (false-high/window):** `at = nowMs()` is captured BEFORE each exec, so Δwall = exec-latency +
735
+ windowMs (always ≥ the real window) → the denominator is if anything INFLATED → cores DEFLATED
736
+ (safe). prev←a, curr←b order correct. An idle process accrues ~0 cputime between reads regardless
737
+ of timing skew → cores ~0; reaching ≥0.5 cores needs ≥15 real CPU-seconds — a genuinely idle
738
+ process cannot. The small-Δwall inflation guard never trips because the real sleep dominates.
739
+ - **B (pid-reuse):** startTime mismatch → null; negative Δcpu → UNKNOWN. Residual (non-blocking):
740
+ a reused pid in the SAME 1-sec lstart that is ITSELF heavily busy could read high — but that is a
741
+ real hog on the wrong-but-busy pid, the accepted lstart-granularity limit.
742
+ - **C:** pid-gone / undefined / NaN cputime / throw all → null; no parse path yields a spurious high.
743
+ - **D:** contract order correct; a swap → negative Δwall → UNKNOWN → safe.
744
+
745
+ Verdict: **Concur with the review** — fails safe in every direction.
746
+
747
+ ### Slice 24 Phase-5 verdict — CONCUR (with one robustness nit folded)
748
+ An independent reviewer traced all five areas — the dark-on-fleet / watch-only-on-dev / PIN-to-kill
749
+ invariant holds:
750
+ - **A interval gating SAFE:** `_externalHogEnabled = resolveDevAgentGate(...) = explicitEnabled ??
751
+ !!config.developmentAgent`. Fleet + enabled-omitted → false → NO setInterval, NO tick, NO kill.
752
+ The interval runs only on developmentAgent:true or explicit enabled:true (both deliberate);
753
+ enabled:false force-darks even a dev agent. guardRegistry.register installs a LAZY getter
754
+ (reads arm/heartbeat only, never tick()).
755
+ - **B dryRun/PIN AIRTIGHT:** default dryRun omitted → true (watch-only). canKillLive needs enabled
756
+ && !dryRun && isMarkerValid && classIsArmed; construction NEVER writes an arm marker → null →
757
+ invalid. So even enabled:true+dryRun:false yields would-kill until a PIN-route epoch-fenced
758
+ marker exists. NO real signal reachable from construction alone.
759
+ - **C SAFE:** setInterval sits after successful construction inside the try; any throw → undefined →
760
+ routes 503, no timer. Timer unref'd.
761
+ - **D (robustness nit, FOLDED):** ehNum rejected NaN but passed negative/zero/huge finite — a garbage
762
+ scanIntervalMs:0 could hot-loop the DEV tick (no kill-safety impact — the gate is unaffected).
763
+ CLOSED: the interval-critical knobs are now positive-clamped (scanIntervalMs ≥ 1000, cpuCoreThreshold
764
+ ≥ 0.1, sampleWindowMs ≥ 1000, sustainedSampleCount ≥ 1).
765
+ - **E SAFE:** externalHogSentinel threads options → ctx (`?? null`); routes get null when dark, no clobber.
766
+
767
+ Verdict: **Concur with the review.**
768
+
769
+ **Slice 26 (cont.) — the full per-component ratchet set.** CI's sharded run surfaced that a new
770
+ LLM component must carry a decision in EVERY per-COMPONENT_CATEGORY registry (not just bench-coverage).
771
+ Added `ExternalHogClassifier` to all of them, each an honest classification: `LLM_UNTRUSTED_INPUT: true`
772
+ (judges the attacker-controllable name/argv), `LLM_JUDGES_CLAIMS: false` (judges a process disposition,
773
+ not a completion/health claim), `LLM_PARSER_CONTRACT: { pending: 'contract-wave-2' }` + its pinned
774
+ baseline (its output is machine-parsed into a closed kill/leave/alert verdict), `LLM_ROUTING_NATURE:
775
+ { nature: 'A', chain: 'SORT' }` (a background bounded verdict), plus the earlier bench-coverage
776
+ (`zombie-classify`) + the routing-registry row + componentCategories (sentinel) + the CapabilityIndex
777
+ prefix. All 7 ratchet suites green (198 tests). Registry decisions, not kill-logic.
778
+
779
+ **Slice 27 — rebase onto main + the four merge-point ratchet declarations.** CI's merge-commit run
780
+ (vs a main 28 commits ahead) surfaced four declaration ratchets the branch predated; all four are
781
+ registry/annotation decisions, ZERO kill-logic changes:
782
+ - **write-domain (§3.5):** `POST /external-hog/arm` + `POST /external-hog/disarm` classified
783
+ MACHINE-LOCAL in `buildWriteDomainRegistry` with a two-axis convergence story whose logical story
784
+ IS the file-level story: the arm marker (`state/external-hog-arm.json`) is per-machine PIN
785
+ consent, and cross-machine convergence would BE the vulnerability (a synced marker silently arms
786
+ a peer's sentinel the operator never consented to — the silent-re-arm class the armEpoch design
787
+ prevents). File-level arm shipped as a `FileClassifier` sync exclusion for that exact path.
788
+ - **feature-delivery-completeness:** the 'External-Hog Zombie Auto-Kill Sentinel' CLAUDE.md section
789
+ tracked in `legacyMigratorSections` — dark dev-gated monitoring guard, template+migrator parity
790
+ via the shared `EXTERNAL_HOG_CLAUDEMD_SECTION` constant, NOT framework-shadowed (same class as
791
+ 'Machine-Coherence Guard' / 'Write Admission').
792
+ - **lint-dev-agent-dark-gate golden map:** the `monitoring.externalHogSentinel` ConfigDefaults block
793
+ (28 lines, OMITS the `enabled` literal — rides resolveDevAgentGate, the #1001 anti-pattern
794
+ avoided) adds NO attributed path; every `enabled: false` line below it shifts +28. Hand-verified
795
+ per entry (path set unchanged, 25 entries).
796
+ - **no-silent-fallbacks:** the PR's 8 net-new flagged catches resolved honestly: the server-boot
797
+ construction-failure catch now REPORTS via DegradationReporter (a guard disabled for the boot is
798
+ a real degradation); the other 7 are reviewed fail-safe catches tagged `@silent-fallback-ok` with
799
+ their safety direction inline (signal-false is the primitive's contract; tick errors are logged;
800
+ verdict-parse falls to the enum shape then null=no-kill; launchctl-empty is the round-8 reviewed
801
+ floor-bounded decision; argv-null skips the candidate; CPU-probe-null refuses the reading; tmux-empty
802
+ leaves the orphan invariant blocking). Count back at main's own level (490 ≤ 491 baseline).
803
+ Also folded at the rebase merge-point: `templates.ts` import-line conflict (main's
804
+ DOORWAY_REGISTRY_CLAUDEMD_SECTION + this branch's EXTERNAL_HOG_CLAUDEMD_SECTION — both kept).
805
+ Rollback: revert the commit; every change is a declaration/annotation, no runtime behavior moves
806
+ except the added DegradationReporter report on a construction-failure path that previously only
807
+ console.error'd.
808
+
809
+ **Slice 27 (cont.) — Self-Action Convergence closure (guard).** Main's new Capacity-Safety gate
810
+ ("No Unbounded Self-Action") landed while this branch was in flight; the sentinel IS the class's
811
+ subject (a killer on a 60s loop), so it gets the real closure, not a carve-out: registered
812
+ `external-hog-kill-breaker` in `SELF_ACTION_CONTROLLERS` — and unlike the seeded entries, the
813
+ model drives the REAL pure brake (`isBreakerTripped`/`recordKill` from ExternalHogKillLedger), so
814
+ the ratchet proves the shipped code converges, not a re-model. Pinned worst case: the same
815
+ respawn-surviving signature sustained-hot on every scan forever → kills settle at K=3 within the
816
+ rolling window (2N horizon = exactly one window, settle-is-real exact), then the brake holds; the
817
+ real steady state is the rate bound ≤K/window/signature with one deduped degradation, ledger
818
+ pruned to retention. `@self-action-controller` marker added to ExternalHogSentinel.ts. Convergence
819
+ ratchet green (30 tests).
820
+
821
+ ## Class-Closure Declaration (display-only mirror)
822
+
823
+ `unbounded-self-action` → closure **guard**. The sentinel is a self-triggered kill controller (a
824
+ 60s scan loop that can signal processes), i.e. the class's exact subject. Guard: registered
825
+ `external-hog-kill-breaker` in `SELF_ACTION_CONTROLLERS` (src/testing/selfActionRegistry.ts),
826
+ covered by the convergence ratchet `tests/unit/self-action-convergence.test.ts` (enforcement:
827
+ ratchet). The model drives the REAL pure brake functions (`isBreakerTripped`/`recordKill` from
828
+ src/monitoring/ExternalHogKillLedger.ts — K=3 per rolling 1h window per signature, class-level
829
+ fallback for volatile keys) under the pinned always-respawning fixture and settles at 3 kills,
830
+ horizon-independent within the window; the real steady state is rate-bounded at ≤K/window/signature
831
+ with one deduped degradation, ledger pruned to retention. Marker `@self-action-controller:
832
+ external-hog-kill-breaker` sits in src/monitoring/ExternalHogSentinel.ts for the forcing lint.