cli-swarm 7.0.38 → 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.
@@ -1,32 +1,11 @@
1
1
  # Swarm 组织架构
2
- ## 协议与层级
3
2
 
4
- `org-chart` 返回 `swarm.org-chart/1.0` JSON,包含三层:
3
+ 默认主代理执行。org-chart没有任务事实时返回0个worker,nextStep.operation为null,由主代理继续工作。
5
4
 
6
- - `board`:决策层,负责派单、验收、停止和回收等决策。
7
- - `management`:固定的 `dispatcher`、`ops`、`security-guard`。
8
- - `execution`:按 `workerCount` 生成的 `worker-NNN` 描述项。
5
+ input可包含workerCount(0–50的整数上限)、projectName、tasks、blueprintEnabled。每个tasks[].facts采用[task-lifecycle.md](task-lifecycle.md)的业务必要性合同。简单、缺失理由、收益不高于协调成本和路径冲突的任务不计入可委派任务。实际数量不超过有依据的独立范围数、依赖图层宽和用户上限,禁止人为夸大估算以增加数量。
9
6
 
10
- 返回的 `permissions` 是角色动作表。实际运行时会在 `dispatch`、`accept` 等关键状态迁移中再检查角色,不应只依赖展示用的权限表。
7
+ org.delegationDecisions逐任务记录delegate及拒绝理由;org.executionMode为single-agent或delegated。recommendedWorkerCount为本次规划的数量上限,宿主应只创建当前就绪任务需要的最少执行者,先复用已有负责人。
11
8
 
12
- ## 输入与输出
9
+ board是主代理;management包含dispatcher、ops、security-guard、coordinator逻辑职责,hostAgentId均为board。fixedAgents只含board,buildAgents不为这些管理职责添加独立智能体。
13
10
 
14
- `input` 可包含 `workerCount`、`projectName`、`tasks` 和 `blueprintEnabled`。当前 `workerCount` 默认为 4,并限制在 1–50。成功输出包含 `org`、`fixedAgents`、`workerCount`、`blueprintEnabled` 和 `nextStep`。
15
-
16
- 如果输入中提供非空 `tasks`,运行时会分析依赖图,把每个无环层的最大宽度(上限 50)返回为 `recommendedWorkerCount`。建议值不会覆盖已生成的 execution worker 数量。
17
-
18
- ## 依赖图错误
19
-
20
- 以下情况会产生结构化 findings 并阻断 `org-chart`:
21
-
22
- - `taskId` 不合法或重复;
23
- - `dependsOn` 引用不存在的任务;
24
- - 依赖图存在环。
25
-
26
- ## 当前边界
27
-
28
- `org-chart` 只生成组织 JSON,不创建真实子智能体、不建立心跳连接、不存储状态。调用方负责根据组织描述建立实际执行者,并保存每次返回的完整状态。
29
-
30
- ## 实现依据
31
-
32
- `swarm-runtime.mjs` 的 `ORG_LAYERS`、`ORG_PERMISSIONS`、`analyzeTaskGraph`、`buildOrgChart` 和 `validateOrgChart` 是本文的权威来源。
11
+ 重复或非法taskId、缺失依赖和依赖环仍阻断。运行时只生成组织数据,不创建实际智能体,不提供对未接入IDE的物理拦截;宿主必须执行相同门禁。启用Aimlock或Swarm并不代表需要多智能体。
@@ -9,10 +9,10 @@
9
9
 
10
10
  | 操作 | 前置 | 成功结果 |
11
11
  |---|---|---|
12
- | `dispatch` | 调用角色是 `dispatcher` 或 `board`;任务为 `backlog`;所有依赖为 `accepted` | `assigned`,写入 owner 和 assignedBy |
12
+ | `dispatch` | 调用角色是 `dispatcher` 或 `board`;任务为 `backlog`;所有依赖为 `accepted` 且有任务绑定的可信证据;提交符合门槛且无冲突的 `input.facts` | `assigned`,写入 owner 和 assignedBy |
13
13
  | `claim` | 任务为 `assigned`;已有 owner 时必须与 workerId 一致 | `claimed`,写入 claimedAt |
14
14
  | `report` | 任务为 `claimed` 或 `running`;worker 与 owner 一致;report 至少含非空 output 或 evidence 字段 | `reported`,写入 report 和 reportedAt |
