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/swarm-runtime.mjs CHANGED
@@ -1,29 +1,24 @@
1
- // swarm v7.0.10:自包含、无外部依赖的确定性蜂群编排运行时。
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"; const TEST_EVIDENCE_SCHEMA = "cli.tax.test-evidence/1.0";
6
- const COMPILER_NAME = "swarm"; const COMPILER_VERSION = "v7.0.37";
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 && ["assigned", "claimed", "running"].includes(task.status)) {
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: "How many worker sub-agents should the brain create?", required: false, example: "6" },
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: "number", minimum: 1, 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"]),
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", "dispatcher", "ops", "security-guard", "coordinator"],
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", "dispatcher", "ops", "security-guard", "coordinator"], workerCount: org.layers.execution.length,
413
- nextStep: blueprintEnabled
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 || "dispatcher"));
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 reported or accepted and every TestEvidence item is valid with exitCode 0",
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.status = "backlog"; task.owner = null;
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
+ }