instar 1.3.38 → 1.3.39

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,38 @@
1
+ /**
2
+ * installAgentMessageHook — production binding for the agent-to-agent Telegram comms
3
+ * receiver (spec MENTOR-LIVE-READINESS §Fix 2b, PR 3b). Composes the PR-1 routing logic
4
+ * (AgentTelegramComms.decideRoute) + the PR-3a storage primitives (AgentTelegramLedger,
5
+ * ProcessedIdStore) into the AgentMessageHook that TelegramAdapter calls before normal
6
+ * dispatch.
7
+ *
8
+ * The role-handler map is **per-recipient** (Echo registers `mentor-reply`; Codey
9
+ * registers `mentor`). Routed messages dispatch via the map; drops are audited; non-a2a
10
+ * messages fall through (`{handled:false}`).
11
+ */
12
+ import { type RecipientConfig, type A2aMessage } from './AgentTelegramComms.js';
13
+ import type { AgentTelegramLedger } from './AgentTelegramLedger.js';
14
+ import type { ProcessedIdStore } from './ProcessedIdStore.js';
15
+ import type { AgentMessageHook } from './TelegramAdapter.js';
16
+ /** A role-handler is called when decideRoute returns 'route' for that role. The handler
17
+ * consumes the message body (post-marker-strip) and any audit context it wants. */
18
+ export type RoleHandler = (msg: A2aMessage, ctx: {
19
+ topicId: number;
20
+ senderBotId?: string;
21
+ }) => Promise<void>;
22
+ export interface InstallAgentMessageHookDeps {
23
+ config: RecipientConfig;
24
+ ledger: AgentTelegramLedger;
25
+ processedIds: ProcessedIdStore;
26
+ /** Map of role → handler. A role-handler is called only when decideRoute returns
27
+ * `route` AND the role is in `config.acceptRoles[from]` — the per-source admission
28
+ * matrix gates ahead of the role-handler map. */
29
+ roleHandlers: Map<string, RoleHandler>;
30
+ }
31
+ /**
32
+ * Build the AgentMessageHook closure. Never throws — any internal error is captured
33
+ * + audited as a 'dropped' row with reason 'agent-marker-malformed' (the closest existing
34
+ * code), and the hook returns `{handled:true}` so a broken handler can't double-process
35
+ * via the user-flow fallback.
36
+ */
37
+ export declare function buildAgentMessageHook(deps: InstallAgentMessageHookDeps): AgentMessageHook;
38
+ //# sourceMappingURL=installAgentMessageHook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installAgentMessageHook.d.ts","sourceRoot":"","sources":["../../src/messaging/installAgentMessageHook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,UAAU,EAChB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;oFACoF;AACpF,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7G,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,YAAY,EAAE,gBAAgB,CAAC;IAC/B;;sDAEkD;IAClD,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACxC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CAuFzF"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * installAgentMessageHook — production binding for the agent-to-agent Telegram comms
3
+ * receiver (spec MENTOR-LIVE-READINESS §Fix 2b, PR 3b). Composes the PR-1 routing logic
4
+ * (AgentTelegramComms.decideRoute) + the PR-3a storage primitives (AgentTelegramLedger,
5
+ * ProcessedIdStore) into the AgentMessageHook that TelegramAdapter calls before normal
6
+ * dispatch.
7
+ *
8
+ * The role-handler map is **per-recipient** (Echo registers `mentor-reply`; Codey
9
+ * registers `mentor`). Routed messages dispatch via the map; drops are audited; non-a2a
10
+ * messages fall through (`{handled:false}`).
11
+ */
12
+ import { decideRoute, } from './AgentTelegramComms.js';
13
+ /**
14
+ * Build the AgentMessageHook closure. Never throws — any internal error is captured
15
+ * + audited as a 'dropped' row with reason 'agent-marker-malformed' (the closest existing
16
+ * code), and the hook returns `{handled:true}` so a broken handler can't double-process
17
+ * via the user-flow fallback.
18
+ */
19
+ export function buildAgentMessageHook(deps) {
20
+ const isoTs = (ms) => new Date(ms).toISOString();
21
+ return async (input) => {
22
+ const ctx = {
23
+ raw: input.text,
24
+ senderIsBot: input.senderIsBot,
25
+ senderChatId: input.senderChatId,
26
+ senderBotId: input.senderBotId,
27
+ now: input.now,
28
+ };
29
+ const decision = decideRoute(ctx, deps.config, {
30
+ isProcessed: (id) => deps.processedIds.hasProcessed(id),
31
+ knownRole: (r) => deps.roleHandlers.has(r),
32
+ });
33
+ if (decision.action === 'fall-through') {
34
+ return { handled: false };
35
+ }
36
+ if (decision.action === 'drop') {
37
+ const row = {
38
+ localTs: isoTs(input.now),
39
+ direction: 'received',
40
+ decision: 'dropped',
41
+ dropReason: decision.reason,
42
+ fromAgent: decision.msg?.from,
43
+ toAgent: decision.msg?.to,
44
+ role: decision.msg?.role,
45
+ id: decision.msg?.id,
46
+ corr: decision.msg?.corr,
47
+ ts: decision.msg?.ts,
48
+ telegramFromBotId: input.senderBotId,
49
+ telegramSenderChatId: input.senderChatId,
50
+ topicId: input.topicId,
51
+ rawPrefix: input.text.slice(0, 200),
52
+ };
53
+ deps.ledger.appendReceived(row);
54
+ return { handled: true };
55
+ }
56
+ // action === 'route'
57
+ const msg = decision.msg;
58
+ const handler = deps.roleHandlers.get(msg.role);
59
+ if (!handler) {
60
+ // Defensive: knownRole said yes but the map slot is undefined (race / wiring bug).
61
+ // Audit as dropped with a synthetic reason; do NOT fall through.
62
+ deps.ledger.appendReceived({
63
+ localTs: isoTs(input.now),
64
+ direction: 'received',
65
+ decision: 'dropped',
66
+ dropReason: 'agent-marker-unknown-role',
67
+ fromAgent: msg.from, toAgent: msg.to, role: msg.role, id: msg.id, corr: msg.corr, ts: msg.ts,
68
+ telegramFromBotId: input.senderBotId,
69
+ telegramSenderChatId: input.senderChatId,
70
+ topicId: input.topicId,
71
+ });
72
+ return { handled: true };
73
+ }
74
+ // Idempotency: mark BEFORE invoking the handler so a handler crash mid-process still
75
+ // dedups the retry (at-least-once delivery, exactly-once attempted processing).
76
+ deps.processedIds.markProcessed(msg.id);
77
+ deps.ledger.appendReceived({
78
+ localTs: isoTs(input.now),
79
+ direction: 'received',
80
+ decision: 'routed',
81
+ fromAgent: msg.from, toAgent: msg.to, role: msg.role, id: msg.id, corr: msg.corr, ts: msg.ts,
82
+ telegramFromBotId: input.senderBotId,
83
+ telegramSenderChatId: input.senderChatId,
84
+ topicId: input.topicId,
85
+ });
86
+ try {
87
+ await handler(msg, { topicId: input.topicId, senderBotId: input.senderBotId });
88
+ }
89
+ catch (err) {
90
+ // A role-handler failure is recorded but doesn't un-mark the id (we still treat
91
+ // the message as processed; retrying would just re-fail). Stage-B forensics can see
92
+ // the routed row + the absence of a downstream effect.
93
+ // eslint-disable-next-line no-console
94
+ console.error(`[a2a] role-handler "${msg.role}" failed:`, err instanceof Error ? err.message : String(err));
95
+ }
96
+ return { handled: true };
97
+ };
98
+ }
99
+ //# sourceMappingURL=installAgentMessageHook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installAgentMessageHook.js","sourceRoot":"","sources":["../../src/messaging/installAgentMessageHook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.38",
3
+ "version": "1.3.39",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-28T00:18:26.420Z",
5
- "instarVersion": "1.3.38",
6
- "entryCount": 192,
4
+ "generatedAt": "2026-05-28T00:38:03.268Z",
5
+ "instarVersion": "1.3.39",
6
+ "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
9
9
  "id": "hook:session-start",