15
- | `accept` | 只允许 `board`;任务为 `reported`;`accept: true` 时必须有通过的 TestEvidence | `accepted`;拒绝时为 `failed` |
15
+ | `accept` | 只允许 `board`;任务为 `reported`;`accept: true` 时必须有 Validator 验证通过的任务绑定签名 TestEvidence | `accepted`;拒绝时为 `failed` |
16
16
 
17
17
  操作失败会返回对应的 `DISPATCH_FAILED`、`CLAIM_FAILED`、`REPORT_FAILED` 或 `ACCEPT_FAILED` finding,不返回伪造成功。
18
18
 
@@ -20,6 +20,14 @@
20
20
 
21
21
  `dispatch`、`claim`、`report`、`accept` 会修改调用方传入的 `tasks` 数组并返回整个数组。调用方必须保存该完整返回值,下一次操作时原样传回;服务端无会话状态,不能只合并一个 task 片段。
22
22
 
23
+ ## 派单事实与可信验收
24
+
25
+ `input.facts` 必须包含 `ownerId`、`estimatedChangedLines`、`fileCount`、`crossModule`、`parallelSafe` 与精确相对 `targetPaths`。规模至少满足200行、3文件或跨模块之一;文件数必须与路径数一致,活动写入路径不得冲突。规模达标不能代替业务必要性。
26
+
27
+ 还必须包含 `delegation`:非空 `businessNeed`、`deliverable`、`acceptanceCriteria`、`mainAgentWork`;`independent` 与 `substantial` 均为 true;有限非负数 `estimatedSavedMinutes` 严格大于 `coordinationMinutes`。估算包含上下文传递、协调和验收成本,禁止虚增估算。简单工作或缺失依据明确拒绝派单,主代理继续执行。组织规划使用相同的 `tasks[].facts`,并在 `delegationDecisions` 返回逐任务理由。
28
+
29
+ 任务在开始前声明 `validationContext`,其 `validationRunId` 必须等于 `taskId`,并绑定计划与产物。`report.evidence` 的可信签名由 Validator 公钥验证;仅本地自报或命令退出码为0仍不能验收为绿色。Blueprint 编译只提供计划,调用方必须补充实际派单事实和可信执行凭证。
30
+
23
31
  ## Blueprint 桥接
24
32
 
25
33
  `blueprint-bridge` 要求非空 `projectName`、非空 tasks、每任务 title 和合法依赖图。成功时它生成 `blueprint.ir/1.0` 与 Blueprint `compile-inline` 请求信封,包含任务节点、依赖边和验收标准。
@@ -16,13 +16,13 @@
16
16
 
17
17
  ## “通过证据”定义
18
18
 
19
- 任务的 `report.evidence` 必须非空,每一项都通过上述结构校验且 `exitCode === 0`,才算 passing TestEvidence。`accept: true` 没有满足该条件时会被阻断。
19
+ 任务的 `report.evidence` 必须非空且exitCode0accept还要求task.validationContext.validationRunId等于taskId,并复用Validator验证subject、artifactSha256、receipt签名、结果与有效期。缺少可信公钥或任一不匹配均阻断。
20
20
 
21
21
  ## 灯色规则
22
22
 
23
23
  | 任务状态 | 条件 | 灯色 |
24
24
  |---|---|---|
25
- | `reported` | 有 passing TestEvidence | green |
25
+ | `reported` | 有 passing TestEvidence,尚未验收 | yellow |
26
26
  | `reported` | 缺少 passing TestEvidence | yellow |
27
27
  | `accepted` | 有 passing TestEvidence | green |
28
28
  | `accepted` | 无 passing TestEvidence | red |
@@ -33,7 +33,7 @@
33
33
 
34
34
  ## 证据信任边界
35
35
 
36
- 当前 Swarm 只校验证据字段和 `exitCode`。它不验证签名、receipt、subject 绑定或 runner 身份;`runner: trusted-runner` 仅是字段值,不得据此声称已完成密码学证明。调用方仍需在 Validator 或受信执行层完成强证据绑定。
36
+ Swarm与Validator共享签名验证实现;单独填写runner字段不能通过。validationContext必须由宿主预先锁定,纯函数传入的角色和任务清单仍需宿主认证,不得宣称远端纯函数拥有持久任务身份控制。
37
37
 
38
38
  ## 实现依据
39
39
 
