instar 1.3.225 → 1.3.227

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 (52) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/devCiFailures.d.ts +39 -0
  4. package/dist/commands/devCiFailures.d.ts.map +1 -0
  5. package/dist/commands/devCiFailures.js +133 -0
  6. package/dist/commands/devCiFailures.js.map +1 -0
  7. package/dist/commands/server.d.ts.map +1 -1
  8. package/dist/commands/server.js +20 -1
  9. package/dist/commands/server.js.map +1 -1
  10. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  11. package/dist/config/ConfigDefaults.js +11 -0
  12. package/dist/config/ConfigDefaults.js.map +1 -1
  13. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  14. package/dist/core/PostUpdateMigrator.js +18 -0
  15. package/dist/core/PostUpdateMigrator.js.map +1 -1
  16. package/dist/core/StateManager.d.ts +22 -1
  17. package/dist/core/StateManager.d.ts.map +1 -1
  18. package/dist/core/StateManager.js +45 -6
  19. package/dist/core/StateManager.js.map +1 -1
  20. package/dist/core/types.d.ts +46 -0
  21. package/dist/core/types.d.ts.map +1 -1
  22. package/dist/core/types.js.map +1 -1
  23. package/dist/monitoring/BurnDetector.d.ts +11 -0
  24. package/dist/monitoring/BurnDetector.d.ts.map +1 -1
  25. package/dist/monitoring/BurnDetector.js +15 -3
  26. package/dist/monitoring/BurnDetector.js.map +1 -1
  27. package/dist/monitoring/SessionReaper.d.ts +74 -1
  28. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  29. package/dist/monitoring/SessionReaper.js +146 -6
  30. package/dist/monitoring/SessionReaper.js.map +1 -1
  31. package/dist/scaffold/templates.d.ts.map +1 -1
  32. package/dist/scaffold/templates.js +9 -0
  33. package/dist/scaffold/templates.js.map +1 -1
  34. package/dist/server/AgentServer.d.ts.map +1 -1
  35. package/dist/server/AgentServer.js +56 -25
  36. package/dist/server/AgentServer.js.map +1 -1
  37. package/package.json +1 -1
  38. package/scripts/generate-builtin-manifest.cjs +1 -0
  39. package/src/data/builtin-manifest.json +59 -51
  40. package/src/scaffold/templates.ts +9 -0
  41. package/upgrades/1.3.226.md +111 -0
  42. package/upgrades/1.3.227.md +109 -0
  43. package/upgrades/burn-detector-activity-gate.eli16.md +53 -0
  44. package/upgrades/dev-ci-failures-tool.eli16.md +21 -0
  45. package/upgrades/reaper-busy-orphan-detect.eli16.md +33 -0
  46. package/upgrades/reaper-cpu-aware-keep.eli16.md +30 -0
  47. package/upgrades/side-effects/burn-detector-activity-gate.md +110 -0
  48. package/upgrades/side-effects/dev-ci-failures-tool.md +54 -0
  49. package/upgrades/side-effects/reaper-busy-orphan-detect.md +85 -0
  50. package/upgrades/side-effects/reaper-cpu-aware-keep.md +90 -0
  51. package/upgrades/side-effects/statemanager-listsessions-cache.md +72 -0
  52. package/upgrades/statemanager-listsessions-cache.eli16.md +36 -0
