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.
- package/README.md +14 -0
- package/broker-account-storage.mjs +135 -0
- package/broker-credentials.mjs +125 -0
- package/broker.mjs +13 -93
- package/installer-storage.mjs +205 -0
- package/installer.mjs +48 -63
- package/official-skill-update.mjs +151 -0
- package/package.json +10 -1
- package/skill/SKILL.md +72 -134
- package/skill/references/ops-heartbeat.md +3 -3
- package/skill/references/org-chart.md +5 -26
- package/skill/references/task-lifecycle.md +10 -2
- package/skill/references/traffic-light.md +3 -3
- package/skill/skill.json +1 -1
- package/swarm-coordinator-model.mjs +1 -0
- package/swarm-runtime.mjs +45 -111
- package/swarm-task-contract.mjs +8 -0
- package/swarm-task-handoff.mjs +19 -3
- package/swarm-task-policy.mjs +142 -0
- package/swarm-task-routing.mjs +3 -0
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
# Swarm 组织架构
|
|
2
|
-
## 协议与层级
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
默认主代理执行。org-chart没有任务事实时返回0个worker,nextStep.operation为null,由主代理继续工作。
|
|
5
4
|
|
|
6
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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`
|
|
19
|
+
任务的 `report.evidence` 必须非空且exitCode为0;accept还要求task.validationContext.validationRunId等于taskId,并复用Validator验证subject、artifactSha256、receipt签名、结果与有效期。缺少可信公钥或任一不匹配均阻断。
|
|
20
20
|
|
|
21
21
|
## 灯色规则
|
|
22
22
|
|
|
23
23
|
| 任务状态 | 条件 | 灯色 |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| `reported` | 有 passing TestEvidence |
|
|
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
|
-
|
|
36
|
+
Swarm与Validator共享签名验证实现;单独填写runner字段不能通过。validationContext必须由宿主预先锁定,纯函数传入的角色和任务清单仍需宿主认证,不得宣称远端纯函数拥有持久任务身份控制。
|
|
37
37
|
|
|
38
38
|
## 实现依据
|
|
39
39
|
|
package/skill/skill.json
CHANGED
|
@@ -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
|
-
|
|
1
|
+
import { text, isObject, validId, finding, ORG_PERMISSIONS, TEST_EVIDENCE_SCHEMA, SHA256_PATTERN } from "./swarm-task-contract.mjs";
|
|
2
|
+
import { dispatchEligibility, buildTasks, dispatchTask, claimTask, reportTask, acceptTask, taskTrafficLight, validateTestEvidence, normalizeTestEvidence, hasPassingTestEvidence, hasTrustedTestEvidence, reclaimTask } from "./swarm-task-policy.mjs";
|
|
3
|
+
// Reviewed modules are compiled into one deployment artifact by build-swarm-runtime.mjs.
|
|
2
4
|
const REQUEST_SCHEMA = "swarm.skill.request/1.0";
|
|
3
5
|
const ALLOWED_EXTERNAL_ENDPOINTS = { blueprint: "https://cli.tax/wvz6zmRWmX" };
|
|
4
6
|
const RESPONSE_SCHEMA = "swarm.skill.response/1.0"; const ERROR_SCHEMA = "swarm.skill.error/1.0";
|
|
5
|
-
const ORG_SCHEMA = "swarm.org-chart/1.0"; const TASK_SCHEMA = "swarm.tasks/1.0";
|
|
6
|
-
const COMPILER_NAME = "swarm"; const COMPILER_VERSION = "v7.0.
|
|
7
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
7
|
+
const ORG_SCHEMA = "swarm.org-chart/1.0"; const TASK_SCHEMA = "swarm.tasks/1.0";
|
|
8
|
+
const COMPILER_NAME = "swarm"; const COMPILER_VERSION = "v7.0.39";
|
|
8
9
|
const PURE_OPERATIONS = new Set([
|
|
9
10
|
"capabilities", "help", "intake", "org-chart", "blueprint-bridge", "dispatch", "claim",
|
|
10
11
|
"report", "accept", "swarm-status", "traffic-light", "security-check", "validate-json", "heartbeat", "reclaim",
|
|
11
12
|
]);
|
|
12
|
-
function text(value) { return String(value ?? ""); }
|
|
13
13
|
function okResponse(requestId, payload) { return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "succeeded", ...payload }; }
|
|
14
14
|
function blockedResponse(requestId, request, findings) {
|
|
15
15
|
return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "blocked", brainMode: null,
|
|
16
16
|
requestedBrainMode: request?.requestedBrainMode ?? "ide", brainUsed: false, revision: null,
|
|
17
17
|
validation: { valid: false, guarantee: "blocked", findings } };
|
|
18
18
|
}
|
|
19
|
-
function finding(severity, ruleId, entityRef, message, evidence = {}) { return { severity, ruleId, entityRef, message, evidence }; }
|
|
20
|
-
function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
21
|
-
function validId(value) { return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(value); }
|
|
22
19
|
const ORG_LAYERS = ["board", "management", "execution"];
|
|
23
20
|
const ORG_ROLES = ["board", "dispatcher", "ops", "security-guard", "coordinator", "worker"];
|
|
24
21
|
const ORG_FIXED_ROLES = new Set(["board", "dispatcher", "ops", "security-guard", "coordinator"]);
|
|
25
|
-
const ORG_PERMISSIONS = { board: ["dispatch", "accept", "reject", "stop", "reclaim", "replace"], dispatcher: ["dispatch", "reassign", "prioritize"], ops: ["heartbeat", "reclaim", "replace"],
|
|
26
|
-
"security-guard": ["block", "alert", "quarantine"], coordinator: ["conflict-scan", "lock", "queue", "baseline-handshake", "dependency-wait", "wake", "need-human"], worker: ["claim", "report", "request-help"], };
|
|
27
22
|
function analyzeTaskGraph(tasks) {
|
|
28
23
|
const findings = [];
|
|
29
24
|
const ids = new Set();
|
|
@@ -53,14 +48,27 @@ function analyzeTaskGraph(tasks) {
|
|
|
53
48
|
return { findings: [], recommendedWorkerCount: Math.min(50, Math.max(...levels)) };
|
|
54
49
|
}
|
|
55
50
|
function buildOrgChart(input = {}) {
|
|
56
|
-
const workerCount = Math.min(50, Math.max(1, Math.floor(Number(input.workerCount) || 4)));
|
|
57
51
|
const projectName = text(input.projectName || "swarm-run");
|
|
58
|
-
let recommendedWorkerCount, recommendationFindings = [];
|
|
52
|
+
let recommendedWorkerCount = 0, recommendationFindings = [];
|
|
53
|
+
const delegationDecisions = [];
|
|
59
54
|
if (Array.isArray(input.tasks) && input.tasks.length > 0) {
|
|
60
55
|
const analysis = analyzeTaskGraph(input.tasks);
|
|
61
|
-
recommendedWorkerCount = analysis.recommendedWorkerCount ?? undefined;
|
|
62
56
|
recommendationFindings = analysis.findings;
|
|
57
|
+
if (!recommendationFindings.length) {
|
|
58
|
+
const eligible = [];
|
|
59
|
+
for (const task of input.tasks) {
|
|
60
|
+
const reason = dispatchEligibility(eligible, task, task.facts);
|
|
61
|
+
delegationDecisions.push({ taskId: task.taskId, delegate: reason === null, reason });
|
|
62
|
+
if (reason === null) eligible.push({ ...task, status: "assigned", dispatch: task.facts });
|
|
63
|
+
}
|
|
64
|
+
recommendedWorkerCount = Math.min(analysis.recommendedWorkerCount, eligible.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (input.workerCount !== undefined && (!Number.isSafeInteger(input.workerCount) || input.workerCount < 0 || input.workerCount > 50)) {
|
|
68
|
+
recommendationFindings.push(finding("P0", "WORKER_COUNT_INVALID", "input.workerCount", "workerCount must be an integer from 0 to 50"));
|
|
63
69
|
}
|
|
70
|
+
const workerCount = recommendationFindings.length ? 0
|
|
71
|
+
: input.workerCount === undefined ? recommendedWorkerCount : Math.min(input.workerCount, recommendedWorkerCount);
|
|
64
72
|
const org = { schemaVersion: ORG_SCHEMA, projectName, layers: { board: [{ agentId: "board", role: "board", title: "决策层·老板/主智能体" }],
|
|
65
73
|
management: [
|
|
66
74
|
{ agentId: "dispatcher", role: "dispatcher", title: "管理层·调度智能体", fixed: true },
|
|
@@ -68,7 +76,8 @@ function buildOrgChart(input = {}) {
|
|
|
68
76
|
{ agentId: "coordinator", role: "coordinator", title: "管理层·自动协调智能体", fixed: true }, ],
|
|
69
77
|
execution: Array.from({ length: workerCount }, (_, i) => ({ agentId: `worker-${String(i + 1).padStart(3, "0")}`,
|
|
70
78
|
role: "worker", title: `执行层·子智能体 ${i + 1}`, fixed: false })),
|
|
71
|
-
}, permissions: ORG_PERMISSIONS };
|
|
79
|
+
}, permissions: ORG_PERMISSIONS, delegationDecisions, executionMode: workerCount === 0 ? "single-agent" : "delegated" };
|
|
80
|
+
for (const role of org.layers.management) role.hostAgentId = "board";
|
|
72
81
|
if (recommendedWorkerCount !== undefined) org.recommendedWorkerCount = recommendedWorkerCount;
|
|
73
82
|
if (recommendationFindings.length) org.recommendationFindings = recommendationFindings;
|
|
74
83
|
return org;
|
|
@@ -113,92 +122,11 @@ function validateProjectJson(project) {
|
|
|
113
122
|
else findings.push(...analyzeTaskGraph(project.tasks).findings);
|
|
114
123
|
return findings;
|
|
115
124
|
}
|
|
116
|
-
function buildTasks(project, org) {
|
|
117
|
-
return (project.tasks ?? []).map((task, index) => ({
|
|
118
|
-
taskId: validId(task.taskId) ? task.taskId : `task-${String(index + 1).padStart(4, "0")}`, title: text(task.title || task.name || `任务 ${index + 1}`), owner: null, status: "backlog",
|
|
119
|
-
priority: text(task.priority || "normal"), dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn : [], assignedBy: null, claimedAt: null, reportedAt: null, report: null, progressPercent: 0,
|
|
120
|
-
progressNote: "", inheritedFrom: null }));
|
|
121
|
-
}
|
|
122
|
-
function dispatchTask(tasks, taskId, workerId, actorRole) {
|
|
123
|
-
if (!ORG_PERMISSIONS[actorRole]?.includes("dispatch")) return { ok: false, error: `role ${actorRole} cannot dispatch` };
|
|
124
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
125
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
126
|
-
if (task.status !== "backlog") return { ok: false, error: `task ${taskId} is ${task.status}, not backlog` };
|
|
127
|
-
for (const dependencyId of Array.isArray(task.dependsOn) ? task.dependsOn : []) {
|
|
128
|
-
const dependency = tasks.find((candidate) => candidate.taskId === dependencyId);
|
|
129
|
-
if (!dependency) return { ok: false, error: `dependency ${dependencyId} not found` };
|
|
130
|
-
if (dependency.status !== "accepted") return { ok: false, error: `dependency ${dependencyId} is ${dependency.status}, not accepted` };
|
|
131
|
-
}
|
|
132
|
-
task.status = "assigned"; task.owner = workerId; task.assignedBy = actorRole;
|
|
133
|
-
return { ok: true, task };
|
|
134
|
-
}
|
|
135
|
-
function claimTask(tasks, taskId, workerId) {
|
|
136
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
137
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
138
|
-
if (task.status !== "assigned") return { ok: false, error: `task ${taskId} is ${task.status}, not assigned` };
|
|
139
|
-
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} claimed by another worker` };
|
|
140
|
-
task.status = "claimed"; task.owner = workerId; task.claimedAt = new Date().toISOString();
|
|
141
|
-
return { ok: true, task };
|
|
142
|
-
}
|
|
143
|
-
function validateTestEvidence(value, entityRef) {
|
|
144
|
-
const findings = [];
|
|
145
|
-
if (!isObject(value)) return [finding("P0", "TEST_EVIDENCE_OBJECT", entityRef, "TestEvidence must be an object")];
|
|
146
|
-
if (value.schemaVersion !== TEST_EVIDENCE_SCHEMA) findings.push(finding("P0", "TEST_EVIDENCE_SCHEMA", `${entityRef}.schemaVersion`, `Expected ${TEST_EVIDENCE_SCHEMA}`));
|
|
147
|
-
if (!text(value.evidenceId).trim()) findings.push(finding("P0", "TEST_EVIDENCE_ID", `${entityRef}.evidenceId`, "evidenceId is required"));
|
|
148
|
-
if (!["test", "build", "lint", "security", "benchmark"].includes(value.kind)) findings.push(finding("P0", "TEST_EVIDENCE_KIND", `${entityRef}.kind`, "kind is unsupported"));
|
|
149
|
-
if (!["local", "trusted-runner"].includes(value.runner)) findings.push(finding("P0", "TEST_EVIDENCE_RUNNER", `${entityRef}.runner`, "runner must be local or trusted-runner"));
|
|
150
|
-
if (!text(value.command).trim()) findings.push(finding("P0", "TEST_EVIDENCE_COMMAND", `${entityRef}.command`, "command is required"));
|
|
151
|
-
if (!Number.isInteger(value.exitCode)) findings.push(finding("P0", "TEST_EVIDENCE_EXIT", `${entityRef}.exitCode`, "exitCode must be an integer"));
|
|
152
|
-
if (typeof value.durationMs !== "number" || !Number.isFinite(value.durationMs) || value.durationMs < 0) findings.push(finding("P0", "TEST_EVIDENCE_DURATION", `${entityRef}.durationMs`, "durationMs must be a finite number >= 0"));
|
|
153
|
-
if (!text(value.summary).trim()) findings.push(finding("P0", "TEST_EVIDENCE_SUMMARY", `${entityRef}.summary`, "summary is required"));
|
|
154
|
-
if (value.artifactSha256 !== undefined && !SHA256_PATTERN.test(value.artifactSha256)) findings.push(finding("P0", "TEST_EVIDENCE_ARTIFACT", `${entityRef}.artifactSha256`, "artifactSha256 must be a lowercase SHA-256 digest"));
|
|
155
|
-
return findings;
|
|
156
|
-
}
|
|
157
|
-
function normalizeTestEvidence(values) {
|
|
158
|
-
if (!Array.isArray(values)) return { evidence: [], findings: [finding("P0", "TEST_EVIDENCE_ARRAY", "report.evidence", "evidence must be an array")] };
|
|
159
|
-
const findings = values.flatMap((value, index) => validateTestEvidence(value, `report.evidence[${index}]`));
|
|
160
|
-
return findings.length ? { evidence: [], findings } : { evidence: values.map((value) => ({ ...value })), findings: [] };
|
|
161
|
-
}
|
|
162
|
-
function hasPassingTestEvidence(task) {
|
|
163
|
-
const evidence = task?.report?.evidence;
|
|
164
|
-
return Array.isArray(evidence) && evidence.length > 0
|
|
165
|
-
&& evidence.every((value, index) => validateTestEvidence(value, `task.report.evidence[${index}]`).length === 0 && value.exitCode === 0);
|
|
166
|
-
}
|
|
167
|
-
function reportTask(tasks, taskId, workerId, report) {
|
|
168
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
169
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
170
|
-
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} not owned by ${workerId}` };
|
|
171
|
-
if (!["claimed", "running"].includes(task.status)) return { ok: false, error: `task ${taskId} is ${task.status}, cannot report` };
|
|
172
|
-
if (!isObject(report)) return { ok: false, error: "report must be an object" };
|
|
173
|
-
if (!text(report.output).trim() && report.evidence === undefined) return { ok: false, error: "report must have output or evidence" };
|
|
174
|
-
const normalizedReport = { output: text(report.output) };
|
|
175
|
-
if (report.evidence !== undefined) {
|
|
176
|
-
const normalized = normalizeTestEvidence(report.evidence);
|
|
177
|
-
if (normalized.findings.length) return { ok: false, error: "report evidence is invalid", findings: normalized.findings };
|
|
178
|
-
normalizedReport.evidence = normalized.evidence;
|
|
179
|
-
}
|
|
180
|
-
task.status = "reported"; task.report = normalizedReport; task.reportedAt = new Date().toISOString();
|
|
181
|
-
return { ok: true, task, hasEvidence: hasPassingTestEvidence(task) };
|
|
182
|
-
}
|
|
183
|
-
function acceptTask(tasks, taskId, accept, actorRole) {
|
|
184
|
-
if (actorRole !== "board") return { ok: false, error: `role ${actorRole} cannot accept` };
|
|
185
|
-
const task = tasks.find((t) => t.taskId === taskId);
|
|
186
|
-
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
187
|
-
if (task.status !== "reported") return { ok: false, error: `task ${taskId} is ${task.status}, not reported` };
|
|
188
|
-
if (accept && !hasPassingTestEvidence(task)) return { ok: false, error: `task ${taskId} has no passing TestEvidence` };
|
|
189
|
-
task.status = accept ? "accepted" : "failed";
|
|
190
|
-
return { ok: true, task };
|
|
191
|
-
}
|
|
192
|
-
function taskTrafficLight(task) {
|
|
193
|
-
if (task.status === "accepted") return hasPassingTestEvidence(task) ? "green" : "red";
|
|
194
|
-
if (task.status === "reported") return hasPassingTestEvidence(task) ? "green" : "yellow";
|
|
195
|
-
if (task.status === "failed" || task.status === "blocked" || task.status === "cancelled") return "red";
|
|
196
|
-
return "yellow";
|
|
197
|
-
}
|
|
198
125
|
const HEARTBEAT_MISS_LIMIT = 3;
|
|
199
126
|
function buildAgents(org, nowIso = null) {
|
|
200
127
|
const now = nowIso || new Date().toISOString(), agents = [];
|
|
201
128
|
for (const layer of ORG_LAYERS) for (const member of org.layers?.[layer] ?? []) {
|
|
129
|
+
if (member.hostAgentId === "board") continue;
|
|
202
130
|
agents.push({ agentId: member.agentId, role: member.role, title: member.title,
|
|
203
131
|
fixed: Boolean(member.fixed), status: "green", lastHeartbeatAt: now,
|
|
204
132
|
heartbeatMisses: 0, currentTaskId: null, progressPercent: 0, progressNote: "" });
|
|
@@ -221,6 +149,7 @@ function scanHeartbeats(agents, nowIso = null) {
|
|
|
221
149
|
const misses = Math.floor((now - new Date(agent.lastHeartbeatAt).getTime()) / 30000);
|
|
222
150
|
agent.heartbeatMisses = Math.max(agent.heartbeatMisses, Math.min(misses, 99));
|
|
223
151
|
if (agent.heartbeatMisses >= HEARTBEAT_MISS_LIMIT && agent.status !== "dead") { agent.status = "dead"; dead.push(agent.agentId); }
|
|
152
|
+
else if (agent.status === "dead") continue;
|
|
224
153
|
else if (agent.heartbeatMisses >= 1) agent.status = "yellow";
|
|
225
154
|
else agent.status = "green";
|
|
226
155
|
}
|
|
@@ -229,16 +158,14 @@ function scanHeartbeats(agents, nowIso = null) {
|
|
|
229
158
|
function reclaimTasks(tasks, workerId) {
|
|
230
159
|
const reclaimed = [];
|
|
231
160
|
for (const task of tasks) {
|
|
232
|
-
if (task.owner === workerId &&
|
|
233
|
-
task.status = "backlog"; task.owner = null; task.inheritedFrom = workerId; reclaimed.push(task.taskId);
|
|
234
|
-
}
|
|
161
|
+
if (task.owner === workerId && reclaimTask(task, workerId)) reclaimed.push(task.taskId);
|
|
235
162
|
}
|
|
236
163
|
return reclaimed;
|
|
237
164
|
}
|
|
238
165
|
function replaceWorker(org, agents, tasks, deadWorkerId, newWorkerId = null) {
|
|
239
166
|
const dead = agents.find((a) => a.agentId === deadWorkerId);
|
|
240
|
-
if (!dead) return { ok: false, error: `dead worker ${deadWorkerId} not found` };
|
|
241
|
-
const replacement = newWorkerId ? agents.find((a) => a.agentId === newWorkerId && a.role === "worker")
|
|
167
|
+
if (!dead || dead.role !== "worker" || dead.status !== "dead") return { ok: false, error: `dead worker ${deadWorkerId} not found` };
|
|
168
|
+
const replacement = newWorkerId ? agents.find((a) => a.agentId === newWorkerId && a.role === "worker" && a.status === "green" && a.agentId !== deadWorkerId)
|
|
242
169
|
: agents.find((a) => a.role === "worker" && a.status === "green" && a.agentId !== deadWorkerId);
|
|
243
170
|
if (!replacement) return { ok: false, error: "no healthy replacement worker available" };
|
|
244
171
|
const inheritedTasks = reclaimTasks(tasks, deadWorkerId);
|
|
@@ -251,7 +178,7 @@ function replaceWorker(org, agents, tasks, deadWorkerId, newWorkerId = null) {
|
|
|
251
178
|
return { ok: true, replacement: replacement.agentId, inheritedTasks };
|
|
252
179
|
}
|
|
253
180
|
const INTAKE_QUESTIONS = [ { id: "goal", prompt: "What must the swarm accomplish? List the parallel/ordered work items or point to the project JSON.", required: true, example: "12 个模块迁移:A1..A12,依赖 A1→A2→A3,其余并行" },
|
|
254
|
-
{ id: "workerCount", prompt: "
|
|
181
|
+
{ id: "workerCount", prompt: "What is the maximum justified worker count? Omit for task-based sizing; simple work stays with the main agent.", required: false, example: "6" },
|
|
255
182
|
{ id: "orgTier", prompt: "Any org-chart constraints? (default: board → dispatcher/ops/security-guard → workers)", required: false, example: "默认三层即可" },
|
|
256
183
|
{ id: "securityPolicy", prompt: "Security policy: strict (block injections) or observe (alert only)?", required: false, example: "strict" },
|
|
257
184
|
{ id: "blueprintEnabled", prompt: "Use Blueprint to plan tasks before dispatch? (yes: tasks are planned by the Blueprint skill for traceable acceptance; no: direct dispatch)", required: false, example: "no" }, ];
|
|
@@ -264,13 +191,18 @@ const nullableStringSchema = { type: ["string", "null"] };
|
|
|
264
191
|
const evidenceSchema = objectSchema({ schemaVersion: { const: TEST_EVIDENCE_SCHEMA }, evidenceId: stringSchema({ minLength: 1 }),
|
|
265
192
|
kind: { enum: ["test", "build", "lint", "security", "benchmark"] }, runner: { enum: ["local", "trusted-runner"] }, command: stringSchema({ minLength: 1 }), exitCode: { type: "integer" }, durationMs: { type: "number", minimum: 0 },
|
|
266
193
|
summary: stringSchema({ minLength: 1 }), artifactSha256: stringSchema({ pattern: SHA256_PATTERN.source }), }, ["schemaVersion", "evidenceId", "kind", "runner", "command", "exitCode", "durationMs", "summary"], { additionalProperties: true });
|
|
194
|
+
const delegationSchema = objectSchema({ businessNeed: stringSchema({ minLength: 1 }), deliverable: stringSchema({ minLength: 1 }),
|
|
195
|
+
acceptanceCriteria: stringSchema({ minLength: 1 }), mainAgentWork: stringSchema({ minLength: 1 }), independent: { type: "boolean" },
|
|
196
|
+
substantial: { type: "boolean" }, estimatedSavedMinutes: { type: "number", minimum: 0 }, coordinationMinutes: { type: "number", minimum: 0 },
|
|
197
|
+
}, ["businessNeed", "deliverable", "acceptanceCriteria", "mainAgentWork", "independent", "substantial", "estimatedSavedMinutes", "coordinationMinutes"]);
|
|
198
|
+
const dispatchSchema = objectSchema({ delegation: delegationSchema, estimatedChangedLines: { type: "integer", minimum: 0 }, fileCount: { type: "integer", minimum: 1 }, crossModule: { type: "boolean" }, parallelSafe: { type: "boolean" }, ownerId: stringSchema({ minLength: 1 }), targetPaths: arraySchema(stringSchema({ minLength: 1 }), { minItems: 1 }) }, ["estimatedChangedLines", "fileCount", "crossModule", "parallelSafe", "ownerId", "targetPaths", "delegation"]);
|
|
267
199
|
const reportSchema = objectSchema({ output: stringSchema(), evidence: arraySchema(evidenceSchema) }, [], {
|
|
268
200
|
anyOf: [{ properties: { output: stringSchema({ minLength: 1 }) }, required: ["output"] }, { required: ["evidence"] }],
|
|
269
201
|
});
|
|
270
202
|
const taskSchema = objectSchema({ taskId: stringSchema({ minLength: 1 }), title: stringSchema(), owner: nullableStringSchema,
|
|
271
203
|
status: { enum: [...TASK_STATUSES] }, priority: stringSchema(), dependsOn: arraySchema(stringSchema()), assignedBy: nullableStringSchema, claimedAt: nullableStringSchema, reportedAt: nullableStringSchema,
|
|
272
204
|
report: { anyOf: [{ type: "null" }, reportSchema] }, progressPercent: { type: "number" }, progressNote: stringSchema(), inheritedFrom: nullableStringSchema, trafficLight: { enum: ["green", "yellow", "red"] }, }, ["taskId", "title", "status", "dependsOn"], { additionalProperties: true });
|
|
273
|
-
const projectTaskSchema = objectSchema({ taskId: stringSchema({ minLength: 1 }), title: stringSchema({ minLength: 1 }),
|
|
205
|
+
const projectTaskSchema = objectSchema({ facts: dispatchSchema, taskId: stringSchema({ minLength: 1 }), title: stringSchema({ minLength: 1 }),
|
|
274
206
|
name: stringSchema(), priority: stringSchema(), dependsOn: arraySchema(stringSchema()) }, ["taskId", "title"], { additionalProperties: true });
|
|
275
207
|
const agentSchema = objectSchema({ agentId: stringSchema({ minLength: 1 }), role: { enum: ORG_ROLES }, title: stringSchema(), fixed: { type: "boolean" }, status: { enum: ["green", "yellow", "red", "dead"] }, lastHeartbeatAt: stringSchema({ format: "date-time" }),
|
|
276
208
|
heartbeatMisses: { type: "integer", minimum: 0 }, currentTaskId: nullableStringSchema, progressPercent: { type: "number" }, progressNote: stringSchema() }, ["agentId", "role", "status"], { additionalProperties: true });
|
|
@@ -287,9 +219,9 @@ const OPERATION_SCHEMAS = Object.freeze({
|
|
|
287
219
|
capabilities: operationSchema({}, [], { capabilities: anyObjectSchema, operationSchemas: anyObjectSchema, skill: anyObjectSchema, nextStep: nextSchema }, ["capabilities", "operationSchemas", "skill", "nextStep"]),
|
|
288
220
|
help: operationSchema({}, [], { help: anyObjectSchema, nextStep: nextSchema }, ["help", "nextStep"]),
|
|
289
221
|
intake: operationSchema({}, [], { questions: arraySchema(anyObjectSchema), nextStep: nextSchema }, ["questions", "nextStep"]),
|
|
290
|
-
"org-chart": operationSchema({ workerCount: { type: "
|
|
222
|
+
"org-chart": operationSchema({ workerCount: { type: "integer", minimum: 0, maximum: 50 }, projectName: stringSchema(), blueprintEnabled: { type: ["boolean", "string"] }, tasks: arraySchema(projectTaskSchema) }, [], { org: anyObjectSchema, blueprintEnabled: { type: "boolean" }, fixedAgents: arraySchema(stringSchema()), workerCount: { type: "integer" }, nextStep: nextSchema }, ["org", "blueprintEnabled", "fixedAgents", "workerCount", "nextStep"]),
|
|
291
223
|
"blueprint-bridge": operationSchema({ projectName: stringSchema({ minLength: 1 }), tasks: arraySchema(projectTaskSchema, { minItems: 1 }) }, ["projectName", "tasks"], { planningStatus: { const: "planned" }, blueprintEnabled: { const: true }, blueprintEndpoint: stringSchema({ format: "uri" }), blueprintRequest: anyObjectSchema, projectFormat: { const: "swarm.project/1.0" }, nextStep: nextSchema }, ["planningStatus", "blueprintEnabled", "blueprintEndpoint", "blueprintRequest", "projectFormat", "nextStep"]),
|
|
292
|
-
dispatch: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), actorRole: { enum: ["dispatcher", "board"] } }, ["tasks", "taskId", "workerId", "actorRole"], { ...taskStateOutput, nextStep: nextSchema }, ["task", "tasks", "stateNote", "nextStep"]),
|
|
224
|
+
dispatch: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), actorRole: { enum: ["dispatcher", "board"] }, facts: dispatchSchema }, ["tasks", "taskId", "workerId", "actorRole", "facts"], { ...taskStateOutput, nextStep: nextSchema }, ["task", "tasks", "stateNote", "nextStep"]),
|
|
293
225
|
claim: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }) }, ["tasks", "taskId", "workerId"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
294
226
|
report: operationSchema({ ...tasksInput, workerId: stringSchema({ minLength: 1 }), report: reportSchema }, ["tasks", "taskId", "workerId", "report"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, evidenceRequired: { type: "boolean" }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
295
227
|
accept: operationSchema({ ...tasksInput, actorRole: { const: "board" }, accept: { type: "boolean" } }, ["tasks", "taskId", "actorRole", "accept"], { ...taskStateOutput, trafficLight: { enum: ["green", "yellow", "red"] }, nextStep: nextSchema }, ["task", "tasks", "trafficLight", "stateNote", "nextStep"]),
|
|
@@ -395,7 +327,7 @@ function runMeta(operation, requestId) {
|
|
|
395
327
|
if (operation === "capabilities") {
|
|
396
328
|
return okResponse(requestId, {
|
|
397
329
|
capabilities: { pure: true, stateless: true, networkRequired: false, filesystemRequired: false,
|
|
398
|
-
operations: [...PURE_OPERATIONS], orgSchema: ORG_SCHEMA, taskSchema: TASK_SCHEMA, testEvidenceSchema: TEST_EVIDENCE_SCHEMA, fixedAgents: ["board"
|
|
330
|
+
operations: [...PURE_OPERATIONS], orgSchema: ORG_SCHEMA, taskSchema: TASK_SCHEMA, testEvidenceSchema: TEST_EVIDENCE_SCHEMA, fixedAgents: ["board"],
|
|
399
331
|
trafficLights: ["green", "yellow", "red"], stateHolder: "caller", coordinator: { command: "cli-swarm local", capabilitiesOperation: "capabilities", stateBoundary: ".coord", messageTypes: ["range-declare", "conflict-alert", "lock-granted", "lock-denied", "baseline-handshake", "need-human", "dependency-wait"] },
|
|
400
332
|
workerRecommendation: "maximum acyclic dependency level width, capped at 50" }, operationSchemas: OPERATION_SCHEMAS, skill: { name: COMPILER_NAME, version: COMPILER_VERSION },
|
|
401
333
|
nextStep: { operation: "intake", instruction: "Ask the intake questions, then build the org-chart and dispatch tasks." } });
|
|
@@ -409,8 +341,10 @@ function runPlanning(operation, requestId, input, request) {
|
|
|
409
341
|
const findings = validateOrgChart(org);
|
|
410
342
|
if (findings.length) return blockedResponse(requestId, request, findings);
|
|
411
343
|
const blueprintEnabled = input.blueprintEnabled === true || text(input.blueprintEnabled).toLowerCase() === "yes";
|
|
412
|
-
return okResponse(requestId, { org, blueprintEnabled, fixedAgents: ["board"
|
|
413
|
-
nextStep:
|
|
344
|
+
return okResponse(requestId, { org, blueprintEnabled, fixedAgents: ["board"], workerCount: org.layers.execution.length,
|
|
345
|
+
nextStep: org.layers.execution.length === 0
|
|
346
|
+
? { operation: null, instruction: "Continue with the main agent. No justified independent work requires subagents; logical management roles do not create agents." }
|
|
347
|
+
: blueprintEnabled
|
|
414
348
|
? { operation: "blueprint-bridge", instruction: "Blueprint is enabled: call blueprint-bridge to plan the project JSON into a traceable blueprint, then dispatch its tasks to the swarm." }
|
|
415
349
|
: { operation: "dispatch", instruction: "Feed the project JSON; dispatch backlog tasks to workers by dependency order." } });
|
|
416
350
|
}
|
|
@@ -423,7 +357,7 @@ function runPlanning(operation, requestId, input, request) {
|
|
|
423
357
|
function runTaskMutation(operation, requestId, input, request) {
|
|
424
358
|
const tasks = Array.isArray(input.tasks) ? input.tasks : [], taskId = text(input.taskId);
|
|
425
359
|
if (operation === "dispatch") {
|
|
426
|
-
const result = dispatchTask(tasks, taskId, text(input.workerId), text(input.actorRole
|
|
360
|
+
const result = dispatchTask(tasks, taskId, text(input.workerId), text(input.actorRole), input.facts);
|
|
427
361
|
if (!result.ok) return blockedResponse(requestId, request, [finding("P0", "DISPATCH_FAILED", input.taskId, result.error, { example: { taskId: "task-0001", workerId: "worker-001" } })]);
|
|
428
362
|
return okResponse(requestId, { task: result.task, tasks, stateNote: STATE_NOTE, nextStep: { operation: "claim", instruction: "The worker can now claim the task." } });
|
|
429
363
|
}
|
|
@@ -455,7 +389,7 @@ function runObservation(operation, requestId, input, request) {
|
|
|
455
389
|
}
|
|
456
390
|
if (operation === "traffic-light") return okResponse(requestId, { trafficLight: taskTrafficLight(input.task ?? {}),
|
|
457
391
|
rules: {
|
|
458
|
-
green: "status is
|
|
392
|
+
green: "status is accepted with task-bound signed TestEvidence verified by Validator",
|
|
459
393
|
yellow: "backlog, assigned, claimed, running, or reported without passing TestEvidence",
|
|
460
394
|
red: "status is \"failed\", \"blocked\", or \"cancelled\"" } });
|
|
461
395
|
if (operation === "security-check") {
|
|
@@ -480,7 +414,7 @@ function runObservation(operation, requestId, input, request) {
|
|
|
480
414
|
const task = tasks.find((candidate) => candidate.taskId === taskId);
|
|
481
415
|
if (!task) return blockedResponse(requestId, request, [finding("P0", "RECLAIM_FAILED", taskId, `task ${taskId} not found`, { example: { taskId: "task-0001" } })]);
|
|
482
416
|
if (task.status === "backlog") return blockedResponse(requestId, request, [finding("P1", "RECLAIM_NOOP", taskId, `task ${taskId} is already backlog`, { example: { taskId: "task-0001" } })]);
|
|
483
|
-
task.
|
|
417
|
+
if (!reclaimTask(task, task.owner)) return { ...blockedResponse(requestId, request, [finding("P0", "RECLAIM_REQUIRES_RECONCILIATION", taskId, "Only unstarted assignments may be automatically reclaimed")]), task, tasks, stateNote: STATE_NOTE };
|
|
484
418
|
return okResponse(requestId, { reclaimed: true, taskId, reason, task, tasks, stateNote: STATE_NOTE });
|
|
485
419
|
}
|
|
486
420
|
export async function run(request) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const TEST_EVIDENCE_SCHEMA = 'cli.tax.test-evidence/1.0';
|
|
2
|
+
export const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
3
|
+
export const ORG_PERMISSIONS = { board: ["dispatch", "accept", "reject", "stop", "reclaim", "replace"], dispatcher: ["dispatch", "reassign", "prioritize"], ops: ["heartbeat", "reclaim", "replace"],
|
|
4
|
+
"security-guard": ["block", "alert", "quarantine"], coordinator: ["conflict-scan", "lock", "queue", "baseline-handshake", "dependency-wait", "wake", "need-human"], worker: ["claim", "report", "request-help"], };
|
|
5
|
+
export function text(value) { return String(value ?? ""); }
|
|
6
|
+
export function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
7
|
+
export function validId(value) { return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(value); }
|
|
8
|
+
export function finding(severity, ruleId, entityRef, message, evidence = {}) { return { severity, ruleId, entityRef, message, evidence }; }
|
package/swarm-task-handoff.mjs
CHANGED
|
@@ -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.
|
|
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)
|