blun-king-cli 9.1.520 → 9.1.525
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/CHANGELOG.md +32 -0
- package/LIESMICH.txt +4 -2
- package/README.md +4 -2
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +1 -2
- package/agent-spine-plugin/CHANGELOG.md +81 -14
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/README.md +6 -4
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/blun.plugin.json +2 -2
- package/agent-spine-plugin/docs/acceptance.md +2 -2
- package/agent-spine-plugin/docs/architecture.md +1 -1
- package/agent-spine-plugin/docs/gateway-runtime.md +2 -2
- package/agent-spine-plugin/docs/host-integration.md +2 -2
- package/agent-spine-plugin/docs/learning.md +37 -2
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/docs/quality-gates.md +1 -1
- package/agent-spine-plugin/docs/relationships.md +1 -1
- package/agent-spine-plugin/docs/session-briefing.md +1 -1
- package/agent-spine-plugin/docs/source-roots.md +1 -1
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +1 -1
- package/agent-spine-plugin/scripts/check-hosts.js +2 -2
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +3 -3
- package/agent-spine-plugin/src/cli.js +69 -5
- package/agent-spine-plugin/src/hook.js +41 -23
- package/agent-spine-plugin/src/index.js +2 -1
- package/agent-spine-plugin/src/lib/acceptance.js +2 -3
- package/agent-spine-plugin/src/lib/attention.js +4 -4
- package/agent-spine-plugin/src/lib/audit.js +12 -3
- package/agent-spine-plugin/src/lib/authentication.js +4 -2
- package/agent-spine-plugin/src/lib/briefing.js +20 -8
- package/agent-spine-plugin/src/lib/catalog.js +28 -3
- package/agent-spine-plugin/src/lib/channel-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/continuity.js +18 -13
- package/agent-spine-plugin/src/lib/documents.js +23 -5
- package/agent-spine-plugin/src/lib/feed-transport.js +4 -2
- package/agent-spine-plugin/src/lib/graph.js +47 -12
- package/agent-spine-plugin/src/lib/learning.js +422 -48
- package/agent-spine-plugin/src/lib/paths.js +50 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +20 -14
- package/agent-spine-plugin/src/lib/preflight.js +50 -26
- package/agent-spine-plugin/src/lib/source-roots.js +38 -7
- package/agent-spine-plugin/src/mcp.js +50 -4
- package/agent-spine-plugin/src/version.js +1 -1
- package/bin/telegram-approval-relay.cjs +2 -0
- package/blun.mjs +111 -8
- package/package.json +8 -2
- package/scripts/check-approval-observability-regression.js +111 -0
- package/scripts/check-bundled-agent-spine-regression.js +48 -0
- package/scripts/check-session-picker-resume-metrics-regression.js +97 -0
- package/scripts/check-session-start-hook-context-regression.js +54 -4
- package/scripts/check-shell-terminal-isolation-regression.js +81 -0
- package/scripts/check-slash-escape-regression.js +89 -0
- package/scripts/check-telegram-loop-exactly-once-regression.js +71 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
5
6
|
|
|
6
7
|
export async function canonicalPath(input = process.cwd()) {
|
|
@@ -49,6 +50,54 @@ export function stateRoot(env = process.env) {
|
|
|
49
50
|
return join(env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "agentspine");
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function comparablePath(value) {
|
|
54
|
+
let cursor = resolve(value);
|
|
55
|
+
const missing = [];
|
|
56
|
+
let canonical = cursor;
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
canonical = realpathSync.native(cursor);
|
|
60
|
+
break;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== "ENOENT") break;
|
|
63
|
+
const parent = dirname(cursor);
|
|
64
|
+
if (parent === cursor) break;
|
|
65
|
+
missing.unshift(basename(cursor));
|
|
66
|
+
cursor = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
canonical = join(canonical, ...missing);
|
|
70
|
+
return canonical.replace(/[\\/]+$/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function samePath(left, right) {
|
|
74
|
+
const normalize = comparablePath;
|
|
75
|
+
const a = normalize(left);
|
|
76
|
+
const b = normalize(right);
|
|
77
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isUserHomeRoot(root, env = process.env) {
|
|
81
|
+
const candidates = [homedir(), env.HOME, env.USERPROFILE,
|
|
82
|
+
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : null]
|
|
83
|
+
.filter((value) => typeof value === "string" && value && isAbsolute(value));
|
|
84
|
+
return candidates.some((candidate) => samePath(root, candidate));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function statePathIsScanExcluded(catalog, path, env = process.env) {
|
|
88
|
+
if (!catalog || catalog.schema !== "agentspine.catalog/v1" || typeof path !== "string") return false;
|
|
89
|
+
const localStateRoot = stateRoot(env);
|
|
90
|
+
const pathBelongsToState = isInside(localStateRoot, path);
|
|
91
|
+
const canonicalRoot = comparablePath(catalog.root);
|
|
92
|
+
const canonicalStateRoot = comparablePath(localStateRoot);
|
|
93
|
+
if (!pathBelongsToState && !isInside(canonicalRoot, comparablePath(path))) return true;
|
|
94
|
+
if (pathBelongsToState && !isInside(canonicalRoot, canonicalStateRoot)) return true;
|
|
95
|
+
return catalog.scanPolicy?.stateRoot === "excluded"
|
|
96
|
+
&& isUserHomeRoot(canonicalRoot, env)
|
|
97
|
+
&& isInside(canonicalRoot, canonicalStateRoot)
|
|
98
|
+
&& pathBelongsToState;
|
|
99
|
+
}
|
|
100
|
+
|
|
52
101
|
export function projectId(root) {
|
|
53
102
|
return createHash("sha256").update(root).digest("hex").slice(0, 20);
|
|
54
103
|
}
|
|
@@ -249,7 +249,7 @@ function sameRuntimeEntity(entity, persona, binding) {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
async function reconcilePersonaGraph(paths, policy, runtime) {
|
|
252
|
-
let { graph } = await loadGraph(paths.catalog.root);
|
|
252
|
+
let { graph } = await loadGraph(paths.catalog.root, paths.catalog);
|
|
253
253
|
const changes = { groupsCreated: 0, entitiesUpdated: 0, membershipsAdded: 0, membershipsRemoved: 0 };
|
|
254
254
|
const activeGroupIds = [...new Set(runtime.personas
|
|
255
255
|
.filter((item) => item.status === "active" && item.groupId !== null)
|
|
@@ -265,9 +265,9 @@ async function reconcilePersonaGraph(paths, policy, runtime) {
|
|
|
265
265
|
}
|
|
266
266
|
if (!existing) {
|
|
267
267
|
await upsertEntity({ root: paths.catalog.root, id: groupId, kind: "group", privacy: "group",
|
|
268
|
-
attributes: { identitySource: "authenticated-persona-roster" }, confidence: 1 });
|
|
268
|
+
attributes: { identitySource: "authenticated-persona-roster" }, confidence: 1, catalog: paths.catalog });
|
|
269
269
|
changes.groupsCreated += 1;
|
|
270
|
-
graph = (await loadGraph(paths.catalog.root)).graph;
|
|
270
|
+
graph = (await loadGraph(paths.catalog.root, paths.catalog)).graph;
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -284,36 +284,40 @@ async function reconcilePersonaGraph(paths, policy, runtime) {
|
|
|
284
284
|
sourceDocument: existing?.sourceDocument
|
|
285
285
|
&& paths.catalog.documents.some((item) => item.relativePath === existing.sourceDocument)
|
|
286
286
|
? existing.sourceDocument : null,
|
|
287
|
-
privacy: persona.groupId ? "group" : "shared", confidence: 1 });
|
|
287
|
+
privacy: persona.groupId ? "group" : "shared", confidence: 1, catalog: paths.catalog });
|
|
288
288
|
changes.entitiesUpdated += 1;
|
|
289
|
-
graph = (await loadGraph(paths.catalog.root)).graph;
|
|
289
|
+
graph = (await loadGraph(paths.catalog.root, paths.catalog)).graph;
|
|
290
290
|
existing = graph.entities.find((item) => item.id === persona.personaId);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
const memberships = graph.entityEdges.filter((edge) => edge.from === persona.personaId && edge.relation === "member-of");
|
|
294
294
|
for (const edge of memberships.filter((item) => persona.status !== "active" || item.to !== persona.groupId)) {
|
|
295
|
-
await unlinkEntities({ root: paths.catalog.root, from: edge.from, to: edge.to, relation: edge.relation
|
|
295
|
+
await unlinkEntities({ root: paths.catalog.root, from: edge.from, to: edge.to, relation: edge.relation,
|
|
296
|
+
catalog: paths.catalog });
|
|
296
297
|
changes.membershipsRemoved += 1;
|
|
297
|
-
graph = (await loadGraph(paths.catalog.root)).graph;
|
|
298
|
+
graph = (await loadGraph(paths.catalog.root, paths.catalog)).graph;
|
|
298
299
|
}
|
|
299
300
|
if (persona.status === "active" && persona.groupId !== null
|
|
300
301
|
&& !graph.entityEdges.some((edge) => edge.from === persona.personaId && edge.to === persona.groupId
|
|
301
302
|
&& edge.relation === "member-of" && edge.privacy === "group")) {
|
|
302
303
|
await linkEntities({ root: paths.catalog.root, from: persona.personaId, to: persona.groupId,
|
|
303
|
-
relation: "member-of", reason: "Authenticated roster membership; context only.", confidence: 1, privacy: "group"
|
|
304
|
+
relation: "member-of", reason: "Authenticated roster membership; context only.", confidence: 1, privacy: "group",
|
|
305
|
+
catalog: paths.catalog });
|
|
304
306
|
changes.membershipsAdded += 1;
|
|
305
|
-
graph = (await loadGraph(paths.catalog.root)).graph;
|
|
307
|
+
graph = (await loadGraph(paths.catalog.root, paths.catalog)).graph;
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
return changes;
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
export async function applyPersonaRoster({
|
|
313
|
+
export async function applyPersonaRoster({
|
|
314
|
+
root = process.cwd(), bindings, rosterScopes = [], confirmation, now = new Date(), catalog: providedCatalog = null
|
|
315
|
+
}) {
|
|
312
316
|
if (confirmation !== CONFIRMATION) throw new Error("persona roster changes require explicit local owner confirmation");
|
|
313
317
|
if (!Array.isArray(bindings) || bindings.length > 256 || (!bindings.length && !rosterScopes.length)) {
|
|
314
318
|
throw new Error("bindings must contain up to 256 persona bindings or one explicit roster scope");
|
|
315
319
|
}
|
|
316
|
-
const paths = await pathsFor(root);
|
|
320
|
+
const paths = await pathsFor(root, providedCatalog);
|
|
317
321
|
return withLock(paths, async () => {
|
|
318
322
|
const [policy, runtime] = await Promise.all([
|
|
319
323
|
readJson(paths.personaPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
|
|
@@ -471,11 +475,13 @@ export async function loadPersonaRuntime(root = process.cwd(), providedCatalog =
|
|
|
471
475
|
return { policy, runtime, ...paths };
|
|
472
476
|
}
|
|
473
477
|
|
|
474
|
-
export async function syncPersonaRosterFromEnvironment({
|
|
478
|
+
export async function syncPersonaRosterFromEnvironment({
|
|
479
|
+
root = process.cwd(), env = process.env, now = new Date(), catalog: providedCatalog = null
|
|
480
|
+
} = {}) {
|
|
475
481
|
const configured = env.AGENTSPINE_PERSONA_ROSTER_FILE;
|
|
476
482
|
if (!configured) return { configured: false, changed: false };
|
|
477
483
|
if (!isAbsolute(configured)) throw new Error("AGENTSPINE_PERSONA_ROSTER_FILE must be an absolute path");
|
|
478
|
-
const catalog = await buildCatalog(root);
|
|
484
|
+
const catalog = providedCatalog || await buildCatalog(root);
|
|
479
485
|
const supplied = await lstat(configured);
|
|
480
486
|
if (supplied.isSymbolicLink() || !supplied.isFile()) throw new Error("persona roster must be a regular non-symlink file");
|
|
481
487
|
const canonical = await realpath(configured);
|
|
@@ -497,7 +503,7 @@ export async function syncPersonaRosterFromEnvironment({ root = process.cwd(), e
|
|
|
497
503
|
const rosterScopes = nativeScopes.map((scope) => ({ authenticator: "host-manifest", issuer: scope.issuer,
|
|
498
504
|
tenantId: scope.tenantId, host: scope.host, profileId: scope.profileId }));
|
|
499
505
|
const result = await applyPersonaRoster({ root: catalog.root, bindings, rosterScopes,
|
|
500
|
-
confirmation: CONFIRMATION, now: value.observedAt || now });
|
|
506
|
+
confirmation: CONFIRMATION, now: value.observedAt || now, catalog });
|
|
501
507
|
return { configured: true, changed: !result.duplicate || result.graphReconciled,
|
|
502
508
|
rosterChanged: !result.duplicate, graphReconciled: result.graphReconciled, graphChanges: result.graphChanges,
|
|
503
509
|
revision: value.revision,
|
|
@@ -21,8 +21,8 @@ const ENV_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
|
21
21
|
const MAX_POLICY_BYTES = 1024 * 1024;
|
|
22
22
|
const MAX_STATE_BYTES = 8 * 1024 * 1024;
|
|
23
23
|
const MAX_PROVIDER_BYTES = 1024 * 1024;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
24
|
+
const STANDARD_REQUIRED_INSTRUCTIONS_BYTES = 8 * 1024;
|
|
25
|
+
const MAX_CLAUDE_REQUIRED_INSTRUCTIONS_BYTES = 16 * 1024;
|
|
26
26
|
const MAX_REQUIRED_MEMORY_BYTES = 6 * 1024;
|
|
27
27
|
const RECEIPT_TTL_MS = 60_000;
|
|
28
28
|
const FORBIDDEN_MEMORY = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\b(?:password|passwort|secret|token|api[-_ ]?key|credential|permission|rights?|roles?|delegat|authoriz|berechtig|freigabe|approval|tool access|file access|network|production|payment|zahlung|policy)\b/i;
|
|
@@ -326,13 +326,16 @@ async function assertNoSymlinkParents(path, allowedRoot) {
|
|
|
326
326
|
}
|
|
327
327
|
return { root, target };
|
|
328
328
|
}
|
|
329
|
-
async function safeReadRequired(path, allowedRoot, maximum =
|
|
329
|
+
async function safeReadRequired(path, allowedRoot, maximum = STANDARD_REQUIRED_INSTRUCTIONS_BYTES, fileHooks = null) {
|
|
330
330
|
const checked = await assertNoSymlinkParents(path, allowedRoot);
|
|
331
331
|
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0);
|
|
332
332
|
const handle = await open(checked.target, flags);
|
|
333
333
|
try {
|
|
334
334
|
const before = await handle.stat();
|
|
335
|
-
if (!before.isFile()
|
|
335
|
+
if (!before.isFile()) throw new Error(`required instruction ${basename(path)} is not a regular file`);
|
|
336
|
+
if (before.size > maximum) {
|
|
337
|
+
throw new Error(`required instruction ${basename(path)} is ${before.size} bytes; mandatory limit is ${maximum} bytes`);
|
|
338
|
+
}
|
|
336
339
|
const buffer = Buffer.alloc(Number(before.size));
|
|
337
340
|
let offset = 0;
|
|
338
341
|
while (offset < buffer.length) {
|
|
@@ -350,8 +353,8 @@ async function safeReadRequired(path, allowedRoot, maximum = MAX_REQUIRED_INSTRU
|
|
|
350
353
|
|| current.size !== before.size || current.mtimeMs !== before.mtimeMs || current.ctimeMs !== before.ctimeMs) {
|
|
351
354
|
throw new Error(`required instruction ${basename(path)} was replaced during preflight`);
|
|
352
355
|
}
|
|
353
|
-
return { bytes: buffer.length, sha256: sha256(buffer),
|
|
354
|
-
identity: sha256(`${before.dev}\0${before.ino}\0${before.size}\0${before.mtimeMs}\0${before.ctimeMs}`) };
|
|
356
|
+
return { content: buffer.toString("utf8"), bytes: buffer.length, sha256: sha256(buffer),
|
|
357
|
+
identity: sha256(`${before.dev}\0${before.ino}\0${before.size}\0${before.mtimeMs}\0${before.ctimeMs}`) };
|
|
355
358
|
} finally { await handle.close(); }
|
|
356
359
|
}
|
|
357
360
|
|
|
@@ -374,6 +377,19 @@ function instructionDocuments(catalog, host) {
|
|
|
374
377
|
return catalog.documents.filter((item) => item.layer === "constitution" && pattern.test(item.relativePath))
|
|
375
378
|
.sort((left, right) => left.precedence - right.precedence || left.relativePath.localeCompare(right.relativePath));
|
|
376
379
|
}
|
|
380
|
+
function instructionBudget(host, usedBytes = 0) {
|
|
381
|
+
const hardLimitBytes = host === "claude"
|
|
382
|
+
? MAX_CLAUDE_REQUIRED_INSTRUCTIONS_BYTES
|
|
383
|
+
: STANDARD_REQUIRED_INSTRUCTIONS_BYTES;
|
|
384
|
+
const overflowBytes = Math.max(0, usedBytes - STANDARD_REQUIRED_INSTRUCTIONS_BYTES);
|
|
385
|
+
return {
|
|
386
|
+
mode: overflowBytes ? "claude-required-overflow" : "standard",
|
|
387
|
+
standardBytes: STANDARD_REQUIRED_INSTRUCTIONS_BYTES,
|
|
388
|
+
hardLimitBytes,
|
|
389
|
+
usedBytes,
|
|
390
|
+
overflowBytes
|
|
391
|
+
};
|
|
392
|
+
}
|
|
377
393
|
async function rejectKnownInstructionSymlinks(resolvedSources, host) {
|
|
378
394
|
const candidates = host === "claude"
|
|
379
395
|
? [join(resolvedSources.hostHome, "CLAUDE.md"), ...ancestorsBetween(resolvedSources.projectRoot, resolvedSources.cwd)
|
|
@@ -485,21 +501,22 @@ export async function runPreflight({ input, scope, resolvedSources, prompt, now
|
|
|
485
501
|
&& item.profileId === exactScope.profileId && item.tenantId === exactScope.tenantId) || null;
|
|
486
502
|
const instructionHost = resolvedSources.host;
|
|
487
503
|
await rejectKnownInstructionSymlinks(resolvedSources, instructionHost);
|
|
488
|
-
const documents = instructionDocuments(resolvedSources.catalog, instructionHost);
|
|
489
|
-
const requiredInstructions = [];
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
504
|
+
const documents = instructionDocuments(resolvedSources.catalog, instructionHost);
|
|
505
|
+
const requiredInstructions = [];
|
|
506
|
+
let instructionBytes = 0;
|
|
507
|
+
const maximumInstructionBytes = instructionBudget(instructionHost).hardLimitBytes;
|
|
508
|
+
for (const document of documents) {
|
|
509
|
+
const allowedRoot = document.sourceScope === "user" ? resolvedSources.hostHome : resolvedSources.projectRoot;
|
|
510
|
+
const snapshot = await safeReadRequired(document.path, allowedRoot, maximumInstructionBytes, fileHooks);
|
|
511
|
+
if (snapshot.sha256 !== document.sha256 || snapshot.bytes !== document.bytes) throw new Error(`required instruction changed after source resolution: ${document.relativePath}`);
|
|
512
|
+
instructionBytes += snapshot.bytes;
|
|
513
|
+
if (instructionBytes > maximumInstructionBytes) {
|
|
514
|
+
throw new Error(`required host instructions total ${instructionBytes} bytes; mandatory limit is ${maximumInstructionBytes} bytes`);
|
|
515
|
+
}
|
|
516
|
+
requiredInstructions.push({ path: document.path, displayPath: document.relativePath, scope: document.sourceScope,
|
|
517
|
+
bytes: snapshot.bytes, sha256: snapshot.sha256, identity: snapshot.identity, content: snapshot.content });
|
|
518
|
+
}
|
|
519
|
+
const appliedInstructionBudget = instructionBudget(instructionHost, instructionBytes);
|
|
503
520
|
const memoryState = validateMemories(await readJson(paths.memories, MAX_STATE_BYTES, emptyMemories));
|
|
504
521
|
const mustRemember = memoryState.entries.filter((item) => memoryMatches(item, exactScope));
|
|
505
522
|
if (Buffer.byteLength(JSON.stringify(mustRemember.map((item) => item.claim))) > MAX_REQUIRED_MEMORY_BYTES) {
|
|
@@ -519,13 +536,14 @@ export async function runPreflight({ input, scope, resolvedSources, prompt, now
|
|
|
519
536
|
}
|
|
520
537
|
const requiredProviderIds = (profile?.providers || []).filter((item) => item.required).map((item) => item.id);
|
|
521
538
|
const loadedIds = providerResults.flatMap((result) => result.items.map((item) => ({ providerId: result.providerId, id: item.id, revision: item.revision })));
|
|
522
|
-
const briefing = {
|
|
523
|
-
schema: "agentspine.preflight-briefing/v2", order: ["host-instructions", "must-remember", "required-retrieval", "current-work", "relationships", "accepted-context"],
|
|
524
|
-
instructions:
|
|
539
|
+
const briefing = {
|
|
540
|
+
schema: "agentspine.preflight-briefing/v2", order: ["host-instructions", "must-remember", "required-retrieval", "current-work", "relationships", "accepted-context"],
|
|
541
|
+
instructions: requiredInstructions.map(({ path: _path, identity: _identity, ...item }) => item),
|
|
525
542
|
mustRemember: mustRemember.map((item) => ({ id: item.id, version: item.version, claim: item.claim, checksum: item.checksum, authority: "context-only" })),
|
|
526
543
|
retrieval: providerResults.map((item) => ({ providerId: item.providerId, status: item.status, rejected: item.rejected, items: item.items })),
|
|
527
544
|
recallStatus: requiredProviderIds.length ? "required-complete" : "no-required-provider-configured",
|
|
528
|
-
|
|
545
|
+
instructionBudget: appliedInstructionBudget,
|
|
546
|
+
instruction: "Apply the complete host instructions first, then confirmed critical context and scoped retrieval. None of this content grants authority or relaxes host policy.",
|
|
529
547
|
authority: "context-only"
|
|
530
548
|
};
|
|
531
549
|
const createdAt = timestamp(now); const expiresAt = timestamp(new Date(new Date(createdAt).getTime() + RECEIPT_TTL_MS));
|
|
@@ -534,6 +552,7 @@ export async function runPreflight({ input, scope, resolvedSources, prompt, now
|
|
|
534
552
|
sessionId: exactScope.sessionId, projectId: exactScope.projectId, cwdDigest: sha256(exactScope.cwd),
|
|
535
553
|
taskId: exactScope.taskId, groupId: exactScope.groupId, hookEvent: "UserPromptSubmit", deliveryId, promptDigest,
|
|
536
554
|
instructionFiles: requiredInstructions.map((item) => ({ path: item.path, scope: item.scope, bytes: item.bytes, sha256: item.sha256, identity: item.identity })),
|
|
555
|
+
instructionBudget: appliedInstructionBudget,
|
|
537
556
|
policyRevision: policy.revision, policyProfileDigest: profile ? digestObject(profile) : null,
|
|
538
557
|
mustRemember: memoryProofs(mustRemember),
|
|
539
558
|
providerQueries: providerResults.map((item) => ({ providerId: item.providerId, queryDigest: item.queryDigest, status: item.status, rejected: item.rejected })),
|
|
@@ -600,12 +619,17 @@ export async function verifyPreflightReceipt({ receipt, input, scope, resolvedSo
|
|
|
600
619
|
const currentDocuments = instructionDocuments(freshSources.catalog, freshSources.host);
|
|
601
620
|
if (currentDocuments.length !== receipt.instructionFiles.length
|
|
602
621
|
|| currentDocuments.some((document, index) => document.path !== receipt.instructionFiles[index]?.path)) return false;
|
|
622
|
+
let instructionBytes = 0;
|
|
623
|
+
const maximumInstructionBytes = instructionBudget(receipt.instructionHost).hardLimitBytes;
|
|
603
624
|
for (const instruction of receipt.instructionFiles) {
|
|
604
625
|
const allowedRoot = instruction.scope === "user" ? freshSources.hostHome : freshSources.projectRoot;
|
|
605
|
-
const currentSnapshot = await safeReadRequired(instruction.path, allowedRoot);
|
|
626
|
+
const currentSnapshot = await safeReadRequired(instruction.path, allowedRoot, maximumInstructionBytes);
|
|
606
627
|
if (currentSnapshot.sha256 !== instruction.sha256 || currentSnapshot.bytes !== instruction.bytes
|
|
607
628
|
|| currentSnapshot.identity !== instruction.identity) return false;
|
|
629
|
+
instructionBytes += currentSnapshot.bytes;
|
|
608
630
|
}
|
|
631
|
+
if (instructionBytes > maximumInstructionBytes
|
|
632
|
+
|| canonical(receipt.instructionBudget) !== canonical(instructionBudget(receipt.instructionHost, instructionBytes))) return false;
|
|
609
633
|
} catch { return false; }
|
|
610
634
|
if (consume) {
|
|
611
635
|
const paths = storagePaths(env);
|
|
@@ -6,6 +6,7 @@ import { ancestorsBetween, canonicalPath, isInside, stateRoot } from "./paths.js
|
|
|
6
6
|
import { indexExplicitDocuments } from "./documents.js";
|
|
7
7
|
import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
|
|
8
8
|
import { purgeIndexedMemoryCache, resolveIndexedMemory } from "./indexed-memory.js";
|
|
9
|
+
import { catalogScanPolicy } from "./catalog.js";
|
|
9
10
|
|
|
10
11
|
export const SOURCE_REGISTRY_SCHEMA = "agentspine.source-roots/v1";
|
|
11
12
|
const MAX_REGISTRY_BYTES = 1024 * 1024;
|
|
@@ -109,6 +110,21 @@ function expandHome(value, home) {
|
|
|
109
110
|
return value;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function samePath(left, right) {
|
|
114
|
+
const normalize = (value) => resolve(value).replace(/[\\/]+$/, "");
|
|
115
|
+
const a = normalize(left);
|
|
116
|
+
const b = normalize(right);
|
|
117
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function homeRoots(env) {
|
|
121
|
+
const candidates = [homedir(), env.HOME, env.USERPROFILE,
|
|
122
|
+
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : null]
|
|
123
|
+
.filter((value) => typeof value === "string" && value && isAbsolute(value));
|
|
124
|
+
return [...new Set(await Promise.all(candidates.map(async (value) =>
|
|
125
|
+
await existingDirectory(value) || resolve(value))))];
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
async function readJsonObject(path) {
|
|
113
129
|
const file = await existingRegular(path);
|
|
114
130
|
if (!file) return null;
|
|
@@ -150,16 +166,22 @@ async function findRoot(cwd, markers) {
|
|
|
150
166
|
let cursor = cwd;
|
|
151
167
|
while (true) {
|
|
152
168
|
for (const marker of markers) {
|
|
153
|
-
try { await lstat(join(cursor, marker)); return cursor; } catch (error) {
|
|
169
|
+
try { await lstat(join(cursor, marker)); return { root: cursor, resolution: "project-marker" }; } catch (error) {
|
|
154
170
|
if (error.code !== "ENOENT") throw error;
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
173
|
const parent = dirname(cursor);
|
|
158
|
-
if (parent === cursor) return cwd;
|
|
174
|
+
if (parent === cursor) return { root: cwd, resolution: "cwd-fallback" };
|
|
159
175
|
cursor = parent;
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
function skippedExtraDirectory(name) {
|
|
180
|
+
const lower = name.toLowerCase();
|
|
181
|
+
return SKIP_EXTRA_DIRS.has(name) || SKIP_EXTRA_DIRS.has(lower)
|
|
182
|
+
|| lower.includes("dropbox") || lower === "onedrive" || lower.startsWith("onedrive - ");
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
async function containsProjectMarker(directory) {
|
|
164
186
|
try {
|
|
165
187
|
await lstat(join(directory, ".git"));
|
|
@@ -201,7 +223,7 @@ async function boundedMarkdownTree(directory, prefix, host, scope, precedenceSta
|
|
|
201
223
|
if (output.length >= maxFiles) throw new Error(`host-native rule tree exceeds ${maxFiles} files`);
|
|
202
224
|
if (entry.isSymbolicLink()) continue;
|
|
203
225
|
const path = join(current, entry.name);
|
|
204
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && !
|
|
226
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && !skippedExtraDirectory(entry.name)) await walk(path);
|
|
205
227
|
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
206
228
|
output.push({ path, id: `${prefix}/${relative(root, path).replaceAll("\\", "/")}`, host, scope,
|
|
207
229
|
binding: "host-native-rule-tree", precedence: precedenceStart + output.length });
|
|
@@ -390,23 +412,29 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
|
|
|
390
412
|
let projectRoot;
|
|
391
413
|
let sources;
|
|
392
414
|
let hostDetails = {};
|
|
415
|
+
let rootResolution = "explicit-root";
|
|
393
416
|
if (host === "codex") {
|
|
394
417
|
const codexHome = env.CODEX_HOME || env.BLUN_HOME || join(homedir(), ".codex");
|
|
395
418
|
hostHome = await existingDirectory(resolve(codexHome)) || resolve(codexHome);
|
|
396
419
|
const config = await codexConfig(hostHome);
|
|
397
|
-
|
|
420
|
+
if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
|
|
421
|
+
else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, config.rootMarkers));
|
|
398
422
|
sources = await codexSources({ cwd: canonicalCwd, projectRoot, codexHome: hostHome, config });
|
|
399
423
|
hostDetails = { rootMarkers: config.rootMarkers, fallbackNames: config.fallbackNames };
|
|
400
424
|
} else {
|
|
401
425
|
hostHome = await existingDirectory(resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")))
|
|
402
426
|
|| resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
|
|
403
|
-
|
|
427
|
+
if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
|
|
428
|
+
else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, [".git"]));
|
|
404
429
|
const result = await claudeSources({ cwd: canonicalCwd, projectRoot, configDir: hostHome, input, env, registry, deadline, memoryHooks });
|
|
405
430
|
sources = result.sources;
|
|
406
431
|
hostDetails = { memoryRoot: result.memoryRoot, memoryProvenance: result.memoryProvenance,
|
|
407
432
|
memoryDiagnostics: result.memoryDiagnostics };
|
|
408
433
|
}
|
|
409
|
-
|
|
434
|
+
const knownHomeRoots = await homeRoots(env);
|
|
435
|
+
const skippedHomeTree = knownHomeRoots.some((root) => samePath(root, projectRoot));
|
|
436
|
+
const skippedFallbackHomeTree = skippedHomeTree && rootResolution === "cwd-fallback";
|
|
437
|
+
if (!skippedHomeTree) {
|
|
410
438
|
sources.push(...await boundedMarkdownTree(projectRoot, "agentspine:project", host, "project", 3000, deadline,
|
|
411
439
|
{ projectBoundary: true, maxFiles: MAX_PROJECT_FILES, maxDirectoryEntries: MAX_PROJECT_DIRECTORY_ENTRIES }));
|
|
412
440
|
}
|
|
@@ -429,7 +457,9 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
|
|
|
429
457
|
scopes: Object.fromEntries(["user", "project", "project-memory"].map((scope) => [scope, documents.filter((item) => item.sourceScope === scope).length])),
|
|
430
458
|
reason: documents.length ? null : "No regular, non-symlink host-native Markdown source exists in the checked scope.",
|
|
431
459
|
personalContinuityLoaded: documents.some((item) => item.sourceScope === "user") || Boolean(activeUserState),
|
|
432
|
-
broadHomeScan: false,
|
|
460
|
+
broadHomeScan: false, projectTreeScan: skippedFallbackHomeTree ? "skipped-unmarked-home"
|
|
461
|
+
: skippedHomeTree ? "skipped-home-root" : "bounded",
|
|
462
|
+
rootResolution, registryRevision: registry.revision,
|
|
433
463
|
...(host === "claude" ? {
|
|
434
464
|
memoryBound: Boolean(hostDetails.memoryRoot),
|
|
435
465
|
memoryRootDigest: hostDetails.memoryRoot ? digest(hostDetails.memoryRoot).slice(0, 16) : null,
|
|
@@ -442,6 +472,7 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
|
|
|
442
472
|
};
|
|
443
473
|
const catalog = {
|
|
444
474
|
schema: "agentspine.catalog/v1", generatedAt: new Date().toISOString(), root: projectRoot,
|
|
475
|
+
scanPolicy: catalogScanPolicy(projectRoot, env),
|
|
445
476
|
preservation: "source-files-are-read-only", documents, conflicts: [], sourceRegistry: diagnostics,
|
|
446
477
|
summary: { total: documents.length, protected: documents.filter((item) => item.protected).length, conflicts: 0,
|
|
447
478
|
byLayer: Object.fromEntries([...new Set(documents.map((item) => item.layer))].sort().map((layer) => [layer, documents.filter((item) => item.layer === layer).length])) }
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./lib/attention.js";
|
|
11
11
|
import {
|
|
12
12
|
addLearningEvidence, configureLearning, deleteLearning, evaluateLearning,
|
|
13
|
-
learningContext, proposeLearning, reviewLearning, rollbackLearning
|
|
13
|
+
learningContext, learningOutcomeStatus, proposeLearning, reviewLearning, rollbackLearning
|
|
14
14
|
} from "./lib/learning.js";
|
|
15
15
|
import { checkDelegation, createTask, taskContext, updateTask } from "./lib/coordination.js";
|
|
16
16
|
import { sharedContext } from "./lib/sharing.js";
|
|
@@ -46,6 +46,8 @@ const tools = [
|
|
|
46
46
|
root: { type: "string" }, cwd: { type: "string" },
|
|
47
47
|
host: { type: "string", enum: ["codex", "claude", "generic"] },
|
|
48
48
|
entityId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
49
|
+
userId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
50
|
+
tenantId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
49
51
|
groupId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
50
52
|
projectId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
51
53
|
currentTaskId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
@@ -240,10 +242,18 @@ const tools = [
|
|
|
240
242
|
type: "object", required: ["kind", "claim", "evidence"],
|
|
241
243
|
properties: {
|
|
242
244
|
root: { type: "string" }, id: { type: "string" },
|
|
243
|
-
kind: { type: "string", enum: ["preference", "no-go", "goal", "correction", "personal-fact", "project-fact", "reference"] },
|
|
245
|
+
kind: { type: "string", enum: ["preference", "no-go", "goal", "correction", "personal-fact", "project-fact", "reference", "behavior"] },
|
|
244
246
|
claim: { type: "string", maxLength: 1000 }, subjectId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
245
247
|
privacy: { type: "string", enum: ["private", "shared", "group"] },
|
|
246
248
|
groupId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
249
|
+
scope: {
|
|
250
|
+
type: "object", additionalProperties: false,
|
|
251
|
+
properties: {
|
|
252
|
+
personaId: { anyOf: [{ type: "string" }, { type: "null" }] }, userId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
253
|
+
tenantId: { anyOf: [{ type: "string" }, { type: "null" }] }, projectId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
254
|
+
groupId: { anyOf: [{ type: "string" }, { type: "null" }] }, taskId: { anyOf: [{ type: "string" }, { type: "null" }] }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
247
257
|
supersedesId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
248
258
|
evidence: { "$ref": "#/$defs/evidence" }
|
|
249
259
|
},
|
|
@@ -297,11 +307,40 @@ const tools = [
|
|
|
297
307
|
properties: {
|
|
298
308
|
root: { type: "string" }, includePrivate: { type: "boolean" },
|
|
299
309
|
groupId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
300
|
-
|
|
310
|
+
scope: {
|
|
311
|
+
type: "object", additionalProperties: false,
|
|
312
|
+
properties: {
|
|
313
|
+
personaId: { anyOf: [{ type: "string" }, { type: "null" }] }, userId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
314
|
+
tenantId: { anyOf: [{ type: "string" }, { type: "null" }] }, projectId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
315
|
+
groupId: { anyOf: [{ type: "string" }, { type: "null" }] }, taskId: { anyOf: [{ type: "string" }, { type: "null" }] }
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
kinds: { type: "array", items: { type: "string", enum: ["preference", "no-go", "goal", "correction", "personal-fact", "project-fact", "reference", "behavior"] } },
|
|
301
319
|
subjectIds: { type: "array", items: { type: "string" } }, maxItems: { type: "integer", minimum: 0, maximum: 50 }
|
|
302
320
|
}
|
|
303
321
|
}
|
|
304
322
|
},
|
|
323
|
+
{
|
|
324
|
+
name: "learning_outcome_status",
|
|
325
|
+
description: "Read outcome receipt counts, contradiction state, and canary health for learned context. This read-only view is context-only and cannot record evidence, promote learning, or grant authority.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
root: { type: "string" },
|
|
330
|
+
scope: {
|
|
331
|
+
type: "object", additionalProperties: false,
|
|
332
|
+
properties: {
|
|
333
|
+
personaId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
334
|
+
userId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
335
|
+
tenantId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
336
|
+
projectId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
337
|
+
groupId: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
338
|
+
taskId: { anyOf: [{ type: "string" }, { type: "null" }] }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
305
344
|
{
|
|
306
345
|
name: "evaluate_learning",
|
|
307
346
|
description: "Evaluate pending candidates against the opt-in low-risk auto-promotion policy. Auto-promotion is disabled by default and limited to project facts and references.",
|
|
@@ -326,7 +365,13 @@ const tools = [
|
|
|
326
365
|
type: "object", additionalProperties: false,
|
|
327
366
|
properties: {
|
|
328
367
|
autoPromote: { type: "boolean" }, minConfidence: { type: "number", minimum: 0.5, maximum: 1 },
|
|
329
|
-
minEvidence: { type: "integer", minimum: 1, maximum: 10 }, maxContextItems: { type: "integer", minimum: 1, maximum: 50 }
|
|
368
|
+
minEvidence: { type: "integer", minimum: 1, maximum: 10 }, maxContextItems: { type: "integer", minimum: 1, maximum: 50 },
|
|
369
|
+
minOutcomeReceipts: { type: "integer", minimum: 2, maximum: 10 },
|
|
370
|
+
minImprovement: { type: "number", minimum: 0, maximum: 1 },
|
|
371
|
+
regressionTolerance: { type: "number", minimum: 0, maximum: 1 },
|
|
372
|
+
outcomeMaxAgeDays: { type: "integer", minimum: 1, maximum: 365 },
|
|
373
|
+
canaryReceipts: { type: "integer", minimum: 1, maximum: 10 },
|
|
374
|
+
canaryTtlDays: { type: "integer", minimum: 1, maximum: 90 }
|
|
330
375
|
}
|
|
331
376
|
}
|
|
332
377
|
}
|
|
@@ -459,6 +504,7 @@ async function callTool(name, args = {}) {
|
|
|
459
504
|
if (name === "add_learning_evidence") return textResult(await addLearningEvidence({ ...args, root }));
|
|
460
505
|
if (name === "review_learning") return textResult(await reviewLearning({ ...args, root }));
|
|
461
506
|
if (name === "learning_context") return textResult(await learningContext({ ...args, root }));
|
|
507
|
+
if (name === "learning_outcome_status") return textResult(await learningOutcomeStatus({ ...args, root }));
|
|
462
508
|
if (name === "evaluate_learning") return textResult(await evaluateLearning({ ...args, root }));
|
|
463
509
|
if (name === "rollback_learning") return textResult(await rollbackLearning({ ...args, root }));
|
|
464
510
|
if (name === "configure_learning") return textResult(await configureLearning({ ...args, root }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.11.4";
|
|
@@ -68,6 +68,8 @@ function resolveTelegramApprovalTarget(allowFrom, configuredChatId) {
|
|
|
68
68
|
: [];
|
|
69
69
|
const configured = String(configuredChatId ?? '').trim();
|
|
70
70
|
if (configured.length > 0) return allowed.includes(configured) ? configured : null;
|
|
71
|
+
const privateChats = allowed.filter((value) => /^[1-9]\d*$/u.test(value));
|
|
72
|
+
if (privateChats.length === 1) return privateChats[0];
|
|
71
73
|
return allowed.length === 1 ? allowed[0] : null;
|
|
72
74
|
}
|
|
73
75
|
|