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,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTIONS, HOSTS, appendReceipt, blocker, capabilities, currentTaskValid, emptyJobs, emptyPolicy,
|
|
3
|
+
exactGrant, executionPolicyFindingsInternal, integer, leaseExpired, lockedStates, normalizeJobs,
|
|
4
|
+
normalizePolicy, pathsFor, preserveJob, readJson, receiptDigest, referenceState,
|
|
5
|
+
requireOwnerConfirmation, resultHash, safeText, selfstarterFindingsInternal, stableId, timestamp,
|
|
6
|
+
validateGrantReferences, withLock
|
|
7
|
+
} from "./selfstarter-core.js";
|
|
8
|
+
import { workspaceFingerprint } from "./selfstarter-workspace.js";
|
|
9
|
+
|
|
10
|
+
export async function resolveSessionJob({
|
|
11
|
+
root = process.cwd(), actorId = null, projectId = null, groupId = null, taskId = null,
|
|
12
|
+
host, sessionId, action = "effect", now = new Date()
|
|
13
|
+
}) {
|
|
14
|
+
if (!ACTIONS.has(action)) throw new Error("session job resolution requires start, resume, or effect");
|
|
15
|
+
if (!HOSTS.has(host)) throw new Error("self-starter host must be claude or codex");
|
|
16
|
+
const session = stableId(sessionId, "sessionId");
|
|
17
|
+
const at = timestamp(now, "now");
|
|
18
|
+
const paths = await pathsFor(root);
|
|
19
|
+
const [policy, state] = await Promise.all([
|
|
20
|
+
readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
|
|
21
|
+
readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs)
|
|
22
|
+
]);
|
|
23
|
+
const { graph, coordination } = await referenceState(paths.catalog);
|
|
24
|
+
const findings = [
|
|
25
|
+
...executionPolicyFindingsInternal(policy, graph, coordination, { checkCurrentReferences: false }),
|
|
26
|
+
...selfstarterFindingsInternal(state, policy, graph, coordination)
|
|
27
|
+
];
|
|
28
|
+
if (findings.length) throw new Error(`self-starter failed closed: ${findings.join(", ")}`);
|
|
29
|
+
const candidates = state.jobs.filter((job) => job.lease?.sessionId === session);
|
|
30
|
+
if (!candidates.length) return null;
|
|
31
|
+
if (candidates.length !== 1) throw new Error("host session has multiple self-starter leases");
|
|
32
|
+
const job = candidates[0];
|
|
33
|
+
if (leaseExpired(job, at)) throw new Error(`job ${job.id} lease expired`);
|
|
34
|
+
for (const [value, expected, label] of [
|
|
35
|
+
[actorId, job.actorId, "actor"], [projectId, job.projectId, "project"],
|
|
36
|
+
[taskId, job.taskId, "task"], [groupId, job.groupId, "group"], [host, job.host, "host"]
|
|
37
|
+
]) {
|
|
38
|
+
if (value !== null && value !== undefined && value !== expected) throw new Error(`self-starter ${label} scope mismatch for ${job.id}`);
|
|
39
|
+
}
|
|
40
|
+
exactGrant(policy, job, action, at);
|
|
41
|
+
if (!currentTaskValid(job, coordination)) throw new Error("self-starter task or assignment changed");
|
|
42
|
+
return {
|
|
43
|
+
jobId: job.id, actorId: job.actorId, projectId: job.projectId, groupId: job.groupId,
|
|
44
|
+
taskId: job.taskId, host: job.host, sessionId: session
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function registerJob({
|
|
49
|
+
root = process.cwd(), id, grantId, maxRetries = 3, leaseSeconds = 120,
|
|
50
|
+
baseRetrySeconds = 5, confirmation, now = new Date()
|
|
51
|
+
}) {
|
|
52
|
+
requireOwnerConfirmation(confirmation);
|
|
53
|
+
const jobId = stableId(id, "jobId");
|
|
54
|
+
const at = timestamp(now, "now");
|
|
55
|
+
const paths = await pathsFor(root);
|
|
56
|
+
const fingerprint = await workspaceFingerprint(paths.catalog.root);
|
|
57
|
+
const { graph, coordination } = await referenceState(paths.catalog);
|
|
58
|
+
return withLock(paths.executionPolicyPath, () => readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy), (policy) => withLock(
|
|
59
|
+
paths.selfstarterPath,
|
|
60
|
+
() => readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs),
|
|
61
|
+
(state) => {
|
|
62
|
+
const policyIssues = executionPolicyFindingsInternal(policy, graph, coordination);
|
|
63
|
+
const stateIssues = selfstarterFindingsInternal(state, policy, graph, coordination);
|
|
64
|
+
if (policyIssues.length || stateIssues.length) throw new Error(`self-starter registration failed closed: ${[...policyIssues, ...stateIssues].join(", ")}`);
|
|
65
|
+
const grant = policy.grants.find((item) => item.id === grantId && item.jobId === jobId);
|
|
66
|
+
if (!grant) throw new Error("registration requires the exact current execution grant");
|
|
67
|
+
if (state.jobs.some((job) => job.id === jobId)) throw new Error("job IDs are immutable");
|
|
68
|
+
const template = {
|
|
69
|
+
id: jobId, taskId: grant.taskId, actorId: grant.actorId, targetId: grant.targetId,
|
|
70
|
+
projectId: grant.projectId, groupId: grant.groupId, host: grant.host,
|
|
71
|
+
capabilities: [...grant.capabilities], grantId: grant.id
|
|
72
|
+
};
|
|
73
|
+
exactGrant(policy, template, "start", at);
|
|
74
|
+
if (!validateGrantReferences(grant, graph, coordination)) throw new Error("execution grant no longer matches the current task");
|
|
75
|
+
const job = {
|
|
76
|
+
...template, status: "waiting",
|
|
77
|
+
checkpoint: {
|
|
78
|
+
sequence: 0, workspaceDigest: fingerprint.digest, workspaceFiles: fingerprint.files,
|
|
79
|
+
workspaceBytes: fingerprint.bytes, lastToolUseId: null, resultDigest: null,
|
|
80
|
+
retryCount: 0, nextRetryAt: null, updatedAt: at
|
|
81
|
+
},
|
|
82
|
+
lease: null, pendingEffect: null,
|
|
83
|
+
maxRetries: integer(maxRetries, "maxRetries", 0, 10),
|
|
84
|
+
leaseSeconds: integer(leaseSeconds, "leaseSeconds", 15, 900),
|
|
85
|
+
baseRetrySeconds: integer(baseRetrySeconds, "baseRetrySeconds", 1, 3600),
|
|
86
|
+
lastBlocker: null, createdAt: at, updatedAt: at, authority: "execution-state-only"
|
|
87
|
+
};
|
|
88
|
+
state.jobs.push(job);
|
|
89
|
+
state.jobs.sort((a, b) => a.id.localeCompare(b.id));
|
|
90
|
+
appendReceipt(state, job, `receipt:registered:${receiptDigest({ id: job.id, at }).slice(0, 20)}`, "registered", at, { checkpoint: job.checkpoint.workspaceDigest });
|
|
91
|
+
return { job, selfstarterPath: paths.selfstarterPath, executionPolicyPath: paths.executionPolicyPath };
|
|
92
|
+
}
|
|
93
|
+
), false);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function cancelJob({ root = process.cwd(), id, reason, confirmation, now = new Date() }) {
|
|
97
|
+
requireOwnerConfirmation(confirmation);
|
|
98
|
+
stableId(id, "jobId");
|
|
99
|
+
const at = timestamp(now, "now");
|
|
100
|
+
const cancelReason = safeText(reason, "reason", 500);
|
|
101
|
+
return lockedStates(root, ({ state, paths }) => {
|
|
102
|
+
const job = state.jobs.find((item) => item.id === id);
|
|
103
|
+
if (!job) throw new Error("unknown self-starter job");
|
|
104
|
+
if (["completed", "cancelled"].includes(job.status)) throw new Error("self-starter job is already terminal");
|
|
105
|
+
preserveJob(state, job, "cancelled", at);
|
|
106
|
+
job.status = "cancelled";
|
|
107
|
+
job.lease = null;
|
|
108
|
+
job.pendingEffect = null;
|
|
109
|
+
job.lastBlocker = { code: "owner-cancelled", at, authority: "execution-state-only" };
|
|
110
|
+
job.cancelReason = cancelReason;
|
|
111
|
+
job.updatedAt = at;
|
|
112
|
+
appendReceipt(state, job, `receipt:cancel:${receiptDigest({ id, at }).slice(0, 20)}`, "cancelled", at, { reasonDigest: resultHash(cancelReason) });
|
|
113
|
+
return { job, selfstarterPath: paths.selfstarterPath };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function deleteJob({ root = process.cwd(), id, confirmation }) {
|
|
118
|
+
requireOwnerConfirmation(confirmation);
|
|
119
|
+
stableId(id, "jobId");
|
|
120
|
+
return lockedStates(root, ({ state, paths }) => {
|
|
121
|
+
const job = state.jobs.find((item) => item.id === id);
|
|
122
|
+
if (job?.lease || job?.pendingEffect) throw new Error("cannot delete a leased or uncheckpointed job");
|
|
123
|
+
const existed = Boolean(job);
|
|
124
|
+
state.jobs = state.jobs.filter((item) => item.id !== id);
|
|
125
|
+
state.history = state.history.filter((entry) => entry.recordId !== id && entry.value?.id !== id);
|
|
126
|
+
state.receipts = state.receipts.filter((receipt) => receipt.jobId !== id);
|
|
127
|
+
return { deleted: existed, id, selfstarterPath: paths.selfstarterPath };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function selfstarterContext({ root = process.cwd(), actorId = null, projectId = null, taskId = null, includeTerminal = false } = {}) {
|
|
132
|
+
const paths = await pathsFor(root);
|
|
133
|
+
const [policy, state] = await Promise.all([
|
|
134
|
+
readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
|
|
135
|
+
readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs)
|
|
136
|
+
]);
|
|
137
|
+
const { graph, coordination } = await referenceState(paths.catalog);
|
|
138
|
+
const findings = [...executionPolicyFindingsInternal(policy, graph, coordination), ...selfstarterFindingsInternal(state, policy, graph, coordination)];
|
|
139
|
+
if (findings.length) throw new Error(`self-starter failed closed: ${findings.join(", ")}`);
|
|
140
|
+
const items = state.jobs.filter((job) => (!actorId || job.actorId === actorId) && (!projectId || job.projectId === projectId)
|
|
141
|
+
&& (!taskId || job.taskId === taskId) && (includeTerminal || !["completed", "cancelled"].includes(job.status)))
|
|
142
|
+
.map((job) => ({
|
|
143
|
+
id: job.id, taskId: job.taskId, actorId: job.actorId, targetId: job.targetId,
|
|
144
|
+
projectId: job.projectId, groupId: job.groupId, host: job.host, status: job.status,
|
|
145
|
+
checkpoint: { ...job.checkpoint }, leased: Boolean(job.lease), pendingEffect: Boolean(job.pendingEffect),
|
|
146
|
+
lastBlocker: job.lastBlocker, updatedAt: job.updatedAt, authority: "execution-state-only"
|
|
147
|
+
}));
|
|
148
|
+
return { schema: "agentspine.selfstarter-context/v1", root: paths.catalog.root, items, authority: "execution-state-only" };
|
|
149
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOSTS, appendReceipt, blocker, currentTaskValid, ensureScope, exactGrant, leaseExpired,
|
|
3
|
+
lockedStates, preserveJob, receiptDigest, resultHash, stableId, timestamp
|
|
4
|
+
} from "./selfstarter-core.js";
|
|
5
|
+
import { incompleteWorkspaceScan, workspaceFingerprint } from "./selfstarter-workspace.js";
|
|
6
|
+
|
|
7
|
+
export async function startOrResumeJob({ root = process.cwd(), actorId, projectId, groupId = null, taskId = null, jobId = null, host, sessionId, now = new Date() }) {
|
|
8
|
+
const scope = {
|
|
9
|
+
actorId: stableId(actorId, "actorId"), projectId: stableId(projectId, "projectId"),
|
|
10
|
+
groupId: stableId(groupId, "groupId", true), taskId: stableId(taskId, "taskId", true),
|
|
11
|
+
jobId: stableId(jobId, "jobId", true), host, sessionId: stableId(sessionId, "sessionId")
|
|
12
|
+
};
|
|
13
|
+
if (!HOSTS.has(host)) throw new Error("self-starter host must be claude or codex");
|
|
14
|
+
const at = timestamp(now, "now");
|
|
15
|
+
const fingerprint = await workspaceFingerprint(root);
|
|
16
|
+
return lockedStates(root, ({ policy, state, coordination, paths }) => {
|
|
17
|
+
const candidates = state.jobs.filter((job) => !["completed", "cancelled", "exhausted"].includes(job.status)
|
|
18
|
+
&& job.actorId === scope.actorId && job.projectId === scope.projectId && job.groupId === scope.groupId
|
|
19
|
+
&& job.host === host && (!scope.taskId || job.taskId === scope.taskId) && (!scope.jobId || job.id === scope.jobId));
|
|
20
|
+
if (!candidates.length) return { job: null, reason: "no-exact-waiting-job", selfstarterPath: paths.selfstarterPath };
|
|
21
|
+
if (candidates.length !== 1) throw new Error("multiple eligible self-starter jobs require an exact job or task scope");
|
|
22
|
+
const job = candidates[0];
|
|
23
|
+
ensureScope(job, scope);
|
|
24
|
+
if (!currentTaskValid(job, coordination)) {
|
|
25
|
+
preserveJob(state, job, "task-invalid", at);
|
|
26
|
+
blocker(job, "task-or-assignment-changed", at);
|
|
27
|
+
appendReceipt(state, job, `receipt:blocked:${receiptDigest({ id: job.id, at, code: job.lastBlocker.code }).slice(0, 20)}`, "blocked", at, { code: job.lastBlocker.code });
|
|
28
|
+
return { job: null, blocked: true, reason: job.lastBlocker.code, selfstarterPath: paths.selfstarterPath };
|
|
29
|
+
}
|
|
30
|
+
if (job.lease && !leaseExpired(job, at) && job.lease.sessionId !== scope.sessionId) {
|
|
31
|
+
throw new Error(`job ${job.id} has an active lease`);
|
|
32
|
+
}
|
|
33
|
+
const hasPriorRun = Boolean(job.lease) || state.receipts.some((receipt) => receipt.jobId === job.id && ["start", "resume"].includes(receipt.event));
|
|
34
|
+
const action = job.checkpoint.sequence === 0 && !hasPriorRun ? "start" : "resume";
|
|
35
|
+
exactGrant(policy, job, action, at);
|
|
36
|
+
if (job.checkpoint.nextRetryAt && new Date(job.checkpoint.nextRetryAt) > new Date(at)) {
|
|
37
|
+
return { job: null, blocked: true, reason: "retry-backoff-active", nextRetryAt: job.checkpoint.nextRetryAt, selfstarterPath: paths.selfstarterPath };
|
|
38
|
+
}
|
|
39
|
+
if (job.status === "blocked" && job.lastBlocker?.code === "host-reported-blocker") {
|
|
40
|
+
return { job: null, blocked: true, reason: "host-reported-blocker", selfstarterPath: paths.selfstarterPath };
|
|
41
|
+
}
|
|
42
|
+
if (job.pendingEffect && leaseExpired(job, at)) {
|
|
43
|
+
if (fingerprint.digest !== job.pendingEffect.beforeDigest) {
|
|
44
|
+
preserveJob(state, job, "crash-workspace-unknown", at);
|
|
45
|
+
blocker(job, "workspace-changed-after-uncheckpointed-effect", at);
|
|
46
|
+
appendReceipt(state, job, `receipt:blocked:${receiptDigest({ id: job.id, at, code: job.lastBlocker.code }).slice(0, 20)}`, "blocked", at, { code: job.lastBlocker.code });
|
|
47
|
+
return { job: null, blocked: true, reason: job.lastBlocker.code, selfstarterPath: paths.selfstarterPath };
|
|
48
|
+
}
|
|
49
|
+
preserveJob(state, job, "crash-recovered", at);
|
|
50
|
+
job.pendingEffect = null;
|
|
51
|
+
job.checkpoint.retryCount += 1;
|
|
52
|
+
if (job.checkpoint.retryCount > job.maxRetries) {
|
|
53
|
+
blocker(job, "retry-budget-exhausted", at);
|
|
54
|
+
return { job: null, blocked: true, reason: job.lastBlocker.code, selfstarterPath: paths.selfstarterPath };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (fingerprint.digest !== job.checkpoint.workspaceDigest) {
|
|
58
|
+
preserveJob(state, job, "workspace-drift", at);
|
|
59
|
+
blocker(job, "workspace-changed-outside-checkpoint", at);
|
|
60
|
+
appendReceipt(state, job, `receipt:blocked:${receiptDigest({ id: job.id, at, code: job.lastBlocker.code }).slice(0, 20)}`, "blocked", at, { code: job.lastBlocker.code });
|
|
61
|
+
return { job: null, blocked: true, reason: job.lastBlocker.code, selfstarterPath: paths.selfstarterPath };
|
|
62
|
+
}
|
|
63
|
+
const receiptId = `receipt:${action}:${receiptDigest({ job: job.id, session: scope.sessionId }).slice(0, 20)}`;
|
|
64
|
+
const existing = state.receipts.find((receipt) => receipt.id === receiptId);
|
|
65
|
+
if (existing && job.lease?.sessionId === scope.sessionId) return { job, action, duplicate: true, receipt: existing, selfstarterPath: paths.selfstarterPath };
|
|
66
|
+
if (existing) return {
|
|
67
|
+
job: null, action, duplicate: true, blocked: true, reason: "session-lifecycle-already-closed",
|
|
68
|
+
receipt: existing, selfstarterPath: paths.selfstarterPath
|
|
69
|
+
};
|
|
70
|
+
preserveJob(state, job, action, at);
|
|
71
|
+
job.status = "running";
|
|
72
|
+
job.lastBlocker = null;
|
|
73
|
+
job.lease = {
|
|
74
|
+
sessionId: scope.sessionId, host, acquiredAt: at,
|
|
75
|
+
expiresAt: new Date(new Date(at).getTime() + job.leaseSeconds * 1000).toISOString(),
|
|
76
|
+
workspaceDigest: fingerprint.digest
|
|
77
|
+
};
|
|
78
|
+
job.updatedAt = at;
|
|
79
|
+
const { receipt } = appendReceipt(state, job, receiptId, action, at, { sessionId: scope.sessionId, workspaceDigest: fingerprint.digest });
|
|
80
|
+
return { job, action, duplicate: false, receipt, selfstarterPath: paths.selfstarterPath, authority: "explicit-local-execution-policy" };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function authorizeJobEffect({
|
|
85
|
+
root = process.cwd(), jobId, actorId, projectId, groupId = null, taskId,
|
|
86
|
+
host, sessionId, toolName, toolUseId, now = new Date()
|
|
87
|
+
}) {
|
|
88
|
+
const scope = {
|
|
89
|
+
jobId: stableId(jobId, "jobId"), actorId: stableId(actorId, "actorId"),
|
|
90
|
+
projectId: stableId(projectId, "projectId"), groupId: stableId(groupId, "groupId", true),
|
|
91
|
+
taskId: stableId(taskId, "taskId"), host, sessionId: stableId(sessionId, "sessionId")
|
|
92
|
+
};
|
|
93
|
+
const capability = `tool:${stableId(toolName, "toolName")}`;
|
|
94
|
+
const deliveryId = stableId(toolUseId, "toolUseId");
|
|
95
|
+
const at = timestamp(now, "now");
|
|
96
|
+
const fingerprint = await workspaceFingerprint(root);
|
|
97
|
+
if (fingerprint.skipped.length) throw incompleteWorkspaceScan(fingerprint.skipped);
|
|
98
|
+
return lockedStates(root, ({ policy, state, coordination, paths }) => {
|
|
99
|
+
const job = state.jobs.find((item) => item.id === scope.jobId);
|
|
100
|
+
if (!job) throw new Error("unknown self-starter job");
|
|
101
|
+
ensureScope(job, scope);
|
|
102
|
+
exactGrant(policy, job, "effect", at);
|
|
103
|
+
if (!currentTaskValid(job, coordination)) throw new Error("self-starter task or assignment changed");
|
|
104
|
+
if (job.status !== "running" || !job.lease || leaseExpired(job, at) || job.lease.sessionId !== scope.sessionId) throw new Error("self-starter effect requires the current live lease");
|
|
105
|
+
if (!job.capabilities.includes(capability)) throw new Error(`default deny: capability ${capability} is not granted for ${job.id}`);
|
|
106
|
+
if (fingerprint.digest !== job.checkpoint.workspaceDigest || fingerprint.digest !== job.lease.workspaceDigest) throw new Error("workspace changed outside the current checkpoint");
|
|
107
|
+
if (job.pendingEffect) {
|
|
108
|
+
if (job.pendingEffect.toolUseId === deliveryId && job.pendingEffect.capability === capability) return { allowed: true, duplicate: true, job, pendingEffect: job.pendingEffect };
|
|
109
|
+
throw new Error("another self-starter effect is already pending");
|
|
110
|
+
}
|
|
111
|
+
preserveJob(state, job, "effect-authorized", at);
|
|
112
|
+
job.pendingEffect = { toolUseId: deliveryId, capability, beforeDigest: fingerprint.digest, authorizedAt: at, sessionId: scope.sessionId };
|
|
113
|
+
job.updatedAt = at;
|
|
114
|
+
const receiptId = `receipt:authorize:${receiptDigest({ job: job.id, toolUseId: deliveryId, capability }).slice(0, 20)}`;
|
|
115
|
+
const { receipt } = appendReceipt(state, job, receiptId, "effect-authorized", at, { toolUseId: deliveryId, capability, beforeDigest: fingerprint.digest });
|
|
116
|
+
return { allowed: true, duplicate: false, job, pendingEffect: job.pendingEffect, receipt, selfstarterPath: paths.selfstarterPath };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function checkpointJobEffect({
|
|
121
|
+
root = process.cwd(), jobId, actorId, projectId, groupId = null, taskId,
|
|
122
|
+
host, sessionId, toolName, toolUseId, success = true, result = null, now = new Date()
|
|
123
|
+
}) {
|
|
124
|
+
const scope = {
|
|
125
|
+
jobId: stableId(jobId, "jobId"), actorId: stableId(actorId, "actorId"),
|
|
126
|
+
projectId: stableId(projectId, "projectId"), groupId: stableId(groupId, "groupId", true),
|
|
127
|
+
taskId: stableId(taskId, "taskId"), host, sessionId: stableId(sessionId, "sessionId")
|
|
128
|
+
};
|
|
129
|
+
const capability = `tool:${stableId(toolName, "toolName")}`;
|
|
130
|
+
const deliveryId = stableId(toolUseId, "toolUseId");
|
|
131
|
+
const at = timestamp(now, "now");
|
|
132
|
+
const fingerprint = await workspaceFingerprint(root);
|
|
133
|
+
return lockedStates(root, ({ policy, state, coordination, paths }) => {
|
|
134
|
+
const job = state.jobs.find((item) => item.id === scope.jobId);
|
|
135
|
+
if (!job) throw new Error("unknown self-starter job");
|
|
136
|
+
ensureScope(job, scope);
|
|
137
|
+
exactGrant(policy, job, "effect", at);
|
|
138
|
+
if (!currentTaskValid(job, coordination)) throw new Error("self-starter task or assignment changed");
|
|
139
|
+
const receiptId = `receipt:checkpoint:${receiptDigest({ job: job.id, toolUseId: deliveryId }).slice(0, 20)}`;
|
|
140
|
+
const existing = state.receipts.find((receipt) => receipt.id === receiptId);
|
|
141
|
+
if (existing && job.checkpoint.lastToolUseId === deliveryId) return { job, duplicate: true, receipt: existing, selfstarterPath: paths.selfstarterPath };
|
|
142
|
+
if (!job.lease || job.lease.sessionId !== scope.sessionId || !job.pendingEffect
|
|
143
|
+
|| job.pendingEffect.toolUseId !== deliveryId || job.pendingEffect.capability !== capability) throw new Error("effect checkpoint does not match the pending authorization");
|
|
144
|
+
preserveJob(state, job, success ? "effect-succeeded" : "effect-failed", at);
|
|
145
|
+
job.checkpoint.sequence += 1;
|
|
146
|
+
job.checkpoint.workspaceDigest = fingerprint.digest;
|
|
147
|
+
job.checkpoint.workspaceFiles = fingerprint.files;
|
|
148
|
+
job.checkpoint.workspaceBytes = fingerprint.bytes;
|
|
149
|
+
job.checkpoint.lastToolUseId = deliveryId;
|
|
150
|
+
job.checkpoint.resultDigest = resultHash(result);
|
|
151
|
+
job.checkpoint.updatedAt = at;
|
|
152
|
+
if (success) {
|
|
153
|
+
job.checkpoint.retryCount = 0;
|
|
154
|
+
job.checkpoint.nextRetryAt = null;
|
|
155
|
+
job.lastBlocker = null;
|
|
156
|
+
job.lease.workspaceDigest = fingerprint.digest;
|
|
157
|
+
job.lease.expiresAt = new Date(new Date(at).getTime() + job.leaseSeconds * 1000).toISOString();
|
|
158
|
+
} else {
|
|
159
|
+
job.checkpoint.retryCount += 1;
|
|
160
|
+
if (job.checkpoint.retryCount > job.maxRetries) blocker(job, "retry-budget-exhausted", at);
|
|
161
|
+
else {
|
|
162
|
+
const seconds = Math.min(3600, job.baseRetrySeconds * (2 ** (job.checkpoint.retryCount - 1)));
|
|
163
|
+
job.checkpoint.nextRetryAt = new Date(new Date(at).getTime() + seconds * 1000).toISOString();
|
|
164
|
+
blocker(job, "effect-failed", at);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
job.pendingEffect = null;
|
|
168
|
+
job.updatedAt = at;
|
|
169
|
+
const { receipt } = appendReceipt(state, job, receiptId, success ? "effect-succeeded" : "effect-failed", at, {
|
|
170
|
+
toolUseId: deliveryId, capability, checkpointSequence: job.checkpoint.sequence,
|
|
171
|
+
workspaceDigest: fingerprint.digest, resultDigest: job.checkpoint.resultDigest, retryCount: job.checkpoint.retryCount
|
|
172
|
+
});
|
|
173
|
+
return { job, duplicate: false, receipt, selfstarterPath: paths.selfstarterPath };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function closeJobLease({ root = process.cwd(), jobId, actorId, projectId, groupId = null, taskId, host, sessionId, status = "waiting", now = new Date() }) {
|
|
178
|
+
if (!["waiting", "blocked", "completed"].includes(status)) throw new Error("closing status must be waiting, blocked, or completed");
|
|
179
|
+
const scope = {
|
|
180
|
+
jobId: stableId(jobId, "jobId"), actorId: stableId(actorId, "actorId"),
|
|
181
|
+
projectId: stableId(projectId, "projectId"), groupId: stableId(groupId, "groupId", true),
|
|
182
|
+
taskId: stableId(taskId, "taskId"), host, sessionId: stableId(sessionId, "sessionId")
|
|
183
|
+
};
|
|
184
|
+
const at = timestamp(now, "now");
|
|
185
|
+
return lockedStates(root, ({ policy, state, coordination, paths }) => {
|
|
186
|
+
const job = state.jobs.find((item) => item.id === scope.jobId);
|
|
187
|
+
if (!job) throw new Error("unknown self-starter job");
|
|
188
|
+
ensureScope(job, scope);
|
|
189
|
+
exactGrant(policy, job, "resume", at);
|
|
190
|
+
if (!currentTaskValid(job, coordination) && status !== "completed") throw new Error("self-starter task or assignment changed");
|
|
191
|
+
const receiptId = `receipt:close:${receiptDigest({ job: job.id, session: scope.sessionId, status }).slice(0, 20)}`;
|
|
192
|
+
const existing = state.receipts.find((receipt) => receipt.id === receiptId);
|
|
193
|
+
if (existing && !job.lease) return { job, duplicate: true, receipt: existing, selfstarterPath: paths.selfstarterPath };
|
|
194
|
+
if (!job.lease || job.lease.sessionId !== scope.sessionId) throw new Error("only the current lease holder may close a job");
|
|
195
|
+
if (job.pendingEffect) throw new Error("cannot close a job with an uncheckpointed effect");
|
|
196
|
+
preserveJob(state, job, "lease-closed", at);
|
|
197
|
+
job.status = status;
|
|
198
|
+
job.lease = null;
|
|
199
|
+
if (status === "blocked") job.lastBlocker = { code: "host-reported-blocker", at, authority: "execution-state-only" };
|
|
200
|
+
job.updatedAt = at;
|
|
201
|
+
const { receipt } = appendReceipt(state, job, receiptId, "lease-closed", at, { sessionId: scope.sessionId, status, checkpointSequence: job.checkpoint.sequence });
|
|
202
|
+
return { job, duplicate: false, receipt, selfstarterPath: paths.selfstarterPath };
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ACTIONS, HOSTS, capabilities, emptyJobs, emptyPolicy, executionPolicyFindingsInternal,
|
|
4
|
+
normalizeJobs, normalizePolicy, pathsFor, preservePolicy, readJson, referenceState,
|
|
5
|
+
requireOwnerConfirmation, safeText, selfstarterFindingsInternal, stableId, timestamp,
|
|
6
|
+
validateGrantReferences, withLock
|
|
7
|
+
} from "./selfstarter-core.js";
|
|
8
|
+
|
|
9
|
+
export function executionPolicyFindings(policy, graph, coordination) {
|
|
10
|
+
return executionPolicyFindingsInternal(policy, graph, coordination);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function selfstarterFindings(state, policy, graph, coordination) {
|
|
14
|
+
return selfstarterFindingsInternal(state, policy, graph, coordination);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadExecutionPolicy(root = process.cwd(), providedCatalog = null) {
|
|
18
|
+
const paths = await pathsFor(root, providedCatalog);
|
|
19
|
+
return { policy: await readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy), ...paths };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadSelfstarter(root = process.cwd(), providedCatalog = null) {
|
|
23
|
+
const paths = await pathsFor(root, providedCatalog);
|
|
24
|
+
return { state: await readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs), ...paths };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function inspectSelfstarter(root = process.cwd(), providedCatalog = null) {
|
|
28
|
+
const paths = await pathsFor(root, providedCatalog);
|
|
29
|
+
let policy = emptyPolicy(paths.catalog.root);
|
|
30
|
+
let state = emptyJobs(paths.catalog.root);
|
|
31
|
+
const errors = [];
|
|
32
|
+
try { policy = await readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy); } catch (error) { errors.push(`policy:${error.message}`); }
|
|
33
|
+
try { state = await readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs); } catch (error) { errors.push(`state:${error.message}`); }
|
|
34
|
+
return { policy, state, errors, ...paths };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function grantExecution({
|
|
38
|
+
root = process.cwd(), id = `execution-grant:${randomUUID()}`, actorId, actions = ["start", "resume", "effect"],
|
|
39
|
+
jobId, taskId, targetId, projectId, groupId = null, host, capabilities: requestedCapabilities,
|
|
40
|
+
reason, expiresAt = null, confirmation, now = new Date()
|
|
41
|
+
}) {
|
|
42
|
+
requireOwnerConfirmation(confirmation);
|
|
43
|
+
const grant = {
|
|
44
|
+
id: stableId(id, "grantId"), actorId: stableId(actorId, "actorId"),
|
|
45
|
+
actions: [...new Set(actions)].sort(), jobId: stableId(jobId, "jobId"), taskId: stableId(taskId, "taskId"),
|
|
46
|
+
targetId: stableId(targetId, "targetId"), projectId: stableId(projectId, "projectId"),
|
|
47
|
+
groupId: stableId(groupId, "groupId", true), host, capabilities: capabilities(requestedCapabilities),
|
|
48
|
+
reason: safeText(reason, "reason", 500), active: true, expiresAt: timestamp(expiresAt, "expiresAt", true)
|
|
49
|
+
};
|
|
50
|
+
if (grant.actions.length !== 3 || grant.actions.some((action) => !ACTIONS.has(action))) throw new Error("a self-starter grant must explicitly include start, resume, and effect");
|
|
51
|
+
if (!HOSTS.has(host)) throw new Error("host must be claude or codex");
|
|
52
|
+
const at = timestamp(now, "now");
|
|
53
|
+
if (grant.expiresAt && new Date(grant.expiresAt) <= new Date(at)) throw new Error("execution grant expiry must be in the future");
|
|
54
|
+
const { catalog, executionPolicyPath } = await pathsFor(root);
|
|
55
|
+
const { graph, coordination } = await referenceState(catalog);
|
|
56
|
+
if (!validateGrantReferences(grant, graph, coordination)) throw new Error("execution grant must match a current assigned task and its exact scope");
|
|
57
|
+
return withLock(executionPolicyPath, () => readJson(executionPolicyPath, catalog.root, normalizePolicy, emptyPolicy), (policy) => {
|
|
58
|
+
const findings = executionPolicyFindingsInternal(policy, graph, coordination, { checkCurrentReferences: false });
|
|
59
|
+
if (findings.length) throw new Error(`execution policy failed closed: ${findings.join(", ")}`);
|
|
60
|
+
if (policy.grants.some((item) => item.id === grant.id || item.jobId === grant.jobId)) throw new Error("execution grant and job IDs are immutable and unique");
|
|
61
|
+
policy.revision += 1;
|
|
62
|
+
Object.assign(grant, {
|
|
63
|
+
revision: policy.revision, createdAt: at, updatedAt: at,
|
|
64
|
+
source: "explicit-local-owner-policy", authority: "explicit-local-execution-policy"
|
|
65
|
+
});
|
|
66
|
+
policy.grants.push(grant);
|
|
67
|
+
policy.grants.sort((a, b) => a.id.localeCompare(b.id));
|
|
68
|
+
return { grant, policyRevision: policy.revision, executionPolicyPath };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function revokeExecution({ root = process.cwd(), id, reason, confirmation, now = new Date() }) {
|
|
73
|
+
requireOwnerConfirmation(confirmation);
|
|
74
|
+
stableId(id, "grantId");
|
|
75
|
+
const at = timestamp(now, "now");
|
|
76
|
+
const revokeReason = safeText(reason, "reason", 500);
|
|
77
|
+
const { catalog, executionPolicyPath } = await pathsFor(root);
|
|
78
|
+
const { graph, coordination } = await referenceState(catalog);
|
|
79
|
+
return withLock(executionPolicyPath, () => readJson(executionPolicyPath, catalog.root, normalizePolicy, emptyPolicy), (policy) => {
|
|
80
|
+
const findings = executionPolicyFindingsInternal(policy, graph, coordination);
|
|
81
|
+
if (findings.length) throw new Error(`execution policy failed closed: ${findings.join(", ")}`);
|
|
82
|
+
const previous = policy.grants.find((grant) => grant.id === id);
|
|
83
|
+
if (!previous || !previous.active) throw new Error("execution grant is unknown or already revoked");
|
|
84
|
+
preservePolicy(policy, previous, at);
|
|
85
|
+
policy.revision += 1;
|
|
86
|
+
const grant = { ...previous, active: false, revokedAt: at, revokeReason, revision: policy.revision, updatedAt: at };
|
|
87
|
+
policy.grants = policy.grants.map((item) => item.id === id ? grant : item);
|
|
88
|
+
return { grant, policyRevision: policy.revision, executionPolicyPath };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, opendir, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { isConfiguredHostProfileRoot, workspaceScanError } from "./selfstarter-core.js";
|
|
5
|
+
|
|
6
|
+
const MAX_WORKSPACE_FILES = 10000;
|
|
7
|
+
const MAX_WORKSPACE_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const EXCLUDED_NAMES = new Set([".git", "node_modules", ".DS_Store"]);
|
|
9
|
+
|
|
10
|
+
export function skippableTraversalError(error) {
|
|
11
|
+
return ["EPERM", "EACCES", "ENOENT"].includes(error?.code);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function skippedPath(skipped, path, error, operation) {
|
|
15
|
+
skipped.push({ path, code: error.code, operation });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function collectWorkspaceFiles(root) {
|
|
19
|
+
const files = [];
|
|
20
|
+
const skipped = [];
|
|
21
|
+
let totalBytes = 0;
|
|
22
|
+
async function walk(directory) {
|
|
23
|
+
let stream;
|
|
24
|
+
try {
|
|
25
|
+
stream = await opendir(directory);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!skippableTraversalError(error)) throw workspaceScanError(error);
|
|
28
|
+
skippedPath(skipped, error.path || directory, error, "opendir");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const entries = [];
|
|
32
|
+
try {
|
|
33
|
+
for await (const entry of stream) entries.push(entry);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!skippableTraversalError(error)) throw workspaceScanError(error);
|
|
36
|
+
skippedPath(skipped, directory, error, "readdir");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (EXCLUDED_NAMES.has(entry.name)) continue;
|
|
42
|
+
const path = join(directory, entry.name);
|
|
43
|
+
let metadata;
|
|
44
|
+
try {
|
|
45
|
+
metadata = await lstat(path);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!skippableTraversalError(error)) throw workspaceScanError(error);
|
|
48
|
+
skippedPath(skipped, error.path || path, error, "lstat");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (metadata.isSymbolicLink()) {
|
|
52
|
+
try {
|
|
53
|
+
await realpath(path);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!skippableTraversalError(error)) throw workspaceScanError(error);
|
|
56
|
+
skippedPath(skipped, error.path || path, error, "realpath");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`workspace fingerprint rejects symbolic link: ${relative(root, path)}`);
|
|
60
|
+
}
|
|
61
|
+
if (metadata.isDirectory()) await walk(path);
|
|
62
|
+
else if (metadata.isFile()) {
|
|
63
|
+
files.push({ path, relativePath: relative(root, path).split(sep).join("/"), size: metadata.size });
|
|
64
|
+
totalBytes += metadata.size;
|
|
65
|
+
if (files.length > MAX_WORKSPACE_FILES || totalBytes > MAX_WORKSPACE_BYTES) {
|
|
66
|
+
throw new Error("workspace exceeds the self-starter fingerprint limit");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await walk(root);
|
|
72
|
+
return { files, skipped: skipped.sort((a, b) => a.path.localeCompare(b.path) || a.operation.localeCompare(b.operation)) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function workspaceFingerprint(inputRoot = process.cwd()) {
|
|
76
|
+
const root = resolve(inputRoot);
|
|
77
|
+
if (isConfiguredHostProfileRoot(root)) {
|
|
78
|
+
throw new Error("self-starter cannot fingerprint a host profile root");
|
|
79
|
+
}
|
|
80
|
+
const collected = await collectWorkspaceFiles(root);
|
|
81
|
+
const files = [];
|
|
82
|
+
const skipped = [...collected.skipped];
|
|
83
|
+
const hash = createHash("sha256");
|
|
84
|
+
for (const file of collected.files) {
|
|
85
|
+
let content;
|
|
86
|
+
try {
|
|
87
|
+
content = await readFile(file.path);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!skippableTraversalError(error)) throw workspaceScanError(error);
|
|
90
|
+
skippedPath(skipped, error.path || file.path, error, "readFile");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
files.push(file);
|
|
94
|
+
hash.update(file.relativePath).update("\0").update(String(file.size)).update("\0");
|
|
95
|
+
hash.update(content).update("\0");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
digest: hash.digest("hex"), files: files.length, bytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
99
|
+
skipped: skipped.sort((a, b) => a.path.localeCompare(b.path) || a.operation.localeCompare(b.operation))
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function incompleteWorkspaceScan(skipped) {
|
|
104
|
+
const first = skipped[0];
|
|
105
|
+
const error = new Error(`workspace scan skipped ${skipped.length} inaccessible path${skipped.length === 1 ? "" : "s"}: ${first.path}`);
|
|
106
|
+
error.code = "AGENTSPINE_SCAN_INCOMPLETE";
|
|
107
|
+
error.path = first.path;
|
|
108
|
+
error.syscall = first.operation;
|
|
109
|
+
error.skipped = skipped;
|
|
110
|
+
return error;
|
|
111
|
+
}
|