instar 1.3.454 → 1.3.455

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cli.js +9 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/test-as-self.d.ts +17 -0
  4. package/dist/commands/test-as-self.d.ts.map +1 -1
  5. package/dist/commands/test-as-self.js +57 -0
  6. package/dist/commands/test-as-self.js.map +1 -1
  7. package/dist/permissions/testing/SlackScenarioHarness.d.ts +97 -5
  8. package/dist/permissions/testing/SlackScenarioHarness.d.ts.map +1 -1
  9. package/dist/permissions/testing/SlackScenarioHarness.js +190 -3
  10. package/dist/permissions/testing/SlackScenarioHarness.js.map +1 -1
  11. package/dist/scaffold/templates.d.ts.map +1 -1
  12. package/dist/scaffold/templates.js +4 -1
  13. package/dist/scaffold/templates.js.map +1 -1
  14. package/dist/server/routes.d.ts.map +1 -1
  15. package/dist/server/routes.js +36 -1
  16. package/dist/server/routes.js.map +1 -1
  17. package/dist/threadline/ThreadlineBootstrap.d.ts +0 -3
  18. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  19. package/dist/threadline/ThreadlineBootstrap.js +1 -35
  20. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  21. package/dist/threadline/client/ThreadlineClient.d.ts +32 -0
  22. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  23. package/dist/threadline/client/ThreadlineClient.js +112 -13
  24. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +78 -78
  27. package/src/scaffold/templates.ts +4 -1
  28. package/upgrades/1.3.455.md +46 -0
  29. package/upgrades/side-effects/slack-test-as-self-harness.md +155 -0
  30. package/upgrades/side-effects/threadline-dup-identity-pr1-resolver.md +162 -0
  31. package/upgrades/side-effects/threadline-dup-identity-pr2-orphan-removal.md +89 -0
@@ -1404,11 +1404,14 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
1404
1404
  instar test-as-self --no-roundtrip # deploy + verify only (no bot needed)
1405
1405
  instar test-as-self --bot-token <secret-drop-id> # + a real Telegram round-trip via a throwaway bot
1406
1406
  instar test-as-self --keep # leave the throwaway running for inspection
1407
+ instar test-as-self --slack # Slack permission demonstration (each principal+request → expected decision + audit entry)
1407
1408
  \`\`\`
1408
1409
 
1409
1410
  **Structural guards (you cannot foot-gun these):** \`--target\` can never be your canonical agent home or a protected agent (e.g. Bob); \`--bot-token\` refuses a raw token on the command line — pass a Secret Drop ID and the token is retrieved in-memory, never via argv. It emits a single JSON report; exit 0 = all steps PASS.
1410
1411
 
1411
- **Proactive trigger:** when you're about to ship or just shipped a change touching the deploy/lifeline/server-startup path, run this against a throwaway home first don't guess from logs.
1412
+ **\`--slack\` — the test-as-self-for-Slack demonstration (credential-free):** extends the throwaway-agent primitive from "is the agent alive?" to "does it enforce the RIGHT decision for each (principal, request) pair?". It runs the deterministic scenario suite through the SAME observer the live Slack adapter calls (resolver → permission gate → decision ledger) and asserts BOTH the verdict AND that the matching audit/ledger entry landed — "verified, not narrated". No Slack tokens, no throwaway deploy. The same suite is reachable over HTTP: \`GET /permissions/scenario-suite\` (logic-only view) and \`POST /permissions/scenario-suite/run\` (audit-asserting). Exit 0 = every row produced its expected decision AND its audit entry.
1413
+
1414
+ **Proactive trigger:** when you're about to ship or just shipped a change touching the deploy/lifeline/server-startup path, run this against a throwaway home first — don't guess from logs. When you touch the Slack org permission system (\`src/permissions/\`), run \`instar test-as-self --slack\` (or \`POST /permissions/scenario-suite/run\`) to prove the gate still enforces every (principal, request) row.
1412
1415
  `;
