instar 1.3.56 → 1.3.58

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 (45) hide show
  1. package/dist/commands/test-as-self.d.ts.map +1 -1
  2. package/dist/commands/test-as-self.js +2 -1
  3. package/dist/commands/test-as-self.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts +33 -0
  5. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  6. package/dist/core/PostUpdateMigrator.js +117 -0
  7. package/dist/core/PostUpdateMigrator.js.map +1 -1
  8. package/dist/monitoring/SessionReplyExtractor.d.ts +31 -0
  9. package/dist/monitoring/SessionReplyExtractor.d.ts.map +1 -0
  10. package/dist/monitoring/SessionReplyExtractor.js +115 -0
  11. package/dist/monitoring/SessionReplyExtractor.js.map +1 -0
  12. package/dist/scaffold/templates.d.ts.map +1 -1
  13. package/dist/scaffold/templates.js +4 -0
  14. package/dist/scaffold/templates.js.map +1 -1
  15. package/dist/scheduler/MentorOnboardingRunner.d.ts +10 -1
  16. package/dist/scheduler/MentorOnboardingRunner.d.ts.map +1 -1
  17. package/dist/scheduler/MentorOnboardingRunner.js.map +1 -1
  18. package/dist/server/AgentServer.d.ts +20 -0
  19. package/dist/server/AgentServer.d.ts.map +1 -1
  20. package/dist/server/AgentServer.js +122 -11
  21. package/dist/server/AgentServer.js.map +1 -1
  22. package/dist/server/routes.d.ts.map +1 -1
  23. package/dist/server/routes.js +1 -0
  24. package/dist/server/routes.js.map +1 -1
  25. package/dist/threadline/AgentDiscovery.d.ts +6 -1
  26. package/dist/threadline/AgentDiscovery.d.ts.map +1 -1
  27. package/dist/threadline/AgentDiscovery.js +3 -0
  28. package/dist/threadline/AgentDiscovery.js.map +1 -1
  29. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  30. package/dist/threadline/ThreadlineBootstrap.js +20 -2
  31. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  32. package/dist/threadline/ThreadlineEndpoints.d.ts +2 -0
  33. package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
  34. package/dist/threadline/ThreadlineEndpoints.js +13 -1
  35. package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/data/builtin-manifest.json +64 -64
  38. package/src/scaffold/templates.ts +4 -0
  39. package/upgrades/1.3.56.md +16 -17
  40. package/upgrades/1.3.57.md +49 -0
  41. package/upgrades/1.3.58.md +47 -0
  42. package/upgrades/side-effects/mentee-agent-name-and-botid-coercion.md +55 -0
  43. package/upgrades/side-effects/mentor-cycle-smoothness.md +48 -0
  44. package/upgrades/side-effects/test-as-self-skill-demote.md +38 -0
  45. package/upgrades/side-effects/threadline-identity-discovery-unification.md +100 -0
