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.
@@ -7,16 +7,16 @@
7
7
 
8
8
  ## 公开 `reclaim` 操作
9
9
 
10
- 输入为 `tasks`、`taskId` 和可选 `reason`。任务不存在时返回 `RECLAIM_FAILED`;已是 `backlog` 时返回 `RECLAIM_NOOP`。其他已找到状态当前都会被设为 `backlog`、清空 owner,并返回完整 tasks。
10
+ 输入为 `tasks`、`taskId` 和可选 `reason`。任务不存在时返回 `RECLAIM_FAILED`;已是 `backlog` 时返回 `RECLAIM_NOOP`。仅assigned可恢复backlog;claimed/running被标记blocked并返回完整状态与RECLAIM_REQUIRES_RECONCILIATION,终态保持不变。
11
11
 
12
- 公开 `reclaim` 不校验调用角色,也不限于 assigned/claimed/running。调用方必须在权威层控制调用者和可回收状态,不得将当前路由写成已完成权限强制。
12
+ 公开 `reclaim` 不校验调用角色,但已限制为尚未开工的assigned。调用方必须在权威层控制调用者和可回收状态,不得将当前路由写成已完成权限强制。
13
13
 
14
14
  ## 内部运维函数
15
15
 
16
16
  - `buildAgents`:从 org JSON 生成带状态、最后心跳、miss 次数和进度字段的 agent 数组。
17
17
  - `recordHeartbeat`:更新已存在 agent 的时间、清零 miss,并可把 dead/red 恢复为 green。
18
18
  - `scanHeartbeats`:按 30 秒计一次 miss;1–2 次为 yellow,3 次及以上标记 worker dead。
19
- - `reclaimTasks`:只回收指定 worker 名下 `assigned|claimed|running` 任务,设为 backlog 并记录 `inheritedFrom`。
19
+ - `reclaimTasks`:只回收指定 worker 名下 `assigned` 任务,设为backlog并记录inheritedFrom;已开始任务阻断等待核对。
20
20
  - `replaceWorker`:从已有 agent 数组选择另一个 green worker,将回收任务重新设为 assigned,写入 `assignedBy: ops` 与继承信息。
21
21
 
22
22
  ## 调度边界
