@agfpd/iapeer-memory 0.1.13 → 0.2.1

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-memory",
3
- "version": "0.1.13",
3
+ "version": "0.2.1",
4
4
  "description": "iapeer-memory — peer memory for the iapeer ecosystem: vault, memoryd (index/search/MCP-http), layer-5 context fragments, role doctrines. The package IS the system; the claude/codex plugins are thin session sockets (docs/10-distribution.md, ADR-009).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@agfpd/iapeer-memory-core": "0.1.13"
30
+ "@agfpd/iapeer-memory-core": "0.2.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.2.0",
package/src/binary.ts CHANGED
@@ -23,7 +23,9 @@
23
23
 
24
24
  import fs from "node:fs";
25
25
  import path from "node:path";
26
+ import type { Egress } from "./egress.js";
26
27
  import { signInstalledBinary, type SigningOutcome } from "./signing.js";
28
+ import { guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
27
29
 
28
30
  export function isCompiledRuntime(): boolean {
29
31
  return import.meta.url.includes("/$bunfs/");
@@ -34,7 +36,10 @@ export type InstallBinaryOutcome =
34
36
  | { action: "skipped-compiled"; outPath: string }
35
37
  | { action: "failed"; outPath: string; detail: string };
36
38
 
37
- export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
39
+ export function installBinary(
40
+ egress: Egress,
41
+ opts: { outPath: string },
42
+ ): InstallBinaryOutcome {
38
43
  const { outPath } = opts;
39
44
  if (isCompiledRuntime()) {
40
45
  return { action: "skipped-compiled", outPath };
@@ -47,20 +52,22 @@ export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
47
52
  `.${path.basename(outPath)}.build.tmp`,
48
53
  );
49
54
 
50
- const proc = Bun.spawnSync(
51
- [process.execPath, "build", "--compile", cliPath, "--outfile", tmp],
52
- { stdout: "pipe", stderr: "pipe" },
53
- );
54
- if (proc.exitCode !== 0 || !fs.existsSync(tmp)) {
55
+ // Self-runtime spawn (egress allowance 2): compiling OUR cli with OUR bun
56
+ // to a path-conventioned target — works in sandboxed e2e by design.
57
+ const proc = egress.spawnSync([
58
+ process.execPath, "build", "--compile", cliPath, "--outfile", tmp,
59
+ ]);
60
+ if (proc.spawnError || proc.exitCode !== 0 || !fs.existsSync(tmp)) {
55
61
  try {
56
- if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
62
+ if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
57
63
  } catch {
58
64
  // best effort
59
65
  }
60
66
  return {
61
67
  action: "failed",
62
68
  outPath,
63
- detail: proc.stderr.toString().trim() || `bun build exited ${proc.exitCode}`,
69
+ detail:
70
+ proc.spawnError || proc.stderr.trim() || `bun build exited ${proc.exitCode}`,
64
71
  };
65
72
  }
66
73
 
@@ -68,12 +75,12 @@ export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
68
75
  fs.renameSync(tmp, outPath); // atomic swap — safe over a running binary on macOS
69
76
  // Stable-identity re-sign on EVERY compile path (TCC grants survive
70
77
  // updates — contract with iapeer, see signing.ts). Soft-fail by design.
71
- const signing = signInstalledBinary(outPath);
78
+ const signing = signInstalledBinary(egress, outPath);
72
79
  return { action: "compiled", outPath, bytes: fs.statSync(outPath).size, signing };
73
80
  }
74
81
 
75
82
  export function removeBinary(outPath: string): "removed" | "absent" {
76
83
  if (!fs.existsSync(outPath)) return "absent";
77
- fs.unlinkSync(outPath);
84
+ guardedUnlinkSync(outPath);
78
85
  return "removed";
79
86
  }
package/src/cli.ts CHANGED
@@ -13,7 +13,9 @@
13
13
  * verify found problems, 2 usage error or not-yet-implemented stage.
14
14
  */
15
15
 
16
+ import { isUnderProdAnchor, sandboxEnvArmed } from "@agfpd/iapeer-memory-core";
16
17
  import { loadConfigFile } from "./config-env.js";
18
+ import { liveEgress } from "./egress.js";
17
19
  import { memoryPaths } from "./paths.js";
18
20
  import { packageVersion } from "./version.js";
19
21
  import { cmdFmUpdate } from "./commands/fm-update.js";
@@ -22,6 +24,7 @@ import { cmdInit } from "./commands/init.js";
22
24
  import { cmdInstallBinary } from "./commands/install-binary.js";
23
25
  import { cmdMemoryd } from "./commands/memoryd.js";
24
26
  import { cmdMigrate } from "./commands/migrate.js";
27
+ import { cmdProvisionPeer, cmdUnprovisionPeer } from "./commands/provision-peer.js";
25
28
  import { cmdRender } from "./commands/render.js";
26
29
  import { cmdStatus } from "./commands/status.js";
27
30
  import { cmdUninstall } from "./commands/uninstall.js";
@@ -47,6 +50,13 @@ Commands:
47
50
  launcher, managed memoryd restart
48
51
  install-binary [--out P] compile the stable CLI binary (~/.local/bin) —
49
52
  init step / repair path; needs package sources
53
+ provision-peer --cwd P --runtime claude|codex --personality NAME [--occasion O]
54
+ merge the direct session surfaces into one peer's
55
+ cwd (claude: hooks/MCP/skills; codex: project MCP;
56
+ idempotent, own keys only); the iapeer core shells
57
+ into this at peer birth
58
+ unprovision-peer --cwd P --runtime claude|codex [--occasion O]
59
+ strip OUR surfaces from one peer's cwd (mirror)
50
60
  fm-update [ops] FILE... structural frontmatter edits + attribution stamp
51
61
  migrate --source DIR move harness auto-memory into the vault
52
62
  (dry-run by default; --apply to execute)
@@ -70,10 +80,26 @@ export async function main(argv: string[]): Promise<number> {
70
80
 
71
81
  // The config file is the env context of every command (except pure
72
82
  // help/version, where a broken file must not block the output).
83
+ // Deny-by-default §7.2 (accepted): under a test sandbox the LIVE host's
84
+ // config.env is never read — it would pull the live vault path, the
85
+ // embedding/reranker endpoints and the production port into a sandboxed
86
+ // process. A test must pass ITS OWN IAPEER_MEMORY_CONFIG_FILE.
73
87
  if (cmd && !["help", "--help", "-h", "version", "--version", "-V"].includes(cmd)) {
74
- loadConfigFile(memoryPaths().configFile);
88
+ const configFile = memoryPaths().configFile;
89
+ if (sandboxEnvArmed() && isUnderProdAnchor(configFile)) {
90
+ console.error(
91
+ `iapeer-memory: live config.env skipped under the test sandbox (${configFile}) — pass IAPEER_MEMORY_CONFIG_FILE`,
92
+ );
93
+ } else {
94
+ loadConfigFile(configFile);
95
+ }
75
96
  }
76
97
 
98
+ // The ONE egress construction point (deny-by-default §4 П2): under a
99
+ // test-sandbox env this is a refusing handle — commands degrade to their
100
+ // SKIP semantics; nothing below re-checks the env.
101
+ const egress = liveEgress();
102
+
77
103
  switch (cmd) {
78
104
  case undefined:
79
105
  case "help":
@@ -87,17 +113,21 @@ export async function main(argv: string[]): Promise<number> {
87
113
  console.log(packageVersion());
88
114
  return 0;
89
115
  case "init":
90
- return cmdInit(rest);
116
+ return cmdInit(rest, egress);
91
117
  case "uninstall":
92
- return cmdUninstall(rest);
118
+ return cmdUninstall(rest, egress);
93
119
  case "status":
94
- return cmdStatus(rest);
120
+ return cmdStatus(rest, egress);
95
121
  case "verify":
96
- return cmdVerify(rest);
122
+ return cmdVerify(rest, egress);
97
123
  case "update":
98
- return cmdUpdate(rest);
124
+ return cmdUpdate(rest, egress);
99
125
  case "install-binary":
100
- return cmdInstallBinary(rest);
126
+ return cmdInstallBinary(rest, egress);
127
+ case "provision-peer":
128
+ return cmdProvisionPeer(rest);
129
+ case "unprovision-peer":
130
+ return cmdUnprovisionPeer(rest);
101
131
  case "fm-update":
102
132
  return cmdFmUpdate(rest);
103
133
  case "migrate":
@@ -107,7 +137,7 @@ export async function main(argv: string[]): Promise<number> {
107
137
  case "memoryd":
108
138
  return cmdMemoryd(rest);
109
139
  case "hook":
110
- return cmdHook(rest);
140
+ return cmdHook(rest, egress);
111
141
  default:
112
142
  console.error(`iapeer-memory: unknown command: ${cmd}\n`);
113
143
  console.error(USAGE);
@@ -32,6 +32,7 @@
32
32
 
33
33
  import fs from "node:fs";
34
34
  import path from "node:path";
35
+ import type { Egress } from "../egress.js";
35
36
  import {
36
37
  DEFAULT_CURATOR_SET,
37
38
  fmUpdate,
@@ -41,6 +42,7 @@ import {
41
42
  } from "@agfpd/iapeer-memory-core";
42
43
  import { memoryPaths, type MemoryPaths } from "../paths.js";
43
44
  import { DEFAULT_HEARTBEAT_STALE_MS } from "./verify.js";
45
+ import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
44
46
 
45
47
  /** Tools whose writes stamp frontmatter. P5 adds "apply_patch" (codex). */
46
48
  export const POST_WRITE_TOOLS: ReadonlySet<string> = new Set([
@@ -193,7 +195,7 @@ export function runSessionStart(opts: {
193
195
  if (!recentKick) {
194
196
  try {
195
197
  fs.mkdirSync(paths.stateDir, { recursive: true });
196
- fs.writeFileSync(stamp, `${new Date(nowMs).toISOString()}\n`);
198
+ guardedWriteFileSync(stamp, `${new Date(nowMs).toISOString()}\n`);
197
199
  opts.kick?.();
198
200
  kicked = true;
199
201
  } catch {
@@ -227,7 +229,7 @@ function logHookError(err: unknown): void {
227
229
  }
228
230
  }
229
231
 
230
- export async function cmdHook(argv: string[]): Promise<number> {
232
+ export async function cmdHook(argv: string[], egress: Egress): Promise<number> {
231
233
  const [event] = argv;
232
234
  try {
233
235
  switch (event) {
@@ -239,13 +241,10 @@ export async function cmdHook(argv: string[]): Promise<number> {
239
241
  case "session-start": {
240
242
  const result = runSessionStart({
241
243
  kick: () => {
244
+ // Self-runtime detached spawn (egress allowance 2): the child
245
+ // re-enters main() with its own egress — sandbox env inherits.
242
246
  const cli = new URL("../cli.ts", import.meta.url).pathname;
243
- const proc = Bun.spawn([process.execPath, cli, "verify", "--repair"], {
244
- stdout: "ignore",
245
- stderr: "ignore",
246
- stdin: "ignore",
247
- });
248
- proc.unref();
247
+ egress.spawnDetached([process.execPath, cli, "verify", "--repair"]);
249
248
  },
250
249
  });
251
250
  if (result.output) console.log(result.output);
@@ -13,10 +13,12 @@
13
13
  * (sent only when exactly one natural peer exists in the registry).
14
14
  *
15
15
  * Step order: deps → vault → config → binary → templates → role peers +
16
- * doctrines + roles manifest → watcher registrationslot declaration
17
- * native-memory sweep (core verb, soft-skip on old cores) host-wide
18
- * guide fragment. Ecosystem steps are skippable (--skip-ecosystem) for
19
- * sandboxed runs; the binary compile is skippable (--skip-binary) for
16
+ * doctrines + roles manifest → fleet mapwatcher registration direct
17
+ * session surfaces sweep (ADR-009 v1.2) legacy v1.1 plugin off (while the
18
+ * old declaration is still readable) slot declaration (v1.2 provision
19
+ * command) native-memory sweep (core verb, soft-skip on old cores)
20
+ * host-wide guide fragment. Ecosystem steps are skippable (--skip-ecosystem)
21
+ * for sandboxed runs; the binary compile is skippable (--skip-binary) for
20
22
  * fast tests.
21
23
  */
22
24
 
@@ -30,11 +32,15 @@ import {
30
32
  type LocaleId,
31
33
  } from "@agfpd/iapeer-memory-core";
32
34
  import { installBinary } from "../binary.js";
35
+ import { IAPEER_BIN, type Egress } from "../egress.js";
33
36
  import { memoryPaths } from "../paths.js";
34
37
  import { provisionVault, writeDefaultConfig } from "../provision.js";
35
38
  import { writeRolesManifest, type RoleEntry } from "../roles.js";
36
- import { applyMemoryPlugin, writeSlot } from "../slot.js";
37
- import { writeFleetMap } from "../fleet.js";
39
+ import { applyMemoryPlugin, readSlot, writeSlot, SLOT_PROVIDER } from "../slot.js";
40
+ import { readFleetMap, writeFleetMap } from "../fleet.js";
41
+ import { withProvisionLock } from "../surfaces/lock.js";
42
+ import { sweepProvision } from "../surfaces/sweep.js";
43
+ import { mcpPort } from "./provision-peer.js";
38
44
  import {
39
45
  doctrineOwnership,
40
46
  guideText,
@@ -68,7 +74,9 @@ type InitFlags = {
68
74
  * the fleet may still carry the predecessor's guide; ours lands by a separate
69
75
  * decision after the plugin swap). */
70
76
  skipGuide: boolean;
71
- iapeerBin: string;
77
+ /** Explicitly named core binary (--iapeer-bin) — undefined means the PATH
78
+ * default; the distinction feeds the egress explicit-bin allowance. */
79
+ iapeerBin?: string;
72
80
  };
73
81
 
74
82
  function parseFlags(argv: string[]): InitFlags | null {
@@ -78,7 +86,6 @@ function parseFlags(argv: string[]): InitFlags | null {
78
86
  skipEcosystem: false,
79
87
  skipBinary: false,
80
88
  skipGuide: false,
81
- iapeerBin: "iapeer",
82
89
  };
83
90
  for (let i = 0; i < argv.length; i++) {
84
91
  const a = argv[i];
@@ -94,7 +101,7 @@ function parseFlags(argv: string[]): InitFlags | null {
94
101
  case "--skip-ecosystem": f.skipEcosystem = true; break;
95
102
  case "--skip-binary": f.skipBinary = true; break;
96
103
  case "--skip-guide": f.skipGuide = true; break;
97
- case "--iapeer-bin": f.iapeerBin = take() ?? "iapeer"; break;
104
+ case "--iapeer-bin": f.iapeerBin = take(); break;
98
105
  default:
99
106
  console.error(`iapeer-memory init: unknown flag: ${a}`);
100
107
  return null;
@@ -109,22 +116,23 @@ type PeerInfo = { personality: string; intelligence?: string; cwd?: string };
109
116
 
110
117
  type RunResult = { exitCode: number; stdout: string; stderr: string };
111
118
 
112
- /** spawnSync that never throws — a missing binary is a result, not a crash. */
113
- function run(cmd: string[]): RunResult {
114
- try {
115
- const proc = Bun.spawnSync(cmd, { stdout: "pipe", stderr: "pipe" });
116
- return {
117
- exitCode: proc.exitCode,
118
- stdout: proc.stdout.toString(),
119
- stderr: proc.stderr.toString(),
120
- };
121
- } catch (err) {
122
- return { exitCode: 127, stdout: "", stderr: String(err) };
123
- }
119
+ /** Egress spawn that never throws — a missing binary (and a refusing test
120
+ * egress) is a result, not a crash: the ecosystem half degrades to the
121
+ * same skip path as «iapeer not on this host». */
122
+ function run(
123
+ egress: Egress,
124
+ cmd: string[],
125
+ opts?: { explicitBin?: boolean },
126
+ ): RunResult {
127
+ const proc = egress.spawnSync(cmd, { explicitBin: opts?.explicitBin });
128
+ if (proc.spawnError) return { exitCode: 127, stdout: "", stderr: proc.spawnError };
129
+ return { exitCode: proc.exitCode, stdout: proc.stdout, stderr: proc.stderr };
124
130
  }
125
131
 
126
- function listPeers(iapeerBin: string): PeerInfo[] | null {
127
- const proc = run([iapeerBin, "list", "--json"]);
132
+ function listPeers(egress: Egress, iapeerBin?: string): PeerInfo[] | null {
133
+ const proc = run(egress, [iapeerBin ?? IAPEER_BIN, "list", "--json"], {
134
+ explicitBin: iapeerBin !== undefined,
135
+ });
128
136
  if (proc.exitCode !== 0) return null;
129
137
  try {
130
138
  return JSON.parse(proc.stdout) as PeerInfo[];
@@ -150,7 +158,7 @@ function ask(question: string, fallback: string): string {
150
158
 
151
159
  // ── the command ──────────────────────────────────────────────────────────────
152
160
 
153
- export async function cmdInit(argv: string[]): Promise<number> {
161
+ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
154
162
  const flags = parseFlags(argv);
155
163
  if (!flags) return 2;
156
164
 
@@ -164,7 +172,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
164
172
  let embeddingEndpoint = flags.embeddingEndpoint ?? "";
165
173
  const rerankerEndpoint = flags.rerankerEndpoint ?? "";
166
174
 
167
- const peers = flags.skipEcosystem ? null : listPeers(flags.iapeerBin);
175
+ const peers = flags.skipEcosystem ? null : listPeers(egress, flags.iapeerBin);
168
176
  const humanDefault = flags.human ?? naturalPeerDefault(peers) ?? "";
169
177
 
170
178
  if (!vault) {
@@ -210,7 +218,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
210
218
  if (flags.skipDeps) {
211
219
  step("deps", "skipped (--skip-deps)");
212
220
  } else {
213
- const ver = run([flags.iapeerBin, "version"]);
221
+ const ver = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "version"], { explicitBin: flags.iapeerBin !== undefined });
214
222
  if (ver.exitCode !== 0) {
215
223
  step("deps", "iapeer foundation not found on PATH — install it first (npx @agfpd/iapeer)", false);
216
224
  } else {
@@ -254,7 +262,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
254
262
  if (flags.skipBinary) {
255
263
  step("binary", "skipped (--skip-binary)");
256
264
  } else {
257
- const bin = installBinary({ outPath: paths.binaryPath });
265
+ const bin = installBinary(egress, { outPath: paths.binaryPath });
258
266
  step(
259
267
  "binary",
260
268
  bin.action === "compiled"
@@ -285,7 +293,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
285
293
  const personality = rolePersonality(role);
286
294
  const exists = (peers ?? []).some((p) => p.personality === personality);
287
295
  if (!exists) {
288
- const created = run([flags.iapeerBin, "create", personality]);
296
+ const created = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "create", personality], { explicitBin: flags.iapeerBin !== undefined });
289
297
  if (created.exitCode !== 0) {
290
298
  rolesOk = false;
291
299
  console.log(` roles create ${personality} failed: ${created.stderr.trim()}`);
@@ -298,7 +306,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
298
306
  // `iapeer list --json` — iapeer 0.2.14), otherwise the core's
299
307
  // DOCUMENTED create default (no --path — требование Артура;
300
308
  // IAPEER_ROOT-aware).
301
- const freshPeers = createdAny ? listPeers(flags.iapeerBin) : peers;
309
+ const freshPeers = createdAny ? listPeers(egress, flags.iapeerBin) : peers;
302
310
  for (const role of ROLE_NAMES) {
303
311
  const personality = rolePersonality(role);
304
312
  const registryCwd = (freshPeers ?? []).find((p) => p.personality === personality)?.cwd;
@@ -349,7 +357,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
349
357
  if (flags.skipEcosystem) {
350
358
  step("fleet", "skipped (--skip-ecosystem)");
351
359
  } else {
352
- const fleet = writeFleetMap({
360
+ const fleet = writeFleetMap(egress, {
353
361
  fleetMapPath: paths.fleetMapPath,
354
362
  iapeerBin: flags.iapeerBin,
355
363
  });
@@ -371,7 +379,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
371
379
  step("timers", "skipped (--skip-ecosystem)");
372
380
  } else {
373
381
  writeLauncherScript({ launcherPath: paths.launcherPath, binaryPath: paths.binaryPath });
374
- const sent = registerWatcher({
382
+ const sent = registerWatcher(egress, {
375
383
  launcherPath: paths.launcherPath,
376
384
  iapeerBin: flags.iapeerBin,
377
385
  });
@@ -390,11 +398,11 @@ export async function cmdInit(argv: string[]): Promise<number> {
390
398
  vaultPath: vault,
391
399
  inboxFolders: [getTaxonomy(locale).folders.inbox, getTaxonomy(locale).folders.inboxHuman],
392
400
  });
393
- const sweep = registerTimer({
401
+ const sweep = registerTimer(egress, {
394
402
  message: sweepTimerMessage({ checkScriptPath: paths.checkScriptPath }),
395
403
  iapeerBin: flags.iapeerBin,
396
404
  });
397
- const dream = registerTimer({
405
+ const dream = registerTimer(egress, {
398
406
  message: dreamTimerMessage(),
399
407
  iapeerBin: flags.iapeerBin,
400
408
  });
@@ -410,39 +418,101 @@ export async function cmdInit(argv: string[]): Promise<number> {
410
418
  );
411
419
  }
412
420
 
413
- // 8. slot declaration (the contract: written by the provider, atomic)
414
- // v1.1: includes the plugin block the core derives installs from.
415
- const slot = writeSlot({
416
- slotPath: paths.slotPath,
417
- version,
418
- heartbeat: paths.heartbeatPath,
419
- });
420
- step(
421
- "slot",
422
- slot.action === "refused-foreign"
423
- ? `slot held by foreign provider "${slot.existing?.provider}" — uninstall it first`
424
- : `${paths.slotPath} (${slot.action}, v${version})`,
425
- slot.action !== "refused-foreign",
426
- );
427
-
428
- // 8b. session plugin across the fleet — the core verb derives the plugin
429
- // from the block JUST written (order matters), installs per-peer on claude
430
- // and host-globally on codex; new peers get it from the core's birth-hook.
431
- // Soft-skip on an older core (verb landed in iapeer 0.2.25).
432
- if (flags.skipEcosystem) {
433
- step("plugin", "skipped (--skip-ecosystem)");
434
- } else if (slot.action === "refused-foreign") {
435
- step("plugin", "skipped (slot refused — nothing to derive the plugin from)");
421
+ // 8. slot + surfaces + v1.1 migration ORDER MATTERS (ADR-009 v1.2):
422
+ // 8a. a FOREIGN slot refuses the whole block (never lay surfaces over
423
+ // another provider's host);
424
+ // 8b. direct surfaces sweep across the existing fleet (the new channel
425
+ // must be in place BEFORE the old one is stripped);
426
+ // 8c. legacy plugin off — while the v1.1 slot is STILL on disk (the
427
+ // core verb derives the plugin identity from the live declaration;
428
+ // overwriting first would leave it nothing to derive from);
429
+ // 8d. slot declaration re-written in the v1.2 form (provision command,
430
+ // no plugin block). Newborns are then the core birth-hook's duty.
431
+ const existingSlot = readSlot(paths.slotPath);
432
+ const slotForeign = existingSlot !== null && existingSlot.provider !== SLOT_PROVIDER;
433
+ if (slotForeign) {
434
+ step("slot", `slot held by foreign provider "${existingSlot?.provider}" — uninstall it first`, false);
435
+ step("surfaces", "skipped (foreign slot — not our host)");
436
436
  } else {
437
- const plug = applyMemoryPlugin({ mode: "on", iapeerBin: flags.iapeerBin });
438
- step(
439
- "plugin",
440
- plug.suppressed
441
- ? "skipped (test sandbox — core calls suppressed)"
442
- : plug.ok
443
- ? "session plugin rolled out across the fleet (iapeer memory-plugin on --all)"
444
- : `soft-skip: ${plug.detail.slice(0, 160) || "verb failed"} — upgrade the iapeer core (≥0.2.25) or run manually: iapeer memory-plugin on --all`,
445
- );
437
+ // 8b. direct session surfaces across the EXISTING fleet — the package's
438
+ // own rail over the fleet map written in 6b.
439
+ let surfacesOk = false;
440
+ if (flags.skipEcosystem) {
441
+ step("surfaces", "skipped (--skip-ecosystem)");
442
+ surfacesOk = true; // sandboxed run — don't block the slot/migration steps
443
+ } else {
444
+ const fleet = readFleetMap(paths.fleetMapPath) ?? [];
445
+ const locked = withProvisionLock({
446
+ stateDir: paths.stateDir,
447
+ fn: () => sweepProvision({ fleet, hooksDir: paths.hooksDir, port: mcpPort() }),
448
+ });
449
+ if (!locked.acquired) {
450
+ step("surfaces", locked.detail, false);
451
+ } else {
452
+ const { results, skipped } = locked.result;
453
+ const failed = results.filter((r) => !r.ok);
454
+ surfacesOk = failed.length === 0;
455
+ step(
456
+ "surfaces",
457
+ `${results.length - failed.length}/${results.length} peer-runtime(s) provisioned` +
458
+ (skipped.length ? `, ${skipped.length} skipped (no session runtime / missing cwd)` : "") +
459
+ " — live sessions pick them up on next restart",
460
+ surfacesOk,
461
+ );
462
+ for (const f of failed) {
463
+ console.log(
464
+ ` surfaces FAIL ${f.personality}:${f.runtime} — ${f.outcomes
465
+ .filter((o) => o.action === "failed")
466
+ .map((o) => `${o.surface}: ${o.detail ?? "failed"}`)
467
+ .join("; ")}`,
468
+ );
469
+ }
470
+ }
471
+ }
472
+
473
+ // 8c. v1.1 → v1.2 migration: sweep the legacy session plugin off the
474
+ // fleet while the old declaration is STILL readable — and ONLY after
475
+ // the direct surfaces landed cleanly.
476
+ let migrationBlocked = false;
477
+ if (!flags.skipEcosystem && existingSlot?.plugin) {
478
+ if (!surfacesOk) {
479
+ migrationBlocked = true;
480
+ step(
481
+ "plugin-off",
482
+ "POSTPONED: direct surfaces did not land cleanly — legacy plugin and v1.1 slot kept (re-run init after fixing)",
483
+ false,
484
+ );
485
+ } else {
486
+ const off = applyMemoryPlugin(egress, { mode: "off", iapeerBin: flags.iapeerBin });
487
+ step(
488
+ "plugin-off",
489
+ off.suppressed
490
+ ? "skipped (test sandbox — core calls suppressed)"
491
+ : off.ok
492
+ ? "legacy v1.1 session plugin swept off the fleet (memory-plugin off --all)"
493
+ : `legacy plugin off failed (${off.detail.slice(0, 120)}) — manual: iapeer memory-plugin off --all (or per peer: claude plugin uninstall iapeer-memory@agfpd --scope project)`,
494
+ );
495
+ }
496
+ }
497
+
498
+ // 8d. slot declaration (atomic; provider-owned). Kept in the v1.1 form
499
+ // while the migration is blocked — the legacy plugin channel stays
500
+ // derivable until the new channel lands.
501
+ if (migrationBlocked) {
502
+ step("slot", "kept v1.1 declaration (migration postponed — see plugin-off)", false);
503
+ } else {
504
+ const slot = writeSlot({
505
+ slotPath: paths.slotPath,
506
+ version,
507
+ binaryPath: paths.binaryPath,
508
+ heartbeat: paths.heartbeatPath,
509
+ });
510
+ step(
511
+ "slot",
512
+ `${paths.slotPath} (${slot.action}, v${version}, provision-command declared)`,
513
+ slot.action !== "refused-foreign",
514
+ );
515
+ }
446
516
  }
447
517
 
448
518
  // 9. native-memory sweep — the core's lever (one home of runtime forms);
@@ -450,7 +520,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
450
520
  if (flags.skipEcosystem) {
451
521
  step("sweep", "skipped (--skip-ecosystem)");
452
522
  } else {
453
- const sweep = run([flags.iapeerBin, "native-memory", "off", "--all"]);
523
+ const sweep = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "native-memory", "off", "--all"], { explicitBin: flags.iapeerBin !== undefined });
454
524
  // One line per peer-runtime — summarise ALL peers, not the last line
455
525
  // (e2e §A finding: ".pop()" named one peer while three were swept).
456
526
  const sweepLines = sweep.stdout.trim().split("\n").filter(Boolean);
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { installBinary } from "../binary.js";
9
+ import type { Egress } from "../egress.js";
9
10
  import { memoryPaths } from "../paths.js";
10
11
 
11
- export function cmdInstallBinary(argv: string[]): number {
12
+ export function cmdInstallBinary(argv: string[], egress: Egress): number {
12
13
  let outPath = memoryPaths().binaryPath;
13
14
  for (let i = 0; i < argv.length; i++) {
14
15
  if (argv[i] === "--out") {
@@ -24,7 +25,7 @@ export function cmdInstallBinary(argv: string[]): number {
24
25
  }
25
26
  }
26
27
 
27
- const outcome = installBinary({ outPath });
28
+ const outcome = installBinary(egress, { outPath });
28
29
  switch (outcome.action) {
29
30
  case "compiled":
30
31
  console.log(
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import { configFromEnv, startMemoryd } from "@agfpd/iapeer-memory-core";
21
21
  import { authorIndexPath, memoryPaths } from "../paths.js";
22
+ import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
22
23
 
23
24
  export async function cmdMemoryd(argv: string[]): Promise<number> {
24
25
  let mcpPort: number | undefined;
@@ -56,7 +57,7 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
56
57
  }
57
58
  // pid file — uninstall's stop handle (best-effort; stale files are
58
59
  // harmless: the reader checks liveness before signalling).
59
- fs.writeFileSync(paths.pidPath, `${process.pid}\n`);
60
+ guardedWriteFileSync(paths.pidPath, `${process.pid}\n`);
60
61
 
61
62
  const freshEditWindowRaw = process.env.IAPEER_MEMORY_FRESH_EDIT_WINDOW_S;
62
63
  const freshEditWindowS =
@@ -101,7 +102,7 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
101
102
  .close()
102
103
  .then(() => {
103
104
  try {
104
- fs.unlinkSync(paths.pidPath);
105
+ guardedUnlinkSync(paths.pidPath);
105
106
  } catch {
106
107
  // best effort
107
108
  }