@agfpd/iapeer-memory 0.1.13 → 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 +105 -38
- package/src/commands/provision-peer.ts +172 -0
- package/src/commands/uninstall.ts +60 -23
- package/src/commands/update.ts +118 -47
- package/src/commands/verify.ts +110 -5
- package/src/fleet.ts +72 -7
- package/src/paths.ts +5 -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/skills.ts +196 -0
package/src/commands/update.ts
CHANGED
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
* 3. doctrines — re-render every role from the roles manifest with the
|
|
13
13
|
* fresh version marker; roles pick it up on their next
|
|
14
14
|
* cold wake (ADR-007), no restarts;
|
|
15
|
-
* 4.
|
|
16
|
-
* 5.
|
|
17
|
-
*
|
|
15
|
+
* 4. fleet — re-write the fleet map from `iapeer list --json`;
|
|
16
|
+
* 5. surfaces — direct per-peer session surfaces sweep over the map
|
|
17
|
+
* (ADR-009 v1.2: the «всё на местах у подключённых пиров»
|
|
18
|
+
* duty — both runtimes, idempotent, repairs drift);
|
|
19
|
+
* 6. plugin-off — v1.1→v1.2 migration: when the on-disk slot still
|
|
20
|
+
* carries a plugin block, sweep the legacy plugin off the
|
|
21
|
+
* fleet (while the old declaration is STILL readable by
|
|
22
|
+
* the core verb) — only after surfaces landed cleanly;
|
|
23
|
+
* 7. slot — re-declare in the v1.2 form (provision command blocks,
|
|
24
|
+
* new version — contract obligation);
|
|
25
|
+
* 8. launcher + triggers + guide — regenerate;
|
|
26
|
+
* 9. memoryd — MANAGED restart: verified SIGTERM via the pid file →
|
|
18
27
|
* the notifier watcher relaunches through the launcher
|
|
19
|
-
* with the NEW binary
|
|
20
|
-
* notifier fact). Not running → nothing to do;
|
|
21
|
-
* 7. plugin — the harness's native plugin update flow (printed hint;
|
|
22
|
-
* the package never reaches into harness internals).
|
|
28
|
+
* with the NEW binary. Not running → nothing to do.
|
|
23
29
|
*
|
|
24
30
|
* Idempotent: same version re-run → identical/no-op on every surface.
|
|
25
31
|
*/
|
|
@@ -34,10 +40,13 @@ import {
|
|
|
34
40
|
type LocaleId,
|
|
35
41
|
} from "@agfpd/iapeer-memory-core";
|
|
36
42
|
import { installBinary } from "../binary.js";
|
|
37
|
-
import { writeFleetMap } from "../fleet.js";
|
|
43
|
+
import { readFleetMap, writeFleetMap } from "../fleet.js";
|
|
38
44
|
import { memoryPaths } from "../paths.js";
|
|
39
45
|
import { readRolesManifest } from "../roles.js";
|
|
40
|
-
import { writeSlot } from "../slot.js";
|
|
46
|
+
import { applyMemoryPlugin, readSlot, writeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
47
|
+
import { withProvisionLock } from "../surfaces/lock.js";
|
|
48
|
+
import { sweepProvision } from "../surfaces/sweep.js";
|
|
49
|
+
import { mcpPort } from "./provision-peer.js";
|
|
41
50
|
import { guideText, materialiseTemplates } from "../templates/index.js";
|
|
42
51
|
import { packageVersion } from "../version.js";
|
|
43
52
|
import {
|
|
@@ -52,8 +61,11 @@ import { stopMemorydByPidFile } from "./uninstall.js";
|
|
|
52
61
|
|
|
53
62
|
export function cmdUpdate(argv: string[]): number {
|
|
54
63
|
let skipBinary = false;
|
|
55
|
-
|
|
64
|
+
let iapeerBin: string | undefined;
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
56
67
|
if (a === "--skip-binary") skipBinary = true;
|
|
68
|
+
else if (a === "--iapeer-bin") iapeerBin = argv[++i];
|
|
57
69
|
else {
|
|
58
70
|
console.error(`iapeer-memory update: unknown flag: ${a}`);
|
|
59
71
|
return 2;
|
|
@@ -123,24 +135,103 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
123
135
|
);
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
// 4.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
// 4. fleet map — personality → cwd × runtimes (the joint of the surfaces
|
|
139
|
+
// sweep below AND memoryd's fragment renderer, docs/05). BEFORE surfaces
|
|
140
|
+
// and BEFORE the memoryd restart: both consume the fresh map.
|
|
141
|
+
{
|
|
142
|
+
const fleet = writeFleetMap({ fleetMapPath: paths.fleetMapPath, iapeerBin });
|
|
143
|
+
step(
|
|
144
|
+
"fleet",
|
|
145
|
+
fleet.action === "written"
|
|
146
|
+
? fleet.detail
|
|
147
|
+
: `fleet map not written (${fleet.detail}) — surfaces sweep runs on the LAST map; fragments stay stale until verify --repair`,
|
|
148
|
+
fleet.action === "written",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
139
151
|
|
|
140
|
-
// 5.
|
|
152
|
+
// 5. direct session surfaces sweep (ADR-009 v1.2) — the update duty:
|
|
153
|
+
// «всё на местах у подключённых пиров, что codex, что claude».
|
|
154
|
+
const existingSlot = readSlot(paths.slotPath);
|
|
155
|
+
const slotForeign = existingSlot !== null && existingSlot.provider !== SLOT_PROVIDER;
|
|
156
|
+
let surfacesOk = false;
|
|
157
|
+
if (slotForeign) {
|
|
158
|
+
step("surfaces", "skipped (foreign slot — not our host)");
|
|
159
|
+
} else {
|
|
160
|
+
const fleet = readFleetMap(paths.fleetMapPath) ?? [];
|
|
161
|
+
const locked = withProvisionLock({
|
|
162
|
+
stateDir: paths.stateDir,
|
|
163
|
+
fn: () => sweepProvision({ fleet, hooksDir: paths.hooksDir, port: mcpPort() }),
|
|
164
|
+
});
|
|
165
|
+
if (!locked.acquired) {
|
|
166
|
+
step("surfaces", locked.detail, false);
|
|
167
|
+
} else {
|
|
168
|
+
const { results, skipped } = locked.result;
|
|
169
|
+
const failed = results.filter((r) => !r.ok);
|
|
170
|
+
surfacesOk = failed.length === 0;
|
|
171
|
+
step(
|
|
172
|
+
"surfaces",
|
|
173
|
+
`${results.length - failed.length}/${results.length} peer-runtime(s) in place` +
|
|
174
|
+
(skipped.length ? `, ${skipped.length} skipped` : "") +
|
|
175
|
+
" — live sessions pick changes up on next restart",
|
|
176
|
+
surfacesOk,
|
|
177
|
+
);
|
|
178
|
+
for (const f of failed) {
|
|
179
|
+
console.log(
|
|
180
|
+
` surfaces FAIL ${f.personality}:${f.runtime} — ${f.outcomes
|
|
181
|
+
.filter((o) => o.action === "failed")
|
|
182
|
+
.map((o) => `${o.surface}: ${o.detail ?? "failed"}`)
|
|
183
|
+
.join("; ")}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 6. v1.1 → v1.2 migration (one-shot per host): the on-disk slot still
|
|
190
|
+
// carries a plugin block — sweep the legacy plugin off the fleet WHILE the
|
|
191
|
+
// old declaration is still readable (the core verb derives the identity
|
|
192
|
+
// from it), and ONLY after the direct surfaces landed cleanly.
|
|
193
|
+
let migrationBlocked = false;
|
|
194
|
+
if (!slotForeign && existingSlot?.plugin) {
|
|
195
|
+
if (!surfacesOk) {
|
|
196
|
+
migrationBlocked = true;
|
|
197
|
+
step(
|
|
198
|
+
"plugin-off",
|
|
199
|
+
"POSTPONED: direct surfaces did not land cleanly — legacy plugin and v1.1 slot kept (fix and re-run update)",
|
|
200
|
+
false,
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
const off = applyMemoryPlugin({ mode: "off" });
|
|
204
|
+
step(
|
|
205
|
+
"plugin-off",
|
|
206
|
+
off.suppressed
|
|
207
|
+
? "skipped (test sandbox — core calls suppressed)"
|
|
208
|
+
: off.ok
|
|
209
|
+
? "legacy v1.1 session plugin swept off the fleet (memory-plugin off --all)"
|
|
210
|
+
: `legacy plugin off failed (${off.detail.slice(0, 120)}) — manual: iapeer memory-plugin off --all`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 7. slot version + v1.2 form (contract obligation). Kept v1.1 while the
|
|
216
|
+
// migration is blocked — the legacy channel stays derivable.
|
|
217
|
+
if (slotForeign) {
|
|
218
|
+
step("slot", `slot held by foreign provider "${existingSlot?.provider}" — not ours to update`, false);
|
|
219
|
+
} else if (migrationBlocked) {
|
|
220
|
+
step("slot", "kept v1.1 declaration (migration postponed — see plugin-off)", false);
|
|
221
|
+
} else {
|
|
222
|
+
const slot = writeSlot({
|
|
223
|
+
slotPath: paths.slotPath,
|
|
224
|
+
version,
|
|
225
|
+
binaryPath: paths.binaryPath,
|
|
226
|
+
heartbeat: paths.heartbeatPath,
|
|
227
|
+
});
|
|
228
|
+
step("slot", `${slot.action} (v${version}, provision-command declared)`, slot.action !== "refused-foreign");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 8. launcher
|
|
141
232
|
step("launcher", writeLauncherScript({ launcherPath: paths.launcherPath, binaryPath: paths.binaryPath }));
|
|
142
233
|
|
|
143
|
-
//
|
|
234
|
+
// 8b. notifier wiring (ADR-015): same-id re-send REPLACES the trigger —
|
|
144
235
|
// the idempotent re-target path (old hosts with target=index migrate to
|
|
145
236
|
// target=scriber by this very step).
|
|
146
237
|
{
|
|
@@ -173,7 +264,7 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
173
264
|
);
|
|
174
265
|
}
|
|
175
266
|
|
|
176
|
-
//
|
|
267
|
+
// 8c. host-wide guide — update an ALREADY-ROLLED-OUT guide only
|
|
177
268
|
// (presence = the rollout sanction; init --skip-guide hosts stay
|
|
178
269
|
// untouched). Vault substituted into {{VAULT_PATH}} (дыра 10.06: the
|
|
179
270
|
// literal placeholder left peers without the write path).
|
|
@@ -190,29 +281,9 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
190
281
|
}
|
|
191
282
|
}
|
|
192
283
|
|
|
193
|
-
//
|
|
194
|
-
// (docs/05; дыра 10.06). BEFORE the memoryd restart below: the fresh
|
|
195
|
-
// daemon renders the whole fleet at startup from this very map.
|
|
196
|
-
{
|
|
197
|
-
const fleet = writeFleetMap({ fleetMapPath: paths.fleetMapPath });
|
|
198
|
-
step(
|
|
199
|
-
"fleet",
|
|
200
|
-
fleet.action === "written"
|
|
201
|
-
? fleet.detail
|
|
202
|
-
: `fleet map not written (${fleet.detail}) — fragments stay stale until verify --repair`,
|
|
203
|
-
fleet.action === "written",
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 6. memoryd managed restart (the watcher relaunches with the new binary)
|
|
284
|
+
// 9. memoryd managed restart (the watcher relaunches with the new binary)
|
|
208
285
|
step("memoryd", `${stopMemorydByPidFile(paths.pidPath)} — the notifier watcher relaunches it with the new binary`);
|
|
209
286
|
|
|
210
|
-
// 7. plugin — harness-owned surface
|
|
211
|
-
console.log(
|
|
212
|
-
" plugin update via the harness's native plugin flow " +
|
|
213
|
-
"(claude/codex plugin manager); the package never reaches into harness internals",
|
|
214
|
-
);
|
|
215
|
-
|
|
216
287
|
console.log(
|
|
217
288
|
failures
|
|
218
289
|
? `\nupdate finished with ${failures} problem(s) — iapeer-memory verify --repair`
|
package/src/commands/verify.ts
CHANGED
|
@@ -25,10 +25,13 @@ import {
|
|
|
25
25
|
renderDoctrine,
|
|
26
26
|
renderedVersion,
|
|
27
27
|
} from "@agfpd/iapeer-memory-core";
|
|
28
|
-
import { writeFleetMap } from "../fleet.js";
|
|
28
|
+
import { readFleetMap, writeFleetMap } from "../fleet.js";
|
|
29
29
|
import { memoryPaths, type MemoryPaths } from "../paths.js";
|
|
30
30
|
import { readRolesManifest } from "../roles.js";
|
|
31
|
-
import { readSlot, writeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
31
|
+
import { readSlot, slotProvisionBlocks, writeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
32
|
+
import { withProvisionLock } from "../surfaces/lock.js";
|
|
33
|
+
import { checkFleetSurfaces, sweepProvision } from "../surfaces/sweep.js";
|
|
34
|
+
import { mcpPort } from "./provision-peer.js";
|
|
32
35
|
import { packageVersion } from "../version.js";
|
|
33
36
|
import {
|
|
34
37
|
dreamTimerMessage,
|
|
@@ -95,6 +98,11 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
95
98
|
|
|
96
99
|
// 1b. memory-provider slot (iapeer memory-slot contract): a provisioned
|
|
97
100
|
// host must declare the slot; a FOREIGN slot is never repaired over.
|
|
101
|
+
// A v1.1 slot (plugin block) is NEVER migrated here — the migration is
|
|
102
|
+
// update's job (plugin off --all must run while the old declaration is
|
|
103
|
+
// readable; a SessionStart-kicked repair racing ahead of update would
|
|
104
|
+
// strand the legacy plugin on the whole fleet).
|
|
105
|
+
let slotIsLegacyV11 = false;
|
|
98
106
|
if (!configOk) {
|
|
99
107
|
results.push({
|
|
100
108
|
name: "memory-slot",
|
|
@@ -103,17 +111,37 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
103
111
|
});
|
|
104
112
|
} else {
|
|
105
113
|
const slot = readSlot(paths.slotPath);
|
|
114
|
+
const expectedBlocks = slotProvisionBlocks(paths.binaryPath);
|
|
115
|
+
const formOk =
|
|
116
|
+
slot !== null &&
|
|
117
|
+
slot.plugin === undefined &&
|
|
118
|
+
JSON.stringify(slot.provision) === JSON.stringify(expectedBlocks.provision) &&
|
|
119
|
+
JSON.stringify(slot.unprovision) === JSON.stringify(expectedBlocks.unprovision);
|
|
106
120
|
if (slot && slot.provider !== SLOT_PROVIDER) {
|
|
107
121
|
results.push({
|
|
108
122
|
name: "memory-slot",
|
|
109
123
|
status: "fail",
|
|
110
124
|
detail: `slot held by foreign provider "${slot.provider}" — refusing to touch (uninstall it first)`,
|
|
111
125
|
});
|
|
112
|
-
} else if (slot
|
|
113
|
-
|
|
126
|
+
} else if (slot?.plugin) {
|
|
127
|
+
slotIsLegacyV11 = true;
|
|
128
|
+
results.push({
|
|
129
|
+
name: "memory-slot",
|
|
130
|
+
status: "fail",
|
|
131
|
+
detail:
|
|
132
|
+
"slot is the legacy v1.1 form (plugin block) — migrate via `iapeer-memory update` (verify never migrates: the plugin-off order is update's duty)",
|
|
133
|
+
});
|
|
134
|
+
} else if (slot && slot.version === version && formOk) {
|
|
135
|
+
results.push({
|
|
136
|
+
name: "memory-slot",
|
|
137
|
+
status: "ok",
|
|
138
|
+
detail: `declared v${slot.version}, provision-command in place`,
|
|
139
|
+
});
|
|
114
140
|
} else {
|
|
115
141
|
const problem = slot
|
|
116
|
-
?
|
|
142
|
+
? slot.version !== version
|
|
143
|
+
? `slot declares v${slot.version}, package is v${version}`
|
|
144
|
+
: "provision-command block missing/drifted"
|
|
117
145
|
: `slot declaration missing at ${paths.slotPath}`;
|
|
118
146
|
if (!repair) {
|
|
119
147
|
results.push({ name: "memory-slot", status: "fail", detail: problem });
|
|
@@ -121,6 +149,7 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
121
149
|
const w = writeSlot({
|
|
122
150
|
slotPath: paths.slotPath,
|
|
123
151
|
version,
|
|
152
|
+
binaryPath: paths.binaryPath,
|
|
124
153
|
heartbeat: paths.heartbeatPath,
|
|
125
154
|
});
|
|
126
155
|
results.push(
|
|
@@ -169,6 +198,82 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
200
|
|
|
201
|
+
// 1d. direct per-peer session surfaces (ADR-009 v1.2) across the fleet
|
|
202
|
+
// map — the self-healing loop for newborns on hosts where the core's
|
|
203
|
+
// birth-hook lagged AND the drift-repair duty (требование №2). Skipped on
|
|
204
|
+
// a legacy v1.1 host: the plugin is still the live channel there, direct
|
|
205
|
+
// surfaces land via update's migration.
|
|
206
|
+
if (!configOk) {
|
|
207
|
+
results.push({ name: "peer-surfaces", status: "skip", detail: "not provisioned (config check failed)" });
|
|
208
|
+
} else if (slotIsLegacyV11) {
|
|
209
|
+
results.push({
|
|
210
|
+
name: "peer-surfaces",
|
|
211
|
+
status: "skip",
|
|
212
|
+
detail: "legacy v1.1 host (plugin channel) — direct surfaces land via `iapeer-memory update`",
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
const fleet = readFleetMap(paths.fleetMapPath);
|
|
216
|
+
if (!fleet) {
|
|
217
|
+
results.push({
|
|
218
|
+
name: "peer-surfaces",
|
|
219
|
+
status: "skip",
|
|
220
|
+
detail: "fleet map unreadable — see fleet-map check",
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
const { checks, skipped } = checkFleetSurfaces({
|
|
224
|
+
fleet,
|
|
225
|
+
hooksDir: paths.hooksDir,
|
|
226
|
+
port: mcpPort(),
|
|
227
|
+
});
|
|
228
|
+
const bad = checks.filter((c) => !c.ok);
|
|
229
|
+
if (bad.length === 0) {
|
|
230
|
+
results.push({
|
|
231
|
+
name: "peer-surfaces",
|
|
232
|
+
status: "ok",
|
|
233
|
+
detail:
|
|
234
|
+
`${checks.length} peer-runtime(s) in place` +
|
|
235
|
+
(skipped.length ? ` (${skipped.length} skipped: no session runtime / missing cwd)` : ""),
|
|
236
|
+
});
|
|
237
|
+
} else if (!repair) {
|
|
238
|
+
for (const b of bad) {
|
|
239
|
+
results.push({
|
|
240
|
+
name: `peer-surfaces[${b.personality}:${b.runtime}]`,
|
|
241
|
+
status: "fail",
|
|
242
|
+
detail: b.problems.join("; "),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
const badPeers = fleet.filter((p) => bad.some((b) => b.cwd === p.cwd));
|
|
247
|
+
const locked = withProvisionLock({
|
|
248
|
+
stateDir: paths.stateDir,
|
|
249
|
+
fn: () => sweepProvision({ fleet: badPeers, hooksDir: paths.hooksDir, port: mcpPort() }),
|
|
250
|
+
});
|
|
251
|
+
if (!locked.acquired) {
|
|
252
|
+
results.push({ name: "peer-surfaces", status: "fail", detail: locked.detail });
|
|
253
|
+
} else {
|
|
254
|
+
const stillBad = locked.result.results.filter((r) => !r.ok);
|
|
255
|
+
results.push(
|
|
256
|
+
stillBad.length === 0
|
|
257
|
+
? {
|
|
258
|
+
name: "peer-surfaces",
|
|
259
|
+
status: "repaired",
|
|
260
|
+
detail: `${bad.length} drifted peer-runtime(s) re-provisioned (${bad
|
|
261
|
+
.map((b) => `${b.personality}:${b.runtime}`)
|
|
262
|
+
.join(", ")})`,
|
|
263
|
+
}
|
|
264
|
+
: {
|
|
265
|
+
name: "peer-surfaces",
|
|
266
|
+
status: "fail",
|
|
267
|
+
detail: `repair failed for ${stillBad
|
|
268
|
+
.map((r) => `${r.personality}:${r.runtime}`)
|
|
269
|
+
.join(", ")}`,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
172
277
|
// 2. memoryd heartbeat
|
|
173
278
|
try {
|
|
174
279
|
const stat = fs.statSync(paths.heartbeatPath);
|
package/src/fleet.ts
CHANGED
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* new peer wakes → SessionStart kick → `verify --repair` re-writes the
|
|
8
8
|
* map → memoryd renders the newcomer's fragment within a tick.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* TEST FUSE (incident 11.06, the FOURTH of its class — first FILE-path one):
|
|
11
|
+
* `iapeer list` is read-only, but its RESULT is the target list of the
|
|
12
|
+
* surfaces sweep — a sandboxed `verify --repair` with no fleet map repaired
|
|
13
|
+
* the map from the LIVE registry and then swept the LIVE peers' cwds with
|
|
14
|
+
* direct surfaces (the send-fuse never saw it: no IAP send involved).
|
|
15
|
+
* Querying the live registry from a test IS the leak — so the default
|
|
16
|
+
* binary is refused under the sandbox fuse; tests that need a fleet pass a
|
|
17
|
+
* fake `iapeerBin` (as before) or write the map file directly.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
import fs from "node:fs";
|
|
@@ -21,7 +26,46 @@ export type FleetMapResult = {
|
|
|
21
26
|
detail: string;
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
type ListedPeer = {
|
|
29
|
+
type ListedPeer = {
|
|
30
|
+
personality?: unknown;
|
|
31
|
+
cwd?: unknown;
|
|
32
|
+
/** iapeer registry: `[{runtime: "claude"|"codex"|…, status}]`. */
|
|
33
|
+
runtimes?: Array<{ runtime?: unknown }>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Fleet-map entry. `runtimes` (ADR-009 v1.2) names the peer's session
|
|
37
|
+
* runtimes from the registry — the surfaces sweep keys its per-runtime
|
|
38
|
+
* forms on it (claude: hooks+mcp+skills; codex: project-local MCP).
|
|
39
|
+
* Core's memoryd reader takes personality/cwd only — additive, fail-open. */
|
|
40
|
+
export type FleetPeer = { personality: string; cwd: string; runtimes: string[] };
|
|
41
|
+
|
|
42
|
+
/** Fail-open fleet-map reader (the package side: the surfaces sweep and
|
|
43
|
+
* verify's per-peer checks). Missing/unreadable map → null — callers report
|
|
44
|
+
* honestly instead of guessing the fleet. Entries without a runtimes array
|
|
45
|
+
* (pre-v1.2 maps) read as `runtimes: []` — the sweep skips them until the
|
|
46
|
+
* next map re-write (init/update/verify --repair). */
|
|
47
|
+
export function readFleetMap(fleetMapPath: string): FleetPeer[] | null {
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(fs.readFileSync(fleetMapPath, "utf-8")) as {
|
|
50
|
+
peers?: Array<{ personality?: unknown; cwd?: unknown; runtimes?: unknown }>;
|
|
51
|
+
};
|
|
52
|
+
if (!Array.isArray(raw?.peers)) return null;
|
|
53
|
+
return raw.peers
|
|
54
|
+
.filter(
|
|
55
|
+
(p): p is { personality: string; cwd: string; runtimes?: unknown } =>
|
|
56
|
+
typeof p?.personality === "string" && typeof p?.cwd === "string",
|
|
57
|
+
)
|
|
58
|
+
.map((p) => ({
|
|
59
|
+
personality: p.personality,
|
|
60
|
+
cwd: p.cwd,
|
|
61
|
+
runtimes: Array.isArray(p.runtimes)
|
|
62
|
+
? p.runtimes.filter((r): r is string => typeof r === "string")
|
|
63
|
+
: [],
|
|
64
|
+
}));
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
25
69
|
|
|
26
70
|
export function writeFleetMap(opts: {
|
|
27
71
|
fleetMapPath: string;
|
|
@@ -29,6 +73,17 @@ export function writeFleetMap(opts: {
|
|
|
29
73
|
/** Injectable for tests. */
|
|
30
74
|
nowIso?: string;
|
|
31
75
|
}): FleetMapResult {
|
|
76
|
+
if (
|
|
77
|
+
opts.iapeerBin === undefined &&
|
|
78
|
+
(process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1" ||
|
|
79
|
+
process.env.IAPEER_TEST_SANDBOX === "1")
|
|
80
|
+
) {
|
|
81
|
+
return {
|
|
82
|
+
action: "failed",
|
|
83
|
+
count: 0,
|
|
84
|
+
detail: "live-registry query suppressed (test sandbox) — pass a fake iapeerBin",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
32
87
|
const bin = opts.iapeerBin ?? "iapeer";
|
|
33
88
|
let stdout: string;
|
|
34
89
|
try {
|
|
@@ -57,15 +112,25 @@ export function writeFleetMap(opts: {
|
|
|
57
112
|
return { action: "failed", count: 0, detail: "iapeer list --json: unparsable output" };
|
|
58
113
|
}
|
|
59
114
|
|
|
60
|
-
const peers = listed
|
|
115
|
+
const peers: FleetPeer[] = listed
|
|
61
116
|
.filter(
|
|
62
|
-
(p): p is { personality: string; cwd: string } =>
|
|
117
|
+
(p): p is ListedPeer & { personality: string; cwd: string } =>
|
|
63
118
|
typeof p.personality === "string" &&
|
|
64
119
|
p.personality.trim() !== "" &&
|
|
65
120
|
typeof p.cwd === "string" &&
|
|
66
121
|
p.cwd.trim() !== "",
|
|
67
122
|
)
|
|
68
|
-
.map((p) => ({
|
|
123
|
+
.map((p) => ({
|
|
124
|
+
personality: p.personality.trim(),
|
|
125
|
+
cwd: p.cwd.trim(),
|
|
126
|
+
runtimes: [
|
|
127
|
+
...new Set(
|
|
128
|
+
(Array.isArray(p.runtimes) ? p.runtimes : [])
|
|
129
|
+
.map((r) => (typeof r?.runtime === "string" ? r.runtime.trim() : ""))
|
|
130
|
+
.filter(Boolean),
|
|
131
|
+
),
|
|
132
|
+
],
|
|
133
|
+
}));
|
|
69
134
|
|
|
70
135
|
const body =
|
|
71
136
|
JSON.stringify(
|
package/src/paths.ts
CHANGED
|
@@ -48,6 +48,10 @@ export type MemoryPaths = {
|
|
|
48
48
|
binaryPath: string;
|
|
49
49
|
/** Materialised package-owned templates (roles, guide) — see templates/index.ts. */
|
|
50
50
|
templatesDir: string;
|
|
51
|
+
/** Materialised hook shims (fail-open bash, 3 lines) — the ABSOLUTE command
|
|
52
|
+
* paths merged into peers' `.claude/settings.json` (ownership lives IN THE
|
|
53
|
+
* DATA: the command path is the identity of our entries — ADR-009 v1.2). */
|
|
54
|
+
hooksDir: string;
|
|
51
55
|
/** memoryd launcher — the notifier watcher's script (wraps the stable binary). */
|
|
52
56
|
launcherPath: string;
|
|
53
57
|
/** Sweep check-script — gates the fail-open inbox sweep (ADR-015). */
|
|
@@ -92,6 +96,7 @@ export function memoryPaths(
|
|
|
92
96
|
binaryPath:
|
|
93
97
|
env.IAPEER_MEMORY_BINARY_PATH || path.join(home, ".local", "bin", "iapeer-memory"),
|
|
94
98
|
templatesDir: path.join(path.dirname(configFile), "templates"),
|
|
99
|
+
hooksDir: path.join(path.dirname(configFile), "hooks"),
|
|
95
100
|
launcherPath: path.join(path.dirname(configFile), "memoryd-launcher.sh"),
|
|
96
101
|
checkScriptPath: path.join(path.dirname(configFile), "inbox-stale-check.sh"),
|
|
97
102
|
fleetMapPath: path.join(stateDir, "fleet.json"),
|
package/src/slot.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory-provider slot declaration — the iapeer memory-slot contract (FINAL
|
|
3
|
-
* iapeer docs fc68c54/e2195a7/c968219
|
|
4
|
-
* the three public surfaces (layer-5
|
|
5
|
-
* notifier watcher) are occupied:
|
|
2
|
+
* Memory-provider slot declaration — the iapeer memory-slot contract (FINAL
|
|
3
|
+
* base, iapeer docs fc68c54/e2195a7/c968219; v1.2 revision agreed 11.06).
|
|
4
|
+
* The slot file tells the core that the three public surfaces (layer-5
|
|
5
|
+
* fragments / MCP tools / daemon under a notifier watcher) are occupied:
|
|
6
6
|
*
|
|
7
7
|
* - the PROVIDER writes and removes the file (our init/uninstall), atomic
|
|
8
8
|
* temp+rename; the core only reads it (absent/unreadable = empty slot);
|
|
@@ -12,14 +12,21 @@
|
|
|
12
12
|
* marker, ADR-010); our `update` re-writes it (P4 obligation);
|
|
13
13
|
* - `heartbeat` (optional) = the absolute path whose mtime memoryd touches —
|
|
14
14
|
* the core may show staleness in `iapeer status`, never acts on it;
|
|
15
|
-
* - `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* - `provision`/`unprovision` (v1.2, ADR-009 v1.2 — boris's birth-joint
|
|
16
|
+
* inversion, schema fixed with the core 11.06): the PROVIDER's OWN command
|
|
17
|
+
* the core shells into at peer birth / verb sweeps / peer removal. The
|
|
18
|
+
* core never learns the surface forms; placeholders {cwd} {runtime}
|
|
19
|
+
* {personality} {occasion} substitute PER-ARGUMENT (argv spawn, no shell,
|
|
20
|
+
* 120s timeout, best-effort + loud warn). Precedence at the core:
|
|
21
|
+
* provision > plugin with NO runtime fallback;
|
|
22
|
+
* - `plugin` (v1.1, deprecated by v1.2): we no longer WRITE it — holding
|
|
23
|
+
* both blocks would make an old core re-install the plugin we swept
|
|
24
|
+
* (agreed 11.06). An old core reads our v1.2 slot as «provider without a
|
|
25
|
+
* plugin» and honestly skips the birth install; the newborn is picked up
|
|
26
|
+
* by the verify --repair sweep. RELEASE ORDER closes even that window on
|
|
27
|
+
* this host: the core ships its v1.2 parser FIRST, our release follows.
|
|
28
|
+
* The type keeps the field so uninstall/update can MIGRATE old slots
|
|
29
|
+
* (plugin off --all while the block is still readable).
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
import fs from "node:fs";
|
|
@@ -28,7 +35,8 @@ import path from "node:path";
|
|
|
28
35
|
export const SLOT_PROVIDER = "iapeer-memory";
|
|
29
36
|
export const SLOT_PACKAGE = "@agfpd/iapeer-memory";
|
|
30
37
|
|
|
31
|
-
/** Mirror of iapeer's MemoryProviderPlugin (src/status/index.ts).
|
|
38
|
+
/** Mirror of iapeer's MemoryProviderPlugin (src/status/index.ts). v1.1
|
|
39
|
+
* legacy: READ-only here (migration off-path); v1.2 slots no longer carry it. */
|
|
32
40
|
export type MemoryProviderPlugin = {
|
|
33
41
|
/** Plugin id in the marketplace (forms `<name>@<marketplace>`). */
|
|
34
42
|
name: string;
|
|
@@ -38,19 +46,54 @@ export type MemoryProviderPlugin = {
|
|
|
38
46
|
marketplaceRef: string;
|
|
39
47
|
};
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/** v1.2 provision command block — argv form (§7 req 1: per-argument
|
|
50
|
+
* placeholder substitution, spawn without a shell). */
|
|
51
|
+
export type MemoryProviderCommand = {
|
|
52
|
+
/** Absolute path (§7 req 2: birth-hooks live in a minimal launchd PATH). */
|
|
53
|
+
command: string;
|
|
54
|
+
args: string[];
|
|
45
55
|
};
|
|
46
56
|
|
|
57
|
+
/** The provision/unprovision blocks of OUR slot — built around the stable
|
|
58
|
+
* installed binary (the same path the hooks/watcher rely on). */
|
|
59
|
+
export function slotProvisionBlocks(binaryPath: string): {
|
|
60
|
+
provision: MemoryProviderCommand;
|
|
61
|
+
unprovision: MemoryProviderCommand;
|
|
62
|
+
} {
|
|
63
|
+
return {
|
|
64
|
+
provision: {
|
|
65
|
+
command: binaryPath,
|
|
66
|
+
args: [
|
|
67
|
+
"provision-peer",
|
|
68
|
+
"--cwd", "{cwd}",
|
|
69
|
+
"--runtime", "{runtime}",
|
|
70
|
+
"--personality", "{personality}",
|
|
71
|
+
"--occasion", "{occasion}",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
unprovision: {
|
|
75
|
+
command: binaryPath,
|
|
76
|
+
args: [
|
|
77
|
+
"unprovision-peer",
|
|
78
|
+
"--cwd", "{cwd}",
|
|
79
|
+
"--runtime", "{runtime}",
|
|
80
|
+
"--occasion", "{occasion}",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
export type MemoryProviderSlot = {
|
|
48
87
|
provider: string;
|
|
49
88
|
package: string;
|
|
50
89
|
version: string;
|
|
51
90
|
registeredAt: string;
|
|
52
91
|
heartbeat?: string;
|
|
92
|
+
/** v1.1 legacy (read for migration; never written by v1.2 code). */
|
|
53
93
|
plugin?: MemoryProviderPlugin;
|
|
94
|
+
/** v1.2 (ADR-009 v1.2). */
|
|
95
|
+
provision?: MemoryProviderCommand;
|
|
96
|
+
unprovision?: MemoryProviderCommand;
|
|
54
97
|
};
|
|
55
98
|
|
|
56
99
|
/** Never throws: missing / unreadable / malformed → null (empty slot). */
|
|
@@ -72,6 +115,8 @@ export type SlotWriteResult = {
|
|
|
72
115
|
export function writeSlot(opts: {
|
|
73
116
|
slotPath: string;
|
|
74
117
|
version: string;
|
|
118
|
+
/** Absolute path of the installed binary — the provision command carrier. */
|
|
119
|
+
binaryPath: string;
|
|
75
120
|
heartbeat?: string;
|
|
76
121
|
/** Injectable for tests. */
|
|
77
122
|
nowIso?: string;
|
|
@@ -80,15 +125,15 @@ export function writeSlot(opts: {
|
|
|
80
125
|
if (existing && existing.provider !== SLOT_PROVIDER) {
|
|
81
126
|
return { action: "refused-foreign", existing };
|
|
82
127
|
}
|
|
128
|
+
const blocks = slotProvisionBlocks(opts.binaryPath);
|
|
83
129
|
if (
|
|
84
130
|
existing &&
|
|
85
131
|
existing.version === opts.version &&
|
|
86
132
|
existing.heartbeat === opts.heartbeat &&
|
|
87
133
|
existing.package === SLOT_PACKAGE &&
|
|
88
|
-
existing.plugin &&
|
|
89
|
-
existing.
|
|
90
|
-
existing.
|
|
91
|
-
existing.plugin.marketplaceRef === SLOT_PLUGIN.marketplaceRef
|
|
134
|
+
existing.plugin === undefined && // a v1.1 slot (plugin block) must MIGRATE to the v1.2 form
|
|
135
|
+
JSON.stringify(existing.provision) === JSON.stringify(blocks.provision) &&
|
|
136
|
+
JSON.stringify(existing.unprovision) === JSON.stringify(blocks.unprovision)
|
|
92
137
|
) {
|
|
93
138
|
return { action: "identical", existing }; // idempotent re-init: no churn
|
|
94
139
|
}
|
|
@@ -98,7 +143,7 @@ export function writeSlot(opts: {
|
|
|
98
143
|
version: opts.version,
|
|
99
144
|
registeredAt: opts.nowIso ?? new Date().toISOString(),
|
|
100
145
|
...(opts.heartbeat ? { heartbeat: opts.heartbeat } : {}),
|
|
101
|
-
|
|
146
|
+
...blocks,
|
|
102
147
|
};
|
|
103
148
|
fs.mkdirSync(path.dirname(opts.slotPath), { recursive: true });
|
|
104
149
|
const tmp = `${opts.slotPath}.tmp`;
|