instar 1.3.460 → 1.3.462

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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Robust consolidated-escalation send for SentinelNotifier.
3
+ *
4
+ * Background (incident 2026-06-09): the sentinel's `sendConsolidated` closures
5
+ * did `try { sendToTopic(lifelineTopicId) } catch { return false }`. The
6
+ * configured lifeline/system topic had been DELETED on the Telegram side, so
7
+ * every send returned `400: message thread not found` — and the bare `catch`
8
+ * black-holed it. 41 stall escalations in one day failed silently; the user got
9
+ * pure silence and could not tell a stalled session from a working one.
10
+ *
11
+ * `TelegramAdapter.ensureLifelineTopic()` already knows how to recreate a
12
+ * deleted lifeline topic and persist the new id — the send path just never
13
+ * invoked it. This helper closes that gap: send to the lifeline topic; on
14
+ * failure, self-heal via ensureLifelineTopic() and retry once; and NEVER
15
+ * swallow the error silently (it is always logged).
16
+ *
17
+ * Pure + adapter-injected so it is unit-testable without a live Telegram.
18
+ */
19
+ export interface ConsolidatedSendTelegram {
20
+ getLifelineTopicId(): number | undefined | null;
21
+ sendToTopic(topicId: number, text: string): Promise<unknown>;
22
+ /** Recreates the lifeline/system topic if deleted, persists the new id, returns it (or null). */
23
+ ensureLifelineTopic(): Promise<number | null>;
24
+ }
25
+ export declare function sendConsolidatedWithSelfHeal(tg: ConsolidatedSendTelegram, text: string, log: (line: string) => void): Promise<boolean>;
26
+ //# sourceMappingURL=sentinelConsolidatedSend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinelConsolidatedSend.d.ts","sourceRoot":"","sources":["../../src/monitoring/sentinelConsolidatedSend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,wBAAwB;IACvC,kBAAkB,IAAI,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAChD,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,iGAAiG;IACjG,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAkBD,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,wBAAwB,EAC5B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC1B,OAAO,CAAC,OAAO,CAAC,CA6ClB"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Robust consolidated-escalation send for SentinelNotifier.
3
+ *
4
+ * Background (incident 2026-06-09): the sentinel's `sendConsolidated` closures
5
+ * did `try { sendToTopic(lifelineTopicId) } catch { return false }`. The
6
+ * configured lifeline/system topic had been DELETED on the Telegram side, so
7
+ * every send returned `400: message thread not found` — and the bare `catch`
8
+ * black-holed it. 41 stall escalations in one day failed silently; the user got
9
+ * pure silence and could not tell a stalled session from a working one.
10
+ *
11
+ * `TelegramAdapter.ensureLifelineTopic()` already knows how to recreate a
12
+ * deleted lifeline topic and persist the new id — the send path just never
13
+ * invoked it. This helper closes that gap: send to the lifeline topic; on
14
+ * failure, self-heal via ensureLifelineTopic() and retry once; and NEVER
15
+ * swallow the error silently (it is always logged).
16
+ *
17
+ * Pure + adapter-injected so it is unit-testable without a live Telegram.
18
+ */
19
+ function errMsg(err) {
20
+ return err instanceof Error ? err.message : String(err);
21
+ }
22
+ /**
23
+ * True when the send error means the target topic is GONE (deleted/closed) — the
24
+ * only case where the message definitely did NOT land, so recreating + retrying
25
+ * is safe. We deliberately do NOT retry other (transient/network) errors: those
26
+ * may have landed at Telegram before the response was lost, and a blind retry
27
+ * would double-post the escalation (this path bypasses the /telegram/reply
28
+ * content-dedup). A transient failure is recovered by the sentinel's next sweep.
29
+ */
30
+ function isTopicGone(msg) {
31
+ return /message thread not found|thread not found|topic[ _-]?deleted|topic[ _-]?closed|chat not found/i.test(msg);
32
+ }
33
+ export async function sendConsolidatedWithSelfHeal(tg, text, log) {
34
+ const topicId = tg.getLifelineTopicId();
35
+ // Fast path: send to the currently-configured lifeline topic.
36
+ if (topicId) {
37
+ try {
38
+ await tg.sendToTopic(topicId, text);
39
+ return true;
40
+ }
41
+ catch (err) {
42
+ // De-swallow (the original `catch { return false }` black-holed this).
43
+ const msg = errMsg(err);
44
+ if (!isTopicGone(msg)) {
45
+ // Not a deleted-topic error — could be transient and may have partially
46
+ // landed. Do NOT retry (avoid a duplicate escalation); the sentinel
47
+ // re-escalates on its next sweep if the condition persists.
48
+ log(`escalation send to lifeline topic ${topicId} failed (${msg}) — transient/non-topic-gone, not retrying`);
49
+ return false;
50
+ }
51
+ log(`escalation send to lifeline topic ${topicId} failed (${msg}: topic gone) — self-healing`);
52
+ }
53
+ }
54
+ else {
55
+ log('escalation has no lifeline topic configured — establishing one');
56
+ }
57
+ // Self-heal: (re)establish the lifeline/system topic (recreates if deleted,
58
+ // persists the new id) and retry the send once.
59
+ let healed;
60
+ try {
61
+ healed = await tg.ensureLifelineTopic();
62
+ }
63
+ catch (err) {
64
+ log(`escalation self-heal (ensureLifelineTopic) threw (${errMsg(err)}) — alert NOT delivered`);
65
+ return false;
66
+ }
67
+ if (!healed) {
68
+ log('escalation self-heal could not establish a lifeline topic — alert NOT delivered');
69
+ return false;
70
+ }
71
+ try {
72
+ await tg.sendToTopic(healed, text);
73
+ return true;
74
+ }
75
+ catch (err) {
76
+ log(`escalation retry to healed lifeline topic ${healed} failed (${errMsg(err)}) — alert NOT delivered`);
77
+ return false;
78
+ }
79
+ }
80
+ //# sourceMappingURL=sentinelConsolidatedSend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinelConsolidatedSend.js","sourceRoot":"","sources":["../../src/monitoring/sentinelConsolidatedSend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,gGAAgG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,EAA4B,EAC5B,IAAY,EACZ,GAA2B;IAE3B,MAAM,OAAO,GAAG,EAAE,CAAC,kBAAkB,EAAE,CAAC;IAExC,8DAA8D;IAC9D,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACpC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,uEAAuE;YACvE,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YACxB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtB,wEAAwE;gBACxE,oEAAoE;gBACpE,4DAA4D;gBAC5D,GAAG,CAAC,qCAAqC,OAAO,YAAY,GAAG,4CAA4C,CAAC,CAAC;gBAC7G,OAAO,KAAK,CAAC;YACf,CAAC;YACD,GAAG,CAAC,qCAAqC,OAAO,YAAY,GAAG,8BAA8B,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,gEAAgE,CAAC,CAAC;IACxE,CAAC;IAED,4EAA4E;IAC5E,gDAAgD;IAChD,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,mBAAmB,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,qDAAqD,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAC/F,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,GAAG,CAAC,iFAAiF,CAAC,CAAC;QACvF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,6CAA6C,MAAM,YAAY,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACzG,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.460",
3
+ "version": "1.3.462",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -173,11 +173,19 @@ If any check fails, the commit is rejected. This is not a warning — it's a blo
173
173
 
