@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.
- package/package.json +2 -2
- package/src/cli.ts +12 -0
- package/src/commands/init.ts +129 -38
- package/src/commands/memoryd.ts +18 -1
- package/src/commands/provision-peer.ts +172 -0
- package/src/commands/render.ts +8 -1
- package/src/commands/uninstall.ts +60 -23
- package/src/commands/update.ts +138 -32
- package/src/commands/verify.ts +147 -4
- package/src/fleet.ts +150 -0
- package/src/paths.ts +10 -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/guide-en.ts +1 -1
- package/src/templates/guide-ru.ts +1 -1
- package/src/templates/index.ts +11 -2
- package/src/templates/skills.ts +196 -0
|
@@ -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("
|
|
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. */
|
|
@@ -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
|
+
};
|