blun-king-cli 9.1.564 → 9.1.566

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.
Files changed (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,337 +1,337 @@
1
- import { readFile, rename, stat, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { buildCatalog } from "./catalog.js";
4
- import { canonicalPath, projectStateDir } from "./paths.js";
5
-
6
- const RELATIONS = new Set([
7
- "loads", "belongs-to", "explains", "supports", "related",
8
- "contradicts", "supersedes-in-relevance"
9
- ]);
10
-
11
- const ENTITY_KINDS = new Set(["person", "agent", "group", "channel", "project"]);
12
- const ENTITY_RELATIONS = new Set([
13
- "knows", "works-with", "member-of", "communicates-via",
14
- "responsible-for", "reports-to", "related"
15
- ]);
16
- const ANNOTATION_LAYERS = new Set([
17
- "soul", "memory-index", "memory-fact", "reference", "project-reference",
18
- "identity", "voice", "conduct", "history"
19
- ]);
20
- const FORBIDDEN_ATTRIBUTE_KEYS = /^(permissions?|rights?|authorization|credentials?|secrets?|tokens?|api[-_]?keys?)$/i;
21
-
22
- function emptyGraph(root) {
23
- return {
24
- schema: "agentspine.graph/v1",
25
- root,
26
- edges: [],
27
- annotations: [],
28
- entities: [],
29
- entityEdges: [],
30
- history: []
31
- };
32
- }
33
-
34
- function normalizeGraph(value, root) {
35
- const graph = value && typeof value === "object" ? value : emptyGraph(root);
36
- graph.schema = "agentspine.graph/v1";
37
- graph.root = root;
38
- for (const key of ["edges", "annotations", "entities", "entityEdges", "history"]) {
39
- if (!Array.isArray(graph[key])) graph[key] = [];
40
- }
41
- return graph;
42
- }
43
-
44
- function preservePrevious(graph, kind, value) {
45
- if (!value) return;
46
- graph.history.push({
47
- kind,
48
- supersededAt: new Date().toISOString(),
49
- value: { ...value, authority: "context-only" },
50
- privacy: value.privacy || "private",
51
- authority: "context-only"
52
- });
53
- }
54
-
55
- function normalizeRelativePath(value, field) {
56
- if (!value || typeof value !== "string") throw new Error(`${field} is required`);
57
- const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
58
- if (normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
59
- throw new Error(`${field} must be a project-relative path`);
60
- }
61
- return normalized;
62
- }
63
-
64
- function normalizeConfidence(value) {
65
- const number = Number(value);
66
- if (!Number.isFinite(number)) throw new Error("confidence must be a finite number");
67
- return Math.max(0, Math.min(1, number));
68
- }
69
-
70
- function validateAttributes(value, path = "attributes") {
71
- if (value === null || value === undefined) return {};
72
- if (Array.isArray(value)) return value.map((item, index) => validateAttributes(item, `${path}[${index}]`));
73
- if (typeof value !== "object") return value;
74
- const result = {};
75
- for (const [key, item] of Object.entries(value)) {
76
- if (FORBIDDEN_ATTRIBUTE_KEYS.test(key)) {
77
- throw new Error(`${path}.${key} is authority data and cannot be stored in relationship memory`);
78
- }
79
- result[key] = validateAttributes(item, `${path}.${key}`);
80
- }
81
- if (Buffer.byteLength(JSON.stringify(result)) > 16384) throw new Error("attributes exceed 16 KiB");
82
- return result;
83
- }
84
-
85
- export async function loadGraph(root, providedCatalog = null) {
86
- const catalog = providedCatalog || await buildCatalog(root);
87
- const directory = await projectStateDir(catalog.root);
88
- const path = join(directory, "graph.json");
89
- try {
90
- const metadata = await stat(path);
91
- if (metadata.size > 5 * 1024 * 1024) throw new Error("relationship graph exceeds the 5 MiB read limit");
92
- return { graph: normalizeGraph(JSON.parse(await readFile(path, "utf8")), catalog.root), graphPath: path, catalog };
93
- } catch (error) {
94
- if (error.code !== "ENOENT") throw error;
95
- return {
96
- graph: emptyGraph(catalog.root),
97
- graphPath: path,
98
- catalog
99
- };
100
- }
101
- }
102
-
103
- async function loadRelationshipGraph(root, providedCatalog = null, { signal = null } = {}) {
104
- const canonicalRoot = providedCatalog?.root || await canonicalPath(root);
105
- const directory = await projectStateDir(canonicalRoot);
106
- const path = join(directory, "graph.json");
107
- try {
108
- const metadata = await stat(path);
109
- if (metadata.size > 5 * 1024 * 1024) throw new Error("relationship graph exceeds the 5 MiB read limit");
110
- const content = await readFile(path, { encoding: "utf8", signal });
111
- return { graph: normalizeGraph(JSON.parse(content), canonicalRoot), graphPath: path };
112
- } catch (error) {
113
- if (error.code !== "ENOENT") throw error;
114
- return { graph: emptyGraph(canonicalRoot), graphPath: path };
115
- }
116
- }
117
-
118
- async function saveGraph(graph, path) {
119
- const temporary = `${path}.${process.pid}.tmp`;
120
- const content = `${JSON.stringify(graph, null, 2)}\n`;
121
- if (Buffer.byteLength(content) > 5 * 1024 * 1024) throw new Error("relationship graph exceeds 5 MiB; archive or compact old observations first");
122
- await writeFile(temporary, content, { mode: 0o600 });
123
- await rename(temporary, path);
124
- }
125
-
126
- export async function linkDocuments({ root = process.cwd(), from, to, relation = "related", reason = "", confidence = 0.5 }) {
127
- if (!RELATIONS.has(relation)) throw new Error(`unsupported relation: ${relation}`);
128
- const { graph, graphPath, catalog } = await loadGraph(root);
129
- from = normalizeRelativePath(from, "from");
130
- to = normalizeRelativePath(to, "to");
131
- const known = new Set(catalog.documents.map((document) => document.relativePath));
132
- if (!known.has(from)) throw new Error(`unknown source document: ${from}`);
133
- if (!known.has(to)) throw new Error(`unknown target document: ${to}`);
134
- const edge = {
135
- from,
136
- to,
137
- relation,
138
- reason: String(reason).slice(0, 1000),
139
- confidence: normalizeConfidence(confidence),
140
- updatedAt: new Date().toISOString(),
141
- authority: "context-only"
142
- };
143
- const key = (item) => `${item.from}\0${item.to}\0${item.relation}`;
144
- preservePrevious(graph, "document-edge", graph.edges.find((item) => key(item) === key(edge)));
145
- graph.edges = graph.edges.filter((item) => key(item) !== key(edge));
146
- graph.edges.push(edge);
147
- graph.edges.sort((a, b) => key(a).localeCompare(key(b)));
148
- await saveGraph(graph, graphPath);
149
- return { edge, graphPath };
150
- }
151
-
152
- export async function annotateDocument({ root = process.cwd(), path, layer, reason = "", confidence = 0.5 }) {
153
- if (!ANNOTATION_LAYERS.has(layer)) throw new Error(`unsupported context-only layer: ${layer}`);
154
- const { graph, graphPath, catalog } = await loadGraph(root);
155
- path = normalizeRelativePath(path, "path");
156
- if (!catalog.documents.some((document) => document.relativePath === path)) {
157
- throw new Error(`unknown document: ${path}`);
158
- }
159
- const annotation = {
160
- path,
161
- layer,
162
- reason: String(reason).slice(0, 1000),
163
- confidence: normalizeConfidence(confidence),
164
- updatedAt: new Date().toISOString(),
165
- authority: "context-only"
166
- };
167
- preservePrevious(graph, "document-annotation", graph.annotations.find((item) => item.path === path));
168
- graph.annotations = graph.annotations.filter((item) => item.path !== path);
169
- graph.annotations.push(annotation);
170
- graph.annotations.sort((a, b) => a.path.localeCompare(b.path));
171
- await saveGraph(graph, graphPath);
172
- return { annotation, graphPath };
173
- }
174
-
175
- export async function upsertEntity({
176
- root = process.cwd(), id, kind, displayName = "", aliases = [],
177
- attributes = {}, sourceDocument = null, confidence = 0.5, privacy = "private", catalog: providedCatalog = null
178
- }) {
179
- if (!id || !/^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/.test(id)) throw new Error("id must be a stable, whitespace-free identifier");
180
- if (!ENTITY_KINDS.has(kind)) throw new Error(`unsupported entity kind: ${kind}`);
181
- if (!["private", "shared", "group"].includes(privacy)) throw new Error(`unsupported privacy scope: ${privacy}`);
182
- if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") throw new Error("attributes must be an object");
183
- const { graph, graphPath, catalog } = await loadGraph(root, providedCatalog);
184
- if (sourceDocument !== null) {
185
- sourceDocument = normalizeRelativePath(sourceDocument, "sourceDocument");
186
- if (!catalog.documents.some((document) => document.relativePath === sourceDocument)) {
187
- throw new Error(`unknown source document: ${sourceDocument}`);
188
- }
189
- }
190
- const entity = {
191
- id,
192
- kind,
193
- displayName: String(displayName).slice(0, 200),
194
- aliases: [...new Set((Array.isArray(aliases) ? aliases : []).map((alias) => String(alias).slice(0, 200)))].slice(0, 20),
195
- attributes: validateAttributes(attributes),
196
- sourceDocument,
197
- confidence: normalizeConfidence(confidence),
198
- privacy,
199
- updatedAt: new Date().toISOString(),
200
- authority: "context-only"
201
- };
202
- preservePrevious(graph, "entity", graph.entities.find((item) => item.id === id));
203
- graph.entities = graph.entities.filter((item) => item.id !== id);
204
- graph.entities.push(entity);
205
- graph.entities.sort((a, b) => a.id.localeCompare(b.id));
206
- await saveGraph(graph, graphPath);
207
- return { entity, graphPath };
208
- }
209
-
210
- export async function linkEntities({ root = process.cwd(), from, to, relation = "related", reason = "", confidence = 0.5,
211
- privacy = "private", catalog: providedCatalog = null }) {
212
- if (!ENTITY_RELATIONS.has(relation)) throw new Error(`unsupported entity relation: ${relation}`);
213
- if (!["private", "shared", "group"].includes(privacy)) throw new Error(`unsupported privacy scope: ${privacy}`);
214
- const { graph, graphPath } = await loadGraph(root, providedCatalog);
215
- const known = new Set(graph.entities.map((entity) => entity.id));
216
- if (!known.has(from)) throw new Error(`unknown source entity: ${from}`);
217
- if (!known.has(to)) throw new Error(`unknown target entity: ${to}`);
218
- const edge = {
219
- from,
220
- to,
221
- relation,
222
- reason: String(reason).slice(0, 1000),
223
- confidence: normalizeConfidence(confidence),
224
- privacy,
225
- updatedAt: new Date().toISOString(),
226
- authority: "context-only"
227
- };
228
- const key = (item) => `${item.from}\0${item.to}\0${item.relation}`;
229
- preservePrevious(graph, "entity-edge", graph.entityEdges.find((item) => key(item) === key(edge)));
230
- graph.entityEdges = graph.entityEdges.filter((item) => key(item) !== key(edge));
231
- graph.entityEdges.push(edge);
232
- graph.entityEdges.sort((a, b) => key(a).localeCompare(key(b)));
233
- await saveGraph(graph, graphPath);
234
- return { edge, graphPath };
235
- }
236
-
237
- export async function unlinkEntities({ root = process.cwd(), from, to, relation = "related", catalog: providedCatalog = null }) {
238
- if (!ENTITY_RELATIONS.has(relation)) throw new Error(`unsupported entity relation: ${relation}`);
239
- const { graph, graphPath } = await loadGraph(root, providedCatalog);
240
- const previous = graph.entityEdges.find((item) => item.from === from && item.to === to && item.relation === relation);
241
- if (!previous) return { removed: null, duplicate: true, graphPath };
242
- preservePrevious(graph, "entity-edge", previous);
243
- graph.entityEdges = graph.entityEdges.filter((item) => !(item.from === from && item.to === to && item.relation === relation));
244
- await saveGraph(graph, graphPath);
245
- return { removed: previous, duplicate: false, graphPath };
246
- }
247
-
248
- function relationshipAudience(graph, groupId) {
249
- const ids = new Set([groupId]);
250
- for (const edge of graph.entityEdges) {
251
- if (edge.relation !== "member-of" || edge.privacy === "private") continue;
252
- if (edge.from === groupId) ids.add(edge.to);
253
- if (edge.to === groupId) ids.add(edge.from);
254
- }
255
- return ids;
256
- }
257
-
258
- async function assembleRelationshipContext({
259
- root = process.cwd(), entityId, includePrivate = false, groupId = null, catalog: providedCatalog = null
260
- }, loadGraphImpl, signal) {
261
- if (!entityId) throw new Error("entityId is required");
262
- const { graph } = await loadGraphImpl(root, providedCatalog, { signal });
263
- if (groupId !== null) {
264
- const group = graph.entities.find((item) => item.id === groupId && item.kind === "group");
265
- if (!group) throw new Error(`unknown group entity: ${groupId}`);
266
- if (includePrivate) throw new Error("private relationship context cannot be assembled for a group audience");
267
- }
268
- const entity = graph.entities.find((item) => item.id === entityId);
269
- if (!entity) throw new Error(`unknown entity: ${entityId}`);
270
- const audience = groupId === null ? null : relationshipAudience(graph, groupId);
271
- if (audience && !audience.has(entityId)) throw new Error(`entity is not a visible member of group: ${groupId}`);
272
- const visible = (item) => {
273
- if (item.attributes?.identityBindingId && item.attributes.identityStatus !== "active") return false;
274
- if (item.privacy === "private") return includePrivate && groupId === null;
275
- if (item.privacy === "group" && !audience) return false;
276
- if (audience && item.id && !audience.has(item.id)) return false;
277
- return true;
278
- };
279
- if (!visible(entity)) throw new Error(`private relationship context requires includePrivate: ${entityId}`);
280
- const entities = new Map(graph.entities.map((item) => [item.id, item]));
281
- const visibleEdge = (edge) => visible(edge)
282
- && (!audience || [edge.from, edge.to].every((id) => audience.has(id)))
283
- && [edge.from, edge.to].every((id) => !entities.has(id) || visible(entities.get(id)));
284
- const directEdges = graph.entityEdges.filter((edge) => (edge.from === entityId || edge.to === entityId) && visibleEdge(edge));
285
- const teamEdges = groupId === null ? [] : graph.entityEdges.filter((edge) => edge.relation === "member-of"
286
- && (edge.from === groupId || edge.to === groupId) && visibleEdge(edge));
287
- const edgeKey = (edge) => `${edge.from}\0${edge.to}\0${edge.relation}`;
288
- const edges = [...new Map([...directEdges, ...teamEdges].map((edge) => [edgeKey(edge), edge])).values()]
289
- .sort((left, right) => edgeKey(left).localeCompare(edgeKey(right)));
290
- const ids = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
291
- ids.add(entityId);
292
- return {
293
- entity,
294
- relatedEntities: graph.entities.filter((item) => ids.has(item.id) && visible(item)),
295
- edges,
296
- history: graph.history.filter((entry) => {
297
- if (!visible(entry)) return false;
298
- const value = entry.value || {};
299
- if (value.id && entities.has(value.id) && !visible(entities.get(value.id))) return false;
300
- if ((value.from || value.to) && !visibleEdge(value)) return false;
301
- return value.id === entityId || value.from === entityId || value.to === entityId;
302
- }),
303
- groupId,
304
- authority: "context-only"
305
- };
306
- }
307
-
308
- export async function relationshipContext(options = {}, { loadGraphImpl = loadRelationshipGraph, timeoutMs = 5000 } = {}) {
309
- const controller = new AbortController();
310
- let timer;
311
- const timeoutError = new Error(`relationship context exceeded its ${timeoutMs} ms local read limit`);
312
- const deadline = new Promise((_resolve, reject) => {
313
- timer = setTimeout(() => {
314
- reject(timeoutError);
315
- controller.abort();
316
- }, timeoutMs);
317
- });
318
- try {
319
- return await Promise.race([assembleRelationshipContext(options, loadGraphImpl, controller.signal), deadline]);
320
- } catch (error) {
321
- if (error !== timeoutError) throw error;
322
- return {
323
- status: "degraded",
324
- reason: "local-state-timeout",
325
- timeoutMs,
326
- entity: null,
327
- relatedEntities: [],
328
- edges: [],
329
- history: [],
330
- groupId: options.groupId ?? null,
331
- authority: "context-only"
332
- };
333
- } finally {
334
- clearTimeout(timer);
335
- controller.abort();
336
- }
337
- }
1
+ import { readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { buildCatalog } from "./catalog.js";
4
+ import { canonicalPath, projectStateDir } from "./paths.js";
5
+
6
+ const RELATIONS = new Set([
7
+ "loads", "belongs-to", "explains", "supports", "related",
8
+ "contradicts", "supersedes-in-relevance"
9
+ ]);
10
+
11
+ const ENTITY_KINDS = new Set(["person", "agent", "group", "channel", "project"]);
12
+ const ENTITY_RELATIONS = new Set([
13
+ "knows", "works-with", "member-of", "communicates-via",
14
+ "responsible-for", "reports-to", "related"
15
+ ]);
16
+ const ANNOTATION_LAYERS = new Set([
17
+ "soul", "memory-index", "memory-fact", "reference", "project-reference",
18
+ "identity", "voice", "conduct", "history"
19
+ ]);
20
+ const FORBIDDEN_ATTRIBUTE_KEYS = /^(permissions?|rights?|authorization|credentials?|secrets?|tokens?|api[-_]?keys?)$/i;
21
+
22
+ function emptyGraph(root) {
23
+ return {
24
+ schema: "agentspine.graph/v1",
25
+ root,
26
+ edges: [],
27
+ annotations: [],
28
+ entities: [],
29
+ entityEdges: [],
30
+ history: []
31
+ };
32
+ }
33
+
34
+ function normalizeGraph(value, root) {
35
+ const graph = value && typeof value === "object" ? value : emptyGraph(root);
36
+ graph.schema = "agentspine.graph/v1";
37
+ graph.root = root;
38
+ for (const key of ["edges", "annotations", "entities", "entityEdges", "history"]) {
39
+ if (!Array.isArray(graph[key])) graph[key] = [];
40
+ }
41
+ return graph;
42
+ }
43
+
44
+ function preservePrevious(graph, kind, value) {
45
+ if (!value) return;
46
+ graph.history.push({
47
+ kind,
48
+ supersededAt: new Date().toISOString(),
49
+ value: { ...value, authority: "context-only" },
50
+ privacy: value.privacy || "private",
51
+ authority: "context-only"
52
+ });
53
+ }
54
+
55
+ function normalizeRelativePath(value, field) {
56
+ if (!value || typeof value !== "string") throw new Error(`${field} is required`);
57
+ const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
58
+ if (normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
59
+ throw new Error(`${field} must be a project-relative path`);
60
+ }
61
+ return normalized;
62
+ }
63
+
64
+ function normalizeConfidence(value) {
65
+ const number = Number(value);
66
+ if (!Number.isFinite(number)) throw new Error("confidence must be a finite number");
67
+ return Math.max(0, Math.min(1, number));
68
+ }
69
+
70
+ function validateAttributes(value, path = "attributes") {
71
+ if (value === null || value === undefined) return {};
72
+ if (Array.isArray(value)) return value.map((item, index) => validateAttributes(item, `${path}[${index}]`));
73
+ if (typeof value !== "object") return value;
74
+ const result = {};
75
+ for (const [key, item] of Object.entries(value)) {
76
+ if (FORBIDDEN_ATTRIBUTE_KEYS.test(key)) {
77
+ throw new Error(`${path}.${key} is authority data and cannot be stored in relationship memory`);
78
+ }
79
+ result[key] = validateAttributes(item, `${path}.${key}`);
80
+ }
81
+ if (Buffer.byteLength(JSON.stringify(result)) > 16384) throw new Error("attributes exceed 16 KiB");
82
+ return result;
83
+ }
84
+
85
+ export async function loadGraph(root, providedCatalog = null) {
86
+ const catalog = providedCatalog || await buildCatalog(root);
87
+ const directory = await projectStateDir(catalog.root);
88
+ const path = join(directory, "graph.json");
89
+ try {
90
+ const metadata = await stat(path);
91
+ if (metadata.size > 5 * 1024 * 1024) throw new Error("relationship graph exceeds the 5 MiB read limit");
92
+ return { graph: normalizeGraph(JSON.parse(await readFile(path, "utf8")), catalog.root), graphPath: path, catalog };
93
+ } catch (error) {
94
+ if (error.code !== "ENOENT") throw error;
95
+ return {
96
+ graph: emptyGraph(catalog.root),
97
+ graphPath: path,
98
+ catalog
99
+ };
100
+ }
101
+ }
102
+
103
+ async function loadRelationshipGraph(root, providedCatalog = null, { signal = null } = {}) {
104
+ const canonicalRoot = providedCatalog?.root || await canonicalPath(root);
105
+ const directory = await projectStateDir(canonicalRoot);
106
+ const path = join(directory, "graph.json");
107
+ try {
108
+ const metadata = await stat(path);
109
+ if (metadata.size > 5 * 1024 * 1024) throw new Error("relationship graph exceeds the 5 MiB read limit");
110
+ const content = await readFile(path, { encoding: "utf8", signal });
111
+ return { graph: normalizeGraph(JSON.parse(content), canonicalRoot), graphPath: path };
112
+ } catch (error) {
113
+ if (error.code !== "ENOENT") throw error;
114
+ return { graph: emptyGraph(canonicalRoot), graphPath: path };
115
+ }
116
+ }
117
+
118
+ async function saveGraph(graph, path) {
119
+ const temporary = `${path}.${process.pid}.tmp`;
120
+ const content = `${JSON.stringify(graph, null, 2)}\n`;
121
+ if (Buffer.byteLength(content) > 5 * 1024 * 1024) throw new Error("relationship graph exceeds 5 MiB; archive or compact old observations first");
122
+ await writeFile(temporary, content, { mode: 0o600 });
123
+ await rename(temporary, path);
124
+ }
125
+
126
+ export async function linkDocuments({ root = process.cwd(), from, to, relation = "related", reason = "", confidence = 0.5 }) {
127
+ if (!RELATIONS.has(relation)) throw new Error(`unsupported relation: ${relation}`);
128
+ const { graph, graphPath, catalog } = await loadGraph(root);
129
+ from = normalizeRelativePath(from, "from");
130
+ to = normalizeRelativePath(to, "to");
131
+ const known = new Set(catalog.documents.map((document) => document.relativePath));
132
+ if (!known.has(from)) throw new Error(`unknown source document: ${from}`);
133
+ if (!known.has(to)) throw new Error(`unknown target document: ${to}`);
134
+ const edge = {
135
+ from,
136
+ to,
137
+ relation,
138
+ reason: String(reason).slice(0, 1000),
139
+ confidence: normalizeConfidence(confidence),
140
+ updatedAt: new Date().toISOString(),
141
+ authority: "context-only"
142
+ };
143
+ const key = (item) => `${item.from}\0${item.to}\0${item.relation}`;
144
+ preservePrevious(graph, "document-edge", graph.edges.find((item) => key(item) === key(edge)));
145
+ graph.edges = graph.edges.filter((item) => key(item) !== key(edge));
146
+ graph.edges.push(edge);
147
+ graph.edges.sort((a, b) => key(a).localeCompare(key(b)));
148
+ await saveGraph(graph, graphPath);
149
+ return { edge, graphPath };
150
+ }
151
+
152
+ export async function annotateDocument({ root = process.cwd(), path, layer, reason = "", confidence = 0.5 }) {
153
+ if (!ANNOTATION_LAYERS.has(layer)) throw new Error(`unsupported context-only layer: ${layer}`);
154
+ const { graph, graphPath, catalog } = await loadGraph(root);
155
+ path = normalizeRelativePath(path, "path");
156
+ if (!catalog.documents.some((document) => document.relativePath === path)) {
157
+ throw new Error(`unknown document: ${path}`);
158
+ }
159
+ const annotation = {
160
+ path,
161
+ layer,
162
+ reason: String(reason).slice(0, 1000),
163
+ confidence: normalizeConfidence(confidence),
164
+ updatedAt: new Date().toISOString(),
165
+ authority: "context-only"
166
+ };
167
+ preservePrevious(graph, "document-annotation", graph.annotations.find((item) => item.path === path));
168
+ graph.annotations = graph.annotations.filter((item) => item.path !== path);
169
+ graph.annotations.push(annotation);
170
+ graph.annotations.sort((a, b) => a.path.localeCompare(b.path));
171
+ await saveGraph(graph, graphPath);
172
+ return { annotation, graphPath };
173
+ }
174
+
175
+ export async function upsertEntity({
176
+ root = process.cwd(), id, kind, displayName = "", aliases = [],
177
+ attributes = {}, sourceDocument = null, confidence = 0.5, privacy = "private", catalog: providedCatalog = null
178
+ }) {
179
+ if (!id || !/^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/.test(id)) throw new Error("id must be a stable, whitespace-free identifier");
180
+ if (!ENTITY_KINDS.has(kind)) throw new Error(`unsupported entity kind: ${kind}`);
181
+ if (!["private", "shared", "group"].includes(privacy)) throw new Error(`unsupported privacy scope: ${privacy}`);
182
+ if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") throw new Error("attributes must be an object");
183
+ const { graph, graphPath, catalog } = await loadGraph(root, providedCatalog);
184
+ if (sourceDocument !== null) {
185
+ sourceDocument = normalizeRelativePath(sourceDocument, "sourceDocument");
186
+ if (!catalog.documents.some((document) => document.relativePath === sourceDocument)) {
187
+ throw new Error(`unknown source document: ${sourceDocument}`);
188
+ }
189
+ }
190
+ const entity = {
191
+ id,
192
+ kind,
193
+ displayName: String(displayName).slice(0, 200),
194
+ aliases: [...new Set((Array.isArray(aliases) ? aliases : []).map((alias) => String(alias).slice(0, 200)))].slice(0, 20),
195
+ attributes: validateAttributes(attributes),
196
+ sourceDocument,
197
+ confidence: normalizeConfidence(confidence),
198
+ privacy,
199
+ updatedAt: new Date().toISOString(),
200
+ authority: "context-only"
201
+ };
202
+ preservePrevious(graph, "entity", graph.entities.find((item) => item.id === id));
203
+ graph.entities = graph.entities.filter((item) => item.id !== id);
204
+ graph.entities.push(entity);
205
+ graph.entities.sort((a, b) => a.id.localeCompare(b.id));
206
+ await saveGraph(graph, graphPath);
207
+ return { entity, graphPath };
208
+ }
209
+
210
+ export async function linkEntities({ root = process.cwd(), from, to, relation = "related", reason = "", confidence = 0.5,
211
+ privacy = "private", catalog: providedCatalog = null }) {
212
+ if (!ENTITY_RELATIONS.has(relation)) throw new Error(`unsupported entity relation: ${relation}`);
213
+ if (!["private", "shared", "group"].includes(privacy)) throw new Error(`unsupported privacy scope: ${privacy}`);
214
+ const { graph, graphPath } = await loadGraph(root, providedCatalog);
215
+ const known = new Set(graph.entities.map((entity) => entity.id));
216
+ if (!known.has(from)) throw new Error(`unknown source entity: ${from}`);
217
+ if (!known.has(to)) throw new Error(`unknown target entity: ${to}`);
218
+ const edge = {
219
+ from,
220
+ to,
221
+ relation,
222
+ reason: String(reason).slice(0, 1000),
223
+ confidence: normalizeConfidence(confidence),
224
+ privacy,
225
+ updatedAt: new Date().toISOString(),
226
+ authority: "context-only"
227
+ };
228
+ const key = (item) => `${item.from}\0${item.to}\0${item.relation}`;
229
+ preservePrevious(graph, "entity-edge", graph.entityEdges.find((item) => key(item) === key(edge)));
230
+ graph.entityEdges = graph.entityEdges.filter((item) => key(item) !== key(edge));
231
+ graph.entityEdges.push(edge);
232
+ graph.entityEdges.sort((a, b) => key(a).localeCompare(key(b)));
233
+ await saveGraph(graph, graphPath);
234
+ return { edge, graphPath };
235
+ }
236
+
237
+ export async function unlinkEntities({ root = process.cwd(), from, to, relation = "related", catalog: providedCatalog = null }) {
238
+ if (!ENTITY_RELATIONS.has(relation)) throw new Error(`unsupported entity relation: ${relation}`);
239
+ const { graph, graphPath } = await loadGraph(root, providedCatalog);
240
+ const previous = graph.entityEdges.find((item) => item.from === from && item.to === to && item.relation === relation);
241
+ if (!previous) return { removed: null, duplicate: true, graphPath };
242
+ preservePrevious(graph, "entity-edge", previous);
243
+ graph.entityEdges = graph.entityEdges.filter((item) => !(item.from === from && item.to === to && item.relation === relation));
244
+ await saveGraph(graph, graphPath);
245
+ return { removed: previous, duplicate: false, graphPath };
246
+ }
247
+
248
+ function relationshipAudience(graph, groupId) {
249
+ const ids = new Set([groupId]);
250
+ for (const edge of graph.entityEdges) {
251
+ if (edge.relation !== "member-of" || edge.privacy === "private") continue;
252
+ if (edge.from === groupId) ids.add(edge.to);
253
+ if (edge.to === groupId) ids.add(edge.from);
254
+ }
255
+ return ids;
256
+ }
257
+
258
+ async function assembleRelationshipContext({
259
+ root = process.cwd(), entityId, includePrivate = false, groupId = null, catalog: providedCatalog = null
260
+ }, loadGraphImpl, signal) {
261
+ if (!entityId) throw new Error("entityId is required");
262
+ const { graph } = await loadGraphImpl(root, providedCatalog, { signal });
263
+ if (groupId !== null) {
264
+ const group = graph.entities.find((item) => item.id === groupId && item.kind === "group");
265
+ if (!group) throw new Error(`unknown group entity: ${groupId}`);
266
+ if (includePrivate) throw new Error("private relationship context cannot be assembled for a group audience");
267
+ }
268
+ const entity = graph.entities.find((item) => item.id === entityId);
269
+ if (!entity) throw new Error(`unknown entity: ${entityId}`);
270
+ const audience = groupId === null ? null : relationshipAudience(graph, groupId);
271
+ if (audience && !audience.has(entityId)) throw new Error(`entity is not a visible member of group: ${groupId}`);
272
+ const visible = (item) => {
273
+ if (item.attributes?.identityBindingId && item.attributes.identityStatus !== "active") return false;
274
+ if (item.privacy === "private") return includePrivate && groupId === null;
275
+ if (item.privacy === "group" && !audience) return false;
276
+ if (audience && item.id && !audience.has(item.id)) return false;
277
+ return true;
278
+ };
279
+ if (!visible(entity)) throw new Error(`private relationship context requires includePrivate: ${entityId}`);
280
+ const entities = new Map(graph.entities.map((item) => [item.id, item]));
281
+ const visibleEdge = (edge) => visible(edge)
282
+ && (!audience || [edge.from, edge.to].every((id) => audience.has(id)))
283
+ && [edge.from, edge.to].every((id) => !entities.has(id) || visible(entities.get(id)));
284
+ const directEdges = graph.entityEdges.filter((edge) => (edge.from === entityId || edge.to === entityId) && visibleEdge(edge));
285
+ const teamEdges = groupId === null ? [] : graph.entityEdges.filter((edge) => edge.relation === "member-of"
286
+ && (edge.from === groupId || edge.to === groupId) && visibleEdge(edge));
287
+ const edgeKey = (edge) => `${edge.from}\0${edge.to}\0${edge.relation}`;
288
+ const edges = [...new Map([...directEdges, ...teamEdges].map((edge) => [edgeKey(edge), edge])).values()]
289
+ .sort((left, right) => edgeKey(left).localeCompare(edgeKey(right)));
290
+ const ids = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
291
+ ids.add(entityId);
292
+ return {
293
+ entity,
294
+ relatedEntities: graph.entities.filter((item) => ids.has(item.id) && visible(item)),
295
+ edges,
296
+ history: graph.history.filter((entry) => {
297
+ if (!visible(entry)) return false;
298
+ const value = entry.value || {};
299
+ if (value.id && entities.has(value.id) && !visible(entities.get(value.id))) return false;
300
+ if ((value.from || value.to) && !visibleEdge(value)) return false;
301
+ return value.id === entityId || value.from === entityId || value.to === entityId;
302
+ }),
303
+ groupId,
304
+ authority: "context-only"
305
+ };
306
+ }
307
+
308
+ export async function relationshipContext(options = {}, { loadGraphImpl = loadRelationshipGraph, timeoutMs = 5000 } = {}) {
309
+ const controller = new AbortController();
310
+ let timer;
311
+ const timeoutError = new Error(`relationship context exceeded its ${timeoutMs} ms local read limit`);
312
+ const deadline = new Promise((_resolve, reject) => {
313
+ timer = setTimeout(() => {
314
+ reject(timeoutError);
315
+ controller.abort();
316
+ }, timeoutMs);
317
+ });
318
+ try {
319
+ return await Promise.race([assembleRelationshipContext(options, loadGraphImpl, controller.signal), deadline]);
320
+ } catch (error) {
321
+ if (error !== timeoutError) throw error;
322
+ return {
323
+ status: "degraded",
324
+ reason: "local-state-timeout",
325
+ timeoutMs,
326
+ entity: null,
327
+ relatedEntities: [],
328
+ edges: [],
329
+ history: [],
330
+ groupId: options.groupId ?? null,
331
+ authority: "context-only"
332
+ };
333
+ } finally {
334
+ clearTimeout(timer);
335
+ controller.abort();
336
+ }
337
+ }