@@ -0,0 +1,109 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The `SessionReaper` gains an **observe-only** busy-orphan detector behind a new
9
+ dark-by-default flag `busyOrphanDetection` — the inverse of #722's
10
+ `cpuAwareActiveProcessKeep`, closing the blind spot that fix can't reach.
11
+
12
+ **The gap.** #722 made the `active-process` keep require *positive CPU progress*
13
+ under load, so a wedged/**idle** child (≈0 CPU) no longer pins an idle session.
14
+ But CPU-progress is only a *proxy* for "useful" — a **useless process that
15
+ SPINS** (a hot-loop, a wedged job burning CPU) looks "active" and would keep the
16
+ session pinned. So #722 catches the idle orphan; it does not catch the *busy*
17
+ orphan.
18
+
19
+ **The detector.** Under CPU pressure, when a session is kept ONLY by an
20
+ `active-process` veto whose child is provably **burning CPU** (the descendant
21
+ CPU-seconds delta is above the idle floor) yet the session ITSELF looks fully
22
+ idle (a positive ready-prompt + a flat, non-growing transcript) across an
23
+ extended dwell (`busyOrphanConfirmTicks`, default 5 ≈ 10 min), the reaper records
24
+ a `busy-orphan-suspected` row in the decision audit (`logs/reaper-audit.jsonl`),
25
+ and a `busy-orphan-cleared` row when a confirmed suspect recovers.
26
+
27
+ **It NEVER changes the keep/kill decision.** This is pure observation — it makes
28
+ the "useless-but-busy child pins an idle session" case *measurable* so that safe
29
+ auto-reclaim can graduate later with real data (the instar observe-first → act
30
+ pattern). Zero false-kill risk by construction.
31
+
32
+ **Conservative by construction:**
33
+ - No-op at `normal` pressure, when the CPU dep is absent, on the first sighting
34
+ (no transcript-growth baseline yet), and whenever the transcript is unresolved
35
+ or growing — all resolve to "not a suspect".
36
+ - Only the `active-process` keep-reason is considered; every earlier reason
37
+ (recent-user, commitment, recovery, …) is untouched.
38
+ - Emits the suspect row exactly ONCE — the tick the dwell crosses the threshold —
39
+ not every tick after (no per-tick audit flood).
40
+
41
+ Ships dark fleet-wide; live on development agents via the `developmentAgent`
42
+ gate (same as #722), so it's dogfooded on a real loaded box.
43
+
44
+ `StateManager.listSessions()` now serves from a short-TTL in-memory cache instead
45
+ of re-reading and re-parsing every session file from disk on every call.
46
+
47
+ **The bug (a systemic CPU hot-loop).** `listSessions()` did `readdirSync` +
48
+ `readFileSync` + `JSON.parse` of **every** session JSON file on **every** call —
49
+ and it's called each tick by the SessionReaper and the sentinels via
50
+ `listRunningSessions()`. So the cost was O(sessions × pollers × tick-rate) disk
51
+ reads, which kept agent servers spinning at 50–67% CPU (a CDP profile showed ~30%
52
+ of server CPU was literally `readFileUtf8`, with `listSessions` on top). It was
53
+ systemic (every agent with sessions + active pollers) and survived restarts —
54
+ which is exactly why bouncing a hot server never helped.
55
+
56
+ **The fix.** A 1-second TTL cache on the full session list. The many sub-second
57
+ read-only callers within a tick now share one disk read; the cache is invalidated
58
+ on every session write (`saveSession` / `removeSession`), so a freshly spawned or
59
+ terminated session is visible on the very next call — no staleness for the cases
60
+ that matter. `listSessions` returns shallow copies, so a caller can't corrupt the
61
+ shared cache. StateManager is the sole in-process writer of session files, so the
62
+ invalidation is complete (cross-machine writes carry at most the 1s TTL of
63
+ staleness, which the reaper/scheduler already tolerate).
64
+
65
+ Net effect: the per-tick session-directory churn collapses to near-zero, fixing
66
+ the agent-server CPU hot-loop fleet-wide with no behavior change.
67
+
68
+ ## What to Tell Your User
69
+
70
+ Nothing — it is dark by default and observe-only (it never changes behavior, just
71
+ writes audit rows). If surfaced at all, surface it as Experimental observability
72
+ that makes the busy-orphan case visible.
73
+
74
+ Nothing required — it's an internal performance fix. If your agent's server was
75
+ running hot / the machine load was high with no obvious cause, this is a likely
76
+ culprit and it's now fixed; servers idle much cooler.
77
+
78
+ ## Summary of New Capabilities
79
+
80
+ - `monitoring.sessionReaper.busyOrphanDetection` (boolean, default false;
81
+ `developmentAgent` agents default true) — observe-only flagging of idle
82
+ sessions pinned by a CPU-burning child.
83
+ - `monitoring.sessionReaper.busyOrphanConfirmTicks` (number, default 5) — the
84
+ consecutive-suspect-tick dwell before a row is emitted.
85
+ - New audit events: `busy-orphan-suspected`, `busy-orphan-cleared`.
86
+
87
+ - None (no new API, route, or config). `StateManager.listSessions()` is now cached
88
+ (1s TTL, write-invalidated); behavior is unchanged, cost drops from O(files) per
89
+ call to one read per second of read-only traffic.
90
+
91
+ ## Evidence
92
+
93
+ - Motivated by a real review catch (the gap a busy/hot orphan defeats the
94
+ CPU-progress proxy) and a live instance (agent servers hot-looping at ~50% CPU
95
+ while otherwise idle).
96
+ - Tests: `tests/unit/session-reaper-busy-orphan.test.ts` — 9 tests, both sides of
97
+ every boundary (flags only on busy-child + idle-session + dwell; never on
98
+ CPU-flat / working-frame / flag-off / off-pressure / no-dep / pre-dwell; emits
99
+ once; clears on recovery; verdict always unchanged) + a wiring-integrity
100
+ assertion. The 56 existing reaper + #722 tests stay green.
101
+
102
+ - Pinned with node's inspector: SIGUSR1 → CDP CPU profile of a hot agent server
103
+ showed 30.8% `readFileUtf8` + 7% `listSessions` (StateManager.js:151), called via
104
+ the reaper/sentinel ticks — the symbolicated JS frame macOS `sample` couldn't show.
105
+ - Tests: `tests/unit/state-manager-listsessions-cache.test.ts` — 6 tests (cache HIT
106
+ within TTL; MISS after TTL; immediate invalidation on saveSession + removeSession;
107
+ status-filter on cached data; copy-safety; corrupt-file skip). The 71 existing
108
+ StateManager tests + 255 consumer tests (reaper, scheduler, restart-all, recovery)
109
+ stay green.
@@ -0,0 +1,53 @@
1
+ # ELI16: Burn-detector activity gate (stop the repeating token-burn noise)
2
+
3
+ Instar watches how many tokens each part of an agent is using. If one part suddenly hogs more than a
4
+ quarter of a whole day's spend, it raises a heads-up: "a component is using more than a quarter of
5
+ the token budget." That's the **token-burn detector**, and it's meant to catch a runaway loop or an
6
+ unexpected cost spike early.
7
+
8
+ ## The bug
9
+
10
+ The detector only asked one question: "Over the last 24 hours, did one part use more than 25% of the
11
+ total?" It never asked the obvious follow-up: "Is that still happening right now?"
12
+
13
+ So here's what went wrong. One big working session ran for a while, used a lot of tokens, and then
14
+ finished. For the next 24 hours that finished session still counted as a big slice of the trailing-day
15
+ total — so the alarm kept going off, over and over, every time the cooldown expired. And because the
16
+ session had stopped, the alarm even said "Projected 0 tokens in next 24h" — it was literally
17
+ announcing "this thing is burning a lot" and "this thing is spending nothing right now" in the same
18
+ breath. That's the noise the user saw: a full day of contradictory, useless heads-ups for something
19
+ that was already over.
20
+
21
+ ## The fix
22
+
23
+ Add one missing check: only raise the alarm if the component is **actually spending right now** —
24
+ meaning it used tokens in the last hour. A big 24-hour share with a zero current rate is a finished
25
+ burst, not a live burn, so it stays quiet. The detector's other trigger (a sudden spike versus a
26
+ component's own 7-day normal) already had this kind of "is it active now" check; this just brings the
27
+ 25%-share trigger up to the same standard.
28
+
29
+ ## What already existed vs. what's new
30
+
31
+ - **Already there:** the burn detector, the 25% trigger, the 1-hour spend lookup, and the throttle
32
+ runbook that decides whether to just alert or actually slow a component down.
33
+ - **New:** (1) the activity gate on the 25% trigger; (2) an operator off-switch and tuning knobs under
34
+ a burn-detection config block, so anyone can silence or retune the alerts without a code change; (3)
35
+ a cleaner alert message (the old internal "Phase 3/Phase 4" wording is gone); (4) a one-paragraph
36
+ note added to every agent's instructions on update so any agent can explain the alert and offer the
37
+ off-switch.
38
+
39
+ ## Safeguards (plain terms)
40
+
41
+ - The change only ever makes the alarm **quieter** — it adds a condition that must also be true before
42
+ alerting. It can never cause a new false alarm.
43
+ - The detector is still just a *reporter*. It doesn't slow anything down or block anything by itself;
44
+ a separate piece decides whether to act. That separation is unchanged.
45
+ - All the new config is optional. If you set nothing, you get the fixed-but-sensible defaults.
46
+ - Rolling it back is a one-line revert with no data cleanup. Worst case, the old noise comes back —
47
+ there's no risk to correctness or to stored data.
48
+
49
+ ## What's NOT fixed (on purpose)
50
+
51
+ It still labels un-attributed spend as `unknown::<some-id>`. That just means "we couldn't pin this
52
+ spend to a named component" — it's not a problem by itself, and giving those a real name is a bigger,
53
+ separate piece of work.
@@ -0,0 +1,21 @@
1
+ # `instar dev:ci-failures` — ELI16
2
+
3
+ Viewable private artifact:
4
+ https://echo.dawn-tunnel.dev/view/41bc3523-a95d-4038-bd43-af8a942e0008?sig=e785278aea6a2e82502cc55fe56b98e55ab4cef543b71ddf32c953db7985c191
5
+
6
+ When a pull request's automated tests go red, you want to know *which* test broke and where.
7
+ Normally you'd open the CI logs — but in some setups the command that fetches those logs comes
8
+ back completely empty, so a red run tells you "something failed" and nothing else. I hit this
9
+ repeatedly while shipping fixes this run, and had to re-discover a workaround each time.
10
+
11
+ This turns that workaround into a permanent tool. `instar dev:ci-failures <pr>` reads a
12
+ *different* GitHub endpoint (the per-check "annotations") that still reports the exact failing
13
+ file, line number, and assertion message even when the logs are blank. It also tidies the
14
+ output: it merges the duplicate failure that the Node-20 and Node-22 test shards both report,
15
+ and it hides the generic CI-runner noise ("Process completed with exit code 1").
16
+
17
+ It's read-only and informational — it never changes anything, it just shows you the failures.
18
+ Pairs with `instar dev:preflight` as the contributor dev-loop toolkit.
19
+
20
+ _Tier-2 · branch `echo/dev-ci-failures-tool` · the first of the "turn this run's friction into
21
+ infrastructure" improvements._
@@ -0,0 +1,33 @@
1
+ # Catching the "busy but useless" process
2
+
3
+ ## The one-sentence version
4
+ Last fix (#722) taught the cleanup robot to ignore a *frozen* leftover program. But a leftover program can also be *spinning uselessly* — burning the engine while going nowhere — and that one still looks "busy." This change makes the robot **notice and write down** those cases (without touching anything yet), so we can see how often it happens before we ever let it act.
5
+
6
+ ## Back to the garage
7
+ Earlier rule: "don't tow a car just because it has an engine — only if the engine is also cold (off)." Good for the junked car with a dead battery.
8
+
9
+ But what about a car parked in its spot with **the engine revving, nobody inside, going nowhere** — a stuck throttle? Its engine is hot, so the "is the engine cold?" test says *keep it*. Yet it's just as useless as the dead one, and it's burning fuel (CPU) and making noise (load) the whole time.
10
+
11
+ This change doesn't tow that car. It puts a **note on the windshield**: "spot occupied by a revving-but-empty car for 10+ minutes." Collect enough notes and we'll know whether stuck-throttle cars are a real problem worth a tow rule — backed by data, not a guess.
12
+
13
+ ## What it actually does
14
+ - Watches for a session that is **sitting idle** (at its ready prompt, transcript not growing) while a child program **keeps burning CPU**.
15
+ - After that's been true for a while (a dwell, ~10 minutes), it writes one line: `busy-orphan-suspected`.
16
+ - When the situation clears, it writes `busy-orphan-cleared`.
17
+ - That's the whole feature. **It never reaps, kills, nudges, or notifies.** Pure note-taking.
18
+
19
+ ## Why note-taking first (instead of just towing)
20
+ The one honest reason *not* to tow a revving car: maybe the driver started it on purpose to warm it up and stepped away — i.e. an agent legitimately kicked off a real background job and went back to its prompt. Telling those two apart reliably is hard. So instead of risking a wrong tow, we **measure first**: gather real examples, learn the signatures, and only then design a safe auto-action. That's the instar way — observe, then act.
21
+
22
+ ## The safety rails
23
+ 1. **Changes no decisions.** The keep/kill logic is byte-for-byte the same; this only adds an audit line.
24
+ 2. **Only under load**, only when CPU is actually measurable, only after a real dwell — never on a hair trigger.
25
+ 3. **One note per episode**, not per tick — no log flood.
26
+ 4. **Dark everywhere except me** (the development agent), so it proves itself on a real loaded machine first.
27
+
28
+ ## Why it matters
29
+ Together with #722 this closes both halves of "a process pins an idle session": the *idle* leftover (#722 can now reclaim it) and the *busy* leftover (this makes it visible). You can't responsibly manage machine load you can't see — this is the seeing.
30
+
31
+ ---
32
+
33
+ **Rendered (verified HTTP 200):** https://echo.dawn-tunnel.dev/view/a6ad4a97-c59b-4a61-81c8-4d2aa8d3d7b4?sig=333db8be8c2b9624668753699e48aaa03a5b208ae963c841293ae4ba4ec0c5f7
@@ -0,0 +1,30 @@
1
+ # Why the reaper got smarter about "busy" sessions
2
+
3
+ ## The one-sentence version
4
+ Before, the cleanup robot thought a session was *busy* if it had **any** helper program still attached — even a frozen, do-nothing one. Now, when the machine is overloaded, it also checks whether that helper is *actually using the CPU*. A frozen helper no longer keeps a dead-idle session alive and hogging the machine.
5
+
6
+ ## Picture it
7
+ Think of a parking garage that only tows cars when it's full. The old rule: "never tow a car if its engine is installed." Trouble is, a junked car with a cold, dead engine *still has an engine installed* — so it sat there forever, taking a spot while the garage overflowed.
8
+
9
+ The new rule (only when the garage is full): "don't tow a car just because it has an engine — tow it only if the engine is also **stone cold** (not running) AND the driver's seat is empty AND nothing's happening inside." A car that's actually running, or has someone in it, is never touched.
10
+
11
+ ## What changed, precisely
12
+ - The reaper used to keep a session whenever a child process **existed**.
13
+ - Now, **under CPU load only**, it also asks: did that child burn any CPU recently?
14
+ - If the child is **flat (≈0 CPU)** *and* the session is sitting idle at a ready prompt *and* its transcript isn't growing → it becomes reclaimable.
15
+ - If the child is doing real work (CPU rising), or the session might be mid-task → it's kept, exactly like before.
16
+
17
+ ## The safety rails (why this won't kill your work)
18
+ 1. **Off when the machine is calm.** Zero change unless CPU pressure is real.
19
+ 2. **Never kills on the CPU check alone** — it still has to pass the "is it truly idle?" proof and the slow, repeated-confirmation + budget gates.
20
+ 3. **Can't measure? Keep it.** First look, missing data, weird readings → always keep.
21
+ 4. **Only the cleanup robot changed.** The guard that stops *other* things from killing your sessions is untouched.
22
+ 5. **Dark by default.** Off across the fleet; on only for the development agent (me) so I can prove it on a real loaded machine first.
23
+ 6. **Leaves a paper trail.** Every time it relaxes the rule it writes a line you can read in `logs/reaper-audit.jsonl`.
24
+
25
+ ## Why it matters
26
+ Idle sessions with a stuck helper (a hung MCP server, a wedged `codex` job) were quietly piling up and starving the machine — which is what makes new sessions fail to spawn and messaging go silent. This lets the reaper finally reclaim exactly those, without ever risking a session that's actually working.
27
+
28
+ ---
29
+
30
+ **Rendered (verified) view:** _set below after creating the tunnel view._
@@ -0,0 +1,110 @@
1
+ # Side-Effects Review — Burn-detector absolute-share activity gate
2
+
3
+ **Version / slug:** `burn-detector-activity-gate`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ The `BurnDetector` absolute-share trigger fired whenever one attribution key crossed 25% of the
11
+ trailing-24h token spend, with no check that the key was still spending. A heavy session that
12
+ finished hours ago kept re-tripping the alarm every cooldown for a full 24h with self-contradictory
13
+ "consumed 67% of 24h spend … Projected 0 tokens" messages. This change adds an activity gate
14
+ (`tokens1h > absoluteShareActivityFloorTokens`, default 0) so the absolute-share trigger only fires
15
+ for a key actively spending in the last hour. It also exposes the previously-hardcoded
16
+ burn-detection config as `monitoring.burnDetection.*` (master off-switch + tuning knobs), cleans the
17
+ alert text, and adds a content-sniffed `PostUpdateMigrator` awareness section. Files touched:
18
+ `src/monitoring/BurnDetector.ts`, `src/server/AgentServer.ts`, `src/core/types.ts`,
19
+ `src/scaffold/templates.ts`, `src/core/PostUpdateMigrator.ts`, plus
20
+ `tests/unit/burn-detection-phase-3.test.ts` and `tests/unit/PostUpdateMigrator-tokenBurnAlerts.test.ts`.
21
+
22
+ ## Decision-point inventory
23
+
24
+ - `BurnDetector.tick` absolute-share trigger — **modify** — adds an AND-condition (`isActivelySpending`)
25
+ before the existing `share > threshold` test. Strictly narrows when the trigger fires; never widens.
26
+ - `AgentServer` burn-detection startup — **modify** — reads `monitoring.burnDetection`; `enabled:false`
27
+ skips the whole subsystem; other knobs are passed through to `BurnDetector` / `BurnThrottleRunbook`
28
+ only when explicitly set (partial configs built without `undefined` keys so a default is never clobbered).
29
+ - `PostUpdateMigrator.migrateClaudeMd` — **add** — one idempotent, content-sniffed section append.
30
+
31
+ ## 1. Over-block
32
+
33
+ **What legitimate inputs does this change reject that it shouldn't?**
34
+
35
+ A genuine live burn that happens to log zero tokens in the most recent hour would be gated out. In
36
+ practice this can't be a sustained burn — `tokens1h` is computed from a real 1h ledger query, so a
37
+ key actively spending necessarily has positive last-hour tokens. The only "rejected" case is a key
38
+ whose 24h share is high but whose current rate is zero, which is by definition not a live burn. The
39
+ default floor of 0 is the most conservative choice: it suppresses only the provably-finished case.
40
+
41
+ ## 2. Under-block
42
+
43
+ **What failure modes does this still miss?**
44
+
45
+ It does not improve attribution — spend still labelled `unknown::<sessionId>` (resolver ran, found no
46
+ named component) is a coverage gap, not a burn, and is out of scope here. A burst that ends but
47
+ leaves a small warm-session cache trickle in the last hour (tokens1h slightly > 0) would still fire
48
+ if its 24h share exceeds the threshold; operators who see that can raise
49
+ `absoluteShareActivityFloorTokens`. The baseline-divergence trigger is unchanged.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ **Is this at the right layer?** Yes. The gate lives inside `BurnDetector.tick` beside the existing
54
+ `rollingBaselineFloor` activity check on the sibling trigger — same layer, same shape. The config
55
+ read lives in `AgentServer` where every other monitoring subsystem is wired. The migration lives
56
+ beside the other `migrateClaudeMd` sections. No re-implementation of an existing primitive.
57
+
58
+ ## 4. Signal vs authority compliance
59
+
60
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
61
+
62
+ - [x] No — this change produces a signal consumed by an existing smart gate.
63
+
64
+ `BurnDetector` is a signal producer: it emits to `DegradationReporter`; the `BurnThrottleRunbook` is
65
+ the authority that decides alert-vs-throttle. This change makes the signal *more* conservative (fewer
66
+ false positives) — it adds no blocking authority and removes none. The off-switch is operator config,
67
+ not runtime decision logic.
68
+
69
+ ## 5. Interactions
70
+
71
+ - **Shadowing:** The activity gate runs before the baseline-divergence trigger in the same key loop.
72
+ When absolute-share is gated out, baseline-divergence is still evaluated normally (and has its own
73
+ `rollingBaselineFloor` gate), so a finished burst produces zero signals from both — confirmed by the
74
+ new "finished burst" unit test.
75
+ - **Double-fire:** None. One signal per key per cooldown, unchanged.
76
+ - **Races:** None. `BurnDetector` reads the ledger; it shares no mutable state with concurrent code.
77
+ - **Feedback loops:** The runbook self-attribution prefix (`burn-throttle-runbook::`) remains exempt,
78
+ so throttle activity can't feed back into the detector.
79
+
80
+ ## 6. External surfaces
81
+
82
+ The user-visible Telegram alert text changes (drops the "Phase 3/Phase 4" jargon; "at current rate"
83
+ → "at the current rate"). The `BurnVerifier.extractTokensLast1h` regex that parses the projected
84
+ number back out of the alert still matches the new wording (the captured group precedes the changed
85
+ suffix) — verified against existing fixtures. Volume of alerts drops sharply (finished bursts no
86
+ longer re-alarm). New optional `monitoring.burnDetection` config keys; absence preserves defaults.
87
+ Existing agents get the CLAUDE.md awareness section on next update via the idempotent migration. No
88
+ database/ledger schema change.
89
+
90
+ ## 7. Rollback cost
91
+
92
+ Pure code + docs change. Revert and ship as the next patch. No persistent state, no data migration,
93
+ no agent-state repair. During the rollback window the only regression is the return of the original
94
+ noise — there is no correctness or data risk. An operator who needs immediate relief without a
95
+ release can set `monitoring.burnDetection.enabled: false` and restart sessions.
96
+
97
+ ## Conclusion
98
+
99
+ The review surfaced no blocking concerns. The change narrows an over-firing signal-producer with a
100
+ gate that mirrors an existing sibling check, adds an operator off-switch, and is fully reversible by
101
+ revert or config. The only residual is the unchanged attribution-coverage gap (`unknown::<id>`),
102
+ explicitly out of scope and tracked as a separate, larger effort. Clear to ship.
103
+
104
+ ## Evidence pointers
105
+
106
+ - `tests/unit/burn-detection-phase-3.test.ts` — 3 new activity-gate tests (both sides of the boundary
107
+ + the configurable-floor path); 65 burn-detection tests green across phases 3–6.
108
+ - `tests/unit/PostUpdateMigrator-tokenBurnAlerts.test.ts` — 5 new migration tests (add / idempotent /
109
+ preserve / skip-missing / template-emits).
110
+ - `tsc --noEmit` clean.
@@ -0,0 +1,54 @@
1
+ # Side-Effects Review — `instar dev:ci-failures`
2
+
3
+ **Version / slug:** `dev-ci-failures-tool`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (Tier-2 — new read-only dev CLI command; no runtime/server surface, no decision boundary)`
7
+
8
+ ## Summary of the change
9
+
10
+ New `instar dev:ci-failures <pr>` CLI command (`src/commands/devCiFailures.ts`, wired in
11
+ `src/cli.ts`) that prints a PR's failing tests via the GitHub check-run annotations API.
12
+ Read-only: it shells out to `gh` GET endpoints only. Adds a manifest catalog entry
13
+ (`cli:dev-ci-failures`, colon-free id per Codey's #716 convention fix) + awareness docs
14
+ (CLAUDE.md, templates.ts, site cli reference). No server route, no config, no state.
15
+
16
+ ## Decision-point inventory
17
+
18
+ 1. **Annotations API vs log parsing.** Chose the check-run *annotations* endpoint because
19
+ `gh run view --log[-failed]` returns 0 bytes in some environments (the friction that
20
+ motivated this). Annotations carry `path:line` + the assertion even when logs are empty.
21
+ 2. **Diagnostic, not a gate (exit 0 even with failures).** It informs; it does not fail the
22
+ shell on a red PR. Exit 1 is reserved for operational errors (PR unresolvable / API
23
+ error) so a script can tell "couldn't run" from "ran, here are the failures."
24
+ 3. **Shard-pair dedup.** The node-20 / node-22 shard checks report the identical failure;
25
+ the command de-dupes by the formatted line so each distinct failure prints once.
26
+ 4. **Noise filter.** Drops `.github/...` annotations and `Process completed with exit code N`
27
+ (CI-runner artifacts, not test failures). When a failed check has no test-level
28
+ annotations (a build/lint/type step), it prints a note rather than silently showing nothing.
29
+
30
+ ## 1. Blast radius / reversibility
31
+
32
+ Additive: one new command file + one cli.ts command block + a manifest entry + doc lines.
33
+ Fully reversible. No existing behavior touched. Read-only external calls (gh GET).
34
+
35
+ ## 2. Failure modes
36
+
37
+ - `gh` not installed / not authenticated → the command surfaces the gh error and exits 1.
38
+ - An annotations-endpoint hiccup for one check → that check is skipped, others still print.
39
+ - A PR with no failed checks → prints "No failed checks", exit 0.
40
+ All handled; none crash.
41
+
42
+ ## 3. Manifest / docs
43
+
44
+ `cli:dev-ci-failures` added to the generator's command list → colon-free manifest id
45
+ (passes the type:name convention). CLAUDE.md and `templates.ts`'s `generateClaudeMd` were
46
+ edited in sync so `verify-deployed-templates` stays green (locally confirmed). Manifest
47
+ `generatedAt` timestamp aside, the generator output is deterministic.
48
+
49
+ ## 4. Tests
50
+
51
+ `tests/unit/devCiFailures.test.ts` — pure `extractFailureLines` (both sides: keeps real
52
+ failures, drops warnings/`.github`/`Process completed`, truncates long messages, handles
53
+ missing path) + `runDevCiFailures` with injected `gh` deps (red PR prints the test; shard
54
+ dedup; green PR; unresolvable PR → exit 1; build-step-with-no-test-annotations note). 10 tests.
@@ -0,0 +1,85 @@
1
+ # Side-Effects Review — SessionReaper observe-only busy-orphan detection
2
+
3
+ **Version / slug:** `reaper-busy-orphan-detect`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (observe-only — never changes a keep/kill verdict; reaper-only; dark by default)`
7
+
8
+ ## Summary of the change
9
+
10
+ Under CPU pressure, when a session is kept by an `active-process` veto whose
11
+ child is provably burning CPU (cpuFlat===false) yet the session itself is idle
12
+ (positive idle prompt + flat transcript) across an extended dwell, the reaper
13
+ records a `busy-orphan-suspected` audit row (and `busy-orphan-cleared` on
14
+ recovery). It is the inverse of #722's `cpuAwareActiveProcessKeep` and closes the
15
+ gap where a *busy* useless process defeats the CPU-progress proxy. Gated by
16
+ `busyOrphanDetection` (dark; dev agents on via `developmentAgent`).
17
+
18
+ ## Decision-point inventory
19
+
20
+ 1. **Suspect predicate** — flag iff ALL hold: `busyOrphanDetection` on, under CPU
21
+ pressure (tier ≠ normal), `blocked.reason === 'active-process'`,
22
+ `cpuFlat === false` (descendant CPU delta above the idle floor), transcript
23
+ delta `static` (resolved + not grown vs the prior tick), and a positive
24
+ idle-prompt frame. Any miss ⇒ not a suspect.
25
+ 2. **Dwell** — emit `busy-orphan-suspected` exactly the tick the consecutive
26
+ suspect streak crosses `busyOrphanConfirmTicks` (default 5). A brief
27
+ background job that finishes within the dwell is never flagged.
28
+ 3. **Clear** — when a confirmed suspect (streak ≥ threshold) stops being a suspect,
29
+ emit `busy-orphan-cleared`. Streaks below the threshold reset silently.
30
+
31
+ ## 1. Over-block (does it ever reap something it shouldn't?)
32
+
33
+ **No — it cannot reap anything.** This change is observe-only: the keep/kill
34
+ verdict path is untouched. Every code path through the detector returns the
35
+ SAME `keep('active-process')` it returned before; the only effect is an audit
36
+ row. The exact "idle session + legit busy background job" case that
37
+ `cpuAwareActiveProcessKeep` was deliberately conservative about is, here, merely
38
+ *logged* — never acted on. A false suspect costs one JSONL line, nothing more.
39
+
40
+ ## 2. Under-block
41
+
42
+ Nothing is loosened. No existing protection, gate, or veto is weakened — the
43
+ shared `ReapGuard`/`ReapAuthority` path and the reaper's own stateful checks are
44
+ entirely unchanged. The detector reads the same already-captured frame and
45
+ transcript (no extra tmux/fs work) and the same `cpuFlat` signal #722 already
46
+ computes.
47
+
48
+ ## 3. Blast radius / rollout
49
+
50
+ Dark by default (`busyOrphanDetection: false`). Dev agents get it via the
51
+ `developmentAgent` gate at the server wiring site (riding #722's IIFE). On the
52
+ fleet the flag is false ⇒ the detection branch is never entered and
53
+ `cpuProgressFlat` returns early exactly as before #722 ⇒ identical behavior.
54
+
55
+ ## 4. Migration parity
56
+
57
+ No `migrateConfig` — same reasoning as #722: the default resolves at the server
58
+ wiring site via the `developmentAgent` gate, so writing an explicit `false` into
59
+ existing configs would wrongly override the gate on dev agents.
60
+ `busyOrphanConfirmTicks` falls back to the code default. The new-agent template
61
+ (`generateClaudeMd`) gains an awareness line; no `migrateClaudeMd` (dark feature).
62
+
63
+ ## 5. Interaction with #722 (cpuAwareActiveProcessKeep)
64
+
65
+ The two are mutually exclusive on the CPU signal and complementary:
66
+ `cpuAwareActiveProcessKeep` acts when `cpuFlat === true` (relaxes the veto);
67
+ busy-orphan detection observes when `cpuFlat === false` (flags). They share the
68
+ `cpuProgressFlat` sampler, whose gate now runs when EITHER feature is on. With
69
+ both off (fleet default), the sampler returns `undefined` immediately and neither
70
+ path engages.
71
+
72
+ ## 6. Audit volume
73
+
74
+ `busy-orphan-suspected` fires at most once per suspect episode (on the dwell
75
+ crossing), `busy-orphan-cleared` once on recovery — bounded, not per-tick. Lands
76
+ in `logs/reaper-audit.jsonl` like the other reaper decision rows. Silent (no
77
+ Telegram, no attention item).
78
+
79
+ ## 7. What it does NOT do
80
+
81
+ - Does not reap, kill, nudge, or notify — observe-only.
82
+ - Does not run off-pressure or without the CPU dep.
83
+ - Does not change `hasActiveProcesses` or any guard.
84
+ - Does not attempt to *fix* a busy orphan (e.g. tree-kill) — that is a separate,
85
+ deliberately-staged follow-up that would build on this measurement.
@@ -0,0 +1,90 @@
1
+ # Side-Effects Review — SessionReaper CPU-aware active-process keep
2
+
3
+ **Version / slug:** `reaper-cpu-aware-keep`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (reaper-only, dark-by-default, no new external surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ Under CPU pressure, the reaper's `active-process` existence-veto additionally
11
+ requires positive descendant-CPU progress (sampled across ticks via the #706
12
+ `descendantCpuSeconds` helper). A CPU-flat child no longer keeps an idle session
13
+ un-reapable; the reaper falls through to its existing transcript-growth +
14
+ positive-idle checks. Gated by `cpuAwareActiveProcessKeep` (dark; dev agents on
15
+ via `developmentAgent`). Reaper-only — the shared `ReapGuard`/`ReapAuthority`
16
+ path is untouched.
17
+
18
+ ## Decision-point inventory
19
+
20
+ 1. **Pressure gate** — `tier === 'normal'` ⇒ never tighten. Off-load behavior is
21
+ byte-for-byte unchanged.
22
+ 2. **CPU-flat threshold** — `ratePerSec < cpuActiveMinRatePerSec` (default 0.02).
23
+ Below ⇒ "flat" (relax the veto); at/above ⇒ working (veto stands).
24
+ 3. **Can't-tell ⇒ KEEP** — feature off, dep absent, sample error, non-finite read,
25
+ first sighting (no prior delta), or CPU went backwards ⇒ `undefined` ⇒ the veto
26
+ is NOT relaxed.
27
+ 4. **Scope of relaxation** — only `blocked.reason === 'active-process'` is relaxed.
28
+ Every earlier keep-reason (protected, recovery, recent-user, commitment,
29
+ subagent, structural-long-work, …) returns first and is never affected.
30
+ 5. **Fall-through, not kill** — relaxing the veto does not reap; the stateful
31
+ transcript-growth + positive-idle gates (then hysteresis + two-phase + budget)
32
+ still must all clear.
33
+
34
+ ## 1. Over-block (false KEEP removed — could it now reap something it shouldn't?)
35
+
36
+ The only sessions newly reachable for reaping are: idle-at-a-ready-prompt,
37
+ transcript-not-growing, under CPU pressure, whose ONLY surviving keep-reason was
38
+ an existing-but-CPU-flat child. That is precisely the wedged/idle-child case we
39
+ want reclaimed. A genuinely-working session is still protected three ways:
40
+ (a) if any descendant burns CPU the delta is positive ⇒ veto stands;
41
+ (b) `descendantCpuSeconds` includes the main process, so a CPU-burning
42
+ extended-think keeps it; (c) the positive-idle gate requires an affirmative
43
+ ready-prompt with no working-footer, so a mid-turn session (footer present)
44
+ returns `no-positive-idle` and is kept regardless. A wedged-but-not-idle session
45
+ (no ready prompt) is likewise kept by positive-idle — the reaper stays
46
+ conservative there (that case is the StaleSessionBackstop's operator-ask job, not
47
+ the reaper's).
48
+
49
+ ## 2. Under-block (did we loosen a real protection?)
50
+
51
+ No protection is loosened for any non-reaper caller. The shared `ReapGuard` used
52
+ by `terminateSession` (the authority that vetoes OTHER killers) is unchanged, so
53
+ external kills still honor the full existence-veto. Only the deliberate,
54
+ pressure-gated, hysteresis-bounded reaper relaxes it, and only under load.
55
+
56
+ ## 3. Blast radius / rollout
57
+
58
+ Dark by default (`cpuAwareActiveProcessKeep: false`). Dev agents get it via the
59
+ `developmentAgent` gate at the server wiring site (`?? !!config.developmentAgent`)
60
+ — an explicit config value always wins. On the fleet (no `developmentAgent`) the
61
+ flag is false ⇒ the new code path is never entered ⇒ identical behavior to
62
+ today. Echo (developmentAgent) dogfoods it live on a genuinely loaded box.
63
+
64
+ ## 4. Migration parity
65
+
66
+ - **Config:** no `migrateConfig` entry is added *by design*. The default is
67
+ resolved at the server wiring site via the `developmentAgent` gate, NOT written
68
+ into `config.json`. Writing an explicit `false` into existing configs would
69
+ *wrongly override* the gate on dev agents (explicit beats `??`). Existing
70
+ agents therefore receive the correct behavior (dev on, fleet off) through the
71
+ code path with no config write. `cpuActiveMinRatePerSec` falls back to the
72
+ `DEFAULT_SESSION_REAPER_CONFIG` value for agents without the field.
73
+ - **CLAUDE.md:** the new-agent template (`generateClaudeMd`) gains an awareness
74
+ line. No `migrateClaudeMd` entry — the feature is dark on the fleet, so existing
75
+ agents don't need the awareness until a future fleet-promotion change (which
76
+ will add it), per the maturity-honesty principle (don't document a dark feature
77
+ to existing agents as if it's live).
78
+
79
+ ## 5. Observability
80
+
81
+ Each veto-relaxation emits a `cpu-keep-tightened` row to `logs/reaper-audit.jsonl`
82
+ (tier, verdict, keptBy, the active threshold). A kill-path behavior change leaves
83
+ a durable trail — never silent.
84
+
85
+ ## 6. What it does NOT do
86
+
87
+ - Does not change `hasActiveProcesses` itself (so `UpdateGate`, `SessionManager`
88
+ idle-detection, and `SessionRecovery` are untouched).
89
+ - Does not kill on CPU-flatness alone (always falls through to the idle proof).
90
+ - Does not run off-pressure or when CPU is unmeasurable.