openclaw-swarm-layer 0.1.0

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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/src/cli/context.d.ts +18 -0
  4. package/dist/src/cli/context.js +60 -0
  5. package/dist/src/cli/output.d.ts +1 -0
  6. package/dist/src/cli/output.js +9 -0
  7. package/dist/src/cli/register-swarm-cli.d.ts +6 -0
  8. package/dist/src/cli/register-swarm-cli.js +130 -0
  9. package/dist/src/cli/swarm-doctor.d.ts +40 -0
  10. package/dist/src/cli/swarm-doctor.js +69 -0
  11. package/dist/src/cli/swarm-init.d.ts +9 -0
  12. package/dist/src/cli/swarm-init.js +10 -0
  13. package/dist/src/cli/swarm-plan.d.ts +13 -0
  14. package/dist/src/cli/swarm-plan.js +39 -0
  15. package/dist/src/cli/swarm-report.d.ts +9 -0
  16. package/dist/src/cli/swarm-report.js +14 -0
  17. package/dist/src/cli/swarm-review.d.ts +8 -0
  18. package/dist/src/cli/swarm-review.js +34 -0
  19. package/dist/src/cli/swarm-run.d.ts +7 -0
  20. package/dist/src/cli/swarm-run.js +39 -0
  21. package/dist/src/cli/swarm-session-cancel.d.ts +6 -0
  22. package/dist/src/cli/swarm-session-cancel.js +64 -0
  23. package/dist/src/cli/swarm-session-cleanup.d.ts +16 -0
  24. package/dist/src/cli/swarm-session-cleanup.js +34 -0
  25. package/dist/src/cli/swarm-session-close.d.ts +6 -0
  26. package/dist/src/cli/swarm-session-close.js +53 -0
  27. package/dist/src/cli/swarm-session-followup.d.ts +7 -0
  28. package/dist/src/cli/swarm-session-followup.js +63 -0
  29. package/dist/src/cli/swarm-session-inspect.d.ts +5 -0
  30. package/dist/src/cli/swarm-session-inspect.js +12 -0
  31. package/dist/src/cli/swarm-session-list.d.ts +4 -0
  32. package/dist/src/cli/swarm-session-list.js +19 -0
  33. package/dist/src/cli/swarm-session-status.d.ts +5 -0
  34. package/dist/src/cli/swarm-session-status.js +85 -0
  35. package/dist/src/cli/swarm-session-steer.d.ts +6 -0
  36. package/dist/src/cli/swarm-session-steer.js +40 -0
  37. package/dist/src/cli/swarm-status.d.ts +81 -0
  38. package/dist/src/cli/swarm-status.js +56 -0
  39. package/dist/src/config.d.ts +159 -0
  40. package/dist/src/config.js +292 -0
  41. package/dist/src/index.d.ts +10 -0
  42. package/dist/src/index.js +24 -0
  43. package/dist/src/lib/json-file.d.ts +5 -0
  44. package/dist/src/lib/json-file.js +42 -0
  45. package/dist/src/lib/paths.d.ts +25 -0
  46. package/dist/src/lib/paths.js +41 -0
  47. package/dist/src/planning/planner.d.ts +3 -0
  48. package/dist/src/planning/planner.js +39 -0
  49. package/dist/src/planning/task-graph.d.ts +8 -0
  50. package/dist/src/planning/task-graph.js +59 -0
  51. package/dist/src/reporting/obsidian-journal.d.ts +7 -0
  52. package/dist/src/reporting/obsidian-journal.js +126 -0
  53. package/dist/src/reporting/operator-summary.d.ts +32 -0
  54. package/dist/src/reporting/operator-summary.js +124 -0
  55. package/dist/src/reporting/reporter.d.ts +10 -0
  56. package/dist/src/reporting/reporter.js +128 -0
  57. package/dist/src/review/review-gate.d.ts +15 -0
  58. package/dist/src/review/review-gate.js +116 -0
  59. package/dist/src/runtime/acp-mapping.d.ts +23 -0
  60. package/dist/src/runtime/acp-mapping.js +50 -0
  61. package/dist/src/runtime/acp-runner.d.ts +11 -0
  62. package/dist/src/runtime/acp-runner.js +83 -0
  63. package/dist/src/runtime/bridge-errors.d.ts +8 -0
  64. package/dist/src/runtime/bridge-errors.js +59 -0
  65. package/dist/src/runtime/bridge-manifest.d.ts +30 -0
  66. package/dist/src/runtime/bridge-manifest.js +87 -0
  67. package/dist/src/runtime/bridge-openclaw-session-adapter.d.ts +48 -0
  68. package/dist/src/runtime/bridge-openclaw-session-adapter.js +142 -0
  69. package/dist/src/runtime/bridge-openclaw-subagent-adapter.d.ts +33 -0
  70. package/dist/src/runtime/bridge-openclaw-subagent-adapter.js +149 -0
  71. package/dist/src/runtime/manual-runner.d.ts +9 -0
  72. package/dist/src/runtime/manual-runner.js +53 -0
  73. package/dist/src/runtime/openclaw-exec-bridge.d.ts +211 -0
  74. package/dist/src/runtime/openclaw-exec-bridge.js +498 -0
  75. package/dist/src/runtime/openclaw-session-adapter.d.ts +48 -0
  76. package/dist/src/runtime/openclaw-session-adapter.js +14 -0
  77. package/dist/src/runtime/openclaw-subagent-adapter.d.ts +42 -0
  78. package/dist/src/runtime/openclaw-subagent-adapter.js +11 -0
  79. package/dist/src/runtime/public-api-seams.d.ts +23 -0
  80. package/dist/src/runtime/public-api-seams.js +79 -0
  81. package/dist/src/runtime/real-openclaw-session-adapter.d.ts +83 -0
  82. package/dist/src/runtime/real-openclaw-session-adapter.js +91 -0
  83. package/dist/src/runtime/retry-engine.d.ts +7 -0
  84. package/dist/src/runtime/retry-engine.js +29 -0
  85. package/dist/src/runtime/runner-registry.d.ts +6 -0
  86. package/dist/src/runtime/runner-registry.js +25 -0
  87. package/dist/src/runtime/session-sync.d.ts +9 -0
  88. package/dist/src/runtime/session-sync.js +165 -0
  89. package/dist/src/runtime/subagent-mapping.d.ts +9 -0
  90. package/dist/src/runtime/subagent-mapping.js +31 -0
  91. package/dist/src/runtime/subagent-runner.d.ts +9 -0
  92. package/dist/src/runtime/subagent-runner.js +63 -0
  93. package/dist/src/runtime/task-runner.d.ts +38 -0
  94. package/dist/src/runtime/task-runner.js +1 -0
  95. package/dist/src/schemas/run.schema.json +51 -0
  96. package/dist/src/schemas/spec.schema.json +30 -0
  97. package/dist/src/schemas/task.schema.json +48 -0
  98. package/dist/src/schemas/workflow-state.schema.json +46 -0
  99. package/dist/src/services/orchestrator.d.ts +47 -0
  100. package/dist/src/services/orchestrator.js +224 -0
  101. package/dist/src/session/session-lifecycle.d.ts +6 -0
  102. package/dist/src/session/session-lifecycle.js +84 -0
  103. package/dist/src/session/session-selector.d.ts +12 -0
  104. package/dist/src/session/session-selector.js +72 -0
  105. package/dist/src/session/session-store.d.ts +14 -0
  106. package/dist/src/session/session-store.js +84 -0
  107. package/dist/src/spec/spec-importer.d.ts +4 -0
  108. package/dist/src/spec/spec-importer.js +80 -0
  109. package/dist/src/state/state-store.d.ts +22 -0
  110. package/dist/src/state/state-store.js +187 -0
  111. package/dist/src/tools/index.d.ts +2 -0
  112. package/dist/src/tools/index.js +116 -0
  113. package/dist/src/types.d.ts +151 -0
  114. package/dist/src/types.js +1 -0
  115. package/dist/src/workspace/workspace-manager.d.ts +8 -0
  116. package/dist/src/workspace/workspace-manager.js +18 -0
  117. package/openclaw.plugin.json +121 -0
  118. package/package.json +62 -0
  119. package/scripts/openclaw-exec-bridge.mjs +4 -0
  120. package/skills/swarm-layer/SKILL.md +358 -0
