blun-king-cli 9.1.567 → 9.1.570
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 +33 -0
- 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 +2 -1
- package/agent-spine-plugin/CHANGELOG.md +1581 -0
- package/agent-spine-plugin/README.md +30 -4
- package/agent-spine-plugin/blun.plugin.json +3 -3
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
- package/agent-spine-plugin/docs/host-integration.md +178 -0
- package/agent-spine-plugin/docs/preflight-recall.md +69 -0
- package/agent-spine-plugin/docs/preservation-contract.md +53 -0
- package/agent-spine-plugin/docs/quality-gates.md +50 -0
- package/agent-spine-plugin/docs/releasing.md +85 -0
- package/agent-spine-plugin/docs/session-timeline.md +251 -0
- package/agent-spine-plugin/docs/source-roots.md +113 -0
- package/agent-spine-plugin/docs/structured-completion.md +67 -0
- package/agent-spine-plugin/docs/world-model.md +94 -0
- package/agent-spine-plugin/hooks/codex.json +2 -2
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +13 -3
- package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
- package/agent-spine-plugin/scripts/check-hosts.js +14 -5
- package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
- package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
- package/agent-spine-plugin/scripts/check-install.js +478 -0
- package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
- package/agent-spine-plugin/scripts/check-syntax.js +29 -0
- package/agent-spine-plugin/scripts/github-actions.js +11 -0
- package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
- package/agent-spine-plugin/scripts/release-check.js +145 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +47 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
- package/agent-spine-plugin/spine-example/1-identity.md +12 -0
- package/agent-spine-plugin/spine-example/2-voice.md +6 -0
- package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
- package/agent-spine-plugin/spine-example/4-history.md +4 -0
- package/agent-spine-plugin/src/cli-agent.js +296 -0
- package/agent-spine-plugin/src/cli-attention.js +95 -0
- package/agent-spine-plugin/src/cli-autonomy.js +36 -0
- package/agent-spine-plugin/src/cli-common.js +71 -0
- package/agent-spine-plugin/src/cli-continuity.js +116 -0
- package/agent-spine-plugin/src/cli-core.js +128 -0
- package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
- package/agent-spine-plugin/src/cli-host.js +21 -0
- package/agent-spine-plugin/src/cli-learning.js +305 -0
- package/agent-spine-plugin/src/cli-premortem.js +16 -0
- package/agent-spine-plugin/src/cli-sharing.js +230 -0
- package/agent-spine-plugin/src/cli.js +40 -1350
- package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
- package/agent-spine-plugin/src/hook.js +185 -567
- package/agent-spine-plugin/src/index.js +16 -2
- package/agent-spine-plugin/src/lib/acceptance.js +113 -11
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
- package/agent-spine-plugin/src/lib/attention-context.js +167 -0
- package/agent-spine-plugin/src/lib/attention-events.js +113 -0
- package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
- package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
- package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
- package/agent-spine-plugin/src/lib/attention.js +9 -600
- package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
- package/agent-spine-plugin/src/lib/audit.js +56 -6
- package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
- package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
- package/agent-spine-plugin/src/lib/autonomy.js +8 -0
- package/agent-spine-plugin/src/lib/briefing.js +67 -4
- package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
- package/agent-spine-plugin/src/lib/catalog.js +1 -1
- package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
- package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
- package/agent-spine-plugin/src/lib/context.js +3 -1
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
- package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
- package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
- package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
- package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
- package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
- package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
- package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
- package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
- package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
- package/agent-spine-plugin/src/lib/documents.js +27 -5
- package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
- package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
- package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
- package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
- package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
- package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
- package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
- package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
- package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
- package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
- package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
- package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
- package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
- package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
- package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
- package/agent-spine-plugin/src/lib/hook-context.js +424 -0
- package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
- package/agent-spine-plugin/src/lib/hook-input.js +27 -0
- package/agent-spine-plugin/src/lib/hook-output.js +140 -0
- package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
- package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
- package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
- package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
- package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
- package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
- package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
- package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
- package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
- package/agent-spine-plugin/src/lib/learning-context.js +130 -0
- package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
- package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
- package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
- package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
- package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
- package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
- package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
- package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
- package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
- package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
- package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
- package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
- package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
- package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
- package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
- package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
- package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
- package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
- package/agent-spine-plugin/src/lib/learning.js +36 -6923
- package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
- package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
- package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
- package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
- package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
- package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
- package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
- package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
- package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
- package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
- package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
- package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
- package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
- package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
- package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
- package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
- package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
- package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
- package/agent-spine-plugin/src/lib/source-roots.js +69 -136
- package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
- package/agent-spine-plugin/src/lib/world-model.js +278 -0
- package/agent-spine-plugin/src/mcp.js +20 -161
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +22 -5
- package/bin/agent-resume-snapshot.cjs +2 -2
- package/bin/agentspine-king-goal-inbox.mjs +111 -0
- package/bin/agentspine-king-goal-intake.mjs +106 -0
- package/bin/baseline-skill-performance-policy.cjs +1 -16
- package/bin/core-bootstrap.js +3 -0
- package/bin/curiosity-scout-policy.cjs +5 -1
- package/bin/input-draft-persistence.cjs +2 -2
- package/bin/king-tui-function-contract.json +33 -0
- package/bin/launcher-restart-policy.cjs +150 -0
- package/bin/launcher-runtime.js +56 -44
- package/bin/managed-context-startup-policy.cjs +27 -0
- package/bin/managed-plugin-selection.cjs +116 -0
- package/bin/mistake-relevance-policy.cjs +1 -1
- package/bin/observer-hooks.cjs +14 -0
- package/bin/oversized-context-offload-policy.cjs +86 -0
- package/bin/plugin-bootstrap.js +7 -40
- package/bin/proactive-compaction-policy.cjs +1 -1
- package/bin/provider-model-refresh-deadline.cjs +53 -0
- package/bin/provider-model-refresh-policy.cjs +107 -0
- package/bin/release-artifact-freeze-policy.cjs +30 -0
- package/bin/repeated-user-message-projection.cjs +3 -126
- package/bin/research-page-result.cjs +74 -0
- package/bin/runtime-exit-ledger.cjs +1 -0
- package/bin/session-compaction-policy.cjs +84 -0
- package/bin/skill-listing-performance-policy.cjs +2 -2
- package/bin/standard-tools-bootstrap.js +0 -37
- package/bin/subagent-skill-policy.cjs +3 -1
- package/bin/telegram-approval-relay.cjs +12 -7
- package/bin/telegram-private-conversation-policy.cjs +3 -2
- package/bin/telegram-queue-handoff-policy.cjs +24 -0
- package/bin/thinking-activity-status-policy.cjs +1 -1
- package/bin/thinking-only-guard.cjs +16 -12
- package/bin/tool-call-loop-policy.cjs +0 -2
- package/bin/tool-result-offload-policy.cjs +11 -2
- package/bin/tui-functional-contract.cjs +55 -0
- package/bin/update-notice.js +18 -14
- package/bin/user-message-offload-policy.cjs +1 -1
- package/bin/user-prompt-hook-origin-policy.cjs +34 -0
- package/bin/windows-node-crash-dump.cjs +110 -0
- package/blun.mjs +2861 -1173
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +5 -18
- package/standard-skills/research-evidence/SKILL.md +39 -0
- package/standard-skills/research-evidence/references/evidence-format.md +82 -0
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
- package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
- package/standard-skills/web-lesen/SKILL.md +37 -22
- package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
- package/telegram-plugin/DELIVERY.md +36 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
- package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
- package/telegram-plugin/dist/bridge.mjs +7 -56
- package/telegram-plugin/dist/mcp-server.mjs +33 -4
- package/agent-spine-plugin/skill/SKILL.md +0 -76
- package/bin/mnemo-connect-heartbeat.cjs +0 -204
- package/bin/mnemo-tool-agent-policy.cjs +0 -22
- package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { buildCatalog } from "./catalog.js";
|
|
5
|
+
import { withOwnedFileLock } from "./owned-file-lock.js";
|
|
6
|
+
import { projectStateDir } from "./paths.js";
|
|
7
|
+
import {
|
|
8
|
+
EVIDENCE_TYPES, PRIVACY, ID_RE, MAX_STATE_BYTES, SECRET_RE, AUTHORITY_ASSERTION_RE,
|
|
9
|
+
emptyLearning
|
|
10
|
+
} from "./learning-schema.js";
|
|
11
|
+
import {
|
|
12
|
+
normalizeState
|
|
13
|
+
} from "./learning-state-upgrade.js";
|
|
14
|
+
import {
|
|
15
|
+
validConfig
|
|
16
|
+
} from "./learning-scope-targets.js";
|
|
17
|
+
import {
|
|
18
|
+
visible
|
|
19
|
+
} from "./learning-context.js";
|
|
20
|
+
|
|
21
|
+
export function date(value, field = "date") {
|
|
22
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
23
|
+
if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
|
|
24
|
+
return parsed.toISOString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function number(value, field, minimum, maximum) {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) {
|
|
30
|
+
throw new Error(`${field} must be between ${minimum} and ${maximum}`);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function integer(value, field, minimum, maximum) {
|
|
36
|
+
const parsed = number(value, field, minimum, maximum);
|
|
37
|
+
if (!Number.isInteger(parsed)) throw new Error(`${field} must be an integer`);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function relativePath(value, field) {
|
|
42
|
+
if (value === null || value === undefined || value === "") return null;
|
|
43
|
+
if (typeof value !== "string") throw new Error(`${field} must be a project-relative path`);
|
|
44
|
+
const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
45
|
+
if (normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
46
|
+
throw new Error(`${field} must be a project-relative path`);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function safeText(value, field, maximum) {
|
|
52
|
+
if (!value || typeof value !== "string") throw new Error(`${field} is required`);
|
|
53
|
+
const text = value.trim().slice(0, maximum);
|
|
54
|
+
if (SECRET_RE.test(text)) throw new Error(`${field} appears to contain a secret and cannot enter learning state`);
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertSafeClaim(claim) {
|
|
59
|
+
if (AUTHORITY_ASSERTION_RE.test(claim)) {
|
|
60
|
+
throw new Error("authority and access claims cannot become learned context");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isGroupMember(graph, groupId, entityId) {
|
|
65
|
+
if (!entityId || entityId === groupId) return true;
|
|
66
|
+
return graph.entityEdges.some((edge) => edge.relation === "member-of" && edge.privacy !== "private" && (
|
|
67
|
+
(edge.from === entityId && edge.to === groupId) || (edge.to === entityId && edge.from === groupId)
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function validateScope(privacy, groupId, graph, subjectId) {
|
|
72
|
+
if (!PRIVACY.has(privacy)) throw new Error(`unsupported privacy scope: ${privacy}`);
|
|
73
|
+
if (subjectId !== null && !graph.entities.some((entity) => entity.id === subjectId)) throw new Error(`unknown subject entity: ${subjectId}`);
|
|
74
|
+
if (privacy === "group") {
|
|
75
|
+
if (!groupId) throw new Error("group privacy requires groupId");
|
|
76
|
+
const group = graph.entities.find((entity) => entity.id === groupId);
|
|
77
|
+
if (!group || group.kind !== "group") throw new Error(`unknown group entity: ${groupId}`);
|
|
78
|
+
if (!isGroupMember(graph, groupId, subjectId)) throw new Error(`subject is not a visible member of group: ${groupId}`);
|
|
79
|
+
} else if (groupId !== null && groupId !== undefined) {
|
|
80
|
+
throw new Error("groupId is only valid with group privacy");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readState(path, root) {
|
|
85
|
+
try {
|
|
86
|
+
const metadata = await stat(path);
|
|
87
|
+
if (metadata.size > MAX_STATE_BYTES) throw new Error("learning state exceeds the 5 MiB read limit");
|
|
88
|
+
return normalizeState(JSON.parse(await readFile(path, "utf8")), root);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code !== "ENOENT") throw error;
|
|
91
|
+
return emptyLearning(root);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function loadLearning(root = process.cwd(), providedCatalog = null) {
|
|
96
|
+
const catalog = providedCatalog || await buildCatalog(root);
|
|
97
|
+
const directory = await projectStateDir(catalog.root);
|
|
98
|
+
const learningPath = join(directory, "learning.json");
|
|
99
|
+
return { learning: await readState(learningPath, catalog.root), learningPath, catalog };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function inspectLearning(root = process.cwd(), providedCatalog = null) {
|
|
103
|
+
const catalog = providedCatalog || await buildCatalog(root);
|
|
104
|
+
const directory = await projectStateDir(catalog.root);
|
|
105
|
+
const learningPath = join(directory, "learning.json");
|
|
106
|
+
try {
|
|
107
|
+
return { learning: await readState(learningPath, catalog.root), learningPath, catalog, error: null };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { learning: emptyLearning(catalog.root), learningPath, catalog, error: error.message };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function saveState(state, path, beforeReplace = null) {
|
|
114
|
+
const content = `${JSON.stringify(state, null, 2)}\n`;
|
|
115
|
+
if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("learning state exceeds 5 MiB; reject or delete old candidates first");
|
|
116
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
117
|
+
await writeFile(temporary, content, { mode: 0o600 });
|
|
118
|
+
try {
|
|
119
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
120
|
+
try {
|
|
121
|
+
await beforeReplace?.();
|
|
122
|
+
await rename(temporary, path);
|
|
123
|
+
break;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const transientWindowsReplace = process.platform === "win32"
|
|
126
|
+
&& ["EACCES", "EBUSY", "EPERM"].includes(error.code);
|
|
127
|
+
if (!transientWindowsReplace || attempt >= 7) throw error;
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
await unlink(temporary).catch((cleanupError) => {
|
|
133
|
+
if (cleanupError.code !== "ENOENT") error.cleanupError = cleanupError;
|
|
134
|
+
});
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function withLock(path, root, task) {
|
|
140
|
+
const lockPath = `${path}.lock`;
|
|
141
|
+
return withOwnedFileLock(lockPath, async ({ assertOwned }) => {
|
|
142
|
+
const state = await readState(path, root);
|
|
143
|
+
if (!validConfig(state.config)) throw new Error("learning configuration is invalid; run the audit before learning");
|
|
144
|
+
const result = await task(state);
|
|
145
|
+
await saveState(state, path, assertOwned);
|
|
146
|
+
return result;
|
|
147
|
+
}).catch((error) => {
|
|
148
|
+
if (error.message === "state is busy; retry shortly") {
|
|
149
|
+
throw new Error("learning state is busy; retry shortly");
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function mutation(root, operation, providedCatalog = null) {
|
|
156
|
+
const catalog = providedCatalog || await buildCatalog(root);
|
|
157
|
+
const { learningPath } = await loadLearning(catalog.root, catalog);
|
|
158
|
+
return withLock(learningPath, catalog.root, async (state) => operation(state, catalog, learningPath));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function preserve(state, kind, value, now) {
|
|
162
|
+
if (!value) return;
|
|
163
|
+
state.history.push({
|
|
164
|
+
kind,
|
|
165
|
+
recordId: value.id || "config",
|
|
166
|
+
subjectId: value.subjectId || null,
|
|
167
|
+
supersededAt: now,
|
|
168
|
+
privacy: value.privacy || "private",
|
|
169
|
+
value: { ...value, authority: "context-only" },
|
|
170
|
+
authority: "context-only"
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function normalizeEvidence(input, catalog, now) {
|
|
175
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("evidence is required");
|
|
176
|
+
const id = input.id || `evidence:${randomUUID()}`;
|
|
177
|
+
if (!ID_RE.test(id)) throw new Error("evidence.id must be a stable, whitespace-free identifier");
|
|
178
|
+
const type = input.type || "interaction";
|
|
179
|
+
if (!EVIDENCE_TYPES.has(type)) throw new Error(`unsupported evidence type: ${type}`);
|
|
180
|
+
const sourceDocument = relativePath(input.sourceDocument, "evidence.sourceDocument");
|
|
181
|
+
let sourceSha256 = null;
|
|
182
|
+
if (sourceDocument !== null) {
|
|
183
|
+
const source = catalog.documents.find((document) => document.relativePath === sourceDocument);
|
|
184
|
+
if (!source) throw new Error(`unknown evidence source document: ${sourceDocument}`);
|
|
185
|
+
sourceSha256 = source.sha256;
|
|
186
|
+
}
|
|
187
|
+
if (type === "document" && !sourceDocument) throw new Error("document evidence requires sourceDocument");
|
|
188
|
+
return {
|
|
189
|
+
id,
|
|
190
|
+
type,
|
|
191
|
+
summary: safeText(input.summary, "evidence.summary", 500),
|
|
192
|
+
sourceDocument,
|
|
193
|
+
sourceSha256,
|
|
194
|
+
confidence: number(input.confidence ?? 0.5, "evidence.confidence", 0, 1),
|
|
195
|
+
observedAt: date(input.observedAt || now, "evidence.observedAt"),
|
|
196
|
+
authority: "context-only"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function evidenceConfidence(evidence) {
|
|
201
|
+
if (!evidence.length) return 0;
|
|
202
|
+
return evidence.reduce((sum, item) => sum + item.confidence, 0) / evidence.length;
|
|
203
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
PAIRED_EVALUATIONS, ROOT_BOUND_EVALUATIONS, REGISTRY_BOUND_EVALUATIONS, INITIAL_TRIAL_EVALUATIONS
|
|
4
|
+
} from "./learning-schema.js";
|
|
5
|
+
import {
|
|
6
|
+
exactScope, digest, revokedMeasurementForCandidate, activeEvaluationBinding
|
|
7
|
+
} from "./learning-scope-targets.js";
|
|
8
|
+
import {
|
|
9
|
+
revokedEvaluationForCandidate, validationOutcomeReferences, validationLeasePayload
|
|
10
|
+
} from "./learning-validation-contracts.js";
|
|
11
|
+
import {
|
|
12
|
+
revokedValidationForCandidate, validationLeaseState, initialTrialTimeout
|
|
13
|
+
} from "./learning-validation-runtime.js";
|
|
14
|
+
import {
|
|
15
|
+
revokedEvidenceSourceAttestationForCandidate
|
|
16
|
+
} from "./learning-measurement-contracts.js";
|
|
17
|
+
import {
|
|
18
|
+
revokedApplicationForCandidate, revokedOutcomeForCandidate
|
|
19
|
+
} from "./learning-delivery-contracts.js";
|
|
20
|
+
import {
|
|
21
|
+
preserve
|
|
22
|
+
} from "./learning-storage.js";
|
|
23
|
+
import {
|
|
24
|
+
outcomeFresh
|
|
25
|
+
} from "./learning-applications.js";
|
|
26
|
+
import {
|
|
27
|
+
outcomeMatchesContract, outcomeMatchesInitialTrial, improvement, rollbackCandidate, recordInitialTrialFailure, recordTrialRetryExhaustion
|
|
28
|
+
} from "./learning-reconciliation.js";
|
|
29
|
+
|
|
30
|
+
export function reconcileCanary(state, candidate, timestamp) {
|
|
31
|
+
const canary = candidate.promotion?.canary;
|
|
32
|
+
if (candidate.status !== "accepted" || candidate.promotion?.mode !== "outcome-canary"
|
|
33
|
+
|| !["active", "validated"].includes(canary?.status)) {
|
|
34
|
+
return { candidate, decision: "unchanged", restored: [] };
|
|
35
|
+
}
|
|
36
|
+
const evaluationRevocation = revokedEvaluationForCandidate(state, candidate);
|
|
37
|
+
if (evaluationRevocation) {
|
|
38
|
+
const result = rollbackCandidate(state, candidate, "learning evaluation contract was explicitly revoked",
|
|
39
|
+
timestamp, "automatic-evaluation-revocation");
|
|
40
|
+
return { ...result, decision: "rolled-back", evaluationRevocation };
|
|
41
|
+
}
|
|
42
|
+
const evidenceSourceAttestationRevocation = revokedEvidenceSourceAttestationForCandidate(state, candidate);
|
|
43
|
+
if (evidenceSourceAttestationRevocation) {
|
|
44
|
+
const result = rollbackCandidate(state, candidate, "learning evidence source attestation was explicitly revoked",
|
|
45
|
+
timestamp, "automatic-evidence-source-attestation-revocation");
|
|
46
|
+
return { ...result, decision: "rolled-back", evidenceSourceAttestationRevocation };
|
|
47
|
+
}
|
|
48
|
+
const validationRevocation = revokedValidationForCandidate(state, candidate);
|
|
49
|
+
if (validationRevocation) {
|
|
50
|
+
const result = rollbackCandidate(state, candidate, "learning validation decision was explicitly revoked",
|
|
51
|
+
timestamp, "automatic-validation-revocation");
|
|
52
|
+
return { ...result, decision: "rolled-back", validationRevocation };
|
|
53
|
+
}
|
|
54
|
+
const measurementRevocation = revokedMeasurementForCandidate(state, candidate);
|
|
55
|
+
if (measurementRevocation) {
|
|
56
|
+
const result = rollbackCandidate(state, candidate, "learning measurement evidence was explicitly revoked",
|
|
57
|
+
timestamp, "automatic-measurement-revocation");
|
|
58
|
+
return { ...result, decision: "rolled-back", measurementRevocation };
|
|
59
|
+
}
|
|
60
|
+
const applicationRevocation = revokedApplicationForCandidate(state, candidate);
|
|
61
|
+
if (applicationRevocation) {
|
|
62
|
+
const result = rollbackCandidate(state, candidate, "learning application evidence was explicitly revoked",
|
|
63
|
+
timestamp, "automatic-application-revocation");
|
|
64
|
+
return { ...result, decision: "rolled-back", applicationRevocation };
|
|
65
|
+
}
|
|
66
|
+
const outcomeRevocation = revokedOutcomeForCandidate(state, candidate);
|
|
67
|
+
if (outcomeRevocation) {
|
|
68
|
+
const result = rollbackCandidate(state, candidate, "learning outcome evidence was explicitly revoked",
|
|
69
|
+
timestamp, "automatic-outcome-revocation");
|
|
70
|
+
return { ...result, decision: "rolled-back", outcomeRevocation };
|
|
71
|
+
}
|
|
72
|
+
if (new Date(canary.expiresAt).getTime() <= new Date(timestamp).getTime()) {
|
|
73
|
+
const result = rollbackCandidate(state, candidate, canary.status === "validated"
|
|
74
|
+
? "validated learning evidence lease expired" : "outcome canary expired before validation", timestamp,
|
|
75
|
+
canary.status === "validated" ? "automatic-validation-stale" : "automatic-stale");
|
|
76
|
+
return { ...result, decision: "rolled-back" };
|
|
77
|
+
}
|
|
78
|
+
const evaluation = canary.evaluationId
|
|
79
|
+
? state.evaluations.find((contract) => contract.id === canary.evaluationId && contract.learningId === candidate.id)
|
|
80
|
+
: null;
|
|
81
|
+
if (canary.evaluationId && (!evaluation || evaluation.digest !== canary.evaluationDigest
|
|
82
|
+
|| new Date(evaluation.expiresAt).getTime() <= new Date(timestamp).getTime())) {
|
|
83
|
+
const result = rollbackCandidate(state, candidate, "outcome evaluation contract is missing, changed, or stale", timestamp, "automatic-stale");
|
|
84
|
+
return { ...result, decision: "rolled-back" };
|
|
85
|
+
}
|
|
86
|
+
if (REGISTRY_BOUND_EVALUATIONS.has(evaluation?.schema)
|
|
87
|
+
&& !activeEvaluationBinding(state, evaluation)) {
|
|
88
|
+
const result = rollbackCandidate(state, candidate, "outcome evaluator registry binding was revoked or changed", timestamp, "automatic-evaluator-revocation");
|
|
89
|
+
return { ...result, decision: "rolled-back" };
|
|
90
|
+
}
|
|
91
|
+
const failedTrial = initialTrialTimeout(state, candidate, timestamp);
|
|
92
|
+
if (failedTrial) {
|
|
93
|
+
const failureReceipt = recordInitialTrialFailure(state, failedTrial, timestamp);
|
|
94
|
+
const result = rollbackCandidate(state, candidate,
|
|
95
|
+
failedTrial.failure === "delivery-timeout"
|
|
96
|
+
? "initial Canary trial missed its immutable model-turn delivery deadline"
|
|
97
|
+
: "initial Canary trial missed its immutable measured-outcome deadline",
|
|
98
|
+
timestamp, "automatic-incomplete-trial", failureReceipt);
|
|
99
|
+
const retryExhaustionReceipt = recordTrialRetryExhaustion(state, failureReceipt);
|
|
100
|
+
return { ...result, decision: "rolled-back", failureReceipt, retryExhaustionReceipt };
|
|
101
|
+
}
|
|
102
|
+
if (canary.status === "validated") {
|
|
103
|
+
const validation = validationLeaseState(state, candidate, timestamp);
|
|
104
|
+
if (REGISTRY_BOUND_EVALUATIONS.has(evaluation?.schema)
|
|
105
|
+
&& validation.status !== "active") {
|
|
106
|
+
const result = rollbackCandidate(state, candidate,
|
|
107
|
+
validation.status === "missing" ? "validated learning is missing its immutable evidence lease"
|
|
108
|
+
: "validated learning evidence lease is no longer current",
|
|
109
|
+
timestamp, validation.status === "missing" ? "automatic-validation-unproven" : "automatic-validation-stale");
|
|
110
|
+
return { ...result, decision: "rolled-back" };
|
|
111
|
+
}
|
|
112
|
+
return { candidate, decision: "unchanged", restored: [] };
|
|
113
|
+
}
|
|
114
|
+
const planned = Boolean(evaluation);
|
|
115
|
+
const receipts = state.outcomes.filter((item) => (planned
|
|
116
|
+
? outcomeMatchesContract(item, evaluation)
|
|
117
|
+
: item.schema === "agentspine.learning-outcome/v2")
|
|
118
|
+
&& item.learningId === candidate.id && item.phase === "after"
|
|
119
|
+
&& (!planned || item.evaluationId === evaluation.id)
|
|
120
|
+
&& exactScope(item.scope, canary.scope) && item.metric.name === canary.metric.name
|
|
121
|
+
&& item.metric.direction === canary.metric.direction && outcomeFresh(item, evaluation, state.config, timestamp)
|
|
122
|
+
&& outcomeMatchesInitialTrial(state, item, evaluation)
|
|
123
|
+
&& state.applications.some((application) => application.id === item.applicationId
|
|
124
|
+
&& application.learningId === candidate.id && exactScope(application.scope, canary.scope)));
|
|
125
|
+
if (receipts.some((item) => item.metric.blockingDefects > 0)) {
|
|
126
|
+
const result = rollbackCandidate(state, candidate, "outcome canary recorded a blocking defect", timestamp, "automatic-regression");
|
|
127
|
+
return { ...result, decision: "rolled-back" };
|
|
128
|
+
}
|
|
129
|
+
const paired = PAIRED_EVALUATIONS.has(evaluation?.schema);
|
|
130
|
+
const rootBound = ROOT_BOUND_EVALUATIONS.has(evaluation?.schema);
|
|
131
|
+
const beforeByEvaluator = paired ? new Map((canary.beforeReceipts || [])
|
|
132
|
+
.map((id) => state.outcomes.find((item) => item.id === id))
|
|
133
|
+
.filter((item) => item?.schema === (rootBound ? "agentspine.learning-outcome/v9" : "agentspine.learning-outcome/v8"))
|
|
134
|
+
.map((item) => [rootBound ? item.measurement.evaluatorRootDigest : item.measurement.evaluatorId, item])) : new Map();
|
|
135
|
+
const eligible = receipts.filter((item) => item.measurement.kind !== "model-suggestion"
|
|
136
|
+
&& (!paired || beforeByEvaluator.has(rootBound ? item.measurement.evaluatorRootDigest : item.measurement.evaluatorId)));
|
|
137
|
+
if (INITIAL_TRIAL_EVALUATIONS.has(evaluation?.schema)) eligible.sort((a, b) =>
|
|
138
|
+
evaluation.initialTrials.after.findIndex((trial) => trial.evaluatorId === a.measurement.evaluatorId)
|
|
139
|
+
- evaluation.initialTrials.after.findIndex((trial) => trial.evaluatorId === b.measurement.evaluatorId));
|
|
140
|
+
const independentEvaluators = new Set(eligible.map((item) => rootBound
|
|
141
|
+
? item.measurement.evaluatorRootDigest : item.measurement.evaluatorId)).size;
|
|
142
|
+
const independentApplications = new Set(eligible.map((item) => item.applicationId)).size;
|
|
143
|
+
const deltas = eligible.map((item) => improvement(canary.metric.direction,
|
|
144
|
+
paired ? beforeByEvaluator.get(rootBound ? item.measurement.evaluatorRootDigest : item.measurement.evaluatorId).metric.value : canary.baseline,
|
|
145
|
+
item.metric.value));
|
|
146
|
+
const thresholds = planned ? evaluation.thresholds : {
|
|
147
|
+
regressionTolerance: state.config.regressionTolerance,
|
|
148
|
+
afterReceipts: state.config.canaryReceipts,
|
|
149
|
+
minImprovement: state.config.minImprovement
|
|
150
|
+
};
|
|
151
|
+
if (deltas.some((value) => value < -thresholds.regressionTolerance)) {
|
|
152
|
+
const result = rollbackCandidate(state, candidate, "outcome canary regressed against its baseline", timestamp, "automatic-regression");
|
|
153
|
+
return { ...result, decision: "rolled-back" };
|
|
154
|
+
}
|
|
155
|
+
if (independentEvaluators < thresholds.afterReceipts || independentApplications < thresholds.afterReceipts
|
|
156
|
+
|| !eligible.some((item) => item.measurement.kind === "objective")) {
|
|
157
|
+
return { candidate, decision: "active", restored: [] };
|
|
158
|
+
}
|
|
159
|
+
const average = deltas.reduce((sum, value) => sum + value, 0) / deltas.length;
|
|
160
|
+
if (average < thresholds.minImprovement) {
|
|
161
|
+
const result = rollbackCandidate(state, candidate, "outcome canary did not meet the minimum measured improvement", timestamp, "automatic-no-improvement");
|
|
162
|
+
return { ...result, decision: "rolled-back" };
|
|
163
|
+
}
|
|
164
|
+
preserve(state, "learning-candidate", candidate, timestamp);
|
|
165
|
+
let validationLease = null;
|
|
166
|
+
if (REGISTRY_BOUND_EVALUATIONS.has(evaluation?.schema)) {
|
|
167
|
+
const binding = activeEvaluationBinding(state, evaluation);
|
|
168
|
+
if (!binding) {
|
|
169
|
+
const result = rollbackCandidate(state, candidate, "outcome evaluator registry binding was revoked or changed",
|
|
170
|
+
timestamp, "automatic-evaluator-revocation");
|
|
171
|
+
return { ...result, decision: "rolled-back" };
|
|
172
|
+
}
|
|
173
|
+
const beforeOutcomes = (canary.beforeReceipts || []).map((id) => state.outcomes.find((item) => item.id === id));
|
|
174
|
+
if (beforeOutcomes.some((item) => !item)) {
|
|
175
|
+
const result = rollbackCandidate(state, candidate, "validated learning baseline evidence is missing",
|
|
176
|
+
timestamp, "automatic-validation-unproven");
|
|
177
|
+
return { ...result, decision: "rolled-back" };
|
|
178
|
+
}
|
|
179
|
+
const payload = validationLeasePayload({
|
|
180
|
+
id: `validation:${randomUUID()}`,
|
|
181
|
+
learningId: candidate.id,
|
|
182
|
+
evaluationId: evaluation.id,
|
|
183
|
+
evaluationDigest: evaluation.digest,
|
|
184
|
+
evaluatorRegistryBindingDigest: binding.digest,
|
|
185
|
+
scope: canary.scope,
|
|
186
|
+
metric: canary.metric,
|
|
187
|
+
beforeOutcomes: validationOutcomeReferences(beforeOutcomes),
|
|
188
|
+
afterOutcomes: validationOutcomeReferences(eligible),
|
|
189
|
+
improvement: average,
|
|
190
|
+
validatedAt: timestamp,
|
|
191
|
+
expiresAt: canary.expiresAt
|
|
192
|
+
});
|
|
193
|
+
validationLease = { ...payload, digest: digest(payload) };
|
|
194
|
+
state.validationLeases.push(validationLease);
|
|
195
|
+
state.validationLeases.sort((a, b) => a.id.localeCompare(b.id));
|
|
196
|
+
}
|
|
197
|
+
const validated = {
|
|
198
|
+
...candidate,
|
|
199
|
+
promotion: {
|
|
200
|
+
...candidate.promotion,
|
|
201
|
+
canary: {
|
|
202
|
+
...canary,
|
|
203
|
+
status: "validated",
|
|
204
|
+
validatedAt: timestamp,
|
|
205
|
+
afterReceipts: eligible.map((item) => item.id),
|
|
206
|
+
improvement: average,
|
|
207
|
+
...(validationLease ? {
|
|
208
|
+
validationLeaseId: validationLease.id,
|
|
209
|
+
validationLeaseDigest: validationLease.digest
|
|
210
|
+
} : {})
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
updatedAt: timestamp,
|
|
214
|
+
authority: "context-only"
|
|
215
|
+
};
|
|
216
|
+
state.candidates = state.candidates.map((entry) => entry.id === candidate.id ? validated : entry);
|
|
217
|
+
return { candidate: validated, decision: "validated", restored: [] };
|
|
218
|
+
}
|