package/skill/skill.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "name": "swarm",
7
7
  "schemaVersion": "swarm.skill.request/1.0",
8
8
  "type": "Skill",
9
- "version": "v7.0.38"
9
+ "version": "v7.0.39"
10
10
  }
@@ -5,6 +5,7 @@ const ACTIONS = new Set(['inspect', 'modify', 'build', 'deploy', 'publish', 'wai
5
5
  const LOCK_TYPES = new Set(['file', 'build', 'deploy'])
6
6
  const MESSAGE_TYPES = new Set([
7
7
  'range-declare',
8
+ 'task-continuation',
8
9
  'conflict-alert',
9
10
  'lock-granted',
10
11
  'lock-denied',
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.38";
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 }; }
@@ -3,7 +3,7 @@ import { resolve, relative } from 'node:path'
3
3
  import { createHash } from 'node:crypto'
4
4
  import { withCoordinationState } from './swarm-coordinator-fs.mjs'
5
5
  import { requireString } from './swarm-coordinator-model.mjs'
6
- import { hasActiveWait, pendingDecision, eventRecord, routeEvent } from './swarm-coordinator-waits.mjs'
6
+ import { hasActiveWait, pendingDecision, eventRecord, routeEvent, addMessage } from './swarm-coordinator-waits.mjs'
7
7
  import { TASKS_SCHEMA, describedTask, accessRequest, requestedTask, targetRequest, routingError,
8
8
  event, requestView, receipt, requests, digest } from './swarm-task-routing-model.mjs'
9
9
 
@@ -21,6 +21,20 @@ function revokeTaskLocks(state, taskId, now) {
21
21
  queued.resolvedAt = now
22
22
  }
23
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
+ }
24
38
  function handoffScope(task, request) {
25
39
  const paths = request.targetPaths.length ? request.targetPaths : task.taskScope
26
40
  if (!paths.every(path => task.taskScope.some(scope => path === scope || path.startsWith(scope + '/')))) {
@@ -55,6 +69,7 @@ async function releaseHandoff(root, input) {
55
69
  original.status = 'waiting'
56
70
  revokeTaskLocks(state, original.taskId, now)
57
71
  request.handoffPaths = paths
72
+ continuationNotice(state, original, request, 'paused', now)
58
73
  request.previousTargetScope = [...target.taskScope]
59
74
  target.taskScope = [...new Set([...target.taskScope, ...paths])]
60
75
  target.baselineHandshake = null
@@ -94,8 +109,9 @@ async function completeMessage(root, input) {
94
109
  revokeTaskLocks(state, task.taskId, now)
95
110
  task.taskScope = request.previousTargetScope
96
111
  task.baselineHandshake = null
97
- event(request, 'handoff-returned', { originalTaskId: original.taskId, refetchPaths: request.targetPaths,
112
+ event(request, 'handoff-returned', { originalTaskId: original.taskId, refetchPaths: request.handoffPaths,
98
113
  requiredAction: 'refetch-and-verify-baseline-before-resume' })
114
+ continuationNotice(state, original, request, 'returned', now)
99
115
  }
100
116
  event(request, 'message-completed', { resultSummary })
101
117
  return { state, output: { schemaVersion: TASKS_SCHEMA, request: requestView(request), receipt: receipt(request) },
@@ -145,7 +161,7 @@ async function resumeHandoff(root, input) {
145
161
  const task = describedTask(state, input), request = accessRequest(state, task, input.requestId)
146
162
  if (request.handoffTaskId !== task.taskId || request.status !== 'completed') routingError('HANDOFF_NOT_COMPLETE', 'only the original task can resume a completed handoff')
147
163
  const resumed = request.history.find(item => item.type === 'handoff-resumed')
148
- if (resumed) return { state, output: { schemaVersion: TASKS_SCHEMA, ...resumed.details, replayed: true }, audit: [] }
164
+ if (resumed) return { state, output: { schemaVersion: TASKS_SCHEMA, ...resumed.details, replayed: true, currentStatus: task.status, historicalBaseline: true }, audit: [] }
149
165
  if (task.status !== 'blocked' || task.blockedReason !== 'baseline-mismatch' || task.routing.handoff !== null
150
166
  || hasActiveWait(state, task) || pendingDecision(state, task)) routingError('TASK_SUSPENDED', 'other task dependencies still prevent resumption')
151
167
  const baseline = await baselineFingerprint(root, request.handoffPaths)