@agfpd/iapeer-memory 0.1.1
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/README.md +3 -0
- package/bin/iapeer-memory +29 -0
- package/package.json +36 -0
- package/src/binary.ts +75 -0
- package/src/cli.ts +120 -0
- package/src/commands/fm-update.ts +104 -0
- package/src/commands/hook.ts +264 -0
- package/src/commands/init.ts +358 -0
- package/src/commands/install-binary.ts +44 -0
- package/src/commands/memoryd.ts +101 -0
- package/src/commands/migrate.ts +112 -0
- package/src/commands/render.ts +199 -0
- package/src/commands/status.ts +155 -0
- package/src/commands/uninstall.ts +126 -0
- package/src/commands/update.ts +138 -0
- package/src/commands/verify.ts +300 -0
- package/src/config-env.ts +74 -0
- package/src/paths.ts +101 -0
- package/src/provision.ts +188 -0
- package/src/roles.ts +43 -0
- package/src/slot.ts +89 -0
- package/src/sync-versions.ts +110 -0
- package/src/templates/guide-en.ts +103 -0
- package/src/templates/guide-ru.ts +99 -0
- package/src/templates/index.ts +105 -0
- package/src/templates/roles-en.ts +232 -0
- package/src/templates/roles-ru.ts +224 -0
- package/src/version.ts +17 -0
- package/src/watcher.ts +175 -0
- package/tsconfig.json +24 -0
package/src/roles.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roles manifest — the init→verify bridge (`<state>/roles.json`):
|
|
3
|
+
* which role peers exist, where their cwd is, which template renders their
|
|
4
|
+
* doctrine. init writes it after `iapeer create`; verify reads it to
|
|
5
|
+
* compare rendered doctrine versions against the package (ADR-010) and
|
|
6
|
+
* `--repair` re-renders from the referenced templates.
|
|
7
|
+
*
|
|
8
|
+
* Role peer location: the CORE'S DEFAULT (`iapeer create` without
|
|
9
|
+
* `--path` → its documented default peers folder). Host-specific layouts
|
|
10
|
+
* must never leak into the product (требование Артура, 10.06).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
export type RoleEntry = { role: string; peerCwd: string; template: string };
|
|
17
|
+
|
|
18
|
+
export type RolesManifest = { roles: RoleEntry[] };
|
|
19
|
+
|
|
20
|
+
export function writeRolesManifest(opts: {
|
|
21
|
+
rolesManifestPath: string;
|
|
22
|
+
roles: RoleEntry[];
|
|
23
|
+
}): void {
|
|
24
|
+
fs.mkdirSync(path.dirname(opts.rolesManifestPath), { recursive: true });
|
|
25
|
+
const tmp = `${opts.rolesManifestPath}.tmp`;
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
tmp,
|
|
28
|
+
JSON.stringify({ roles: opts.roles } satisfies RolesManifest, null, 2) + "\n",
|
|
29
|
+
"utf-8",
|
|
30
|
+
);
|
|
31
|
+
fs.renameSync(tmp, opts.rolesManifestPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Never throws: absent/malformed → null (verify treats it as "init has not run"). */
|
|
35
|
+
export function readRolesManifest(rolesManifestPath: string): RolesManifest | null {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(rolesManifestPath, "utf-8")) as RolesManifest;
|
|
38
|
+
if (!Array.isArray(parsed.roles)) return null;
|
|
39
|
+
return parsed;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/slot.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-provider slot declaration — the iapeer memory-slot contract (FINAL,
|
|
3
|
+
* iapeer docs fc68c54/e2195a7/c968219). The slot file tells the core that
|
|
4
|
+
* the three public surfaces (layer-5 fragments / MCP tools / daemon under a
|
|
5
|
+
* notifier watcher) are occupied:
|
|
6
|
+
*
|
|
7
|
+
* - the PROVIDER writes and removes the file (our init/uninstall), atomic
|
|
8
|
+
* temp+rename; the core only reads it (absent/unreadable = empty slot);
|
|
9
|
+
* - a slot held by a FOREIGN provider is never touched — explicit refusal
|
|
10
|
+
* (mirror of the core's own init-step refusal);
|
|
11
|
+
* - `version` = the package version (the same single source as the doctrine
|
|
12
|
+
* marker, ADR-010); our `update` re-writes it (P4 obligation);
|
|
13
|
+
* - `heartbeat` (optional) = the absolute path whose mtime memoryd touches —
|
|
14
|
+
* the core may show staleness in `iapeer status`, never acts on it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
export const SLOT_PROVIDER = "iapeer-memory";
|
|
21
|
+
export const SLOT_PACKAGE = "@agfpd/iapeer-memory";
|
|
22
|
+
|
|
23
|
+
export type MemoryProviderSlot = {
|
|
24
|
+
provider: string;
|
|
25
|
+
package: string;
|
|
26
|
+
version: string;
|
|
27
|
+
registeredAt: string;
|
|
28
|
+
heartbeat?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Never throws: missing / unreadable / malformed → null (empty slot). */
|
|
32
|
+
export function readSlot(slotPath: string): MemoryProviderSlot | null {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(fs.readFileSync(slotPath, "utf-8")) as MemoryProviderSlot;
|
|
35
|
+
if (!parsed || typeof parsed.provider !== "string") return null;
|
|
36
|
+
return parsed;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SlotWriteResult = {
|
|
43
|
+
action: "written" | "identical" | "refused-foreign";
|
|
44
|
+
existing: MemoryProviderSlot | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function writeSlot(opts: {
|
|
48
|
+
slotPath: string;
|
|
49
|
+
version: string;
|
|
50
|
+
heartbeat?: string;
|
|
51
|
+
/** Injectable for tests. */
|
|
52
|
+
nowIso?: string;
|
|
53
|
+
}): SlotWriteResult {
|
|
54
|
+
const existing = readSlot(opts.slotPath);
|
|
55
|
+
if (existing && existing.provider !== SLOT_PROVIDER) {
|
|
56
|
+
return { action: "refused-foreign", existing };
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
existing &&
|
|
60
|
+
existing.version === opts.version &&
|
|
61
|
+
existing.heartbeat === opts.heartbeat &&
|
|
62
|
+
existing.package === SLOT_PACKAGE
|
|
63
|
+
) {
|
|
64
|
+
return { action: "identical", existing }; // idempotent re-init: no churn
|
|
65
|
+
}
|
|
66
|
+
const slot: MemoryProviderSlot = {
|
|
67
|
+
provider: SLOT_PROVIDER,
|
|
68
|
+
package: SLOT_PACKAGE,
|
|
69
|
+
version: opts.version,
|
|
70
|
+
registeredAt: opts.nowIso ?? new Date().toISOString(),
|
|
71
|
+
...(opts.heartbeat ? { heartbeat: opts.heartbeat } : {}),
|
|
72
|
+
};
|
|
73
|
+
fs.mkdirSync(path.dirname(opts.slotPath), { recursive: true });
|
|
74
|
+
const tmp = `${opts.slotPath}.tmp`;
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(slot, null, 2) + "\n", "utf-8");
|
|
76
|
+
fs.renameSync(tmp, opts.slotPath);
|
|
77
|
+
return { action: "written", existing };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type SlotRemoveResult = "removed" | "absent" | "refused-foreign";
|
|
81
|
+
|
|
82
|
+
/** Uninstall removes ONLY our own declaration; a foreign slot is left intact. */
|
|
83
|
+
export function removeSlot(slotPath: string): SlotRemoveResult {
|
|
84
|
+
const existing = readSlot(slotPath);
|
|
85
|
+
if (!existing) return "absent";
|
|
86
|
+
if (existing.provider !== SLOT_PROVIDER) return "refused-foreign";
|
|
87
|
+
fs.unlinkSync(slotPath);
|
|
88
|
+
return "removed";
|
|
89
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest version sync (docs/10 §Версионная синхронизация): propagate the
|
|
3
|
+
* facade `package/package.json` version into every other manifest of the
|
|
4
|
+
* monorepo — `core/package.json` + `adapters/{claude,codex}` plugin
|
|
5
|
+
* manifests. Wired into the npm `version` lifecycle, runnable standalone:
|
|
6
|
+
*
|
|
7
|
+
* bun src/sync-versions.ts
|
|
8
|
+
*
|
|
9
|
+
* Missing manifests are reported and skipped (the adapters land in P2/P5 —
|
|
10
|
+
* the script must not fail before they exist). The MergeMind
|
|
11
|
+
* sync-plugin-version pattern, generalised to N manifests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export type SyncOutcome = { file: string; action: "updated" | "identical" | "missing" };
|
|
18
|
+
|
|
19
|
+
/** Relative (to the monorepo root) manifests that must carry one version. */
|
|
20
|
+
export const SYNC_TARGETS = [
|
|
21
|
+
"core/package.json",
|
|
22
|
+
"adapters/claude/.claude-plugin/plugin.json",
|
|
23
|
+
"adapters/codex/.codex-plugin/plugin.json",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export function syncVersions(opts: {
|
|
27
|
+
rootDir: string;
|
|
28
|
+
version: string;
|
|
29
|
+
targets?: readonly string[];
|
|
30
|
+
}): SyncOutcome[] {
|
|
31
|
+
const targets = opts.targets ?? SYNC_TARGETS;
|
|
32
|
+
const outcomes: SyncOutcome[] = [];
|
|
33
|
+
for (const rel of targets) {
|
|
34
|
+
const file = path.join(opts.rootDir, rel);
|
|
35
|
+
let raw: string;
|
|
36
|
+
try {
|
|
37
|
+
raw = fs.readFileSync(file, "utf-8");
|
|
38
|
+
} catch {
|
|
39
|
+
outcomes.push({ file: rel, action: "missing" });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const manifest = JSON.parse(raw) as Record<string, unknown>;
|
|
43
|
+
if (manifest.version === opts.version) {
|
|
44
|
+
outcomes.push({ file: rel, action: "identical" });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
manifest.version = opts.version;
|
|
48
|
+
// 2-space indent + trailing newline — the repo's manifest style.
|
|
49
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
50
|
+
outcomes.push({ file: rel, action: "updated" });
|
|
51
|
+
}
|
|
52
|
+
return outcomes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The facade's dependency on the core is an EXACT version pin kept in
|
|
57
|
+
* lockstep by this script (release decision: two npm packages, one shared
|
|
58
|
+
* version). `npm publish` ships the manifest verbatim — it does NOT
|
|
59
|
+
* rewrite the `workspace:*` protocol (only bun/pnpm publish do), so the
|
|
60
|
+
* pin on disk is the published truth. Locally the exact pin still resolves
|
|
61
|
+
* to the workspace copy: bun matches workspace packages against semver
|
|
62
|
+
* ranges, and syncVersions keeps core/package.json at the same version.
|
|
63
|
+
*/
|
|
64
|
+
export function syncCoreDependencyPin(opts: {
|
|
65
|
+
packageManifestPath: string;
|
|
66
|
+
version: string;
|
|
67
|
+
}): SyncOutcome {
|
|
68
|
+
const rel = path.basename(opts.packageManifestPath);
|
|
69
|
+
let raw: string;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(opts.packageManifestPath, "utf-8");
|
|
72
|
+
} catch {
|
|
73
|
+
return { file: rel, action: "missing" };
|
|
74
|
+
}
|
|
75
|
+
const manifest = JSON.parse(raw) as {
|
|
76
|
+
dependencies?: Record<string, string>;
|
|
77
|
+
};
|
|
78
|
+
const deps = manifest.dependencies ?? {};
|
|
79
|
+
if (deps["@agfpd/iapeer-memory-core"] === opts.version) {
|
|
80
|
+
return { file: rel, action: "identical" };
|
|
81
|
+
}
|
|
82
|
+
deps["@agfpd/iapeer-memory-core"] = opts.version;
|
|
83
|
+
manifest.dependencies = deps;
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
opts.packageManifestPath,
|
|
86
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
87
|
+
"utf-8",
|
|
88
|
+
);
|
|
89
|
+
return { file: rel, action: "updated" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (import.meta.main) {
|
|
93
|
+
const pkgDir = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
|
94
|
+
const pkg = JSON.parse(
|
|
95
|
+
fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
|
|
96
|
+
) as { version: string };
|
|
97
|
+
const outcomes = syncVersions({
|
|
98
|
+
rootDir: path.dirname(pkgDir),
|
|
99
|
+
version: pkg.version,
|
|
100
|
+
});
|
|
101
|
+
outcomes.push(
|
|
102
|
+
syncCoreDependencyPin({
|
|
103
|
+
packageManifestPath: path.join(pkgDir, "package.json"),
|
|
104
|
+
version: pkg.version,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
for (const o of outcomes) {
|
|
108
|
+
console.log(`sync-versions: ${o.action.padEnd(9)} ${o.file}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writer's guide — EN base. The HOST-WIDE layer-5 fragment: every peer of
|
|
3
|
+
* the fleet reads this on every cold wake (ADR-001). Token-frugal by
|
|
4
|
+
* design — bloat here costs the whole team on every session. Source of
|
|
5
|
+
* truth: docs/01–05; the style base is the proven MergeMind guide.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const GUIDE_EN = `# iapeer-memory — the team's shared memory
|
|
9
|
+
|
|
10
|
+
iapeer-memory is the team's shared memory (agents + human): the canon
|
|
11
|
+
(knowledge / decisions / ideas / projects / lists) plus each agent's
|
|
12
|
+
personal memory. You read it and you write it.
|
|
13
|
+
|
|
14
|
+
**Search the memory first.** Someone may have already solved your problem
|
|
15
|
+
and written it down. Use \`vault_search\` (then \`Read\` the note),
|
|
16
|
+
\`vault_graph\` for a note's neighborhood, \`vault_map\` for the global map.
|
|
17
|
+
|
|
18
|
+
**Verify before acting.** A note is a snapshot at write time. Check that
|
|
19
|
+
the function/file/flag still exists — especially before the user acts on
|
|
20
|
+
your recommendation.
|
|
21
|
+
|
|
22
|
+
**On a conflict between memory and observation — trust the observation.**
|
|
23
|
+
Update or deprecate the stale note. This is living memory.
|
|
24
|
+
|
|
25
|
+
## Write proactively — you don't exist between sessions
|
|
26
|
+
|
|
27
|
+
A session is ephemeral: when it ends, the context is gone. The vault is
|
|
28
|
+
the ONLY thing that survives. Not written down → lost forever.
|
|
29
|
+
|
|
30
|
+
Write without asking permission. A meaningful result (a fix, a decision,
|
|
31
|
+
a pitfall, feedback, a system nuance) is recorded immediately, by you.
|
|
32
|
+
Canon material → a draft in \`00_Inbox/\`; personal material → your own
|
|
33
|
+
agent-memory folder. Asking the human "should I write this down?" is an
|
|
34
|
+
anti-pattern.
|
|
35
|
+
|
|
36
|
+
**Write concisely.** Notes are injected into readers' contexts; bloat
|
|
37
|
+
costs tokens for the whole team.
|
|
38
|
+
|
|
39
|
+
**Sweep as you write.** Added or updated a note → immediately check
|
|
40
|
+
whether an older note on the same topic became stale: flip its \`status\`
|
|
41
|
+
to the final token, or delete it (your own memory notes only, and only
|
|
42
|
+
with zero graph connections — check with \`vault_graph\` first).
|
|
43
|
+
|
|
44
|
+
## Canon drafts → \`00_Inbox/\`
|
|
45
|
+
|
|
46
|
+
Write the BARE BODY only — no frontmatter, no links section (the post-write
|
|
47
|
+
hook stamps the 4 draft fields; the links section belongs to the Index):
|
|
48
|
+
|
|
49
|
+
Write("<vault>/00_Inbox/<Meaningful title>.md", "<body>")
|
|
50
|
+
|
|
51
|
+
Canon style: idiomatic vault language, academic tone, self-contained text
|
|
52
|
+
(no dialogue references, expand abbreviations on first use), no emoji.
|
|
53
|
+
The canon's viewpoint is objective knowledge about a system — your
|
|
54
|
+
personal "how I do it" belongs in your agent memory instead. Mark
|
|
55
|
+
hypotheses as hypotheses. The filename IS the title readers will see in
|
|
56
|
+
their index — make it understandable without opening the file.
|
|
57
|
+
|
|
58
|
+
## Your agent memory → \`06_Agent_Memory/<your name>/\`
|
|
59
|
+
|
|
60
|
+
Personal notes useful to you, written directly (no inbox). Frontmatter:
|
|
61
|
+
only \`subtype\` + \`description\`, the hook fills the rest:
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
subtype: <one of below>
|
|
65
|
+
description: "1–2 sentences on the content"
|
|
66
|
+
---
|
|
67
|
+
<body>
|
|
68
|
+
|
|
69
|
+
Five \`subtype\` values: \`feedback\` (from colleagues), \`context\`
|
|
70
|
+
(project/task handoff), \`reference\` (your navigation marks and
|
|
71
|
+
procedures), \`person_profile\` (facts about a person), \`pitfall\`
|
|
72
|
+
(a rule born from one incident — stepped on it once, wrote it down).
|
|
73
|
+
|
|
74
|
+
Material that is BOTH team knowledge and personal → do both: a draft in
|
|
75
|
+
\`00_Inbox/\` + a memory note mentioning it inline as \`[[Draft title]]\`.
|
|
76
|
+
|
|
77
|
+
## Editing rules
|
|
78
|
+
|
|
79
|
+
Edit the BODY of your own notes freely (you are \`author\` or in
|
|
80
|
+
\`coauthors\`) — reword, update, replace stale text in place; no
|
|
81
|
+
"Update YYYY-MM-DD" journals. Additions to FOREIGN notes go through a new
|
|
82
|
+
inbox draft (the Index merges and adds you to \`coauthors\`).
|
|
83
|
+
|
|
84
|
+
From the frontmatter you change ONLY \`status\`, by your note type's scale
|
|
85
|
+
(knowledge/list/memory: current → outdated; idea: new → in_progress /
|
|
86
|
+
dropped; decision: accepted → superseded; project files: active → paused /
|
|
87
|
+
completed; phases: planned → active → completed / paused / cancelled).
|
|
88
|
+
The decision STATEMENT in \`02_Decisions/\` is immutable — supersede it
|
|
89
|
+
with a new draft instead. \`needs_review\` is the Index's field, not yours.
|
|
90
|
+
Never edit the links section, tags, \`type\`, or another agent's memory
|
|
91
|
+
folder.
|
|
92
|
+
|
|
93
|
+
Deletion (\`rm\`) — only in YOUR memory folder and only at zero
|
|
94
|
+
connections (\`vault_graph\` first); otherwise flip \`status\` and let the
|
|
95
|
+
Index archive. Renames — same watershed.
|
|
96
|
+
|
|
97
|
+
## Projects
|
|
98
|
+
|
|
99
|
+
\`Plan <name>.md\` = the high-level phase list; \`Phase — <title>.md\` =
|
|
100
|
+
that phase's task checklist. Both are append-only: add, don't rewrite
|
|
101
|
+
history. The full Plan text is read with \`Read\` — the path is in your
|
|
102
|
+
index.
|
|
103
|
+
`;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Гайд писателя — RU-пресет. Host-wide фрагмент слоя 5; см. заголовок
|
|
3
|
+
* guide-en.ts. Семантическое зеркало EN-базы с RU-таксономией; стилевая
|
|
4
|
+
* база — живой гайд MergeMind.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const GUIDE_RU = `# iapeer-memory — общая память команды
|
|
8
|
+
|
|
9
|
+
iapeer-memory — общая память команды (агенты + человек): канон
|
|
10
|
+
(знания / решения / идеи / проекты / списки) + личная оперативка каждого
|
|
11
|
+
агента. Ты читаешь и пишешь.
|
|
12
|
+
|
|
13
|
+
**Перед ответом — сначала ищи в памяти.** Кто-то мог решить эту задачу и
|
|
14
|
+
записать. \`vault_search\` (найденное читай \`Read\`), \`vault_graph\` —
|
|
15
|
+
окрестность заметки, \`vault_map\` — глобальная карта.
|
|
16
|
+
|
|
17
|
+
**Проверяй прежде чем действовать.** Заметка — снимок на момент записи.
|
|
18
|
+
Проверь, что функция/файл/флаг существуют сейчас — особенно когда
|
|
19
|
+
пользователь собирается действовать по твоей рекомендации.
|
|
20
|
+
|
|
21
|
+
**При конфликте память vs наблюдение — доверяй наблюдению.** Обнови или
|
|
22
|
+
депрекируй устаревшую заметку. Память живая.
|
|
23
|
+
|
|
24
|
+
## Пиши проактивно — между сессиями тебя нет
|
|
25
|
+
|
|
26
|
+
Сессия эфемерна: завершится — контекст исчезнет. Vault — единственное,
|
|
27
|
+
что выживает. Не записал → потеряно навсегда.
|
|
28
|
+
|
|
29
|
+
Пиши без спроса. Осмысленный результат (фикс, решение, грабли, фидбек,
|
|
30
|
+
нюанс системы) фиксируется сразу, тобой. Канон — черновиком в
|
|
31
|
+
\`00_Входящие/\`; личное — в свою оперативку. Вопрос человеку «записать
|
|
32
|
+
ли?» — анти-паттерн.
|
|
33
|
+
|
|
34
|
+
**Пиши кратко.** Заметки инжектятся в контекст читателей; раздутость
|
|
35
|
+
стоит токенов всей команде.
|
|
36
|
+
|
|
37
|
+
**Подметай за собой — там же, сразу.** Записал или обновил заметку →
|
|
38
|
+
проверь, не устарела ли прежняя по теме: ставь финальный \`status\` или
|
|
39
|
+
удаляй (только свою оперативку и только при нуле связей — сначала
|
|
40
|
+
\`vault_graph\`).
|
|
41
|
+
|
|
42
|
+
## Канон-черновики → \`00_Входящие/\`
|
|
43
|
+
|
|
44
|
+
Пиши ГОЛОЕ ТЕЛО — без frontmatter и без секции связей (post-write хук
|
|
45
|
+
проставит 4 поля черновика; секция связей — зона Индекса):
|
|
46
|
+
|
|
47
|
+
Write("<vault>/00_Входящие/<Понятное название>.md", "<тело>")
|
|
48
|
+
|
|
49
|
+
Стиль канона: идиоматичный русский, академический тон, самодостаточный
|
|
50
|
+
текст (без отсылок к диалогу, аббревиатуры расшифровывай при первом
|
|
51
|
+
упоминании), без эмодзи. Ракурс канона — объективное знание о системе;
|
|
52
|
+
личное «как я действую» — в оперативку. Гипотезы помечай как гипотезы.
|
|
53
|
+
Имя файла = title, который читатели увидят в индексе — должно быть понятно
|
|
54
|
+
без открытия файла.
|
|
55
|
+
|
|
56
|
+
## Твоя оперативка → \`06_Оперативка_агентов/<твоё имя>/\`
|
|
57
|
+
|
|
58
|
+
Личные заметки, полезные тебе, пишутся напрямую (мимо Входящих).
|
|
59
|
+
Frontmatter: только \`subtype\` + \`description\`, остальное дозаполнит хук:
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
subtype: <одно из ниже>
|
|
63
|
+
description: «Краткое 1–2 предложения о содержимом»
|
|
64
|
+
---
|
|
65
|
+
<тело>
|
|
66
|
+
|
|
67
|
+
Пять значений \`subtype\`: \`обратная_связь\` (от коллег), \`контекст\`
|
|
68
|
+
(handoff проекта/задачи), \`справка\` (навигационные метки и процедурные
|
|
69
|
+
приёмы), \`профиль_человека\` (факты о человеке), \`грабли\` (правило после
|
|
70
|
+
конкретного инцидента — наступил раз, записал).
|
|
71
|
+
|
|
72
|
+
Материал и для команды, и лично тебе → делай оба: черновик во Входящих +
|
|
73
|
+
заметка в оперативке с inline \`[[Название черновика]]\` в теле.
|
|
74
|
+
|
|
75
|
+
## Правила правок
|
|
76
|
+
|
|
77
|
+
Тело СВОИХ заметок (ты \`author\` или в \`coauthors\`) правь свободно —
|
|
78
|
+
переформулируй, заменяй устаревшее прямо в тексте; без журналов
|
|
79
|
+
«## Update YYYY-MM-DD». Дополнение к ЧУЖОЙ заметке — новым черновиком во
|
|
80
|
+
Входящих (Индекс сольёт и допишет тебя в \`coauthors\`).
|
|
81
|
+
|
|
82
|
+
Из frontmatter меняешь ТОЛЬКО \`status\` по шкале типа (знание/список/
|
|
83
|
+
оперативка: актуально → устарело; идея: новая → реализуется / отброшена;
|
|
84
|
+
решение: принято → заменено; файлы проекта: активный → на паузе /
|
|
85
|
+
завершён; фазы: запланирована → активная → завершена / на паузе /
|
|
86
|
+
отменена). Утверждение решения в \`02_Решения/\` иммутабельно — замена
|
|
87
|
+
только новым черновиком. \`needs_review\` — поле Индекса, не твоё.
|
|
88
|
+
Секцию связей, теги, \`type\` и чужую оперативку не трогаешь никогда.
|
|
89
|
+
|
|
90
|
+
Удаление (\`rm\`) — только своя оперативка и только при нуле связей
|
|
91
|
+
(сначала \`vault_graph\`); иначе финальный \`status\`, заархивирует Индекс.
|
|
92
|
+
Переименование — тот же водораздел.
|
|
93
|
+
|
|
94
|
+
## Проекты
|
|
95
|
+
|
|
96
|
+
\`План <имя>.md\` — высокоуровневый список фаз; \`Фаза — <название>.md\` —
|
|
97
|
+
чеклист задач фазы. Оба append-only: дописывай, не переписывай историю.
|
|
98
|
+
Полный текст Плана читается \`Read\` — путь есть в твоём индексе.
|
|
99
|
+
`;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template registry + materialisation. Content is EMBEDDED in the package
|
|
3
|
+
* (TS constants → bundled into the compiled binary; no fs lookup into an
|
|
4
|
+
* evictable npx cache). init/update MATERIALISE the templates to
|
|
5
|
+
* `<plugins>/iapeer-memory/templates/<locale>/…` — the stable on-disk form
|
|
6
|
+
* the roles manifest points at and `verify --repair` re-renders from.
|
|
7
|
+
*
|
|
8
|
+
* Ownership: unlike the vault 99_System seeds (operator-owned, written
|
|
9
|
+
* once), the materialised templates are PACKAGE-owned runtime artifacts —
|
|
10
|
+
* they follow the package version and are overwritten on change
|
|
11
|
+
* (bytes-compare, no mtime churn).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import type { LocaleId } from "@agfpd/iapeer-memory-core";
|
|
17
|
+
import { GUIDE_EN } from "./guide-en.js";
|
|
18
|
+
import { GUIDE_RU } from "./guide-ru.js";
|
|
19
|
+
import {
|
|
20
|
+
COPYWRITER_DOCTRINE_EN,
|
|
21
|
+
DREAMWEAVER_DOCTRINE_EN,
|
|
22
|
+
INDEX_DOCTRINE_EN,
|
|
23
|
+
} from "./roles-en.js";
|
|
24
|
+
import {
|
|
25
|
+
COPYWRITER_DOCTRINE_RU,
|
|
26
|
+
DREAMWEAVER_DOCTRINE_RU,
|
|
27
|
+
INDEX_DOCTRINE_RU,
|
|
28
|
+
} from "./roles-ru.js";
|
|
29
|
+
|
|
30
|
+
export const ROLE_NAMES = ["index", "copywriter", "dreamweaver"] as const;
|
|
31
|
+
export type RoleName = (typeof ROLE_NAMES)[number];
|
|
32
|
+
|
|
33
|
+
const ROLES: Record<LocaleId, Record<RoleName, string>> = {
|
|
34
|
+
en: {
|
|
35
|
+
index: INDEX_DOCTRINE_EN,
|
|
36
|
+
copywriter: COPYWRITER_DOCTRINE_EN,
|
|
37
|
+
dreamweaver: DREAMWEAVER_DOCTRINE_EN,
|
|
38
|
+
},
|
|
39
|
+
ru: {
|
|
40
|
+
index: INDEX_DOCTRINE_RU,
|
|
41
|
+
copywriter: COPYWRITER_DOCTRINE_RU,
|
|
42
|
+
dreamweaver: DREAMWEAVER_DOCTRINE_RU,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GUIDES: Record<LocaleId, string> = { en: GUIDE_EN, ru: GUIDE_RU };
|
|
47
|
+
|
|
48
|
+
export function roleDoctrineTemplate(locale: LocaleId, role: RoleName): string {
|
|
49
|
+
return ROLES[locale][role];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function guideText(locale: LocaleId): string {
|
|
53
|
+
return GUIDES[locale];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Stable on-disk path of a materialised role template. */
|
|
57
|
+
export function roleTemplatePath(
|
|
58
|
+
templatesDir: string,
|
|
59
|
+
locale: LocaleId,
|
|
60
|
+
role: RoleName,
|
|
61
|
+
): string {
|
|
62
|
+
return path.join(templatesDir, locale, `${role}.md`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function guideTemplatePath(templatesDir: string, locale: LocaleId): string {
|
|
66
|
+
return path.join(templatesDir, locale, "guide.md");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type MaterialiseResult = { written: string[]; identical: string[] };
|
|
70
|
+
|
|
71
|
+
/** Write the locale's templates to disk (package-owned: overwrite on change). */
|
|
72
|
+
export function materialiseTemplates(opts: {
|
|
73
|
+
templatesDir: string;
|
|
74
|
+
locale: LocaleId;
|
|
75
|
+
}): MaterialiseResult {
|
|
76
|
+
const written: string[] = [];
|
|
77
|
+
const identical: string[] = [];
|
|
78
|
+
const entries: Array<[string, string]> = [
|
|
79
|
+
...ROLE_NAMES.map(
|
|
80
|
+
(role): [string, string] => [
|
|
81
|
+
roleTemplatePath(opts.templatesDir, opts.locale, role),
|
|
82
|
+
roleDoctrineTemplate(opts.locale, role),
|
|
83
|
+
],
|
|
84
|
+
),
|
|
85
|
+
[guideTemplatePath(opts.templatesDir, opts.locale), guideText(opts.locale)],
|
|
86
|
+
];
|
|
87
|
+
for (const [file, content] of entries) {
|
|
88
|
+
let existing: string | null = null;
|
|
89
|
+
try {
|
|
90
|
+
existing = fs.readFileSync(file, "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
existing = null;
|
|
93
|
+
}
|
|
94
|
+
if (existing === content) {
|
|
95
|
+
identical.push(file);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
99
|
+
const tmp = `${file}.tmp`;
|
|
100
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
101
|
+
fs.renameSync(tmp, file);
|
|
102
|
+
written.push(file);
|
|
103
|
+
}
|
|
104
|
+
return { written, identical };
|
|
105
|
+
}
|