@agfpd/iapeer-memory 0.1.12 → 0.1.13
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/commands/init.ts +25 -1
- package/src/commands/memoryd.ts +18 -1
- package/src/commands/render.ts +8 -1
- package/src/commands/update.ts +36 -1
- package/src/commands/verify.ts +38 -0
- package/src/fleet.ts +85 -0
- package/src/paths.ts +5 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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.1.13"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "^1.2.0",
|
package/src/commands/init.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { memoryPaths } from "../paths.js";
|
|
|
34
34
|
import { provisionVault, writeDefaultConfig } from "../provision.js";
|
|
35
35
|
import { writeRolesManifest, type RoleEntry } from "../roles.js";
|
|
36
36
|
import { applyMemoryPlugin, writeSlot } from "../slot.js";
|
|
37
|
+
import { writeFleetMap } from "../fleet.js";
|
|
37
38
|
import {
|
|
38
39
|
doctrineOwnership,
|
|
39
40
|
guideText,
|
|
@@ -340,6 +341,27 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
340
341
|
);
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
// 6b. fleet map — personality → cwd for memoryd's fragment renderer
|
|
345
|
+
// (docs/05; дыра 10.06: без карты пиры не получали paths-блок и индекс).
|
|
346
|
+
// ПЕРЕД watcher-регистрацией: memoryd, поднятый notifier'ом, рендерит
|
|
347
|
+
// весь флот уже на старте. Roles-степ выше уже создал ролевых пиров —
|
|
348
|
+
// карта включает их.
|
|
349
|
+
if (flags.skipEcosystem) {
|
|
350
|
+
step("fleet", "skipped (--skip-ecosystem)");
|
|
351
|
+
} else {
|
|
352
|
+
const fleet = writeFleetMap({
|
|
353
|
+
fleetMapPath: paths.fleetMapPath,
|
|
354
|
+
iapeerBin: flags.iapeerBin,
|
|
355
|
+
});
|
|
356
|
+
step(
|
|
357
|
+
"fleet",
|
|
358
|
+
fleet.action === "written"
|
|
359
|
+
? fleet.detail
|
|
360
|
+
: `fleet map not written (${fleet.detail}) — fragments stay off until verify --repair`,
|
|
361
|
+
fleet.action === "written",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
343
365
|
// 7. notifier wiring (ADR-015, инверсия): the EVENT trigger targets the
|
|
344
366
|
// SCRIBER (first receiver); two TIMERS target the index — weekly
|
|
345
367
|
// DREAM_TICK and the check-gated fail-open sweep. Registrant = index for
|
|
@@ -448,7 +470,9 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
448
470
|
if (flags.skipGuide) {
|
|
449
471
|
step("guide", "skipped (--skip-guide) — roll out by a separate decision after the fleet plugin swap");
|
|
450
472
|
} else {
|
|
451
|
-
|
|
473
|
+
// vault substituted into the {{VAULT_PATH}} marker (дыра 10.06: the
|
|
474
|
+
// literal placeholder left peers without the write path).
|
|
475
|
+
const guidePath = writeHostWideGuideFragment(iapeerDir, guideText(locale, vault));
|
|
452
476
|
step("guide", guidePath);
|
|
453
477
|
}
|
|
454
478
|
|
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) => {
|
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;
|
package/src/commands/update.ts
CHANGED
|
@@ -24,17 +24,21 @@
|
|
|
24
24
|
* Idempotent: same version re-run → identical/no-op on every surface.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
27
29
|
import {
|
|
28
30
|
configFromEnv,
|
|
29
31
|
isLocaleId,
|
|
30
32
|
renderDoctrine,
|
|
33
|
+
writeHostWideGuideFragment,
|
|
31
34
|
type LocaleId,
|
|
32
35
|
} from "@agfpd/iapeer-memory-core";
|
|
33
36
|
import { installBinary } from "../binary.js";
|
|
37
|
+
import { writeFleetMap } from "../fleet.js";
|
|
34
38
|
import { memoryPaths } from "../paths.js";
|
|
35
39
|
import { readRolesManifest } from "../roles.js";
|
|
36
40
|
import { writeSlot } from "../slot.js";
|
|
37
|
-
import { materialiseTemplates } from "../templates/index.js";
|
|
41
|
+
import { guideText, materialiseTemplates } from "../templates/index.js";
|
|
38
42
|
import { packageVersion } from "../version.js";
|
|
39
43
|
import {
|
|
40
44
|
dreamTimerMessage,
|
|
@@ -169,6 +173,37 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
169
173
|
);
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
// 5c. host-wide guide — update an ALREADY-ROLLED-OUT guide only
|
|
177
|
+
// (presence = the rollout sanction; init --skip-guide hosts stay
|
|
178
|
+
// untouched). Vault substituted into {{VAULT_PATH}} (дыра 10.06: the
|
|
179
|
+
// literal placeholder left peers without the write path).
|
|
180
|
+
{
|
|
181
|
+
const iapeerDir = path.dirname(paths.slotPath);
|
|
182
|
+
const guidePath = path.join(iapeerDir, "fragments", "iapeer-memory.md");
|
|
183
|
+
if (!fs.existsSync(guidePath)) {
|
|
184
|
+
step("guide", "not rolled out on this host — left untouched (roll out via init)");
|
|
185
|
+
} else if (!vaultPathForDoctrines) {
|
|
186
|
+
step("guide", "unprovisioned env — vault unknown, guide left as is", false);
|
|
187
|
+
} else {
|
|
188
|
+
writeHostWideGuideFragment(iapeerDir, guideText(locale, vaultPathForDoctrines));
|
|
189
|
+
step("guide", `${guidePath} re-written (v${version}, vault path substituted)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 5d. fleet map — personality → cwd for memoryd's fragment renderer
|
|
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
|
+
|
|
172
207
|
// 6. memoryd managed restart (the watcher relaunches with the new binary)
|
|
173
208
|
step("memoryd", `${stopMemorydByPidFile(paths.pidPath)} — the notifier watcher relaunches it with the new binary`);
|
|
174
209
|
|
package/src/commands/verify.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
renderDoctrine,
|
|
26
26
|
renderedVersion,
|
|
27
27
|
} from "@agfpd/iapeer-memory-core";
|
|
28
|
+
import { writeFleetMap } from "../fleet.js";
|
|
28
29
|
import { memoryPaths, type MemoryPaths } from "../paths.js";
|
|
29
30
|
import { readRolesManifest } from "../roles.js";
|
|
30
31
|
import { readSlot, writeSlot, SLOT_PROVIDER } from "../slot.js";
|
|
@@ -131,6 +132,43 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// 1c. fleet map — memoryd's fragment renderer reads it (docs/05; дыра
|
|
136
|
+
// 10.06: без карты пиры не получали paths-блок и индекс автора). Repair
|
|
137
|
+
// re-writes from `iapeer list --json` — the self-healing loop for new
|
|
138
|
+
// peers (SessionStart kick → repair → map fresh → memoryd renders the
|
|
139
|
+
// newcomer on the next heartbeat tick).
|
|
140
|
+
if (!configOk) {
|
|
141
|
+
results.push({ name: "fleet-map", status: "skip", detail: "not provisioned (config check failed)" });
|
|
142
|
+
} else {
|
|
143
|
+
let mapPeers = -1; // -1 = unreadable/missing
|
|
144
|
+
try {
|
|
145
|
+
const raw = JSON.parse(fs.readFileSync(paths.fleetMapPath, "utf-8")) as {
|
|
146
|
+
peers?: unknown[];
|
|
147
|
+
};
|
|
148
|
+
mapPeers = Array.isArray(raw?.peers) ? raw.peers.length : -1;
|
|
149
|
+
} catch {
|
|
150
|
+
mapPeers = -1;
|
|
151
|
+
}
|
|
152
|
+
if (mapPeers > 0) {
|
|
153
|
+
results.push({ name: "fleet-map", status: "ok", detail: `${mapPeers} peer(s) in ${paths.fleetMapPath}` });
|
|
154
|
+
} else {
|
|
155
|
+
const problem =
|
|
156
|
+
mapPeers === 0
|
|
157
|
+
? `fleet map is empty at ${paths.fleetMapPath}`
|
|
158
|
+
: `fleet map missing/unreadable at ${paths.fleetMapPath}`;
|
|
159
|
+
if (!repair) {
|
|
160
|
+
results.push({ name: "fleet-map", status: "fail", detail: problem });
|
|
161
|
+
} else {
|
|
162
|
+
const w = writeFleetMap({ fleetMapPath: paths.fleetMapPath, iapeerBin: opts.iapeerBin });
|
|
163
|
+
results.push(
|
|
164
|
+
w.action === "written"
|
|
165
|
+
? { name: "fleet-map", status: "repaired", detail: `${problem} — ${w.detail}` }
|
|
166
|
+
: { name: "fleet-map", status: "fail", detail: `${problem}; repair failed — ${w.detail}` },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
// 2. memoryd heartbeat
|
|
135
173
|
try {
|
|
136
174
|
const stat = fs.statSync(paths.heartbeatPath);
|
package/src/fleet.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet map — the personality → cwd joint between the package (ecosystem
|
|
3
|
+
* knowledge, ADR-009) and core memoryd's per-peer fragment renderer
|
|
4
|
+
* (docs/05). Written from `iapeer list --json` (the registry cwd is the
|
|
5
|
+
* FACT — iapeer 0.2.14); memoryd reads it fail-open and re-checks the
|
|
6
|
+
* mtime on every heartbeat tick, so the self-healing loop is:
|
|
7
|
+
* new peer wakes → SessionStart kick → `verify --repair` re-writes the
|
|
8
|
+
* map → memoryd renders the newcomer's fragment within a tick.
|
|
9
|
+
*
|
|
10
|
+
* `iapeer list` is READ-ONLY on the host — no test fuse needed here
|
|
11
|
+
* (the fuse class guards host MUTATIONS); deterministic tests pass a
|
|
12
|
+
* fake `iapeerBin` instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
export type FleetMapResult = {
|
|
19
|
+
action: "written" | "failed";
|
|
20
|
+
count: number;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ListedPeer = { personality?: unknown; cwd?: unknown };
|
|
25
|
+
|
|
26
|
+
export function writeFleetMap(opts: {
|
|
27
|
+
fleetMapPath: string;
|
|
28
|
+
iapeerBin?: string;
|
|
29
|
+
/** Injectable for tests. */
|
|
30
|
+
nowIso?: string;
|
|
31
|
+
}): FleetMapResult {
|
|
32
|
+
const bin = opts.iapeerBin ?? "iapeer";
|
|
33
|
+
let stdout: string;
|
|
34
|
+
try {
|
|
35
|
+
const proc = Bun.spawnSync([bin, "list", "--json"], {
|
|
36
|
+
stdout: "pipe",
|
|
37
|
+
stderr: "pipe",
|
|
38
|
+
});
|
|
39
|
+
if (proc.exitCode !== 0) {
|
|
40
|
+
return {
|
|
41
|
+
action: "failed",
|
|
42
|
+
count: 0,
|
|
43
|
+
detail:
|
|
44
|
+
(proc.stderr.toString().trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
stdout = proc.stdout.toString();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return { action: "failed", count: 0, detail: `${bin} unavailable: ${String(err)}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let listed: ListedPeer[];
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(stdout) as unknown;
|
|
55
|
+
listed = Array.isArray(raw) ? (raw as ListedPeer[]) : [];
|
|
56
|
+
} catch {
|
|
57
|
+
return { action: "failed", count: 0, detail: "iapeer list --json: unparsable output" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const peers = listed
|
|
61
|
+
.filter(
|
|
62
|
+
(p): p is { personality: string; cwd: string } =>
|
|
63
|
+
typeof p.personality === "string" &&
|
|
64
|
+
p.personality.trim() !== "" &&
|
|
65
|
+
typeof p.cwd === "string" &&
|
|
66
|
+
p.cwd.trim() !== "",
|
|
67
|
+
)
|
|
68
|
+
.map((p) => ({ personality: p.personality.trim(), cwd: p.cwd.trim() }));
|
|
69
|
+
|
|
70
|
+
const body =
|
|
71
|
+
JSON.stringify(
|
|
72
|
+
{ updatedAt: opts.nowIso ?? new Date().toISOString(), peers },
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
) + "\n";
|
|
76
|
+
fs.mkdirSync(path.dirname(opts.fleetMapPath), { recursive: true });
|
|
77
|
+
const tmp = `${opts.fleetMapPath}.tmp`;
|
|
78
|
+
fs.writeFileSync(tmp, body, "utf-8");
|
|
79
|
+
fs.renameSync(tmp, opts.fleetMapPath); // atomic — memoryd may race a read
|
|
80
|
+
return {
|
|
81
|
+
action: "written",
|
|
82
|
+
count: peers.length,
|
|
83
|
+
detail: `${peers.length} peer(s) → ${opts.fleetMapPath}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -52,6 +52,10 @@ export type MemoryPaths = {
|
|
|
52
52
|
launcherPath: string;
|
|
53
53
|
/** Sweep check-script — gates the fail-open inbox sweep (ADR-015). */
|
|
54
54
|
checkScriptPath: string;
|
|
55
|
+
/** Fleet map (personality → cwd) — written by init/update/verify --repair
|
|
56
|
+
* from `iapeer list --json`, consumed by memoryd's fragment renderer
|
|
57
|
+
* (docs/05; дыра 10.06: без карты пиры не получали paths-блок и индекс). */
|
|
58
|
+
fleetMapPath: string;
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
export function memoryPaths(
|
|
@@ -90,6 +94,7 @@ export function memoryPaths(
|
|
|
90
94
|
templatesDir: path.join(path.dirname(configFile), "templates"),
|
|
91
95
|
launcherPath: path.join(path.dirname(configFile), "memoryd-launcher.sh"),
|
|
92
96
|
checkScriptPath: path.join(path.dirname(configFile), "inbox-stale-check.sh"),
|
|
97
|
+
fleetMapPath: path.join(stateDir, "fleet.json"),
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -46,7 +46,7 @@ with zero graph connections — check with \`vault_graph\` first).
|
|
|
46
46
|
Write the BARE BODY only — no frontmatter, no links section (the post-write
|
|
47
47
|
hook stamps the 4 draft fields; the links section belongs to the Index):
|
|
48
48
|
|
|
49
|
-
Write("
|
|
49
|
+
Write("{{VAULT_PATH}}/00_Inbox/<Meaningful title>.md", "<body>")
|
|
50
50
|
|
|
51
51
|
Canon style: idiomatic vault language, academic tone, self-contained text
|
|
52
52
|
(no dialogue references, expand abbreviations on first use), no emoji.
|
|
@@ -44,7 +44,7 @@ iapeer-memory — общая память команды (агенты + чел
|
|
|
44
44
|
Пиши ГОЛОЕ ТЕЛО — без frontmatter и без секции связей (post-write хук
|
|
45
45
|
проставит 4 поля черновика; секция связей — зона Индекса):
|
|
46
46
|
|
|
47
|
-
Write("
|
|
47
|
+
Write("{{VAULT_PATH}}/00_Входящие/<Понятное название>.md", "<тело>")
|
|
48
48
|
|
|
49
49
|
Стиль канона: идиоматичный русский, академический тон, самодостаточный
|
|
50
50
|
текст (без отсылок к диалогу, аббревиатуры расшифровывай при первом
|
package/src/templates/index.ts
CHANGED
|
@@ -95,8 +95,17 @@ export function roleDoctrineTemplate(locale: LocaleId, role: RoleName): string {
|
|
|
95
95
|
return ROLES[locale][role];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Writer-guide text. With `vaultPath` the `{{VAULT_PATH}}` marker is
|
|
100
|
+
* substituted (host fact — дыра 10.06: the host-wide guide shipped the
|
|
101
|
+
* write-path as a literal placeholder, peers could not know where to
|
|
102
|
+
* write); without it the marker is preserved — that is the TEMPLATE form
|
|
103
|
+
* (materialiseTemplates keeps templates host-neutral).
|
|
104
|
+
*/
|
|
105
|
+
export function guideText(locale: LocaleId, vaultPath?: string): string {
|
|
106
|
+
const text = GUIDES[locale];
|
|
107
|
+
if (!vaultPath) return text;
|
|
108
|
+
return text.replaceAll("{{VAULT_PATH}}", vaultPath);
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
/** Stable on-disk path of a materialised role template. */
|