blun-king-cli 9.1.567 → 9.1.569
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/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 +2 -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 +2517 -1048
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +3 -17
- 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,211 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { withOwnedFileLock } from "./owned-file-lock.js";
|
|
6
|
+
import { replaceFileWithRetry } from "./filesystem-retry.js";
|
|
7
|
+
|
|
8
|
+
const MARKER_SCHEMA = "agentspine.codex-skill-installation/v1";
|
|
9
|
+
const AUTHORITY = "host-registration-only";
|
|
10
|
+
const SKILL_NAME = "agent-spine";
|
|
11
|
+
const MAX_SKILL_BYTES = 256 * 1024;
|
|
12
|
+
|
|
13
|
+
function sha256(value) {
|
|
14
|
+
return createHash("sha256").update(value).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonical(value) {
|
|
18
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
19
|
+
if (value && typeof value === "object") {
|
|
20
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function seal(value) {
|
|
26
|
+
return { ...value, digest: sha256(canonical(value)) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function verifySeal(value) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.digest !== "string") return false;
|
|
31
|
+
const { digest, ...payload } = value;
|
|
32
|
+
return digest === sha256(canonical(payload));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function exactDirectory(path, label, { create = false } = {}) {
|
|
36
|
+
if (!isAbsolute(path)) throw new Error(`${label} must be absolute`);
|
|
37
|
+
const target = resolve(path);
|
|
38
|
+
if (create) await mkdir(target, { recursive: true, mode: 0o700 });
|
|
39
|
+
const metadata = await lstat(target);
|
|
40
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
41
|
+
throw new Error(`${label} must be a regular non-symlink directory`);
|
|
42
|
+
}
|
|
43
|
+
return realpath(target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function regularOrMissing(path, label) {
|
|
47
|
+
try {
|
|
48
|
+
const metadata = await lstat(path);
|
|
49
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
50
|
+
throw new Error(`${label} must be a regular non-symlink file`);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code === "ENOENT") return false;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function atomicWrite(path, content, assertOwned) {
|
|
60
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
61
|
+
let handle;
|
|
62
|
+
try {
|
|
63
|
+
handle = await open(temporary, "wx", 0o600);
|
|
64
|
+
await handle.writeFile(content);
|
|
65
|
+
await handle.sync();
|
|
66
|
+
await handle.close();
|
|
67
|
+
handle = null;
|
|
68
|
+
await replaceFileWithRetry(temporary, path, { beforeAttempt: assertOwned });
|
|
69
|
+
} finally {
|
|
70
|
+
await handle?.close();
|
|
71
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function sourceSkill(packageRoot) {
|
|
76
|
+
const root = await exactDirectory(packageRoot, "AgentSpine package root");
|
|
77
|
+
const packagePath = join(root, "package.json");
|
|
78
|
+
const skillPath = join(root, "skills", SKILL_NAME, "SKILL.md");
|
|
79
|
+
if (!await regularOrMissing(packagePath, "AgentSpine package manifest")) {
|
|
80
|
+
throw new Error("AgentSpine package manifest is missing");
|
|
81
|
+
}
|
|
82
|
+
if (!await regularOrMissing(skillPath, "AgentSpine skill source")) {
|
|
83
|
+
throw new Error("AgentSpine skill source is missing");
|
|
84
|
+
}
|
|
85
|
+
const [packageText, content] = await Promise.all([
|
|
86
|
+
readFile(packagePath, "utf8"), readFile(skillPath)
|
|
87
|
+
]);
|
|
88
|
+
if (content.length === 0 || content.length > MAX_SKILL_BYTES) {
|
|
89
|
+
throw new Error("AgentSpine skill source size is outside the install limit");
|
|
90
|
+
}
|
|
91
|
+
const version = JSON.parse(packageText).version;
|
|
92
|
+
if (typeof version !== "string" || !version) throw new Error("AgentSpine package version is missing");
|
|
93
|
+
if (!content.toString("utf8").startsWith("---\nname: agent-spine\n")) {
|
|
94
|
+
throw new Error("AgentSpine skill source has invalid frontmatter");
|
|
95
|
+
}
|
|
96
|
+
return { packageRoot: root, sourcePath: skillPath, version, content, digest: sha256(content) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readMarker(path) {
|
|
100
|
+
if (!await regularOrMissing(path, "Codex AgentSpine skill marker")) return null;
|
|
101
|
+
let value;
|
|
102
|
+
try { value = JSON.parse(await readFile(path, "utf8")); }
|
|
103
|
+
catch (error) { throw new Error("Codex AgentSpine skill marker is malformed", { cause: error }); }
|
|
104
|
+
if (!verifySeal(value) || value.schema !== MARKER_SCHEMA || !["updating", "installed"].includes(value.state)) {
|
|
105
|
+
throw new Error("Codex AgentSpine skill marker is invalid");
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function digestOrNull(path) {
|
|
111
|
+
return await regularOrMissing(path, "Codex AgentSpine SKILL.md") ? sha256(await readFile(path)) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function recoverMarker(marker, markerPath, skillPath, assertOwned) {
|
|
115
|
+
if (!marker) return null;
|
|
116
|
+
const actual = await digestOrNull(skillPath);
|
|
117
|
+
if (marker.state === "installed") {
|
|
118
|
+
if (actual !== marker.skillDigest) throw new Error("installed Codex AgentSpine skill was changed outside its managed update");
|
|
119
|
+
return marker;
|
|
120
|
+
}
|
|
121
|
+
if (actual === marker.targetDigest) {
|
|
122
|
+
const recovered = seal({
|
|
123
|
+
schema: MARKER_SCHEMA, state: "installed", skillName: SKILL_NAME,
|
|
124
|
+
skillDigest: marker.targetDigest, version: marker.targetVersion,
|
|
125
|
+
packageRoot: marker.targetPackageRoot, authority: AUTHORITY
|
|
126
|
+
});
|
|
127
|
+
await atomicWrite(markerPath, `${JSON.stringify(recovered, null, 2)}\n`, assertOwned);
|
|
128
|
+
return recovered;
|
|
129
|
+
}
|
|
130
|
+
if (actual === marker.previousDigest) {
|
|
131
|
+
if (actual === null) {
|
|
132
|
+
await assertOwned();
|
|
133
|
+
await rm(markerPath, { force: true });
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const recovered = seal({
|
|
137
|
+
schema: MARKER_SCHEMA, state: "installed", skillName: SKILL_NAME,
|
|
138
|
+
skillDigest: marker.previousDigest, version: marker.previousVersion,
|
|
139
|
+
packageRoot: marker.previousPackageRoot, authority: AUTHORITY
|
|
140
|
+
});
|
|
141
|
+
await atomicWrite(markerPath, `${JSON.stringify(recovered, null, 2)}\n`, assertOwned);
|
|
142
|
+
return recovered;
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Codex AgentSpine skill update has an unrecognized partial state");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function defaultCodexSkillsRoot(env = process.env) {
|
|
148
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
149
|
+
return resolve(env.AGENTSPINE_CODEX_SKILLS_ROOT || join(home, ".agents", "skills"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function installCodexSkill({
|
|
153
|
+
skillsRoot = defaultCodexSkillsRoot(), packageRoot,
|
|
154
|
+
faultAfter = null
|
|
155
|
+
} = {}) {
|
|
156
|
+
const source = await sourceSkill(packageRoot);
|
|
157
|
+
const root = await exactDirectory(skillsRoot, "Codex user skills root", { create: true });
|
|
158
|
+
const skillDirectoryPath = join(root, SKILL_NAME);
|
|
159
|
+
const lockPath = join(root, ".agent-spine-install.lock");
|
|
160
|
+
if (await regularOrMissing(lockPath, "Codex AgentSpine skill install lock")) {
|
|
161
|
+
// withOwnedFileLock owns regular stale/current lock handling.
|
|
162
|
+
}
|
|
163
|
+
return withOwnedFileLock(lockPath, async ({ assertOwned, recovered: lockRecovered }) => {
|
|
164
|
+
let created = false;
|
|
165
|
+
try { await exactDirectory(skillDirectoryPath, "Codex AgentSpine skill directory"); }
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error.code !== "ENOENT") throw error;
|
|
168
|
+
await mkdir(skillDirectoryPath, { mode: 0o700 });
|
|
169
|
+
created = true;
|
|
170
|
+
}
|
|
171
|
+
const skillDirectory = await realpath(skillDirectoryPath);
|
|
172
|
+
const markerPath = join(skillDirectory, ".agentspine-install.json");
|
|
173
|
+
const skillPath = join(skillDirectory, "SKILL.md");
|
|
174
|
+
let marker = await readMarker(markerPath);
|
|
175
|
+
if (!marker && !created) {
|
|
176
|
+
throw new Error("Codex already has an unmanaged agent-spine skill directory");
|
|
177
|
+
}
|
|
178
|
+
marker = await recoverMarker(marker, markerPath, skillPath, assertOwned);
|
|
179
|
+
if (marker?.skillDigest === source.digest && marker.version === source.version
|
|
180
|
+
&& marker.packageRoot === source.packageRoot) {
|
|
181
|
+
return {
|
|
182
|
+
schema: "agentspine.codex-skill-install-result/v1", status: "unchanged",
|
|
183
|
+
version: source.version, packageRoot: source.packageRoot, skillPath, skillDigest: source.digest,
|
|
184
|
+
markerPath, markerDigest: marker.digest, recovered: lockRecovered, authority: AUTHORITY
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const updating = seal({
|
|
188
|
+
schema: MARKER_SCHEMA, state: "updating", skillName: SKILL_NAME,
|
|
189
|
+
previousDigest: marker?.skillDigest ?? null, previousVersion: marker?.version ?? null,
|
|
190
|
+
previousPackageRoot: marker?.packageRoot ?? null,
|
|
191
|
+
targetDigest: source.digest, targetVersion: source.version,
|
|
192
|
+
targetPackageRoot: source.packageRoot, authority: AUTHORITY
|
|
193
|
+
});
|
|
194
|
+
await atomicWrite(markerPath, `${JSON.stringify(updating, null, 2)}\n`, assertOwned);
|
|
195
|
+
if (faultAfter === "marker") throw new Error("synthetic crash after Codex skill update marker");
|
|
196
|
+
await atomicWrite(skillPath, source.content, assertOwned);
|
|
197
|
+
if (faultAfter === "skill") throw new Error("synthetic crash after Codex skill publication");
|
|
198
|
+
const installed = seal({
|
|
199
|
+
schema: MARKER_SCHEMA, state: "installed", skillName: SKILL_NAME,
|
|
200
|
+
skillDigest: source.digest, version: source.version,
|
|
201
|
+
packageRoot: source.packageRoot, authority: AUTHORITY
|
|
202
|
+
});
|
|
203
|
+
await atomicWrite(markerPath, `${JSON.stringify(installed, null, 2)}\n`, assertOwned);
|
|
204
|
+
return {
|
|
205
|
+
schema: "agentspine.codex-skill-install-result/v1",
|
|
206
|
+
status: marker ? "updated" : "installed", version: source.version, packageRoot: source.packageRoot,
|
|
207
|
+
skillPath, skillDigest: source.digest, markerPath, markerDigest: installed.digest,
|
|
208
|
+
recovered: lockRecovered, authority: AUTHORITY
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ancestorsBetween, canonicalPath, isInside } from "./paths.js";
|
|
|
4
4
|
import { buildCatalog } from "./catalog.js";
|
|
5
5
|
import { attachDocumentSnapshot, documentSnapshotContent } from "./documents.js";
|
|
6
6
|
import { loadGraph } from "./graph.js";
|
|
7
|
+
import { readCatalogDocument } from "./catalog-document-read.js";
|
|
7
8
|
|
|
8
9
|
function depth(root, path) {
|
|
9
10
|
const value = relative(root, dirname(path));
|
|
@@ -131,8 +132,9 @@ export async function resolveContext({ root = process.cwd(), cwd = root, host =
|
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
export async function readDocument({ root = process.cwd(), path, offset = 0, length = 65536 }) {
|
|
135
|
+
export async function readDocument({ root = process.cwd(), path, offset = 0, length = 65536, catalog: providedCatalog = null }) {
|
|
135
136
|
if (!path) throw new Error("path is required");
|
|
137
|
+
if (providedCatalog) return readCatalogDocument({ root, path, offset, length, catalog: providedCatalog });
|
|
136
138
|
const catalog = await buildCatalog(root);
|
|
137
139
|
const absolute = resolve(catalog.root, path);
|
|
138
140
|
const document = catalog.documents.find((item) => item.path === absolute);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { mkdir, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { withOwnedFileLock } from "./owned-file-lock.js";
|
|
4
|
+
import { canonicalPath, projectStateDir } from "./paths.js";
|
|
5
|
+
import { writePremortemFile } from "./delivery-premortem-file.js";
|
|
6
|
+
import { inspectPremortemState } from "./delivery-premortem.js";
|
|
7
|
+
import { parsePremortemRequirementId } from "./delivery-premortem-binding.js";
|
|
8
|
+
import { premortemBlock as block, premortemBoundary as boundary } from "./delivery-premortem-results.js";
|
|
9
|
+
import { premortemSha256 as sha256, premortemTime as at,
|
|
10
|
+
sealPremortem as seal, validPremortemSeal as validSeal,
|
|
11
|
+
validPremortemTime as validTime } from "./delivery-premortem-codec.js";
|
|
12
|
+
|
|
13
|
+
const STATE_SCHEMA = "agentspine.delivery-agent-usage/v1";
|
|
14
|
+
const RECEIPT_SCHEMA = "agentspine.delivery-agent-use-receipt/v1";
|
|
15
|
+
const AUTHORITY = "context-only";
|
|
16
|
+
const MAX_BYTES = 32 * 1024;
|
|
17
|
+
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
18
|
+
|
|
19
|
+
export const DELIVERY_AGENT_USE_TEXT = [
|
|
20
|
+
"Reuse a host-verified briefing for this requirement; no second content fetch is needed.",
|
|
21
|
+
"Use delivery_knowledge_query and record_delivery_premortem for relevant preparation.",
|
|
22
|
+
"Without a verified briefing, report uncertainty; session_briefing is optional. No automatic retry or work restriction.",
|
|
23
|
+
"Only matching host or MCP evidence counts; claims, foreign or reused receipts do not. Context-only; no authority."
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
function stateMaterial(state) {
|
|
27
|
+
const material = { ...state };
|
|
28
|
+
delete material.integrityDigest;
|
|
29
|
+
return material;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sealState(state) {
|
|
33
|
+
return { ...state, integrityDigest: sha256(stateMaterial(state)) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validReceipt(value, stage, requirementId) {
|
|
37
|
+
return value && value.schema === RECEIPT_SCHEMA && value.stage === stage
|
|
38
|
+
&& value.requirementId === requirementId && value.authority === AUTHORITY
|
|
39
|
+
&& (value.origin === undefined || value.origin === "verified-host-preflight")
|
|
40
|
+
&& DIGEST_RE.test(value.inputDigest || "") && DIGEST_RE.test(value.resultDigest || "")
|
|
41
|
+
&& validTime(value.recordedAt) && validSeal(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateState(state, parsed) {
|
|
45
|
+
if (!state || state.schema !== STATE_SCHEMA || state.authority !== AUTHORITY
|
|
46
|
+
|| state.requirementId !== `premortem-requirement:${parsed.laneDigest}:${parsed.generationDigest}`
|
|
47
|
+
|| state.laneDigest !== parsed.laneDigest || state.generationDigest !== parsed.generationDigest
|
|
48
|
+
|| !Number.isInteger(state.revision) || state.revision < 0 || state.revision > 3
|
|
49
|
+
|| (state.briefing !== null && !validReceipt(state.briefing, "briefing", state.requirementId))
|
|
50
|
+
|| (state.knowledge !== null && !validReceipt(state.knowledge, "knowledge", state.requirementId))
|
|
51
|
+
|| Boolean(state.knowledge) > Boolean(state.briefing)
|
|
52
|
+
|| (state.consumedAt !== null && !validTime(state.consumedAt))
|
|
53
|
+
|| (state.consumedDigest !== null && !DIGEST_RE.test(state.consumedDigest))
|
|
54
|
+
|| Boolean(state.consumedAt) !== Boolean(state.consumedDigest)
|
|
55
|
+
|| !DIGEST_RE.test(state.integrityDigest || "")
|
|
56
|
+
|| state.integrityDigest !== sha256(stateMaterial(state))) {
|
|
57
|
+
const error = new Error("delivery AgentSpine usage state failed integrity validation");
|
|
58
|
+
error.code = "AGENTSPINE_USAGE_UNCERTAIN";
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function pathsFor(root, requirementId) {
|
|
65
|
+
const parsed = parsePremortemRequirementId(requirementId);
|
|
66
|
+
const directory = join(await projectStateDir(await canonicalPath(root)), "delivery-agent-usage");
|
|
67
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
68
|
+
const path = join(directory, `${parsed.generationDigest}.json`);
|
|
69
|
+
return { parsed, path, lockPath: `${path}.lock` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readState(path, parsed) {
|
|
73
|
+
try {
|
|
74
|
+
const text = await readFile(path, "utf8");
|
|
75
|
+
if (Buffer.byteLength(text) > MAX_BYTES) throw new Error("delivery AgentSpine usage state exceeds 32 KiB");
|
|
76
|
+
return validateState(JSON.parse(text), parsed);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") return null;
|
|
79
|
+
if (error instanceof SyntaxError) {
|
|
80
|
+
const invalid = new Error("delivery AgentSpine usage state is not valid JSON");
|
|
81
|
+
invalid.code = "AGENTSPINE_USAGE_UNCERTAIN";
|
|
82
|
+
throw invalid;
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function emptyState(requirementId, parsed) {
|
|
89
|
+
return sealState({ schema: STATE_SCHEMA, requirementId,
|
|
90
|
+
laneDigest: parsed.laneDigest, generationDigest: parsed.generationDigest,
|
|
91
|
+
revision: 0, briefing: null, knowledge: null,
|
|
92
|
+
consumedAt: null, consumedDigest: null, authority: AUTHORITY });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function currentRequirement(root, requirementId) {
|
|
96
|
+
const current = await inspectPremortemState({ root, requirementId });
|
|
97
|
+
if (current.status === "degraded") return current;
|
|
98
|
+
if (current.blocked) return current;
|
|
99
|
+
if (current.status === "absent" || !current.requirement) {
|
|
100
|
+
return block("stale", `AgentSpine cannot bind usage to current requirement ${requirementId}.`,
|
|
101
|
+
{ requirementId });
|
|
102
|
+
}
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function receipt(stage, requirementId, input, result, now, verifiedHostBriefing) {
|
|
107
|
+
return seal({ schema: RECEIPT_SCHEMA, stage, requirementId,
|
|
108
|
+
...(verifiedHostBriefing ? { origin: "verified-host-preflight" } : {}),
|
|
109
|
+
inputDigest: sha256(input), resultDigest: sha256(result),
|
|
110
|
+
recordedAt: at(now), authority: AUTHORITY });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function recordStage({ root, requirementId, stage, input, result,
|
|
114
|
+
verifiedHostBriefing = false, preserveConsumed = false, now = new Date() }) {
|
|
115
|
+
try {
|
|
116
|
+
const current = await currentRequirement(root, requirementId);
|
|
117
|
+
if (current.status === "degraded" || current.blocked) return current;
|
|
118
|
+
const paths = await pathsFor(root, requirementId);
|
|
119
|
+
return await withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
|
|
120
|
+
let state = await readState(paths.path, paths.parsed)
|
|
121
|
+
|| emptyState(requirementId, paths.parsed);
|
|
122
|
+
if (!preserveConsumed && state.briefing?.origin !== "verified-host-preflight"
|
|
123
|
+
&& state.consumedAt && stage === "briefing" && current.closed && !current.consumed) {
|
|
124
|
+
state = emptyState(requirementId, paths.parsed);
|
|
125
|
+
} else if (state.consumedAt) return block("reused",
|
|
126
|
+
`AgentSpine usage receipts for ${requirementId} were already consumed.`, { requirementId });
|
|
127
|
+
if (stage === "knowledge" && !state.briefing) return block("missing-briefing",
|
|
128
|
+
"AgentSpine delivery preflight is missing stage 1: session_briefing.", { requirementId });
|
|
129
|
+
if (stage === "briefing" && !verifiedHostBriefing && state.briefing?.origin === "verified-host-preflight") {
|
|
130
|
+
return { status: "satisfied-by-host", blocked: false, requirementId,
|
|
131
|
+
receipt: structuredClone(state.briefing), digest: state.briefing.digest };
|
|
132
|
+
}
|
|
133
|
+
const proposed = receipt(stage, requirementId, input, result, now, verifiedHostBriefing);
|
|
134
|
+
if (state[stage]) {
|
|
135
|
+
if (state[stage].inputDigest !== proposed.inputDigest
|
|
136
|
+
|| state[stage].resultDigest !== proposed.resultDigest) {
|
|
137
|
+
return block(`conflicting-${stage}`,
|
|
138
|
+
`AgentSpine delivery preflight has a conflicting ${stage} call.`, { requirementId });
|
|
139
|
+
}
|
|
140
|
+
return { status: "duplicate", blocked: false, requirementId,
|
|
141
|
+
receipt: structuredClone(state[stage]), digest: state[stage].digest };
|
|
142
|
+
}
|
|
143
|
+
state[stage] = proposed;
|
|
144
|
+
state.revision += 1;
|
|
145
|
+
state = sealState(state);
|
|
146
|
+
await writePremortemFile(paths.path, state, assertOwned, MAX_BYTES);
|
|
147
|
+
return { status: "recorded", blocked: false, requirementId,
|
|
148
|
+
receipt: structuredClone(proposed), digest: proposed.digest };
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return boundary(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function deliveryAgentUseGuidance(root, requirementId) {
|
|
156
|
+
return { root, requirementId, calls: ["session_briefing", "delivery_knowledge_query",
|
|
157
|
+
"record_delivery_premortem"], instruction: DELIVERY_AGENT_USE_TEXT };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function recordDeliveryBriefingUse(input) {
|
|
161
|
+
return recordStage({ ...input, stage: "briefing" });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function recordDeliveryKnowledgeUse(input) {
|
|
165
|
+
return recordStage({ ...input, stage: "knowledge" });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function verifyDeliveryAgentUse({ root, requirementId }) {
|
|
169
|
+
try {
|
|
170
|
+
const current = await currentRequirement(root, requirementId);
|
|
171
|
+
if (current.status === "degraded" || current.blocked) return current;
|
|
172
|
+
const paths = await pathsFor(root, requirementId);
|
|
173
|
+
const state = await readState(paths.path, paths.parsed);
|
|
174
|
+
if (!state?.briefing) return block("missing-briefing",
|
|
175
|
+
"AgentSpine delivery preflight is missing stage 1: session_briefing.", { requirementId });
|
|
176
|
+
if (!state.knowledge) return block("missing-knowledge",
|
|
177
|
+
"AgentSpine delivery preflight is missing stage 2: delivery_knowledge_query.", { requirementId });
|
|
178
|
+
if (state.consumedAt) return block("reused",
|
|
179
|
+
`AgentSpine usage receipts for ${requirementId} were already consumed.`, { requirementId });
|
|
180
|
+
return { status: "verified", blocked: false, requirementId,
|
|
181
|
+
briefingReceipt: structuredClone(state.briefing),
|
|
182
|
+
knowledgeReceipt: structuredClone(state.knowledge),
|
|
183
|
+
digest: sha256({ briefing: state.briefing.digest, knowledge: state.knowledge.digest }) };
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return boundary(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function consumeDeliveryAgentUse({ root, requirementId, now = new Date() }) {
|
|
190
|
+
try {
|
|
191
|
+
const verified = await verifyDeliveryAgentUse({ root, requirementId });
|
|
192
|
+
if (verified.blocked || verified.status === "degraded") return verified;
|
|
193
|
+
const paths = await pathsFor(root, requirementId);
|
|
194
|
+
return withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
|
|
195
|
+
let state = await readState(paths.path, paths.parsed);
|
|
196
|
+
if (state.consumedAt) return { status: "duplicate", blocked: false,
|
|
197
|
+
requirementId, digest: state.consumedDigest };
|
|
198
|
+
state.consumedAt = at(now);
|
|
199
|
+
state.consumedDigest = sha256({ requirementId, usageDigest: verified.digest });
|
|
200
|
+
state.revision += 1;
|
|
201
|
+
state = sealState(state);
|
|
202
|
+
await writePremortemFile(paths.path, state, assertOwned, MAX_BYTES);
|
|
203
|
+
return { status: "consumed", blocked: false, requirementId,
|
|
204
|
+
digest: state.consumedDigest };
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return boundary(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function removeDeliveryAgentUse({ root, requirementId }) {
|
|
212
|
+
try {
|
|
213
|
+
const paths = await pathsFor(root, requirementId);
|
|
214
|
+
return withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
|
|
215
|
+
await assertOwned();
|
|
216
|
+
await unlink(paths.path).catch((error) => {
|
|
217
|
+
if (error.code !== "ENOENT") throw error;
|
|
218
|
+
});
|
|
219
|
+
return { status: "removed", blocked: false, requirementId };
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return boundary(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { withOwnedFileLock } from "./owned-file-lock.js";
|
|
4
|
+
import { canonicalPath, projectStateDir } from "./paths.js";
|
|
5
|
+
import { writePremortemFile } from "./delivery-premortem-file.js";
|
|
6
|
+
import { inspectPremortemState } from "./delivery-premortem.js";
|
|
7
|
+
import { canonicalPremortem as canonical, premortemSha256 as sha256,
|
|
8
|
+
premortemTime as at, sealPremortem as seal,
|
|
9
|
+
validPremortemSeal as validSeal, validPremortemTime as validTime } from "./delivery-premortem-codec.js";
|
|
10
|
+
|
|
11
|
+
const ASSIGNMENT_SCHEMA = "agentspine.delivery-assignment/v1";
|
|
12
|
+
const POINTER_SCHEMA = "agentspine.delivery-assignment-pointer/v1";
|
|
13
|
+
const AUTHORITY = "context-only";
|
|
14
|
+
const MAX_BYTES = 16 * 1024;
|
|
15
|
+
const ID_RE = /^assignment:[a-f0-9]{64}$/;
|
|
16
|
+
|
|
17
|
+
function sessionBinding(binding) {
|
|
18
|
+
return {
|
|
19
|
+
host: binding.host,
|
|
20
|
+
sessionId: binding.sessionId,
|
|
21
|
+
projectId: binding.projectId
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sessionDigest(binding) {
|
|
26
|
+
return sha256({ schema: "agentspine.delivery-assignment-session/v1",
|
|
27
|
+
binding: sessionBinding(binding) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function eventDigest(binding, eventId) {
|
|
31
|
+
return sha256({ schema: "agentspine.delivery-assignment-event/v1",
|
|
32
|
+
binding: sessionBinding(binding), eventId });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validAssignment(value, expectedSessionDigest = null) {
|
|
36
|
+
return value && value.schema === ASSIGNMENT_SCHEMA && value.authority === AUTHORITY
|
|
37
|
+
&& ID_RE.test(value.assignmentId || "")
|
|
38
|
+
&& /^[a-f0-9]{64}$/.test(value.sessionDigest || "")
|
|
39
|
+
&& /^[a-f0-9]{64}$/.test(value.eventDigest || "")
|
|
40
|
+
&& (value.predecessorAssignmentId === null || ID_RE.test(value.predecessorAssignmentId || ""))
|
|
41
|
+
&& value.binding && canonical(sessionBinding(value.binding)) === canonical(sessionBinding(value))
|
|
42
|
+
&& (!expectedSessionDigest || value.sessionDigest === expectedSessionDigest)
|
|
43
|
+
&& validTime(value.createdAt) && validSeal(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validPointer(value, digest) {
|
|
47
|
+
return value && value.schema === POINTER_SCHEMA && value.authority === AUTHORITY
|
|
48
|
+
&& value.sessionDigest === digest && validAssignment(value.assignment, digest)
|
|
49
|
+
&& value.assignmentId === value.assignment.assignmentId
|
|
50
|
+
&& Number.isInteger(value.revision) && value.revision > 0
|
|
51
|
+
&& validTime(value.updatedAt) && validSeal(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function pathsFor(root, binding) {
|
|
55
|
+
const digest = sessionDigest(binding);
|
|
56
|
+
const directory = join(await projectStateDir(await canonicalPath(root)),
|
|
57
|
+
"delivery-assignments", digest);
|
|
58
|
+
await mkdir(join(directory, "events"), { recursive: true, mode: 0o700 });
|
|
59
|
+
return { digest, directory, pointer: join(directory, "active.json"),
|
|
60
|
+
lock: join(directory, "active.lock") };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readJson(path, maxBytes = MAX_BYTES) {
|
|
64
|
+
try {
|
|
65
|
+
const text = await readFile(path, "utf8");
|
|
66
|
+
if (Buffer.byteLength(text) > maxBytes) throw new Error("delivery assignment state exceeds 16 KiB");
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code === "ENOENT") return null;
|
|
70
|
+
if (error instanceof SyntaxError) throw new Error("delivery assignment state is not valid JSON");
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readPointer(paths) {
|
|
76
|
+
const value = await readJson(paths.pointer);
|
|
77
|
+
if (value && !validPointer(value, paths.digest)) {
|
|
78
|
+
throw new Error("delivery assignment pointer failed integrity validation");
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assignmentFor(binding, eventId, digest, predecessor, now) {
|
|
84
|
+
const event = eventDigest(binding, eventId);
|
|
85
|
+
return seal({ schema: ASSIGNMENT_SCHEMA,
|
|
86
|
+
assignmentId: `assignment:${sha256({ sessionDigest: digest, eventDigest: event })}`,
|
|
87
|
+
sessionDigest: digest, eventDigest: event,
|
|
88
|
+
host: binding.host, sessionId: binding.sessionId, projectId: binding.projectId,
|
|
89
|
+
binding: { host: binding.host, sessionId: binding.sessionId,
|
|
90
|
+
projectId: binding.projectId, entityId: binding.entityId || null,
|
|
91
|
+
groupId: binding.groupId || null, taskId: binding.taskId || null,
|
|
92
|
+
goalId: binding.goalId || null, goalStepId: binding.goalStepId || null,
|
|
93
|
+
queueId: binding.queueId || null, gatewayAttempt: binding.gatewayAttempt ?? null,
|
|
94
|
+
planDefinitionsDigest: binding.planDefinitionsDigest || null },
|
|
95
|
+
predecessorAssignmentId: predecessor?.assignmentId || null,
|
|
96
|
+
createdAt: at(now), authority: AUTHORITY });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pointerFor(paths, assignment, previous, now) {
|
|
100
|
+
return seal({ schema: POINTER_SCHEMA, sessionDigest: paths.digest,
|
|
101
|
+
assignmentId: assignment.assignmentId, assignment,
|
|
102
|
+
revision: (previous?.revision || 0) + 1,
|
|
103
|
+
updatedAt: at(now), authority: AUTHORITY });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function publishEvent(path, assignment, assertOwned) {
|
|
107
|
+
const existing = await readJson(path);
|
|
108
|
+
if (existing) {
|
|
109
|
+
if (!validAssignment(existing, assignment.sessionDigest)
|
|
110
|
+
|| existing.assignmentId !== assignment.assignmentId
|
|
111
|
+
|| existing.eventDigest !== assignment.eventDigest) {
|
|
112
|
+
throw new Error("delivery assignment event conflicts with stored history");
|
|
113
|
+
}
|
|
114
|
+
return existing;
|
|
115
|
+
}
|
|
116
|
+
await writePremortemFile(path, assignment, assertOwned, MAX_BYTES);
|
|
117
|
+
return assignment;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function beginDeliveryAssignment({ root, binding, eventId, now = new Date() }) {
|
|
121
|
+
if (typeof eventId !== "string" || !eventId || eventId.length > 512 || /[\0\r\n]/.test(eventId)) {
|
|
122
|
+
throw new Error("delivery assignment requires an exact prompt eventId");
|
|
123
|
+
}
|
|
124
|
+
const paths = await pathsFor(root, binding);
|
|
125
|
+
return withOwnedFileLock(paths.lock, async ({ assertOwned }) => {
|
|
126
|
+
const previous = await readPointer(paths);
|
|
127
|
+
const proposed = assignmentFor(binding, eventId, paths.digest, previous?.assignment || null, now);
|
|
128
|
+
const path = join(paths.directory, "events", `${proposed.eventDigest}.json`);
|
|
129
|
+
const assignment = await publishEvent(path, proposed, assertOwned);
|
|
130
|
+
if (previous?.assignmentId === assignment.assignmentId) {
|
|
131
|
+
return { status: "duplicate", blocked: false, assignmentId: assignment.assignmentId,
|
|
132
|
+
assignment: structuredClone(assignment) };
|
|
133
|
+
}
|
|
134
|
+
if (assignment.predecessorAssignmentId !== (previous?.assignmentId || null)) {
|
|
135
|
+
return { status: "stale-assignment", blocked: true,
|
|
136
|
+
reason: "A replayed prompt cannot replace the current delivery assignment." };
|
|
137
|
+
}
|
|
138
|
+
await writePremortemFile(paths.pointer, pointerFor(paths, assignment, previous, now),
|
|
139
|
+
assertOwned, MAX_BYTES);
|
|
140
|
+
return { status: "started", blocked: false, assignmentId: assignment.assignmentId,
|
|
141
|
+
assignment: structuredClone(assignment) };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function resolveDeliveryAssignment({ root, binding, assignmentId = null }) {
|
|
146
|
+
const paths = await pathsFor(root, binding);
|
|
147
|
+
const pointer = await readPointer(paths);
|
|
148
|
+
if (!pointer) return { status: "legacy", blocked: false, assignmentId: null, assignment: null };
|
|
149
|
+
if (assignmentId && assignmentId !== pointer.assignmentId) {
|
|
150
|
+
return { status: "foreign-assignment", blocked: true,
|
|
151
|
+
reason: "The supplied delivery assignment is not active for this exact session and project." };
|
|
152
|
+
}
|
|
153
|
+
const stored = pointer.assignment.binding;
|
|
154
|
+
for (const key of ["entityId", "groupId", "taskId"]) {
|
|
155
|
+
if (stored[key] && binding[key] && stored[key] !== binding[key]) {
|
|
156
|
+
return { status: "foreign-assignment", blocked: true,
|
|
157
|
+
reason: `The active delivery assignment is bound to a different ${key}.` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { status: "resolved", blocked: false, assignmentId: pointer.assignmentId,
|
|
161
|
+
assignment: structuredClone(pointer.assignment) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function continueDeliveryAssignment({ root, binding, assignmentId }) {
|
|
165
|
+
if (!ID_RE.test(assignmentId || "")) {
|
|
166
|
+
return { status: "foreign-assignment", blocked: true,
|
|
167
|
+
reason: "Continuation requires the exact active assignmentId returned by this host session." };
|
|
168
|
+
}
|
|
169
|
+
const paths = await pathsFor(root, binding);
|
|
170
|
+
// Serialize the check with new assignments. Continuation never advances the pointer
|
|
171
|
+
// or rewrites history, including when a process exits during this inspection.
|
|
172
|
+
return withOwnedFileLock(paths.lock, async () => {
|
|
173
|
+
const current = await resolveDeliveryAssignment({ root, binding, assignmentId });
|
|
174
|
+
if (current.blocked) return current;
|
|
175
|
+
if (!current.assignment) return { status: "foreign-assignment", blocked: true,
|
|
176
|
+
reason: "This session has no active assignment matching the continuation." };
|
|
177
|
+
for (const key of ["entityId", "groupId", "taskId", "goalId", "goalStepId", "queueId",
|
|
178
|
+
"gatewayAttempt", "planDefinitionsDigest"]) {
|
|
179
|
+
if ((current.assignment.binding[key] || null) !== (binding[key] || null)) {
|
|
180
|
+
return { status: "foreign-assignment", blocked: true,
|
|
181
|
+
reason: `Continuation requires the same explicit ${key} binding, including absence.` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const exact = assignmentPremortemBinding(binding, current);
|
|
185
|
+
const inspected = await inspectPremortemState({ root, binding: exact });
|
|
186
|
+
if (inspected.blocked || inspected.status === "degraded") return inspected;
|
|
187
|
+
if (!inspected.requirement) return { status: "missing", blocked: true,
|
|
188
|
+
reason: "The exact continuation scope has no existing premortem requirement." };
|
|
189
|
+
if (inspected.closed || inspected.consumed) return { status: "finalized", blocked: true,
|
|
190
|
+
reason: "A completed assignment cannot be continued; a new delivery needs fresh preflight calls." };
|
|
191
|
+
if (inspected.conflicted) return { status: "conflict", blocked: true,
|
|
192
|
+
requirementId: inspected.requirementId,
|
|
193
|
+
reason: "Continuation cannot repair a conflicted registration or bypass its recovery checks." };
|
|
194
|
+
return { ...current, status: "continued", requirement: inspected.requirement,
|
|
195
|
+
requirementId: inspected.requirementId, binding: exact };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function withActiveDeliveryAssignment({ root, binding }, action) {
|
|
200
|
+
const paths = await pathsFor(root, binding);
|
|
201
|
+
return withOwnedFileLock(paths.lock, async ({ assertOwned }) => {
|
|
202
|
+
const current = await resolveDeliveryAssignment({ root, binding,
|
|
203
|
+
assignmentId: binding.assignmentId });
|
|
204
|
+
if (current.blocked) return current;
|
|
205
|
+
if (!current.assignment || current.assignmentId !== binding.assignmentId) {
|
|
206
|
+
return { status: "foreign-assignment", blocked: true,
|
|
207
|
+
reason: "Completion requires the active assignment for this session and project." };
|
|
208
|
+
}
|
|
209
|
+
const result = await action(assertOwned);
|
|
210
|
+
await assertOwned();
|
|
211
|
+
return result;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function assignmentPremortemBinding(binding, result) {
|
|
216
|
+
if (binding.goalId || binding.queueId) return { ...binding, assignmentId: null };
|
|
217
|
+
if (!result?.assignment) return { ...binding, assignmentId: null };
|
|
218
|
+
return { ...binding, ...result.assignment.binding,
|
|
219
|
+
assignmentId: result.assignment.assignmentId };
|
|
220
|
+
}
|