@agfpd/iapeer-memory 0.1.12 → 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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Direct codex session surface — per-peer MCP via the PROJECT-LOCAL
3
+ * `<cwd>/.codex/config.toml` (ADR-009 v1.2; требование Артура №3 «глобально
4
+ * не класть» — proven satisfiable by the iapeer smoke 10.06 ~18:40: a
5
+ * project-local `[mcp_servers]` block in a TRUSTED cwd is read by
6
+ * `codex mcp list` AND imported end-to-end into exec sessions; the trust
7
+ * record is written by the core's provision, keyed on the realpath).
8
+ *
9
+ * Block form mirrors the core's PROVEN host-wide `[mcp_servers.iapeer]`
10
+ * block (iapeer src/init/index.ts writeCodexMcpConfig — live on the whole
11
+ * codex fleet), pointed at memoryd:
12
+ *
13
+ * - `url` — the memoryd HTTP-MCP endpoint. TOML carries no env
14
+ * substitution: the LITERAL port is baked at provision time from the
15
+ * host config (IAPEER_MEMORY_MCP_PORT, default 8766); a port change is
16
+ * re-baked by the update/verify sweep;
17
+ * - `default_tools_approval_mode = "approve"` — no per-tool dialog;
18
+ * - `bearer_token_env_var = "IAPEER_BEARER"` — flips codex's authStatus
19
+ * so the tools import (#21532/#4707 workaround); the value is the
20
+ * NON-SECRET dummy the core's launch exports to EVERY codex peer;
21
+ * - `env_http_headers."X-IAPeer-Identity" = "PEER_IDENTITY"` — per-peer
22
+ * identity from the launch env (`codex-<personality>`; memoryd's parser
23
+ * strips the runtime prefix). A codex session started OUTSIDE an iapeer
24
+ * launch carries no PEER_IDENTITY — same unattributed fallback the
25
+ * claude env form has.
26
+ *
27
+ * The file CARRIES FOREIGN CONTENT — the core's native-memory lever writes
28
+ * `[features] memories = false` here, the operator may keep own sections.
29
+ * Merge = line-based section surgery on OUR header namespace only
30
+ * (`[mcp_servers.iapeer-memory]` + its subsections), atomic write; unlike
31
+ * the core's append-if-absent we REPLACE a drifted block (repair duty,
32
+ * требование №2). Hooks/skills for codex are the P5 experiment — MCP is
33
+ * deliberately the only codex surface here.
34
+ */
35
+
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+ import type { SurfaceOutcome } from "./claude.js";
39
+
40
+ export const CODEX_MCP_SECTION = "mcp_servers.iapeer-memory";
41
+ const SECTION_HEADER_RE = /^\s*\[/;
42
+ const OUR_HEADER_RE = /^\s*\[mcp_servers\.iapeer-memory(\.[A-Za-z0-9_.-]+)?\]\s*$/;
43
+
44
+ export function codexConfigPath(cwd: string): string {
45
+ return path.join(cwd, ".codex", "config.toml");
46
+ }
47
+
48
+ export function expectedCodexBlock(port: number): string {
49
+ return [
50
+ `[${CODEX_MCP_SECTION}]`,
51
+ `url = "http://127.0.0.1:${port}/mcp"`,
52
+ `default_tools_approval_mode = "approve"`,
53
+ `bearer_token_env_var = "IAPEER_BEARER"`,
54
+ "",
55
+ `[${CODEX_MCP_SECTION}.env_http_headers]`,
56
+ `"X-IAPeer-Identity" = "PEER_IDENTITY"`,
57
+ "",
58
+ ].join("\n");
59
+ }
60
+
61
+ function writeFileAtomic(filePath: string, content: string): void {
62
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
+ const tmp = `${filePath}.tmp`;
64
+ fs.writeFileSync(tmp, content, "utf-8");
65
+ fs.renameSync(tmp, filePath);
66
+ }
67
+
68
+ /** Strip every section under OUR header namespace; foreign lines unchanged. */
69
+ function withoutOurSections(lines: string[]): string[] {
70
+ const kept: string[] = [];
71
+ let inOurs = false;
72
+ for (const line of lines) {
73
+ if (SECTION_HEADER_RE.test(line)) inOurs = OUR_HEADER_RE.test(line);
74
+ if (!inOurs) kept.push(line);
75
+ }
76
+ // drop the trailing blank run our removal may have exposed
77
+ while (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop();
78
+ return kept;
79
+ }
80
+
81
+ function hasOurSection(text: string): boolean {
82
+ return text.split("\n").some((l) => OUR_HEADER_RE.test(l));
83
+ }
84
+
85
+ export function mergeCodexMcp(opts: { cwd: string; port: number }): SurfaceOutcome {
86
+ const configPath = codexConfigPath(opts.cwd);
87
+ let text = "";
88
+ try {
89
+ text = fs.readFileSync(configPath, "utf-8");
90
+ } catch {
91
+ // no config yet → create
92
+ }
93
+ const lines = text.length ? text.split("\n") : [];
94
+ const foreign = withoutOurSections(lines);
95
+ const next =
96
+ (foreign.length ? `${foreign.join("\n")}\n\n` : "") + expectedCodexBlock(opts.port);
97
+ if (next === text) {
98
+ return { surface: "mcp", action: "already", path: configPath };
99
+ }
100
+ writeFileAtomic(configPath, next);
101
+ return { surface: "mcp", action: "written", path: configPath };
102
+ }
103
+
104
+ export function removeCodexMcp(opts: { cwd: string }): SurfaceOutcome {
105
+ const configPath = codexConfigPath(opts.cwd);
106
+ let text: string;
107
+ try {
108
+ text = fs.readFileSync(configPath, "utf-8");
109
+ } catch {
110
+ return { surface: "mcp", action: "absent", path: configPath };
111
+ }
112
+ if (!hasOurSection(text)) {
113
+ return { surface: "mcp", action: "absent", path: configPath };
114
+ }
115
+ const foreign = withoutOurSections(text.split("\n"));
116
+ if (foreign.every((l) => l.trim() === "")) {
117
+ // nothing but our block lived here — leave the cwd exactly as found
118
+ fs.unlinkSync(configPath);
119
+ return { surface: "mcp", action: "removed", path: configPath, detail: "file removed (empty after our block)" };
120
+ }
121
+ writeFileAtomic(configPath, `${foreign.join("\n")}\n`);
122
+ return { surface: "mcp", action: "removed", path: configPath };
123
+ }
124
+
125
+ export function provisionCodexPeer(opts: { cwd: string; port: number }): SurfaceOutcome[] {
126
+ return [mergeCodexMcp(opts)];
127
+ }
128
+
129
+ export function unprovisionCodexPeer(opts: { cwd: string }): SurfaceOutcome[] {
130
+ return [removeCodexMcp(opts)];
131
+ }
132
+
133
+ /** Read-only drift check (verify's eye, D3): the expected block must sit in
134
+ * the project-local config byte-exact. */
135
+ export function checkCodexPeer(opts: {
136
+ cwd: string;
137
+ port: number;
138
+ }): Array<{ surface: SurfaceOutcome["surface"]; ok: boolean; detail: string }> {
139
+ const configPath = codexConfigPath(opts.cwd);
140
+ let text: string;
141
+ try {
142
+ text = fs.readFileSync(configPath, "utf-8");
143
+ } catch {
144
+ return [{ surface: "mcp", ok: false, detail: `no codex config at ${configPath}` }];
145
+ }
146
+ const expected = expectedCodexBlock(opts.port);
147
+ const ok = text.includes(expected);
148
+ return [
149
+ ok
150
+ ? { surface: "mcp", ok: true, detail: `[${CODEX_MCP_SECTION}] block in place` }
151
+ : hasOurSection(text)
152
+ ? { surface: "mcp", ok: false, detail: `[${CODEX_MCP_SECTION}] block drifted in ${configPath}` }
153
+ : { surface: "mcp", ok: false, detail: `[${CODEX_MCP_SECTION}] block missing in ${configPath}` },
154
+ ];
155
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Host-wide provision lock — the iapeer v1.2 contract obliges the provision
3
+ * command to TOLERATE PARALLEL CALLS (the locking the plugin manager used to
4
+ * give moved to the provider; §7 requirement 3). The core may fire
5
+ * provision-peer concurrently (peer births race sweeps); two unsynchronised
6
+ * read-merge-writes of the SAME settings.json would lose one writer's keys.
7
+ *
8
+ * Form: mkdir-based exclusive lock (atomic on every POSIX fs, no flock in
9
+ * Bun's stable API). One lock for the whole host — provision bodies are
10
+ * milliseconds of file I/O, serialising them is simpler and strictly safer
11
+ * than per-cwd granularity. Stale detection: a lock directory older than
12
+ * STALE_MS belongs to a crashed run — broken and re-taken (provision is
13
+ * idempotent by contract, a double-run repairs, never corrupts).
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+
19
+ const RETRY_MS = 50;
20
+ const DEFAULT_TIMEOUT_MS = 15_000;
21
+ export const STALE_MS = 120_000;
22
+
23
+ export type LockResult<T> =
24
+ | { acquired: true; result: T }
25
+ | { acquired: false; detail: string };
26
+
27
+ function tryTake(lockDir: string): boolean {
28
+ try {
29
+ fs.mkdirSync(lockDir); // atomic: EEXIST when held
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function breakIfStale(lockDir: string): void {
37
+ try {
38
+ const stat = fs.statSync(lockDir);
39
+ if (Date.now() - stat.mtimeMs > STALE_MS) fs.rmdirSync(lockDir);
40
+ } catch {
41
+ // raced away or unreadable — the next tryTake decides
42
+ }
43
+ }
44
+
45
+ export function withProvisionLock<T>(opts: {
46
+ stateDir: string;
47
+ fn: () => T;
48
+ timeoutMs?: number;
49
+ }): LockResult<T> {
50
+ const lockDir = path.join(opts.stateDir, "provision.lock.d");
51
+ fs.mkdirSync(opts.stateDir, { recursive: true });
52
+ const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
53
+ while (!tryTake(lockDir)) {
54
+ breakIfStale(lockDir);
55
+ if (Date.now() >= deadline) {
56
+ return {
57
+ acquired: false,
58
+ detail: `provision lock busy for ${Math.round((opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 1000)}s (${lockDir}) — another provision hung? stale locks self-break after ${STALE_MS / 1000}s`,
59
+ };
60
+ }
61
+ Bun.sleepSync(RETRY_MS);
62
+ }
63
+ try {
64
+ return { acquired: true, result: opts.fn() };
65
+ } finally {
66
+ try {
67
+ fs.rmdirSync(lockDir);
68
+ } catch {
69
+ // already gone (stale-broken by a peer) — nothing to release
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Fleet-wide surfaces sweep — the package's own rail over fleet.json
3
+ * (ADR-009 v1.2). The core's birth-hook covers NEWBORNS via the slot's
4
+ * provision command; everything fleet-wide (init coverage of the existing
5
+ * fleet, update's «всё на местах» duty, verify --repair self-healing) walks
6
+ * the fleet map HERE — peer × session-runtime, claude and codex forms.
7
+ *
8
+ * Session runtimes are exactly {claude, codex}: telegram/notifier and other
9
+ * infra runtimes carry no session config surfaces. A peer entry without a
10
+ * runtimes array (pre-v1.2 map) is SKIPPED and reported — the next map
11
+ * re-write (same command) picks it up.
12
+ *
13
+ * The caller holds the provision lock around the WHOLE sweep (one
14
+ * acquisition, not per-peer — the sweep body is pure file I/O).
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import {
19
+ checkClaudePeer,
20
+ provisionClaudePeer,
21
+ unprovisionClaudePeer,
22
+ type SurfaceOutcome,
23
+ } from "./claude.js";
24
+ import { checkCodexPeer, provisionCodexPeer, unprovisionCodexPeer } from "./codex.js";
25
+ import type { FleetPeer } from "../fleet.js";
26
+
27
+ export const SESSION_RUNTIMES = ["claude", "codex"] as const;
28
+ export type SessionRuntime = (typeof SESSION_RUNTIMES)[number];
29
+
30
+ export type PeerSweepResult = {
31
+ personality: string;
32
+ runtime: SessionRuntime;
33
+ cwd: string;
34
+ /** worst action across the peer-runtime's surfaces */
35
+ ok: boolean;
36
+ outcomes: SurfaceOutcome[];
37
+ };
38
+
39
+ export type SweepSummary = {
40
+ results: PeerSweepResult[];
41
+ /** peers skipped: no session runtimes in the map entry / vanished cwd */
42
+ skipped: Array<{ personality: string; reason: string }>;
43
+ };
44
+
45
+ function sessionRuntimesOf(peer: FleetPeer): SessionRuntime[] {
46
+ return SESSION_RUNTIMES.filter((r) => peer.runtimes.includes(r));
47
+ }
48
+
49
+ export function sweepProvision(opts: {
50
+ fleet: FleetPeer[];
51
+ hooksDir: string;
52
+ port: number;
53
+ }): SweepSummary {
54
+ const results: PeerSweepResult[] = [];
55
+ const skipped: SweepSummary["skipped"] = [];
56
+ for (const peer of opts.fleet) {
57
+ const runtimes = sessionRuntimesOf(peer);
58
+ if (runtimes.length === 0) {
59
+ skipped.push({
60
+ personality: peer.personality,
61
+ reason: peer.runtimes.length
62
+ ? `no session runtime (${peer.runtimes.join(",")})`
63
+ : "no runtimes in fleet map (pre-v1.2 entry) — re-write the map",
64
+ });
65
+ continue;
66
+ }
67
+ if (!fs.existsSync(peer.cwd)) {
68
+ skipped.push({ personality: peer.personality, reason: `cwd missing: ${peer.cwd}` });
69
+ continue;
70
+ }
71
+ for (const runtime of runtimes) {
72
+ const outcomes =
73
+ runtime === "codex"
74
+ ? provisionCodexPeer({ cwd: peer.cwd, port: opts.port })
75
+ : provisionClaudePeer({
76
+ cwd: peer.cwd,
77
+ hooksDir: opts.hooksDir,
78
+ port: opts.port,
79
+ personality: peer.personality,
80
+ });
81
+ results.push({
82
+ personality: peer.personality,
83
+ runtime,
84
+ cwd: peer.cwd,
85
+ ok: outcomes.every((o) => o.action !== "failed"),
86
+ outcomes,
87
+ });
88
+ }
89
+ }
90
+ return { results, skipped };
91
+ }
92
+
93
+ export function sweepUnprovision(opts: { fleet: FleetPeer[] }): SweepSummary {
94
+ const results: PeerSweepResult[] = [];
95
+ const skipped: SweepSummary["skipped"] = [];
96
+ for (const peer of opts.fleet) {
97
+ const runtimes = sessionRuntimesOf(peer);
98
+ if (runtimes.length === 0) {
99
+ skipped.push({ personality: peer.personality, reason: "no session runtime" });
100
+ continue;
101
+ }
102
+ // a vanished cwd is fine on the off-path — surfaces report `absent`
103
+ for (const runtime of runtimes) {
104
+ const outcomes =
105
+ runtime === "codex"
106
+ ? unprovisionCodexPeer({ cwd: peer.cwd })
107
+ : unprovisionClaudePeer({ cwd: peer.cwd });
108
+ results.push({
109
+ personality: peer.personality,
110
+ runtime,
111
+ cwd: peer.cwd,
112
+ ok: outcomes.every((o) => o.action !== "failed"),
113
+ outcomes,
114
+ });
115
+ }
116
+ }
117
+ return { results, skipped };
118
+ }
119
+
120
+ export type PeerCheckResult = {
121
+ personality: string;
122
+ runtime: SessionRuntime;
123
+ cwd: string;
124
+ ok: boolean;
125
+ problems: string[];
126
+ };
127
+
128
+ export function checkFleetSurfaces(opts: {
129
+ fleet: FleetPeer[];
130
+ hooksDir: string;
131
+ port: number;
132
+ }): { checks: PeerCheckResult[]; skipped: Array<{ personality: string; reason: string }> } {
133
+ const checks: PeerCheckResult[] = [];
134
+ const skipped: Array<{ personality: string; reason: string }> = [];
135
+ for (const peer of opts.fleet) {
136
+ const runtimes = sessionRuntimesOf(peer);
137
+ if (runtimes.length === 0) {
138
+ skipped.push({
139
+ personality: peer.personality,
140
+ reason: peer.runtimes.length
141
+ ? `no session runtime (${peer.runtimes.join(",")})`
142
+ : "no runtimes in fleet map (pre-v1.2 entry)",
143
+ });
144
+ continue;
145
+ }
146
+ if (!fs.existsSync(peer.cwd)) {
147
+ skipped.push({ personality: peer.personality, reason: `cwd missing: ${peer.cwd}` });
148
+ continue;
149
+ }
150
+ for (const runtime of runtimes) {
151
+ const surfaceChecks =
152
+ runtime === "codex"
153
+ ? checkCodexPeer({ cwd: peer.cwd, port: opts.port })
154
+ : checkClaudePeer({
155
+ cwd: peer.cwd,
156
+ hooksDir: opts.hooksDir,
157
+ port: opts.port,
158
+ personality: peer.personality,
159
+ });
160
+ checks.push({
161
+ personality: peer.personality,
162
+ runtime,
163
+ cwd: peer.cwd,
164
+ ok: surfaceChecks.every((c) => c.ok),
165
+ problems: surfaceChecks.filter((c) => !c.ok).map((c) => `${c.surface}: ${c.detail}`),
166
+ });
167
+ }
168
+ }
169
+ return { checks, skipped };
170
+ }
@@ -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. */
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Embedded skill files — the DIRECT-surface form of the four session skills
3
+ * (ADR-009 v1.2: direct per-peer surfaces instead of the plugin socket).
4
+ * Bodies are the boris-accepted plugin skills (adapters/claude/skills, spot-
5
+ * checked against the live CLI 10.06) with exactly two deltas:
6
+ *
7
+ * 1. names are namespaced `iapeer-memory-*` (boris design input: direct
8
+ * skills lose the plugin namespace `/iapeer-memory:name` — the prefix
9
+ * replaces it; the `copywriter` collision class);
10
+ * 2. "plugin" wording → "session surfaces" where it described the socket
11
+ * form (the socket is now files merged into the peer's cwd).
12
+ *
13
+ * provision-peer materialises them to `<cwd>/.claude/skills/<name>/SKILL.md`
14
+ * (bytes-compare, package-owned — overwritten on version change; the
15
+ * `iapeer-memory-` directory prefix is OUR namespace, unprovision removes
16
+ * every directory matching it).
17
+ */
18
+
19
+ export type SkillName =
20
+ | "iapeer-memory-init"
21
+ | "iapeer-memory-status"
22
+ | "iapeer-memory-migrate"
23
+ | "iapeer-memory-distill";
24
+
25
+ export const SKILL_NAMES: readonly SkillName[] = [
26
+ "iapeer-memory-init",
27
+ "iapeer-memory-status",
28
+ "iapeer-memory-migrate",
29
+ "iapeer-memory-distill",
30
+ ] as const;
31
+
32
+ /** Directory-name prefix that marks a skill directory as OURS (the removal
33
+ * glob of unprovision — the namespace promise of the `iapeer-memory-*`
34
+ * naming). */
35
+ export const SKILL_DIR_PREFIX = "iapeer-memory-";
36
+
37
+ const SKILL_INIT = `---
38
+ name: iapeer-memory-init
39
+ description: "Use when the user asks to install, provision or initialize iapeer-memory on this host (\\"set up iapeer-memory\\", \\"init memory\\", \\"provision the vault\\"). Thin facade over \`iapeer-memory init\`: the procedure lives in the package CLI, not here."
40
+ allowed-tools: ["Bash", "AskUserQuestion"]
41
+ ---
42
+
43
+ # Provision iapeer-memory on this host
44
+
45
+ The session surfaces are only a socket (ADR-009) — provisioning is owned by
46
+ the package CLI. Do not improvise installation steps around it.
47
+
48
+ 1. Locate the CLI: \`command -v iapeer-memory || ls ~/.local/bin/iapeer-memory\`.
49
+ Missing → run via \`npx @agfpd/iapeer-memory\` instead.
50
+ 2. Init is two-mode. On a tty it prompts; your Bash calls have NO tty, so
51
+ without \`--vault\` init refuses (silently provisioning a default storage
52
+ path is forbidden). Collect the answers from the user first
53
+ (AskUserQuestion), then run:
54
+ \`iapeer-memory init --vault PATH --locale en|ru
55
+ [--embedding-endpoint URL] [--reranker-endpoint URL]\`.
56
+ Do NOT ask for the human owner: init reads the iapeer registry and uses
57
+ the single natural peer by itself (don't ask what the stack already
58
+ knows). Pass \`--human NAME\` only when the registry can't answer (zero or
59
+ several natural peers) and the user wants a human role.
60
+ 3. Init prints a step table (deps → vault → config → binary → templates →
61
+ roles → fleet → watcher → surfaces → slot → sweep → guide) and is
62
+ idempotent: on exit 1 re-running init is the official repair path,
63
+ together with \`iapeer-memory verify --repair\`.
64
+ 4. A host that is already provisioned and only version-stale wants the
65
+ update story, not init: \`npx @agfpd/iapeer-memory@latest update\`.
66
+
67
+ After success, check the chain with the \`iapeer-memory-status\` skill.
68
+ (\`iapeer onboard\` runs this same init from the core's host phase —
69
+ full-stack onboarding already covers memory.)
70
+ `;
71
+
72
+ const SKILL_STATUS = `---
73
+ name: iapeer-memory-status
74
+ description: "Use when the user asks for the iapeer-memory status (\\"memory status\\", \\"is the vault index alive\\", \\"check iapeer-memory\\", \\"is memoryd running\\"). Read-only facade over \`iapeer-memory status\`: package ↔ surfaces link first, then the CLI's own diagnostics. Never repairs anything."
75
+ allowed-tools: ["Bash"]
76
+ ---
77
+
78
+ # iapeer-memory status — read-only diagnostics
79
+
80
+ The session surfaces are the socket, the package is the system (ADR-009).
81
+ This skill's first duty is to DIAGNOSE A BROKEN LINK between them — a
82
+ session whose surfaces are wired but whose system is missing must say so
83
+ explicitly.
84
+
85
+ 1. **Socket → system link**: \`command -v iapeer-memory || ls ~/.local/bin/iapeer-memory\`.
86
+ Missing → report: "session surfaces present, package missing — the socket
87
+ has no system behind it; run: npx @agfpd/iapeer-memory init". Stop here.
88
+ 2. **Everything else**: run \`iapeer-memory status\` and relay its table —
89
+ verify checks (config, memory-slot, memoryd heartbeat, notifier watcher,
90
+ role doctrine versions, per-peer surfaces), slot-file, mcp-endpoint
91
+ probe, search pipeline, inbox load. Exit 1 = something needs attention.
92
+
93
+ Reading the table: \`search\` shows the LIVE per-component pipeline from the
94
+ running memoryd (bm25/embedding/reranker/graph) and falls back to the
95
+ static config view when memoryd is down; a growing \`inbox\` count means the
96
+ Index curator is not keeping up.
97
+ `;
98
+
99
+ const SKILL_MIGRATE = `---
100
+ name: iapeer-memory-migrate
101
+ description: "Use when the user asks to migrate harness auto-memory into iapeer-memory (\\"migrate memory\\", \\"move auto-memory to the vault\\", \\"перенеси auto-memory\\"), or when connecting a peer that has accumulated Claude auto-memory. Facade over \`iapeer-memory migrate\`: the skill resolves the claude-specific SOURCE directory, the deterministic engine does the rest (dry-run → confirm → apply, with backups)."
102
+ argument-hint: "<agent> [<project-dir>]"
103
+ allowed-tools: ["Bash", "AskUserQuestion"]
104
+ ---
105
+
106
+ # Migrate Claude auto-memory into the vault
107
+
108
+ The engine (\`iapeer-memory migrate\`) is source-agnostic — THIS skill owns the
109
+ claude-specific knowledge of where auto-memory lives. (The codex source is
110
+ NOT wired yet: its live format is unverified — never guess it.)
111
+
112
+ ## Resolve the source directory
113
+
114
+ - **Launchd/persistent peer** (no \`<project-dir>\` argument):
115
+ \`SOURCE=~/.claude/agent-memory/<agent>/\`
116
+ - **Project session** (\`<project-dir>\` given): the slug is the absolute
117
+ path with every non-alphanumeric character replaced by \`-\` — dots too:
118
+ \`/a/b.c\` → \`-a-b-c\` (so \`~/.iapeer/...\` yields a double dash). When in
119
+ doubt, \`ls ~/.claude/projects/\` and match.
120
+ \`SOURCE=~/.claude/projects/<slug>/memory/\`
121
+
122
+ No directory or no \`.md\` files inside → nothing to migrate; say so and stop.
123
+
124
+ ## Run
125
+
126
+ 1. Dry-run first: \`iapeer-memory migrate --source "$SOURCE" --agent <agent>\`
127
+ — show the user the plan verbatim (per-file type → subtype mapping,
128
+ skip lists, totals).
129
+ 2. Ask for confirmation (AskUserQuestion).
130
+ 3. Apply: same command + \`--apply\`. Per-file backups land under
131
+ \`~/.iapeer/state/iapeer-memory/migrate-backups/\` before conversion; an
132
+ existing target note is never overwritten.
133
+ 4. Report: migrated/skipped/errors + backup path.
134
+
135
+ ## After migration
136
+
137
+ A \`feedback\` note that is semantically a pitfall cannot be told apart
138
+ deterministically — re-filing such notes to \`pitfall\` is the agent's manual
139
+ step afterwards (the iapeer-memory-distill skill covers it).
140
+ `;
141
+
142
+ const SKILL_DISTILL = `---
143
+ name: iapeer-memory-distill
144
+ description: "Use when the user asks the agent to clean up its own memory (\\"distill your memory\\", \\"прибери свою память\\", \\"clean up your operative notes\\"). Deep MANUAL distillation of the agent's own agent-memory folder, in-session, user in the loop — deeper than the DreamWeaver weekly tick: fact-checks, re-filing, promoting team knowledge to canon."
145
+ allowed-tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"]
146
+ ---
147
+
148
+ # Distill your own agent memory
149
+
150
+ You are cleaning YOUR OWN folder: \`<vault>/06_Agent_Memory/<your personality>/\`
151
+ (RU locale: \`06_Оперативка_агентов/<…>/\`). Identity comes from
152
+ \`PEER_PERSONALITY\` — if it is empty, refuse: you cannot know whose memory
153
+ you are touching. An absent/empty folder = nothing to distill; say so and stop.
154
+
155
+ ## The link-watershed rule (before ANY identity-changing operation)
156
+
157
+ Deleting, renaming or replacing a note splits on the wikilink graph — query
158
+ the note's connections first (vault_graph MCP tool):
159
+
160
+ - **0 incoming + 0 outgoing** → isolated; act directly (\`rm\` / rename).
161
+ - **≥1 link in either direction** → the note is part of the graph; set
162
+ \`status\` to the deprecated token instead and (if needed) write a
163
+ replacement note. The Index archives it on its PERMANENT_CHANGED pass.
164
+
165
+ Body edits that keep identity (rewording, updating description, switching
166
+ subtype) need no graph check.
167
+
168
+ ## Passes
169
+
170
+ 1. **Inventory**: list every note; for each — subtype, status, age, one-line
171
+ gist.
172
+ 2. **Dedup**: near-duplicate notes about one topic → merge into the
173
+ strongest one, deprecate the rest (watershed rule).
174
+ 3. **Compress**: bloated notes → tighten to the essentials; notes are
175
+ injected into readers' contexts, bloat costs the whole team tokens.
176
+ 4. **Verify**: notes asserting local facts (paths, flags, versions) —
177
+ re-check the fact cheaply where possible; stale → fix or deprecate.
178
+ 5. **Re-file**: \`feedback\` notes that are semantically pitfalls (a rule
179
+ born from one incident) → subtype \`pitfall\`; other mis-filed subtypes
180
+ likewise.
181
+ 6. **Promote**: material useful to the whole team → draft into the inbox
182
+ folder (canon style: self-contained, objective), keep the personal
183
+ angle in your memory note with an inline \`[[draft title]]\` link.
184
+ 7. **Report**: summary to the user — counts per pass, anything that needs
185
+ their decision.
186
+
187
+ Confirm with the user between passes 6 and 7 when the promote list is
188
+ non-empty — moving knowledge to canon is visible to the whole team.
189
+ `;
190
+
191
+ export const SKILL_BODIES: Record<SkillName, string> = {
192
+ "iapeer-memory-init": SKILL_INIT,
193
+ "iapeer-memory-status": SKILL_STATUS,
194
+ "iapeer-memory-migrate": SKILL_MIGRATE,
195
+ "iapeer-memory-distill": SKILL_DISTILL,
196
+ };