cli-swarm 7.0.37 → 7.0.39
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/README.md +14 -0
- package/broker-account-storage.mjs +135 -0
- package/broker-credentials.mjs +125 -0
- package/broker.mjs +13 -93
- package/installer-storage.mjs +205 -0
- package/installer.mjs +48 -63
- package/official-skill-update.mjs +151 -0
- package/package.json +16 -2
- package/skill/SKILL.md +82 -134
- package/skill/references/ops-heartbeat.md +3 -3
- package/skill/references/org-chart.md +5 -26
- package/skill/references/task-lifecycle.md +10 -2
- package/skill/references/task-routing.md +64 -0
- package/skill/references/traffic-light.md +3 -3
- package/skill/skill.json +1 -1
- package/swarm-coordinator-model.mjs +52 -0
- package/swarm-coordinator-waits.mjs +44 -24
- package/swarm-coordinator.mjs +11 -4
- package/swarm-runtime.mjs +45 -111
- package/swarm-task-contract.mjs +8 -0
- package/swarm-task-handoff.mjs +180 -0
- package/swarm-task-policy.mjs +142 -0
- package/swarm-task-routing-model.mjs +163 -0
- package/swarm-task-routing-schemas.mjs +24 -0
- package/swarm-task-routing.mjs +178 -0
package/swarm-runtime.mjs
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
import { text, isObject, validId, finding, ORG_PERMISSIONS, TEST_EVIDENCE_SCHEMA, SHA256_PATTERN } from "./swarm-task-contract.mjs";
|
|
2
|
+
import { dispatchEligibility, buildTasks, dispatchTask, claimTask, reportTask, acceptTask, taskTrafficLight, validateTestEvidence, normalizeTestEvidence, hasPassingTestEvidence, hasTrustedTestEvidence, reclaimTask } from "./swarm-task-policy.mjs";
|
|
3
|
+
// Reviewed modules are compiled into one deployment artifact by build-swarm-runtime.mjs.
|
|
2
4
|
const REQUEST_SCHEMA = "swarm.skill.request/1.0";
|
|
3
5
|
const ALLOWED_EXTERNAL_ENDPOINTS = { blueprint: "https://cli.tax/wvz6zmRWmX" };
|
|
4
6
|
const RESPONSE_SCHEMA = "swarm.skill.response/1.0"; const ERROR_SCHEMA = "swarm.skill.error/1.0";
|
|
5
|
-
const ORG_SCHEMA = "swarm.org-chart/1.0"; const TASK_SCHEMA = "swarm.tasks/1.0";
|
|
6
|
-
const COMPILER_NAME = "swarm"; const COMPILER_VERSION = "v7.0.
|
|
7
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
7
|
+
const ORG_SCHEMA = "swarm.org-chart/1.0"; const TASK_SCHEMA = "swarm.tasks/1.0";
|
|
8
|
+
const COMPILER_NAME = "swarm"; const COMPILER_VERSION = "v7.0.39";
|
|
8
9
|
const PURE_OPERATIONS = new Set([
|
|
9
10
|
"capabilities", "help", "intake", "org-chart", "blueprint-bridge", "dispatch", "claim",
|
|
10
11
|
"report", "accept", "swarm-status", "traffic-light", "security-check", "validate-json", "heartbeat", "reclaim",
|
|
11
12
|
]);
|
|
12
|
-
function text(value) { return String(value ?? ""); }
|
|
13
13
|
function okResponse(requestId, payload) { return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "succeeded", ...payload }; }
|
|
14
14
|
function blockedResponse(requestId, request, findings) {
|
|
15
15
|
return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "blocked", brainMode: null,
|
|
16
16
|
requestedBrainMode: request?.requestedBrainMode ?? "ide", brainUsed: false, revision: null,
|
|
17
17
|
validation: { valid: false, guarantee: "blocked", findings } };
|
|
18
18
|
}
|
|
19
|
-
function finding(severity, ruleId, entityRef, message, evidence = {}) { return { severity, ruleId, entityRef, message, evidence }; }
|
|
20
|
-
function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
21
|
-
function validId(value) { return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(value); }
|
|
22
19
|
const ORG_LAYERS = ["board", "management", "execution"];
|
|
23
20
|
const ORG_ROLES = ["board", "dispatcher", "ops", "security-guard", "coordinator", "worker"];
|
|
24
21
|
const ORG_FIXED_ROLES = new Set(["board", "dispatcher", "ops", "security-guard", "coordinator"]);
|
|
25
|
-
const ORG_PERMISSIONS = { board: ["dispatch", "accept", "reject", "stop", "reclaim", "replace"], dispatcher: ["dispatch", "reassign", "prioritize"], ops: ["heartbeat", "reclaim", "replace"],
|
|
26
|
-
"security-guard": ["block", "alert", "quarantine"], coordinator: ["conflict-scan", "lock", "queue", "baseline-handshake", "dependency-wait", "wake", "need-human"], worker: ["claim", "report", "request-help"], };
|
|
27
22
|
function analyzeTaskGraph(tasks) {
|
|
28
23
|
const findings = [];
|
|
29
24
|
const ids = new Set();
|
|
@@ -53,14 +48,27 @@ function analyzeTaskGraph(tasks) {
|
|
|
53
48
|
return { findings: [], recommendedWorkerCount: Math.min(50, Math.max(...levels)) };
|
|
54
49
|
}
|
|
55
50
|
function buildOrgChart(input = {}) {
|
|
56
|
-
const workerCount = Math.min(50, Math.max(1, Math.floor(Number(input.workerCount) || 4)));
|
|
57
51
|
const projectName = text(input.projectName || "swarm-run");
|
|
58
|
-
let recommendedWorkerCount, recommendationFindings = [];
|
|
52
|
+
let recommendedWorkerCount = 0, recommendationFindings = [];
|
|
53
|
+
const delegationDecisions = [];
|
|
59
54
|
if (Array.isArray(input.tasks) && input.tasks.length > 0) {
|
|
60
55
|
const analysis = analyzeTaskGraph(input.tasks);
|
|
61
|
-
recommendedWorkerCount = analysis.recommendedWorkerCount ?? undefined;
|
|
62
56
|
recommendationFindings = analysis.findings;
|
|
57
|
+
if (!recommendationFindings.length) {
|
|
58
|
+
const eligible = [];
|
|
59
|
+
for (const task of input.tasks) {
|
|
60
|
+
const reason = dispatchEligibility(eligible, task, task.facts);
|
|
61
|
+
delegationDecisions.push({ taskId: task.taskId, delegate: reason === null, reason });
|
|
62
|
+
if (reason === null) eligible.push({ ...task, status: "assigned", dispatch: task.facts });
|
|
63
|
+
}
|
|
64
|
+
recommendedWorkerCount = Math.min(analysis.recommendedWorkerCount, eligible.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (input.workerCount !== undefined && (!Number.isSafeInteger(input.workerCount) || input.workerCount < 0 || input.workerCount > 50)) {
|
|
68
|
+
recommendationFindings.push(finding("P0", "WORKER_COUNT_INVALID", "input.workerCount", "workerCount must be an integer from 0 to 50"));
|
|
63
69
|
}
|
|
70
|
+
const workerCount = recommendationFindings.length ? 0
|
|
71
|
+
: input.workerCount === undefined ? recommendedWorkerCount : Math.min(input.workerCount, recommendedWorkerCount);
|
|
64
72
|
const org = { schemaVersion: ORG_SCHEMA, projectName, layers: { board: [{ agentId: "board", role: "board", title: "决策层·老板/主智能体" }],
|
|
65
73
|
management: [
|
|
66
74
|
{ agentId: "dispatcher", role: "dispatcher", title: "管理层·调度智能体", fixed: true },
|
|
@@ -68,7 +76,8 @@ function buildOrgChart(input = {}) {
|
|
|
68
76
|
{ agentId: "coordinator", role: "coordinator", title: "管理层·自动协调智能体", fixed: true }, ],
|
|
69
77
|
execution: Array.from({ length: workerCount }, (_, i) => ({ agentId: `worker-${String(i + 1).padStart(3, "0")}`,
|
|
70
78
|
role: "worker", title: `执行层·子智能体 ${i + 1}`, fixed: false })),
|
|
71
|
-
}, permissions: ORG_PERMISSIONS };
|
|
79
|
+
}, permissions: ORG_PERMISSIONS, delegationDecisions, executionMode: workerCount === 0 ? "single-agent" : "delegated" };
|
|
80
|
+
for (const role of org.layers.management) role.hostAgentId = "board";
|
|
72
81
|
if (recommendedWorkerCount !== undefined) org.recommendedWorkerCount = recommendedWorkerCount;
|
|
73
82
|
if (recommendationFindings.length) org.recommendationFindings = recommendationFindings;
|
|
74
83
|
return org;
|
|
@@ -113,92 +122,11 @@ function validateProjectJson(project) {
|
|
|
113
122
|
else findings.push(...analyzeTaskGraph(project.tasks).findings);
|
|
114
123
|
return findings;
|
|
115
124
|
}
|
|
116
|
-
function buildTasks(project, org) {
|
|
117
|
-
return (project.tasks ?? []).map((task, index) => ({
|
|
118
|
-
taskId: validId(task.taskId) ? task.taskId : `task-${String(index + 1).padStart(4, "0")}`, title: text(task.title || task.name || `任务 ${index + 1}`), owner: null, status: "backlog",
|
|
119
|
-
priority: text(task.priority || "normal"), dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn : [], assignedBy: null, claimedAt: null, reportedAt: null, report: null, progressPercent: 0,
|
|
120
|
-
progressNote: "", inheritedFrom: null }));
|
|
121
|
-
}
|
|
122
|
-
function dispatchTask(tasks, taskId, workerId, actorRole) {
|
|
123
|
-
if (!ORG_PERMISSIONS[actorRole]?.includes("dispatch")) return { ok: false, error: `role ${actorRole} cannot dispatch` };
|
|
124
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
125
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
126
|
-
if (task.status !== "backlog") return { ok: false, error: `task ${taskId} is ${task.status}, not backlog` };
|
|
127
|
-
for (const dependencyId of Array.isArray(task.dependsOn) ? task.dependsOn : []) {
|
|
128
|
-
const dependency = tasks.find((candidate) => candidate.taskId === dependencyId);
|
|
129
|
-
if (!dependency) return { ok: false, error: `dependency ${dependencyId} not found` };
|
|
130
|
-
if (dependency.status !== "accepted") return { ok: false, error: `dependency ${dependencyId} is ${dependency.status}, not accepted` };
|
|
131
|
-
}
|
|
132
|
-
task.status = "assigned"; task.owner = workerId; task.assignedBy = actorRole;
|
|
133
|
-
return { ok: true, task };
|
|
134
|
-
}
|
|
135
|
-
function claimTask(tasks, taskId, workerId) {
|
|
136
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
137
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
138
|
-
if (task.status !== "assigned") return { ok: false, error: `task ${taskId} is ${task.status}, not assigned` };
|
|
139
|
-
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} claimed by another worker` };
|
|
140
|
-
task.status = "claimed"; task.owner = workerId; task.claimedAt = new Date().toISOString();
|
|
141
|
-
return { ok: true, task };
|
|
142
|
-
}
|
|
143
|
-
function validateTestEvidence(value, entityRef) {
|
|
144
|
-
const findings = [];
|
|
145
|
-
if (!isObject(value)) return [finding("P0", "TEST_EVIDENCE_OBJECT", entityRef, "TestEvidence must be an object")];
|
|
146
|
-
if (value.schemaVersion !== TEST_EVIDENCE_SCHEMA) findings.push(finding("P0", "TEST_EVIDENCE_SCHEMA", `${entityRef}.schemaVersion`, `Expected ${TEST_EVIDENCE_SCHEMA}`));
|
|
147
|
-
if (!text(value.evidenceId).trim()) findings.push(finding("P0", "TEST_EVIDENCE_ID", `${entityRef}.evidenceId`, "evidenceId is required"));
|
|
148
|
-
if (!["test", "build", "lint", "security", "benchmark"].includes(value.kind)) findings.push(finding("P0", "TEST_EVIDENCE_KIND", `${entityRef}.kind`, "kind is unsupported"));
|
|
149
|
-
if (!["local", "trusted-runner"].includes(value.runner)) findings.push(finding("P0", "TEST_EVIDENCE_RUNNER", `${entityRef}.runner`, "runner must be local or trusted-runner"));
|
|
150
|
-
if (!text(value.command).trim()) findings.push(finding("P0", "TEST_EVIDENCE_COMMAND", `${entityRef}.command`, "command is required"));
|
|
151
|
-
if (!Number.isInteger(value.exitCode)) findings.push(finding("P0", "TEST_EVIDENCE_EXIT", `${entityRef}.exitCode`, "exitCode must be an integer"));
|
|
152
|
-
if (typeof value.durationMs !== "number" || !Number.isFinite(value.durationMs) || value.durationMs < 0) findings.push(finding("P0", "TEST_EVIDENCE_DURATION", `${entityRef}.durationMs`, "durationMs must be a finite number >= 0"));
|
|
153
|
-
if (!text(value.summary).trim()) findings.push(finding("P0", "TEST_EVIDENCE_SUMMARY", `${entityRef}.summary`, "summary is required"));
|
|
154
|
-
if (value.artifactSha256 !== undefined && !SHA256_PATTERN.test(value.artifactSha256)) findings.push(finding("P0", "TEST_EVIDENCE_ARTIFACT", `${entityRef}.artifactSha256`, "artifactSha256 must be a lowercase SHA-256 digest"));
|
|
155
|
-
return findings;
|
|
156
|
-
}
|
|
157
|
-
function normalizeTestEvidence(values) {
|
|
158
|
-
if (!Array.isArray(values)) return { evidence: [], findings: [finding("P0", "TEST_EVIDENCE_ARRAY", "report.evidence", "evidence must be an array")] };
|
|
159
|
-
const findings = values.flatMap((value, index) => validateTestEvidence(value, `report.evidence[${index}]`));
|
|
160
|
-
return findings.length ? { evidence: [], findings } : { evidence: values.map((value) => ({ ...value })), findings: [] };
|
|
161
|
-
}
|
|
162
|
-
function hasPassingTestEvidence(task) {
|
|
163
|
-
const evidence = task?.report?.evidence;
|
|
164
|
-
return Array.isArray(evidence) && evidence.length > 0
|
|
165
|
-
&& evidence.every((value, index) => validateTestEvidence(value, `task.report.evidence[${index}]`).length === 0 && value.exitCode === 0);
|
|
166
|
-
}
|
|
167
|
-
function reportTask(tasks, taskId, workerId, report) {
|
|
168
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
169
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
170
|
-
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} not owned by ${workerId}` };
|
|
171
|
-
if (!["claimed", "running"].includes(task.status)) return { ok: false, error: `task ${taskId} is ${task.status}, cannot report` };
|
|
172
|
-
if (!isObject(report)) return { ok: false, error: "report must be an object" };
|
|
173
|
-
if (!text(report.output).trim() && report.evidence === undefined) return { ok: false, error: "report must have output or evidence" };
|
|
174
|
-
const normalizedReport = { output: text(report.output) };
|
|
175
|
-
if (report.evidence !== undefined) {
|
|
176
|
-
const normalized = normalizeTestEvidence(report.evidence);
|
|
177
|
-
if (normalized.findings.length) return { ok: false, error: "report evidence is invalid", findings: normalized.findings };
|
|
178
|
-
normalizedReport.evidence = normalized.evidence;
|
|
179
|
-
}
|
|
180
|
-
task.status = "reported"; task.report = normalizedReport; task.reportedAt = new Date().toISOString();
|
|
181
|
-
return { ok: true, task, hasEvidence: hasPassingTestEvidence(task) };
|
|
182
|
-
}
|
|
183
|
-
function acceptTask(tasks, taskId, accept, actorRole) {
|
|
184
|
-
if (actorRole !== "board") return { ok: false, error: `role ${actorRole} cannot accept` };
|
|
185
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
186
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
187
|
-
if (task.status !== "reported") return { ok: false, error: `task ${taskId} is ${task.status}, not reported` };
|
|
188
|
-
if (accept && !hasPassingTestEvidence(task)) return { ok: false, error: `task ${taskId} has no passing TestEvidence` };
|
|
189
|
-
task.status = accept ? "accepted" : "failed";
|
|
190
|
-
return { ok: true, task };
|
|
191
|
-
}
|
|
192
|
-
function taskTrafficLight(task) {
|
|
193
|
-
if (task.status === "accepted") return hasPassingTestEvidence(task) ? "green" : "red";
|
|
194
|
-
if (task.status === "reported") return hasPassingTestEvidence(task) ? "green" : "yellow";
|
|
195
|
-
if (task.status === "failed" || task.status === "blocked" || task.status === "cancelled") return "red";
|
|
196
|
-
return "yellow";
|
|
197
|
-
}
|
|
198
125
|
const HEARTBEAT_MISS_LIMIT = 3;
|
|
199
126
|
function buildAgents(org, nowIso = null) {
|
|
200
127
|
const now = nowIso || new Date().toISOString(), agents = [];
|
|
201
128
|
for (const layer of ORG_LAYERS) for (const member of org.layers?.[layer] ?? []) {
|
|
129
|
+
if (member.hostAgentId === "board") continue;
|
|
202
130
|
agents.push({ agentId: member.agentId, role: member.role, title: member.title,
|
|
203
131
|
fixed: Boolean(member.fixed), status: "green", lastHeartbeatAt: now,
|
|
204
132
|
heartbeatMisses: 0, currentTaskId: null, progressPercent: 0, progressNote: "" });
|
|
@@ -221,6 +149,7 @@ function scanHeartbeats(agents, nowIso = null) {
|
|
|
221
149
|
const misses = Math.floor((now - new Date(agent.lastHeartbeatAt).getTime()) / 30000);
|
|
222
150
|
agent.heartbeatMisses = Math.max(agent.heartbeatMisses, Math.min(misses, 99));
|
|
223
151
|
if (agent.heartbeatMisses >= HEARTBEAT_MISS_LIMIT && agent.status !== "dead") { agent.status = "dead"; dead.push(agent.agentId); }
|
|
152
|
+
else if (agent.status === "dead") continue;
|
|
224
153
|
else if (agent.heartbeatMisses >= 1) agent.status = "yellow";
|
|
225
154
|
else agent.status = "green";
|
|
226
155
|
}
|
|
@@ -229,16 +158,14 @@ function scanHeartbeats(agents, nowIso = null) {
|
|
|
229
158
|
function reclaimTasks(tasks, workerId) {
|
|
230
159
|
const reclaimed = [];
|
|
231
160
|
for (const task of tasks) {
|
|
232
|
-
if (task.owner === workerId &&
|
|
233
|
-
task.status = "backlog"; task.owner = null; task.inheritedFrom = workerId; reclaimed.push(task.taskId);
|
|
234
|
-
}
|
|
161
|
+
if (task.owner === workerId && reclaimTask(task, workerId)) reclaimed.push(task.taskId);
|
|
235
162
|
}
|
|
236
163
|
return reclaimed;
|
|
237
164
|
}
|
|
238
165
|
function replaceWorker(org, agents, tasks, deadWorkerId, newWorkerId = null) {
|
|
239
166
|
const dead = agents.find((a) => a.agentId === deadWorkerId);
|
|
240
|
-
if (!dead) return { ok: false, error: `dead worker ${deadWorkerId} not found` };
|
|
241
|
-
const replacement = newWorkerId ? agents.find((a) => a.agentId === newWorkerId && a.role === "worker")
|
|
167
|
+
if (!dead || dead.role !== "worker" || dead.status !== "dead") return { ok: false, error: `dead worker ${deadWorkerId} not found` };
|
|
168
|
+
const replacement = newWorkerId ? agents.find((a) => a.agentId === newWorkerId && a.role === "worker" && a.status === "green" && a.agentId !== deadWorkerId)
|
|
242
169
|
: agents.find((a) => a.role === "worker" && a.status === "green" && a.agentId !== deadWorkerId);
|
|
243
170
|
if (!replacement) return { ok: false, error: "no healthy replacement worker available" };
|
|
244
171
|
const inheritedTasks = reclaimTasks(tasks, deadWorkerId);
|
|
@@ -251,7 +178,7 @@ function replaceWorker(org, agents, tasks, deadWorkerId, newWorkerId = null) {
|
|
|
251
178
|
return { ok: true, replacement: replacement.agentId, inheritedTasks };
|
|
252
179
|
}
|
|
253
180
|
const INTAKE_QUESTIONS = [ { id: "goal", prompt: "What must the swarm accomplish? List the parallel/ordered work items or point to the project JSON.", required: true, example: "12 个模块迁移:A1..A12,依赖 A1→A2→A3,其余并行" },
|
|
254
|
-
{ id: "workerCount", prompt: "
|
|
181
|
+
{ id: "workerCount", prompt: "What is the maximum justified worker count? Omit for task-based sizing; simple work stays with the main agent.", required: false, example: "6" },
|
|
255
182
|
{ id: "orgTier", prompt: "Any org-chart constraints? (default: board → dispatcher/ops/security-guard → workers)", required: false, example: "默认三层即可" },
|
|
256
183
|
{ id: "securityPolicy", prompt: "Security policy: strict (block injections) or observe (alert only)?", required: false, example: "strict" },
|
|
257
184
|
{ id: "blueprintEnabled", prompt: "Use Blueprint to plan tasks before dispatch? (yes: tasks are planned by the Blueprint skill for traceable acceptance; no: direct dispatch)", required: false, example: "no" }, ];
|
|
@@ -264,13 +191,18 @@ const nullableStringSchema = { type: ["string", "null"] };
|
|
|
264
191
|
const evidenceSchema = objectSchema({ schemaVersion: { const: TEST_EVIDENCE_SCHEMA }, evidenceId: stringSchema({ minLength: 1 }),
|
|
265
192
|
kind: { enum: ["test", "build", "lint", "security", "benchmark"] }, runner: { enum: ["local", "trusted-runner"] }, command: stringSchema({ minLength: 1 }), exitCode: { type: "integer" }, durationMs: { type: "number", minimum: 0 },
|
|
266
193
|
summary: stringSchema({ minLength: 1 }), artifactSha256: stringSchema({ pattern: SHA256_PATTERN.source }), }, ["schemaVersion", "evidenceId", "kind", "runner", "command", "exitCode", "durationMs", "summary"], { additionalProperties: true });
|
|
194
|
+
const delegationSchema = objectSchema({ businessNeed: stringSchema({ minLength: 1 }), deliverable: stringSchema({ minLength: 1 }),
|
|
195
|
+
acceptanceCriteria: stringSchema({ minLength: 1 }), mainAgentWork: stringSchema({ minLength: 1 }), independent: { type: "boolean" },
|
|
196
|
+
substantial: { type: "boolean" }, estimatedSavedMinutes: { type: "number", minimum: 0 }, coordinationMinutes: { type: "number", minimum: 0 },
|
|
197
|
+
}, ["businessNeed", "deliverable", "acceptanceCriteria", "mainAgentWork", "independent", "substantial", "estimatedSavedMinutes", "coordinationMinutes"]);
|
|
198
|
+
const dispatchSchema = objectSchema({ delegation: delegationSchema, estimatedChangedLines: { type: "integer", minimum: 0 }, fileCount: { type: "integer", minimum: 1 }, crossModule: { type: "boolean" }, parallelSafe: { type: "boolean" }, ownerId: stringSchema({ minLength: 1 }), targetPaths: arraySchema(stringSchema({ minLength: 1 }), { minItems: 1 }) }, ["estimatedChangedLines", "fileCount", "crossModule", "parallelSafe", "ownerId", "targetPaths", "delegation"]);
|
|
267
199
|
const reportSchema = objectSchema({ output: stringSchema(), evidence: arraySchema(evidenceSchema) }, [], {
|
|
268
200
|
anyOf: [{ properties: { output: stringSchema({ minLength: 1 }) }, required: ["output"] }, { required: ["evidence"] }],
|
|
269
201
|
});
|
|
270
202
|
const taskSchema = objectSchema({ taskId: stringSchema({ minLength: 1 }), title: stringSchema(), owner: nullableStringSchema,
|
|
271
203
|
status: { enum: [...TASK_STATUSES] }, priority: stringSchema(), dependsOn: arraySchema(stringSchema()), assignedBy: nullableStringSchema, claimedAt: nullableStringSchema, reportedAt: nullableStringSchema,
|
|
272
204
|
report: { anyOf: [{ type: "null" }, reportSchema] }, progressPercent: { type: "number" }, progressNote: stringSchema(), inheritedFrom: nullableStringSchema, trafficLight: { enum: ["green", "yellow", "red"] }, }, ["taskId", "title", "status", "dependsOn"], { additionalProperties: true });
|
|
273
|
-
const projectTaskSchema = objectSchema({ taskId: stringSchema({ minLength: 1 }), title: stringSchema({ minLength: 1 }),
|
|
205
|
+
const projectTaskSchema = objectSchema({ facts: dispatchSchema, taskId: stringSchema({ minLength: 1 }), title: stringSchema({ minLength: 1 }),
|
|
274
206
|
name: stringSchema(), priority: stringSchema(), dependsOn: arraySchema(stringSchema()) }, ["taskId", "title"], { additionalProperties: true });
|
|
275
207
|
const agentSchema = objectSchema({ agentId: stringSchema({ minLength: 1 }), role: { enum: ORG_ROLES }, title: stringSchema(), fixed: { type: "boolean" }, status: { enum: ["green", "yellow", "red", "dead"] }, lastHeartbeatAt: stringSchema({ format: "date-time" }),
|
|
276
208
|
heartbeatMisses: { type: "integer", minimum: 0 }, currentTaskId: nullableStringSchema, progressPercent: { type: "number" }, progressNote: stringSchema() }, ["agentId", "role", "status"], { additionalProperties: true });
|
|
@@ -287,9 +219,9 @@ const OPERATION_SCHEMAS = Object.freeze({
|
|
|
287
219
|
capabilities: operationSchema({}, [], { capabilities: anyObjectSchema, operationSchemas: anyObjectSchema, skill: anyObjectSchema, nextStep: nextSchema }, ["capabilities", "operationSchemas", "skill", "nextStep"]),
|
|
288
220
|
help: operationSchema({}, [], { help: anyObjectSchema, nextStep: nextSchema }, ["help", "nextStep"]),
|
|
289
221
|
intake: operationSchema({}, [], { questions: arraySchema(anyObjectSchema), nextStep: nextSchema }, ["questions", "nextStep"]),
|
|
290
|
-
"org-chart": operationSchema({ workerCount: { type: "
|
|
222
|
+
"org-chart": operationSchema({ workerCount: { type: "integer", minimum: 0, maximum: 50 }, projectName: stringSchema(), blueprintEnabled: { type: ["boolean", "string"] }, tasks: arraySchema(projectTaskSchema) }, [], { org: anyObjectSchema, blueprintEnabled: { type: "boolean" }, fixedAgents: arraySchema(stringSchema()), workerCount: { type: "integer" }, nextStep: nextSchema }, ["org", "blueprintEnabled", "fixedAgents", "workerCount", "nextStep"]),
|
|
291
223
|
"blueprint-bridge": operationSchema({ projectName: stringSchema({ minLength: 1 }), tasks: arraySchema(projectTaskSchema, { minItems: 1 }) }, ["projectName", "tasks"], { planningStatus: { const: "planned" }, blueprintEnabled: { const: true }, blueprintEndpoint: stringSchema({ format: "uri" }), blueprintRequest: anyObjectSchema, projectFormat: { const: "swarm.project/1.0" }, nextStep: nextSchema }, ["planningStatus", "blueprintEnabled", "blueprintEndpoint", "blueprintRequest", "projectFormat", "nextStep"]),
|
|
292
|
-
dispatch: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), actorRole: { enum: ["dispatcher", "board"] } }, ["tasks", "taskId", "workerId", "actorRole"], { ...taskStateOutput, nextStep: nextSchema }, ["task", "tasks", "stateNote", "nextStep"]),
|
|
224
|
+
dispatch: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), actorRole: { enum: ["dispatcher", "board"] }, facts: dispatchSchema }, ["tasks", "taskId", "workerId", "actorRole", "facts"], { ...taskStateOutput, nextStep: nextSchema }, ["task", "tasks", "stateNote", "nextStep"]),
|
|
293
225
|
claim: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }) }, ["tasks", "taskId", "workerId"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
294
226
|
report: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), report: reportSchema }, ["tasks", "taskId", "workerId", "report"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, evidenceRequired: { type: "boolean" }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
295
227
|
accept: operationSchema({ ...tasksInput, actorRole: { const: "board" }, accept: { type: "boolean" } }, ["tasks", "taskId", "actorRole", "accept"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
@@ -395,7 +327,7 @@ function runMeta(operation, requestId) {
|
|
|
395
327
|
if (operation === "capabilities") {
|
|
396
328
|
return okResponse(requestId, {
|
|
397
329
|
capabilities: { pure: true, stateless: true, networkRequired: false, filesystemRequired: false,
|
|
398
|
-
operations: [...PURE_OPERATIONS], orgSchema: ORG_SCHEMA, taskSchema: TASK_SCHEMA, testEvidenceSchema: TEST_EVIDENCE_SCHEMA, fixedAgents: ["board"
|
|
330
|
+
operations: [...PURE_OPERATIONS], orgSchema: ORG_SCHEMA, taskSchema: TASK_SCHEMA, testEvidenceSchema: TEST_EVIDENCE_SCHEMA, fixedAgents: ["board"],
|
|
399
331
|
trafficLights: ["green", "yellow", "red"], stateHolder: "caller", coordinator: { command: "cli-swarm local", capabilitiesOperation: "capabilities", stateBoundary: ".coord", messageTypes: ["range-declare", "conflict-alert", "lock-granted", "lock-denied", "baseline-handshake", "need-human", "dependency-wait"] },
|
|
400
332
|
workerRecommendation: "maximum acyclic dependency level width, capped at 50" }, operationSchemas: OPERATION_SCHEMAS, skill: { name: COMPILER_NAME, version: COMPILER_VERSION },
|
|
401
333
|
nextStep: { operation: "intake", instruction: "Ask the intake questions, then build the org-chart and dispatch tasks." } });
|
|
@@ -409,8 +341,10 @@ function runPlanning(operation, requestId, input, request) {
|
|
|
409
341
|
const findings = validateOrgChart(org);
|
|
410
342
|
if (findings.length) return blockedResponse(requestId, request, findings);
|
|
411
343
|
const blueprintEnabled = input.blueprintEnabled === true || text(input.blueprintEnabled).toLowerCase() === "yes";
|
|
412
|
-
return okResponse(requestId, { org, blueprintEnabled, fixedAgents: ["board"
|
|
413
|
-
nextStep:
|
|
344
|
+
return okResponse(requestId, { org, blueprintEnabled, fixedAgents: ["board"], workerCount: org.layers.execution.length,
|
|
345
|
+
nextStep: org.layers.execution.length === 0
|
|
346
|
+
? { operation: null, instruction: "Continue with the main agent. No justified independent work requires subagents; logical management roles do not create agents." }
|
|
347
|
+
: blueprintEnabled
|
|
414
348
|
? { operation: "blueprint-bridge", instruction: "Blueprint is enabled: call blueprint-bridge to plan the project JSON into a traceable blueprint, then dispatch its tasks to the swarm." }
|
|
415
349
|
: { operation: "dispatch", instruction: "Feed the project JSON; dispatch backlog tasks to workers by dependency order." } });
|
|
416
350
|
}
|
|
@@ -423,7 +357,7 @@ function runPlanning(operation, requestId, input, request) {
|
|
|
423
357
|
function runTaskMutation(operation, requestId, input, request) {
|
|
424
358
|
const tasks = Array.isArray(input.tasks) ? input.tasks : [], taskId = text(input.taskId);
|
|
425
359
|
if (operation === "dispatch") {
|
|
426
|
-
const result = dispatchTask(tasks, taskId, text(input.workerId), text(input.actorRole
|
|
360
|
+
const result = dispatchTask(tasks, taskId, text(input.workerId), text(input.actorRole), input.facts);
|
|
427
361
|
if (!result.ok) return blockedResponse(requestId, request, [finding("P0", "DISPATCH_FAILED", input.taskId, result.error, { example: { taskId: "task-0001", workerId: "worker-001" } })]);
|
|
428
362
|
return okResponse(requestId, { task: result.task, tasks, stateNote: STATE_NOTE, nextStep: { operation: "claim", instruction: "The worker can now claim the task." } });
|
|
429
363
|
}
|
|
@@ -455,7 +389,7 @@ function runObservation(operation, requestId, input, request) {
|
|
|
455
389
|
}
|
|
456
390
|
if (operation === "traffic-light") return okResponse(requestId, { trafficLight: taskTrafficLight(input.task ?? {}),
|
|
457
391
|
rules: {
|
|
458
|
-
green: "status is
|
|
392
|
+
green: "status is accepted with task-bound signed TestEvidence verified by Validator",
|
|
459
393
|
yellow: "backlog, assigned, claimed, running, or reported without passing TestEvidence",
|
|
460
394
|
red: "status is \"failed\", \"blocked\", or \"cancelled\"" } });
|
|
461
395
|
if (operation === "security-check") {
|
|
@@ -480,7 +414,7 @@ function runObservation(operation, requestId, input, request) {
|
|
|
480
414
|
const task = tasks.find((candidate) => candidate.taskId === taskId);
|
|
481
415
|
if (!task) return blockedResponse(requestId, request, [finding("P0", "RECLAIM_FAILED", taskId, `task ${taskId} not found`, { example: { taskId: "task-0001" } })]);
|
|
482
416
|
if (task.status === "backlog") return blockedResponse(requestId, request, [finding("P1", "RECLAIM_NOOP", taskId, `task ${taskId} is already backlog`, { example: { taskId: "task-0001" } })]);
|
|
483
|
-
task.
|
|
417
|
+
if (!reclaimTask(task, task.owner)) return { ...blockedResponse(requestId, request, [finding("P0", "RECLAIM_REQUIRES_RECONCILIATION", taskId, "Only unstarted assignments may be automatically reclaimed")]), task, tasks, stateNote: STATE_NOTE };
|
|
484
418
|
return okResponse(requestId, { reclaimed: true, taskId, reason, task, tasks, stateNote: STATE_NOTE });
|
|
485
419
|
}
|
|
486
420
|
export async function run(request) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const TEST_EVIDENCE_SCHEMA = 'cli.tax.test-evidence/1.0';
|
|
2
|
+
export const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
3
|
+
export const ORG_PERMISSIONS = { board: ["dispatch", "accept", "reject", "stop", "reclaim", "replace"], dispatcher: ["dispatch", "reassign", "prioritize"], ops: ["heartbeat", "reclaim", "replace"],
|
|
4
|
+
"security-guard": ["block", "alert", "quarantine"], coordinator: ["conflict-scan", "lock", "queue", "baseline-handshake", "dependency-wait", "wake", "need-human"], worker: ["claim", "report", "request-help"], };
|
|
5
|
+
export function text(value) { return String(value ?? ""); }
|
|
6
|
+
export function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
7
|
+
export function validId(value) { return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(value); }
|
|
8
|
+
export function finding(severity, ruleId, entityRef, message, evidence = {}) { return { severity, ruleId, entityRef, message, evidence }; }
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { lstat, opendir, readFile, realpath } from 'node:fs/promises'
|
|
2
|
+
import { resolve, relative } from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { withCoordinationState } from './swarm-coordinator-fs.mjs'
|
|
5
|
+
import { requireString } from './swarm-coordinator-model.mjs'
|
|
6
|
+
import { hasActiveWait, pendingDecision, eventRecord, routeEvent, addMessage } from './swarm-coordinator-waits.mjs'
|
|
7
|
+
import { TASKS_SCHEMA, describedTask, accessRequest, requestedTask, targetRequest, routingError,
|
|
8
|
+
event, requestView, receipt, requests, digest } from './swarm-task-routing-model.mjs'
|
|
9
|
+
|
|
10
|
+
function revokeTaskLocks(state, taskId, now) {
|
|
11
|
+
for (const lock of state.locks.filter(item => item.taskId === taskId && item.status === 'active')) {
|
|
12
|
+
lock.status = 'released'
|
|
13
|
+
lock.releasedAt = now
|
|
14
|
+
const released = eventRecord(lock.agentId, 'lock-released', { lockId: lock.lockId, resource: lock.resource, reason: 'task-handoff' }, now)
|
|
15
|
+
state.events.push(released)
|
|
16
|
+
routeEvent(state, released)
|
|
17
|
+
}
|
|
18
|
+
for (const queued of state.queue.filter(item => item.request.taskId === taskId && item.status === 'queued')) {
|
|
19
|
+
queued.status = 'rejected'
|
|
20
|
+
queued.reason = 'explicit-task-handoff'
|
|
21
|
+
queued.resolvedAt = now
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function continuationNotice(state, original, request, phase, now) {
|
|
25
|
+
const payload = { schemaVersion: 'swarm.task-continuation/1.0', requestId: request.requestId,
|
|
26
|
+
taskId: original.taskId, chainId: original.chainId, hostId: original.routing.hostId,
|
|
27
|
+
threadId: original.routing.threadId, goal: original.routing.goal, phase,
|
|
28
|
+
remainingRequirements: original.routing.requirements.filter(item => !original.routing.completedRequirementIds.includes(item.id)),
|
|
29
|
+
nextAction: original.routing.nextAction, refetchPaths: request.handoffPaths,
|
|
30
|
+
requiredAction: phase === 'returned' ? 'handoff-resume' : 'await-handoff-result',
|
|
31
|
+
freshAimlockSnapshotRequired: true, executionAuthorized: false }
|
|
32
|
+
const signal = eventRecord(original.agentId, 'handoff-' + phase, payload, now)
|
|
33
|
+
state.events.push(signal)
|
|
34
|
+
routeEvent(state, signal)
|
|
35
|
+
addMessage(state, 'task-continuation', 'coordinator', original.agentId, payload, now)
|
|
36
|
+
addMessage(state, 'task-continuation', 'coordinator', 'human', payload, now)
|
|
37
|
+
}
|
|
38
|
+
function handoffScope(task, request) {
|
|
39
|
+
const paths = request.targetPaths.length ? request.targetPaths : task.taskScope
|
|
40
|
+
if (!paths.every(path => task.taskScope.some(scope => path === scope || path.startsWith(scope + '/')))) {
|
|
41
|
+
routingError('SCOPE_REQUIRED', 'handoff cannot grant paths outside the original owner scope')
|
|
42
|
+
}
|
|
43
|
+
return paths
|
|
44
|
+
}
|
|
45
|
+
async function releaseHandoff(root, input) {
|
|
46
|
+
return withCoordinationState(root, state => {
|
|
47
|
+
const original = describedTask(state, input), request = accessRequest(state, original, input.requestId)
|
|
48
|
+
if (request.handoffTaskId !== original.taskId || !request.forceCurrent) routingError('HANDOFF_OWNER_REQUIRED', 'only the existing owner can release an explicitly requested handoff')
|
|
49
|
+
const summary = requireString(input.checkpointSummary, 'checkpointSummary')
|
|
50
|
+
if (original.routing.handoff !== null && original.routing.handoff.requestId === request.requestId) {
|
|
51
|
+
return { state, output: { schemaVersion: TASKS_SCHEMA, request: requestView(request), released: true }, audit: [] }
|
|
52
|
+
}
|
|
53
|
+
if (request.status !== 'pending-handoff' || original.status !== 'active'
|
|
54
|
+
|| original.routing.handoff !== null || pendingDecision(state, original) || hasActiveWait(state, original)) {
|
|
55
|
+
routingError('HANDOFF_NOT_READY', 'the owner must reach an active safe checkpoint before releasing this work')
|
|
56
|
+
}
|
|
57
|
+
const target = requestedTask(state, original, request.targetTaskId)
|
|
58
|
+
if (target.status !== 'active' || target.routing.handoff !== null || pendingDecision(state, target) || hasActiveWait(state, target)) {
|
|
59
|
+
routingError('TARGET_SUSPENDED', 'the receiving task is not ready for handoff')
|
|
60
|
+
}
|
|
61
|
+
if (requests(state).some(other => other.requestId !== request.requestId && other.targetTaskId === target.taskId
|
|
62
|
+
&& other.handoffTaskId !== null && ['pending-delivery', 'accepted'].includes(other.status))) {
|
|
63
|
+
routingError('HANDOFF_BUSY', 'finish the existing scoped handoff before receiving another')
|
|
64
|
+
}
|
|
65
|
+
const paths = handoffScope(original, request), now = new Date().toISOString()
|
|
66
|
+
original.routing.handoff = { requestId: request.requestId, checkpointSummary: summary,
|
|
67
|
+
checkpoint: { nextAction: original.routing.nextAction, completedRequirementIds: [...original.routing.completedRequirementIds] },
|
|
68
|
+
releasedAt: now }
|
|
69
|
+
original.status = 'waiting'
|
|
70
|
+
revokeTaskLocks(state, original.taskId, now)
|
|
71
|
+
request.handoffPaths = paths
|
|
72
|
+
continuationNotice(state, original, request, 'paused', now)
|
|
73
|
+
request.previousTargetScope = [...target.taskScope]
|
|
74
|
+
target.taskScope = [...new Set([...target.taskScope, ...paths])]
|
|
75
|
+
target.baselineHandshake = null
|
|
76
|
+
request.status = 'pending-delivery'
|
|
77
|
+
event(request, 'handoff-released', { ownerTaskId: original.taskId, targetTaskId: target.taskId,
|
|
78
|
+
checkpointSummary: summary, paths, freshAimlockSnapshotRequired: true })
|
|
79
|
+
return { state, output: { schemaVersion: TASKS_SCHEMA, request: requestView(request), released: true,
|
|
80
|
+
freshAimlockSnapshotRequired: true }, audit: [{ event: 'handoff-released', requestId: request.requestId,
|
|
81
|
+
taskId: original.taskId, targetTaskId: target.taskId, paths }] }
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
async function completeMessage(root, input) {
|
|
85
|
+
return withCoordinationState(root, state => {
|
|
86
|
+
const { task, request } = targetRequest(state, input)
|
|
87
|
+
const resultSummary = requireString(input.resultSummary, 'resultSummary')
|
|
88
|
+
if (request.status === 'completed') {
|
|
89
|
+
if (request.resultSummary !== resultSummary) routingError('RESULT_CONFLICT', 'completed request already has a different result')
|
|
90
|
+
return { state, output: { schemaVersion: TASKS_SCHEMA, request: requestView(request), receipt: receipt(request) }, audit: [] }
|
|
91
|
+
}
|
|
92
|
+
if (request.status !== 'accepted' || request.receiptId === null) routingError('ACCEPT_REQUIRED', 'only accepted requests can complete')
|
|
93
|
+
if (task.status !== 'active' || task.routing.handoff !== null || hasActiveWait(state, task) || pendingDecision(state, task)) {
|
|
94
|
+
routingError('TASK_SUSPENDED', 'a suspended task cannot complete work')
|
|
95
|
+
}
|
|
96
|
+
const now = new Date().toISOString()
|
|
97
|
+
request.status = 'completed'
|
|
98
|
+
request.resultSummary = resultSummary
|
|
99
|
+
request.completedAt = now
|
|
100
|
+
if (request.handoffTaskId !== null) {
|
|
101
|
+
const original = requestedTask(state, task, request.handoffTaskId)
|
|
102
|
+
if (original.status !== 'waiting' || original.routing.handoff === null || original.routing.handoff.requestId !== request.requestId) {
|
|
103
|
+
routingError('HANDOFF_STATE_INVALID', 'the original task no longer owns this handoff checkpoint')
|
|
104
|
+
}
|
|
105
|
+
original.routing.handoff = null
|
|
106
|
+
original.status = 'blocked'
|
|
107
|
+
original.blockedReason = 'baseline-mismatch'
|
|
108
|
+
original.baselineHandshake = null
|
|
109
|
+
revokeTaskLocks(state, task.taskId, now)
|
|
110
|
+
task.taskScope = request.previousTargetScope
|
|
111
|
+
task.baselineHandshake = null
|
|
112
|
+
event(request, 'handoff-returned', { originalTaskId: original.taskId, refetchPaths: request.handoffPaths,
|
|
113
|
+
requiredAction: 'refetch-and-verify-baseline-before-resume' })
|
|
114
|
+
continuationNotice(state, original, request, 'returned', now)
|
|
115
|
+
}
|
|
116
|
+
event(request, 'message-completed', { resultSummary })
|
|
117
|
+
return { state, output: { schemaVersion: TASKS_SCHEMA, request: requestView(request), receipt: receipt(request) },
|
|
118
|
+
audit: [{ event: 'message-completed', taskId: task.taskId, requestId: request.requestId }] }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
export const TASK_HANDOFF_HANDLERS = Object.freeze({ 'handoff-release': releaseHandoff, 'message-complete': completeMessage, 'handoff-resume': resumeHandoff })
|
|
122
|
+
|
|
123
|
+
const MAX_BASELINE_ENTRIES = 1_000
|
|
124
|
+
const MAX_BASELINE_BYTES = 16 * 1_024 * 1_024
|
|
125
|
+
async function baselineFingerprint(rootValue, paths) {
|
|
126
|
+
const root = await realpath(rootValue), entries = new Map(), visited = new Set()
|
|
127
|
+
let bytes = 0
|
|
128
|
+
async function inspect(target) {
|
|
129
|
+
if (visited.has(target)) return
|
|
130
|
+
if (visited.size >= MAX_BASELINE_ENTRIES) routingError('BASELINE_LIMIT', 'handoff baseline needs a narrower approved scope')
|
|
131
|
+
visited.add(target)
|
|
132
|
+
let status
|
|
133
|
+
try { status = await lstat(target) } catch (error) {
|
|
134
|
+
if (error.code !== 'ENOENT') throw error
|
|
135
|
+
entries.set(relative(root, target), 'missing')
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (status.isSymbolicLink() || await realpath(target) !== target) routingError('BASELINE_INVALID', 'handoff baseline cannot traverse symbolic links')
|
|
139
|
+
if (status.isDirectory()) {
|
|
140
|
+
const directory = await opendir(target)
|
|
141
|
+
for await (const entry of directory) await inspect(resolve(target, entry.name))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (!status.isFile()) routingError('BASELINE_INVALID', 'handoff baseline contains a non-regular file')
|
|
145
|
+
const key = relative(root, target)
|
|
146
|
+
if (entries.has(key)) return
|
|
147
|
+
bytes += status.size
|
|
148
|
+
if (bytes > MAX_BASELINE_BYTES) routingError('BASELINE_LIMIT', 'handoff baseline needs a narrower approved scope')
|
|
149
|
+
entries.set(key, createHash('sha256').update(await readFile(target)).digest('hex'))
|
|
150
|
+
}
|
|
151
|
+
for (const path of paths) {
|
|
152
|
+
const target = resolve(root, path)
|
|
153
|
+
if (target === root || !target.startsWith(root + '/')) routingError('BASELINE_INVALID', 'handoff path escapes its workspace')
|
|
154
|
+
await inspect(target)
|
|
155
|
+
}
|
|
156
|
+
const files = [...entries].sort(([left], [right]) => left.localeCompare(right))
|
|
157
|
+
return { baselineHash: digest(files), files }
|
|
158
|
+
}
|
|
159
|
+
async function resumeHandoff(root, input) {
|
|
160
|
+
return withCoordinationState(root, async state => {
|
|
161
|
+
const task = describedTask(state, input), request = accessRequest(state, task, input.requestId)
|
|
162
|
+
if (request.handoffTaskId !== task.taskId || request.status !== 'completed') routingError('HANDOFF_NOT_COMPLETE', 'only the original task can resume a completed handoff')
|
|
163
|
+
const resumed = request.history.find(item => item.type === 'handoff-resumed')
|
|
164
|
+
if (resumed) return { state, output: { schemaVersion: TASKS_SCHEMA, ...resumed.details, replayed: true, currentStatus: task.status, historicalBaseline: true }, audit: [] }
|
|
165
|
+
if (task.status !== 'blocked' || task.blockedReason !== 'baseline-mismatch' || task.routing.handoff !== null
|
|
166
|
+
|| hasActiveWait(state, task) || pendingDecision(state, task)) routingError('TASK_SUSPENDED', 'other task dependencies still prevent resumption')
|
|
167
|
+
const baseline = await baselineFingerprint(root, request.handoffPaths)
|
|
168
|
+
const previousBaselineHash = task.baselineHash
|
|
169
|
+
task.baselineHash = baseline.baselineHash
|
|
170
|
+
task.baselineHandshake = null
|
|
171
|
+
task.status = 'active'
|
|
172
|
+
task.blockedReason = null
|
|
173
|
+
task.routing.revision += 1
|
|
174
|
+
const result = { taskId: task.taskId, baselineHash: baseline.baselineHash, previousBaselineHash,
|
|
175
|
+
refetchedFiles: baseline.files, freshAimlockSnapshotRequired: true }
|
|
176
|
+
event(request, 'handoff-resumed', result)
|
|
177
|
+
return { state, output: { schemaVersion: TASKS_SCHEMA, ...result, replayed: false },
|
|
178
|
+
audit: [{ event: 'handoff-resumed', taskId: task.taskId, requestId: request.requestId, baselineHash: baseline.baselineHash }] }
|
|
179
|
+
})
|
|
180
|
+
}
|