174
174
  The pre-push gate (`scripts/pre-push-gate.js`) re-verifies at push time: any release commit whose upgrade notes claim a fix or feature must have a matching artifact in `upgrades/side-effects/`.
175
175
 
176
+ ### Phase 7 — Auto-merge on green (EVERY tier — never pause to ask)
177
+
178
+ When the PR is open and CI goes green, the agent **merges it** — it does NOT pause to ask the operator "ready to merge?" / "want me to merge?". By the time CI is green, the change has already cleared every quality gate this skill enforces: the converged + approved spec (Tier 2) or the staged ELI16 + side-effects artifact (Tier 1), the side-effects review (plus the second-pass review for high-risk changes), the full instar-dev pre-commit + pre-push gates, and the complete CI suite (unit shards + integration + e2e). **Green CI = mergeable. Full stop.** Asking the operator to merge a green PR is redundant ceremony that stalls autonomous delivery — a *Structure-over-Willpower* regression this phase exists to prevent.
179
+
180
+ Perform the merge with `node scripts/safe-merge.mjs <PR#> --squash --admin`. That wrapper re-imposes the requirement `--admin` removes: it waits for every check to finish, REFUSES if any check is red (and specifically confirms an e2e check ran and passed), and only then merges — so the branch-protection safety is preserved even on a behind / hot branch (no separate `update-branch` + full CI re-run needed). After the merge lands, narrate the ship via `POST /telegram/post-update` (the Agent Updates channel) — never a "ready to merge?" question in the working topic.
181
+
182
+ The ONLY thing that stops the merge is a genuinely-red check **on this change**: fix it and re-run. An unrelated environmental flake (a different test failing run-to-run, a tmux/server-boot timeout, a CDN 504) is re-run (`gh run rerun --failed` or a fresh push) — never escalated to the operator as a "should I merge?" question. (Source: operator directive 2026-06-09, topic 23178 — "never pause and ask me to merge; we have enough infra in place to ensure it's good to merge by the time it gets there.")
183
+
176
184
  ## Tiered development (tier signal → you decide → audited)
