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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { evidenceState, readValidationSubject, validatorReceiptSubject } from "cli-validator/runtime";
|
|
2
|
+
import { text, isObject, validId, finding, ORG_PERMISSIONS, TEST_EVIDENCE_SCHEMA, SHA256_PATTERN } from "./swarm-task-contract.mjs";
|
|
3
|
+
|
|
4
|
+
const MIN_DISPATCH_LINES = 200;
|
|
5
|
+
const MIN_DISPATCH_FILES = 3;
|
|
6
|
+
const ACTIVE_TASK_STATES = new Set(["assigned", "claimed", "running"]);
|
|
7
|
+
function dispatchEligibility(tasks, task, facts) {
|
|
8
|
+
if (!isObject(facts) || !Number.isSafeInteger(facts.estimatedChangedLines) || facts.estimatedChangedLines < 0
|
|
9
|
+
|| !Number.isSafeInteger(facts.fileCount) || facts.fileCount < 1 || typeof facts.crossModule !== "boolean"
|
|
10
|
+
|| typeof facts.parallelSafe !== "boolean" || !validId(facts.ownerId)
|
|
11
|
+
|| !Array.isArray(facts.targetPaths) || facts.targetPaths.length !== facts.fileCount
|
|
12
|
+
|| new Set(facts.targetPaths).size !== facts.fileCount
|
|
13
|
+
|| facts.targetPaths.some(path => typeof path !== "string" || !path.trim() || path.startsWith("/")
|
|
14
|
+
|| path.split("/").some(part => !part || part === "." || part === "..") || /[\\*?\[\]{}]/.test(path))) return "dispatch-facts-required";
|
|
15
|
+
const delegation = facts.delegation;
|
|
16
|
+
if (!isObject(delegation) || ["businessNeed", "deliverable", "acceptanceCriteria", "mainAgentWork"]
|
|
17
|
+
.some(key => typeof delegation[key] !== "string" || !delegation[key].trim())
|
|
18
|
+
|| typeof delegation.independent !== "boolean" || typeof delegation.substantial !== "boolean"
|
|
19
|
+
|| !Number.isFinite(delegation.estimatedSavedMinutes) || delegation.estimatedSavedMinutes < 0
|
|
20
|
+
|| !Number.isFinite(delegation.coordinationMinutes) || delegation.coordinationMinutes < 0) return "dispatch-business-case-required";
|
|
21
|
+
if (!delegation.substantial) return "dispatch-simple-work-stays-local";
|
|
22
|
+
if (!delegation.independent) return "dispatch-not-independent";
|
|
23
|
+
if (delegation.estimatedSavedMinutes <= delegation.coordinationMinutes) return "dispatch-overhead-exceeds-benefit";
|
|
24
|
+
if (facts.estimatedChangedLines < MIN_DISPATCH_LINES && facts.fileCount < MIN_DISPATCH_FILES
|
|
25
|
+
&& !facts.crossModule) return "dispatch-under-threshold";
|
|
26
|
+
if (!facts.parallelSafe) return "dispatch-not-parallel-safe";
|
|
27
|
+
if (task.originOwnerId !== undefined && task.originOwnerId !== facts.ownerId) return "dispatch-owner-mismatch";
|
|
28
|
+
const overlaps = (left, right) => left === right || left.startsWith(right + "/") || right.startsWith(left + "/");
|
|
29
|
+
for (const other of tasks.filter(item => item !== task && ACTIVE_TASK_STATES.has(item.status))) {
|
|
30
|
+
if (!Array.isArray(other.dispatch?.targetPaths)) return "dispatch-peer-scope-missing";
|
|
31
|
+
if (facts.targetPaths.some(path => other.dispatch.targetPaths.some(otherPath => overlaps(path, otherPath)))) return "dispatch-write-conflict";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function hasTrustedTestEvidence(task) {
|
|
36
|
+
if (!hasPassingTestEvidence(task) || !isObject(task.validationContext)
|
|
37
|
+
|| task.validationContext.validationRunId !== task.taskId) return false;
|
|
38
|
+
const subject = readValidationSubject(task.validationContext, "task.validationContext");
|
|
39
|
+
if (!subject.value) return false;
|
|
40
|
+
const digest = validatorReceiptSubject(subject.value);
|
|
41
|
+
return task.report.evidence.every(evidence => evidenceState(evidence, subject.value, digest) === "valid");
|
|
42
|
+
}
|
|
43
|
+
function reclaimTask(task, workerId) {
|
|
44
|
+
if (task.status === "assigned") {
|
|
45
|
+
task.status = "backlog"; task.owner = null; task.inheritedFrom = workerId;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (["claimed", "running"].includes(task.status)) {
|
|
49
|
+
task.status = "blocked"; task.blockedReason = "execution-outcome-unknown";
|
|
50
|
+
task.progressNote = "Reconcile the original execution before authorizing another attempt";
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function buildTasks(project, org) {
|
|
55
|
+
return (project.tasks ?? []).map((task, index) => ({
|
|
56
|
+
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",
|
|
57
|
+
priority: text(task.priority || "normal"), dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn : [], assignedBy: null, claimedAt: null, reportedAt: null, report: null, progressPercent: 0,
|
|
58
|
+
progressNote: "", inheritedFrom: null, ...(task.validationContext === undefined ? {} : { validationContext: structuredClone(task.validationContext) }) }));
|
|
59
|
+
}
|
|
60
|
+
function dispatchTask(tasks, taskId, workerId, actorRole, facts) {
|
|
61
|
+
if (!ORG_PERMISSIONS[actorRole]?.includes("dispatch")) return { ok: false, error: `role ${actorRole} cannot dispatch` };
|
|
62
|
+
const task = tasks.find((t) => t.taskId === taskId);
|
|
63
|
+
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
64
|
+
if (task.status !== "backlog") return { ok: false, error: `task ${taskId} is ${task.status}, not backlog` };
|
|
65
|
+
for (const dependencyId of Array.isArray(task.dependsOn) ? task.dependsOn : []) {
|
|
66
|
+
const dependency = tasks.find((candidate) => candidate.taskId === dependencyId);
|
|
67
|
+
if (!dependency) return { ok: false, error: `dependency ${dependencyId} not found` };
|
|
68
|
+
if (dependency.status === "accepted" && !hasTrustedTestEvidence(dependency)) return { ok: false, error: `dependency ${dependencyId} has no verified evidence` };
|
|
69
|
+
if (dependency.status !== "accepted") return { ok: false, error: `dependency ${dependencyId} is ${dependency.status}, not accepted` };
|
|
70
|
+
}
|
|
71
|
+
const error = dispatchEligibility(tasks, task, facts);
|
|
72
|
+
if (error) return { ok: false, error };
|
|
73
|
+
task.dispatch = structuredClone(facts);
|
|
74
|
+
if (task.originOwnerId === undefined) task.originOwnerId = facts.ownerId;
|
|
75
|
+
task.status = "assigned"; task.owner = workerId; task.assignedBy = actorRole;
|
|
76
|
+
return { ok: true, task };
|
|
77
|
+
}
|
|
78
|
+
function claimTask(tasks, taskId, workerId) {
|
|
79
|
+
const task = tasks.find((t) => t.taskId === taskId);
|
|
80
|
+
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
81
|
+
if (task.status !== "assigned") return { ok: false, error: `task ${taskId} is ${task.status}, not assigned` };
|
|
82
|
+
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} claimed by another worker` };
|
|
83
|
+
task.status = "claimed"; task.owner = workerId; task.claimedAt = new Date().toISOString();
|
|
84
|
+
return { ok: true, task };
|
|
85
|
+
}
|
|
86
|
+
function validateTestEvidence(value, entityRef) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
if (!isObject(value)) return [finding("P0", "TEST_EVIDENCE_OBJECT", entityRef, "TestEvidence must be an object")];
|
|
89
|
+
if (value.schemaVersion !== TEST_EVIDENCE_SCHEMA) findings.push(finding("P0", "TEST_EVIDENCE_SCHEMA", `${entityRef}.schemaVersion`, `Expected ${TEST_EVIDENCE_SCHEMA}`));
|
|
90
|
+
if (!text(value.evidenceId).trim()) findings.push(finding("P0", "TEST_EVIDENCE_ID", `${entityRef}.evidenceId`, "evidenceId is required"));
|
|
91
|
+
if (!["test", "build", "lint", "security", "benchmark"].includes(value.kind)) findings.push(finding("P0", "TEST_EVIDENCE_KIND", `${entityRef}.kind`, "kind is unsupported"));
|
|
92
|
+
if (!["local", "trusted-runner"].includes(value.runner)) findings.push(finding("P0", "TEST_EVIDENCE_RUNNER", `${entityRef}.runner`, "runner must be local or trusted-runner"));
|
|
93
|
+
if (!text(value.command).trim()) findings.push(finding("P0", "TEST_EVIDENCE_COMMAND", `${entityRef}.command`, "command is required"));
|
|
94
|
+
if (!Number.isInteger(value.exitCode)) findings.push(finding("P0", "TEST_EVIDENCE_EXIT", `${entityRef}.exitCode`, "exitCode must be an integer"));
|
|
95
|
+
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"));
|
|
96
|
+
if (!text(value.summary).trim()) findings.push(finding("P0", "TEST_EVIDENCE_SUMMARY", `${entityRef}.summary`, "summary is required"));
|
|
97
|
+
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"));
|
|
98
|
+
return findings;
|
|
99
|
+
}
|
|
100
|
+
function normalizeTestEvidence(values) {
|
|
101
|
+
if (!Array.isArray(values)) return { evidence: [], findings: [finding("P0", "TEST_EVIDENCE_ARRAY", "report.evidence", "evidence must be an array")] };
|
|
102
|
+
const findings = values.flatMap((value, index) => validateTestEvidence(value, `report.evidence[${index}]`));
|
|
103
|
+
return findings.length ? { evidence: [], findings } : { evidence: values.map((value) => ({ ...value })), findings: [] };
|
|
104
|
+
}
|
|
105
|
+
function hasPassingTestEvidence(task) {
|
|
106
|
+
const evidence = task?.report?.evidence;
|
|
107
|
+
return Array.isArray(evidence) && evidence.length > 0
|
|
108
|
+
&& evidence.every((value, index) => validateTestEvidence(value, `task.report.evidence[${index}]`).length === 0 && value.exitCode === 0);
|
|
109
|
+
}
|
|
110
|
+
function reportTask(tasks, taskId, workerId, report) {
|
|
111
|
+
const task = tasks.find((t) => t.taskId === taskId);
|
|
112
|
+
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
113
|
+
if (task.owner && task.owner !== workerId) return { ok: false, error: `task ${taskId} not owned by ${workerId}` };
|
|
114
|
+
if (!["claimed", "running"].includes(task.status)) return { ok: false, error: `task ${taskId} is ${task.status}, cannot report` };
|
|
115
|
+
if (!isObject(report)) return { ok: false, error: "report must be an object" };
|
|
116
|
+
if (!text(report.output).trim() && report.evidence === undefined) return { ok: false, error: "report must have output or evidence" };
|
|
117
|
+
const normalizedReport = { output: text(report.output) };
|
|
118
|
+
if (report.evidence !== undefined) {
|
|
119
|
+
const normalized = normalizeTestEvidence(report.evidence);
|
|
120
|
+
if (normalized.findings.length) return { ok: false, error: "report evidence is invalid", findings: normalized.findings };
|
|
121
|
+
normalizedReport.evidence = normalized.evidence;
|
|
122
|
+
}
|
|
123
|
+
task.status = "reported"; task.report = normalizedReport; task.reportedAt = new Date().toISOString();
|
|
124
|
+
task.selfReported = !hasTrustedTestEvidence(task);
|
|
125
|
+
return { ok: true, task, hasEvidence: hasPassingTestEvidence(task) };
|
|
126
|
+
}
|
|
127
|
+
function acceptTask(tasks, taskId, accept, actorRole) {
|
|
128
|
+
if (actorRole !== "board") return { ok: false, error: `role ${actorRole} cannot accept` };
|
|
129
|
+
const task = tasks.find((t) => t.taskId === taskId);
|
|
130
|
+
if (!task) return { ok: false, error: `task ${taskId} not found` };
|
|
131
|
+
if (task.status !== "reported") return { ok: false, error: `task ${taskId} is ${task.status}, not reported` };
|
|
132
|
+
if (accept && !hasTrustedTestEvidence(task)) return { ok: false, error: `task ${taskId} has no verified task-bound TestEvidence` };
|
|
133
|
+
task.status = accept ? "accepted" : "failed";
|
|
134
|
+
return { ok: true, task };
|
|
135
|
+
}
|
|
136
|
+
function taskTrafficLight(task) {
|
|
137
|
+
if (task.status === "accepted") return hasTrustedTestEvidence(task) ? "green" : "red";
|
|
138
|
+
if (task.status === "reported") return "yellow";
|
|
139
|
+
if (task.status === "failed" || task.status === "blocked" || task.status === "cancelled") return "red";
|
|
140
|
+
return "yellow";
|
|
141
|
+
}
|
|
142
|
+
export { dispatchEligibility, buildTasks, dispatchTask, claimTask, reportTask, acceptTask, taskTrafficLight, validateTestEvidence, normalizeTestEvidence, hasPassingTestEvidence, hasTrustedTestEvidence, reclaimTask };
|
package/swarm-task-routing.mjs
CHANGED
|
@@ -18,6 +18,9 @@ export function resumeView(state, task) {
|
|
|
18
18
|
goalDigest: routing.descriptionDigest, goalRevision: routing.revision,
|
|
19
19
|
remainingRequirements: remaining.original, nextAction: routing.nextAction,
|
|
20
20
|
checkpointAt: routing.checkpointAt, status: task.status,
|
|
21
|
+
continuationNotifications: state.messages.filter(item => item.type === 'task-continuation'
|
|
22
|
+
&& item.payload.taskId === task.taskId && item.payload.chainId === task.chainId
|
|
23
|
+
&& item.to === task.agentId),
|
|
21
24
|
canContinue: task.status === 'active' && routing.handoff === null
|
|
22
25
|
&& !pendingDecision(state, task) && !hasActiveWait(state, task),
|
|
23
26
|
completionAllowed: !remaining.original.length && !remaining.pending.length && routing.handoff === null,
|