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,65 @@
|
|
|
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 { premortemSha256 as sha256, premortemTime as at,
|
|
7
|
+
sealPremortem as seal, validPremortemSeal as validSeal,
|
|
8
|
+
validPremortemTime as validTime } from "./delivery-premortem-codec.js";
|
|
9
|
+
|
|
10
|
+
const SCHEMA = "agentspine.delivery-premortem-registration-rejection/v1";
|
|
11
|
+
const AUTHORITY = "context-only";
|
|
12
|
+
const MAX_BYTES = 8 * 1024;
|
|
13
|
+
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
14
|
+
|
|
15
|
+
function valid(value) {
|
|
16
|
+
return value && value.schema === SCHEMA && value.authority === AUTHORITY
|
|
17
|
+
&& typeof value.requirementId === "string"
|
|
18
|
+
&& DIGEST_RE.test(value.priorArtifactDigest || "")
|
|
19
|
+
&& DIGEST_RE.test(value.proposedArtifactDigest || "")
|
|
20
|
+
&& DIGEST_RE.test(value.laneDigest || "")
|
|
21
|
+
&& validTime(value.rejectedAt) && validSeal(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function read(path) {
|
|
25
|
+
try {
|
|
26
|
+
const text = await readFile(path, "utf8");
|
|
27
|
+
if (Buffer.byteLength(text) > MAX_BYTES) throw new Error("premortem rejection exceeds 8 KiB");
|
|
28
|
+
const value = JSON.parse(text);
|
|
29
|
+
if (!valid(value)) throw new Error("premortem rejection failed integrity validation");
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ENOENT") return null;
|
|
33
|
+
if (error instanceof SyntaxError) throw new Error("premortem rejection is not valid JSON");
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function recordPremortemRegistrationRejection({ root, state, proposed, now = new Date() }) {
|
|
39
|
+
const receipt = seal({ schema: SCHEMA,
|
|
40
|
+
requirementId: state.requirement.requirementId,
|
|
41
|
+
laneDigest: state.laneDigest,
|
|
42
|
+
priorArtifactDigest: state.artifact.digest,
|
|
43
|
+
proposedArtifactDigest: proposed.digest,
|
|
44
|
+
reason: "conflicts-with-first-registration",
|
|
45
|
+
rejectedAt: at(now), authority: AUTHORITY });
|
|
46
|
+
const digest = sha256({ laneDigest: state.laneDigest,
|
|
47
|
+
priorArtifactDigest: state.artifact.digest, proposedArtifactDigest: proposed.digest });
|
|
48
|
+
const directory = join(await projectStateDir(await canonicalPath(root)),
|
|
49
|
+
"delivery-premortem-rejections", state.laneDigest);
|
|
50
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
51
|
+
const path = join(directory, `${digest}.json`);
|
|
52
|
+
return withOwnedFileLock(`${path}.lock`, async ({ assertOwned }) => {
|
|
53
|
+
const existing = await read(path);
|
|
54
|
+
if (existing) return { status: "duplicate", digest,
|
|
55
|
+
receiptDigest: existing.digest, receipt: existing };
|
|
56
|
+
await writePremortemFile(path, receipt, assertOwned, MAX_BYTES);
|
|
57
|
+
return { status: "recorded", digest, receiptDigest: receipt.digest, receipt };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function inspectPremortemRegistrationRejection({ root, laneDigest, digest }) {
|
|
62
|
+
const directory = join(await projectStateDir(await canonicalPath(root)),
|
|
63
|
+
"delivery-premortem-rejections", laneDigest);
|
|
64
|
+
return read(join(directory, `${digest}.json`));
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createPremortemAttachment } from "./delivery-premortem-closure.js";
|
|
2
|
+
|
|
3
|
+
export function premortemBlock(status, reason, extra = {}) {
|
|
4
|
+
return { status, blocked: true, reason, ...extra };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function degradedPremortem(error) {
|
|
8
|
+
return { status: "degraded", blocked: false,
|
|
9
|
+
reason: String(error?.message || error).slice(0, 400) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function premortemBoundary(error) {
|
|
13
|
+
if (error?.code === "AGENTSPINE_PREMORTEM_MISMATCH") {
|
|
14
|
+
return premortemBlock("mismatch", String(error.message).slice(0, 400));
|
|
15
|
+
}
|
|
16
|
+
if (error?.code === "AGENTSPINE_PREMORTEM_FINALIZED") {
|
|
17
|
+
return premortemBlock("finalized", String(error.message).slice(0, 400));
|
|
18
|
+
}
|
|
19
|
+
return degradedPremortem(error);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function closedPremortemResult(state, requirementId) {
|
|
23
|
+
const attachment = state.binding.goalId ? createPremortemAttachment(state) : null;
|
|
24
|
+
return { status: "closed", blocked: false, requirementId,
|
|
25
|
+
artifact: structuredClone(state.artifact), digest: state.artifact.digest,
|
|
26
|
+
closure: structuredClone(state.closure), checkpointAttachment: attachment,
|
|
27
|
+
outcomeReceiptAttachment: attachment ? structuredClone(attachment) : null };
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { lookupPremortemLaneIndex } from "./delivery-premortem-index.js";
|
|
3
|
+
import { premortemSha256 as sha256,
|
|
4
|
+
premortemMismatchError as mismatchError } from "./delivery-premortem-codec.js";
|
|
5
|
+
|
|
6
|
+
function sameIndexedLane(state, pointer) {
|
|
7
|
+
const binding = state?.binding;
|
|
8
|
+
return state?.laneDigest === pointer.laneDigest
|
|
9
|
+
&& binding?.goalId === pointer.goalId
|
|
10
|
+
&& binding?.goalStepId === pointer.goalStepId
|
|
11
|
+
&& binding?.queueId === pointer.queueId
|
|
12
|
+
&& binding?.gatewayAttempt === pointer.gatewayAttempt
|
|
13
|
+
&& binding?.planDefinitionsDigest === pointer.planDefinitionsDigest
|
|
14
|
+
&& sha256(binding?.sessionId || "") === pointer.sessionDigest;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sessionConflict(binding) {
|
|
18
|
+
return mismatchError(
|
|
19
|
+
`A different host session already wrote in ${binding.goalId}/${binding.goalStepId}, `
|
|
20
|
+
+ `queue ${binding.queueId}, gateway attempt ${binding.gatewayAttempt}. This session cannot `
|
|
21
|
+
+ "continue that attempt; fail or expire the lease, reconcile, and reclaim the queue so "
|
|
22
|
+
+ "AGENTSPINE_GATEWAY_ATTEMPT advances before retrying."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function premortemGoalWriterScopeCheck({ stateDirectory, binding, laneDigest, readState }) {
|
|
27
|
+
if (!binding.goalId || !binding.goalStepId || !binding.queueId || binding.gatewayAttempt === null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return async () => {
|
|
31
|
+
const indexed = await lookupPremortemLaneIndex({ stateDirectory,
|
|
32
|
+
goalId: binding.goalId, goalStepId: binding.goalStepId,
|
|
33
|
+
queueId: binding.queueId, gatewayAttempt: binding.gatewayAttempt });
|
|
34
|
+
if (indexed.blocked) throw mismatchError(indexed.reason);
|
|
35
|
+
if (indexed.status === "degraded") throw new Error(indexed.reason);
|
|
36
|
+
for (const pointer of indexed.pointers || []) {
|
|
37
|
+
if (pointer.laneDigest === laneDigest) continue;
|
|
38
|
+
const state = await readState(join(stateDirectory, `${pointer.laneDigest}.json`),
|
|
39
|
+
pointer.laneDigest);
|
|
40
|
+
if (!state || !sameIndexedLane(state, pointer)) {
|
|
41
|
+
throw mismatchError("A premortem session pointer does not match its exact lane state.");
|
|
42
|
+
}
|
|
43
|
+
if (state.firstWrite) throw sessionConflict(binding);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { writePremortemFile } from "./delivery-premortem-file.js";
|
|
4
|
+
import { canonicalPremortem as canonical, premortemMismatchError as mismatchError,
|
|
5
|
+
premortemSha256 as sha256 } from "./delivery-premortem-codec.js";
|
|
6
|
+
|
|
7
|
+
const SCHEMA = "agentspine.delivery-premortem-write-index/v2";
|
|
8
|
+
const AUTHORITY = "context-only";
|
|
9
|
+
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
10
|
+
const EDGE_RE = /^[a-f0-9]{2}$/;
|
|
11
|
+
const MAX_RECENT_WRITE_IDS = 64;
|
|
12
|
+
const MAX_NODE_BYTES = 64 * 1024;
|
|
13
|
+
const BUCKET_KEYS = ["authority", "depth", "digest", "entries", "kind", "prefix", "schema"];
|
|
14
|
+
const BRANCH_KEYS = ["authority", "children", "depth", "digest", "kind", "prefix", "schema"];
|
|
15
|
+
|
|
16
|
+
function validEntry(entry) {
|
|
17
|
+
return entry && DIGEST_RE.test(entry.idDigest || "")
|
|
18
|
+
&& DIGEST_RE.test(entry.inputDigest || "") && DIGEST_RE.test(entry.writeDigest || "")
|
|
19
|
+
&& typeof entry.inputKnown === "boolean"
|
|
20
|
+
&& Object.keys(entry).sort().join(",") === "idDigest,inputDigest,inputKnown,writeDigest";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeEntry(write) {
|
|
24
|
+
const entry = { idDigest: write?.idDigest, inputDigest: write?.inputDigest,
|
|
25
|
+
inputKnown: write?.inputKnown, writeDigest: write?.writeDigest || write?.digest };
|
|
26
|
+
if (!validEntry(entry)) throw new Error("premortem write identity is invalid");
|
|
27
|
+
return entry;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function findPremortemWrite(entries, idDigest) {
|
|
31
|
+
return entries.find((entry) => entry.idDigest === idDigest) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function premortemWriteIsLedgered(entries, write) {
|
|
35
|
+
const entry = findPremortemWrite(entries, write?.idDigest);
|
|
36
|
+
return entry?.inputDigest === write?.inputDigest && entry?.inputKnown === write?.inputKnown
|
|
37
|
+
&& entry?.writeDigest === write?.digest;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validPremortemWriteLedger(entries, firstWrite = null, lastWrite = null) {
|
|
41
|
+
return Array.isArray(entries) && entries.length <= MAX_RECENT_WRITE_IDS
|
|
42
|
+
&& new Set(entries.map((entry) => entry?.idDigest)).size === entries.length
|
|
43
|
+
&& entries.every(validEntry) && (!lastWrite || premortemWriteIsLedgered(entries, lastWrite))
|
|
44
|
+
&& (!firstWrite || entries.length === MAX_RECENT_WRITE_IDS
|
|
45
|
+
|| premortemWriteIsLedgered(entries, firstWrite));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function appendPremortemWrite(entries, write) {
|
|
49
|
+
if (entries.length >= MAX_RECENT_WRITE_IDS) entries.shift();
|
|
50
|
+
entries.push(writeEntry(write));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function indexDirectory(statePath, laneDigest) {
|
|
54
|
+
if (!DIGEST_RE.test(laneDigest || "")) throw new Error("premortem write lane is invalid");
|
|
55
|
+
return join(dirname(dirname(statePath)), "delivery-premortem-write-index", laneDigest, "nodes");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function deliveryPremortemWriteNodePath(statePath, laneDigest, nodeDigest) {
|
|
59
|
+
if (!DIGEST_RE.test(nodeDigest || "")) throw new Error("premortem write node digest is invalid");
|
|
60
|
+
return join(indexDirectory(statePath, laneDigest), `${nodeDigest}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nodeMaterial(kind, depth, prefix, payload) {
|
|
64
|
+
return { schema: SCHEMA, kind, depth, prefix, ...payload, authority: AUTHORITY };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sealedNode(kind, depth, prefix, payload) {
|
|
68
|
+
const material = nodeMaterial(kind, depth, prefix, payload);
|
|
69
|
+
return { ...material, digest: sha256(material) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nodeBytes(node) {
|
|
73
|
+
return Buffer.byteLength(`${JSON.stringify(node, null, 2)}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validDepth(depth) {
|
|
77
|
+
return Number.isInteger(depth) && depth >= 0 && depth <= 64 && depth % 2 === 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validNode(value, expectedDigest, expectedDepth, expectedPrefix) {
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
82
|
+
|| value.schema !== SCHEMA || value.authority !== AUTHORITY
|
|
83
|
+
|| value.digest !== expectedDigest || value.depth !== expectedDepth
|
|
84
|
+
|| value.prefix !== expectedPrefix || !validDepth(value.depth)
|
|
85
|
+
|| typeof value.prefix !== "string" || value.prefix.length !== value.depth
|
|
86
|
+
|| !/^[a-f0-9]*$/.test(value.prefix)) return false;
|
|
87
|
+
const keys = value.kind === "bucket" ? BUCKET_KEYS : value.kind === "branch" ? BRANCH_KEYS : [];
|
|
88
|
+
if (canonical(Object.keys(value).sort()) !== canonical(keys)) return false;
|
|
89
|
+
if (value.kind === "bucket") {
|
|
90
|
+
if (!Array.isArray(value.entries) || !value.entries.length
|
|
91
|
+
|| !value.entries.every(validEntry)
|
|
92
|
+
|| !value.entries.every((entry) => entry.idDigest.startsWith(value.prefix))
|
|
93
|
+
|| new Set(value.entries.map((entry) => entry.idDigest)).size !== value.entries.length
|
|
94
|
+
|| canonical(value.entries) !== canonical([...value.entries]
|
|
95
|
+
.sort((left, right) => left.idDigest.localeCompare(right.idDigest)))) return false;
|
|
96
|
+
} else {
|
|
97
|
+
if (value.depth >= 64 || !value.children || typeof value.children !== "object"
|
|
98
|
+
|| Array.isArray(value.children)) return false;
|
|
99
|
+
const edges = Object.keys(value.children);
|
|
100
|
+
if (!edges.length || edges.length > 256 || !edges.every((edge) => EDGE_RE.test(edge)
|
|
101
|
+
&& DIGEST_RE.test(value.children[edge] || ""))
|
|
102
|
+
|| new Set(Object.values(value.children)).size !== edges.length) return false;
|
|
103
|
+
}
|
|
104
|
+
const material = { ...value };
|
|
105
|
+
delete material.digest;
|
|
106
|
+
return value.digest === sha256(material) && nodeBytes(value) <= MAX_NODE_BYTES;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readNode(statePath, laneDigest, nodeDigest, depth, prefix) {
|
|
110
|
+
const path = deliveryPremortemWriteNodePath(statePath, laneDigest, nodeDigest);
|
|
111
|
+
try {
|
|
112
|
+
const text = await readFile(path, "utf8");
|
|
113
|
+
if (Buffer.byteLength(text) > MAX_NODE_BYTES) {
|
|
114
|
+
throw mismatchError("premortem write index node exceeds 64 KiB");
|
|
115
|
+
}
|
|
116
|
+
let value;
|
|
117
|
+
try { value = JSON.parse(text); } catch { throw new Error("premortem write index node is not valid JSON"); }
|
|
118
|
+
if (!validNode(value, nodeDigest, depth, prefix)) {
|
|
119
|
+
throw mismatchError("premortem write index node failed integrity validation");
|
|
120
|
+
}
|
|
121
|
+
return { path, value };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code === "ENOENT") {
|
|
124
|
+
throw mismatchError("premortem write index references a missing node");
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function saveNode(statePath, laneDigest, node, assertOwned) {
|
|
131
|
+
const directory = indexDirectory(statePath, laneDigest);
|
|
132
|
+
const path = deliveryPremortemWriteNodePath(statePath, laneDigest, node.digest);
|
|
133
|
+
await assertOwned();
|
|
134
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
135
|
+
try {
|
|
136
|
+
const existing = await readNode(statePath, laneDigest, node.digest, node.depth, node.prefix);
|
|
137
|
+
if (canonical(existing.value) !== canonical(node)) {
|
|
138
|
+
throw mismatchError("premortem write index node conflicts with its digest");
|
|
139
|
+
}
|
|
140
|
+
return node.digest;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error.code !== "AGENTSPINE_PREMORTEM_MISMATCH"
|
|
143
|
+
|| !/references a missing node/.test(error.message)) throw error;
|
|
144
|
+
}
|
|
145
|
+
await writePremortemFile(path, node, assertOwned, MAX_NODE_BYTES);
|
|
146
|
+
return node.digest;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function bucket(depth, prefix, entries) {
|
|
150
|
+
return sealedNode("bucket", depth, prefix, { entries: [...entries]
|
|
151
|
+
.sort((left, right) => left.idDigest.localeCompare(right.idDigest)) });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function branch(depth, prefix, children) {
|
|
155
|
+
return sealedNode("branch", depth, prefix, { children: Object.fromEntries(
|
|
156
|
+
Object.entries(children).sort(([left], [right]) => left.localeCompare(right))) });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function buildNode(statePath, laneDigest, depth, prefix, entries, assertOwned) {
|
|
160
|
+
const candidate = bucket(depth, prefix, entries);
|
|
161
|
+
if (nodeBytes(candidate) <= MAX_NODE_BYTES) {
|
|
162
|
+
await saveNode(statePath, laneDigest, candidate, assertOwned);
|
|
163
|
+
return candidate.digest;
|
|
164
|
+
}
|
|
165
|
+
if (depth >= 64) throw new Error("premortem write index bucket could not be split");
|
|
166
|
+
const grouped = new Map();
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const edge = entry.idDigest.slice(depth, depth + 2);
|
|
169
|
+
if (!grouped.has(edge)) grouped.set(edge, []);
|
|
170
|
+
grouped.get(edge).push(entry);
|
|
171
|
+
}
|
|
172
|
+
const children = {};
|
|
173
|
+
for (const [edge, group] of grouped) {
|
|
174
|
+
children[edge] = await buildNode(statePath, laneDigest, depth + 2,
|
|
175
|
+
`${prefix}${edge}`, group, assertOwned);
|
|
176
|
+
}
|
|
177
|
+
const candidateBranch = branch(depth, prefix, children);
|
|
178
|
+
await saveNode(statePath, laneDigest, candidateBranch, assertOwned);
|
|
179
|
+
return candidateBranch.digest;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function insertNode(statePath, laneDigest, nodeDigest, depth, prefix, entry, assertOwned) {
|
|
183
|
+
if (!nodeDigest) return buildNode(statePath, laneDigest, depth, prefix, [entry], assertOwned);
|
|
184
|
+
const { value } = await readNode(statePath, laneDigest, nodeDigest, depth, prefix);
|
|
185
|
+
if (value.kind === "bucket") {
|
|
186
|
+
const prior = findPremortemWrite(value.entries, entry.idDigest);
|
|
187
|
+
if (prior) {
|
|
188
|
+
if (canonical(prior) !== canonical(entry)) {
|
|
189
|
+
throw mismatchError("premortem write index has conflicting input for an existing ID");
|
|
190
|
+
}
|
|
191
|
+
return nodeDigest;
|
|
192
|
+
}
|
|
193
|
+
return buildNode(statePath, laneDigest, depth, prefix, [...value.entries, entry], assertOwned);
|
|
194
|
+
}
|
|
195
|
+
const edge = entry.idDigest.slice(depth, depth + 2);
|
|
196
|
+
const next = await insertNode(statePath, laneDigest, value.children[edge] || null,
|
|
197
|
+
depth + 2, `${prefix}${edge}`, entry, assertOwned);
|
|
198
|
+
return saveNode(statePath, laneDigest,
|
|
199
|
+
branch(depth, prefix, { ...value.children, [edge]: next }), assertOwned);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function inspectPremortemWriteProof({ statePath, laneDigest, rootDigest, idDigest }) {
|
|
203
|
+
if (!DIGEST_RE.test(idDigest || "")) throw new Error("premortem write ID is invalid");
|
|
204
|
+
if (rootDigest === null) return { entry: null, paths: [] };
|
|
205
|
+
if (!DIGEST_RE.test(rootDigest || "")) throw mismatchError("premortem write index root is invalid");
|
|
206
|
+
let digest = rootDigest;
|
|
207
|
+
let depth = 0;
|
|
208
|
+
let prefix = "";
|
|
209
|
+
const paths = [];
|
|
210
|
+
while (digest) {
|
|
211
|
+
const loaded = await readNode(statePath, laneDigest, digest, depth, prefix);
|
|
212
|
+
paths.push(loaded.path);
|
|
213
|
+
if (loaded.value.kind === "bucket") {
|
|
214
|
+
return { entry: findPremortemWrite(loaded.value.entries, idDigest), paths };
|
|
215
|
+
}
|
|
216
|
+
const edge = idDigest.slice(depth, depth + 2);
|
|
217
|
+
digest = loaded.value.children[edge] || null;
|
|
218
|
+
prefix += edge;
|
|
219
|
+
depth += 2;
|
|
220
|
+
}
|
|
221
|
+
return { entry: null, paths };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function readPremortemWriteIndex(input) {
|
|
225
|
+
return (await inspectPremortemWriteProof(input)).entry;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function verifyPremortemWriteIndex({ statePath, state }) {
|
|
229
|
+
const writes = [state.firstWrite, state.lastWrite]
|
|
230
|
+
.filter((write, index, all) => write && all.findIndex((item) => item?.idDigest === write.idDigest) === index);
|
|
231
|
+
for (const write of writes) {
|
|
232
|
+
const entry = await readPremortemWriteIndex({ statePath, laneDigest: state.laneDigest,
|
|
233
|
+
rootDigest: state.writeIndexRoot, idDigest: write.idDigest });
|
|
234
|
+
if (!entry || entry.inputDigest !== write.inputDigest || entry.inputKnown !== write.inputKnown
|
|
235
|
+
|| entry.writeDigest !== write.digest) {
|
|
236
|
+
throw mismatchError("premortem write index does not prove a bound write");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function inspectPremortemWriteIndexes({ statePath, state, maxNodes = 512 }) {
|
|
243
|
+
const directory = indexDirectory(statePath, state.laneDigest);
|
|
244
|
+
const result = { directory, paths: [], nodes: [], errors: [], truncations: [] };
|
|
245
|
+
if (!state.writeIndexRoot) return result;
|
|
246
|
+
const pending = [{ digest: state.writeIndexRoot, depth: 0, prefix: "" }];
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
while (pending.length && seen.size < maxNodes) {
|
|
249
|
+
const current = pending.shift();
|
|
250
|
+
if (seen.has(current.digest)) continue;
|
|
251
|
+
seen.add(current.digest);
|
|
252
|
+
const path = deliveryPremortemWriteNodePath(statePath, state.laneDigest, current.digest);
|
|
253
|
+
result.paths.push(path);
|
|
254
|
+
try {
|
|
255
|
+
const loaded = await readNode(statePath, state.laneDigest, current.digest,
|
|
256
|
+
current.depth, current.prefix);
|
|
257
|
+
result.nodes.push(structuredClone(loaded.value));
|
|
258
|
+
if (loaded.value.kind === "branch") {
|
|
259
|
+
for (const [edge, digest] of Object.entries(loaded.value.children)) {
|
|
260
|
+
pending.push({ digest, depth: current.depth + 2, prefix: `${current.prefix}${edge}` });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
result.errors.push({ path, reason: String(error.message).slice(0, 400),
|
|
265
|
+
code: typeof error.code === "string" ? error.code : null });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (pending.length) result.truncations.push({ path: directory,
|
|
269
|
+
reason: `limited to ${maxNodes} reachable write-index nodes` });
|
|
270
|
+
try { await verifyPremortemWriteIndex({ statePath, state }); } catch (error) {
|
|
271
|
+
result.errors.push({ path: statePath, reason: String(error.message).slice(0, 400),
|
|
272
|
+
code: typeof error.code === "string" ? error.code : null });
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function appendPremortemWriteIndex({ statePath, laneDigest, rootDigest,
|
|
278
|
+
write, assertOwned }) {
|
|
279
|
+
if (rootDigest !== null && !DIGEST_RE.test(rootDigest || "")) {
|
|
280
|
+
throw mismatchError("premortem write index root is invalid");
|
|
281
|
+
}
|
|
282
|
+
const entry = writeEntry(write);
|
|
283
|
+
const nextRoot = await insertNode(statePath, laneDigest, rootDigest, 0, "", entry, assertOwned);
|
|
284
|
+
return { rootDigest: nextRoot, entry };
|
|
285
|
+
}
|