@agfpd/iapeer-memory 0.2.3 → 0.2.4

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.2.3",
3
+ "version": "0.2.4",
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.2.3"
30
+ "@agfpd/iapeer-memory-core": "0.2.4"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.2.0",
package/src/cli.ts CHANGED
@@ -24,6 +24,7 @@ import { cmdInit } from "./commands/init.js";
24
24
  import { cmdInstallBinary } from "./commands/install-binary.js";
25
25
  import { cmdMemoryd } from "./commands/memoryd.js";
26
26
  import { cmdMigrate } from "./commands/migrate.js";
27
+ import { cmdDreamPaths } from "./commands/dream-paths.js";
27
28
  import { cmdProvisionPeer, cmdUnprovisionPeer } from "./commands/provision-peer.js";
28
29
  import { cmdRender } from "./commands/render.js";
29
30
  import { cmdStatus } from "./commands/status.js";
@@ -61,6 +62,10 @@ Commands:
61
62
  fm-update [ops] FILE... structural frontmatter edits + attribution stamp
62
63
  migrate --source DIR move harness auto-memory into the vault
63
64
  (dry-run by default; --apply to execute)
65
+ dream-paths tick-time DreamWeaver fan-out resolution: agent
66
+ memory folders + transcript globs per runtime
67
+ from the LIVE registry (the Index shells this on
68
+ DREAM_TICK; read-only)
64
69
  render index|fragment|doctrine|guide
65
70
  render one artifact explicitly (memoryd does this
66
71
  continuously; render is the manual/scripted path)
