@agfpd/iapeer-memory 0.1.12 → 0.2.0
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 +2 -2
- package/src/cli.ts +12 -0
- package/src/commands/init.ts +129 -38
- package/src/commands/memoryd.ts +18 -1
- package/src/commands/provision-peer.ts +172 -0
- package/src/commands/render.ts +8 -1
- package/src/commands/uninstall.ts +60 -23
- package/src/commands/update.ts +138 -32
- package/src/commands/verify.ts +147 -4
- package/src/fleet.ts +150 -0
- package/src/paths.ts +10 -0
- package/src/slot.ts +67 -22
- package/src/surfaces/claude.ts +494 -0
- package/src/surfaces/codex.ts +155 -0
- package/src/surfaces/lock.ts +72 -0
- package/src/surfaces/sweep.ts +170 -0
- package/src/templates/guide-en.ts +1 -1
- package/src/templates/guide-ru.ts +1 -1
- package/src/templates/index.ts +11 -2
- package/src/templates/skills.ts +196 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
30
|
+
"@agfpd/iapeer-memory-core": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "^1.2.0",
|
package/src/cli.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { cmdInit } from "./commands/init.js";
|
|
|
22
22
|
import { cmdInstallBinary } from "./commands/install-binary.js";
|
|
23
23
|
import { cmdMemoryd } from "./commands/memoryd.js";
|
|
24
24
|
import { cmdMigrate } from "./commands/migrate.js";
|
|
25
|
+
import { cmdProvisionPeer, cmdUnprovisionPeer } from "./commands/provision-peer.js";
|
|
25
26
|
import { cmdRender } from "./commands/render.js";
|
|
26
27
|
import { cmdStatus } from "./commands/status.js";
|
|
27
28
|
import { cmdUninstall } from "./commands/uninstall.js";
|
|
@@ -47,6 +48,13 @@ Commands:
|
|
|
47
48
|
launcher, managed memoryd restart
|
|
48
49
|
install-binary [--out P] compile the stable CLI binary (~/.local/bin) —
|
|
49
50
|
init step / repair path; needs package sources
|
|
51
|
+
provision-peer --cwd P --runtime claude|codex --personality NAME [--occasion O]
|
|
52
|
+
merge the direct session surfaces into one peer's
|
|
53
|
+
cwd (claude: hooks/MCP/skills; codex: project MCP;
|
|
54
|
+
idempotent, own keys only); the iapeer core shells
|
|
55
|
+
into this at peer birth
|
|
56
|
+
unprovision-peer --cwd P --runtime claude|codex [--occasion O]
|
|
57
|
+
strip OUR surfaces from one peer's cwd (mirror)
|
|
50
58
|
fm-update [ops] FILE... structural frontmatter edits + attribution stamp
|
|
51
59
|
migrate --source DIR move harness auto-memory into the vault
|
|
52
60
|
(dry-run by default; --apply to execute)
|
|
@@ -98,6 +106,10 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
98
106
|
return cmdUpdate(rest);
|
|
99
107
|
case "install-binary":
|
|
100
108
|
return cmdInstallBinary(rest);
|
|
109
|
+
case "provision-peer":
|
|
110
|
+
return cmdProvisionPeer(rest);
|
|
111
|
+
case "unprovision-peer":
|
|
112
|
+
return cmdUnprovisionPeer(rest);
|
|
101
113
|
case "fm-update":
|
|
102
114
|
return cmdFmUpdate(rest);
|
|
103
115
|
case "migrate":
|
package/src/commands/init.ts
CHANGED
|
@@ -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 →
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* doctrines + roles manifest → fleet map → watcher 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
|
|
|
@@ -33,7 +35,11 @@ import { installBinary } from "../binary.js";
|
|
|
33
35
|
import { memoryPaths } from "../paths.js";
|
|
34
36
|
import { provisionVault, writeDefaultConfig } from "../provision.js";
|
|
35
37
|
import { writeRolesManifest, type RoleEntry } from "../roles.js";
|
|
36
|
-
import { applyMemoryPlugin, writeSlot } from "../slot.js";
|
|
38
|
+
import { applyMemoryPlugin, readSlot, writeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
39
|
+
import { readFleetMap, writeFleetMap } from "../fleet.js";
|
|
40
|
+
import { withProvisionLock } from "../surfaces/lock.js";
|
|
41
|
+
import { sweepProvision } from "../surfaces/sweep.js";
|
|
42
|
+
import { mcpPort } from "./provision-peer.js";
|
|
37
43
|
import {
|
|
38
44
|
doctrineOwnership,
|
|
39
45
|
guideText,
|
|
@@ -340,6 +346,27 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
340
346
|
);
|
|
341
347
|
}
|
|
342
348
|
|
|
349
|
+
// 6b. fleet map — personality → cwd for memoryd's fragment renderer
|
|
350
|
+
// (docs/05; дыра 10.06: без карты пиры не получали paths-блок и индекс).
|
|
351
|
+
// ПЕРЕД watcher-регистрацией: memoryd, поднятый notifier'ом, рендерит
|
|
352
|
+
// весь флот уже на старте. Roles-степ выше уже создал ролевых пиров —
|
|
353
|
+
// карта включает их.
|
|
354
|
+
if (flags.skipEcosystem) {
|
|
355
|
+
step("fleet", "skipped (--skip-ecosystem)");
|
|
356
|
+
} else {
|
|
357
|
+
const fleet = writeFleetMap({
|
|
358
|
+
fleetMapPath: paths.fleetMapPath,
|
|
359
|
+
iapeerBin: flags.iapeerBin,
|
|
360
|
+
});
|
|
361
|
+
step(
|
|
362
|
+
"fleet",
|
|
363
|
+
fleet.action === "written"
|
|
364
|
+
? fleet.detail
|
|
365
|
+
: `fleet map not written (${fleet.detail}) — fragments stay off until verify --repair`,
|
|
366
|
+
fleet.action === "written",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
// 7. notifier wiring (ADR-015, инверсия): the EVENT trigger targets the
|
|
344
371
|
// SCRIBER (first receiver); two TIMERS target the index — weekly
|
|
345
372
|
// DREAM_TICK and the check-gated fail-open sweep. Registrant = index for
|
|
@@ -388,39 +415,101 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
388
415
|
);
|
|
389
416
|
}
|
|
390
417
|
|
|
391
|
-
// 8. slot
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// 8b. session plugin across the fleet — the core verb derives the plugin
|
|
407
|
-
// from the block JUST written (order matters), installs per-peer on claude
|
|
408
|
-
// and host-globally on codex; new peers get it from the core's birth-hook.
|
|
409
|
-
// Soft-skip on an older core (verb landed in iapeer 0.2.25).
|
|
410
|
-
if (flags.skipEcosystem) {
|
|
411
|
-
step("plugin", "skipped (--skip-ecosystem)");
|
|
412
|
-
} else if (slot.action === "refused-foreign") {
|
|
413
|
-
step("plugin", "skipped (slot refused — nothing to derive the plugin from)");
|
|
418
|
+
// 8. slot + surfaces + v1.1 migration — ORDER MATTERS (ADR-009 v1.2):
|
|
419
|
+
// 8a. a FOREIGN slot refuses the whole block (never lay surfaces over
|
|
420
|
+
// another provider's host);
|
|
421
|
+
// 8b. direct surfaces sweep across the existing fleet (the new channel
|
|
422
|
+
// must be in place BEFORE the old one is stripped);
|
|
423
|
+
// 8c. legacy plugin off — while the v1.1 slot is STILL on disk (the
|
|
424
|
+
// core verb derives the plugin identity from the live declaration;
|
|
425
|
+
// overwriting first would leave it nothing to derive from);
|
|
426
|
+
// 8d. slot declaration re-written in the v1.2 form (provision command,
|
|
427
|
+
// no plugin block). Newborns are then the core birth-hook's duty.
|
|
428
|
+
const existingSlot = readSlot(paths.slotPath);
|
|
429
|
+
const slotForeign = existingSlot !== null && existingSlot.provider !== SLOT_PROVIDER;
|
|
430
|
+
if (slotForeign) {
|
|
431
|
+
step("slot", `slot held by foreign provider "${existingSlot?.provider}" — uninstall it first`, false);
|
|
432
|
+
step("surfaces", "skipped (foreign slot — not our host)");
|
|
414
433
|
} else {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
434
|
+
// 8b. direct session surfaces across the EXISTING fleet — the package's
|
|
435
|
+
// own rail over the fleet map written in 6b.
|
|
436
|
+
let surfacesOk = false;
|
|
437
|
+
if (flags.skipEcosystem) {
|
|
438
|
+
step("surfaces", "skipped (--skip-ecosystem)");
|
|
439
|
+
surfacesOk = true; // sandboxed run — don't block the slot/migration steps
|
|
440
|
+
} else {
|
|
441
|
+
const fleet = readFleetMap(paths.fleetMapPath) ?? [];
|
|
442
|
+
const locked = withProvisionLock({
|
|
443
|
+
stateDir: paths.stateDir,
|
|
444
|
+
fn: () => sweepProvision({ fleet, hooksDir: paths.hooksDir, port: mcpPort() }),
|
|
445
|
+
});
|
|
446
|
+
if (!locked.acquired) {
|
|
447
|
+
step("surfaces", locked.detail, false);
|
|
448
|
+
} else {
|
|
449
|
+
const { results, skipped } = locked.result;
|
|
450
|
+
const failed = results.filter((r) => !r.ok);
|
|
451
|
+
surfacesOk = failed.length === 0;
|
|
452
|
+
step(
|
|
453
|
+
"surfaces",
|
|
454
|
+
`${results.length - failed.length}/${results.length} peer-runtime(s) provisioned` +
|
|
455
|
+
(skipped.length ? `, ${skipped.length} skipped (no session runtime / missing cwd)` : "") +
|
|
456
|
+
" — live sessions pick them up on next restart",
|
|
457
|
+
surfacesOk,
|
|
458
|
+
);
|
|
459
|
+
for (const f of failed) {
|
|
460
|
+
console.log(
|
|
461
|
+
` surfaces FAIL ${f.personality}:${f.runtime} — ${f.outcomes
|
|
462
|
+
.filter((o) => o.action === "failed")
|
|
463
|
+
.map((o) => `${o.surface}: ${o.detail ?? "failed"}`)
|
|
464
|
+
.join("; ")}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 8c. v1.1 → v1.2 migration: sweep the legacy session plugin off the
|
|
471
|
+
// fleet while the old declaration is STILL readable — and ONLY after
|
|
472
|
+
// the direct surfaces landed cleanly.
|
|
473
|
+
let migrationBlocked = false;
|
|
474
|
+
if (!flags.skipEcosystem && existingSlot?.plugin) {
|
|
475
|
+
if (!surfacesOk) {
|
|
476
|
+
migrationBlocked = true;
|
|
477
|
+
step(
|
|
478
|
+
"plugin-off",
|
|
479
|
+
"POSTPONED: direct surfaces did not land cleanly — legacy plugin and v1.1 slot kept (re-run init after fixing)",
|
|
480
|
+
false,
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
const off = applyMemoryPlugin({ mode: "off", iapeerBin: flags.iapeerBin });
|
|
484
|
+
step(
|
|
485
|
+
"plugin-off",
|
|
486
|
+
off.suppressed
|
|
487
|
+
? "skipped (test sandbox — core calls suppressed)"
|
|
488
|
+
: off.ok
|
|
489
|
+
? "legacy v1.1 session plugin swept off the fleet (memory-plugin off --all)"
|
|
490
|
+
: `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)`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 8d. slot declaration (atomic; provider-owned). Kept in the v1.1 form
|
|
496
|
+
// while the migration is blocked — the legacy plugin channel stays
|
|
497
|
+
// derivable until the new channel lands.
|
|
498
|
+
if (migrationBlocked) {
|
|
499
|
+
step("slot", "kept v1.1 declaration (migration postponed — see plugin-off)", false);
|
|
500
|
+
} else {
|
|
501
|
+
const slot = writeSlot({
|
|
502
|
+
slotPath: paths.slotPath,
|
|
503
|
+
version,
|
|
504
|
+
binaryPath: paths.binaryPath,
|
|
505
|
+
heartbeat: paths.heartbeatPath,
|
|
506
|
+
});
|
|
507
|
+
step(
|
|
508
|
+
"slot",
|
|
509
|
+
`${paths.slotPath} (${slot.action}, v${version}, provision-command declared)`,
|
|
510
|
+
slot.action !== "refused-foreign",
|
|
511
|
+
);
|
|
512
|
+
}
|
|
424
513
|
}
|
|
425
514
|
|
|
426
515
|
// 9. native-memory sweep — the core's lever (one home of runtime forms);
|
|
@@ -448,7 +537,9 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
448
537
|
if (flags.skipGuide) {
|
|
449
538
|
step("guide", "skipped (--skip-guide) — roll out by a separate decision after the fleet plugin swap");
|
|
450
539
|
} else {
|
|
451
|
-
|
|
540
|
+
// vault substituted into the {{VAULT_PATH}} marker (дыра 10.06: the
|
|
541
|
+
// literal placeholder left peers without the write path).
|
|
542
|
+
const guidePath = writeHostWideGuideFragment(iapeerDir, guideText(locale, vault));
|
|
452
543
|
step("guide", guidePath);
|
|
453
544
|
}
|
|
454
545
|
|
package/src/commands/memoryd.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import { configFromEnv, startMemoryd } from "@agfpd/iapeer-memory-core";
|
|
21
|
-
import { memoryPaths } from "../paths.js";
|
|
21
|
+
import { authorIndexPath, memoryPaths } from "../paths.js";
|
|
22
22
|
|
|
23
23
|
export async function cmdMemoryd(argv: string[]): Promise<number> {
|
|
24
24
|
let mcpPort: number | undefined;
|
|
@@ -72,6 +72,23 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
|
|
|
72
72
|
humanName: human ?? process.env.IAPEER_MEMORY_HUMAN_NAME ?? null,
|
|
73
73
|
freshEditWindowS,
|
|
74
74
|
mcpPort: noMcp ? null : mcpPort,
|
|
75
|
+
// Per-peer fragment rendering (docs/05; дыра 10.06): the package owns
|
|
76
|
+
// the ecosystem joint — fleet-map path + paths-block facts; core
|
|
77
|
+
// renders at startup, on vault changes and on fleet-map changes.
|
|
78
|
+
fragments: {
|
|
79
|
+
fleetMapPath: paths.fleetMapPath,
|
|
80
|
+
paths: {
|
|
81
|
+
vault: config.vaultPath,
|
|
82
|
+
db: config.index.dbPath,
|
|
83
|
+
config: paths.configFile,
|
|
84
|
+
state: paths.stateDir,
|
|
85
|
+
cache: paths.cacheDir,
|
|
86
|
+
logs: paths.logsDir,
|
|
87
|
+
},
|
|
88
|
+
authorIndexPathFor: (agent) => authorIndexPath(paths, agent),
|
|
89
|
+
indexAgent: process.env.IAPEER_MEMORY_INDEX_AGENT || "index",
|
|
90
|
+
projectsRoot: process.env.IAPEER_MEMORY_PROJECTS_ROOT || undefined,
|
|
91
|
+
},
|
|
75
92
|
});
|
|
76
93
|
|
|
77
94
|
return await new Promise<number>((resolve) => {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `iapeer-memory provision-peer|unprovision-peer --cwd <abs> --runtime <r>
|
|
3
|
+
* --personality <p> [--occasion <o>]` — the provider half of the iapeer
|
|
4
|
+
* v1.2 slot contract
|
|
5
|
+
* (ADR-009 v1.2: direct per-peer surfaces; the core shells into THIS command
|
|
6
|
+
* at peer birth/sweeps and never learns the surface forms).
|
|
7
|
+
*
|
|
8
|
+
* Contract obligations (§7, agreed with the iapeer core 10.06):
|
|
9
|
+
* - argv form, absolute paths, no shell — the core spawns us directly;
|
|
10
|
+
* - IDEMPOTENT: re-running repairs, never corrupts (every surface is a
|
|
11
|
+
* read-merge-write of our own keys only, atomic);
|
|
12
|
+
* - tolerant of PARALLEL calls — the host-wide provision lock serialises
|
|
13
|
+
* bodies (lock.ts);
|
|
14
|
+
* - {occasion} dictionary: birth | sweep-on | off-peer | off-all | remove.
|
|
15
|
+
* We have NO host-global surfaces (требование Артура №3 «глобально не
|
|
16
|
+
* класть» — even the codex MCP form is project-local), so the ref-count
|
|
17
|
+
* distinction off-peer vs off-all is moot here: every occasion of the
|
|
18
|
+
* un-verb means «strip this peer's surfaces». Validated, logged, not
|
|
19
|
+
* branched on.
|
|
20
|
+
*
|
|
21
|
+
* Runtime forms: claude — hooks + mcp + skills (surfaces/claude.ts);
|
|
22
|
+
* codex — per-peer MCP via `<cwd>/.codex/config.toml` (surfaces/codex.ts;
|
|
23
|
+
* hooks/skills are the P5 experiment). Exit: 0 ok, 1 a surface failed,
|
|
24
|
+
* 2 usage.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import { memoryPaths } from "../paths.js";
|
|
29
|
+
import {
|
|
30
|
+
provisionClaudePeer,
|
|
31
|
+
unprovisionClaudePeer,
|
|
32
|
+
type SurfaceOutcome,
|
|
33
|
+
} from "../surfaces/claude.js";
|
|
34
|
+
import { provisionCodexPeer, unprovisionCodexPeer } from "../surfaces/codex.js";
|
|
35
|
+
import { withProvisionLock } from "../surfaces/lock.js";
|
|
36
|
+
|
|
37
|
+
/** The memoryd MCP port FACT of this host (config.env is already loaded into
|
|
38
|
+
* the process env by the CLI boot) — baked literally into both MCP surface
|
|
39
|
+
* forms (no env substitution in either, D2 decision). Shared by the verbs
|
|
40
|
+
* here and the fleet sweeps in init/update/verify. Same resolution as
|
|
41
|
+
* status.ts. */
|
|
42
|
+
export function mcpPort(): number {
|
|
43
|
+
return Number(process.env.IAPEER_MEMORY_MCP_PORT || "") || 8766;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const OCCASIONS = ["birth", "sweep-on", "off-peer", "off-all", "remove"] as const;
|
|
47
|
+
export type Occasion = (typeof OCCASIONS)[number];
|
|
48
|
+
|
|
49
|
+
const RUNTIMES = ["claude", "codex"] as const;
|
|
50
|
+
|
|
51
|
+
type Flags = {
|
|
52
|
+
cwd: string;
|
|
53
|
+
runtime: (typeof RUNTIMES)[number];
|
|
54
|
+
occasion: Occasion;
|
|
55
|
+
personality: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function parseFlags(
|
|
59
|
+
verb: "provision-peer" | "unprovision-peer",
|
|
60
|
+
argv: string[],
|
|
61
|
+
defaultOccasion: Occasion,
|
|
62
|
+
): Flags | null {
|
|
63
|
+
let cwd = "";
|
|
64
|
+
let runtime = "";
|
|
65
|
+
let occasion: string = defaultOccasion;
|
|
66
|
+
let personality = "";
|
|
67
|
+
for (let i = 0; i < argv.length; i++) {
|
|
68
|
+
const a = argv[i];
|
|
69
|
+
switch (a) {
|
|
70
|
+
case "--cwd": cwd = argv[++i] ?? ""; break;
|
|
71
|
+
case "--runtime": runtime = argv[++i] ?? ""; break;
|
|
72
|
+
case "--occasion": occasion = argv[++i] ?? ""; break;
|
|
73
|
+
case "--personality": personality = argv[++i] ?? ""; break;
|
|
74
|
+
default:
|
|
75
|
+
console.error(`iapeer-memory ${verb}: unknown flag: ${a}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!cwd || !cwd.startsWith("/")) {
|
|
80
|
+
console.error(`iapeer-memory ${verb}: --cwd must be an absolute path (got "${cwd}")`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!(RUNTIMES as readonly string[]).includes(runtime)) {
|
|
84
|
+
console.error(`iapeer-memory ${verb}: --runtime must be one of ${RUNTIMES.join("|")} (got "${runtime}")`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!(OCCASIONS as readonly string[]).includes(occasion)) {
|
|
88
|
+
console.error(`iapeer-memory ${verb}: --occasion must be one of ${OCCASIONS.join("|")} (got "${occasion}")`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// claude provision bakes the LITERAL identity header (battle form of the
|
|
92
|
+
// core's own .mcp.json) — without the personality there is nothing honest
|
|
93
|
+
// to bake. The core's executor supports the {personality} placeholder
|
|
94
|
+
// (agreed 11.06); the package sweep reads it from fleet.json. The un-verb
|
|
95
|
+
// needs no identity: removal matches our key/marks, not the header value.
|
|
96
|
+
if (verb === "provision-peer" && runtime === "claude" && !personality) {
|
|
97
|
+
console.error(
|
|
98
|
+
"iapeer-memory provision-peer: --personality is required for --runtime claude " +
|
|
99
|
+
"(the literal MCP identity header is baked at provision time)",
|
|
100
|
+
);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
cwd,
|
|
105
|
+
runtime: runtime as Flags["runtime"],
|
|
106
|
+
occasion: occasion as Occasion,
|
|
107
|
+
personality,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function report(verb: string, flags: Flags, outcomes: SurfaceOutcome[]): number {
|
|
112
|
+
let failed = false;
|
|
113
|
+
for (const o of outcomes) {
|
|
114
|
+
if (o.action === "failed") failed = true;
|
|
115
|
+
console.log(
|
|
116
|
+
`${o.action === "failed" ? "FAIL" : "ok "} ${o.surface.padEnd(7)} ${o.action}${o.detail ? ` — ${o.detail}` : ""} (${o.path})`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
console.log(
|
|
120
|
+
`${verb}: ${flags.runtime} peer at ${flags.cwd} (occasion: ${flags.occasion})` +
|
|
121
|
+
(failed ? " — FAILED; re-run is the repair path (idempotent)" : "") +
|
|
122
|
+
"\npickup: surfaces apply on the peer's NEXT session start (live sessions do not re-read them)",
|
|
123
|
+
);
|
|
124
|
+
return failed ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function cmdProvisionPeer(argv: string[]): number {
|
|
128
|
+
const flags = parseFlags("provision-peer", argv, "sweep-on");
|
|
129
|
+
if (!flags) return 2;
|
|
130
|
+
if (!fs.existsSync(flags.cwd)) {
|
|
131
|
+
console.error(`iapeer-memory provision-peer: cwd does not exist: ${flags.cwd}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const paths = memoryPaths();
|
|
135
|
+
const locked = withProvisionLock({
|
|
136
|
+
stateDir: paths.stateDir,
|
|
137
|
+
fn: () =>
|
|
138
|
+
flags.runtime === "codex"
|
|
139
|
+
? provisionCodexPeer({ cwd: flags.cwd, port: mcpPort() })
|
|
140
|
+
: provisionClaudePeer({
|
|
141
|
+
cwd: flags.cwd,
|
|
142
|
+
hooksDir: paths.hooksDir,
|
|
143
|
+
port: mcpPort(),
|
|
144
|
+
personality: flags.personality,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
if (!locked.acquired) {
|
|
148
|
+
console.error(`iapeer-memory provision-peer: ${locked.detail}`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
return report("provision-peer", flags, locked.result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function cmdUnprovisionPeer(argv: string[]): number {
|
|
155
|
+
const flags = parseFlags("unprovision-peer", argv, "off-peer");
|
|
156
|
+
if (!flags) return 2;
|
|
157
|
+
// a vanished cwd is a VALID un-provision target (occasion=remove races the
|
|
158
|
+
// peer directory removal) — every surface simply reports `absent`
|
|
159
|
+
const paths = memoryPaths();
|
|
160
|
+
const locked = withProvisionLock({
|
|
161
|
+
stateDir: paths.stateDir,
|
|
162
|
+
fn: () =>
|
|
163
|
+
flags.runtime === "codex"
|
|
164
|
+
? unprovisionCodexPeer({ cwd: flags.cwd })
|
|
165
|
+
: unprovisionClaudePeer({ cwd: flags.cwd }),
|
|
166
|
+
});
|
|
167
|
+
if (!locked.acquired) {
|
|
168
|
+
console.error(`iapeer-memory unprovision-peer: ${locked.detail}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
return report("unprovision-peer", flags, locked.result);
|
|
172
|
+
}
|
package/src/commands/render.ts
CHANGED
|
@@ -166,7 +166,14 @@ function renderGuide(argv: string[]): number {
|
|
|
166
166
|
"the whole fleet — the target is never implicit)",
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
let text = fs.readFileSync(source, "utf-8");
|
|
170
|
+
// {{VAULT_PATH}} marker → host fact (дыра 10.06); unprovisioned env
|
|
171
|
+
// keeps the marker — an honest template passthrough, never a guess.
|
|
172
|
+
try {
|
|
173
|
+
text = text.replaceAll("{{VAULT_PATH}}", configFromEnv().vaultPath);
|
|
174
|
+
} catch {
|
|
175
|
+
// no config — leave the marker as is
|
|
176
|
+
}
|
|
170
177
|
const written = writeHostWideGuideFragment(target, text);
|
|
171
178
|
console.log(`render guide: ${written}`);
|
|
172
179
|
return 0;
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* host. SYMMETRY OBLIGATION of the memory-slot contract: the provider that
|
|
4
4
|
* writes the slot declaration removes it.
|
|
5
5
|
*
|
|
6
|
-
* What it removes:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* What it removes: direct session surfaces across the fleet (ADR-009 v1.2 —
|
|
7
|
+
* own entries/keys/dirs only, swept BEFORE the declaration falls), the slot
|
|
8
|
+
* declaration (own only — a foreign slot is refused), notifier triggers,
|
|
9
|
+
* memoryd (verified-pid stop), the compiled binary. What it deliberately
|
|
10
|
+
* KEEPS: the vault (user data), the package config (operator-owned),
|
|
11
|
+
* state/cache (cheap to rebuild, may hold migrate backups!).
|
|
11
12
|
*
|
|
12
13
|
* Native auto-memory of the fleet is NOT restored (contract decision,
|
|
13
14
|
* c968219): silent re-enabling would quietly resurrect split memory across
|
|
@@ -18,7 +19,10 @@
|
|
|
18
19
|
import fs from "node:fs";
|
|
19
20
|
import { memoryPaths } from "../paths.js";
|
|
20
21
|
import { removeBinary } from "../binary.js";
|
|
22
|
+
import { readFleetMap } from "../fleet.js";
|
|
21
23
|
import { applyMemoryPlugin, readSlot, removeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
24
|
+
import { withProvisionLock } from "../surfaces/lock.js";
|
|
25
|
+
import { sweepUnprovision } from "../surfaces/sweep.js";
|
|
22
26
|
import {
|
|
23
27
|
DREAM_TRIGGER_ID,
|
|
24
28
|
SWEEP_TRIGGER_ID,
|
|
@@ -95,26 +99,59 @@ export function cmdUninstall(argv: string[]): number {
|
|
|
95
99
|
const paths = memoryPaths();
|
|
96
100
|
let failed = false;
|
|
97
101
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// order with the core, auto-removal: a dead provider's plugin must not
|
|
102
|
-
// keep injecting). Guard: only when the slot is OURS — running `off`
|
|
103
|
-
// against a foreign declaration would strip the FOREIGN provider's plugin.
|
|
104
|
-
// codex nuance: the codex plugin is host-global, so `off` there is always
|
|
105
|
-
// `--all` semantics; per-peer off on codex answers «use --all».
|
|
102
|
+
// Direct session surfaces OFF across the fleet BEFORE removing the
|
|
103
|
+
// declaration (ADR-009 v1.2 mirror symmetry: a dead provider's surfaces
|
|
104
|
+
// must not keep pointing at a void). Guard: only when the slot is OURS.
|
|
106
105
|
const declared = readSlot(paths.slotPath);
|
|
107
106
|
if (declared && declared.provider === SLOT_PROVIDER) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
const fleet = readFleetMap(paths.fleetMapPath);
|
|
108
|
+
if (!fleet) {
|
|
109
|
+
console.log(
|
|
110
|
+
`surfaces : fleet map missing/unreadable (${paths.fleetMapPath}) — nothing swept; ` +
|
|
111
|
+
"manual per peer: iapeer-memory unprovision-peer --cwd <cwd> --runtime <r>",
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
const locked = withProvisionLock({
|
|
115
|
+
stateDir: paths.stateDir,
|
|
116
|
+
fn: () => sweepUnprovision({ fleet }),
|
|
117
|
+
});
|
|
118
|
+
if (!locked.acquired) {
|
|
119
|
+
console.log(`surfaces : ${locked.detail}`);
|
|
120
|
+
failed = true;
|
|
121
|
+
} else {
|
|
122
|
+
const { results, skipped } = locked.result;
|
|
123
|
+
const bad = results.filter((r) => !r.ok);
|
|
124
|
+
console.log(
|
|
125
|
+
`surfaces : stripped from ${results.length - bad.length}/${results.length} peer-runtime(s)` +
|
|
126
|
+
(skipped.length ? ` (${skipped.length} skipped)` : ""),
|
|
127
|
+
);
|
|
128
|
+
for (const b of bad) {
|
|
129
|
+
failed = true;
|
|
130
|
+
console.log(
|
|
131
|
+
`surfaces : FAIL ${b.personality}:${b.runtime} — ${b.outcomes
|
|
132
|
+
.filter((o) => o.action === "failed")
|
|
133
|
+
.map((o) => `${o.surface}: ${o.detail ?? "failed"}`)
|
|
134
|
+
.join("; ")}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Legacy v1.1 path: the slot still carries a plugin block — sweep the
|
|
141
|
+
// session plugin off via the core verb WHILE the declaration is alive
|
|
142
|
+
// (it derives the identity from it; agreed order, auto-removal).
|
|
143
|
+
if (declared.plugin) {
|
|
144
|
+
const off = applyMemoryPlugin({ mode: "off", iapeerBin });
|
|
145
|
+
console.log(
|
|
146
|
+
`plugin : ${
|
|
147
|
+
off.suppressed
|
|
148
|
+
? "skipped (test sandbox — core calls suppressed)"
|
|
149
|
+
: off.ok
|
|
150
|
+
? "legacy session plugin removed across the fleet (memory-plugin off --all; codex side is host-global)"
|
|
151
|
+
: `off not applied (${off.detail.slice(0, 160)}) — manual fallback (works without the slot): per claude peer \`claude plugin uninstall iapeer-memory@agfpd --scope project\` from its cwd; codex (host-global): \`codex plugin remove iapeer-memory@agfpd\``
|
|
152
|
+
}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
const slot = removeSlot(paths.slotPath);
|