@@ -396,6 +396,14 @@
396
396
  "contentHash": "813fd9164514fd11d1bdc67f4dbee02a74679b3ca46bf1b39893c62deb2e58cb",
397
397
  "since": "2025-01-01"
398
398
  },
399
+ "skill:test-as-self": {
400
+ "id": "skill:test-as-self",
401
+ "type": "skill",
402
+ "domain": "skills",
403
+ "sourcePath": ".claude/skills/test-as-self/SKILL.md",
404
+ "contentHash": "54849a1b6f6d87c57d8254e55bf7826fa18dc99c878971d0eae43b6b09c48d93",
405
+ "since": "2025-01-01"
406
+ },
399
407
  "route-group:health": {
400
408
  "id": "route-group:health",
401
409
  "type": "route-group",
@@ -1513,7 +1521,7 @@
1513
1521
  "type": "subsystem",
1514
1522
  "domain": "communication",
1515
1523
  "sourcePath": "src/lifeline/TelegramLifeline.ts",
1516
- "contentHash": "792bafba6fb25e34341a3b73cb8c9cb882b946530b652d8d271af09d1242a93e",
1524
+ "contentHash": "6f5b9a817019b735b9afe3fded858ce713c8bc770b1d3bf018b65905eef25378",
1517
1525
  "since": "2025-01-01"
1518
1526
  },