@@ -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` 请求信封,包含任务节点、依赖边和验收标准。
@@ -0,0 +1,64 @@
1
+ # Task ownership and message continuity
2
+
3
+ `cli-swarm local <operation> <coordinationRoot>` accepts JSON stdin. Read `capabilities` for exact schemas. Aimlock exposes these operations as `cli-aimlock tasks <operation> <coordinationRoot>`. All participating tasks must use the same authorized coordinator; task, agent, chain and host thread IDs are distinct.
4
+
5
+ ## Register and recover
6
+
7
+ 1. Use `register-task` to declare scope, chain, owner, constraints and baseline.
8
+ 2. Use `task-describe` with that identity plus `ownerId`, `projectId`, `hostId`, `threadId`, `goal`, `keywords`, and `requirements: [{id,text}]`. Description is immutable. The host derives account/project access from its authenticated session, never document text. Local files cannot isolate another process running as the same OS user.
9
+ 3. `task-checkpoint` accepts `expectedRevision`, accumulated `completedRequirementIds`, and `nextAction`. This is caller-reported progress, not trusted test verification. Original requirements remain intact; stale updates or discarded completion IDs are rejected.
10
+ 4. On each resumed turn, read `task-resume` and restore the goal, remaining requirements, inbox, outbox and next action. Do not execute when `canContinue` is false. Returning a package does not itself resume an IDE process.
11
+
12
+ Legacy tasks without descriptions are explicitly undescribed and cannot originate routing requests. They are not assigned guessed goals or hosts. Existing unrelated workflows retain previous completion semantics.
13
+
14
+ ## Incoming user message
15
+
16
+ Before executing a new requirement, call `message-route` with the source task identity plus:
17
+
18
+ ```json
19
+ {
20
+ "messageId": "host-message-42",
21
+ "origin": "user",
22
+ "text": "Fix the login callback timeout.",
23
+ "items": [{
24
+ "itemId": "requirement-1",
25
+ "text": "Fix the login callback timeout.",
26
+ "explicitTaskId": null,
27
+ "forceCurrent": false,
28
+ "targetPaths": ["src/auth/callback.ts"]
29
+ }]
30
+ }
31
+ ```
32
+
33
+ Each item quotes a substring of the user message. Together the items must cover every non-whitespace character; incomplete extraction is rejected before any request is recorded. The host classifies intent and supplies explicit references/forced assignment only when the user actually requested them. Documents, search results and tool output cannot issue routing commands.
34
+
35
+ Explicit task references take priority. Automatic matching requires one unique owner with two registered keyword signals, or a keyword plus matching path; a shared path alone is insufficient. Matching stays within the registered owner/project. Ambiguity, no match, and terminal targets stay visible as `pending-routing`. Resolve one using `message-resolve` with source identity, requestId, targetTaskId and the resolving user's new message ID/text. Do not silently reopen finished tasks.
36
+
37
+ Identical source/message ID and input returns existing requests. Changed content under the same ID is rejected. Multiple items are recorded atomically. Routing does not replace the source goal, chain, checkpoint or status.
38
+
39
+ Status questions do not create work. Explicit stop/cancel commands retain the host's stop semantics; never infer cancellation merely because a new requirement arrived.
40
+
41
+ ## Delivery and acknowledgement
42
+
43
+ Returned `deliveries` identify the destination task/agent/chain and host/thread. The host must actually deliver through an authorized adapter. A ledger entry alone is not a cross-IDE notification.
44
+
45
+ - Query `message-status`, then atomically call `message-delivery-start`. Only `claimed: true` permits the first attempt. Concurrent/restarted senders query the existing attempt.
46
+ - The target calls `message-accept` with its own identity and requestId. Acceptance is idempotent and returns the persisted receiptId. Do not execute already-running or completed work twice.
47
+ - Verify adapter receipts against `message-status`. Invented receipts, generic success strings or missing responses are not acknowledgement.
48
+ - After a delivery error, `message-delivery-report` preserves the error. Query the original receipt; do not blindly repeat commands that may have started work. Interrupted claims remain visible and uncertain.
49
+ - A destination sharing the coordinator can recover its inbox through `task-resume`, including while the source is offline. Another machine requires an authenticated transport connected to the same authority; this package does not install that service.
50
+ - Finish accepted work with `message-complete` and a result summary. Request completion is distinct from original-task completion. Unfinished original requirements or unacknowledged work block task completion.
51
+
52
+ Aimlock's exported `handleTaskMessage(root,input,adapter,options)` performs claim/delivery/receipt checks and calls `adapter.continueTask` only when the source can continue. `adapter.deliver` addresses the returned destination and returns its real acknowledgement. Host integration supplies these functions; commands are never evaluated from message text. Delivery waits at most 30 seconds by default; an explicit positive `options.deliveryTimeoutMs` sets the host deadline. A timeout does not cancel the external delivery. The helper checks the real receipt, persists uncertainty and continues a runnable source; replay never blindly resends.
53
+
54
+ ## Explicit current-task takeover
55
+
56
+ `forceCurrent` with another identified owner produces `pending-handoff`. The existing owner reaches a safe checkpoint and calls `handoff-release`; this does not kill an in-flight write.
57
+
58
+ Release preserves both goals, pauses only that owner's task, revokes its leases/queued grants, and records scope/checkpoint. The receiver requires a fresh Aimlock scope/snapshot/write pass; old passes cannot authorize added paths. A receiver may have only one active scoped takeover.
59
+
60
+ After `message-complete`, receiver scope is restored, stale leases revoked, and the previous owner requires baseline refresh. Its host calls `handoff-resume`, which reads actual scoped fingerprints, checks other waits/decisions and restores only that task. Fresh Aimlock snapshots and revalidation remain required; old tests do not validate changed files.
61
+
62
+ ## Host integration boundary
63
+
64
+ The IDE must call the entry point on each user message and consume inboxes on resume. Updating a skill alone cannot intercept unintegrated hosts. Separate working directories must intentionally connect to the same authority; independent `.coord` folders do not coordinate. Do not scan unrelated users' conversations or copy entire histories/credentials to locate a task.
@@ -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.37"
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',
@@ -17,6 +18,53 @@ const WAIT_EVENT_ACTIONS = new Set(['wake-with-package'])
17
18
  const TASK_STATUSES = new Set(['active', 'waiting', 'completed', 'failed', 'reclaimed'])
18
19
  const SHA256_PATTERN = /^[0-9a-f]{64}$/
19
20
 
21
+ const DECISION_SCHEMA_TASK = 'swarm.coord-decision/2.0'
22
+ const DECISION_SCHEMA_AGENT = 'swarm.coord-decision/1.0'
23
+
24
+ function validateDecisionIdentifiers(value, label) {
25
+ const normalized = requireStringArray(value, label, { nonEmpty: true }).map((entry) => identifier(entry, label))
26
+ if (normalized.some((entry, index) => entry !== value[index])) {
27
+ coordinatorError('SWARM_COORD_DECISION_SCHEMA_INVALID', 'decision identifiers must use their canonical form')
28
+ }
29
+ }
30
+
31
+ function validateDecisionScope(decision) {
32
+ if (!decision || typeof decision !== 'object' || Array.isArray(decision)
33
+ || ![DECISION_SCHEMA_TASK, DECISION_SCHEMA_AGENT].includes(decision.schemaVersion)) {
34
+ coordinatorError('SWARM_COORD_DECISION_SCHEMA_INVALID', 'decision scope schema is unsupported')
35
+ }
36
+ if (!['pending', 'resolved'].includes(decision.status)) {
37
+ coordinatorError('SWARM_COORD_DECISION_SCHEMA_INVALID', 'decision status is invalid')
38
+ }
39
+ validateDecisionIdentifiers(decision.agents, 'decision.agents')
40
+ if (decision.schemaVersion === DECISION_SCHEMA_TASK) {
41
+ validateDecisionIdentifiers(decision.taskIds, 'decision.taskIds')
42
+ } else if (Object.hasOwn(decision, 'taskIds')) {
43
+ coordinatorError('SWARM_COORD_DECISION_SCHEMA_INVALID', 'legacy agent decisions cannot declare task scope')
44
+ }
45
+ }
46
+
47
+ function decisionTargetsTask(decision, task) {
48
+ validateDecisionScope(decision)
49
+ return decision.schemaVersion === DECISION_SCHEMA_TASK
50
+ ? decision.taskIds.includes(task.taskId) : decision.agents.includes(task.agentId)
51
+ }
52
+
53
+ function decisionResumesTask(decision, task) {
54
+ return decisionTargetsTask(decision, task) && decision.status === 'resolved'
55
+ && (decision.answer === 'resume-task:' + task.taskId || decision.answer === 'resume:' + task.agentId)
56
+ }
57
+
58
+ function decisionTasks(state, decision) {
59
+ validateDecisionScope(decision)
60
+ const tasks = state.tasks.filter((task) => decisionTargetsTask(decision, task))
61
+ if (decision.schemaVersion === DECISION_SCHEMA_TASK
62
+ && (tasks.length !== decision.taskIds.length || tasks.some((task) => !decision.agents.includes(task.agentId)))) {
63
+ coordinatorError('SWARM_COORD_DECISION_TARGET_INVALID', 'decision targets must identify registered tasks owned by its agents')
64
+ }
65
+ return tasks
66
+ }
67
+
20
68
  function findTask(state, input) {
21
69
  const taskId = identifier(input.taskId, 'taskId')
22
70
  const task = state.tasks.find((item) => item.taskId === taskId)
@@ -278,6 +326,10 @@ function validateInputShape(input, schema) {
278
326
  }
279
327
 
280
328
  export {
329
+ DECISION_SCHEMA_TASK,
330
+ decisionTargetsTask,
331
+ decisionResumesTask,
332
+ decisionTasks,
281
333
  findTask,
282
334
  coordinationMessage,
283
335
  detectWaitCycles,
@@ -1,13 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { coordinatorError, identifier, readCoordinationState, withCoordinationState } from './swarm-coordinator-fs.mjs'
3
- import { coordinationMessage, detectWaitCycles, findTask, normalizeWait, requireString, requireTaskStatus } from './swarm-coordinator-model.mjs'
3
+ import { DECISION_SCHEMA_TASK, coordinationMessage, decisionTargetsTask, decisionTasks, detectWaitCycles, findTask, normalizeWait, requireString, requireTaskStatus } from './swarm-coordinator-model.mjs'
4
+
5
+ import { assertTaskRequestsCompleted } from './swarm-task-routing-model.mjs'
4
6
 
5
7
  const LOCAL_SCHEMA = 'swarm.coordinator-local/1.0'
6
8
  const WAIT_POLL_MS = 250
9
+ const BASELINE_BLOCKED_REASON = 'baseline-mismatch'
7
10
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'reclaimed'])
8
11
 
9
12
  function pendingDecision(state, task) {
10
- return state.decisions.some((decision) => decision.status === 'pending' && decision.agents.includes(task.agentId))
13
+ return state.decisions.some((decision) => decisionTargetsTask(decision, task) && decision.status === 'pending')
11
14
  }
12
15
 
13
16
  function hasActiveWait(state, task) {
@@ -36,7 +39,7 @@ function wakeWait(state, wait, resolution, event, now) {
36
39
  wait.resolution = event
37
40
  const task = state.tasks.find((item) => item.taskId === wait.taskId)
38
41
  if (task && task.status === 'waiting' && !hasActiveWait(state, task) && !pendingDecision(state, task)) {
39
- task.status = 'active'
42
+ task.status = task.blockedReason === BASELINE_BLOCKED_REASON ? 'blocked' : 'active'
40
43
  task.updatedAt = now
41
44
  }
42
45
  if (resolution === 'event-received') {
@@ -55,7 +58,9 @@ function routeEvent(state, event) {
55
58
  }
56
59
 
57
60
  function confirmationRequest(decision) {
58
- const options = decision.agents.map((agentId) => ({ id: 'resume:' + agentId, label: '先恢复 ' + agentId, hint: '唤醒该智能体先解除依赖' }))
61
+ const options = decision.schemaVersion === DECISION_SCHEMA_TASK
62
+ ? decision.taskIds.map((taskId) => ({ id: 'resume-task:' + taskId, label: '先恢复任务 ' + taskId, hint: '仅恢复所选任务,其他任务保持原状态' }))
63
+ : decision.agents.map((agentId) => ({ id: 'resume:' + agentId, label: '先恢复 ' + agentId, hint: '仅在对应一个任务时恢复' }))
59
64
  options.push({ id: 'abort', label: '终止等待', hint: '终止相关等待并保持任务阻塞' })
60
65
  return { schemaVersion: 'confirm-protocol.skill.request/1.0', requestId: 'confirm-' + decision.decisionId,
61
66
  operation: 'interaction-request', input: { interaction: {
@@ -65,19 +70,20 @@ function confirmationRequest(decision) {
65
70
  } } }
66
71
  }
67
72
 
68
- function createDecision(state, kind, agents, waitIds, question, riskDescription, now) {
69
- const decision = { schemaVersion: 'swarm.coord-decision/1.0', decisionId: 'decision-' + randomUUID(),
70
- kind, agents: [...new Set(agents)], waitIds: [...new Set(waitIds)], question, riskDescription,
73
+ function createDecision(state, kind, agents, waitIds, question, riskDescription, now, taskIds) {
74
+ const decision = { schemaVersion: DECISION_SCHEMA_TASK, decisionId: 'decision-' + randomUUID(),
75
+ kind, agents: [...new Set(agents)], taskIds, waitIds: [...new Set(waitIds)], question, riskDescription,
71
76
  status: 'pending', answer: null, actorId: null, createdAt: now, resolvedAt: null }
77
+ const targets = decisionTasks(state, decision)
72
78
  state.decisions.push(decision)
73
- for (const task of state.tasks.filter((item) => agents.includes(item.agentId) && !TERMINAL_STATUSES.has(item.status))) {
79
+ for (const task of targets.filter((item) => !TERMINAL_STATUSES.has(item.status))) {
74
80
  task.status = 'blocked'
75
- task.blockedReason = 'human-decision'
81
+ if (task.blockedReason !== BASELINE_BLOCKED_REASON) task.blockedReason = 'human-decision'
76
82
  task.updatedAt = now
77
83
  }
78
84
  const waits = state.waits.filter((wait) => waitIds.includes(wait.waitId))
79
85
  .map(({ waitId, waiter, waitFor, event, purpose, deadlineAt }) => ({ waitId, waiter, waitFor, event, purpose, deadlineAt }))
80
- addMessage(state, 'need-human', 'coordinator', 'human', { decisionId: decision.decisionId, kind, agents, waits }, now)
86
+ addMessage(state, 'need-human', 'coordinator', 'human', { decisionId: decision.decisionId, kind, agents, taskIds, waits }, now)
81
87
  return { decision, status: 'blocked', confirmationRequired: true, confirmProtocolRequest: confirmationRequest(decision),
82
88
  nextStep: { operation: 'confirm-protocol', instruction: 'Invoke Confirm Protocol and wait for the human answer.' } }
83
89
  }
@@ -90,7 +96,7 @@ function applyCycles(state, now) {
90
96
  if (!waits.length) continue
91
97
  requests.push(createDecision(state, 'dependency-cycle', agents, waits.map((wait) => wait.waitId),
92
98
  '检测到依赖等待成环:' + agents.join(' → ') + '。请选择先恢复的智能体。',
93
- '依赖成环会导致全部相关任务无限等待,必须由真人决定执行顺序。', now))
99
+ '依赖成环会导致全部相关任务无限等待,必须由真人决定执行顺序。', now, [...new Set(waits.map((wait) => wait.taskId))]))
94
100
  const dependencies = waits.map(({ waiter, waitFor, event, purpose }) => ({ waiter, waitFor, event, purpose }))
95
101
  for (const wait of waits) wakeWait(state, wait, 'deadlock-interrupted', { cycle, dependencies }, now)
96
102
  }
@@ -173,6 +179,7 @@ async function taskStatus(repositoryRoot, input) {
173
179
  if (status === 'active' && (pendingDecision(state, task) || hasActiveWait(state, task) || task.status === 'blocked')) {
174
180
  coordinatorError('SWARM_COORD_TASK_BLOCKED', 'resolve dependencies or the human decision before activation')
175
181
  }
182
+ if (status === 'completed' && task.routing) assertTaskRequestsCompleted(state, task)
176
183
  const now = new Date().toISOString()
177
184
  const undeclaredWait = status === 'waiting' && !hasActiveWait(state, task)
178
185
  task.status = undeclaredWait ? 'blocked' : status
@@ -212,7 +219,7 @@ function applyTimeouts(state, now) {
212
219
  if (wait.onTimeout === 'escalate-need-human' || count >= 2) {
213
220
  decisions.push(createDecision(state, 'dependency-timeout', [wait.waiter, wait.waitFor], [wait.waitId],
214
221
  wait.waiter + ' 等待 ' + wait.waitFor + ' 的 ' + wait.event + ' 已超时,请选择后续动作。',
215
- '依赖事件未在声明期限内到达,继续静默等待可能导致任务停滞。', now))
222
+ '依赖事件未在声明期限内到达,继续静默等待可能导致任务停滞。', now, [wait.taskId]))
216
223
  wakePackages.push(wakeWait(state, wait, 'timeout-interrupted', { timeoutCount: count }, now))
217
224
  } else {
218
225
  const resolution = wait.onTimeout === 'abandon-wait' ? 'timeout-abandoned' : 'timeout-continued'
@@ -222,29 +229,42 @@ function applyTimeouts(state, now) {
222
229
  return { timedOut, decisions, wakePackages }
223
230
  }
224
231
 
232
+ function selectedDecisionTask(state, decision, answer) {
233
+ const tasks = decisionTasks(state, decision).filter((task) => !TERMINAL_STATUSES.has(task.status))
234
+ if (answer === 'abort') return null
235
+ if (answer.startsWith('resume-task:')) {
236
+ const selected = tasks.find((task) => answer === 'resume-task:' + task.taskId)
237
+ if (selected) return selected
238
+ } else if (answer.startsWith('resume:')) {
239
+ const matching = tasks.filter((task) => answer === 'resume:' + task.agentId)
240
+ if (matching.length === 1) return matching[0]
241
+ if (matching.length > 1) {
242
+ coordinatorError('SWARM_COORD_DECISION_ANSWER_AMBIGUOUS', 'agent owns multiple decision tasks; select a task explicitly')
243
+ }
244
+ }
245
+ coordinatorError('SWARM_COORD_DECISION_ANSWER_INVALID', 'answer must select a nonterminal task declared by this decision')
246
+ }
247
+
225
248
  async function resolveHuman(repositoryRoot, input) {
226
249
  return withCoordinationState(repositoryRoot, async (state) => {
227
250
  const decisionId = identifier(input.decisionId, 'decisionId')
228
251
  const decision = state.decisions.find((item) => item.decisionId === decisionId)
229
252
  if (!decision || decision.status !== 'pending') coordinatorError('SWARM_COORD_DECISION_NOT_PENDING', 'decision is not pending')
230
253
  const answer = requireString(input.answer, 'answer')
231
- if (![...decision.agents.map((agent) => 'resume:' + agent), 'abort'].includes(answer)) {
232
- coordinatorError('SWARM_COORD_DECISION_ANSWER_INVALID', 'answer is not a declared option')
233
- }
254
+ const selectedTask = selectedDecisionTask(state, decision, answer)
234
255
  const now = new Date().toISOString()
235
256
  decision.status = 'resolved'
236
257
  decision.answer = answer
237
258
  decision.actorId = identifier(input.actorId, 'actorId')
238
259
  decision.resolvedAt = now
239
- for (const task of state.tasks.filter((item) => answer === 'resume:' + item.agentId && !TERMINAL_STATUSES.has(item.status))) {
240
- if (!pendingDecision(state, task)) {
241
- task.status = hasActiveWait(state, task) ? 'waiting' : 'active'
242
- task.blockedReason = null
243
- task.updatedAt = now
244
- }
260
+ if (selectedTask && !pendingDecision(state, selectedTask) && selectedTask.blockedReason === 'human-decision') {
261
+ selectedTask.status = hasActiveWait(state, selectedTask) ? 'waiting' : 'active'
262
+ selectedTask.blockedReason = null
263
+ selectedTask.updatedAt = now
245
264
  }
246
265
  return { state, output: { schemaVersion: LOCAL_SCHEMA, decision },
247
- audit: [{ event: 'decision-resolved', decisionId, answer, actorId: decision.actorId }] }
266
+ audit: [{ event: 'decision-resolved', decisionId, answer, actorId: decision.actorId,
267
+ taskId: selectedTask === null ? null : selectedTask.taskId }] }
248
268
  })
249
269
  }
250
270
 
@@ -266,13 +286,13 @@ async function waitForEvent(repositoryRoot, input, tick) {
266
286
  const task = findTask(before, input)
267
287
  const wait = before.waits.find((item) => item.waitId === waitId && item.taskId === task.taskId)
268
288
  if (!wait) coordinatorError('SWARM_COORD_WAIT_NOT_FOUND', 'wait is not registered for this task')
269
- const pending = before.decisions.filter((item) => item.status === 'pending' && item.agents.includes(task.agentId))
289
+ const pending = before.decisions.filter((item) => decisionTargetsTask(item, task) && item.status === 'pending')
270
290
  if (wait.status === 'active' && pending.length) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked',
271
291
  wait, wakePackage: null, decisions: pending, confirmProtocolRequests: pending.map(confirmationRequest) }
272
292
  if (wait.status !== 'active') {
273
293
  const message = before.messages.findLast((item) => item.payload.waitId === waitId && item.payload.schemaVersion === 'swarm.wake-package/1.0')
274
294
  if (!message) coordinatorError('SWARM_COORD_WAKE_PACKAGE_MISSING', 'resolved wait has no wake package')
275
- const decisions = before.decisions.filter((item) => item.status === 'pending' && item.agents.includes(task.agentId))
295
+ const decisions = before.decisions.filter((item) => decisionTargetsTask(item, task) && item.status === 'pending')
276
296
  return { schemaVersion: LOCAL_SCHEMA, status: decisions.length || task.status !== 'active' ? 'blocked' : 'resolved',
277
297
  wait, wakePackage: message.payload, decisions, confirmProtocolRequests: decisions.map(confirmationRequest) }
278
298
  }
@@ -1,16 +1,19 @@
1
+ import { TASK_ROUTING_SCHEMAS } from './swarm-task-routing-schemas.mjs'
2
+ import { TASK_ROUTING_HANDLERS } from './swarm-task-routing.mjs'
1
3
  import { createHash, randomUUID, verify } from 'node:crypto'
2
4
  import { lstat, readFile } from 'node:fs/promises'
3
5
  import { resolve } from 'node:path'
4
6
  import { LEASE_SCHEMA, coordinatorError, identifier, leasePayload, readCoordinationState,
5
7
  withCoordinationReadLock, withCoordinationState, writeSignedLease } from './swarm-coordinator-fs.mjs'
6
8
  import { findTask, locksConflict, normalizeLockRequest, normalizeTaskCard, requireString,
7
- scanTaskConflicts, validateInputShape } from './swarm-coordinator-model.mjs'
9
+ scanTaskConflicts, validateInputShape, decisionTargetsTask } from './swarm-coordinator-model.mjs'
8
10
  import { addMessage, eventRecord, routeEvent, createDecision, applyCycles, applyTimeouts,
9
11
  assertTaskRunnable, pendingDecision, hasActiveWait, dependencyWait, publishEvent, taskStatus,
10
12
  resolveHuman, cancelWait, waitForEvent } from './swarm-coordinator-waits.mjs'
11
13
 
12
14
  const LOCAL_SCHEMA = 'swarm.coordinator-local/1.0'
13
15
  const OPERATIONS = Object.freeze([
16
+ ...Object.keys(TASK_ROUTING_SCHEMAS),
14
17
  'capabilities', 'register-task', 'conflict-scan', 'lock-acquire', 'lock-renew',
15
18
  'lock-release', 'lock-queue-status', 'baseline-handshake', 'dependency-wait', 'event-publish',
16
19
  'task-status', 'tick', 'resolve-human', 'wait-for-event', 'wait-cancel', 'status',
@@ -46,6 +49,7 @@ const WAIT_SCHEMA = objectSchema(
46
49
  refetchPaths: stringArray },
47
50
  )
48
51
  const OPERATION_SCHEMAS = Object.freeze({
52
+ ...TASK_ROUTING_SCHEMAS,
49
53
  capabilities: objectSchema([], {}),
50
54
  'register-task': TASK_CARD_SCHEMA,
51
55
  'conflict-scan': objectSchema(['taskId'], { taskId: string }),
@@ -82,7 +86,7 @@ function validateReplacement(state, card) {
82
86
  if (card.supersedesTaskId === null) return
83
87
  const previous = state.tasks.find((task) => task.taskId === card.supersedesTaskId)
84
88
  const pending = previous && state.decisions.some((decision) => decision.status === 'pending'
85
- && decision.agents.includes(previous.agentId))
89
+ && decisionTargetsTask(decision, previous))
86
90
  const covered = previous && previous.taskScope.every((path) => card.taskScope.some((scope) => (
87
91
  path === scope || path.startsWith(scope + '/')
88
92
  )))
@@ -123,7 +127,7 @@ async function conflictScan(repositoryRoot, input) {
123
127
  other.status = 'waiting'
124
128
  return createDecision(state, 'requirement-conflict', [task.agentId, other.agentId], [],
125
129
  `任务 ${task.taskId} 与 ${conflict.taskId} 的需求声明冲突,请选择先恢复的智能体。`,
126
- '矛盾需求同时执行会产生不可预测的覆盖,必须由真人裁决。', new Date().toISOString())
130
+ '矛盾需求同时执行会产生不可预测的覆盖,必须由真人裁决。', new Date().toISOString(), [task.taskId, other.taskId])
127
131
  })
128
132
  return { state, output: { schemaVersion: LOCAL_SCHEMA, taskId, conflicts, messages,
129
133
  decisions, zeroConflict: conflicts.length === 0 }, audit: [{ event: 'conflict-scan', taskId, conflictCount: conflicts.length }] }
@@ -213,7 +217,7 @@ function rejectQueuedLock(state, queued, reason, now) {
213
217
  const result = createDecision(state, 'lock-queue-timeout', [queued.request.agentId], [],
214
218
  '锁队列 ' + queued.queueId + ' 已于 ' + queued.deadlineAt + ' 超时;资源 '
215
219
  + queued.request.resource + ',路径 ' + queued.request.paths.join(', ') + '。请核查占锁方后决定恢复或终止任务。',
216
- '原队列不会再次授锁。恢复后必须提交带新明确等待上限的申请,不能把超时当作已取得锁。', now)
220
+ '原队列不会再次授锁。恢复后必须提交带新明确等待上限的申请,不能把超时当作已取得锁。', now, [queued.request.taskId])
217
221
  queued.decisionId = result.decision.decisionId
218
222
  queued.confirmProtocolRequest = result.confirmProtocolRequest
219
223
  return result
@@ -405,6 +409,7 @@ async function coordinatorStatus(repositoryRoot) {
405
409
  }
406
410
 
407
411
  const HANDLERS = Object.freeze({
412
+ ...TASK_ROUTING_HANDLERS,
408
413
  'register-task': registerTask,
409
414
  'conflict-scan': conflictScan,
410
415
  'lock-acquire': acquireLock,
@@ -444,3 +449,5 @@ export {
444
449
  OPERATIONS,
445
450
  executeCoordinatorOperation,
446
451
  }
452
+
453
+ export { decisionTargetsTask, decisionResumesTask } from './swarm-coordinator-model.mjs'