177
185
 
178
186
  Not every change is the same size or risk, so not every change pays the same process cost. The commit gate (`scripts/instar-dev-precommit.js`) prints a **tier SIGNAL** — a suggested tier from the change's size (LOC + files) and a **risk floor** raised by any safety-invariant, irreversibility, migration/fleet-rollout, or new-capability signal (`scripts/lib/classify-tier.mjs`). The signal **informs**; it never decides. **You DECLARE the tier** in the trace via `write-trace.mjs --tier <1|2|3> --tier-reasoning "<why>"`. This is the constitution's **The Body and the Mind** made executable (`docs/STANDARDS-REGISTRY.md` → The Substrate): the gate (body) informs, the agent (mind) decides, the decision is audited.
179
187
 
180
- - **Tier 1 (small / low-risk):** lighter requirement set — a staged **ELI16** + a staged **side-effects** artifact, no pre-approved converged spec. Declare it with `--tier 1 --eli16-path <path> --side-effects-path <path>` (`--spec` is optional at Tier 1). The PR is the review surface, and **Echo auto-merges a clean Tier-1 on green CI** with operator spot-check.
188
+ - **Tier 1 (small / low-risk):** lighter requirement set — a staged **ELI16** + a staged **side-effects** artifact, no pre-approved converged spec. Declare it with `--tier 1 --eli16-path <path> --side-effects-path <path>` (`--spec` is optional at Tier 1). The PR is the review surface. (Auto-merge-on-green is **not** a Tier-1 privilege per Phase 7 it applies to EVERY tier; Tier 1 differs only in the lighter pre-merge gate, never in the merge behavior.)
181
189
  - **Tier 2+ (everything else):** the full chain above — converged + approved spec, ELI16, side-effects, fresh trace. A **Tier-3 project step** is just a Tier-2 spec; nothing extra is enforced at the gate. **No declared tier → Tier-2** (back-compatible).
182
190
 
183
191
  **The decision is audited.** Every in-scope commit appends one line to `.instar/instar-dev-decisions.jsonl` (signal, declared tier, risk floor + reasons). When you declare **under** the risk-signaled floor, the gate prints a loud `belowFloor` notice and records `belowFloor:true` — it does **not** block (you hold authority), but the override is now a reviewable record. Per **Close the Loop**, those `belowFloor` rates get reviewed on a cadence so the risk-floor list grows.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-10T03:11:13.813Z",