@@ -0,0 +1,39 @@
1
+ import { defaultSwarmPluginConfig } from "../config.js";
2
+ import { upsertTaskStatuses, validateTaskGraph } from "./task-graph.js";
3
+ function taskIdForPhase(phaseId, index) {
4
+ return `${phaseId}-task-${index + 1}`;
5
+ }
6
+ export function planTasksFromSpec(spec, config) {
7
+ const resolvedConfig = { ...defaultSwarmPluginConfig, ...config };
8
+ const tasks = [];
9
+ for (const phase of spec.phases) {
10
+ const phaseTasks = phase.tasks.length > 0 ? phase.tasks : [`Execute ${phase.title}`];
11
+ phaseTasks.forEach((taskTitle, index) => {
12
+ const previousTask = tasks[tasks.length - 1];
13
+ tasks.push({
14
+ taskId: taskIdForPhase(phase.phaseId, index),
15
+ specId: spec.specId,
16
+ phaseId: phase.phaseId,
17
+ title: taskTitle,
18
+ description: taskTitle,
19
+ kind: "coding",
20
+ deps: previousTask ? [previousTask.taskId] : [],
21
+ status: "planned",
22
+ workspace: {
23
+ mode: resolvedConfig.defaultWorkspaceMode,
24
+ },
25
+ runner: {
26
+ type: resolvedConfig.defaultRunner,
27
+ },
28
+ review: {
29
+ required: resolvedConfig.reviewRequiredByDefault,
30
+ },
31
+ });
32
+ });
33
+ }
34
+ const validation = validateTaskGraph(tasks);
35
+ if (!validation.ok) {
36
+ throw new Error(`Invalid task graph: ${validation.errors.join("; ")}`);
37
+ }
38
+ return upsertTaskStatuses(tasks);
39
+ }
@@ -0,0 +1,8 @@
1
+ import type { TaskNode } from "../types.js";
2
+ export type TaskGraphValidationResult = {
3
+ ok: boolean;
4
+ errors: string[];
5
+ };
6
+ export declare function validateTaskGraph(tasks: TaskNode[]): TaskGraphValidationResult;
7
+ export declare function getRunnableTasks(tasks: TaskNode[]): TaskNode[];
8
+ export declare function upsertTaskStatuses(tasks: TaskNode[]): TaskNode[];
@@ -0,0 +1,59 @@
1
+ export function validateTaskGraph(tasks) {
2
+ const errors = [];
3
+ const ids = new Set();
4
+ const tasksById = new Map(tasks.map((task) => [task.taskId, task]));
5
+ for (const task of tasks) {
6
+ if (ids.has(task.taskId)) {
7
+ errors.push(`duplicate taskId: ${task.taskId}`);
8
+ }
9
+ ids.add(task.taskId);
10
+ for (const dep of task.deps) {
11
+ if (!tasksById.has(dep)) {
12
+ errors.push(`missing dependency: ${task.taskId} -> ${dep}`);
13
+ }
14
+ }
15
+ }
16
+ const visiting = new Set();
17
+ const visited = new Set();
18
+ const visit = (taskId) => {
19
+ if (visited.has(taskId)) {
20
+ return;
21
+ }
22
+ if (visiting.has(taskId)) {
23
+ errors.push(`cycle detected at taskId: ${taskId}`);
24
+ return;
25
+ }
26
+ visiting.add(taskId);
27
+ const task = tasksById.get(taskId);
28
+ if (task) {
29
+ for (const dep of task.deps) {
30
+ visit(dep);
31
+ }
32
+ }
33
+ visiting.delete(taskId);
34
+ visited.add(taskId);
35
+ };
36
+ tasks.forEach((task) => visit(task.taskId));
37
+ return { ok: errors.length === 0, errors };
38
+ }
39
+ export function getRunnableTasks(tasks) {
40
+ const taskStatus = new Map(tasks.map((task) => [task.taskId, task.status]));
41
+ return tasks.filter((task) => {
42
+ if (task.status !== "planned" && task.status !== "ready") {
43
+ return false;
44
+ }
45
+ return task.deps.every((dep) => taskStatus.get(dep) === "done");
46
+ }).map((task) => ({
47
+ ...task,
48
+ status: task.status === "planned" ? "ready" : task.status,
49
+ }));
50
+ }
51
+ export function upsertTaskStatuses(tasks) {
52
+ const runnableIds = new Set(getRunnableTasks(tasks).map((task) => task.taskId));
53
+ return tasks.map((task) => {
54
+ if (task.status === "planned" && runnableIds.has(task.taskId)) {
55
+ return { ...task, status: "ready" };
56
+ }
57
+ return task;
58
+ });
59
+ }
@@ -0,0 +1,7 @@
1
+ import type { ObsidianJournalConfig } from "../config.js";
2
+ import type { SwarmPaths } from "../lib/paths.js";
3
+ import type { RunRecord, SpecDoc, WorkflowState } from "../types.js";
4
+ export declare function journalRunEntry(paths: SwarmPaths, journal: ObsidianJournalConfig, runRecord: RunRecord): Promise<void>;
5
+ export declare function journalReviewEntry(paths: SwarmPaths, journal: ObsidianJournalConfig, taskId: string, decision: "approve" | "reject", note?: string): Promise<void>;
6
+ export declare function journalSpecArchive(paths: SwarmPaths, journal: ObsidianJournalConfig, spec: SpecDoc): Promise<void>;
7
+ export declare function journalCompletionSummary(paths: SwarmPaths, journal: ObsidianJournalConfig, workflow: WorkflowState, runs: RunRecord[]): Promise<void>;
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir } from "../lib/json-file.js";
4
+ function timestamp() {
5
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
6
+ }
7
+ async function ensureTableFile(filePath, header, columns) {
8
+ await ensureDir(path.dirname(filePath));
9
+ try {
10
+ await fs.access(filePath);
11
+ }
12
+ catch {
13
+ await fs.writeFile(filePath, `# ${header}\n\n${columns}\n`, "utf8");
14
+ }
15
+ }
16
+ async function appendToFile(filePath, line) {
17
+ await fs.appendFile(filePath, `${line}\n`, "utf8");
18
+ }
19
+ /**
20
+ * Write to local path first, then async-mirror to Obsidian if configured.
21
+ * Local write is synchronous (awaited). Obsidian write is fire-and-forget.
22
+ */
23
+ async function dualWrite(localPath, obsidianPath, content, init) {
24
+ // Local: always write
25
+ await ensureTableFile(localPath, init.header, init.columns);
26
+ await appendToFile(localPath, content);
27
+ // Obsidian: async mirror (fire-and-forget)
28
+ if (obsidianPath) {
29
+ ensureTableFile(obsidianPath, init.header, init.columns)
30
+ .then(() => appendToFile(obsidianPath, content))
31
+ .catch(() => { });
32
+ }
33
+ }
34
+ async function dualWriteFile(localPath, obsidianPath, content) {
35
+ await ensureDir(path.dirname(localPath));
36
+ await fs.writeFile(localPath, content, "utf8");
37
+ if (obsidianPath) {
38
+ ensureDir(path.dirname(obsidianPath))
39
+ .then(() => fs.writeFile(obsidianPath, content, "utf8"))
40
+ .catch(() => { });
41
+ }
42
+ }
43
+ // --- Run Log ---
44
+ export async function journalRunEntry(paths, journal, runRecord) {
45
+ if (!journal.enableRunLog)
46
+ return;
47
+ const line = `| ${timestamp()} | \`${runRecord.runId}\` | ${runRecord.taskId} | ${runRecord.runner.type} | ${runRecord.status} | ${runRecord.resultSummary?.slice(0, 80) ?? ""} |`;
48
+ const init = {
49
+ header: "Run Log",
50
+ columns: "| Time | Run ID | Task | Runner | Status | Summary |\n|------|--------|------|--------|--------|---------|",
51
+ };
52
+ await dualWrite(paths.localRunLogPath, paths.obsidianRunLogPath, line, init);
53
+ }
54
+ // --- Review Log ---
55
+ export async function journalReviewEntry(paths, journal, taskId, decision, note) {
56
+ if (!journal.enableReviewLog)
57
+ return;
58
+ const noteText = note ?? "";
59
+ const line = `| ${timestamp()} | ${taskId} | **${decision}** | ${noteText} |`;
60
+ const init = {
61
+ header: "Review Log",
62
+ columns: "| Time | Task | Decision | Note |\n|------|------|----------|------|",
63
+ };
64
+ await dualWrite(paths.localReviewLogPath, paths.obsidianReviewLogPath, line, init);
65
+ }
66
+ // --- Spec Archive ---
67
+ export async function journalSpecArchive(paths, journal, spec) {
68
+ if (!journal.enableSpecArchive)
69
+ return;
70
+ const content = [
71
+ `# ${spec.title}`,
72
+ ``,
73
+ `> Archived from \`${path.basename(spec.sourcePath)}\` at ${timestamp()}`,
74
+ ``,
75
+ `## Goals`,
76
+ ...spec.goals.map((g) => `- ${g}`),
77
+ ``,
78
+ ...(spec.constraints.length > 0
79
+ ? [`## Constraints`, ...spec.constraints.map((c) => `- ${c}`), ``]
80
+ : []),
81
+ ...(spec.acceptanceCriteria.length > 0
82
+ ? [`## Acceptance Criteria`, ...spec.acceptanceCriteria.map((a) => `- ${a}`), ``]
83
+ : []),
84
+ `## Phases`,
85
+ ...spec.phases.flatMap((p) => [`### ${p.title}`, ...p.tasks.map((t) => `- ${t}`), ``]),
86
+ ].join("\n");
87
+ const localPath = path.join(paths.localSpecsArchiveDir, `${spec.specId}.md`);
88
+ const obsidianPath = paths.obsidianSpecsDir ? path.join(paths.obsidianSpecsDir, `${spec.specId}.md`) : undefined;
89
+ await dualWriteFile(localPath, obsidianPath, content);
90
+ }
91
+ // --- Completion Summary ---
92
+ export async function journalCompletionSummary(paths, journal, workflow, runs) {
93
+ if (!journal.enableCompletionSummary)
94
+ return;
95
+ const doneTasks = workflow.tasks.filter((t) => t.status === "done").length;
96
+ const totalTasks = workflow.tasks.length;
97
+ const deadLetterTasks = workflow.tasks.filter((t) => t.status === "dead_letter").length;
98
+ const totalRuns = runs.length;
99
+ const completedRuns = runs.filter((r) => r.status === "completed").length;
100
+ const failedRuns = runs.filter((r) => r.status === "failed" || r.status === "timed_out").length;
101
+ const content = [
102
+ `# Completion Summary`,
103
+ ``,
104
+ `> Generated at ${timestamp()}`,
105
+ ``,
106
+ `## Workflow`,
107
+ `- Spec: ${workflow.activeSpecId ?? "(none)"}`,
108
+ `- Lifecycle: ${workflow.lifecycle}`,
109
+ `- Tasks: ${doneTasks}/${totalTasks} done${deadLetterTasks > 0 ? `, ${deadLetterTasks} dead letter` : ""}`,
110
+ ``,
111
+ `## Execution`,
112
+ `- Total runs: ${totalRuns}`,
113
+ `- Completed: ${completedRuns}`,
114
+ `- Failed/Timed out: ${failedRuns}`,
115
+ ``,
116
+ `## Tasks`,
117
+ ...workflow.tasks.map((t) => `- **${t.title}** — \`${t.status}\` (${t.runner.type})`),
118
+ ``,
119
+ `## Timeline`,
120
+ ...runs
121
+ .sort((a, b) => a.startedAt.localeCompare(b.startedAt))
122
+ .map((r) => `- ${r.startedAt.slice(0, 19)} — \`${r.runId}\` ${r.runner.type} [${r.status}]${r.resultSummary ? ` — ${r.resultSummary.slice(0, 60)}` : ""}`),
123
+ ``,
124
+ ].join("\n");
125
+ await dualWriteFile(paths.localCompletionPath, paths.obsidianCompletionPath, content);
126
+ }
@@ -0,0 +1,32 @@
1
+ import type { RunRecord, WorkflowState } from "../types.js";
2
+ export type ReviewQueueItem = {
3
+ taskId: string;
4
+ title?: string;
5
+ status?: string;
6
+ latestRunId?: string;
7
+ latestRunStatus?: string;
8
+ latestRunSummary?: string;
9
+ recommendedAction?: string;
10
+ };
11
+ export type AttentionItem = {
12
+ kind: "review" | "blocked" | "running" | "dead_letter";
13
+ taskId: string;
14
+ title?: string;
15
+ message: string;
16
+ latestRunId?: string;
17
+ latestRunStatus?: string;
18
+ latestRunSummary?: string;
19
+ recommendedAction: string;
20
+ };
21
+ export type OperatorHighlight = {
22
+ kind: "completed" | "failed" | "cancelled" | "timed_out";
23
+ runId: string;
24
+ taskId: string;
25
+ runner: string;
26
+ summary?: string;
27
+ recommendedAction: string;
28
+ };
29
+ export declare function buildReviewQueueItems(workflow: WorkflowState, runs: RunRecord[]): ReviewQueueItem[];
30
+ export declare function buildAttentionItems(workflow: WorkflowState, runs: RunRecord[]): AttentionItem[];
31
+ export declare function buildOperatorHighlights(runs: RunRecord[]): OperatorHighlight[];
32
+ export declare function buildRecommendedActions(workflow: WorkflowState, runs: RunRecord[]): string[];
@@ -0,0 +1,124 @@
1
+ function buildLatestRunByTask(runs) {
2
+ const latestRunByTask = new Map();
3
+ for (const run of [...runs].sort((left, right) => right.startedAt.localeCompare(left.startedAt))) {
4
+ if (!latestRunByTask.has(run.taskId)) {
5
+ latestRunByTask.set(run.taskId, run);
6
+ }
7
+ }
8
+ return latestRunByTask;
9
+ }
10
+ function toReviewQueueItem(task, latestRun) {
11
+ return {
12
+ taskId: task?.taskId ?? "(unknown)",
13
+ title: task?.title,
14
+ status: task?.status,
15
+ latestRunId: latestRun?.runId,
16
+ latestRunStatus: latestRun?.status,
17
+ latestRunSummary: latestRun?.resultSummary,
18
+ recommendedAction: "Review the latest run outcome and approve or reject the task.",
19
+ };
20
+ }
21
+ export function buildReviewQueueItems(workflow, runs) {
22
+ const latestRunByTask = buildLatestRunByTask(runs);
23
+ return workflow.reviewQueue.map((taskId) => {
24
+ const task = workflow.tasks.find((entry) => entry.taskId === taskId);
25
+ return toReviewQueueItem(task, latestRunByTask.get(taskId));
26
+ });
27
+ }
28
+ export function buildAttentionItems(workflow, runs) {
29
+ const latestRunByTask = buildLatestRunByTask(runs);
30
+ const items = [];
31
+ for (const taskId of workflow.reviewQueue) {
32
+ const task = workflow.tasks.find((entry) => entry.taskId === taskId);
33
+ const latestRun = latestRunByTask.get(taskId);
34
+ items.push({
35
+ kind: "review",
36
+ taskId,
37
+ title: task?.title,
38
+ message: `Review required for ${task?.title ?? taskId}`,
39
+ latestRunId: latestRun?.runId,
40
+ latestRunStatus: latestRun?.status,
41
+ latestRunSummary: latestRun?.resultSummary,
42
+ recommendedAction: "Open the latest run summary, inspect artifacts, then approve or reject the task.",
43
+ });
44
+ }
45
+ for (const task of workflow.tasks.filter((entry) => entry.status === "blocked")) {
46
+ const latestRun = latestRunByTask.get(task.taskId);
47
+ items.push({
48
+ kind: "blocked",
49
+ taskId: task.taskId,
50
+ title: task.title,
51
+ message: `Task is blocked: ${task.title}`,
52
+ latestRunId: latestRun?.runId,
53
+ latestRunStatus: latestRun?.status,
54
+ latestRunSummary: latestRun?.resultSummary,
55
+ recommendedAction: "Inspect the blocking outcome, fix the issue, then rerun or explicitly reject/close the task.",
56
+ });
57
+ }
58
+ for (const task of workflow.tasks.filter((entry) => entry.status === "dead_letter")) {
59
+ const latestRun = latestRunByTask.get(task.taskId);
60
+ items.push({
61
+ kind: "dead_letter",
62
+ taskId: task.taskId,
63
+ title: task.title,
64
+ message: `Task exhausted all retries: ${task.title}`,
65
+ latestRunId: latestRun?.runId,
66
+ latestRunStatus: latestRun?.status,
67
+ latestRunSummary: latestRun?.resultSummary,
68
+ recommendedAction: "Review retry history, fix the root cause, then manually reset and rerun the task.",
69
+ });
70
+ }
71
+ for (const task of workflow.tasks.filter((entry) => entry.status === "running")) {
72
+ const latestRun = latestRunByTask.get(task.taskId);
73
+ items.push({
74
+ kind: "running",
75
+ taskId: task.taskId,
76
+ title: task.title,
77
+ message: `Task is still running: ${task.title}`,
78
+ latestRunId: latestRun?.runId,
79
+ latestRunStatus: latestRun?.status,
80
+ latestRunSummary: latestRun?.resultSummary,
81
+ recommendedAction: "Poll session status again or wait for completion before taking review action.",
82
+ });
83
+ }
84
+ return items;
85
+ }
86
+ export function buildOperatorHighlights(runs) {
87
+ const wantedStatuses = new Set(["completed", "failed", "cancelled", "timed_out"]);
88
+ const seen = new Set();
89
+ const highlights = [];
90
+ for (const run of [...runs].sort((left, right) => right.startedAt.localeCompare(left.startedAt))) {
91
+ if (!wantedStatuses.has(run.status)) {
92
+ continue;
93
+ }
94
+ if (seen.has(run.status)) {
95
+ continue;
96
+ }
97
+ seen.add(run.status);
98
+ highlights.push({
99
+ kind: run.status,
100
+ runId: run.runId,
101
+ taskId: run.taskId,
102
+ runner: run.runner.type,
103
+ summary: run.resultSummary,
104
+ recommendedAction: run.status === "completed"
105
+ ? "Inspect the completion summary and clear review items if the outcome is acceptable."
106
+ : run.status === "failed"
107
+ ? "Inspect the failure summary, fix the issue, then rerun or reject the task."
108
+ : run.status === "cancelled"
109
+ ? "Confirm whether cancellation was intentional, then either unblock or rerun the task."
110
+ : "Inspect the timeout context and decide whether to retry with a larger timeout.",
111
+ });
112
+ }
113
+ return highlights;
114
+ }
115
+ export function buildRecommendedActions(workflow, runs) {
116
+ const actions = new Set();
117
+ for (const item of buildAttentionItems(workflow, runs)) {
118
+ actions.add(item.recommendedAction);
119
+ }
120
+ for (const item of buildOperatorHighlights(runs)) {
121
+ actions.add(item.recommendedAction);
122
+ }
123
+ return [...actions];
124
+ }
@@ -0,0 +1,10 @@
1
+ import type { SwarmPluginConfig } from "../config.js";
2
+ import type { RunRecord, WorkflowState } from "../types.js";
3
+ import { StateStore } from "../state/state-store.js";
4
+ export type ReportWriteResult = {
5
+ report: string;
6
+ localReportPath: string;
7
+ obsidianReportPath?: string;
8
+ };
9
+ export declare function buildWorkflowReport(workflow: WorkflowState, stateStore?: StateStore, runs?: RunRecord[]): string;
10
+ export declare function writeWorkflowReport(projectRoot: string, workflow: WorkflowState, config?: Partial<SwarmPluginConfig>): Promise<ReportWriteResult>;
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+ import { ensureDir } from "../lib/json-file.js";
3
+ import { resolveSwarmPaths } from "../lib/paths.js";
4
+ import { buildAttentionItems, buildOperatorHighlights, buildRecommendedActions, buildReviewQueueItems } from "./operator-summary.js";
5
+ import { SessionStore } from "../session/session-store.js";
6
+ import { summarizeSessionReuseForTask } from "../session/session-selector.js";
7
+ import { StateStore } from "../state/state-store.js";
8
+ import fs from "node:fs/promises";
9
+ function buildRunLines(runs) {
10
+ const sorted = [...runs].sort((left, right) => right.startedAt.localeCompare(left.startedAt)).slice(0, 5);
11
+ return sorted.map((run) => {
12
+ const summary = run.resultSummary?.trim();
13
+ const suffix = summary ? ` - ${summary}` : "";
14
+ return `- ${run.runId}: ${run.runner.type} [${run.status}]${suffix}`;
15
+ });
16
+ }
17
+ function buildReviewQueueLines(workflow, runs) {
18
+ return buildReviewQueueItems(workflow, runs).map((item) => {
19
+ const suffix = item.latestRunSummary ? ` - ${item.latestRunSummary}` : "";
20
+ return `- ${item.taskId}: ${item.title ?? "(unknown task)"}${suffix}`;
21
+ });
22
+ }
23
+ function buildAttentionLines(workflow, runs) {
24
+ return buildAttentionItems(workflow, runs).map((item) => {
25
+ const suffix = item.latestRunSummary ? ` - ${item.latestRunSummary}` : "";
26
+ return `- [${item.kind}] ${item.taskId}: ${item.message}${suffix} | Action: ${item.recommendedAction}`;
27
+ });
28
+ }
29
+ function buildHighlightLines(runs) {
30
+ return buildOperatorHighlights(runs).map((item) => {
31
+ const suffix = item.summary ? ` - ${item.summary}` : "";
32
+ return `- [${item.kind}] ${item.runId}: ${item.runner}/${item.taskId}${suffix} | Action: ${item.recommendedAction}`;
33
+ });
34
+ }
35
+ function buildRecommendedActionLines(workflow, runs) {
36
+ return buildRecommendedActions(workflow, runs).map((action) => `- ${action}`);
37
+ }
38
+ function buildSessionLines(sessions) {
39
+ return [...sessions]
40
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
41
+ .slice(0, 5)
42
+ .map((session) => {
43
+ const suffix = session.summary ? ` - ${session.summary}` : "";
44
+ return `- ${session.sessionId}: ${session.runner}/${session.mode} [${session.state}]${suffix}`;
45
+ });
46
+ }
47
+ function buildSessionCandidateLines(workflow, sessions) {
48
+ return workflow.tasks.map((task) => {
49
+ const candidate = summarizeSessionReuseForTask(task, sessions);
50
+ const binding = candidate.bindingKey ? ` binding=${candidate.bindingKey}` : "";
51
+ const selected = candidate.selectedSessionId ? ` selected=${candidate.selectedSessionId}` : "";
52
+ return `- ${task.taskId}: policy=${candidate.policy} eligible=${candidate.eligible}${binding}${selected} - ${candidate.reason}`;
53
+ });
54
+ }
55
+ export function buildWorkflowReport(workflow, stateStore = new StateStore(), runs = []) {
56
+ const summary = stateStore.summarizeWorkflow(workflow);
57
+ const taskLines = workflow.tasks.map((task) => `- ${task.taskId}: ${task.title} [${task.status}]${task.review.required ? " review" : ""}`);
58
+ const runLines = buildRunLines(runs);
59
+ const reviewQueueLines = buildReviewQueueLines(workflow, runs);
60
+ const attentionLines = buildAttentionLines(workflow, runs);
61
+ const highlightLines = buildHighlightLines(runs);
62
+ const recommendedActionLines = buildRecommendedActionLines(workflow, runs);
63
+ return [
64
+ `# Swarm Report`,
65
+ ``,
66
+ `- Project: ${path.basename(workflow.projectRoot)}`,
67
+ `- Lifecycle: ${summary.lifecycle}`,
68
+ `- Active spec: ${summary.activeSpecId ?? "(none)"}`,
69
+ `- Total tasks: ${summary.totalTasks}`,
70
+ `- Ready tasks: ${summary.readyTasks}`,
71
+ `- Running tasks: ${summary.runningTasks}`,
72
+ `- Blocked tasks: ${summary.blockedTasks}`,
73
+ `- Dead letter tasks: ${summary.deadLetterTasks}`,
74
+ `- Review queue: ${summary.reviewQueueSize}`,
75
+ ...(workflow.lastAction
76
+ ? [
77
+ `- Last action: ${workflow.lastAction.type}${workflow.lastAction.message ? ` - ${workflow.lastAction.message}` : ""}`,
78
+ ]
79
+ : []),
80
+ ``,
81
+ `## Attention`,
82
+ ...(attentionLines.length > 0 ? attentionLines : ["- (none)"]),
83
+ ``,
84
+ `## Tasks`,
85
+ ...(taskLines.length > 0 ? taskLines : ["- (none)"]),
86
+ ``,
87
+ `## Review Queue`,
88
+ ...(reviewQueueLines.length > 0 ? reviewQueueLines : ["- (none)"]),
89
+ ``,
90
+ `## Highlights`,
91
+ ...(highlightLines.length > 0 ? highlightLines : ["- (none)"]),
92
+ ``,
93
+ `## Recommended Actions`,
94
+ ...(recommendedActionLines.length > 0 ? recommendedActionLines : ["- (none)"]),
95
+ ``,
96
+ `## Recent Runs`,
97
+ ...(runLines.length > 0 ? runLines : ["- (none)"]),
98
+ ].join("\n");
99
+ }
100
+ export async function writeWorkflowReport(projectRoot, workflow, config) {
101
+ const paths = resolveSwarmPaths(projectRoot, config);
102
+ const stateStore = new StateStore(config);
103
+ const sessionStore = new SessionStore(config);
104
+ const runs = await stateStore.loadRuns(projectRoot);
105
+ const sessions = await sessionStore.listSessions(projectRoot);
106
+ const report = buildWorkflowReport(workflow, stateStore, runs);
107
+ const sessionCandidateLines = buildSessionCandidateLines(workflow, sessions);
108
+ const reportWithSessions = [
109
+ report,
110
+ ``,
111
+ `## Sessions`,
112
+ ...(buildSessionLines(sessions).length > 0 ? buildSessionLines(sessions) : ["- (none)"]),
113
+ ``,
114
+ `## Session Reuse Candidates`,
115
+ ...(sessionCandidateLines.length > 0 ? sessionCandidateLines : ["- (none)"]),
116
+ ].join("\n");
117
+ await ensureDir(paths.reportsDir);
118
+ await fs.writeFile(paths.localReportPath, `${reportWithSessions}\n`, "utf8");
119
+ if (paths.obsidianReportPath) {
120
+ await ensureDir(path.dirname(paths.obsidianReportPath));
121
+ await fs.writeFile(paths.obsidianReportPath, `${reportWithSessions}\n`, "utf8");
122
+ }
123
+ return {
124
+ report: reportWithSessions,
125
+ localReportPath: paths.localReportPath,
126
+ obsidianReportPath: paths.obsidianReportPath,
127
+ };
128
+ }
@@ -0,0 +1,15 @@
1
+ import type { RunRecord, TaskNode, WorkflowState } from "../types.js";
2
+ export type ReviewDecision = "approve" | "reject";
3
+ export type ReviewResult = {
4
+ workflow: WorkflowState;
5
+ task: TaskNode;
6
+ runRecord?: RunRecord;
7
+ };
8
+ export declare function enqueueReview(workflow: WorkflowState, taskId: string): WorkflowState;
9
+ export declare function applyAcpRunStatusToWorkflow(workflow: WorkflowState, params: {
10
+ taskId: string;
11
+ runStatus: RunRecord["status"];
12
+ summary?: string;
13
+ at?: string;
14
+ }): WorkflowState;
15
+ export declare function applyReviewDecision(workflow: WorkflowState, taskId: string, decision: ReviewDecision, note?: string): ReviewResult;
@@ -0,0 +1,116 @@
1
+ export function enqueueReview(workflow, taskId) {
2
+ if (workflow.reviewQueue.includes(taskId)) {
3
+ return workflow;
4
+ }
5
+ return {
6
+ ...workflow,
7
+ lifecycle: "reviewing",
8
+ reviewQueue: [...workflow.reviewQueue, taskId],
9
+ };
10
+ }
11
+ export function applyAcpRunStatusToWorkflow(workflow, params) {
12
+ const task = workflow.tasks.find((entry) => entry.taskId === params.taskId);
13
+ if (!task) {
14
+ throw new Error(`Unknown taskId: ${params.taskId}`);
15
+ }
16
+ const actionTime = params.at ?? new Date().toISOString();
17
+ function withLastAction(nextWorkflow, type, message) {
18
+ return {
19
+ ...nextWorkflow,
20
+ lastAction: {
21
+ at: actionTime,
22
+ type,
23
+ message,
24
+ },
25
+ };
26
+ }
27
+ let nextTask = task;
28
+ let nextWorkflow = workflow;
29
+ if (params.runStatus === "accepted" || params.runStatus === "running") {
30
+ nextTask = {
31
+ ...task,
32
+ status: "running",
33
+ };
34
+ nextWorkflow = {
35
+ ...workflow,
36
+ lifecycle: "running",
37
+ tasks: workflow.tasks.map((entry) => (entry.taskId === task.taskId ? nextTask : entry)),
38
+ };
39
+ return withLastAction(nextWorkflow, `run:${params.runStatus}`, params.summary);
40
+ }
41
+ if (params.runStatus === "completed") {
42
+ nextTask = {
43
+ ...task,
44
+ status: task.review.required ? "review_required" : "done",
45
+ review: {
46
+ ...task.review,
47
+ status: task.review.required ? "pending" : task.review.status,
48
+ },
49
+ };
50
+ nextWorkflow = {
51
+ ...workflow,
52
+ lifecycle: task.review.required ? "reviewing" : "planned",
53
+ tasks: workflow.tasks.map((entry) => (entry.taskId === task.taskId ? nextTask : entry)),
54
+ };
55
+ const queued = task.review.required ? enqueueReview(nextWorkflow, task.taskId) : nextWorkflow;
56
+ return withLastAction(queued, `run:${params.runStatus}`, params.summary);
57
+ }
58
+ if (params.runStatus === "failed" || params.runStatus === "timed_out") {
59
+ nextTask = {
60
+ ...task,
61
+ status: "review_required",
62
+ review: {
63
+ ...task.review,
64
+ status: "pending",
65
+ },
66
+ };
67
+ nextWorkflow = {
68
+ ...workflow,
69
+ lifecycle: "reviewing",
70
+ tasks: workflow.tasks.map((entry) => (entry.taskId === task.taskId ? nextTask : entry)),
71
+ };
72
+ return withLastAction(enqueueReview(nextWorkflow, task.taskId), `run:${params.runStatus}`, params.summary);
73
+ }
74
+ if (params.runStatus === "cancelled") {
75
+ nextTask = {
76
+ ...task,
77
+ status: "blocked",
78
+ };
79
+ return withLastAction({
80
+ ...workflow,
81
+ lifecycle: "blocked",
82
+ tasks: workflow.tasks.map((entry) => (entry.taskId === task.taskId ? nextTask : entry)),
83
+ reviewQueue: workflow.reviewQueue.filter((entry) => entry !== task.taskId),
84
+ }, `run:${params.runStatus}`, params.summary);
85
+ }
86
+ return workflow;
87
+ }
88
+ export function applyReviewDecision(workflow, taskId, decision, note) {
89
+ const task = workflow.tasks.find((entry) => entry.taskId === taskId);
90
+ if (!task) {
91
+ throw new Error(`Unknown taskId: ${taskId}`);
92
+ }
93
+ const updatedTask = {
94
+ ...task,
95
+ status: decision === "approve" ? "done" : "blocked",
96
+ review: {
97
+ ...task.review,
98
+ status: decision === "approve" ? "approved" : "rejected",
99
+ },
100
+ };
101
+ const nextWorkflow = {
102
+ ...workflow,
103
+ lifecycle: decision === "approve" ? "planned" : "blocked",
104
+ tasks: workflow.tasks.map((entry) => (entry.taskId === taskId ? updatedTask : entry)),
105
+ reviewQueue: workflow.reviewQueue.filter((entry) => entry !== taskId),
106
+ lastAction: {
107
+ at: new Date().toISOString(),
108
+ type: `review:${decision}`,
109
+ message: note,
110
+ },
111
+ };
112
+ return {
113
+ workflow: nextWorkflow,
114
+ task: updatedTask,
115
+ };
116
+ }