@agfpd/iapeer-memory 0.3.0 → 0.3.2
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 +5 -0
- package/src/commands/bake.ts +78 -0
- package/src/commands/hook.ts +106 -2
- package/src/commands/init.ts +10 -7
- package/src/commands/update.ts +13 -0
- package/src/surfaces/claude.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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.3.
|
|
30
|
+
"@agfpd/iapeer-memory-core": "0.3.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "^1.2.0",
|
package/src/cli.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { liveEgress } from "./egress.js";
|
|
|
19
19
|
import { memoryPaths } from "./paths.js";
|
|
20
20
|
import { packageVersion } from "./version.js";
|
|
21
21
|
import { cmdFmUpdate } from "./commands/fm-update.js";
|
|
22
|
+
import { cmdBake } from "./commands/bake.js";
|
|
22
23
|
import { cmdHook } from "./commands/hook.js";
|
|
23
24
|
import { cmdInit } from "./commands/init.js";
|
|
24
25
|
import { cmdInstallBinary } from "./commands/install-binary.js";
|
|
@@ -60,6 +61,8 @@ Commands:
|
|
|
60
61
|
unprovision-peer --cwd P --runtime claude|codex [--occasion O]
|
|
61
62
|
strip OUR surfaces from one peer's cwd (mirror)
|
|
62
63
|
fm-update [ops] FILE... structural frontmatter edits + attribution stamp
|
|
64
|
+
bake FILE... re-stamp YOUR authorship on notes you wrote via
|
|
65
|
+
bash/shell (which bypasses the post-write hook)
|
|
63
66
|
migrate --source DIR move harness auto-memory into the vault
|
|
64
67
|
(dry-run by default; --apply to execute)
|
|
65
68
|
dream-collect [--gate] deterministic weekly-tick pre-filter (zero LLM):
|
|
@@ -147,6 +150,8 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
147
150
|
return cmdUnprovisionPeer(rest);
|
|
148
151
|
case "fm-update":
|
|
149
152
|
return cmdFmUpdate(rest);
|
|
153
|
+
case "bake":
|
|
154
|
+
return cmdBake(rest);
|
|
150
155
|
case "migrate":
|
|
151
156
|
return cmdMigrate(rest);
|
|
152
157
|
case "render":
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `iapeer-memory bake <files>` — re-stamp `last_edited_by = <agent>` (+ `updated`)
|
|
3
|
+
* on the given notes. THE PROBLEM: a shell/bash write to the vault (`echo >`,
|
|
4
|
+
* `sed -i`, `tee`, `cp`/`mv` …) bypasses the PostToolUse hook, so memoryd sees
|
|
5
|
+
* only the fs-change with no identity and humanEditPass mis-attributes it to the
|
|
6
|
+
* human. `bake` fixes the attribution: it makes the note a settled AGENT edit
|
|
7
|
+
* (humanEditPass then echo-agent-skips it; it also OVERWRITES a stamp memoryd may
|
|
8
|
+
* have raced in). It is `fm-update` with NO field ops — a pure attribution stamp.
|
|
9
|
+
* The just-in-time post-Bash hook reminds the agent to run it (no standing rule —
|
|
10
|
+
* Артур's design: the reminder fires only on a detected bash-vault-write).
|
|
11
|
+
*
|
|
12
|
+
* iapeer-memory bake [--agent NAME] [--vault PATH] FILE [FILE ...]
|
|
13
|
+
*
|
|
14
|
+
* identity: `--agent` → PEER_PERSONALITY → IAPEER_MEMORY_AGENT_NAME
|
|
15
|
+
* (resolveAgentName, нюанс 10 — never guessed from cwd). No identity → error
|
|
16
|
+
* (a stamp with no author would be meaningless).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
collectOps,
|
|
21
|
+
DEFAULT_CURATOR_SET,
|
|
22
|
+
fmUpdate,
|
|
23
|
+
getTaxonomy,
|
|
24
|
+
isLocaleId,
|
|
25
|
+
resolveAgentName,
|
|
26
|
+
} from "@agfpd/iapeer-memory-core";
|
|
27
|
+
|
|
28
|
+
export function cmdBake(argv: string[]): number {
|
|
29
|
+
let agent: string | null = null;
|
|
30
|
+
let vault = "";
|
|
31
|
+
const files: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === "--agent") {
|
|
36
|
+
agent = argv[++i] ?? null;
|
|
37
|
+
} else if (a === "--vault") {
|
|
38
|
+
vault = argv[++i] ?? "";
|
|
39
|
+
} else if (a.startsWith("--")) {
|
|
40
|
+
console.error(`iapeer-memory bake: unknown flag: ${a}`);
|
|
41
|
+
return 2;
|
|
42
|
+
} else {
|
|
43
|
+
files.push(a);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!files.length) {
|
|
47
|
+
console.error("iapeer-memory bake: no files given");
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const localeRaw = process.env.IAPEER_MEMORY_LOCALE || "en";
|
|
52
|
+
if (!isLocaleId(localeRaw)) {
|
|
53
|
+
console.error(`iapeer-memory bake: unknown locale "${localeRaw}"`);
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
const resolved = resolveAgentName(agent);
|
|
57
|
+
if (!resolved) {
|
|
58
|
+
console.error(
|
|
59
|
+
"iapeer-memory bake: no agent identity (set PEER_PERSONALITY or pass --agent NAME)",
|
|
60
|
+
);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const curatorSet = (process.env.IAPEER_MEMORY_CURATOR_SET || "")
|
|
64
|
+
.split(",")
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
fmUpdate({
|
|
69
|
+
files,
|
|
70
|
+
ops: collectOps({ set: [], unset: [], listAdd: [], listRemove: [] }),
|
|
71
|
+
agent: resolved,
|
|
72
|
+
vault: vault || process.env.IAPEER_MEMORY_VAULT_PATH || "",
|
|
73
|
+
taxonomy: getTaxonomy(localeRaw),
|
|
74
|
+
curatorSet: curatorSet.length ? curatorSet : DEFAULT_CURATOR_SET,
|
|
75
|
+
stamp: true,
|
|
76
|
+
});
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
package/src/commands/hook.ts
CHANGED
|
@@ -492,15 +492,119 @@ function logHookError(err: unknown): void {
|
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
// ── bash-write attribution reminder (Артур's just-in-time design) ────────────
|
|
496
|
+
// A shell/bash write to the vault bypasses this hook's Write/Edit path, so
|
|
497
|
+
// memoryd sees only the fs-change with no identity and mis-attributes it to the
|
|
498
|
+
// human. When a Bash command WRITES to a vault `.md`, remind the agent to
|
|
499
|
+
// re-stamp its authorship via `iapeer-memory bake`. There is NO standing rule
|
|
500
|
+
// (Артур): the reminder is the ONLY carrier, emitted just-in-time. Detection is
|
|
501
|
+
// best-effort — obscure forms are missed; the one false positive we must avoid
|
|
502
|
+
// is read-then-redirect-elsewhere (`cat note.md > /tmp/x`), so we capture the
|
|
503
|
+
// write-TARGET of each operator, never every vault path mentioned.
|
|
504
|
+
|
|
505
|
+
// One path token: "double-quoted" | 'single-quoted' | bare (no shell metachars).
|
|
506
|
+
// Quotes are REQUIRED to carry spaces — the real vault path has them
|
|
507
|
+
// ("…/Mobile Documents/…"), so a space-blind matcher would miss every write.
|
|
508
|
+
const PATH_TOKEN = String.raw`(?:"([^"]*)"|'([^']*)'|([^\s'"|&;<>()]+))`;
|
|
509
|
+
/** The matched path value from a PATH_TOKEN match starting at group `i`. */
|
|
510
|
+
function pathVal(m: RegExpMatchArray, i: number): string {
|
|
511
|
+
return m[i] ?? m[i + 1] ?? m[i + 2] ?? "";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Write-TARGET paths of a bash command (best-effort): the path each write
|
|
515
|
+
* operator writes TO (never a read arg). Covers `>`/`>>`, `tee`, `sed -i`,
|
|
516
|
+
* `cp`/`mv`; handles quoted paths with spaces. */
|
|
517
|
+
export function bashWriteTargets(cmd: string): string[] {
|
|
518
|
+
const targets: string[] = [];
|
|
519
|
+
// redirection `>`/`>>` TARGET — excludes `2>` (fd), `>&` and `>(...)`.
|
|
520
|
+
for (const m of cmd.matchAll(new RegExp(String.raw`(?<![0-9&])>>?\s*(?![&(])` + PATH_TOKEN, "g"))) {
|
|
521
|
+
const t = pathVal(m, 1);
|
|
522
|
+
if (t) targets.push(t);
|
|
523
|
+
}
|
|
524
|
+
// `tee [-a] FILE...` — path tokens until a pipe / terminator.
|
|
525
|
+
for (const seg of cmd.matchAll(/\btee\b([^|&;]*)/g)) {
|
|
526
|
+
for (const m of seg[1].matchAll(new RegExp(PATH_TOKEN, "g"))) {
|
|
527
|
+
const t = pathVal(m, 1);
|
|
528
|
+
if (t && !t.startsWith("-")) targets.push(t);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// `sed -i…` / `--in-place` edits its file args in place → any `.md` token.
|
|
532
|
+
if (/\bsed\b\s+(?:-\S*i\S*|--in-place)/.test(cmd)) {
|
|
533
|
+
for (const m of cmd.matchAll(new RegExp(PATH_TOKEN, "g"))) {
|
|
534
|
+
const t = pathVal(m, 1);
|
|
535
|
+
if (t.endsWith(".md")) targets.push(t);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// `cp`/`mv SRC… DEST` — the last path token of the segment is the destination.
|
|
539
|
+
for (const seg of cmd.matchAll(/\b(?:cp|mv)\b([^|&;]*)/g)) {
|
|
540
|
+
const toks: string[] = [];
|
|
541
|
+
for (const m of seg[1].matchAll(new RegExp(PATH_TOKEN, "g"))) {
|
|
542
|
+
const t = pathVal(m, 1);
|
|
543
|
+
if (t && !t.startsWith("-")) toks.push(t);
|
|
544
|
+
}
|
|
545
|
+
if (toks.length >= 2) targets.push(toks[toks.length - 1]);
|
|
546
|
+
}
|
|
547
|
+
return targets;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** A bake-reminder additionalContext when a Bash event wrote to a vault `.md`,
|
|
551
|
+
* else null. */
|
|
552
|
+
export function bashVaultWriteReminder(
|
|
553
|
+
text: string,
|
|
554
|
+
env: Record<string, string | undefined> = process.env,
|
|
555
|
+
): string | null {
|
|
556
|
+
let event: { tool_name?: string; tool_input?: { command?: string }; cwd?: string };
|
|
557
|
+
try {
|
|
558
|
+
event = JSON.parse(text);
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (event.tool_name !== "Bash") return null;
|
|
563
|
+
const cmd = event.tool_input?.command ?? "";
|
|
564
|
+
const vault = env.IAPEER_MEMORY_VAULT_PATH ?? "";
|
|
565
|
+
if (!cmd || !vault) return null;
|
|
566
|
+
const vaultRoot = path.resolve(vault);
|
|
567
|
+
const cwd = event.cwd && path.isAbsolute(event.cwd) ? event.cwd : null;
|
|
568
|
+
const hits: string[] = [];
|
|
569
|
+
for (const raw of bashWriteTargets(cmd)) {
|
|
570
|
+
if (!raw.endsWith(".md")) continue;
|
|
571
|
+
let abs: string;
|
|
572
|
+
if (path.isAbsolute(raw)) abs = path.resolve(raw);
|
|
573
|
+
else if (cwd) abs = path.resolve(cwd, raw);
|
|
574
|
+
else continue; // relative + unknown cwd → best-effort skip
|
|
575
|
+
if (abs === vaultRoot || abs.startsWith(vaultRoot + path.sep)) hits.push(raw);
|
|
576
|
+
}
|
|
577
|
+
if (!hits.length) return null;
|
|
578
|
+
const list = hits
|
|
579
|
+
.map((h) => (/[\s'"]/.test(h) ? `'${h.replace(/'/g, "'\\''")}'` : h))
|
|
580
|
+
.join(" ");
|
|
581
|
+
return (
|
|
582
|
+
"[iapeer-memory] you wrote a vault note via bash — the post-write hook didn't see it, " +
|
|
583
|
+
"so memoryd would mis-attribute it to the human. Re-stamp your authorship:\n" +
|
|
584
|
+
` iapeer-memory bake ${list}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
495
588
|
export async function cmdHook(argv: string[], egress: Egress): Promise<number> {
|
|
496
589
|
const [event] = argv;
|
|
497
590
|
try {
|
|
498
591
|
switch (event) {
|
|
499
592
|
case "post-write": {
|
|
500
593
|
const text = await Bun.stdin.text();
|
|
501
|
-
const result = runPostWrite(text); // sync: stamp + tag gate
|
|
594
|
+
const result = runPostWrite(text); // sync: stamp + tag gate (Write/Edit)
|
|
502
595
|
const hints = await collectDedupAndLinkHints(text, egress); // async, fail-open §3a/§3b
|
|
503
|
-
|
|
596
|
+
let output = mergeHookOutput(result.output, hints);
|
|
597
|
+
// Bash events have no stamp/dedup output — instead, a just-in-time bake
|
|
598
|
+
// reminder when the command wrote to a vault note (mutually exclusive
|
|
599
|
+
// with the Write/Edit path above; both never fire for one event).
|
|
600
|
+
if (!output) {
|
|
601
|
+
const bake = bashVaultWriteReminder(text);
|
|
602
|
+
if (bake) {
|
|
603
|
+
output = JSON.stringify({
|
|
604
|
+
hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: bake },
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
504
608
|
if (output) console.log(output);
|
|
505
609
|
return 0;
|
|
506
610
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -358,16 +358,19 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
|
|
|
358
358
|
roleEntries.push({ role, peerCwd, template });
|
|
359
359
|
}
|
|
360
360
|
writeRolesManifest({ rolesManifestPath: paths.rolesManifestPath, roles: roleEntries });
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
361
|
+
// ALL curators (Index/Scriber/DreamWeaver) are TICK-STATELESS — each wakes
|
|
362
|
+
// for a discrete pass (CURATOR_TICK / dream-tick / on-demand) and carries
|
|
363
|
+
// no state between wakes. They run EPHEMERAL (a clean session per wake): a
|
|
364
|
+
// PERSISTENT session would accumulate a STALE model after a deploy — exactly
|
|
365
|
+
// the migration class an ephemeral flip structurally kills. Patch the one
|
|
366
|
+
// key into each core-owned profile, no-clobber.
|
|
367
|
+
const wakePolicies = roleEntries.map((r) => `${r.role}:${patchWakePolicyEphemeral(r.peerCwd)}`);
|
|
368
|
+
const wakeOk = wakePolicies.every((w) => !w.endsWith("missing-profile"));
|
|
366
369
|
step(
|
|
367
370
|
"roles",
|
|
368
371
|
`${roleEntries.map((r) => rolePersonality(r.role as (typeof ROLE_NAMES)[number])).join(", ")} ` +
|
|
369
|
-
`(doctrines v${version},
|
|
370
|
-
rolesOk && roleEntries.length === ROLE_NAMES.length &&
|
|
372
|
+
`(doctrines v${version}, wake_policy ${wakePolicies.join(" ")}, manifest ${paths.rolesManifestPath})`,
|
|
373
|
+
rolesOk && roleEntries.length === ROLE_NAMES.length && wakeOk,
|
|
371
374
|
);
|
|
372
375
|
}
|
|
373
376
|
|
package/src/commands/update.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
DREAM_TRIGGER_ID,
|
|
58
58
|
LEGACY_SWEEP_TRIGGER_ID,
|
|
59
59
|
dreamTimerMessage,
|
|
60
|
+
patchWakePolicyEphemeral,
|
|
60
61
|
registerTimer,
|
|
61
62
|
registerWatcher,
|
|
62
63
|
unregisterTimer,
|
|
@@ -139,6 +140,18 @@ export function cmdUpdate(argv: string[], egress: Egress): number {
|
|
|
139
140
|
.join(", ") + ` (v${version}; roles pick it up on next cold wake)`,
|
|
140
141
|
missing.length === 0,
|
|
141
142
|
);
|
|
143
|
+
// wake_policy: all curators are tick-stateless ephemeral workers — a
|
|
144
|
+
// PERSISTENT session accumulates a stale model after a deploy (the migration
|
|
145
|
+
// class an ephemeral flip kills). Re-assert ephemeral on every update
|
|
146
|
+
// (no-clobber, idempotent — "identical" when already set).
|
|
147
|
+
const wp = manifest.roles.map((r) => `${r.role}:${patchWakePolicyEphemeral(r.peerCwd)}`);
|
|
148
|
+
const wpMissing = wp.filter((w) => w.endsWith("missing-profile")).length;
|
|
149
|
+
step(
|
|
150
|
+
"wake_policy",
|
|
151
|
+
`${wp.join(" ")} (ephemeral — fresh session per wake)` +
|
|
152
|
+
(wpMissing ? ` — ${wpMissing} profile(s) not yet born (peer birth creates them)` : ""),
|
|
153
|
+
true, // best-effort reconciliation; a missing peer profile is a birth concern, never an update failure
|
|
154
|
+
);
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
// 4. fleet map — personality → cwd × runtimes (the joint of the surfaces
|
package/src/surfaces/claude.ts
CHANGED
|
@@ -162,7 +162,11 @@ export function expectedHookEntries(
|
|
|
162
162
|
): Record<"PostToolUse" | "SessionStart", HookEntry> {
|
|
163
163
|
return {
|
|
164
164
|
PostToolUse: {
|
|
165
|
-
|
|
165
|
+
// Bash is included so a shell write to the vault (which bypasses the
|
|
166
|
+
// Write/Edit path) triggers the just-in-time bake-attribution reminder.
|
|
167
|
+
// The bin fast-exits for the (vast majority of) bash commands that touch
|
|
168
|
+
// no vault note.
|
|
169
|
+
matcher: "Write|Edit|MultiEdit|Bash",
|
|
166
170
|
hooks: [{ type: "command", command: shimPath(hooksDir, "post-write") }],
|
|
167
171
|
},
|
|
168
172
|
SessionStart: {
|