1413
1416
 
1414
1417
  if (hasTelegram) {
@@ -0,0 +1,46 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ When two registrations for the same agent name existed on the Threadline relay — a live one and a
9
+ stale leftover (a "dead twin") — a sender resolving that name could land on the dead one, and its
10
+ messages silently vanished (delivered-but-never-received). The fix is in the client name resolver
11
+ (`ThreadlineClient`): each known agent now carries a live/offline flag (sourced from the live
12
+ presence status the relay already reports), name resolution **prefers the single live registration**,
13
+ re-discovers a stale cache so a dead twin can't win, and — when two registrations are *genuinely*
14
+ both live (a real two-machine agent, or an impostor) — surfaces a disambiguation prompt instead of
15
+ silently guessing. A companion change removes the dead code (`loadOrCreateIdentityKeys`) that minted
16
+ the orphan identity which became the dead twin in the first place, so no new duplicates are created
17
+ fleet-wide. No relay deploy is required — the relay already reported the right information; the client
18
+ was discarding it.
19
+
20
+ ## What to Tell Your User
21
+
22
+ If another agent reported that messages to you were "going through on their side but never arriving"
23
+ — the classic symptom of a wedged agent-to-agent channel — this fixes it: your peer's software now
24
+ resolves your name to your live address instead of a stale duplicate. You don't need to do anything;
25
+ it takes effect after your agent updates and restarts. In the meantime, a peer can always reach you
26
+ by addressing your exact routing fingerprint instead of your name. If a peer ever gets an "ambiguous
27
+ agent name" prompt, that's intentional — it means two live registrations share your name, and they
28
+ should pick by fingerprint.
29
+
30
+ ## Summary of New Capabilities
31
+
32
+ This is a reliability/correctness fix, not a new feature — agent-to-agent name resolution now lands
33
+ on the live registration and never silently delivers to a dead duplicate. Agents also stop creating
34
+ an unused orphan identity file on boot.
35
+
36
+ ## Evidence
37
+
38
+ - 17 unit tests drive the real on-path resolver (`ThreadlineClient.resolveAgent` / `findAgentByName`):
39
+ live-vs-dead → resolves live; two-online → surfaces ambiguity; offline-only cache → re-discovers;
40
+ merge retains crypto keys; rate-limit early-resolve; partial-match parity.
41
+ - 1 relay-backed end-to-end test stands up a real relay with a live + a dead "echo" and asserts a
42
+ sender resolves the name to the live fingerprint, never the dead twin.
43
+ - 12 `ThreadlineBootstrap` unit tests (incl. "does NOT create identity-keys.json" + a CI guard that
44
+ the orphan-minting cannot be re-introduced). Typecheck clean; 1693 existing threadline tests green.
45
+ - Spec (converged iter 3, approved): `docs/specs/threadline-duplicate-identity-resolution.md`;
46
+ side-effects review with independent second-pass CONCUR.
@@ -0,0 +1,155 @@
1
+ # Side-Effects Review — test-as-self for Slack (permission demonstration harness)
2
+
3
+ **Version / slug:** `slack-test-as-self-harness`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `not required (Tier 1)`
7
+
8
+ ## Summary of the change
9
+
10
+ Pillar 4 / milestone 4 of the Slack org permission system (`SLACK-ORG-INTEGRATION-SPEC.md` §8).
11
+ This is a **test/demonstration harness over the EXISTING `SlackPermissionGate`** — it adds no
12
+ new runtime decision logic. It extends the existing `SlackScenarioHarness` (six gate-direct rows,
13
+ decision-only) with: (a) two more scenario rows — a granted-member-floor (→ allow/`floor-granted`)
14
+ and an unregistered outsider (→ refuse/`unregistered`) — completing the deterministic
15
+ credential-free subset; (b) a `StaticGrantStore` (deterministic fixture mirroring
16
+ `MandateBackedGrantStore`) and a `CastUserLookup` registry resolver, both plain test doubles that
17
+ feed the existing gate; (c) `runAuditedScenarioSuite`, which drives every row through the SAME
18
+ `SlackPermissionObserver` the live `SlackAdapter._handleMessage` calls (resolver → gate →
19
+ `PermissionDecisionLedger`) and asserts BOTH the verdict AND the matching audit-ledger entry
20
+ ("verified, not narrated"). It exposes an executable surface: `POST /permissions/scenario-suite/run`
21
+ and `instar test-as-self --slack`. Files: `src/permissions/testing/SlackScenarioHarness.ts`,
22
+ `src/server/routes.ts`, `src/commands/test-as-self.ts`, `src/cli.ts`, `src/scaffold/templates.ts`,
23
+ plus three new test files and two updated test files.
24
+
25
+ ## Decision-point inventory
26
+
27
+ This change touches NO runtime decision point. Every component invokes the existing gate and
28
+ reads the existing ledger; nothing gates, blocks, filters, or constrains agent behavior.
29
+
30
+ - `SlackPermissionGate.evaluate` — **pass-through** — unchanged; the harness only calls it.
31
+ - `buildSliceZeroGate()` (test helper) — **modify** — now constructs the gate with a
32
+ `StaticGrantStore`. Deny-by-default for everyone except the single Grace fixture, so no
33
+ existing row's verdict changes; it solely enables the `floor-granted` path for the new row.
34
+ - `StaticGrantStore` / `CastUserLookup` — **add** — test fixtures (a `GrantStore` and a
35
+ `UserLookup` test-double). No authority; they only supply inputs to the existing gate.
36
+ - `POST /permissions/scenario-suite/run` / `test-as-self --slack` — **add** — read-only/dev
37
+ executable wrappers that run the suite into a throwaway temp dir and return a report.
38
+
39
+ ---
40
+
41
+ ## 1. Over-block
42
+
43
+ No block/allow surface — over-block not applicable. The harness asserts the existing gate's
44
+ verdicts; it never itself rejects any input. (The gate's own over/under-block behavior is what
45
+ the harness *measures*, and is governed by the gate's own spec, unchanged here.)
46
+
47
+ ---
48
+
49
+ ## 2. Under-block
50
+
51
+ No block/allow surface — under-block not applicable. One harness-level honesty note: if a future
52
+ gate change altered a verdict, the suite would catch it as a row mismatch (that is the point — it
53
+ is a regression wall). The audit-assertion half also catches the subtler failure where a verdict
54
+ is returned but the ledger write silently fails.
55
+
56
+ ---
57
+
58
+ ## 3. Level-of-abstraction fit
59
+
60
+ Correct layer. This is a **demonstration/test harness** sitting ABOVE the gate, deliberately
61
+ driving the production observe path (`SlackPermissionObserver`) rather than re-implementing it.
62
+ It reuses the real `SlackPermissionGate`, real `SlackPermissionObserver`, real
63
+ `SlackPrincipalResolver`, and real `PermissionDecisionLedger`. The `StaticGrantStore` is the
64
+ deterministic, credential-free mirror of `MandateBackedGrantStore` (the live A4 path needs a
65
+ PIN-gated signed mandate that cannot run in CI) — it implements the SAME `GrantStore` interface
66
+ so the gate's `floor-granted` branch is exercised identically; only the grant's provenance
67
+ differs (a fixture vs a signed mandate), which is exactly the deterministic-vs-live seam (§8.3).
68
+
69
+ ---
70
+
71
+ ## 4. Signal vs authority compliance
72
+
73
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
74
+
75
+ - [x] **No — this change has no block/allow surface.**
76
+
77
+ The harness/route/CLI hold no authority. They invoke the existing gate and read its ledger. The
78
+ `StaticGrantStore` is a test fixture feeding the existing gate, not a new authority. Nothing here
79
+ makes a brittle blocking decision.
80
+
81
+ ---
82
+
83
+ ## 5. Interactions
84
+
85
+ - **Shadowing:** none. The harness runs the gate in isolation; it does not sit in any live
86
+ message path. (The full-pipeline test invokes `_handleMessage` directly in a test process with
87
+ a fake bot token — it never touches a real Slack workspace.)
88
+ - **Double-fire:** none. The audit-asserting run uses its OWN ledger in a fresh temp dir, so it
89
+ cannot collide with the live `/permissions/decisions` ledger or any live observer.
90
+ - **Races:** none. Each suite run is hermetic (its own temp state dir).
91
+ - **Feedback loops:** none. The behavior baseline (`RelationshipBehaviorStore`) is NOT wired into
92
+ the harness observer, so running the suite writes no durable baseline that could feed back into
93
+ the live anomaly scorer.
94
+
95
+ ---
96
+
97
+ ## 6. External surfaces
98
+
99
+ - **Other agents / users / external systems:** none. No real Slack tokens, no network calls to
100
+ Slack (the fake-token reaction/user-lookup calls inside `_handleMessage` fail closed with
101
+ `invalid_auth` and are swallowed — they are best-effort UI sugar, not assertions).
102
+ - **Persistent state:** none in the running agent. The audit-asserting suite writes its ledger
103
+ into `os.tmpdir()`, never `ctx.config.stateDir`; an integration test explicitly asserts the
104
+ returned `ledgerPath` is not under the route's configured state dir.
105
+ - **CLAUDE.md template:** the agent template's `## Test-As-Self` section gains a documented
106
+ `--slack` line + the two HTTP endpoints (Agent Awareness Standard). New agents receive it via
107
+ `init`; this is a doc surface, not runtime behavior — see Migration Parity note in the conclusion.
108
+ - **Timing/runtime conditions we don't control:** none — the suite is fully deterministic
109
+ (heuristic classifier + static baselines + static grants + injectable nothing-from-network).
110
+
111
+ ---
112
+
113
+ ## 7. Rollback cost
114
+
115
+ Pure additive code + tests + docs. Rollback = revert the commit and ship a patch. No persistent
116
+ state to migrate (the suite's only writes are throwaway temp dirs). No agent-state repair. No
117
+ user-visible regression during the rollback window (this is a maintainer/dev demonstration tool,
118
+ not a user-facing capability). The one behavior tweak (`buildSliceZeroGate` gains a grant store)
119
+ is confined to the test harness and is deny-by-default for all but the Grace fixture.
120
+
121
+ ---
122
+
123
+ ## Conclusion
124
+
125
+ The review found no decision-point surface and no signal-vs-authority concern: every component
126
+ invokes the existing gate and reads the existing ledger. The change's value is precisely that it
127
+ makes the gate's governance *watchable and self-verified* — each (principal, request) row produces
128
+ its expected verdict AND its matching audit entry, through the same observer the live adapter uses.
129
+ The deterministic-vs-live seam is honest (the live-Slack-workspace layer is explicitly out of scope
130
+ and remains operator-driven). All three test tiers are green; `tsc` and `lint` are clean. Clear to
131
+ commit (Tier 1). **Migration Parity note:** the only agent-installed artifact touched is the
132
+ CLAUDE.md template doc line for a dev-only tool — the route + CLI ship as compiled dist that every
133
+ agent receives automatically on update; no `PostUpdateMigrator` entry is warranted for a doc line
134
+ on a maintainer-only surface (consistent with how the existing `/permissions/scenario-suite` route
135
+ is treated).
136
+
137
+ ---
138
+
139
+ ## Second-pass review (if required)
140
+
141
+ Not required — Tier 1 (a test/demonstration harness with no block/allow surface and no session
142
+ lifecycle / gate / sentinel / watchdog runtime change).
143
+
144
+ ---
145
+
146
+ ## Evidence pointers
147
+
148
+ - `tests/unit/slack-scenario-audit-harness.test.ts` — 16 tests (cast, fixtures, both decision
149
+ boundaries per row, audit-entry assertion, durable-ledger read-back).
150
+ - `tests/integration/slack-permission-pipeline.test.ts` — 3 tests (every row through
151
+ `_handleMessage` → observer → ledger; granted-member + unregistered-outsider live-path rows).
152
+ - `tests/integration/permissions-routes.test.ts` — updated; `GET /permissions/scenario-suite`
153
+ (8 rows) + `POST /permissions/scenario-suite/run` (verdict AND audit per row).
154
+ - `tests/e2e/slack-scenario-suite-route.test.ts` — 2 tests (feature alive: 200 + full green report).
155
+ - CLI smoke: `node dist/cli.js test-as-self --slack` → all 8 rows `audit✓`, `VERDICT: PASS`, exit 0.
@@ -0,0 +1,162 @@
1
+ # Side-Effects Review — Threadline duplicate-identity resolver (PR1: client live-preference)
2
+
3
+ **Version / slug:** `threadline-dup-identity-pr1-resolver`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `independent reviewer subagent — CONCUR (Phase 5; high-risk: name→fingerprint dispatch resolution)`
7
+
8
+ ## Summary of the change
9
+
10
+ Closes the duplicate-identity silent-drop class on the exact path a sender uses
11
+ (`threadline_send` → `ThreadlineClient.resolveAgent` → `findAgentByName`). Driven by the
12
+ converged spec `docs/specs/threadline-duplicate-identity-resolution.md` (changes A/B/C).
13
+ Single file touched: `src/threadline/client/ThreadlineClient.ts`. (A) `KnownAgent` gains an
14
+ `online` flag, merged — never replaced — from the relay's `discover_result.status` (the relay
15
+ already derives that status from live presence; the client was discarding it). (B)
16
+ `findAgentByName` prefers the single live registration among same-name rows in both the exact and
17
+ partial branches, and still throws ambiguity when ≥2 are online (never silently picks). (C)
18
+ `resolveAgent` re-discovers once (cooldown-gated, rate-limit-aware) when the cached match is
19
+ offline, so a stale dead twin can be superseded by a live one. Tests: 17 unit on the real
20
+ resolver + 1 relay-backed e2e (live-vs-dead echo → resolves to live).
21
+
22
+ ## Decision-point inventory
23
+
24
+ - `ThreadlineClient.findAgentByName` (name → fingerprint resolution) — **modify** — adds a
25
+ live-registration preference before the existing ambiguity throw; the throw is preserved for
26
+ genuinely-ambiguous (≥2 online, or all offline >1) cases.
27
+ - `ThreadlineClient.resolveAgent` (cache → re-discover orchestration) — **modify** — adds one
28
+ cooldown-gated re-discovery when the resolved match is offline.
29
+ - `ThreadlineClient.discover` (relay discovery wait) — **modify** — adds a rate-limit-filtered
30
+ early-resolve so a throttled re-discovery fails fast instead of hanging to the 10s timeout.
31
+ - `ThreadlineClient` discover-result ingestion — **modify** — merge (not replace) so a keyless
32
+ discovery frame cannot strip crypto keys; map `status` → `online`.
33
+
34
+ ---
35
+
36
+ ## 1. Over-block
37
+
38
+ **What legitimate inputs does this change reject that it shouldn't?**
39
+
40
+ No new block/allow surface. The one place resolution can now *fail* where it previously
41
+ "succeeded" is when ≥2 same-name rows are simultaneously online — but that failure is the
42
+ existing ambiguity throw, which is the correct, pre-existing behavior (require `name:fingerprint`).
43
+ A legitimately-multi-machine agent (two keys, both online) now gets that loud ambiguity rather
44
+ than an arbitrary pick — intended, and the error text points at the disambiguation syntax + saved
45
+ nicknames. No legitimate single-recipient send is newly rejected: a live-vs-dead pair now resolves
46
+ (previously it threw or silently dropped); a single offline peer still resolves (returns the
47
+ offline fingerprint, preserving offline-queue semantics).
48
+
49
+ ---
50
+
51
+ ## 2. Under-block
52
+
53
+ **What failure modes does this still miss?**
54
+
55
+ Name discovery remains trust-on-first-use and unauthenticated at the relay (the `name` is not
56
+ cryptographically bound — pre-existing, documented in the spec's §Security). If the real agent is
57
+ genuinely offline AND only an impostor with the same name is online, the exactly-one-online rule
58
+ will prefer the impostor — but ONLY during a real outage of the genuine agent (an impostor cannot
59
+ make itself the *only* online "echo" while the real one is connected, because presence is keyed per
60
+ fingerprint). This is a pre-existing TOFU property the change makes more deterministic in that
61
+ narrow window; the spec records it and leaves room for a future `verified`-fingerprint tiebreak.
62
+ Not closed here by design.
63
+
64
+ ---
65
+
66
+ ## 3. Level-of-abstraction fit
67
+
68
+ **Is this at the right layer?**
69
+
70
+ Yes — and this is the headline correction from convergence. v1 of the spec put the fix at the
71
+ relay's `RegistryStore.search()` ORDER BY, which is OFF the instar send path (the client ingests
72
+ discovery into a fingerprint-keyed Map and resolves locally). The fix belongs in the client
73
+ resolver — the lowest layer that actually decides name → fingerprint for a send — and it CONSUMES
74
+ a signal the relay already produces (live `status`) rather than re-implementing liveness. No new
75
+ parallel gate; it feeds off existing infrastructure.
76
+
77
+ ---
78
+
79
+ ## 4. Signal vs authority compliance
80
+
81
+ **Required reference:** docs/signal-vs-authority.md
82
+
83
+ - [x] No — this change produces/consumes a selection signal; it has no brittle blocking authority.
84
+
85
+ The change is a *selection preference* (prefer the live registration). It never silently picks
86
+ among multiple live registrations — it surfaces the existing ambiguity throw for the operator/caller
87
+ to disambiguate. No message is blocked, dropped, or rewritten. Liveness is sourced from the relay's
88
+ live-presence-derived `status`, not a brittle local heuristic.
89
+
90
+ ---
91
+
92
+ ## 5. Interactions
93
+
94
+ - **Shadowing:** the online-preference runs *before* the existing ambiguity throw in
95
+ `findAgentByName`; it does not shadow the throw — it narrows when the throw fires (only ≥2 online,
96
+ or >1 all-offline). The `fingerprintPrefix` (`name:fingerprint`) path still wins ahead of both,
97
+ unchanged.
98
+ - **Double-fire:** the offline re-discovery is gated by a per-name 30s cooldown
99
+ (`lastRediscoverByName`), so a burst of sends to an offline target cannot re-query the relay
100
+ repeatedly; the existing cache-miss re-discovery path is unchanged (not cooldown-gated).
101
+ - **Races:** `discover()` now registers an `error` listener alongside `discover-result`; both share
102
+ a `settled` guard + `cleanup()` so a late frame after either fires is a no-op. The error listener
103
+ filters strictly on `code === RATE_LIMITED`, so an unrelated error frame (e.g. a concurrent send's
104
+ `RECIPIENT_OFFLINE`) cannot spuriously resolve discovery (unit-tested).
105
+ - **Feedback loops:** none — discovery output feeds resolution, not back into discovery (the
106
+ cooldown bounds the one re-query).
107
+
108
+ ---
109
+
110
+ ## 6. External surfaces
111
+
112
+ - **Other agents:** the merge-not-replace fix actually PREVENTS a latent regression — a keyless
113
+ discovery frame no longer strips a peer's cached crypto keys, so E2E `send()` to a
114
+ previously-keyed peer is not downgraded to plaintext. Name-resolved sends to freshly-discovered
115
+ peers continue to use the relay plaintext-authenticated path (transport TLS + Ed25519), unchanged.
116
+ - **External systems:** none. No relay deploy (the relay already sends live status). No HTTP route
117
+ added or changed. No config knob, no dashboard surface, no CLAUDE.md template change (transparent
118
+ correctness fix).
119
+ - **Persistent state:** none — `knownAgents` and `lastRediscoverByName` are process-local, never
120
+ persisted. No DB/ledger/migration.
121
+ - **Timing:** the offline re-discovery adds at most one discovery round-trip (≤ the 10s discover
122
+ timeout, and fails fast on rate-limit) per resolve when the cached match is offline and the
123
+ cooldown allows.
124
+
125
+ ---
126
+
127
+ ## 7. Rollback cost
128
+
129
+ Pure client-side code change in one file. Rollback = revert the PR; `findAgentByName`/`resolveAgent`
130
+ return to throw-on-ambiguity / cache-hit behavior. No persistent state, no data migration, no agent
131
+ state repair. Reaches the fleet on the next server/session restart after auto-update activates (the
132
+ in-memory cache self-heals on restart). No user-visible regression during the rollback window.
133
+
134
+ ---
135
+
136
+ ## Conclusion
137
+
138
+ The review produced no design changes beyond what convergence already folded in. The change is a
139
+ narrowly-scoped, single-file client resolver fix that consumes an existing relay signal, never
140
+ silently picks among live registrations, and carries a comprehensive on-path test suite (17 unit +
141
+ 1 relay-backed e2e, all green; 1693 existing threadline tests unaffected; typecheck clean). High-risk
142
+ classification (name→fingerprint dispatch resolution) triggers the Phase-5 second-pass review below.
143
+
144
+ ---
145
+
146
+ ## Second-pass review (if required)
147
+
148
+ **Reviewer:** independent reviewer subagent
149
+ **Independent read of the artifact: concur**
150
+
151
+ Audited the actual implementation against the spec, both test files, the relay wire contract
152
+ (`relay/types.ts`), and the route callsite (`routes.ts:17500-17626`). Confirmed grounded in code:
153
+ the merge provably retains `publicKey`/`x25519PublicKey` (keyless frame → `??` keeps existing);
154
+ `pickSingleOnline` returns the single online match and the ambiguity throw fires for ≥2-online in
155
+ BOTH the exact and partial branches; `resolveAgent` re-discovers only on an offline match, returns
156
+ the offline fingerprint when still offline (no 404), and propagates the ambiguity throw (caught at
157
+ the route as a 500 with disambiguation guidance); `discover()` early-resolves only on
158
+ `code===RATE_LIMITED`. Four findings, all MINOR: the `online===undefined` case (now covered by added
159
+ tests), a `status`-absent frame mapping to `online:false` (harmless — `status` is non-optional on the
160
+ wire), a vestigial `status` property on the stored object (no consumer reads it), and an artifact
161
+ test-count miscount (now corrected to 17). No over-block, no authority creep, no external-surface
162
+ change. Clear to ship.
@@ -0,0 +1,89 @@
1
+ # Side-Effects Review — Threadline orphan-identity removal (PR2: stop minting identity-keys.json)
2
+
3
+ **Version / slug:** `threadline-dup-identity-pr2-orphan-removal`
4
+ **Date:** `2026-06-09`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `not required (Tier 1 — dead-code removal, no decision surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ Removes `ThreadlineBootstrap.loadOrCreateIdentityKeys` and the now-dead `identityKeys` field
11
+ from `ThreadlineBootstrapResult` (change D of the converged spec
12
+ `docs/specs/threadline-duplicate-identity-resolution.md`). That function minted an orphan
13
+ `threadline/identity-keys.json` keypair on every agent boot, independent of the canonical
14
+ `identity.json` — the exact artifact that became the dead "echo" twin polluting the relay. Its
15
+ output was consumed nowhere (verified: zero `.identityKeys` readers in `src/` and `tests/`;
16
+ `server.ts` never destructures it). The handshake and relay-client paths source identity via
17
+ `HandshakeManager.getOrCreateIdentity()` / `IdentityManager` (canonical `identity.json`) and are
18
+ untouched. Files: `src/threadline/ThreadlineBootstrap.ts` (remove function, field, call, return,
19
+ two now-unused imports, the orphan-only constant) + `tests/unit/threadline/ThreadlineBootstrap.test.ts`
20
+ (delete the orphan-file persistence/permissions/corrupted-regeneration tests, drop the
21
+ `result.identityKeys` assertions, add a "does NOT create identity-keys.json" test + a CI guard that
22
+ `loadOrCreateIdentityKeys` is absent from the file).
23
+
24
+ ## Decision-point inventory
25
+
26
+ - No decision point. This removes a write-only code path (`loadOrCreateIdentityKeys`) that gated
27
+ nothing, blocked nothing, and whose output fed nothing. Pure dead-code removal.
28
+
29
+ ---
30
+
31
+ ## 1. Over-block
32
+
33
+ No block/allow surface — over-block not applicable.
34
+
35
+ ## 2. Under-block
36
+
37
+ No block/allow surface — under-block not applicable.
38
+
39
+ ## 3. Level-of-abstraction fit
40
+
41
+ Correct layer: the orphan keypair was a vestige of an older identity model superseded by
42
+ `IdentityManager` (canonical `identity.json`). Removing the minting function (not the on-disk file)
43
+ is the minimal, right-altitude change. #479 deliberately fenced off this removal as needing its own
44
+ spec ("deleting risks the handshake path"); this PR is that spec's change D, and the handshake-path
45
+ risk is rebutted with evidence — `HandshakeManager.getOrCreateIdentity()` reads
46
+ `{stateDir}/threadline/identity.json`, never the orphan `identity-keys.json` (verified).
47
+
48
+ ## 4. Signal vs authority compliance
49
+
50
+ - [x] No — this change has no block/allow surface (dead-code removal).
51
+
52
+ ## 5. Interactions
53
+
54
+ - **Shadowing / double-fire:** none — the removed function had no callers beyond the single
55
+ bootstrap call site, and its result flowed nowhere.
56
+ - **Races:** none — it only wrote an unused file; removing it removes one boot-time fs write.
57
+ - **Feedback loops:** none.
58
+ - Existing `identity-keys.json` files already on disk become permanently inert (they already were —
59
+ nothing read them since #479). They are NOT deleted by this change (a destructive fleet file-sweep
60
+ is tracked separately, out of scope).
61
+
62
+ ## 6. External surfaces
63
+
64
+ - **Other agents / relay:** strictly improves coherence — no new orphan identity is ever minted, so
65
+ the duplicate-registration source is closed fleet-wide. No new orphan can be registered on the relay.
66
+ - **Persistent state:** stops creating one unused file per agent. Existing inert files left in place
67
+ (no migration). No DB/ledger change.
68
+ - **External systems / config / dashboard / CLAUDE.md template:** none.
69
+
70
+ ## 7. Rollback cost
71
+
72
+ Pure code-deletion. Rollback = revert the PR; `loadOrCreateIdentityKeys` is reinstated and resumes
73
+ writing the (unused) file. No data migration, no agent-state repair, no user-visible regression.
74
+
75
+ ---
76
+
77
+ ## Conclusion
78
+
79
+ Dead-code removal closing the source of the duplicate-identity artifact, paired with the companion
80
+ client resolver fix (PR1). No decision surface, no external behavior change beyond ceasing to write
81
+ an unused file. The #479 handshake-risk fence is rebutted with verified evidence. 12 bootstrap unit
82
+ tests green (including the new "no orphan file" assertion + CI guard); typecheck clean; no other
83
+ consumer of the removed field exists. Tier 1: small, low-risk, second-pass not required.
84
+
85
+ ---
86
+
87
+ ## Second-pass review (if required)
88
+
89
+ **Reviewer:** not required (Tier 1 — dead-code removal, no block/allow or lifecycle decision surface).