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,424 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { recordAttentionEvent } from "./attention.js";
|
|
3
|
+
import { catalogForStateRoot } from "./catalog.js";
|
|
4
|
+
import { claimChannelEvent } from "./channel-runtime.js";
|
|
5
|
+
import { loadContinuity } from "./continuity.js";
|
|
6
|
+
import { resolveSessionJob, startOrResumeJob } from "./selfstarter.js";
|
|
7
|
+
|
|
8
|
+
const STANDARD_HOST_CONTEXT_BYTES = 9500;
|
|
9
|
+
const MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES = 32 * 1024;
|
|
10
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
|
|
11
|
+
export const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
|
|
12
|
+
const SELFSTART_EVENTS = new Set(["SessionStart", "PostCompact"]);
|
|
13
|
+
|
|
14
|
+
export function boundedId(value, field) {
|
|
15
|
+
if (value === null || value === undefined || value === "") return null;
|
|
16
|
+
if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} is invalid`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function positiveInteger(value, field) {
|
|
21
|
+
if (value === null || value === undefined || value === "") return null;
|
|
22
|
+
const parsed = typeof value === "number"
|
|
23
|
+
? value
|
|
24
|
+
: (typeof value === "string" && /^[1-9][0-9]*$/.test(value) ? Number(value) : Number.NaN);
|
|
25
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${field} is invalid`);
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function promptFromInput(input) {
|
|
30
|
+
for (const key of ["prompt", "user_prompt", "message", "input"]) {
|
|
31
|
+
const value = input[key];
|
|
32
|
+
if (typeof value === "string") return value;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
const text = value
|
|
35
|
+
.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string")
|
|
36
|
+
.map((part) => part.text)
|
|
37
|
+
.join("\n")
|
|
38
|
+
.trim();
|
|
39
|
+
if (text) return text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hostContextLimit(preflight) {
|
|
46
|
+
return preflight?.receipt?.instructionBudget?.mode === "claude-required-overflow"
|
|
47
|
+
? MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES
|
|
48
|
+
: STANDARD_HOST_CONTEXT_BYTES;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hostFromInput(input) {
|
|
52
|
+
const explicit = input.host || input.provider || process.env.AGENTSPINE_HOST;
|
|
53
|
+
if (["claude", "codex", "generic"].includes(explicit)) return explicit;
|
|
54
|
+
if ((typeof input.model === "string" && input.model.trim()) || process.env.PLUGIN_ROOT || process.env.CODEX_HOME
|
|
55
|
+
|| process.env.BLUN_PLUGIN_ROOT || process.env.BLUN_HOME) return "codex";
|
|
56
|
+
return "claude";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function gatewayEnvironmentContext(env = process.env) {
|
|
60
|
+
if (env.AGENTSPINE_GATEWAY_CONTEXT !== "agentspine.gateway-start/v1") return null;
|
|
61
|
+
return {
|
|
62
|
+
host: boundedId(env.AGENTSPINE_HOST, "AGENTSPINE_HOST"),
|
|
63
|
+
entityId: boundedId(env.AGENTSPINE_ENTITY_ID, "AGENTSPINE_ENTITY_ID"),
|
|
64
|
+
groupId: boundedId(env.AGENTSPINE_GROUP_ID, "AGENTSPINE_GROUP_ID"),
|
|
65
|
+
projectId: boundedId(env.AGENTSPINE_PROJECT_ID, "AGENTSPINE_PROJECT_ID"),
|
|
66
|
+
taskId: boundedId(env.AGENTSPINE_TASK_ID, "AGENTSPINE_TASK_ID"),
|
|
67
|
+
queueId: boundedId(env.AGENTSPINE_GATEWAY_QUEUE_ID, "AGENTSPINE_GATEWAY_QUEUE_ID"),
|
|
68
|
+
goalId: boundedId(env.AGENTSPINE_GOAL_ID, "AGENTSPINE_GOAL_ID"),
|
|
69
|
+
goalStepId: boundedId(env.AGENTSPINE_GOAL_STEP_ID, "AGENTSPINE_GOAL_STEP_ID"),
|
|
70
|
+
planDefinitionsDigest: boundedId(env.AGENTSPINE_PLAN_DEFINITIONS_DIGEST,
|
|
71
|
+
"AGENTSPINE_PLAN_DEFINITIONS_DIGEST"),
|
|
72
|
+
gatewayAttempt: positiveInteger(env.AGENTSPINE_GATEWAY_ATTEMPT, "AGENTSPINE_GATEWAY_ATTEMPT"),
|
|
73
|
+
eventId: boundedId(env.AGENTSPINE_CHANNEL_EVENT_ID, "AGENTSPINE_CHANNEL_EVENT_ID"),
|
|
74
|
+
provider: boundedId(env.AGENTSPINE_CHANNEL_PROVIDER, "AGENTSPINE_CHANNEL_PROVIDER")
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function gatewayBound(gateway, key, suppliedValue, field, allowAbsent = false) {
|
|
79
|
+
if (!gateway) return suppliedValue;
|
|
80
|
+
const gatewayValue = gateway[key];
|
|
81
|
+
if (gatewayValue === null || gatewayValue === undefined || gatewayValue === "") {
|
|
82
|
+
if (allowAbsent) return suppliedValue;
|
|
83
|
+
if (suppliedValue !== undefined && suppliedValue !== null && suppliedValue !== "") {
|
|
84
|
+
throw new Error(`${field} does not match the authenticated gateway binding`);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (suppliedValue !== undefined && suppliedValue !== null
|
|
89
|
+
&& suppliedValue !== "" && suppliedValue !== gatewayValue) {
|
|
90
|
+
throw new Error(`${field} does not match the authenticated gateway binding`);
|
|
91
|
+
}
|
|
92
|
+
return gatewayValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runtimeScope(input, root, userStateRoot = null, catalog) {
|
|
96
|
+
const { continuity: projectContinuity } = await loadContinuity(root, catalog);
|
|
97
|
+
const userCatalog = userStateRoot && userStateRoot !== root
|
|
98
|
+
? catalogForStateRoot(catalog, userStateRoot) : catalog;
|
|
99
|
+
const userContinuity = userStateRoot && userStateRoot !== root
|
|
100
|
+
? (await loadContinuity(userStateRoot, userCatalog)).continuity
|
|
101
|
+
: projectContinuity;
|
|
102
|
+
const continuity = {
|
|
103
|
+
config: {
|
|
104
|
+
...projectContinuity.config,
|
|
105
|
+
enabled: userContinuity.config.enabled,
|
|
106
|
+
minConfidence: userContinuity.config.minConfidence,
|
|
107
|
+
minDirectness: userContinuity.config.minDirectness,
|
|
108
|
+
minEvidence: userContinuity.config.minEvidence,
|
|
109
|
+
maxPromptBytes: userContinuity.config.maxPromptBytes,
|
|
110
|
+
maxBriefingBytes: userContinuity.config.maxBriefingBytes,
|
|
111
|
+
defaultEntityId: userContinuity.config.defaultEntityId
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
|
|
115
|
+
? input.agent_spine_scope : input;
|
|
116
|
+
const gateway = gatewayEnvironmentContext();
|
|
117
|
+
const suppliedAttempt = positiveInteger(supplied.gateway_attempt ?? supplied.gatewayAttempt,
|
|
118
|
+
"gatewayAttempt");
|
|
119
|
+
return {
|
|
120
|
+
entityId: boundedId(gatewayBound(gateway, "entityId",
|
|
121
|
+
supplied.entity_id ?? supplied.entityId, "entityId")
|
|
122
|
+
?? (gateway ? null : continuity.config.defaultEntityId), "entityId"),
|
|
123
|
+
userId: boundedId(supplied.user_id ?? supplied.userId ?? process.env.AGENTSPINE_USER_ID, "userId"),
|
|
124
|
+
tenantId: boundedId(supplied.tenant_id ?? supplied.tenantId ?? process.env.AGENTSPINE_TENANT_ID, "tenantId"),
|
|
125
|
+
groupId: boundedId(gatewayBound(gateway, "groupId",
|
|
126
|
+
supplied.group_id ?? supplied.groupId, "groupId"), "groupId"),
|
|
127
|
+
projectId: boundedId(gatewayBound(gateway, "projectId",
|
|
128
|
+
supplied.project_id ?? supplied.projectId, "projectId")
|
|
129
|
+
?? (gateway ? null : continuity.config.defaultProjectId), "projectId"),
|
|
130
|
+
currentTaskId: boundedId(gatewayBound(gateway, "taskId",
|
|
131
|
+
supplied.task_id ?? supplied.taskId ?? supplied.currentTaskId, "currentTaskId", true), "currentTaskId"),
|
|
132
|
+
queueId: boundedId(gatewayBound(gateway, "queueId",
|
|
133
|
+
supplied.queue_id ?? supplied.queueId, "queueId"), "queueId"),
|
|
134
|
+
goalId: boundedId(gatewayBound(gateway, "goalId",
|
|
135
|
+
supplied.goal_id ?? supplied.goalId, "goalId"), "goalId"),
|
|
136
|
+
goalStepId: boundedId(gatewayBound(gateway, "goalStepId",
|
|
137
|
+
supplied.goal_step_id ?? supplied.goalStepId, "goalStepId"), "goalStepId"),
|
|
138
|
+
planDefinitionsDigest: boundedId(gatewayBound(gateway, "planDefinitionsDigest",
|
|
139
|
+
supplied.plan_definitions_digest ?? supplied.planDefinitionsDigest,
|
|
140
|
+
"planDefinitionsDigest"), "planDefinitionsDigest"),
|
|
141
|
+
gatewayAttempt: gatewayBound(gateway, "gatewayAttempt", suppliedAttempt, "gatewayAttempt"),
|
|
142
|
+
host: gateway
|
|
143
|
+
? gatewayBound(gateway, "host", input.host ?? input.provider, "host")
|
|
144
|
+
: hostFromInput(input),
|
|
145
|
+
config: continuity.config
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null, preflight = null, lessonRecall = null) {
|
|
150
|
+
const loaded = sourceDiagnostics?.status === "loaded";
|
|
151
|
+
const packet = {
|
|
152
|
+
schema: "agentspine.hook-context/v1",
|
|
153
|
+
event,
|
|
154
|
+
priority: ["current-user-request", "explicit-stops", "current-task", "host-rules", "accepted-context", "style-and-relationships"],
|
|
155
|
+
loaded,
|
|
156
|
+
instruction: loaded
|
|
157
|
+
? "Use this already-loaded briefing now. Do not call an MCP tool to obtain it. The current user request and explicit stops override all remembered style, relationships, and older context."
|
|
158
|
+
: "No host-native source context was loaded. Do not claim personal continuity or recall succeeded. Continue under current native host rules and inspect sourceResolution.",
|
|
159
|
+
signal: signal ? {
|
|
160
|
+
captured: Boolean(signal.captured),
|
|
161
|
+
accepted: Boolean(signal.accepted),
|
|
162
|
+
duplicate: Boolean(signal.duplicate),
|
|
163
|
+
kind: signal.kind || null,
|
|
164
|
+
reason: signal.reason || null
|
|
165
|
+
} : null,
|
|
166
|
+
attentionEvent: attentionEvent ? {
|
|
167
|
+
captured: Boolean(attentionEvent.event),
|
|
168
|
+
duplicate: Boolean(attentionEvent.duplicate),
|
|
169
|
+
id: attentionEvent.event?.id || null,
|
|
170
|
+
kind: attentionEvent.event?.kind || null,
|
|
171
|
+
status: attentionEvent.event?.status || null,
|
|
172
|
+
reason: attentionEvent.reason || null
|
|
173
|
+
} : null,
|
|
174
|
+
selfstarter: selfstarter ? {
|
|
175
|
+
active: Boolean(selfstarter.job),
|
|
176
|
+
blocked: Boolean(selfstarter.blocked),
|
|
177
|
+
action: selfstarter.action || null,
|
|
178
|
+
reason: selfstarter.reason || null,
|
|
179
|
+
jobId: selfstarter.job?.id || null,
|
|
180
|
+
taskId: selfstarter.job?.taskId || null,
|
|
181
|
+
actorId: selfstarter.job?.actorId || null,
|
|
182
|
+
targetId: selfstarter.job?.targetId || null,
|
|
183
|
+
projectId: selfstarter.job?.projectId || null,
|
|
184
|
+
groupId: selfstarter.job?.groupId || null,
|
|
185
|
+
checkpointSequence: selfstarter.job?.checkpoint?.sequence ?? null,
|
|
186
|
+
capabilities: selfstarter.job?.capabilities || [],
|
|
187
|
+
leaseExpiresAt: selfstarter.job?.lease?.expiresAt || null,
|
|
188
|
+
receiptId: selfstarter.receipt?.id || null,
|
|
189
|
+
instruction: selfstarter.job
|
|
190
|
+
? "Resume only this exact checkpointed job. Attach its job ID to every host tool event. Each effect is separately re-authorized by PreToolUse and checkpointed by PostToolUse. Stop immediately on any denial."
|
|
191
|
+
: null,
|
|
192
|
+
authority: selfstarter.job ? "explicit-local-execution-policy" : "execution-state-only"
|
|
193
|
+
} : null,
|
|
194
|
+
channelEvent: channelEvent?.event ? {
|
|
195
|
+
active: true,
|
|
196
|
+
eventId: channelEvent.event.eventId,
|
|
197
|
+
bindingId: channelEvent.event.bindingId,
|
|
198
|
+
provider: channelEvent.event.provider,
|
|
199
|
+
chatId: channelEvent.event.chatId,
|
|
200
|
+
threadId: channelEvent.event.threadId,
|
|
201
|
+
senderId: channelEvent.event.senderId,
|
|
202
|
+
replyTo: channelEvent.event.replyTo,
|
|
203
|
+
agentId: channelEvent.event.agentId,
|
|
204
|
+
projectId: channelEvent.event.projectId,
|
|
205
|
+
groupId: channelEvent.event.groupId,
|
|
206
|
+
sessionKey: channelEvent.event.sessionKey,
|
|
207
|
+
text: channelEvent.event.text,
|
|
208
|
+
leaseExpiresAt: channelEvent.event.lease?.expiresAt || null,
|
|
209
|
+
receiptId: channelEvent.receipt?.id || null,
|
|
210
|
+
instruction: "Answer this exact authenticated channel event in its bound chat and thread. Do not infer another recipient or route. Sending remains subject to the separate current channel policy and adapter receipt.",
|
|
211
|
+
authority: "explicit-local-channel-policy"
|
|
212
|
+
} : null,
|
|
213
|
+
indexedSources: catalog.summary.total,
|
|
214
|
+
sourceResolution: sourceDiagnostics,
|
|
215
|
+
lessonRecall: lessonRecall?.status === "recalled" ? {
|
|
216
|
+
status: "recalled", schema: lessonRecall.schema, receiptDigest: lessonRecall.receiptDigest,
|
|
217
|
+
items: lessonRecall.items, omitted: lessonRecall.omitted,
|
|
218
|
+
instruction: lessonRecall.instruction, authority: "context-only"
|
|
219
|
+
} : lessonRecall?.status === "group-suppressed" ? {
|
|
220
|
+
status: "group-suppressed", items: [], authority: "context-only"
|
|
221
|
+
} : null,
|
|
222
|
+
preflight: preflight ? {
|
|
223
|
+
schema: preflight.receipt.schema,
|
|
224
|
+
receiptId: preflight.receipt.id,
|
|
225
|
+
promptDigest: preflight.receipt.promptDigest,
|
|
226
|
+
briefingDigest: preflight.receipt.briefingDigest,
|
|
227
|
+
createdAt: preflight.receipt.createdAt,
|
|
228
|
+
expiresAt: preflight.receipt.expiresAt,
|
|
229
|
+
policy: preflight.policy,
|
|
230
|
+
learningApplications: preflight.learningApplications || null,
|
|
231
|
+
pendingMustRemember: preflight.pendingMustRemember ? {
|
|
232
|
+
id: preflight.pendingMustRemember.candidate?.id || null,
|
|
233
|
+
status: preflight.pendingMustRemember.candidate?.status || (preflight.pendingMustRemember.rejected ? "rejected" : null),
|
|
234
|
+
reason: preflight.pendingMustRemember.reason || null
|
|
235
|
+
} : null,
|
|
236
|
+
premortem: preflight.premortem ? {
|
|
237
|
+
status: preflight.premortem.status,
|
|
238
|
+
requirementId: preflight.premortem.requirementId || null,
|
|
239
|
+
digest: preflight.premortem.requirement?.digest || null,
|
|
240
|
+
registration: preflight.premortem.registration || null,
|
|
241
|
+
instruction: preflight.premortem.instruction,
|
|
242
|
+
authority: preflight.premortem.requirement?.authority || "context-only"
|
|
243
|
+
} : null,
|
|
244
|
+
briefing: preflight.briefing,
|
|
245
|
+
instruction: "This exact turn passed the mandatory pre-answer gate. Apply the complete preflight briefing before answering.",
|
|
246
|
+
authority: "preflight-proof-only"
|
|
247
|
+
} : null,
|
|
248
|
+
briefing,
|
|
249
|
+
authority: "context-only"
|
|
250
|
+
};
|
|
251
|
+
return JSON.stringify(packet);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function selfstarterInput(input) {
|
|
255
|
+
const value = input.agent_spine_job;
|
|
256
|
+
if (value === undefined || value === null) return null;
|
|
257
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_job must be one object");
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function channelEventInput(input) {
|
|
262
|
+
const gateway = gatewayEnvironmentContext();
|
|
263
|
+
const value = input.agent_spine_channel_event ?? (gateway?.eventId && gateway?.provider
|
|
264
|
+
? { event_id: gateway.eventId, provider: gateway.provider } : null);
|
|
265
|
+
if (value === undefined || value === null) return null;
|
|
266
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_channel_event must be one object");
|
|
267
|
+
const unknown = Object.keys(value).filter((key) => !["event_id", "eventId", "provider"].includes(key));
|
|
268
|
+
if (unknown.length) throw new Error(`agent_spine_channel_event contains unknown field: ${unknown.sort()[0]}`);
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function hookDeliveryId(input) {
|
|
273
|
+
return boundedId(input.tool_use_id ?? input.event_id ?? input.hook_event_id, "toolUseId");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function sessionId(input) {
|
|
277
|
+
return boundedId(input.session_id ?? input.sessionId, "sessionId");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function selfstarterRootSkipped(sourceDiagnostics) {
|
|
281
|
+
return ["skipped-unmarked-home", "skipped-home-root", "skipped-profile-root"]
|
|
282
|
+
.includes(sourceDiagnostics?.projectTreeScan);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function startSelfstarter(input, event, root, scope, sourceDiagnostics) {
|
|
286
|
+
if (!SELFSTART_EVENTS.has(event)) return null;
|
|
287
|
+
const requested = selfstarterInput(input);
|
|
288
|
+
if (selfstarterRootSkipped(sourceDiagnostics)) {
|
|
289
|
+
if (requested) throw new Error("self-starter cannot use a user home or host profile as its workspace root");
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const session = sessionId(input);
|
|
293
|
+
if (!scope.entityId || !scope.projectId || !session) {
|
|
294
|
+
if (requested) throw new Error("self-starter start requires an exact actor, project, and host session");
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return startOrResumeJob({
|
|
298
|
+
root, actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
299
|
+
taskId: scope.currentTaskId, jobId: boundedId(requested?.job_id ?? requested?.jobId, "jobId"),
|
|
300
|
+
host: scope.host, sessionId: session, now: input.timestamp || new Date()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function startChannelEvent(input, event, root, scope, catalog) {
|
|
305
|
+
const requested = channelEventInput(input);
|
|
306
|
+
if (!requested) return null;
|
|
307
|
+
if (event !== "SessionStart") throw new Error("channel event claims are accepted only at SessionStart");
|
|
308
|
+
const session = sessionId(input);
|
|
309
|
+
if (!scope.entityId || !scope.projectId || !session) {
|
|
310
|
+
throw new Error("channel event start requires an exact agent, project, and host session");
|
|
311
|
+
}
|
|
312
|
+
const workerId = `channel-worker:${createHash("sha256").update(`${scope.host}\0${session}`).digest("hex").slice(0, 24)}`;
|
|
313
|
+
const claim = await claimChannelEvent({
|
|
314
|
+
root, eventId: boundedId(requested.event_id ?? requested.eventId, "channelEventId"),
|
|
315
|
+
agentId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
316
|
+
provider: boundedId(requested.provider, "channelProvider"), workerId,
|
|
317
|
+
now: input.timestamp || new Date(), catalog
|
|
318
|
+
});
|
|
319
|
+
if (!claim.event) throw new Error("the exact channel event is unavailable in this agent lane");
|
|
320
|
+
return claim;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function selfstarterScope(input, scope, root, action) {
|
|
324
|
+
const requested = selfstarterInput(input);
|
|
325
|
+
const hostSession = sessionId(input);
|
|
326
|
+
const supplied = {
|
|
327
|
+
actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
328
|
+
taskId: scope.currentTaskId, host: scope.host, sessionId: hostSession
|
|
329
|
+
};
|
|
330
|
+
if (requested) return { ...supplied, jobId: boundedId(requested.job_id ?? requested.jobId, "jobId") };
|
|
331
|
+
if (!hostSession) return null;
|
|
332
|
+
return resolveSessionJob({ root, ...supplied, action, now: input.timestamp || new Date() });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function toolSucceeded(input) {
|
|
336
|
+
if (input.success === false || input.is_error === true || input.tool_error) return false;
|
|
337
|
+
if (input.tool_result && typeof input.tool_result === "object" && input.tool_result.isError === true) return false;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function toolResult(input) {
|
|
342
|
+
return input.tool_result ?? input.tool_response ?? input.result ?? (input.tool_error ? { failed: true } : null);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function eventReceipt(input, event, scope, discriminator = "lifecycle") {
|
|
346
|
+
const supplied = input.event_id ?? input.hook_event_id ?? input.tool_use_id ?? input.session_id;
|
|
347
|
+
if (typeof supplied === "string" && ID_RE.test(supplied)) {
|
|
348
|
+
return `receipt:${createHash("sha256").update(`${event}\0${supplied}\0${discriminator}`).digest("hex").slice(0, 24)}`;
|
|
349
|
+
}
|
|
350
|
+
const material = [event, scope.host, scope.entityId, scope.projectId, scope.currentTaskId, discriminator].join("\0");
|
|
351
|
+
return `receipt:${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function heartbeatReceipt(input, scope) {
|
|
355
|
+
const at = new Date(input.timestamp || Date.now());
|
|
356
|
+
if (!Number.isFinite(at.getTime())) throw new Error("heartbeat timestamp is invalid");
|
|
357
|
+
const minute = at.toISOString().slice(0, 16);
|
|
358
|
+
const material = [scope.host, scope.entityId, scope.groupId, scope.projectId, scope.currentTaskId, minute].join("\0");
|
|
359
|
+
return `receipt:heartbeat:${createHash("sha256").update(material).digest("hex").slice(0, 20)}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function minimalAttentionSignal(prompt) {
|
|
363
|
+
if (typeof prompt !== "string" || Buffer.byteLength(prompt) > 16384) return null;
|
|
364
|
+
const rules = [
|
|
365
|
+
{ kind: "promise", re: /^(?:i promise(?: to)?|i will|ich verspreche(?:,)?|ich werde|prometo(?: que)?|voy a|jag lovar att|jag kommer att)\s+(.+)$/i, prefix: "Promise: " },
|
|
366
|
+
{ kind: "blocker", re: /^(?:blocker|blocked(?: by)?|i am blocked(?: by)?|ich bin blockiert(?: durch)?|blockiert durch|bloquead[oa](?: por)?|estoy bloquead[oa](?: por)?|blockerad(?: av)?|jag är blockerad(?: av)?)\s*[:,-]?\s*(.+)$/i, prefix: "Blocker: " }
|
|
367
|
+
];
|
|
368
|
+
for (const rule of rules) {
|
|
369
|
+
const match = prompt.trim().match(rule.re);
|
|
370
|
+
if (!match) continue;
|
|
371
|
+
const value = match[1].trim().replace(/\s+/g, " ").replace(/[.!?]+$/, "").slice(0, 220);
|
|
372
|
+
if (value) return { kind: rule.kind, summary: `${rule.prefix}${value}` };
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function captureAttentionLifecycle(input, event, root, scope, catalog) {
|
|
378
|
+
if (!ATTENTION_WRITE_EVENTS.has(event)) return null;
|
|
379
|
+
const explicit = input.agent_spine_attention && typeof input.agent_spine_attention === "object"
|
|
380
|
+
&& !Array.isArray(input.agent_spine_attention) ? input.agent_spine_attention : null;
|
|
381
|
+
let proposed = explicit;
|
|
382
|
+
let automaticHeartbeat = false;
|
|
383
|
+
if (!proposed && event === "UserPromptSubmit" && scope.config.enabled) {
|
|
384
|
+
proposed = minimalAttentionSignal(promptFromInput(input));
|
|
385
|
+
if (proposed && scope.groupId) return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
|
|
386
|
+
}
|
|
387
|
+
if (!proposed && event === "PostToolUse" && scope.currentTaskId) {
|
|
388
|
+
proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "active" };
|
|
389
|
+
automaticHeartbeat = true;
|
|
390
|
+
}
|
|
391
|
+
if (!proposed && ["Stop", "SubagentStop"].includes(event) && scope.currentTaskId) {
|
|
392
|
+
proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "stopped" };
|
|
393
|
+
}
|
|
394
|
+
if (!proposed) return null;
|
|
395
|
+
if (event === "PostToolUse" && ![input.event_id, input.hook_event_id, input.tool_use_id].some((value) => typeof value === "string" && ID_RE.test(value))) {
|
|
396
|
+
throw new Error("PostToolUse attention requires a stable host delivery ID");
|
|
397
|
+
}
|
|
398
|
+
if (!scope.entityId || !scope.projectId || !scope.currentTaskId) {
|
|
399
|
+
if (explicit) throw new Error("lifecycle attention event is missing exact actor, project, or task scope");
|
|
400
|
+
return { event: null, duplicate: false, reason: "missing-exact-scope" };
|
|
401
|
+
}
|
|
402
|
+
const privacy = proposed.privacy || "private";
|
|
403
|
+
if (event === "UserPromptSubmit" && privacy === "group") {
|
|
404
|
+
return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
|
|
405
|
+
}
|
|
406
|
+
const discriminator = proposed.id || `${proposed.kind}:${proposed.status || ""}:${proposed.summary}`;
|
|
407
|
+
return recordAttentionEvent({
|
|
408
|
+
root,
|
|
409
|
+
id: boundedId(proposed.id, "attentionEventId"),
|
|
410
|
+
kind: proposed.kind,
|
|
411
|
+
summary: proposed.summary,
|
|
412
|
+
status: proposed.status || null,
|
|
413
|
+
entityId: scope.entityId,
|
|
414
|
+
groupId: privacy === "group" ? scope.groupId : null,
|
|
415
|
+
projectId: scope.projectId,
|
|
416
|
+
taskId: scope.currentTaskId,
|
|
417
|
+
privacy,
|
|
418
|
+
dueAt: proposed.due_at ?? proposed.dueAt ?? null,
|
|
419
|
+
receiptId: automaticHeartbeat ? heartbeatReceipt(input, scope) : eventReceipt(input, event, scope, discriminator),
|
|
420
|
+
host: scope.host,
|
|
421
|
+
hookEvent: event,
|
|
422
|
+
observedAt: input.timestamp || new Date(), catalog
|
|
423
|
+
});
|
|
424
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FINAL_MESSAGE_KEYS = [
|
|
2
|
+
"last_assistant_message", "assistant_message", "final_assistant_message", "final_message", "response"
|
|
3
|
+
];
|
|
4
|
+
|
|
5
|
+
export function resolveFinalAssistantMessage(input) {
|
|
6
|
+
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
7
|
+
const candidates = [];
|
|
8
|
+
const malformed = [];
|
|
9
|
+
for (const key of FINAL_MESSAGE_KEYS) {
|
|
10
|
+
if (!Object.hasOwn(source, key)) continue;
|
|
11
|
+
if (typeof source[key] === "string") candidates.push({ key, text: source[key] });
|
|
12
|
+
else malformed.push(key);
|
|
13
|
+
}
|
|
14
|
+
if (new Set(candidates.map(({ text }) => text)).size > 1) return {
|
|
15
|
+
status: "conflict", known: false, text: "", aliases: candidates.map(({ key }) => key),
|
|
16
|
+
reason: `final assistant message aliases conflict: ${candidates.map(({ key }) => key).join(", ")}`
|
|
17
|
+
};
|
|
18
|
+
if (malformed.length) return {
|
|
19
|
+
status: "malformed", known: false, text: "", aliases: malformed,
|
|
20
|
+
reason: `final assistant message aliases are non-string: ${malformed.join(", ")}`
|
|
21
|
+
};
|
|
22
|
+
if (candidates.length) return { status: "known", known: true, text: candidates[0].text,
|
|
23
|
+
aliases: candidates.map(({ key }) => key), reason: null };
|
|
24
|
+
return { status: "unavailable", known: false, text: "", aliases: [],
|
|
25
|
+
reason: "final assistant message is unavailable to this hook" };
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_STDIN_BYTES = 64 * 1024;
|
|
2
|
+
|
|
3
|
+
export const SILENT_OVERSIZE_POST_TOOL_USE = Symbol("silent-oversize-post-tool-use");
|
|
4
|
+
export const SILENT_OVERSIZE_POST_TOOL_USE_ARG = "--silent-oversize-post-tool-use";
|
|
5
|
+
|
|
6
|
+
export async function readHookInput({ silentOversizePostToolUse = false } = {}) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let bytes = 0;
|
|
9
|
+
let oversized = false;
|
|
10
|
+
for await (const chunk of process.stdin) {
|
|
11
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
12
|
+
const remaining = Math.max(0, MAX_STDIN_BYTES - bytes);
|
|
13
|
+
if (remaining) chunks.push(buffer.subarray(0, remaining));
|
|
14
|
+
bytes += buffer.length;
|
|
15
|
+
if (bytes > MAX_STDIN_BYTES) oversized = true;
|
|
16
|
+
}
|
|
17
|
+
if (oversized) {
|
|
18
|
+
if (silentOversizePostToolUse) return SILENT_OVERSIZE_POST_TOOL_USE;
|
|
19
|
+
throw new Error("hook input exceeds the 64 KiB limit");
|
|
20
|
+
}
|
|
21
|
+
const value = Buffer.concat(chunks, bytes).toString("utf8");
|
|
22
|
+
const parsed = value.trim() ? JSON.parse(value) : {};
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
24
|
+
throw new Error("hook input must be one JSON object");
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const BLUN_MESSAGE_MAX_BYTES = 1200;
|
|
2
|
+
const BLUN_BOUND_MARKER = "\n[optional runtime detail omitted: 1200-byte bound]";
|
|
3
|
+
|
|
4
|
+
function compactPremortemRegistration(premortem, includeRoot = true) {
|
|
5
|
+
const root = premortem?.registration?.root;
|
|
6
|
+
const requirementId = premortem?.requirementId || premortem?.registration?.requirementId;
|
|
7
|
+
const target = includeRoot && typeof root === "string"
|
|
8
|
+
? ` with root ${JSON.stringify(root)}` : " for the current project";
|
|
9
|
+
return [
|
|
10
|
+
"Recommended delivery preparation (advisory; does not block coding or replies):",
|
|
11
|
+
`session_briefing, delivery_knowledge_query, then record_delivery_premortem${target}.`,
|
|
12
|
+
`Requirement: ${requirementId || "<unavailable; continue the authorized task>"}.`,
|
|
13
|
+
"Only stored call receipts from this session and goal step count; the calls grant no authority.",
|
|
14
|
+
"Completion: Premortem closure sha256 <64hex>, latest write digest, and all three check IDs with results."
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function blunRuntimeContext(context) {
|
|
19
|
+
const detailed = JSON.parse(context);
|
|
20
|
+
const sourceResolution = detailed.sourceResolution ? {
|
|
21
|
+
status: detailed.sourceResolution.status || null,
|
|
22
|
+
reason: detailed.sourceResolution.reason || null,
|
|
23
|
+
...(detailed.sourceResolution.incomplete ? {
|
|
24
|
+
incomplete: true,
|
|
25
|
+
warning: detailed.sourceResolution.warning || "Host-native source context is incomplete."
|
|
26
|
+
} : {})
|
|
27
|
+
} : null;
|
|
28
|
+
const runtime = {
|
|
29
|
+
schema: "agentspine.blun-runtime-context/v1",
|
|
30
|
+
event: detailed.event,
|
|
31
|
+
loaded: Boolean(detailed.loaded),
|
|
32
|
+
failedClosed: detailed.failedClosed ? true : undefined,
|
|
33
|
+
indexedSources: detailed.indexedSources || 0,
|
|
34
|
+
sourceResolution,
|
|
35
|
+
instruction: detailed.loaded
|
|
36
|
+
? "Detailed AgentSpine context is available on demand through session_briefing. Load it only when the current request needs continuity."
|
|
37
|
+
: detailed.instruction,
|
|
38
|
+
authority: "context-only"
|
|
39
|
+
};
|
|
40
|
+
if (detailed.signal && (detailed.signal.captured || detailed.signal.accepted || detailed.signal.reason)) {
|
|
41
|
+
runtime.signal = detailed.signal;
|
|
42
|
+
}
|
|
43
|
+
if (detailed.attentionEvent
|
|
44
|
+
&& (detailed.attentionEvent.captured || detailed.attentionEvent.duplicate || detailed.attentionEvent.reason)) {
|
|
45
|
+
runtime.attentionEvent = detailed.attentionEvent;
|
|
46
|
+
}
|
|
47
|
+
if (detailed.selfstarter && (detailed.selfstarter.active || detailed.selfstarter.blocked)) {
|
|
48
|
+
runtime.selfstarter = detailed.selfstarter;
|
|
49
|
+
}
|
|
50
|
+
if (detailed.channelEvent?.active) runtime.channelEvent = detailed.channelEvent;
|
|
51
|
+
if (detailed.preflight?.premortem) runtime.premortem = detailed.preflight.premortem;
|
|
52
|
+
return JSON.stringify(runtime);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function blunRuntimeMessage(context) {
|
|
56
|
+
const runtime = JSON.parse(blunRuntimeContext(context));
|
|
57
|
+
const warning = runtime.sourceResolution?.incomplete ? ` Warning: ${runtime.sourceResolution.warning}` : "";
|
|
58
|
+
const base = runtime.loaded
|
|
59
|
+
? `AgentSpine ready: ${runtime.indexedSources} sources indexed. Load detailed continuity only on demand through session_briefing.${warning}`
|
|
60
|
+
: `AgentSpine unavailable${runtime.sourceResolution?.reason ? `: ${runtime.sourceResolution.reason}` : ""}. ${runtime.instruction}`;
|
|
61
|
+
const active = {};
|
|
62
|
+
if (runtime.signal && (runtime.signal.captured || runtime.signal.accepted
|
|
63
|
+
|| String(runtime.signal.reason || "").startsWith("rejected:"))) {
|
|
64
|
+
active.signal = runtime.signal;
|
|
65
|
+
}
|
|
66
|
+
if (runtime.attentionEvent && (runtime.attentionEvent.captured || runtime.attentionEvent.duplicate
|
|
67
|
+
|| String(runtime.attentionEvent.reason || "").startsWith("rejected:"))) {
|
|
68
|
+
active.attentionEvent = runtime.attentionEvent;
|
|
69
|
+
}
|
|
70
|
+
if (runtime.selfstarter) active.selfstarter = runtime.selfstarter;
|
|
71
|
+
if (runtime.channelEvent) active.channelEvent = runtime.channelEvent;
|
|
72
|
+
const details = Object.keys(active).length === 0
|
|
73
|
+
? ""
|
|
74
|
+
: `\nActive AgentSpine runtime data: ${JSON.stringify(active)}`;
|
|
75
|
+
const root = runtime.premortem?.registration?.root;
|
|
76
|
+
const instruction = typeof root === "string"
|
|
77
|
+
? runtime.premortem?.instruction?.replace("registration.root", () => `root ${JSON.stringify(root)}`)
|
|
78
|
+
: runtime.premortem?.instruction;
|
|
79
|
+
const premortem = instruction ? `\n${instruction}` : "";
|
|
80
|
+
const message = `${base}${details}${premortem}`;
|
|
81
|
+
if (Buffer.byteLength(message) <= BLUN_MESSAGE_MAX_BYTES) return message;
|
|
82
|
+
const compact = `${base}${BLUN_BOUND_MARKER}\n${compactPremortemRegistration(runtime.premortem)}`;
|
|
83
|
+
if (Buffer.byteLength(compact) <= BLUN_MESSAGE_MAX_BYTES) return compact;
|
|
84
|
+
return `${base}${BLUN_BOUND_MARKER}\n${compactPremortemRegistration(runtime.premortem, false)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function hookOutput(event, context, env = process.env) {
|
|
88
|
+
if (env.BLUN_PLUGIN_ROOT) {
|
|
89
|
+
return { hookSpecificOutput: { hookEventName: event, message: blunRuntimeMessage(context) } };
|
|
90
|
+
}
|
|
91
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function blockedHookOutput(event, reason, env = process.env) {
|
|
95
|
+
const output = { decision: "block", reason };
|
|
96
|
+
if (!env.CLAUDE_PLUGIN_ROOT || env.PLUGIN_ROOT) return output;
|
|
97
|
+
if (event === "PreToolUse") return { hookSpecificOutput: {
|
|
98
|
+
hookEventName: event, permissionDecision: "deny", permissionDecisionReason: reason
|
|
99
|
+
} };
|
|
100
|
+
return output;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function denyTool(reason) {
|
|
104
|
+
process.stdout.write(`${JSON.stringify(blockedHookOutput("PreToolUse", reason))}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function blockPrompt(reason) {
|
|
108
|
+
process.stdout.write(`${JSON.stringify(blockedHookOutput("UserPromptSubmit", reason))}\n`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function blockStop(event, reason) {
|
|
112
|
+
process.stdout.write(`${JSON.stringify(blockedHookOutput(event, reason))}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function lifecycleOutput(event, artifactGuard, premortem, deliveryVerification = null, env = process.env,
|
|
116
|
+
sourceWarning = null, lessonRecall = null) {
|
|
117
|
+
const messages = sourceWarning ? [`AgentSpine source warning: ${sourceWarning}`] : [];
|
|
118
|
+
if (artifactGuard?.reason) messages.push(artifactGuard.reason);
|
|
119
|
+
if (deliveryVerification?.status === "test-failed" && deliveryVerification.reason) {
|
|
120
|
+
messages.push(deliveryVerification.reason);
|
|
121
|
+
}
|
|
122
|
+
if (/^[a-f0-9]{64}$/.test(premortem?.writeDigest || "")) {
|
|
123
|
+
messages.push([
|
|
124
|
+
premortem.writeIntent
|
|
125
|
+
? "AgentSpine recorded the allowed mutation intent for the delivery premortem."
|
|
126
|
+
: "AgentSpine recorded the direct write for the delivery premortem.",
|
|
127
|
+
`Premortem latest write sha256 ${premortem.writeDigest}`
|
|
128
|
+
].join("\n"));
|
|
129
|
+
}
|
|
130
|
+
if (lessonRecall?.status === "recalled") {
|
|
131
|
+
messages.push(JSON.stringify({
|
|
132
|
+
schema: lessonRecall.schema, receiptDigest: lessonRecall.receiptDigest,
|
|
133
|
+
instruction: lessonRecall.instruction, items: lessonRecall.items, omitted: lessonRecall.omitted,
|
|
134
|
+
authority: "context-only"
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
if (!messages.length) return {};
|
|
138
|
+
const field = env.BLUN_PLUGIN_ROOT ? "message" : "additionalContext";
|
|
139
|
+
return { hookSpecificOutput: { hookEventName: event, [field]: messages.join("\n") } };
|
|
140
|
+
}
|