1519
1527
  "subsystem:orphan-process-reaper": {
@@ -4,25 +4,25 @@
4
4
 
5
5
  ## What Changed
6
6
 
7
- Closes a fleet-wide "dark guardrail" migration gap. `instar init` wires a full set of PreToolUse `Bash` guardrail hooks for new agents, but the existing-agent update path (`PostUpdateMigrator.migrateSettings`) only ever switched on two of them. So four guardrails — `deferral-detector.js` (the false-blocker / anti-deferral pre-filter), `grounding-before-messaging.sh`, `external-communication-guard.js`, and `post-action-reflection.js` — were copied to disk on every existing agent but never wired into `.claude/settings.json`, leaving them installed-but-inert. Same failure class as the 2026-05-27 silent-stall incident.
7
+ Self-Propagation, Part 1 — **structural fix for the Telegram "two listeners" clash (409 dual-poll)**. Telegram allows exactly one long-poller per bot token; instar has two potential pollers (the lifeline and the server). Until now, the server only stayed quiet when started with `--no-telegram` (or on a standby machine) — operator discipline, not structure. Start the server without that flag while the lifeline polled the same token → guaranteed 409 (the failure mode the 2026-05-27 live test hit).
8
8
 
9
- This release introduces a single source of truth for the canonical instar PreToolUse hook set (`src/core/instarSettingsHooks.ts`), consumed by BOTH the new-agent path (`init.ts`) and the existing-agent path, so the two can never drift again. `migrateSettings` now idempotently ensures every canonical Bash guardrail is present (appends only the missing ones; never reorders or removes; leaves custom hooks alone).
9
+ This release introduces a tiny "I am polling" lease the lifeline refreshes each successful poll tick. The server checks for it before starting its own polling; if a live lease for the same bot token is present, the server auto-demotes to send-only with a clear log line. Then dual-polling cannot happen regardless of how the server is started. The lease stores the bot token's SHA-256 hash, never the raw token. Every read path is fail-OPEN — a missing/stale/mismatched lease means the server polls as today, so setups without a lifeline see ZERO behavioral change.
10
+
11
+ Pairs with Part 2 (the `instar test-as-self` harness — shipping under the same approved spec in a separate PR).
10
12
 
11
13
  ## What to Tell Your User
12
14
 
13
- - Four guardrail hooks that were quietly switched off on existing agents — including the "false blocker" catcher that stops the agent handing a doable task back to you — come on at this update. New agents already had them; no action needed.
15
+ - A whole class of "two things trying to listen at once" Telegram errors is fixed structurally — it can no longer happen on a single machine no matter how the server got started.
14
16
 
15
17
  ## Summary of New Capabilities
16
18
 
17
19
  | Capability | How to Use |
18
20
  |-----------|-----------|
19
- | Existing agents get the full instar PreToolUse guardrail set on update | Automatic on update; no action needed |
20
- | Anti-drift: new-agent + existing-agent hook lists share one constant | Adding a hook to `INSTAR_BASH_PRETOOLUSE_HOOKS` wires both paths; a unit test locks the set |
21
+ | Server auto-demotes to Telegram send-only when the lifeline owns the poll slot | Automatic; the lifeline writes the lease, the server reads it on startup |
21
22
 
22
23
  ## Evidence
23
24
 
24
- - New module + tests: `src/core/instarSettingsHooks.ts`, `tests/unit/instar-settings-hooks.test.ts` (16), `tests/unit/PostUpdateMigrator-pretooluse-parity.test.ts` (3, real migrateSettings).
25
- - Spec: `docs/specs/EXISTING-AGENT-PRETOOLUSE-HOOK-PARITY-SPEC.md` (approved)
26
- - ELI16: `docs/specs/EXISTING-AGENT-PRETOOLUSE-HOOK-PARITY-SPEC.eli16.md`
27
- - Side-effects review: `upgrades/side-effects/existing-agent-pretooluse-hook-parity.md`
28
- - Context: Task 3 of the 2026-05-27 silent-stalls postmortem.
25
+ - New module: `src/lifeline/TelegramPollOwnerLease.ts` (atomic write, fail-OPEN read, SHA-256 tokenHash).
26
+ - Wired: `src/lifeline/TelegramLifeline.ts` writes the lease on each successful poll; `src/commands/server.ts` checks it before `telegram.start()`.
27
+ - Tests: `tests/unit/TelegramPollOwnerLease.test.ts` (16 — both sides of every boundary + security contract) + `tests/unit/poll-owner-lease-wiring.test.ts` (4 — no dead code).
28
+ - Spec: `docs/specs/SELF-PROPAGATION-HARNESS-SPEC.md` (approved). Side-effects: `upgrades/side-effects/self-propagation-poll-owner-lease.md`.
@@ -0,0 +1,50 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ Two related groundwork increments land together:
8
+
9
+ Self-Propagation, Part 2 v1 — the **`/test-as-self` skill**: a deterministic post-deploy verifier for a throwaway agent home. Reads the Part 1 poll-ownership lease (present / fresh / well-formed / tokenHash-only security check), greps the server log for the Part 1 demote line (proves Part 1 actually fired in the live deploy — not just shipped), and tails the server + lifeline logs for the actual crash signatures (heap-OOM / `CheckIneffectiveMarkCompact` / Abort trap / libc++abi / SIGABRT — the signatures the 2026-05-27 mmtest diagnosis had wrong). Emits a single JSON report; exit 0 = all PASS; 1 = fail or crash detected.
10
+
11
+ Includes a runbook (`SKILL.md`) for the tight deploy recipe and an explicit list of what v1 does NOT do (auto-mint bot, full Playwright Telegram round-trip, one-button command — those ship in Part 2.1 under the same approved spec). Pairs with Part 1 (the structural poll-ownership lease, shipping in #446).
12
+
13
+ ---
14
+
15
+ **Agent-to-agent Telegram comms primitive — receiver wiring lands.** The TelegramAdapter
16
+ now exposes a pre-dispatch hook (`setAgentMessageHook`) that the receiver side of the a2a
17
+ primitive can install. When set, the hook intercepts text messages BEFORE normal user
18
+ dispatch — if the message carries a valid `[a2a:from=… to=… role=… id=… corr=… ts=… v=1]`
19
+ marker, the hook routes it to the registered role-handler or drops it as an a2a security
20
+ event (every drop reason audited). When the hook returns "not handled" (no marker), the
21
+ existing user-message flow continues unchanged. Ships **dark** — no caller invokes
22
+ `setAgentMessageHook` yet; the mentor consumer wires it in the follow-up.
23
+
24
+ The hook composer in `installAgentMessageHook.ts` ties together: the routing decision
25
+ (from PR 1), the audit ledger and processed-id store (from PR 3a), and a per-recipient
26
+ role-handler map. Spoof defense, per-source role acceptance, replay-window, and
27
+ idempotency are all enforced by the composed flow — Stage-B forensics gets a routed +
28
+ dropped audit trail it can replay.
29
+
30
+ ## What to Tell Your User
31
+
32
+ - A new test-as-self skill that runs a deterministic check on a throwaway test deploy — verifies Part 1's structural fix actually fired in practice, and captures any crash signature for you instead of you guessing from log forensics.
33
+ - Still plumbing for the agent-to-agent Telegram feature; nothing changes for your agent today. Your normal Telegram messages flow exactly as before — only text messages with a fully-formed agent tag at the top are even considered, and nothing reads them yet.
34
+
35
+ ## Summary of New Capabilities
36
+
37
+ | Capability | How to Use |
38
+ |-----------|-----------|
39
+ | `/test-as-self` runbook + verifier | Follow the recipe in `.claude/skills/test-as-self/SKILL.md`; run `node .claude/skills/test-as-self/scripts/verify.mjs --dir <test-agent-home>` |
40
+ | TelegramAdapter `setAgentMessageHook` | Internal — pre-dispatch hook fired on text messages before `onTopicMessage`; `{handled:true}` skips normal dispatch, `{handled:false}` falls through |
41
+ | `buildAgentMessageHook` (composer) | Internal — `src/messaging/installAgentMessageHook.ts`; composes decideRoute + ledger + processed-id store + role-handler map into the hook closure |
42
+
43
+ ## Evidence
44
+
45
+ - Bundled skill: `.claude/skills/test-as-self/SKILL.md` + `scripts/verify.mjs`.
46
+ - Installer: `src/commands/init.ts` (`installTestAsSelfSkill`, mirrors `installBuildSkill`; non-destructive, idempotent).
47
+ - Tests: `tests/unit/test-as-self-verify.test.ts` (20 — both sides of every check boundary; security check rejects raw-token in file).
48
+ - Spec: `docs/specs/SELF-PROPAGATION-HARNESS-SPEC.md` (approved). Side-effects: `upgrades/side-effects/self-propagation-test-as-self-skill.md`.
49
+
50
+ For the a2a receiver hook: 7 new unit tests (43 a2a-related tests total, all green) — fall-through on non-marker (no audit flood); route → handler called + routed audit row + id marked processed; idempotency (re-delivered id dropped, handler not called a 2nd time); user-spoof defense (handler not called even when from/id match); unknown-role drop; handler-error doesn't crash dispatch + id stays marked; TelegramAdapter `setAgentMessageHook` accepts + clears. `tsc --noEmit` clean. The change to Echo's primary Telegram dispatch is gated behind `if (this.agentMessageHook && ...)` and nothing calls `setAgentMessageHook` yet — the user message flow is byte-for-byte unchanged in production until the mentor consumer lands.
@@ -0,0 +1,102 @@
1
+ # Side-Effects Review — a2a recipient hook + TelegramAdapter wiring (PR 3b)
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Fix 2b "Implementation surface"
4
+ items 3 + the recipient routing flow + Codey's round-2 anti-loop discipline. PR 3 part b.
5
+ **Change:** Wire the agent-to-agent Telegram comms receiver into the TelegramAdapter
6
+ dispatch pipeline as a **pre-dispatch hook** (not a wrapper around an external handler).
7
+ **Files:** `src/messaging/TelegramAdapter.ts` (additive: new field + setter + insertion
8
+ point in the text-dispatch path), `src/messaging/installAgentMessageHook.ts` (new
9
+ production binding that composes decideRoute + ledger + processed-id store + role-handler
10
+ map into the hook closure), `tests/unit/messaging/installAgentMessageHook.test.ts` (new —
11
+ end-to-end hook closure tests + adapter wiring-integrity).
12
+
13
+ ## Design correction (honesty up-front)
14
+
15
+ The spec's PR-3b text described "wrap at server.ts:~3038." Reading the code revealed
16
+ that's the TelegramAdapter *construction* line — there's no `telegram.onMessage(handler)`
17
+ registration there or anywhere in the main dispatch (the only caller of
18
+ `telegram.onMessage(...)` codebase-wide is `TelegramConfirmationTransport.ts:109`, a small
19
+ provider). Telegram message handling is internal to TelegramAdapter — dispatch happens
20
+ inside `poll()`'s per-update processing. The cleaner integration was a dedicated
21
+ `agentMessageHook` field that the adapter calls **before** the existing
22
+ `onTopicMessage`/`this.handler` dispatch in the text path (other message types can't
23
+ carry the `[a2a:…]` marker, so they bypass entirely). One insertion point, minimally
24
+ invasive.
25
+
26
+ ## What changed
27
+
28
+ 1. **TelegramAdapter** (`src/messaging/TelegramAdapter.ts`):
29
+ - New exported types: `AgentMessageHookInput`, `AgentMessageHook`.
30
+ - New private field `agentMessageHook?: AgentMessageHook` + public setter
31
+ `setAgentMessageHook(hook | undefined)`.
32
+ - In the text-dispatch path (after the Prompt Gate, before `onTopicMessage` —
33
+ i.e. AFTER auth + sentinel + prompt-gate, BEFORE normal user routing), the adapter
34
+ calls the hook if set. On `{handled:true}` → return (skip normal dispatch). On
35
+ `{handled:false}` → continue. On hook error → log + fall through (a broken hook
36
+ must NOT freeze the message pipeline).
37
+ 2. **installAgentMessageHook** (new — production hook composer):
38
+ - Builds an `AgentMessageHook` closure from injected `{config, ledger, processedIds,
39
+ roleHandlers}`.
40
+ - Calls `decideRoute(ctx, config, {isProcessed, knownRole})`.
41
+ - On `fall-through` → returns `{handled:false}` + writes NO row (user messages don't
42
+ flood the audit).
43
+ - On `drop` → writes a `ReceiveAuditRow` with the drop reason + returns `{handled:true}`.
44
+ - On `route` → marks the id processed **before** invoking the handler (so a handler
45
+ crash mid-process still dedups the retry — at-least-once delivery / exactly-once
46
+ attempted processing), writes a `decision:'routed'` row, then awaits the role
47
+ handler. A handler error is logged but does not unmark the id (retrying would just
48
+ re-fail; Stage-B can see the routed row + the absent downstream effect).
49
+
50
+ ## The seven questions
51
+
52
+ 1. **Over-block.** The hook only fires on TEXT messages with the field set + falls through
53
+ on `no-marker`. Other message types bypass entirely. The dispatch order (after
54
+ sentinel + prompt-gate) preserves priority for emergency-stop and active prompt-gate
55
+ collection.
56
+ 2. **Under-block.** The hook NEVER falls through on a marker-shaped message — it always
57
+ routes or drops (security event). The user-spoof defense (round-2 adversarial F1) is
58
+ inherited from `decideRoute`. Idempotency on receive: `processedIds.markProcessed` runs
59
+ before the handler so a retry can't double-invoke.
60
+ 3. **Level-of-abstraction fit.** Hook field on the adapter; composer module owns the
61
+ policy; pure routing logic stays in `decideRoute` (PR 1). Three layers, single
62
+ responsibility each, fully injectable for tests.
63
+ 4. **Signal vs authority.** `decideRoute` is the routing authority (unchanged from PR 1);
64
+ the hook is its actuator + audit writer + idempotency boundary.
65
+ 5. **Interactions — THE risk, and how it's bounded.** This touches Echo's primary message
66
+ dispatch in TelegramAdapter (the channel to the user). The change is dark-by-default:
67
+ the hook field starts undefined → the new code path is `if (this.agentMessageHook && ...)`
68
+ which is a no-op until something calls `setAgentMessageHook`. No production wiring
69
+ does that yet — the mentor consumer (PR 3c) will. Hook errors fall through to normal
70
+ flow (log + continue), so even a broken hook can't freeze user messages. The existing
71
+ Telegram suites (71 + 25 + 8 + 3 + 7 = 114 tests) pass unchanged.
72
+ 6. **External surfaces.** None new in this PR. The wiring module exports
73
+ `buildAgentMessageHook` for PR 3c (or any future consumer) to call.
74
+ 7. **Rollback cost.** Trivial — revert removes the additive field, setter, hook composer.
75
+ Dispatch loop returns to identical-to-pre-PR behavior (no field set → no code path
76
+ touched).
77
+
78
+ ## Testing
79
+
80
+ 7 new unit tests in `installAgentMessageHook.test.ts`, all green:
81
+ - **fall-through**: non-marker user message → `{handled:false}` + NO audit row (don't
82
+ flood the ledger with every user message).
83
+ - **route**: valid marker + registered role-handler → handler called with the parsed
84
+ message + topic id; `decision:'routed'` audit row written; id marked processed.
85
+ - **idempotency**: re-delivered marker (same id) → dropped with reason
86
+ `agent-marker-duplicate`; handler NOT called a second time.
87
+ - **spoof defense**: human-typed marker (`senderIsBot:false`, no `sender_chat`) → dropped
88
+ with `agent-marker-spoofed-by-user`; handler NOT called.
89
+ - **unknown-role**: marker with no registered handler → dropped with
90
+ `agent-marker-unknown-role`; never falls through to user flow.
91
+ - **handler error**: a role-handler that throws → logged, hook returns `{handled:true}`,
92
+ id stays marked (no infinite retry on a broken handler).
93
+ - **TelegramAdapter wiring-integrity**: `setAgentMessageHook` accepts a hook + sets the
94
+ field; setting `undefined` clears it.
95
+
96
+ Plus the 36 existing a2a tests (PRs 1, 2a, 2b, 3a) — 43 a2a-related tests in total, all
97
+ green. `tsc --noEmit` clean.
98
+
99
+ ## Migration parity
100
+
101
+ None — additive: no caller invokes `setAgentMessageHook` yet (PR 3c will). No config
102
+ consumed, no routes, no agent-installed file changes.
@@ -0,0 +1,43 @@
1
+ # Side-Effects Review — Self-Propagation, Part 1 (Poll-Ownership Lease)
2
+
3
+ **Spec:** docs/specs/SELF-PROPAGATION-HARNESS-SPEC.md (approved: true) — Part 1 of two. Part 2 (`instar test-as-self` harness) ships as a separate PR under the same approved spec.
4
+
5
+ Closes the Telegram 409 dual-poll **class** structurally. Telegram allows exactly one long-poller per bot token. instar has two potential pollers — the lifeline (canonical, forwards to the server) and the server's `TelegramAdapter` (`telegram.start()`). The server only enters send-only when started with `--no-telegram` or on a standby machine; nothing structurally detects that a lifeline already owns the slot. Start the server without `--no-telegram` while a lifeline polls the same token → guaranteed 409 (the 2026-05-27 live-test failure mode). This was operator discipline, not structure.
6
+
7
+ ## What changed
8
+ - `src/lifeline/TelegramPollOwnerLease.ts` — NEW. `tokenHash(botToken)` (SHA-256 hex; security contract: NEVER stores the raw token), `writeLease(stateDir, token, pid, now?)` (atomic tmp+rename + best-effort never-throws), `readLease(stateDir, now?, staleMs?)` (fail-OPEN at every error path — missing/unparseable/wrong-shape/wrong-version/stale all return null), `lifelineOwnsPoll(stateDir, token, now?, staleMs?)` (the server's one-question API). Lease lives at `state/telegram-poll-owner.json`. Default staleness 90 s (comfortably > 2× Telegram long-poll timeout + backoff; clears within ~minute when the lifeline genuinely dies).
9
+ - `src/lifeline/TelegramLifeline.ts` — calls `writePollOwnerLease(stateDir, token, pid)` right after the successful-poll reset block (after `consecutive409s = 0`). Refreshes the heartbeat on every successful tick. Best-effort + non-throwing — a lease-write hiccup never disturbs the polling loop.
10
+ - `src/commands/server.ts` — before the send-only vs full-poll branch, computes `lifelineOwnsPolling = lifelineOwnsTelegramPoll(stateDir, token)`. The send-only branch now ALSO triggers when `lifelineOwnsPolling` is true; the full-poll branch is gated on `!lifelineOwnsPolling`. Send-only log now distinguishes "lifeline owns polling (lease detected)" so operators see the structural decision in action.
11
+
12
+ ## Tests
13
+ - `tests/unit/TelegramPollOwnerLease.test.ts` (16) — `tokenHash` security contract (never contains the raw token) + determinism + uniqueness; round-trip write/read; on-disk file never contains the raw token; overwrite-refreshes; ALL of the fail-OPEN paths (missing, unparseable, wrong-shape, unknown-version, stale); the lifelineOwnsPoll decision matrix — both sides of every boundary (live+match=TRUE; live+mismatch=FALSE; stale=FALSE; missing=FALSE; corrupted=FALSE).
14
+ - `tests/unit/poll-owner-lease-wiring.test.ts` (4) — lifeline import + call in the success branch; server import + the `lifelineOwnsPolling` computation; the send-only branch includes the new term; the full-poll branch is gated on `!lifelineOwnsPolling`; the server NEVER writes the lease (single-writer = lifeline only); the lease path is stable. Guards the PR#334 dead-code failure mode.
15
+
16
+ ## Signal vs authority
17
+ - The lease is a SIGNAL (the lifeline declares "I'm polling"). The server holds the AUTHORITY to act on it (auto-demote). The lifeline never tries to demote the server itself.
18
+ - Fail-OPEN is the safety polarity: ANY read miss, any malformed lease, ANY mismatched tokenHash, ANY stale heartbeat → the server polls as today. The lease can only ever CAUSE silence; we err on the side of NEVER silencing a fine agent.
19
+
20
+ ## Security
21
+ - The bot token is the secret. The lease stores its SHA-256 hash, never the raw value. Verified by a unit test against both the in-memory return AND the on-disk file. A read of `state/telegram-poll-owner.json` reveals only `{pid, tokenHash, heartbeatTs, v}`.
22
+ - File mode follows the stateDir's existing convention (no special mode set; atomic via tmp+rename so a partial write is never observable).
23
+
24
+ ## Over/under-demote
25
+ - OVER (server falsely demotes to send-only): mitigated by tokenHash match (a lifeline polling a DIFFERENT token doesn't trigger demote) + 90 s staleness. Worst case: a still-live lifeline + same-token + same-machine → exactly the scenario we WANT demote in.
26
+ - UNDER (server fails to demote when it should): the lease must be present + fresh + token-match. Race window at lifeline startup before the first successful poll (lease not yet written) — acceptable: the existing 409-backoff handling absorbs the brief window, and within one poll tick the lease is up.
27
+
28
+ ## Near-silent compliance
29
+ N/A — no new user-facing notifications. The server's send-only log line gains one descriptor ("lease detected").
30
+
31
+ ## Migration parity
32
+ - Server + lifeline source ship in the same version → no agent-installed-file change, no config change. The new behavior takes effect on first server restart after update (the existing lifeline writes the lease; the new server reads it). Existing setups without a lifeline poller see ZERO behavioral change (fail-OPEN preserves current full-poll path).
33
+
34
+ ## Rollback
35
+ Revert the 3 files (one new module + two callsite edits) + the 2 tests. Worst case: agents return to today's "operator discipline" model (start with `--no-telegram` to avoid the 409). No data migration concern (the lease file is stateless ephemera — safe to leave behind or delete).
36
+
37
+ ## Tests / verification
38
+ - Tier 1 unit: TelegramPollOwnerLease + wiring-integrity (above; 20 tests green).
39
+ - Tier 3 live: test-as-self before merge — start the lifeline + start the server on the same machine WITHOUT `--no-telegram` and confirm the server logs "lifeline owns polling (lease detected)" and Telegram returns no 409. (This is what Part 2 — the harness — automates.)
40
+
41
+ ## NOT in this PR (tracked)
42
+ - Part 2: the `instar test-as-self` CLI command + skill (a much larger, hardware-touching deliverable).
43
+ - Live mmtest OOM crash reproduction (only possible under the harness in Part 2).
@@ -0,0 +1,42 @@
1
+ # Side-Effects Review — Self-Propagation, Part 2 v1 (/test-as-self skill)
2
+
3
+ **Spec:** docs/specs/SELF-PROPAGATION-HARNESS-SPEC.md (approved: true) — Part 2 v1 of two. Part 1 (the structural poll-ownership lease) ships separately (PR #446) under the same approved spec. Pairs with it: Part 1 is the structural fix; Part 2 is the verifier that proves Part 1 actually fired in a live deploy + captures crash signatures deterministically.
4
+
5
+ ## What changed
6
+ - `.claude/skills/test-as-self/SKILL.md` (new) — runbook: when to use, the tight deploy recipe (pick throwaway dir → init → optional bot config → lifeline start → server start → verify → teardown), and explicit deferrals for v1 (no auto-mint, no full Playwright round-trip, no one-button command yet — those land in Part 2.1).
7
+ - `.claude/skills/test-as-self/scripts/verify.mjs` (new) — deterministic post-deploy verifier (~190 lines, no deps): reads the Part 1 lease file (present / fresh / well-formed / tokenHashOnly security check), greps `logs/server.log` for the Part 1 "send-only mode (lifeline owns polling)" demote line, tails server+lifeline logs for the actual crash signatures (FATAL ERROR / heap out of memory / CheckIneffectiveMarkCompact / Abort trap / SIGABRT / libc++abi / Segmentation fault). Single-JSON-report on stdout; exit 0 = all PASS; 1 = fail / crash. Importable for unit testing.
8
+ - `src/commands/init.ts` — new `installTestAsSelfSkill(skillsDir)` mirroring `installBuildSkill`: prefers bundled `.claude/skills/test-as-self/*`, falls back to a pointer SKILL.md if the bundle is missing. Wired into `installBuiltinSkills` between `installBuildSkill` and `installAutonomousSkill`. Idempotent (only installs missing files; preserves operator customizations).
9
+ - `tests/unit/test-as-self-verify.test.ts` (20) — both sides of every check boundary: lease present/missing/unparseable/wrong-shape, fresh/stale, tokenHash-only (security: rejects raw-token in file; rejects non-64-hex), server demote present/missing/no-log, crash signatures detected per pattern, none on clean logs, line preserved for forensics. Aggregate report: all-pass; any fail; crash-with-other-passes.
10
+
11
+ ## Signal vs authority
12
+ The verifier is **diagnostic**, not authoritative — it never modifies the test agent, never restarts processes, never blocks anything. It just reads files and emits a report. Authority to act on the report (e.g. "Part 1 didn't fire, investigate") sits with the operator/agent, where it should.
13
+
14
+ ## Security
15
+ - The verifier asserts the lease file contains a 64-hex `tokenHash` AND grep-checks for a raw-bot-token shape (`/\b\d{6,}:[A-Za-z0-9_-]{20,}\b/`). If either fails, `lease.tokenHashOnly` is FAIL with detail "CRITICAL: file appears to contain a raw bot-token shape". This catches a regression where someone accidentally writes the raw token to the lease file.
16
+ - The verifier never reads, requires, or echoes a bot token itself.
17
+
18
+ ## Idempotency / migration parity
19
+ - New skill → no migration needed per CLAUDE.md (`installBuiltinSkills` is called from `refreshHooksAndSettings` on every update; only writes missing files; preserves customizations). Existing agents receive the skill on next update automatically.
20
+ - Bundled SKILL.md + scripts/verify.mjs ship with the npm package via `.claude/skills/test-as-self/`. The fallback path in `installTestAsSelfSkill` covers the (rare) case where the bundle is missing — installs a minimal pointer SKILL.md so the skill exists at the canonical slug.
21
+
22
+ ## Over/under
23
+ - OVER: the runbook is operator-followed (not one-button), so it depends on the operator pasting the right paths/tokens. Acceptable for v1 — the operator-friction trade is offset by deterministic verification.
24
+ - UNDER: v1 does NOT auto-test a full Telegram round-trip (lifeline polls; verifier confirms the lease; but no probe message is sent and asserted). The Part 1 lease is verified structurally + at the live-log level — that's the high-leverage half. Round-trip + auto-mint + OOM-reproduction-script ship in Part 2.1.
25
+
26
+ ## Interactions
27
+ - Pairs with Part 1: the verifier's `server.demoteLogged` check fires the "lifeline owns polling" pattern Part 1 logs. Without Part 1 deployed, that check FAILS — which is correctly diagnostic (says "Part 1 may not be wired"), not a false positive.
28
+ - No interaction with the autonomous skill, /build skill, or any other built-in.
29
+
30
+ ## Rollback
31
+ Delete the skill dir (`.claude/skills/test-as-self/`), revert the init.ts hook + the new function (3 chunks), revert the test file. Existing agents that already installed the skill keep their copy (the skill is non-destructive — operator can simply `rm -rf .claude/skills/test-as-self/` per their deploy).
32
+
33
+ ## Tests / verification
34
+ - Tier 1 unit: 20 tests, both sides of every check boundary; verified imports the REAL verify.mjs (no copy).
35
+ - Runtime smoke (manual, performed during this build): wrote a fake lease + demote line + clean logs into a temp dir; `node verify.mjs --dir TMP` returned exit 0 with all checks PASS.
36
+ - Tier 3 live: test-as-self before merge — run the runbook end-to-end on a real throwaway agent home, exercise the verifier against the actual live deploy with Part 1 wired, confirm all checks PASS.
37
+
38
+ ## NOT in this PR (tracked, Part 2.1)
39
+ - One-button `instar test-as-self` CLI command.
40
+ - Auto-mint bot via Secret Drop integration.
41
+ - Full Telegram round-trip via the Playwright profile (send probe, assert reply).
42
+ - OOM-reproduction script with controlled inputs.