instar 1.2.3 → 1.2.5

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,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * secret-drop-retrieve.mjs — fetch a Secret Drop submission and stream the
4
+ * requested field value to stdout WITHOUT ever printing the response body.
5
+ * All informational output goes to stderr and is limited to field NAMES,
6
+ * lengths, and HTTP status — never values.
7
+ *
8
+ * Hardened replacement for the ad-hoc `curl + jq` / `curl + python` pattern
9
+ * that historically leaked credentials in plaintext into the Bash tool
10
+ * transcript. The lesson: when probing an unknown JSON shape, NEVER fall
11
+ * back to `console.log(JSON.stringify(body))` — that's how plaintext
12
+ * credentials end up in shell history, session transcripts, and downstream
13
+ * LLM context.
14
+ *
15
+ * Usage:
16
+ * node .instar/scripts/secret-drop-retrieve.mjs <token> <field-name>
17
+ * → prints the field value to stdout (single line, no trailing newline)
18
+ * → suitable for piping: `... password | gh secret set X`
19
+ *
20
+ * node .instar/scripts/secret-drop-retrieve.mjs <token> --names
21
+ * → prints field names + lengths to stderr; nothing to stdout
22
+ * → use to discover what fields the submission contains
23
+ *
24
+ * node .instar/scripts/secret-drop-retrieve.mjs <token> <field> --consume
25
+ * → opt into one-shot semantics (the submission is removed after read).
26
+ * Default is peek (non-destructive) per the 2026-05-20 hardening.
27
+ *
28
+ * Config:
29
+ * Reads `authToken` and `port` from `.instar/config.json` relative to cwd.
30
+ * `INSTAR_PORT` env var overrides the config port.
31
+ *
32
+ * Exit codes:
33
+ * 0 — value or names printed successfully
34
+ * 1 — HTTP error, missing field, or unexpected response shape
35
+ * 2 — usage error (missing args, cannot read config)
36
+ */
37
+
38
+ import * as fs from 'node:fs';
39
+
40
+ const args = process.argv.slice(2);
41
+ const token = args[0];
42
+ const mode = args[1];
43
+ const consume = args.includes('--consume');
44
+
45
+ if (!token || !mode || mode === '--consume') {
46
+ process.stderr.write('usage: secret-drop-retrieve.mjs <token> <field-name|--names> [--consume]\n');
47
+ process.exit(2);
48
+ }
49
+
50
+ let authToken;
51
+ let port;
52
+ try {
53
+ const config = JSON.parse(fs.readFileSync('.instar/config.json', 'utf-8'));
54
+ authToken = config.authToken;
55
+ port = config.port;
56
+ } catch (e) {
57
+ process.stderr.write('cannot read .instar/config.json: ' + e.message + '\n');
58
+ process.exit(2);
59
+ }
60
+
61
+ // Port resolution: env > config. Mirrors the telegram-reply.sh precedence
62
+ // so a single explicit INSTAR_PORT override flows through every agent script.
63
+ const resolvedPort = process.env.INSTAR_PORT || port;
64
+ if (!resolvedPort) {
65
+ process.stderr.write('cannot resolve port: no INSTAR_PORT env and no port in .instar/config.json\n');
66
+ process.exit(2);
67
+ }
68
+
69
+ const url = `http://localhost:${resolvedPort}/secrets/retrieve/${token}${consume ? '?consume=true' : ''}`;
70
+ const res = await fetch(url, {
71
+ method: 'POST',
72
+ headers: { 'Authorization': `Bearer ${authToken}` },
73
+ });
74
+
75
+ if (!res.ok) {
76
+ // Surface ONLY status + the structured error message field, never the body.
77
+ let errMsg = '(no error message)';
78
+ try {
79
+ const body = await res.json();
80
+ if (typeof body?.error === 'string') errMsg = body.error;
81
+ } catch {
82
+ // Don't fall back to printing raw response body — that's the leak we're
83
+ // explicitly hardening against. Just report the HTTP status.
84
+ }
85
+ process.stderr.write(`HTTP ${res.status}: ${errMsg}\n`);
86
+ process.exit(1);
87
+ }
88
+
89
+ const body = await res.json();
90
+
91
+ // Locate the values bag. The server's known shape is body.values; defend
92
+ // against future shape changes by checking the two most-likely nestings,
93
+ // but NEVER print the body itself.
94
+ const values = body?.values
95
+ ?? body?.submission?.values
96
+ ?? body?.fields
97
+ ?? null;
98
+
99
+ if (!values || typeof values !== 'object') {
100
+ process.stderr.write('retrieve: response did not contain a values bag (keys at top: '
101
+ + Object.keys(body || {}).join(', ') + ')\n');
102
+ process.exit(1);
103
+ }
104
+
105
+ if (mode === '--names') {
106
+ // Field-name listing for debugging — names + lengths only, never values.
107
+ for (const k of Object.keys(values)) {
108
+ const v = values[k];
109
+ const len = typeof v === 'string' ? v.length : -1;
110
+ process.stderr.write(` ${k} (length ${len})\n`);
111
+ }
112
+ process.exit(0);
113
+ }
114
+
115
+ const v = values[mode];
116
+ if (typeof v !== 'string') {
117
+ process.stderr.write(`retrieve: field '${mode}' not found (available: `
118
+ + Object.keys(values).join(', ') + ')\n');
119
+ process.exit(1);
120
+ }
121
+
122
+ // Stream the value to stdout, no trailing newline so it pipes cleanly into
123
+ // `gh secret set` or `bw unlock --raw`. process.stdout.write (not console.log)
124
+ // — no extra newline, no trailing whitespace to confuse downstream consumers.
125
+ process.stdout.write(v);
@@ -0,0 +1,69 @@
1
+ # Upgrade Guide — v1.2.4 (worktree detector → Telegram alerts)
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ **Layer 4 detector now surfaces misplaced worktrees through the Telegram
8
+ attention queue, not just the JSONL fallback.** The agent worktree
9
+ convention's lifeline detector — added in v1.1.0 (PR #278) — has been
10
+ running on every agent server boot and dutifully appending detected
11
+ violations to `<stateDir>/audit/worktree-detector.jsonl`, but the
12
+ emissions never reached the operator's Telegram. That was a known
13
+ follow-up tracked in v1.1.0's Deferred list.
14
+
15
+ This release closes that gap by moving the detector invocation from
16
+ its old position before `TelegramAdapter` was initialized to a new
17
+ position immediately after both TelegramAdapter setup blocks (the
18
+ send-only and full-mode paths in `src/commands/server.ts`). When the
19
+ agent has Telegram configured, the detector now passes
20
+ `telegram.createAttentionItem` as its emit-attention callback;
21
+ otherwise it falls back to the JSONL append exactly as before.
22
+
23
+ The detector remains **signal-only** — every emission goes through
24
+ the existing `AttentionItem.id` dedupe contract, the JSONL fallback
25
+ keeps its O_NOFOLLOW + fstat + 24h rolling-window dedupe, and the
26
+ detector still never blocks, moves, or deletes anything. The only
27
+ behavioral change visible to the operator is: misplaced-worktree
28
+ alerts now arrive as Telegram attention topics on the next agent
29
+ restart, instead of being silently written to a log file the
30
+ operator was unlikely to discover.
31
+
32
+ ## Evidence
33
+
34
+ Reproduction prior: an agent running v1.1.0–v1.2.3 with one or more
35
+ worktrees of the shared instar repo outside any registered agent's
36
+ `.worktrees/` safe area would, on every server boot, emit zero
37
+ Telegram alerts and silently append JSONL lines under
38
+ `<stateDir>/audit/worktree-detector.jsonl`. Verified by inspecting
39
+ the server-start log on echo's machine after each release —
40
+ "Worktree detector: N misplaced worktree(s) flagged" appeared in
41
+ console output but no attention topic was ever created.
42
+
43
+ After this PR: the same configuration produces the same JSONL line
44
+ PLUS a Telegram attention topic per misplaced worktree, deduped by
45
+ the same `worktree-misplaced:sha256(path)` id the JSONL already
46
+ used. The console line gains a `via Telegram` / `via JSONL fallback`
47
+ suffix so the operator can tell which channel ran. Verified by
48
+ three new wireup tests in
49
+ `tests/unit/AgentWorktreeDetector-attention-wireup.test.ts`:
50
+ shape-compatibility between detector output and adapter input,
51
+ async-callback await safety (the detector awaits the emit so a
52
+ Telegram API roundtrip latency is honored), and JSONL fallback
53
+ preservation when `emitAttention` is undefined.
54
+
55
+ ## What to Tell Your User
56
+
57
+ - "Your agents now actually tell you when they spot a worktree in the unsafe location. Before this release, the warning was being written to a log file in the background — the agent saw it, but you never did. Now it shows up as an attention topic in Telegram the next time the agent starts up. Same dedupe rules as before, so you won't get pinged twice for the same one within a day."
58
+
59
+ ## Summary of New Capabilities
60
+
61
+ | Capability | How to Use |
62
+ |-----------|-----------|
63
+ | Worktree detector emits to Telegram | Automatic on every agent server boot. When the agent has Telegram configured, misplaced-worktree detections create attention topics; otherwise the JSONL fallback at `<stateDir>/audit/worktree-detector.jsonl` continues as before. Dedupe is path-hash-based via the same `worktree-misplaced:sha256(path)` id the JSONL has always used. |
64
+ | Console reports the channel | The startup log line now reads `Worktree detector: N misplaced worktree(s) flagged via Telegram` (or `via JSONL fallback`) so the operator can confirm which channel ran without grepping the audit file. |
65
+
66
+ ## Deferred (Tracked Follow-ups)
67
+
68
+ - The 24h rolling-window JSONL dedupe runs only on the fallback path. The Telegram path leans on `AttentionItem.id` collision in TelegramAdapter's existing `attentionItems` Map — that lookup is per-process and is reset on agent restart, so two boots within 24h on a Telegram-configured agent could theoretically emit twice for the same misplaced worktree. In practice agent restarts are infrequent enough that this isn't a real source of noise; if it ever becomes one, the fix is to consult the JSONL dedupe state from the Telegram path too.
69
+ - The detector's first-run burst on machines with many pre-existing misplaced worktrees (echo's machine has ~30 from before the convention shipped) is now Telegram-visible for the first time. Operators may want to bulk-acknowledge them with `/done` rather than processing each individually.
@@ -0,0 +1,56 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: minor -->
4
+ <!-- minor = new agent-facing capability without breaking changes -->
5
+
6
+ ## What Changed
7
+
8
+ **feat(secret-drop): ship the hardened retrieve helper as a template — and rewrite every line of agent guidance that previously taught the unsafe pattern.**
9
+
10
+ The 2026-05-20 incident exposed a credential-leak class: when an agent retrieved a Secret Drop submission via a raw `curl /secrets/retrieve/TOKEN`, the response body — including the secret value — landed in the Bash tool transcript. The fix existed as a one-off `secret-drop-retrieve.mjs` on a single agent's machine, but no other agent had it, and every line of agent-facing guidance (CLAUDE.md template, the spawned-session system message, the stuck-consumer retry message) still recommended the unsafe `curl` pattern.
11
+
12
+ This release closes the gap on six interlocking surfaces:
13
+
14
+ 1. **Ship the hardened helper as a template.** `src/templates/scripts/secret-drop-retrieve.mjs` streams the requested field value to stdout, prints field NAMES + lengths to stderr, and structurally refuses to print the response body. New agents get it via `installSecretDropRetrieve` (called from three `init.ts` paths next to `installSerendipityCapture`).
15
+
16
+ 2. **Migrate existing agents.** `PostUpdateMigrator.migrateScripts` always-overwrites `.instar/scripts/secret-drop-retrieve.mjs` on every update (same pattern as `convergence-check.sh`). Idempotent — running the migrator twice produces identical content.
17
+
18
+ 3. **Rewrite the spawn message.** `src/server/routes.ts` no longer instructs the agent to `curl ... /secrets/retrieve/TOKEN` when a Secret Drop arrives. The new message points at `node .instar/scripts/secret-drop-retrieve.mjs TOKEN <field-name>` with an explicit warning against the raw curl pattern.
19
+
20
+ 4. **Rewrite the stuck-consumer retry message.** Same hardened command form, same explicit warning.
21
+
22
+ 5. **Rewrite the CLAUDE.md template.** `src/scaffold/templates.ts` documents the hardened command as the required retrieval pattern, with a one-line explanation of the leak class.
23
+
24
+ 6. **Rewrite existing CLAUDE.md files.** `PostUpdateMigrator.migrateClaudeMd` detects the legacy `curl /secrets/retrieve/TOKEN` line (port-tolerant — matches any local port literal) and replaces it with the hardened guidance. Idempotent: a CLAUDE.md that already documents `secret-drop-retrieve.mjs` is left alone.
25
+
26
+ 7. **Update the /capabilities `secrets.retrievalHint`.** The hint now gives the actual command form so an agent reading `/capabilities` learns the safe pattern directly from the discovery surface.
27
+
28
+ ## What to Tell Your User
29
+
30
+ No user-visible behavior change. Agents will start using the hardened retrieve helper automatically — the next time they receive a Secret Drop submission, the spawned session reads the hardened command form from the system message, not the unsafe curl. Existing agents pick up the helper script and the CLAUDE.md rewrite on the next instar update run.
31
+
32
+ If you were ever shown the unsafe curl-against-secrets-retrieve pattern in chat, that guidance has been retired across every surface; the hardened helper is the only documented path now.
33
+
34
+ ## Summary of New Capabilities
35
+
36
+ For the agent:
37
+
38
+ - `.instar/scripts/secret-drop-retrieve.mjs TOKEN field-name` — streams the value to stdout, never prints the body.
39
+ - `.instar/scripts/secret-drop-retrieve.mjs TOKEN --names` — discover field names + lengths.
40
+ - `.instar/scripts/secret-drop-retrieve.mjs TOKEN field-name --consume` — opt into one-shot semantics.
41
+
42
+ ## Evidence
43
+
44
+ The leak class was reproduced before the fix: running `curl -X POST -H "Authorization: Bearer $AUTH" http://localhost:4042/secrets/retrieve/<token>` against a known submission prints the full JSON `{"values":{"password":"actual-secret-here"}, ...}` to stdout, which lands in the Bash tool transcript.
45
+
46
+ After the fix, the hardened helper isolates the value path: `node .instar/scripts/secret-drop-retrieve.mjs <token> password` streams only `actual-secret-here` to stdout (no newline, pipeable), and prints nothing else. The `--names` mode prints only `password (length 16)` to stderr; stdout stays empty.
47
+
48
+ New tests:
49
+ - `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — 8 cases covering script install (presence, executable bit, content invariants, idempotency) + CLAUDE.md rewrite (legacy → hardened, idempotent, port-tolerant).
50
+ - The existing `tests/unit/capabilities-discoverability.test.ts` continues to pass with the updated `secrets.retrievalHint` string.
51
+
52
+ Spec: `docs/specs/secret-drop-hardened-retrieve-template.md`
53
+ ELI16: `docs/specs/secret-drop-hardened-retrieve-template.eli16.md`
54
+ Side-effects: `upgrades/side-effects/secret-drop-hardened-retrieve-template.md`
55
+
56
+ Origin: 2026-05-20 leak incident (topic 9984) + 2026-05-21 case-study audit (topic 11141, follow-up #1).
@@ -0,0 +1,123 @@
1
+ # Side-Effects Review — Secret Drop Hardened Retrieve Template
2
+
3
+ **Version / slug:** `secret-drop-hardened-retrieve-template`
4
+ **Date:** `2026-05-21`
5
+ **Author:** `Echo (instar-developing agent)`
6
+ **Second-pass reviewer:** `not required` (template install + documentation rewrite; no new gate/block authority)
7
+
8
+ ## Summary of the change
9
+
10
+ Ships the hardened `secret-drop-retrieve.mjs` as an `src/templates/scripts/` template; wires it into `init.ts` (for new agents) and `PostUpdateMigrator.migrateScripts` (for existing agents); rewrites the `[secret-drop-received]` spawn message, the `[secret-drop-stuck]` retry message, the CLAUDE.md template, and the `/capabilities.secrets.retrievalHint` to instruct the hardened command form. Adds a `migrateClaudeMd` block that detects the legacy `curl /secrets/retrieve/TOKEN` line and rewrites it in place; idempotent and port-tolerant.
11
+
12
+ Files touched:
13
+
14
+ - `src/templates/scripts/secret-drop-retrieve.mjs` — new template.
15
+ - `src/commands/init.ts` — `installSecretDropRetrieve` + three call sites.
16
+ - `src/core/PostUpdateMigrator.ts` — `migrateScripts` block (always-overwrite); `migrateClaudeMd` block (port-tolerant rewrite).
17
+ - `src/server/routes.ts` — spawn message + stuck-consumer retry message + `/capabilities.secrets.retrievalHint`.
18
+ - `src/scaffold/templates.ts` — CLAUDE.md Secret Drop documentation block.
19
+ - `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — new tests (8 cases).
20
+ - `upgrades/NEXT.md` — release notes (minor bump).
21
+ - `docs/specs/secret-drop-hardened-retrieve-template.{md,eli16.md}` — spec + ELI16.
22
+
23
+ ## Decision-point inventory
24
+
25
+ - `[secret-drop-received]` spawn message — **modify** — guidance the agent reads to learn how to retrieve. Unsafe → hardened.
26
+ - `[secret-drop-stuck]` retry message — **modify** — same retrieval path on the retry branch.
27
+ - CLAUDE.md Secret Drop block (template + migrator) — **modify** — agent's static retrieval guidance.
28
+ - `/capabilities.secrets.retrievalHint` — **modify** — discovery-surface guidance.
29
+
30
+ No new gate/block surface introduced. The `/secrets/retrieve` endpoint behavior is unchanged.
31
+
32
+ ---
33
+
34
+ ## 1. Over-block
35
+
36
+ **What legitimate inputs does this change reject that it shouldn't?**
37
+
38
+ No block/allow surface — over-block not applicable. The migrator's CLAUDE.md rewrite is the closest thing to a block: it refuses to touch a CLAUDE.md that already documents `secret-drop-retrieve.mjs` (idempotent skip). That's the desired behavior — over-block would require the migrator to refuse legitimate rewrites, which it does not.
39
+
40
+ ---
41
+
42
+ ## 2. Under-block
43
+
44
+ **What failure modes does this still miss?**
45
+
46
+ The migrator's regex matches the exact legacy-line pattern. A CLAUDE.md that someone manually customized to use a different unsafe pattern (e.g., `wget` instead of `curl`, or a piped one-liner) would slip past the rewrite. Acceptable: the rewrite is the structural fix for the documented unsafe pattern; bespoke unsafe variants are out of scope and would be visible to anyone reading their own CLAUDE.md.
47
+
48
+ The hardened helper itself still reads `.instar/config.json` for the authToken. If the config file is missing, the script exits 2 with a clear stderr message; if the file is present but `authToken` is empty, the script does a request with an empty bearer and the server returns 401 — the script prints the 401 status (no body). Both paths are safe.
49
+
50
+ ---
51
+
52
+ ## 3. Level-of-abstraction fit
53
+
54
+ **Is this at the right layer?**
55
+
56
+ The change lives at the agent-facing instruction layer, which is the correct level. Three reasons:
57
+
58
+ 1. The leak is caused by the agent running a documented command verbatim. The fix is to make the documented command safe.
59
+ 2. Adding a server-side gate (e.g., refuse `/secrets/retrieve` without a hardened-client header) would create a brittle detector with block authority — violating the signal-vs-authority principle. The server stays permissive; the agent layer is hardened by making safe the only documented path.
60
+ 3. The hardened helper itself is a thin wrapper around `fetch`. It doesn't introduce new architecture; it constrains how the value flows out of the response.
61
+
62
+ ---
63
+
64
+ ## 4. Signal vs authority compliance
65
+
66
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
67
+
68
+ **Does this change hold blocking authority with brittle logic?**
69
+
70
+ - [x] No — this change has no block/allow surface at runtime.
71
+
72
+ The migrator's CLAUDE.md rewrite has commit-time block authority via the precommit hook, but only by virtue of being a code change; the migrator itself does not refuse anything at runtime. The hardened helper has no gate logic — it just streams a value or exits with an error code.
73
+
74
+ ---
75
+
76
+ ## 5. Interactions
77
+
78
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
79
+
80
+ - **Shadowing:** the `[secret-drop-received]` spawn message replaces the existing one verbatim (no append, no shadow). The existing 5-minute cleanup timer and the stuck-consumer event from PR #288 are unchanged — both rely on server-side state, not on the system-message content.
81
+ - **Double-fire:** the migrator runs each of `migrateScripts` and `migrateClaudeMd` once per `instar update` invocation. Neither path is recursive.
82
+ - **Races:** the migrator runs single-threaded against an agent that is not yet started; no concurrent writers to the files it touches.
83
+ - **Feedback loops:** none. The agent reading the new guidance produces no input back into the migrator.
84
+
85
+ The `PostUpdateMigrator.loadRelayTemplate` helper is reused (not duplicated) for loading the new template. Symmetric with `getTelegramReplyScript`.
86
+
87
+ ---
88
+
89
+ ## 6. External surfaces
90
+
91
+ **Does this change anything visible outside the immediate code path?**
92
+
93
+ - **Agents on the same machine:** yes — every existing agent picks up `.instar/scripts/secret-drop-retrieve.mjs` on the next update, and any agent whose CLAUDE.md still has the legacy retrieve line gets it rewritten in place. The behavioral change is that the agent learns a safer command form. The unsafe `/secrets/retrieve` endpoint continues to work unchanged for any caller (including custom integrations); only the guidance is updated.
94
+ - **Other agents on the install base:** same — Codex / Gemini agents that read CLAUDE.md / AGENTS.md / GEMINI.md get the rewrite via `migrateFrameworkShadowCapabilities` (existing migrator path that mirrors CLAUDE.md changes to non-Claude shadows).
95
+ - **External systems:** no external surface touched.
96
+ - **Persistent state:** none beyond the file install (which is by design).
97
+ - **Timing / runtime conditions:** the new helper makes one `fetch` call per invocation; same latency profile as the curl it replaces.
98
+
99
+ ---
100
+
101
+ ## 7. Rollback cost
102
+
103
+ **If this turns out wrong in production, what's the back-out?**
104
+
105
+ Pure code change. Revert the migrator + init + templates + spec/artifact files. The script previously installed by the migrator stays on disk for existing agents (the rollback doesn't delete). The CLAUDE.md rewrites previously applied to existing agents also stay — which is fine because the hardened guidance points at a script that still exists (the install path doesn't go away during a code-only rollback).
106
+
107
+ Realistic rollback: revert + patch release. Under 5 minutes. The only concern would be a downstream agent that started depending on a quirk of the hardened helper (e.g., expecting an exact stderr format). The script's output contract is documented in the file header; downstream tooling using it should pin against that, not against ephemeral implementation details.
108
+
109
+ ---
110
+
111
+ ## Conclusion
112
+
113
+ This is a low-risk hardening change focused on agent-facing guidance. It closes the structural side of the 2026-05-20 leak class: every documented path now teaches the safe pattern. The runtime change is a script install + a sequence of string rewrites; no new decision points introduced; signal-vs-authority preserved. No second-pass review required.
114
+
115
+ ---
116
+
117
+ ## Evidence pointers
118
+
119
+ - Spec: `docs/specs/secret-drop-hardened-retrieve-template.md`
120
+ - ELI16: `docs/specs/secret-drop-hardened-retrieve-template.eli16.md`
121
+ - Tests: `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` (8 cases, all green).
122
+ - Existing coverage that continues to pass with the updated guidance strings: `tests/unit/capabilities-discoverability.test.ts`, `tests/unit/secret-drop-hardening.test.ts`.
123
+ - Origin: 2026-05-20 leak incident (topic 9984); 2026-05-21 case-study audit (topic 11141, follow-up #1).
@@ -0,0 +1,194 @@
1
+ # Side-Effects Review — Layer 4 detector → AttentionQueue wireup
2
+
3
+ **Version / slug:** `worktree-detector-attention-wireup`
4
+ **Date:** `2026-05-21`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (see §"Phase 5 trigger check")
7
+
8
+ ## Summary of the change
9
+
10
+ The Layer 4 lifeline detector (from the agent worktree convention,
11
+ shipped in v1.1.0 via PR #278) was running on every agent server boot
12
+ and emitting to its JSONL fallback only — the Telegram AttentionQueue
13
+ wireup was deferred because, in `startServer()`, the detector was
14
+ invoked at line ~2208 while `TelegramAdapter` doesn't initialize until
15
+ line ~2810/2897. The deferred state is documented in the v1.1.0 NEXT.md
16
+ and in `upgrades/side-effects/agent-worktree-convention-layer-3-4.md`.
17
+
18
+ This PR closes the gap by moving the detector invocation to **after**
19
+ both `TelegramAdapter` setup blocks. When Telegram is configured, the
20
+ detector now passes `telegram.createAttentionItem` as its
21
+ `emitAttention` callback; when it isn't, the JSONL fallback fires
22
+ exactly as before. The detector remains signal-only — no blocking,
23
+ no moves, no deletes.
24
+
25
+ **Files touched:**
26
+ - `src/commands/server.ts` (+62 / -29 lines: removed the old early invocation, replaced with a one-paragraph stub comment; added the new conditional invocation after the full-mode Telegram block).
27
+ - `tests/unit/AgentWorktreeDetector-attention-wireup.test.ts` (new, 3 cases — shape-compat, async-await safety, JSONL-fallback preservation).
28
+ - `upgrades/NEXT.md` (new — fresh v1.2.4 release).
29
+ - `package.json` (1.2.3 → 1.2.4).
30
+
31
+ ## Decision-point inventory
32
+
33
+ - **Detector emission target selection** — *modify*. Was unconditional
34
+ JSONL; is now Telegram-when-configured-else-JSONL. The selection
35
+ rule is a pure presence check (`telegram ? emitToTelegram : undefined`),
36
+ no judgment. Both paths exist in the spec.
37
+ - **AttentionItem.id dedupe** — *pass-through*. The detector already
38
+ produces `worktree-misplaced:sha256(path)` ids; TelegramAdapter's
39
+ existing `attentionItems` Map collapses repeats. No new dedupe logic.
40
+ - **JSONL fallback rolling-window** — *unchanged*. Same 24h, same
41
+ O_NOFOLLOW + fstat owner/mode gate on the fallback file.
42
+
43
+ No new authorities introduced. No new detectors. Just a wiring change
44
+ that connects an existing signal producer (detector) to an existing
45
+ authority surface (AttentionQueue).
46
+
47
+ ---
48
+
49
+ ## 1. Over-block
50
+
51
+ No block/allow surface — over-block not applicable. The detector emits
52
+ attention items; AttentionQueue is the authority that decides what to
53
+ do with them. Neither side rejects the user/operator from anything.
54
+
55
+ ## 2. Under-block
56
+
57
+ No block/allow surface — under-block not applicable.
58
+
59
+ A related coverage gap worth naming (and tracked in NEXT.md Deferred):
60
+ the 24h JSONL dedupe is per-file. The Telegram path leans on
61
+ `AttentionItem.id` collision in TelegramAdapter's in-memory
62
+ `attentionItems` Map, which is reset on every agent restart. Two
63
+ agent restarts within 24h on a Telegram-configured agent could
64
+ theoretically re-emit for the same misplaced worktree path. In
65
+ practice, agent restarts are infrequent; if noise becomes a problem,
66
+ the fix is to consult the JSONL dedupe state from the Telegram path
67
+ too. Acceptable for v1.
68
+
69
+ ## 3. Level-of-abstraction fit
70
+
71
+ Right layer. The detector lives where every other lifecycle observer
72
+ lives (`src/core/`), invoked from the agent boot path
73
+ (`src/commands/server.ts`), feeding the existing AttentionQueue
74
+ surface in TelegramAdapter. The move from "before TelegramAdapter
75
+ init" to "after TelegramAdapter init" is the only change; everything
76
+ else reuses primitives that have been in production since v1.1.0.
77
+
78
+ A smarter gate (the AttentionQueue) already exists; this PR feeds it
79
+ the signal it was always intended to consume.
80
+
81
+ ## 4. Signal vs authority compliance
82
+
83
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
84
+
85
+ **Does this change hold blocking authority with brittle logic?**
86
+
87
+ - [x] **No — this change produces a signal consumed by an existing smart gate.** The detector remains signal-only. AttentionQueue (TelegramAdapter's `attentionItems` registry) is the authority that owns dedupe, topic creation, and operator routing. The detector's only authoritative decision is the path-based "is this worktree under a registered safe root" filter, which is hard-invariant validation (carve-out in `docs/signal-vs-authority.md`).
88
+
89
+ The detector's invariant that the audit ledger is **never** consumed
90
+ as an allowlist remains in force — the path-based rule is the
91
+ detector's only authority surface, and that rule is restated in
92
+ `src/core/AgentWorktreeDetector.ts`'s file-level comment.
93
+
94
+ ## 5. Interactions
95
+
96
+ - **Shadowing:** the detector now runs AFTER `telegram.start()` and
97
+ after the Telegram routing/callback wireup (`wireTelegramRouting`
98
+ + `wireTelegramCallbacks`). It still runs BEFORE the AgentServer
99
+ HTTP listener (line 7037), so no inbound traffic is missed during
100
+ the detection sweep.
101
+ - **Double-fire:** dedupe is owned by AttentionQueue (`item.id`
102
+ collision) for the Telegram path and by the JSONL rolling-window
103
+ scan for the fallback path. Two concurrent agent boots against the
104
+ same instar repo would each emit; the AttentionQueue collapse
105
+ protects the Telegram side, and the JSONL state-dir is per-agent
106
+ so cross-agent file contention is impossible.
107
+ - **Races:** the detector awaits each `emitAttention` call (verified
108
+ by the async-await test in
109
+ `tests/unit/AgentWorktreeDetector-attention-wireup.test.ts`). The
110
+ TelegramAdapter's `createAttentionItem` does a network roundtrip
111
+ (`createForumTopic`); awaiting it keeps the detector's
112
+ `result.emitted` counter accurate.
113
+ - **Feedback loops:** the detector reads `git worktree list` and
114
+ emits attention items. AttentionQueue creates Telegram topics. No
115
+ loop back into `git worktree`.
116
+ - **Startup ordering:** moving the detector down ~700 lines in
117
+ `startServer()` lengthens the time between `registerAgent` and
118
+ the first detection by the duration of TelegramAdapter init. On
119
+ echo's machine that's typically <500ms (the Telegram
120
+ `start()` call awaits a long-poll handshake). No user-visible
121
+ latency change.
122
+
123
+ ## 6. External surfaces
124
+
125
+ - **Other agents on the same machine:** none. Each agent's
126
+ detection emits to its own TelegramAdapter / JSONL.
127
+ - **Other users of the install base:** purely additive — for any
128
+ agent that wasn't already configured for Telegram, behavior is
129
+ byte-identical to v1.2.3 (JSONL only). For Telegram-configured
130
+ agents, attention topics will appear on next agent restart per
131
+ misplaced worktree.
132
+ - **External systems:** Telegram (additional topic creates via
133
+ `createForumTopic`). Rate-limited by Telegram's API; one topic
134
+ per detection emission per misplaced path per process. Real-world
135
+ impact on echo's machine: ~30 attention topics on first boot
136
+ after this release, then near-zero per subsequent boot (all
137
+ deduped via item.id).
138
+ - **Persistent state:** the existing
139
+ `<stateDir>/audit/worktree-detector.jsonl` fallback file is
140
+ unchanged. Telegram attention topic registrations land in the
141
+ existing TelegramAdapter state (`<stateDir>/state/attention-items.json`,
142
+ managed by the adapter itself).
143
+ - **Timing:** detector still uses the existing 2-second timeout
144
+ on `git worktree list --porcelain`. Unchanged.
145
+
146
+ ## 7. Rollback cost
147
+
148
+ - **Code:** revert this commit. The detector returns to its pre-PR
149
+ position (early in `startServer()` with `emitAttention` always
150
+ undefined), and the JSONL fallback resumes as the only emission
151
+ channel. No data migration. Attention topics already created on
152
+ Telegram remain — they're operator-managed and don't depend on
153
+ this PR's wiring.
154
+ - **Persistent state:** no schema change. The
155
+ `<stateDir>/state/attention-items.json` registry is the same one
156
+ TelegramAdapter has owned since v0.10.x.
157
+ - **Agent state repair:** none required.
158
+ - **User visibility during rollback:** new attention topics stop
159
+ appearing; existing ones stay until the operator `/done`s or
160
+ `/wontdo`s them. The JSONL trail continues unchanged.
161
+
162
+ Total rollback time: under 2 minutes (one revert).
163
+
164
+ ## Conclusion
165
+
166
+ Pure wireup change. The detector and AttentionQueue have both been
167
+ in production since v1.1.0; this PR connects them. Signal-only
168
+ contract is preserved. Three new tests pin the shape-compat,
169
+ async-await safety, and JSONL-fallback-preservation invariants.
170
+ Pre-push gate green.
171
+
172
+ Phase 5 trigger check: the change does **not** touch outbound or
173
+ inbound messaging *dispatch* (the detector emits a category that
174
+ AttentionQueue already handles), session lifecycle, compaction,
175
+ coherence gates, idempotency at transport, trust levels, or anything
176
+ named sentinel/guard/gate/watchdog. The detector is signal-only and
177
+ AttentionQueue is the long-existing authority. No second-pass
178
+ reviewer required.
179
+
180
+ Clear to ship.
181
+
182
+ ## Evidence pointers
183
+
184
+ - **Wireup tests:** `tests/unit/AgentWorktreeDetector-attention-wireup.test.ts`
185
+ (3 cases: shape-compat between detector output and adapter input,
186
+ async-callback await safety, JSONL fallback preservation).
187
+ - **Spec:** `docs/specs/AGENT-WORKTREE-CONVENTION-SPEC.md` (approved
188
+ 2026-05-17 22:35 UTC).
189
+ - **Sibling artifacts:**
190
+ - `upgrades/side-effects/agent-worktree-convention-layer-1-2-5.md`
191
+ (Layer 1+2+5, merged in PR #277 as `bdf8508f`).
192
+ - `upgrades/side-effects/agent-worktree-convention-layer-3-4.md`
193
+ (Layer 3+4, merged in PR #278 as `c7e8e08b`; documented this
194
+ wireup as deferred follow-up).