@@ -125,6 +130,8 @@ export async function main(argv: string[]): Promise<number> {
125
130
  return cmdUpdate(rest, egress);
126
131
  case "install-binary":
127
132
  return cmdInstallBinary(rest, egress);
133
+ case "dream-paths":
134
+ return cmdDreamPaths(rest, egress);
128
135
  case "provision-peer":
129
136
  return cmdProvisionPeer(rest, egress);
130
137
  case "unprovision-peer":
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `iapeer-memory dream-paths [--iapeer-bin P]` — tick-time resolution of the
3
+ * DreamWeaver fan-out (P5 §4.3, boris-accepted form (б) + source (1)).
4
+ *
5
+ * The weekly DREAM_TICK lands in a FRESH index session; the Index shells
6
+ * THIS verb and fans DreamWeaver out over its output — one agent-memory
7
+ * subfolder per task, transcript globs riding along. Resolution happens AT
8
+ * THE TICK, never baked into the timer registration: a baked snapshot
9
+ * re-creates the «фаза D мертва, glob-skip маскирует» class one layer down.
10
+ *
11
+ * SOURCE = the LIVE registry (`iapeer list --json`), not fleet.json — the
12
+ * freshness proof (facts, 11.06): birth-provision does NOT touch fleet.json
13
+ * (writeFleetMap call sites: init/update/verify --repair only) and the
14
+ * SessionStart kick is heartbeat-gated (silent on a healthy host), so a
15
+ * peer born after the last update is INVISIBLE to fleet.json for weeks —
16
+ * the live registry is the only source that sees it. Read-as-egress: a
17
+ * legitimate live channel of the prod CLI (the refusing test handle blocks
18
+ * it; hermetic tests pass --iapeer-bin).
19
+ *
20
+ * READ-ONLY by contract: one registry list spawn + vault readdir +
21
+ * realpath. No writes, no signals, no detached spawns.
22
+ *
23
+ * Transcript path forms (host facts):
24
+ * claude — `~/.claude/projects/<slug(cwd)>/*.jsonl`; slug = every
25
+ * non-alphanumeric of the REGISTRY cwd → '-' (live form:
26
+ * /Users/macmini/.iapeer/peers/index → -Users-macmini--iapeer-peers-index;
27
+ * the registry cwd verbatim, NOT realpath — claude slugs the path the
28
+ * session launched in);
29
+ * codex — `~/.codex/sessions/**\/rollout-*.jsonl` (HOST-WIDE pool) +
30
+ * `cwdFilter` = realpath(cwd): the worker matches the payload's
31
+ * session_meta.cwd — the iapeer-contract realpath rule.
32
+ *
33
+ * Folders without a live peer get `transcripts: []` — phase D skips them
34
+ * honestly (A–C still run); peers without a memory subfolder are not in
35
+ * the fan-out (nothing to consolidate).
36
+ */
37
+
38
+ import fs from "node:fs";
39
+ import os from "node:os";
40
+ import path from "node:path";
41
+ import { getTaxonomy, isLocaleId } from "@agfpd/iapeer-memory-core";
42
+ import type { Egress } from "../egress.js";
43
+ import { queryRegistry, type FleetPeer } from "../fleet.js";
44
+
45
+ /** Claude projects-dir slug — the live disk form (ls ~/.claude/projects). */
46
+ export function claudeProjectSlug(cwd: string): string {
47
+ return cwd.replace(/[^A-Za-z0-9]/g, "-");
48
+ }
49
+
50
+ export type TranscriptSpec = {
51
+ runtime: "claude" | "codex";
52
+ glob: string;
53
+ /** codex only: the worker filters the HOST-WIDE pool by the payload's
54
+ * session_meta.cwd against this realpath (iapeer contract). */
55
+ cwdFilter?: string;
56
+ };
57
+
58
+ export function transcriptSpecs(peer: FleetPeer, home: string): TranscriptSpec[] {
59
+ const specs: TranscriptSpec[] = [];
60
+ if (peer.runtimes.includes("claude")) {
61
+ specs.push({
62
+ runtime: "claude",
63
+ glob: path.join(home, ".claude", "projects", claudeProjectSlug(peer.cwd), "*.jsonl"),
64
+ });
65
+ }
66
+ if (peer.runtimes.includes("codex")) {
67
+ let real = peer.cwd;
68
+ try {
69
+ real = fs.realpathSync(peer.cwd);
70
+ } catch {
71
+ // vanished cwd — keep the registry form; the filter simply matches nothing
72
+ }
73
+ specs.push({
74
+ runtime: "codex",
75
+ glob: path.join(home, ".codex", "sessions", "**", "rollout-*.jsonl"),
76
+ cwdFilter: real,
77
+ });
78
+ }
79
+ return specs;
80
+ }
81
+
82
+ export type DreamFolder = {
83
+ agent: string;
84
+ path: string;
85
+ transcripts: TranscriptSpec[];
86
+ };
87
+
88
+ export function buildDreamPaths(opts: {
89
+ vault: string;
90
+ agentMemoryFolder: string;
91
+ peers: FleetPeer[];
92
+ home: string;
93
+ }): DreamFolder[] {
94
+ const memoryRoot = path.join(opts.vault, opts.agentMemoryFolder);
95
+ let entries: fs.Dirent[];
96
+ try {
97
+ entries = fs.readdirSync(memoryRoot, { withFileTypes: true });
98
+ } catch {
99
+ return [];
100
+ }
101
+ const byPersonality = new Map(opts.peers.map((p) => [p.personality, p]));
102
+ return entries
103
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
104
+ .sort((a, b) => a.name.localeCompare(b.name))
105
+ .map((e) => {
106
+ const peer = byPersonality.get(e.name);
107
+ return {
108
+ agent: e.name,
109
+ path: path.join(memoryRoot, e.name),
110
+ transcripts: peer ? transcriptSpecs(peer, opts.home) : [],
111
+ };
112
+ });
113
+ }
114
+
115
+ export function cmdDreamPaths(argv: string[], egress: Egress): number {
116
+ let iapeerBin: string | undefined;
117
+ for (let i = 0; i < argv.length; i++) {
118
+ const a = argv[i];
119
+ if (a === "--iapeer-bin") iapeerBin = argv[++i];
120
+ else {
121
+ console.error(`iapeer-memory dream-paths: unknown flag: ${a}`);
122
+ return 2;
123
+ }
124
+ }
125
+
126
+ const vault = process.env.IAPEER_MEMORY_VAULT_PATH ?? "";
127
+ if (!vault) {
128
+ console.error("iapeer-memory dream-paths: IAPEER_MEMORY_VAULT_PATH is not set — not provisioned");
129
+ return 1;
130
+ }
131
+ const localeRaw = process.env.IAPEER_MEMORY_LOCALE || "en";
132
+ if (!isLocaleId(localeRaw)) {
133
+ console.error(`iapeer-memory dream-paths: unknown locale "${localeRaw}"`);
134
+ return 1;
135
+ }
136
+
137
+ const q = queryRegistry(egress, { iapeerBin });
138
+ if ("error" in q) {
139
+ // LOUD: a silent empty fan-out would re-create the masked-dead-phase
140
+ // class — the Index sees this line and reports instead of guessing.
141
+ console.error(`iapeer-memory dream-paths: live registry unavailable — ${q.error}`);
142
+ return 1;
143
+ }
144
+
145
+ const folders = buildDreamPaths({
146
+ vault,
147
+ agentMemoryFolder: getTaxonomy(localeRaw).folders.agentMemory,
148
+ peers: q.peers,
149
+ home: os.homedir(),
150
+ });
151
+ console.log(JSON.stringify({ vault, folders }, null, 2));
152
+ return 0;
153
+ }
package/src/fleet.ts CHANGED
@@ -70,65 +70,72 @@ export function readFleetMap(fleetMapPath: string): FleetPeer[] | null {
70
70
  }
71
71
  }
