@agfpd/iapeer 0.4.112 → 0.4.114

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.112",
3
+ "version": "0.4.114",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
package/src/cli/index.ts CHANGED
@@ -450,7 +450,7 @@ export async function addRuntime(
450
450
  delete cleanEnv.PEER_PERSONALITY
451
451
  delete cleanEnv.PEER_RUNTIME
452
452
  delete cleanEnv.PEER_IDENTITY
453
- const { initPeer } = await import('../init/index.ts')
453
+ const { initPeer, hasClaudeMcpWiring } = await import('../init/index.ts')
454
454
  const out: AddRuntimeOutcome[] = []
455
455
  for (const p of targets) {
456
456
  // Skip an infra-DEFAULT peer only when the runtime being added is AGENTIC: giving a
@@ -467,9 +467,12 @@ export async function addRuntime(
467
467
  // Short-circuiting on the declaration alone would report "already" while the channel
468
468
  // does not exist — so for infra, converge: a missing plist means there IS work to do.
469
469
  // initPeer is idempotent, so the repair costs nothing when everything is in place.
470
+ // Same for claude: declared on a codex-born peer ≠ its `.mcp.json` wiring exists.
470
471
  const realized = isInfraRuntime(runtime)
471
472
  ? existsSync(resolveAlwaysOnTarget(p.personality, runtime as Runtime, env).path)
472
- : true
473
+ : runtime === 'claude'
474
+ ? hasClaudeMcpWiring(p.cwd)
475
+ : true
473
476
  if (p.runtimes.includes(runtime as Runtime) && realized) {
474
477
  out.push({ personality: p.personality, action: 'already' })
475
478
  continue
@@ -481,7 +484,10 @@ export async function addRuntime(
481
484
  out.push({
482
485
  personality: p.personality,
483
486
  action: 'added',
484
- detail: [repair ? 'declared but no plist — channel re-provisioned' : undefined, ...warns].filter(Boolean).join(' | ') || undefined,
487
+ detail:
488
+ [repair ? (isInfraRuntime(runtime) ? 'declared but no plist — channel re-provisioned' : 'declared but not wired — runtime re-provisioned') : undefined, ...warns]
489
+ .filter(Boolean)
490
+ .join(' | ') || undefined,
485
491
  })
486
492
  } catch (e) {
487
493
  out.push({ personality: p.personality, action: 'failed', detail: e instanceof Error ? e.message : String(e) })
@@ -524,7 +530,14 @@ export async function defaultRuntime(
524
530
  const index = readPeersIndex({ env })
525
531
  const targets = opts.all === true ? index.peers : index.peers.filter(p => p.personality === opts.peer)
526
532
  if (targets.length === 0) throw new Error(opts.peer ? `peer "${opts.peer}" is not registered` : 'no targets — pass --peer <p> or --all')
533
+ // Cross-peer: strip the caller's identity env (same reason as add-runtime).
534
+ const cleanEnv: NodeJS.ProcessEnv = { ...env }
535
+ delete cleanEnv.PEER_PERSONALITY
536
+ delete cleanEnv.PEER_RUNTIME
537
+ delete cleanEnv.PEER_IDENTITY
538
+ const { initPeer, hasClaudeMcpWiring } = await import('../init/index.ts')
527
539
  const out: DefaultRuntimeOutcome[] = []
540
+ let rewired = false
528
541
  for (const p of targets) {
529
542
  if (isInfraRuntime(p.runtime)) {
530
543
  out.push({ personality: p.personality, action: 'skipped-infra-peer', detail: `default runtime "${p.runtime}" is infra` })
@@ -536,10 +549,6 @@ export async function defaultRuntime(
536
549
  out.push({ personality: p.personality, action: 'failed', detail: `no local profile at ${p.cwd}` })
537
550
  continue
538
551
  }
539
- if (profile.runtime === runtime) {
540
- out.push({ personality: p.personality, action: 'already' })
541
- continue
542
- }
543
552
  if (!profile.runtimes.includes(runtime as Runtime)) {
544
553
  out.push({
545
554
  personality: p.personality,
@@ -548,15 +557,35 @@ export async function defaultRuntime(
548
557
  })
549
558
  continue
550
559
  }
551
- writePeerProfileAtomic(p.cwd, { ...profile, runtime: runtime as Runtime })
552
- out.push({ personality: p.personality, action: 'flipped' })
560
+ // DECLARED ≠ REALIZED (live 2026-10-17: the fleet flipped codex→claude onto peers
561
+ // that declared claude but were never wired — no `.mcp.json` iapeer server, no
562
+ // memory-slot surfaces → claude sessions without send_to_peer). Routing must never
563
+ // sit on an unwired runtime: converge FIRST via initPeer (idempotent: MCP wiring +
564
+ // slot provision for every declared agentic runtime) — also for an ALREADY-flipped
565
+ // peer, so re-running the verb is the repair for a fleet flipped by an older binary.
566
+ let detail: string | undefined
567
+ if (runtime === 'claude' && !hasClaudeMcpWiring(p.cwd)) {
568
+ const warns: string[] = []
569
+ await initPeer({ cwd: p.cwd, runtime: runtime as Runtime, env: cleanEnv, warn: m => warns.push(m) })
570
+ detail = ['declared but not wired — runtime re-provisioned', ...warns].join(' | ')
571
+ rewired = true
572
+ }
573
+ if (profile.runtime === runtime) {
574
+ out.push({ personality: p.personality, action: 'already', detail })
575
+ continue
576
+ }
577
+ // Re-read: initPeer's merge-writer may have touched the profile.
578
+ writePeerProfileAtomic(p.cwd, { ...(readPeerProfile(p.cwd) ?? profile), runtime: runtime as Runtime })
579
+ out.push({ personality: p.personality, action: 'flipped', detail })
553
580
  } catch (e) {
554
581
  out.push({ personality: p.personality, action: 'failed', detail: e instanceof Error ? e.message : String(e) })
555
582
  }
556
583
  }
557
584
  // Self-heal the registry from the flipped locals so routing flips in the same
558
585
  // command (REPLACE projection — the verify --fix rail).
559
- if (out.some(o => o.action === 'flipped')) await reindexFromLocals({ env })
586
+ // initPeer's upsert sets the registry default to the provisioned runtime — reindex
587
+ // after a rewire too, so an `already` peer's registry stays projected from its local.
588
+ if (rewired || out.some(o => o.action === 'flipped')) await reindexFromLocals({ env })
560
589
  return out
561
590
  }
562
591
 
package/src/init/index.ts CHANGED
@@ -599,6 +599,18 @@ export async function initPeer(opts: InitPeerOptions): Promise<InitPeerResult> {
599
599
  function hasClaudeMarker(cwd: string): boolean {
600
600
  return existsSync(join(cwd, '.claude'))
601
601
  }
602
+ /** Claude side REALIZED: the cwd's `.mcp.json` carries the foundation daemon server.
603
+ * A peer born codex-primary declares claude but never got this wiring (init wires
604
+ * claude only for a claude primary / `.claude` marker) → a claude session without
605
+ * send_to_peer (live, fleet switch 2026-10-17). */
606
+ export function hasClaudeMcpWiring(cwd: string): boolean {
607
+ try {
608
+ const doc = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8')) as { mcpServers?: Record<string, unknown> }
609
+ return Boolean(doc.mcpServers?.[IAPEER_MCP_SERVER_NAME])
610
+ } catch {
611
+ return false
612
+ }
613
+ }
602
614
  function hasCodexMarker(cwd: string): boolean {
603
615
  return existsSync(join(cwd, '.codex'))
604
616
  }