blun-king-cli 9.1.563 → 9.1.565

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 (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. 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
+ }