@@ -0,0 +1,49 @@
1
+ # Instar Upgrade Guide — NEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ **1. Mentor cycle round-trip — final fixes (verified live end-to-end).** Three bugs that only surfaced when the full Echo↔Codey round-trip ran on real servers:
8
+
9
+ 1. **`mentor.menteeAgentName` config.** The mentor derived the mentee's agent name as `instar-${menteeFramework}` (e.g. `instar-codex-cli`), but the mentee registers under its real name (`instar-codey`). The mismatch broke same-machine peer lookup AND the reply allowlist. New optional `mentor.menteeAgentName` (defaults to `instar-${menteeFramework}`) carries the real registry name.
10
+ 2. **botId string coercion.** `mentor.menteeBotId` is often stored as a JSON number, but the a2a marker's `senderBotId` is always a string. The allowlist compares with `===`, so a number/string mismatch silently dropped every reply as `agent-marker-unknown`. All botId allowlist entries are now `String()`-coerced.
11
+ 3. **`senderBotId` = sender's own bot id.** The unified transport sent the *recipient's* bot id as the inbox `senderBotId`; the recipient's allowlist check (`knownAgents[from].botId === senderBotId`) therefore always failed. It now sends the *sender's* own bot id (`ownPrimaryBotId()` for replies, the mentor bot id for sends).
12
+
13
+ With these, the mentor cycle round-trips live: a mentor prompt reaches the mentee, the mentee spawns a session + replies, and the reply lands in the mentor's `mentor-replies.jsonl`.
14
+
15
+ **2. Threadline identity-discovery unification** (spec `docs/specs/threadline-identity-discovery-unification.md`, approved). Fixes a fleet-wide bug where an agent advertised a Threadline identity its own relay does not answer to — so peers who discovered it got a dead address and their messages silently vanished.
16
+
17
+ Discovery (`agent-info.json`) and `/threadline/health` now advertise the **routing fingerprint** — the address the relay actually registers with — plus a `publicKey`/`identityPub` set to the SAME canonical `identity.json` key, so the two are internally consistent (`fingerprint === computeFingerprint(publicKey)`). Both are resolved via `IdentityManager.get()`, the exact read-only call the relay client uses, so discovery and routing can never re-diverge.
18
+
19
+ ## What to Tell Your User
20
+
21
+ - The cross-agent mentor cycle now works fully end-to-end on the same machine — verified with a real round-trip. These were the last three wiring bugs (an agent-name assumption, a number-vs-string comparison, and a sender-identity mixup). No config changes required unless your mentee's registry name differs from the default (instar plus the framework name), in which case set the mentee agent name in config.
22
+ - If another agent's messages to me were vanishing (their side said "sent" but nothing arrived), the Threadline fix addresses it: I was handing out an address my relay doesn't listen on. Now I publish the right one everywhere, and existing agents are repaired automatically on the next update/restart — no action needed.
23
+
24
+ ## Summary of New Capabilities
25
+
26
+ | Capability | How to Use |
27
+ |-----------|-----------|
28
+ | `mentor.menteeAgentName` | Set in `.instar/config.json` when the mentee's agent-registry name differs from `instar-<menteeFramework>` (e.g. `instar-codey`). Defaults to the framework-derived name. |
29
+ | Robust botId matching | botId allowlist entries are string-coerced; `senderBotId` is the sender's own bot id. No action needed — fixes silent reply drops. |
30
+ | `fingerprint` field in `/threadline/health` + `agent-info.json` | `curl localhost:PORT/threadline/health` → `fingerprint` is the routable relay address |
31
+ | Threadline fleet repair migrator | `migrateThreadlineAgentInfoIdentity` runs on update; rewrites a diverged `agent-info.json` to the canonical consistent pair (no-op when already aligned or no routing identity) |
32
+
33
+ ## Behavior Notes (Threadline)
34
+
35
+ - **No fabrication:** an agent with no resolvable routing identity (none on disk, or a locked-encrypted `identity.json`) now OMITS `fingerprint`/`publicKey` from discovery instead of inventing a dead address. It becomes relay-discoverable once it has a routing identity (self-heals on the next boot).
36
+ - **Safe encoding/consumer story:** `publicKey` stays hex and is a valid 32-byte Ed25519 key; `/threadline/health.identityPub` switches to the canonical key only when one resolves. The E2E handshake exchanges keys inline (does not read these fields) and `verifyAgent` does no real crypto challenge, so the switch is safe.
37
+
38
+ ## Evidence
39
+
40
+ - **Mentor cycle:** extended `mentor-reply-via-inbox` E2E with a second case proving the menteeAgentName override + numeric-configured botId (string senderBotId) routes + persists. Both cases green; all prior mentor/mentee/inbox tests green. **Live-verified**: full Echo→Codey→Echo round-trip persisted to `mentor-replies.jsonl`. Side-effects: `upgrades/side-effects/mentee-agent-name-and-botid-coercion.md`.
41
+ - **Threadline — wiring (load-bearing):** boot the Threadline stack with a seeded canonical identity → `agent-info.json.fingerprint` equals `IdentityManager.getOrCreate().fingerprint` (the relay's registration source). `tests/e2e/threadline/identity-discovery-wiring.test.ts`.
42
+ - **Threadline — integration:** `/threadline/health` returns a non-empty `identityPub` + a `fingerprint` equal to the relay-registration fingerprint, internally consistent. `tests/integration/threadline/identity-discovery-health.test.ts`.
43
+ - **Threadline — unit (both-sides):** identity present → consistent pair advertised; no/locked identity → both omitted; orphan `identity-keys.json` present → canonical advertised, never the orphan hex. `ThreadlineBootstrap.test.ts`, `ThreadlineEndpoints.test.ts`.
44
+ - **Threadline — migrator:** diverged → repaired; aligned → no-op; no-identity → no-op (no fabrication); idempotent. `tests/unit/PostUpdateMigrator-threadlineAgentInfoIdentity.test.ts`. Verified end-to-end via test-as-self against the built dist (positive + no-identity boundary).
45
+
46
+ ## Out of Scope (Threadline — tracked separately)
47
+
48
+ - Multi-machine same-fingerprint advertisement coordination (cross-machine seamlessness spec). This fix sets the `machine` field and is neutral for multi-machine.
49
+ - Retiring the orphan `identity-keys.json` / collapsing the keypairs. This fix stops *advertising* the orphan; deleting it touches the handshake layer and needs its own spec.
@@ -0,0 +1,47 @@
1
+ # Instar Upgrade Guide — NEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ **Mentor cycle: the two remaining smoothness issues fixed.**
8
+
9
+ 1. **Robust reply capture (no more empty / raw-JSON replies).** The mentee
10
+ role-handler captured the tmux pane after the session completed — racing
11
+ the reaper (→ empty reply) and yielding raw codex stream JSON. It now reads
12
+ the session's persisted transcript: codex → the rollout's
13
+ `task_complete.last_agent_message` (clean prose), claude → the JSONL
14
+ transcript's last assistant text. The tmux capture remains a fallback when
15
+ no transcript is found. New `SessionReplyExtractor` module (pure, unit-
16
+ tested) + `extractMenteeReplyFromTranscript` wiring.
17
+
18
+ 2. **`isMenteeBusy` is remote-aware.** It checked Echo's LOCAL
19
+ `sessionManager.listRunningSessions()` — always non-empty, so every mentor
20
+ tick was wrongly blocked as "busy" (the mentee is a separate remote agent;
21
+ Echo's local sessions say nothing about it). It now gates on the
22
+ `OutstandingPromptTracker` — busy iff a prior mentor prompt to this mentee
23
+ is still unanswered. That's the honest per-mentee signal and the same
24
+ anti-ping-pong invariant, so real ticks now fire when the mentee is idle.
25
+
26
+ ## What to Tell Your User
27
+
28
+ The cross-agent mentor cycle now runs cleanly: replies come back as readable
29
+ prose (not empty, not raw stream JSON), and the scheduled mentor tick actually
30
+ fires when the mentee is free (it was previously always self-blocked). No
31
+ config changes.
32
+
33
+ ## Summary of New Capabilities
34
+
35
+ | Capability | How to Use |
36
+ |-----------|-----------|
37
+ | Transcript-based mentee reply capture | Automatic — the mentee reply is read from the session transcript (codex rollout / claude JSONL), clean prose, robust to the reap race. |
38
+ | Remote-aware mentor safe-window | Automatic — `isMenteeBusy` gates on the outstanding-prompt tracker, so `/mentor/tick` runs when no prior prompt is in-flight. |
39
+
40
+ ## Evidence
41
+
42
+ 10 new unit tests for `SessionReplyExtractor` (codex task_complete / agent_message /
43
+ response_item fallbacks + claude assistant extraction + malformed tolerance),
44
+ all green. mentor-runner + mentor/mentee/inbox suites still green; `tsc
45
+ --noEmit` clean. Live round-trip re-verification (tick-driven, clean prose)
46
+ runs post-release. Side-effects:
47
+ `upgrades/side-effects/mentor-cycle-smoothness.md`.
@@ -0,0 +1,55 @@
1
+ # Side-Effects Review — menteeAgentName + botId coercion + senderBotId fix
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Recipient side. Final
4
+ fixes closing the live round-trip (the reply leg landed in #469; these are the
5
+ three remaining wiring bugs found by running it end-to-end).
6
+
7
+ **Change:** `src/scheduler/MentorOnboardingRunner.ts` (+1 optional config
8
+ field) + `src/server/AgentServer.ts` (menteeAgentName usage at 3 derivation
9
+ sites, botId String-coercion at 3 allowlist sites, senderBotId=sender's-own
10
+ via new `ownPrimaryBotId()` helper) + extended E2E test.
11
+
12
+ ## What changed
13
+ 1. `MentorConfig.menteeAgentName?: string` — the mentee's real registry name.
14
+ Used in `deliverToMentee`, `installMentorMessageHook` (mentor side), and
15
+ `installMentorReceiverHook`, each falling back to `instar-${menteeFramework}`.
16
+ 2. botId `String()`-coercion at every knownAgents construction (mentor side,
17
+ mentee side, receiver hook) — config may store a JSON number; the marker
18
+ senderBotId is always a string; the allowlist uses `===`.
19
+ 3. `deliverA2aMessage` now takes `fromBotId` (sender's own bot id) and sends
20
+ it as the inbox `senderBotId`. `ownPrimaryBotId()` derives this agent's bot
21
+ id from its messaging config token. Replies pass `ownPrimaryBotId()`; sends
22
+ pass the mentor bot id.
23
+
24
+ ## The seven questions
25
+ 1. **Over-block.** N/A — these LOOSEN an over-strict (buggy) comparison that
26
+ was dropping legitimate replies. Coercion only makes a number match its
27
+ string form; it does not widen the allowlist to new agents.
28
+ 2. **Under-block.** The allowlist still requires an exact (coerced) botId
29
+ match + known agent name. Spoof defense intact — a wrong botId still drops.
30
+ 3. **Level-of-abstraction fit.** Small, local fixes at the exact sites; one
31
+ tiny helper (`ownPrimaryBotId`). No new infrastructure.
32
+ 4. **Signal vs authority.** Unchanged — allowlist still decides route/drop.
33
+ 5. **Interactions.** `menteeAgentName` defaults preserve back-compat for any
34
+ agent whose registry name already equals `instar-<framework>`. Coercion is
35
+ idempotent on already-string values.
36
+ 6. **External surfaces.** One new optional config key (`mentor.menteeAgentName`).
37
+ No new routes.
38
+ 7. **Rollback cost.** Trivial — revert restores framework-derived names + the
39
+ number/string comparison (re-introducing the silent reply drop).
40
+
41
+ ## Testing
42
+ Extended `mentor-reply-via-inbox` E2E (now 2 cases): the original
43
+ (framework-derived name, string botId) + the new regression (menteeAgentName
44
+ override, numeric-configured botId, string senderBotId) — both route + persist.
45
+ All prior mentor/mentee/inbox tests green. tsc clean.
46
+
47
+ **Live verification (test-as-self):** the worktree build was shadow-deployed to
48
+ the live Echo + Codey pair; a real mentor prompt round-tripped — Codey spawned
49
+ a mentee session, replied via /a2a/inbox, and Echo persisted the reply to
50
+ mentor-replies.jsonl (corr=FULL-…, from=instar-codey, transport=a2a-inbox-local).
51
+
52
+ ## Migration parity
53
+ `mentor.menteeAgentName` is optional with a back-compat default — no migration
54
+ needed. The botId coercion + senderBotId fix are pure code (no config/state
55
+ change). No PostUpdateMigrator change.
@@ -0,0 +1,48 @@
1
+ # Side-Effects Review — mentor cycle smoothness (capture + busy-gate)
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Recipient side. The two
4
+ fast-follows from the live round-trip: reply-capture reliability + the
5
+ remote-mentee safe-window.
6
+
7
+ **Change:** new `src/monitoring/SessionReplyExtractor.ts` (pure extractors) +
8
+ `src/server/AgentServer.ts` (`extractMenteeReplyFromTranscript` helper, wired
9
+ into the mentee handler as transcript-first/tmux-fallback; `isMenteeBusy`
10
+ gates on OutstandingPromptTracker) + unit tests.
11
+
12
+ ## What changed
13
+ 1. `extractCodexFinalMessage` / `extractClaudeFinalMessage` — pure, tolerant
14
+ parsers returning the final assistant prose (null when absent).
15
+ 2. `extractMenteeReplyFromTranscript(session, spawnTs)` — codex: newest rollout
16
+ written ≥ spawn → task_complete.last_agent_message; claude: the
17
+ `<claudeSessionId>.jsonl` under ~/.claude/projects → last assistant text.
18
+ 3. Mentee handler: after the bounded-wait, prefer the transcript reply; keep
19
+ the existing capture-while-alive tmux read as the only fallback.
20
+ 4. `isMenteeBusy`: `!getOrCreateMentorOutstanding().canSendTo(menteeAgent).ok`
21
+ instead of Echo's local session count.
22
+
23
+ ## The seven questions
24
+ 1. **Over-block.** isMenteeBusy now blocks LESS (only when a real prompt is
25
+ outstanding) — it was over-blocking before (always busy). Correct direction;
26
+ the anti-ping-pong invariant is preserved (one outstanding prompt per mentee).
27
+ 2. **Under-block.** The outstanding-prompt gate still prevents concurrent
28
+ prompts to the same mentee. Transcript reading is read-only (no new writes).
29
+ 3. **Level-of-abstraction fit.** Extractors are pure + isolated; the helper
30
+ reuses findRecentRolloutFiles (existing). isMenteeBusy reuses the existing
31
+ tracker. No new infrastructure.
32
+ 4. **Signal vs authority.** Extractors are pure signal; the handler decides.
33
+ isMenteeBusy is a gate input to the existing safe-window decision.
34
+ 5. **Interactions.** Reads ~/.codex/sessions + ~/.claude/projects (read-only).
35
+ Reuses OutstandingPromptTracker (shared with deliverToMentee — consistent
36
+ semantics). No new shared mutable state.
37
+ 6. **External surfaces.** None new. mentor-replies.jsonl now carries clean
38
+ prose instead of stream JSON (content-quality change, same schema).
39
+ 7. **Rollback cost.** Trivial — revert restores tmux-only capture + the local
40
+ session-count busy check.
41
+
42
+ ## Testing
43
+ 10 new unit tests (SessionReplyExtractor) green; mentor-runner + mentor/mentee/
44
+ inbox suites green; tsc clean. Live tick-driven round-trip with clean-prose
45
+ capture verified post-release (the autonomous run's completion criterion).
46
+
47
+ ## Migration parity
48
+ No config keys, no state schema change. Pure code. No PostUpdateMigrator change.
@@ -0,0 +1,38 @@
1
+ # Side-Effects Review — test-as-self SKILL Part 2.1 demote + migrator (Track F follow-up)
2
+
3
+ **Scope.** `.claude/skills/test-as-self/SKILL.md` (lead with the one-button
4
+ command, demote the manual recipe to fallback), `src/core/PostUpdateMigrator.ts`
5
+ (`migrateTestAsSelfSkill` + registration), `tests/unit/migrate-test-as-self-skill.test.ts`.
6
+
7
+ **What + why.** Track F shipped the `instar test-as-self` command but left the
8
+ SKILL.md as the v1 manual runbook (deferred, tracked). This closes that loop:
9
+ the SKILL now leads with "## The one-button path (Part 2.1 — use this first)"
10
+ (the command), with the manual recipe demoted to fallback — so an agent reading
11
+ the skill reaches for the command first. Plus a PostUpdateMigrator method so
12
+ EXISTING agents' on-disk SKILL.md gets the update (installBuiltinSkills is
13
+ install-if-missing, so a dedicated migration is the only path per the Migration
14
+ Parity Standard).
15
+
16
+ **Side-effects review.**
17
+ - **Content-only skill change** — no behavior change; the command itself shipped
18
+ in Track F (#472). This just makes it discoverable in the skill surface.
19
+ - **Migration is conservative + idempotent** — re-copies the bundled SKILL only
20
+ when the installed copy LACKS the Part 2.1 marker AND still matches the stock
21
+ fingerprint (`Throwaway-Deploy Harness` + `verify.mjs`). A customized skill is
22
+ left untouched (recorded in result.skipped); an already-updated one is a no-op;
23
+ a not-installed one is a no-op. Mirrors the proven migrateBuildSkillMethodology
24
+ pattern exactly.
25
+ - **No data/secret impact** — pure doc/skill content.
26
+
27
+ **Test coverage.** Unit `tests/unit/migrate-test-as-self-skill.test.ts` (4):
28
+ stock-v1 → updated (marker present, result.upgraded set); idempotent (already-
29
+ marked → unchanged, no upgrade); customized → untouched (result.skipped);
30
+ not-installed → no-op (no error). All green; tsc clean. The test also implicitly
31
+ verifies the bundled SKILL.md carries the new marker.
32
+
33
+ **Migration parity.** This IS the migration-parity fix for Track F's SKILL
34
+ update. New agents get the new SKILL via installBuiltinSkills; existing agents
35
+ get it via `migrateTestAsSelfSkill` on update.
36
+
37
+ **Rollback.** Revert. The SKILL reverts to the v1 manual-first runbook; the
38
+ command still works (it's independent). No data change.
@@ -0,0 +1,100 @@
1
+ # Side-Effects Review — Threadline Identity-Discovery Unification
2
+
3
+ **Slug:** `threadline-identity-discovery-unification`
4
+ **Date:** 2026-05-28
5
+ **Author:** Echo (instar developer)
6
+ **Spec:** `docs/specs/threadline-identity-discovery-unification.md` (converged iter 2, approved)
7
+
8
+ ## Summary of the change
9
+
10
+ Discovery was advertising an identity the agent's own relay does not answer to.
11
+ `announcePresence` wrote `agent-info.json.publicKey` from the orphan
12
+ `threadline/identity-keys.json` hex key, and `/threadline/health` reported
13
+ `identityPub` from the HandshakeManager key (empty on some agents) — while the
14
+ relay client registers (and routes) by the canonical `identity.json`
15
+ fingerprint. A peer who discovered the agent got a non-routable address, so
16
+ their messages vanished (their side `sent=true`, no "Accepted message from …"
17
+ on the receiver). Fleet-wide for any agent whose canonical identity was
18
+ generated independently of its legacy keys.
19
+
20
+ The fix advertises the **routing identity** everywhere, internally consistent:
21
+ `agent-info.json` and `/threadline/health` now publish a `fingerprint`
22
+ (routable relay address) plus a `publicKey`/`identityPub` set to the SAME
23
+ canonical key, sourced from `IdentityManager.get()` — the exact read-only API
24
+ the relay client uses, so the two can never re-diverge.
25
+
26
+ **Files changed (source):**
27
+ - `src/threadline/AgentDiscovery.ts` — `fingerprint` field on `AgentInfoFile`,
28
+ `ThreadlineAgentInfo`, `HealthResponse`; `announcePresence` accepts +
29
+ persists it; `pingThreadlineHealth`/`verifyAgent` propagate it.
30
+ - `src/threadline/ThreadlineBootstrap.ts` — resolve `IdentityManager.get()` up
31
+ front; advertise canonical `publicKey`(hex)+`fingerprint`+`machine`; omit both
32
+ when `get()` is null (no fabrication).
33
+ - `src/threadline/ThreadlineEndpoints.ts` — `/threadline/health` sources
34
+ `identityPub`+`fingerprint` from `IdentityManager.get()`; `stateDir` added to
35
+ config; falls back to the handshake key + omits fingerprint when no routing
36
+ identity resolves.
37
+ - `src/server/routes.ts` — pass `stateDir` to `createThreadlineRoutes`.
38
+ - `src/core/PostUpdateMigrator.ts` — `migrateThreadlineAgentInfoIdentity`
39
+ (belt-and-suspenders fleet repair) + `migrateClaudeMd` routing-fingerprint
40
+ guidance section.
41
+ - `src/scaffold/templates.ts` — CLAUDE.md template "What address reaches me"
42
+ section (Agent Awareness).
43
+ - `src/data/builtin-manifest.json` — regenerated by the build (template hashes).
44
+
45
+ **Files changed (tests):** `ThreadlineBootstrap.test.ts`,
46
+ `ThreadlineEndpoints.test.ts` (+3 new: migrator unit, health integration,
47
+ bootstrap wiring e2e).
48
+
49
+ **Drive-by (unrelated, green-keeping):** `src/commands/test-as-self.ts` line 60
50
+ — `new URL(import.meta.url).pathname` → `fileURLToPath(import.meta.url)`. This
51
+ is the URL-pathname-path-encoding pattern (breaks when the project path
52
+ contains spaces) that the pre-push gate (`scripts/pre-push-gate.js`) flags; it
53
+ was introduced into `test-as-self.ts` by an earlier merge and the pre-push gate
54
+ blocks any push until it is fixed (the check is pre-push-only, not in CI, which
55
+ is why it landed). One-line fix to the documented-correct pattern; no behavior
56
+ change.
57
+
58
+ ## Blast radius / consumers
59
+
60
+ - **`agent-info.json.publicKey` encoding** is unchanged (hex) and now sourced
61
+ from the canonical key. Consumers (`verifyAgent` does `Buffer.from(.., 'hex')`
62
+ + 32-byte length check) still work; the key is a valid 32-byte Ed25519 key.
63
+ - **`/threadline/health.identityPub`** switches from the handshake key to the
64
+ canonical routing key WHEN one resolves. Safe: `verifyAgent` does not perform
65
+ a real cryptographic challenge (nonce generated, never exchanged), and the E2E
66
+ handshake exchanges keys inline — it does not read `agent-info.json` or the
67
+ health `identityPub`. Verified by reading `ThreadlineEndpoints.ts` handshake
68
+ paths + `AgentDiscovery.verifyAgent`.
69
+ - **No-fabrication boundary:** a relay-disabled / no-identity / locked-encrypted
70
+ agent now OMITS `fingerprint`+`publicKey` instead of advertising a key for an
71
+ identity on no relay. This is a behavior change for brand-new agents on their
72
+ very first announce (before the relay client creates an identity) — correct
73
+ per spec (not relay-discoverable until a routing identity exists), self-heals
74
+ on the next boot once the identity is created.
75
+ - **Migrator** only rewrites `agent-info.json` when `get()` resolves AND the
76
+ file is misaligned; no-ops otherwise (idempotent, atomic write, never
77
+ fabricates).
78
+
79
+ ## Migration parity
80
+
81
+ Existing agents: the in-`announcePresence` rewrite self-heals on the
82
+ post-update server restart; `migrateThreadlineAgentInfoIdentity` closes the
83
+ narrow update-without-restart window; `migrateClaudeMd` adds the guidance for
84
+ agents whose CLAUDE.md predates this. New agents get all of it via `init` +
85
+ the template.
86
+
87
+ ## Out of scope (tracked, not deferred)
88
+
89
+ - Multi-machine same-fingerprint advertisement coordination →
90
+ `project_cross_machine_seamlessness_spec`. This fix sets the `machine` field
91
+ (attributable) and is neutral for multi-machine.
92
+ - Retiring the orphan `identity-keys.json` / collapsing the 3–4 keypairs →
93
+ `bug_threadline_identity_divergence_discovery`. This fix stops *advertising*
94
+ the orphan; deleting it touches the handshake layer and needs its own spec.
95
+
96
+ ## Risk
97
+
98
+ Low. Additive at the field level; `agent-info.json` is regenerated on every
99
+ boot regardless. Reverting removes the field + migrator with no data loss. All
100
+ three test tiers + wiring + both-sides green; full `test:push` suite green.