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.
@@ -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 };
@@ -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,