@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 +2 -2
- package/src/binary.ts +17 -10
- package/src/cli.ts +38 -8
- package/src/commands/hook.ts +7 -8
- package/src/commands/init.ts +136 -66
- package/src/commands/install-binary.ts +3 -2
- package/src/commands/memoryd.ts +3 -2
- package/src/commands/provision-peer.ts +172 -0
- package/src/commands/status.ts +15 -8
- package/src/commands/uninstall.ts +78 -48
- package/src/commands/update.ts +125 -53
- package/src/commands/verify.ts +125 -13
- package/src/egress.ts +181 -0
- package/src/fleet.ts +94 -31
- package/src/paths.ts +5 -0
- package/src/provision.ts +3 -2
- package/src/roles.ts +2 -1
- package/src/signing.ts +19 -23
- package/src/slot.ts +97 -52
- package/src/surfaces/claude.ts +497 -0
- package/src/surfaces/codex.ts +156 -0
- package/src/surfaces/lock.ts +72 -0
- package/src/surfaces/sweep.ts +170 -0
- package/src/sync-versions.ts +3 -2
- package/src/templates/index.ts +2 -1
- package/src/templates/skills.ts +196 -0
- package/src/watcher.ts +72 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory",
|
|
3
|
-
"version": "0.1
|
|
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
|
|
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(
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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))
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/commands/hook.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
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
|
|
|
@@ -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
|
-
|
|
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()
|
|
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
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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(
|
package/src/commands/memoryd.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
105
|
+
guardedUnlinkSync(paths.pidPath);
|
|
105
106
|
} catch {
|
|
106
107
|
// best effort
|
|
107
108
|
}
|