@agfpd/iapeer-memory 0.1.11 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer-memory",
3
- "version": "0.1.11",
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.11"
30
+ "@agfpd/iapeer-memory-core": "0.1.13"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.2.0",
@@ -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
- const guidePath = writeHostWideGuideFragment(iapeerDir, guideText(locale));
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
 
@@ -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) => {
@@ -166,7 +166,14 @@ function renderGuide(argv: string[]): number {
166
166
  "the whole fleet — the target is never implicit)",
167
167
  );
168
168
  }
169
- const text = fs.readFileSync(source, "utf-8");
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;
@@ -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
 
@@ -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("<vault>/00_Inbox/<Meaningful title>.md", "<body>")
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("<vault>/00_Входящие/<Понятное название>.md", "<тело>")
47
+ Write("{{VAULT_PATH}}/00_Входящие/<Понятное название>.md", "<тело>")
48
48
 
49
49
  Стиль канона: идиоматичный русский, академический тон, самодостаточный
50
50
  текст (без отсылок к диалогу, аббревиатуры расшифровывай при первом
@@ -95,8 +95,17 @@ export function roleDoctrineTemplate(locale: LocaleId, role: RoleName): string {
95
95
  return ROLES[locale][role];
96
96
  }
97
97
 
98
- export function guideText(locale: LocaleId): string {
99
- return GUIDES[locale];
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. */