72
72
 
73
- export function writeFleetMap(
73
+ /** Live-registry query — the ONE place `iapeer list --json` is parsed.
74
+ * Shared by writeFleetMap (the persisted map) and dream-paths (the
75
+ * tick-time resolution; freshness fact: birth does NOT touch fleet.json
76
+ * and the SessionStart kick is heartbeat-gated, so the LIVE registry is
77
+ * the only source that sees a newborn before the next update). */
78
+ export function queryRegistry(
74
79
  egress: Egress,
75
- opts: {
76
- fleetMapPath: string;
77
- iapeerBin?: string;
78
- /** Injectable for tests. */
79
- nowIso?: string;
80
- },
81
- ): FleetMapResult {
80
+ opts: { iapeerBin?: string },
81
+ ): { peers: FleetPeer[] } | { error: string } {
82
82
  const bin = opts.iapeerBin ?? IAPEER_BIN;
83
83
  const proc = egress.spawnSync([bin, "list", "--json"], {
84
84
  explicitBin: opts.iapeerBin !== undefined,
85
85
  });
86
86
  if (proc.refused) {
87
- return {
88
- action: "failed",
89
- count: 0,
90
- detail: "live-registry query suppressed (test sandbox) — pass a fake iapeerBin",
91
- };
87
+ return { error: "live-registry query suppressed (test sandbox) — pass a fake iapeerBin" };
92
88
  }
93
89
  if (proc.spawnError) {
94
- return { action: "failed", count: 0, detail: `${bin} unavailable: ${proc.spawnError}` };
90
+ return { error: `${bin} unavailable: ${proc.spawnError}` };
95
91
  }
96
92
  if (proc.exitCode !== 0) {
97
- return {
98
- action: "failed",
99
- count: 0,
100
- detail: (proc.stderr.trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160),
101
- };
93
+ return { error: (proc.stderr.trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160) };
102
94
  }
103
- const stdout = proc.stdout;
104
-
105
95
  let listed: ListedPeer[];
106
96
  try {
107
- const raw = JSON.parse(stdout) as unknown;
97
+ const raw = JSON.parse(proc.stdout) as unknown;
108
98
  listed = Array.isArray(raw) ? (raw as ListedPeer[]) : [];
109
99
  } catch {
110
- return { action: "failed", count: 0, detail: "iapeer list --json: unparsable output" };
100
+ return { error: "iapeer list --json: unparsable output" };
111
101
  }
102
+ return {
103
+ peers: listed
104
+ .filter(
105
+ (p): p is ListedPeer & { personality: string; cwd: string } =>
106
+ typeof p.personality === "string" &&
107
+ p.personality.trim() !== "" &&
108
+ typeof p.cwd === "string" &&
109
+ p.cwd.trim() !== "",
110
+ )
111
+ .map((p) => ({
112
+ personality: p.personality.trim(),
113
+ cwd: p.cwd.trim(),
114
+ runtimes: [
115
+ ...new Set(
116
+ (Array.isArray(p.runtimes) ? p.runtimes : [])
117
+ .map((r) => (typeof r?.runtime === "string" ? r.runtime.trim() : ""))
118
+ .filter(Boolean),
119
+ ),
120
+ ],
121
+ })),
122
+ };
123
+ }
112
124
 
113
- const peers: FleetPeer[] = listed
114
- .filter(
115
- (p): p is ListedPeer & { personality: string; cwd: string } =>
116
- typeof p.personality === "string" &&
117
- p.personality.trim() !== "" &&
118
- typeof p.cwd === "string" &&
119
- p.cwd.trim() !== "",
120
- )
121
- .map((p) => ({
122
- personality: p.personality.trim(),
123
- cwd: p.cwd.trim(),
124
- runtimes: [
125
- ...new Set(
126
- (Array.isArray(p.runtimes) ? p.runtimes : [])
127
- .map((r) => (typeof r?.runtime === "string" ? r.runtime.trim() : ""))
128
- .filter(Boolean),
129
- ),
130
- ],
131
- }));
125
+ export function writeFleetMap(
126
+ egress: Egress,
127
+ opts: {
128
+ fleetMapPath: string;
129
+ iapeerBin?: string;
130
+ /** Injectable for tests. */
131
+ nowIso?: string;
132
+ },
133
+ ): FleetMapResult {
134
+ const q = queryRegistry(egress, { iapeerBin: opts.iapeerBin });
135
+ if ("error" in q) {
136
+ return { action: "failed", count: 0, detail: q.error };
137
+ }
138
+ const peers = q.peers;
132
139
 
133
140
  const body =
134
141
  JSON.stringify(
@@ -51,13 +51,17 @@ different world.
51
51
  scriber thread stalled: place the stale drafts UNVETTED by the usual
52
52
  rules; \`needs_review: true\` already travels with each file. The
53
53
  Scriber re-vets them with the next PERMANENT_BATCH once alive.
54
- - **DREAM_TICK** (notifier timer, weekly) — fan out DreamWeaver over the
55
- agent-memory subfolders (including your own), strictly one folder per
56
- task, sequentially. DreamWeaver takes tasks ONLY from you (the one
57
- exception: a folder's owner may task it on their own folder); put
58
- everything it needs INTO the task. Task: \`{agent, path, mode,
59
- transcripts_window_days}\` consolidation report; archive what it
60
- deprecated, act on its \`attention\` blocks yourself.
54
+ - **DREAM_TICK** (notifier timer, weekly) — run \`iapeer-memory
55
+ dream-paths\` (read-only; the LIVE registry at tick time) and fan out
56
+ DreamWeaver over the folders of its output (including your own),
57
+ strictly one folder per task, sequentially. DreamWeaver takes tasks ONLY
58
+ from you (the one exception: a folder's owner may task it on their own
59
+ folder). Task: \`{agent, path, mode, transcripts_window_days,
60
+ transcripts}\` copy \`transcripts\` from the verb's output AS IS
61
+ (globs + the codex cwdFilter; path forms are the code's zone, not
62
+ yours). A verb error = report to the owner, never guess the fleet. On
63
+ the consolidation report: archive what it deprecated, act on its
64
+ \`attention\` blocks yourself.
61
65
  - **Direct IAP** from agents or the human — structure questions; never
62
66
  run searches for others (they have their own vault tools).
63
67
 
@@ -237,7 +241,7 @@ on-demand from a folder's OWNER for their own folder only. One task = one
237
241
  clean window = ONE outbound message (the final consolidation report to the
238
242
  task sender). Discipline: touch ONLY the folder named in the task.
239
243
 
240
- Task: \`{agent, path, mode, transcripts_window_days}\`.
244
+ Task: \`{agent, path, mode, transcripts_window_days, transcripts}\`.
241
245
 
242
246
  ## The four phases
243
247
 
@@ -253,11 +257,14 @@ Task: \`{agent, path, mode, transcripts_window_days}\`.
253
257
  mentions in bodies; read the targets; on a clear mismatch (file gone,
254
258
  function renamed) write an updated note and flip the old one to the
255
259
  outdated token. LOCAL checks only.
256
- - **D — Transcript scan.** Read the runtime's session transcripts for the
257
- window (\`transcripts_window_days\`, adapter-scoped paths; no paths/empty
258
- glob skip the phase). Find user phrases that formulate a rule with 2+
259
- explicit confirmations in different sessions; check against existing
260
- feedback notes; write new notes with quotes for what's missing.
260
+ - **D — Transcript scan.** Read the session transcripts for the window
261
+ (\`transcripts_window_days\`) per the task's \`transcripts\`: each entry
262
+ is a glob; for \`runtime: codex\` the store is HOST-WIDE take ONLY the
263
+ sessions whose \`session_meta.cwd\` equals the entry's \`cwdFilter\`
264
+ (foreign cwds are foreign memory). No entries / empty glob → skip the
265
+ phase. Find user phrases that formulate a rule with 2+ explicit
266
+ confirmations in different sessions; check against existing feedback
267
+ notes; write new notes with quotes for what's missing.
261
268
 
262
269
  ## Hard limits
263
270
 
@@ -44,13 +44,16 @@ locale: ru
44
44
  НЕВЫЧИТАННЫМИ по обычным правилам; \`needs_review: true\` уже едет с
45
45
  каждым файлом. Scriber довычитает их со следующей PERMANENT_BATCH,
46
46
  когда оживёт.
47
- - **DREAM_TICK** (notifier-таймер, еженедельно) — fan-out DreamWeaver по
48
- подпапкам оперативки (включая твою), строго одна папка на задачу,
49
- последовательно. DreamWeaver берёт задачи ТОЛЬКО от тебя (единственное
50
- исключение: владелец папки на свою собственную); клади в задачу всё
51
- необходимое. Задача: \`{agent, path, mode, transcripts_window_days}\`
52
- отчёт консолидации; архивируй устаревшее по его отчёту, его
53
- \`attention\`-блоки отрабатывай сам.
47
+ - **DREAM_TICK** (notifier-таймер, еженедельно) — выполни
48
+ \`iapeer-memory dream-paths\` (read-only; живой реестр на момент тика) и
49
+ fan-out DreamWeaver по папкам его вывода (включая твою), строго одна
50
+ папка на задачу, последовательно. DreamWeaver берёт задачи ТОЛЬКО от
51
+ тебя (единственное исключение: владелец папки на свою собственную).
52
+ Задача: \`{agent, path, mode, transcripts_window_days, transcripts}\`
53
+ \`transcripts\` перекладывай из вывода verb'а КАК ЕСТЬ (глобы + codex
54
+ cwdFilter; формы путей — зона кода, не твоя). Ошибка verb'а = доложи
55
+ владельцу, флот не угадывай. По отчёту консолидации архивируй
56
+ устаревшее, \`attention\`-блоки отрабатывай сам.
54
57
  - **Прямые IAP** от агентов и человека — вопросы структуры; чужие поиски
55
58
  не выполняешь (у агентов свои vault-тулы).
56
59
 
@@ -224,7 +227,7 @@ on-demand от ВЛАДЕЛЬЦА папки — только на его соб
224
227
  одно чистое окно = ОДНО исходящее (финальный отчёт консолидации
225
228
  постановщику). Дисциплина: трогай ТОЛЬКО папку из задачи.
226
229
 
227
- Задача: \`{agent, path, mode, transcripts_window_days}\`.
230
+ Задача: \`{agent, path, mode, transcripts_window_days, transcripts}\`.
228
231
 
229
232
  ## Четыре фазы
230
233
 
@@ -239,11 +242,14 @@ on-demand от ВЛАДЕЛЬЦА папки — только на его соб
239
242
  env-переменных; прочитай цели; при явном расхождении (файла нет, функция
240
243
  переименована) — новая updated-заметка + старая в «устарело». Только
241
244
  ЛОКАЛЬНЫЕ проверки.
242
- - **D — Скан транскриптов.** Прочитай транскрипты сессий рантайма за окно
243
- (\`transcripts_window_days\`, adapter-scoped пути; путей нет / glob пуст —
244
- фаза пропускается). Найди user-фразы, формулирующие правило, с 2+ явными
245
- подтверждениями в разных сессиях; сверь с существующими feedback-заметками;
246
- недостающееновой заметкой с цитатами.
245
+ - **D — Скан транскриптов.** Прочитай транскрипты сессий за окно
246
+ (\`transcripts_window_days\`) по \`transcripts\` из задачи: для каждой
247
+ записи glob; у \`runtime: codex\` хранилище HOST-WIDE, бери ТОЛЬКО
248
+ сессии, чей \`session_meta.cwd\` равен \`cwdFilter\` записи (чужие cwd —
249
+ чужая память). Записей нет / glob пуст фаза пропускается. Найди
250
+ user-фразы, формулирующие правило, с 2+ явными подтверждениями в разных
251
+ сессиях; сверь с существующими feedback-заметками; недостающее — новой
252
+ заметкой с цитатами.
247
253
 
248
254
  ## Жёсткие границы
249
255
 
package/src/watcher.ts CHANGED
@@ -201,9 +201,12 @@ export function dreamTimerMessage(opts?: {
201
201
  return JSON.stringify({
202
202
  when: opts?.cron ?? "0 4 * * 1",
203
203
  message:
204
- "DREAM_TICK: weekly agent-memory consolidation. Fan out DreamWeaver " +
205
- "over the agent-memory subfolders (including your own), strictly one " +
206
- "folder per task, sequentially per your doctrine.",
204
+ "DREAM_TICK: weekly agent-memory consolidation. Run `iapeer-memory " +
205
+ "dream-paths` (read-only) and fan out DreamWeaver over its folders — " +
206
+ "strictly one folder per task, sequentially, carrying that folder's " +
207
+ "`transcripts` (globs + codex cwdFilter) into the task — per your " +
208
+ "doctrine. The verb resolves the LIVE registry at tick time; an error " +
209
+ "line from it = report, do not guess the fleet.",
207
210
  target: opts?.target ?? "index",
208
211
  id: opts?.id ?? DREAM_TRIGGER_ID,
209
212
  });