5
- "instarVersion": "1.3.460",
4
+ "generatedAt": "2026-06-10T03:41:06.411Z",
5
+ "instarVersion": "1.3.462",
6
6
  "entryCount": 199,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,30 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Adds **Phase 7 — Auto-merge on green** to the `instar-dev` skill: once an instar-dev PR's CI is fully
9
+ green, the instar-developing agent merges it via the existing `scripts/safe-merge.mjs` wrapper (which
10
+ waits for every check, refuses on any red, and confirms the e2e job ran and passed) and narrates the
11
+ ship via the Agent Updates channel — it no longer pauses to ask the operator "ready to merge?".
12
+ Auto-merge-on-green now applies to **every tier**, not just Tier-1; the Tier-1 wording is corrected so
13
+ it no longer reads as a Tier-1-only privilege. No runtime code changes — the merge mechanism and CI
14
+ gates it relies on are unchanged. Structure-over-Willpower fix for an operator directive (never pause
15
+ to ask for merge; the gates already guarantee mergeability by the time CI is green).
16
+
17
+ ## What to Tell Your User
18
+
19
+ None — internal change (no user-facing surface).
20
+
21
+ ## Summary of New Capabilities
22
+
23
+ None — internal change (no user-facing surface).
24
+
25
+ ## Evidence
26
+
27
+ - `skills/instar-dev/SKILL.md` — new Phase 7 + corrected Tier-1 line.
28
+ - Mechanism unchanged: `scripts/safe-merge.mjs` already re-imposes all-checks-green (incl. e2e) before
29
+ merging. Tier-1 instar-dev gate passed (ELI16 + side-effects + trace); this PR auto-merges on green
30
+ per the very phase it adds.
@@ -0,0 +1,83 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Makes the SentinelNotifier's consolidated-escalation delivery **self-heal a deleted lifeline/system topic instead of silently swallowing the error**. Both `sendConsolidated` closures in `server.ts` (the sentinel-notify path and the stop-notify path) did `try { telegram.sendToTopic(lifelineTopicId, text) } catch { return false }`. When the lifeline/system topic is deleted on the Telegram side, every send returns `400: message thread not found`, and the bare `catch` black-holed it — so stall/stop escalations the system generated never reached the user, with zero log trace. New shared helper `sendConsolidatedWithSelfHeal` (`src/monitoring/sentinelConsolidatedSend.ts`): sends to the lifeline topic; on failure it (1) logs the real error (de-swallow) and (2) calls the adapter's existing `ensureLifelineTopic()` — which recreates a deleted topic and persists the new id — then retries the send once. Both call sites now use it. No behavior change on the happy path (a working topic sends and returns immediately, never touching `ensureLifelineTopic`).
9
+
10
+ ## What to Tell Your User
11
+
12
+ If your "Lifeline"/system topic ever gets deleted, the alerts the system tries to send you there (a session went quiet, a session stopped) used to vanish without a trace — you'd just get silence. Now those alerts heal themselves: the system recreates the topic and delivers the alert, and any delivery failure is logged instead of disappearing. (This is the foundation under the broader "tell me when a session stalls, in that session's own topic, and auto-recover it" work.)
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ | Capability | How to Use |
17
+ |-----------|-----------|
18
+ | Sentinel/stop escalations self-heal a deleted lifeline topic + never swallow the send error | automatic — no config |
19
+
20
+ ## Evidence
21
+
22
+ Reproduction (live, 2026-06-09): the configured lifeline topic (`lifelineTopicId: 2`, "Lifeline") had been deleted on Telegram. `logs/sentinel-events.jsonl` showed the active-silence sentinel correctly detecting stalled sessions and emitting `escalated` ("session went quiet ~16 min ago, want me to dig in?") — immediately followed by `notify-error: sendConsolidated returned false`, **41 times in one day**. A direct probe (`POST /telegram/reply/2`) returned exactly `400: Bad Request: message thread not found`, confirming the dead topic. The user received pure silence, so a stalled session was indistinguishable from a working one.
23
+
24
+ After the fix: `tests/unit/sentinel-consolidated-send.test.ts` (7 cases) pins the happy path (no ensureLifelineTopic call), the dead-topic self-heal (recreate id 2→new, retry, deliver), the no-topic-configured path, and all three failure modes (ensure returns null / ensure throws / retry fails) — each returning false WITH a logged reason, never a silent swallow. tsc + repo lint clean. (The 15 unrelated A2ARedeliverySentinel / SessionActivitySentinel local failures are pre-existing on main, not introduced here.)
25
+
26
+ ## The problem
27
+
28
+ The operator couldn't tell whether a session had stalled or was just working —
29
+ because for almost an hour it heard nothing. Here's what was actually happening
30
+ under the hood, and it's worse than "the agent didn't notice."
31
+
32
+ The agent's stall-detector **did** notice. It saw a session go quiet, gave it a
33
+ nudge, and tried to send the operator a message: "this session went quiet ~16
34
+ minutes ago, want me to dig in?" That message goes to a dedicated "Lifeline"
35
+ system topic in Telegram.
36
+
37
+ But that Lifeline topic had been **deleted** on the Telegram side, while the
38
+ config still pointed at it. So every send came back with `message thread not
39
+ found` — and the code did `catch { return false }`, i.e. it threw the error in
40
+ the trash and moved on. **41 times in one day** the system had something
41
+ important to tell the operator and it silently failed to send. The operator got
42
+ pure silence, which looks exactly like "nothing's wrong."
43
+
44
+ So the real bug wasn't a missing alert — it was a generated alert dying on the
45
+ way out the door, invisibly.
46
+
47
+ ## What already exists
48
+
49
+ The Telegram adapter already has a method, `ensureLifelineTopic()`, that knows
50
+ how to **recreate** a deleted Lifeline topic and remember the new one. The alert
51
+ path just never called it — and worse, it hid the failure instead of reacting to
52
+ it.
53
+
54
+ ## What's new
55
+
56
+ One small, shared helper that both alert paths now use. When it tries to send an
57
+ alert and the send fails, it:
58
+
59
+ 1. **Logs the real error** instead of swallowing it — so a delivery failure can
60
+ never again be invisible.
61
+ 2. **Heals the topic** — calls `ensureLifelineTopic()` to recreate the deleted
62
+ topic, then retries the send once.
63
+
64
+ If the topic is fine, nothing changes — it sends and returns immediately. Only on
65
+ a failure does the heal-and-retry kick in.
66
+
67
+ ## The safeguards in plain terms
68
+
69
+ - **No silent failures, ever.** Every send failure is now logged with the real
70
+ reason, so the next time something can't be delivered, it's diagnosable in
71
+ seconds instead of invisible for hours.
72
+ - **Self-healing, not just louder.** A deleted system topic now repairs itself
73
+ and the alert still gets through.
74
+ - **Zero change on the happy path.** A working topic behaves exactly as before —
75
+ the heal logic only runs when a send actually fails.
76
+ - **Bounded.** It retries once, never loops.
77
+
78
+ ## What you need to decide
79
+
80
+ Nothing — it ships as a normal patch with safe behavior. This is the foundation
81
+ under the bigger piece you asked for (stall alerts that land in the *stalled
82
+ session's own topic*, and auto-recovering the session, not just alerting): step
83
+ one was making sure these alerts can't silently disappear in the first place.
@@ -0,0 +1,88 @@
1
+ # Side-Effects Review — instar-dev: auto-merge on green (every tier)
2
+
3
+ **Version / slug:** `instar-dev-automerge-on-green`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `not required (Tier 1 — documentation/process change to the dev skill; the merge mechanism it mandates (safe-merge.mjs + CI) is unchanged and already gated)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds **Phase 7 — Auto-merge on green (every tier)** to `skills/instar-dev/SKILL.md`, and corrects the
11
+ Tier-1 description so auto-merge no longer reads as a Tier-1-only privilege. The new phase mandates that
12
+ once an instar-dev PR's CI is fully green, the agent merges it via the existing
13
+ `scripts/safe-merge.mjs <PR> --squash --admin` wrapper and narrates the ship via `/telegram/post-update`
14
+ — it does NOT pause to ask the operator "ready to merge?". Documentation/process change only; no runtime
15
+ code, no new gate, no new merge mechanism (safe-merge.mjs already exists and re-imposes all-checks-green
16
+ before merging, including an explicit e2e-ran-and-passed check). Driven by the operator directive
17
+ (2026-06-09, topic 23178): "never pause and ask me to merge; we have enough infra in place to ensure
18
+ it's good to merge by the time it gets there. Lets fix this via infrastructure."
19
+
20
+ ## Decision-point inventory
21
+
22
+ - No decision point added/changed. This is a process-doc change to the instar-dev skill. The actual
23
+ merge safety lives in `safe-merge.mjs` (waits for all checks, refuses on any red, confirms e2e) + CI
24
+ branch protection — both unchanged.
25
+
26
+ ## 1. Over-block
27
+
28
+ No block/allow surface — over-block not applicable.
29
+
30
+ ## 2. Under-block
31
+
32
+ No block/allow surface — under-block not applicable. (The only behavior changed is removing a human
33
+ pause AFTER green CI; the green verdict itself — the actual quality gate — is unchanged. `safe-merge.mjs`
34
+ still refuses to merge anything not fully green.)
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ Correct layer. The fix Justin asked for is behavioral: stop pausing to ask for merge. The instar-dev
39
+ skill is exactly where the dev-agent's process is defined, so adding Phase 7 there is the
40
+ right-altitude structural fix (Structure > Willpower — the behavior is now baked into the governing
41
+ process doc, not left to the agent "remembering" not to pause). The merge mechanism it points at
42
+ (`safe-merge.mjs`) already existed; this just makes invoking it the mandated final step for every tier.
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ - [x] No — this change has no block/allow surface (documentation/process change).
47
+
48
+ ## 5. Interactions
49
+
50
+ - **Shadowing / double-fire:** none. `safe-merge.mjs` is idempotent-safe (it no-ops / refuses if the PR
51
+ isn't green or is already merged).
52
+ - **Races:** none introduced.
53
+ - **Existing Tier-1 auto-merge wording:** the prior line implied auto-merge was a Tier-1 privilege;
54
+ Phase 7 generalizes it and the Tier-1 line is corrected to point at Phase 7, so the two no longer
55
+ conflict.
56
+
57
+ ## 6. External surfaces
58
+
59
+ - **Merge behavior:** the agent now auto-merges green instar-dev PRs instead of asking. This is the
60
+ intended change. It is bounded by CI + `safe-merge.mjs` (refuses on any red, confirms e2e ran+passed),
61
+ so an unsafe PR cannot be auto-merged. Ship narration moves to the Agent Updates channel
62
+ (`/telegram/post-update`) rather than a "ready to merge?" question in the working topic.
63
+ - **Migration Parity:** this updates the **instar-dev DEV skill** — used only by the instar-developing
64
+ agent (Echo / a dev-assigned agent), not an end-user feature skill. Per the known skill-install gap
65
+ (the root `skills/` dir, including `instar-dev`, is not synced by `installBuiltinSkills()` and has no
66
+ PostUpdateMigrator path — a separately-tracked issue), the canonical source change here reaches the
67
+ repo; the installed-copy sync for dev agents rides that pre-existing gap. The authoring agent updates
68
+ its own installed `.claude/skills/instar-dev/SKILL.md` directly so the behavior is live immediately.
69
+ A general fix to dev-skill sync is out of scope (it is the standing skill-install-gap project).
70
+ - **No config / CLAUDE.md template / hook / route change.** No persisted state.
71
+
72
+ ## 7. Rollback cost
73
+
74
+ Pure documentation revert — revert the PR; the skill returns to the Tier-1-only auto-merge wording. No
75
+ data migration, no runtime impact, no agent-state repair.
76
+
77
+ ## Conclusion
78
+
79
+ A focused Structure-over-Willpower fix for the operator's directive: auto-merge-on-green becomes the
80
+ mandated final phase for every instar-dev tier, removing the redundant "ask to merge" pause while
81
+ leaving the actual merge-safety mechanism (`safe-merge.mjs` + CI) untouched. Tier 1 (doc/process change
82
+ to the dev skill); second-pass not required. It will be auto-merged on green per the very phase it adds.
83
+
84
+ ## Second-pass review (if required)
85
+
86
+ **Reviewer:** not required — Tier-1 documentation/process change to the dev skill; no block/allow,
87
+ session-lifecycle, sentinel/guard/gate, or recovery-path code is modified. The merge mechanism
88
+ (`safe-merge.mjs`) and CI gates it relies on are unchanged.
@@ -0,0 +1,65 @@
1
+ # Side-Effects Review — sentinel escalation self-heal (no silent swallow)
2
+
3
+ **Version / slug:** `sentinel-escalation-selfheal`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `independent reviewer subagent — concern raised, addressed in code`
7
+
8
+ ## Summary of the change
9
+
10
+ Both `sendConsolidated` closures in `server.ts` (sentinel-notify ~6988, stop-notify ~9689) used `try { telegram.sendToTopic(lifelineTopicId) } catch { return false }`. A deleted lifeline/system topic made every send throw `message thread not found`, swallowed silently — 41 stall/stop escalations black-holed in one day. New helper `src/monitoring/sentinelConsolidatedSend.ts#sendConsolidatedWithSelfHeal(tg, text, log)`: send to the lifeline topic; on failure, log the real error and call the adapter's existing `ensureLifelineTopic()` (recreates a deleted topic + persists the new id), then retry once. Both sites now delegate to it. Files: `src/monitoring/sentinelConsolidatedSend.ts` (new), `src/commands/server.ts` (import + 2 call sites), `tests/unit/sentinel-consolidated-send.test.ts` (new).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - Sentinel/stop escalation DELIVERY (not a block/allow gate) — **modify** — adds a self-heal + retry + de-swallow around the existing send. No new gating decision; it only changes how a notification is delivered and whether failures are visible.
15
+
16
+ ## 1. Over-block
17
+
18
+ No block/allow surface — this is a delivery path, not a gate. It cannot reject any input. (Over-block N/A.)
19
+
20
+ ## 2. Under-block
21
+
22
+ N/A (no gate). Residual delivery risk: if `ensureLifelineTopic()` can't create a topic (non-forum chat, Telegram down), the alert still isn't delivered — but now it returns false WITH a logged reason (previously silent). The SentinelNotifier's own escalation-state handling of a false return is unchanged.
23
+
24
+ ## 3. Level-of-abstraction fit
25
+
26
+ Correct layer. The fix lives at the escalation-delivery callback where the swallow was, and reuses the adapter's existing `ensureLifelineTopic()` (the right owner of topic lifecycle) rather than re-implementing topic creation. Extracted to a small injected-dependency module so it's unit-testable without a live Telegram (the inline closures were untestable, which is why the swallow shipped).
27
+
28
+ ## 4. Signal vs authority compliance
29
+
30
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
31
+
32
+ - [x] No — this has no block/allow surface; it is a delivery mechanism. It adds no authority; it makes an existing best-effort send self-heal and stop hiding failures.
33
+
34
+ ## 5. Interactions
35
+
36
+ - **Shadowing / double-fire:** retries the send at most once, and ONLY when the first error matches a topic-gone signature (`message thread not found` / topic deleted/closed / chat not found) — the only case where the message definitely did NOT land. A transient/other error (e.g. 429, network blip that may have landed at Telegram before the response was lost) is NOT retried — it logs and returns false, and the sentinel re-escalates on its next sweep. This eliminates the double-post window: a blind retry would NOT be caught by the `/telegram/reply` ~15-min content-dedup, because this helper calls `sendToTopic` directly (bypassing that route). No loop.
37
+ - **ensureLifelineTopic side effect:** it creates a Telegram topic + persists `lifelineTopicId` (via `persistLifelineTopicId`). This is the SAME self-heal it already performs on startup; invoking it on send-failure is the intended use ("called on startup and can be called periodically"). It runs only on a failure path (rare), so no topic-creation spam.
38
+ - **Two call sites, one helper:** both sentinel-notify and stop-notify now share identical, tested behavior (previously duplicated inline). No behavior divergence.
39
+ - **Races:** the helper is stateless; `ensureLifelineTopic` owns its own concurrency. No new shared state.
40
+
41
+ ## 6. External surfaces
42
+
43
+ - **Users:** ships to the install base via normal release. Visible effect: system escalations (session-quiet, session-stopped) now actually arrive even if the Lifeline topic was deleted, and never silently fail. A recreated Lifeline topic appears once if the old one was deleted (expected, one-time).
44
+ - **Persistent state:** `ensureLifelineTopic` persists the new `lifelineTopicId` — same as its existing startup behavior. No new state shape.
45
+ - **Telegram:** one extra API call (`ensureLifelineTopic`) only on a send failure.
46
+
47
+ ## 7. Rollback cost
48
+
49
+ Pure code change (new module + 2 call-site swaps), no migration. Back-out = revert; behavior returns to the prior silent-swallow. Low. (The original incident — a dead topic 2 — self-heals on the first escalation after deploy; no manual topic surgery needed.)
50
+
51
+ ## Conclusion
52
+
53
+ A focused robustness fix: it converts a 100%-silent escalation-delivery failure (dead lifeline topic, error swallowed) into self-heal-and-retry with full logging, reusing the adapter's existing topic-recreation. No gating surface, signal-vs-authority compliant, no migration. It is the foundation for the follow-on work the operator requested (route stall alerts to the stalled session's own topic + confidence-gated auto-recovery of the session) — tracked separately. Because it touches the messaging-escalation delivery path, a Phase-5 second-pass review is requested.
54
+
55
+ ## Second-pass review (if required)
56
+
57
+ **Reviewer:** independent reviewer subagent (general-purpose)
58
+ **Independent read of the artifact: concern raised → addressed in code**
59
+
60
+ The reviewer confirmed the self-heal flow is correct on all 7 paths (happy path doesn't call ensureLifelineTopic; dead-topic heals+retries on the new id; no-topic-configured establishes one; all three failure modes return false + log, never throw to the caller; bounded single retry; both call sites converted with the old swallow gone; tsc-clean type compatibility; ensureLifelineTopic only on failure). It raised ONE valid concern: the original artifact claimed the ~15-min `/telegram/reply` content-dedup covered the partial-send-then-throw duplicate window, but this helper calls `sendToTopic` directly and bypasses that dedup — so a blind retry on a transient (non-topic-gone) error that had actually landed could double-post one escalation. **Addressed in code** (the reviewer's recommended belt-and-suspenders): the retry is now gated on an `isTopicGone(err)` signature check — only deleted/closed-topic errors (where the message definitely didn't land) self-heal + retry; transient errors log + return false (no retry), recovered by the next sentinel sweep. Eliminates the duplicate window entirely. New test case `transient (non-topic-gone) send error → does NOT retry` pins it (exactly one send attempt, ensureLifelineTopic not called). 7/7 helper tests green; tsc clean.
61
+
62
+ ## Evidence pointers
63
+
64
+ - Live: `logs/sentinel-events.jsonl` — 41 × `notify-error: sendConsolidated returned false` paired with `active-silence`/`stop` escalations on 2026-06-09; `POST /telegram/reply/2` → `400: message thread not found`; config `lifelineTopicId: 2` (deleted on Telegram).
65
+ - Tests: `tests/unit/sentinel-consolidated-send.test.ts` (6 cases, both sides incl. all failure modes). tsc + lint clean. (15 unrelated A2A/SessionActivity sentinel test failures are pre-existing on main, verified by stash-compare.)