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,34 @@
1
+ import { resolveSwarmPaths } from "../lib/paths.js";
2
+ import { journalCompletionSummary, journalReviewEntry } from "../reporting/obsidian-journal.js";
3
+ import { writeWorkflowReport } from "../reporting/reporter.js";
4
+ import { applyReviewDecision } from "../review/review-gate.js";
5
+ import { resolveStateStore } from "./context.js";
6
+ export async function runSwarmReview(options, context) {
7
+ const decision = options.approve ? "approve" : options.reject ? "reject" : null;
8
+ if (!decision) {
9
+ throw new Error("Either --approve or --reject is required");
10
+ }
11
+ const stateStore = resolveStateStore(context);
12
+ const reportConfig = context?.config ?? stateStore.config;
13
+ const workflow = await stateStore.loadWorkflow(options.project);
14
+ const result = applyReviewDecision(workflow, options.task, decision, options.note);
15
+ await stateStore.saveWorkflow(options.project, result.workflow);
16
+ const report = await writeWorkflowReport(options.project, result.workflow, reportConfig);
17
+ // Obsidian journal: review log
18
+ const paths = resolveSwarmPaths(options.project, reportConfig);
19
+ await journalReviewEntry(paths, stateStore.config.obsidianJournal, options.task, decision, options.note);
20
+ // Obsidian journal: completion summary (when all tasks done)
21
+ const allDone = result.workflow.tasks.every((t) => t.status === "done" || t.status === "dead_letter");
22
+ if (allDone && result.workflow.tasks.length > 0) {
23
+ const runs = await stateStore.loadRuns(options.project);
24
+ await journalCompletionSummary(paths, stateStore.config.obsidianJournal, result.workflow, runs);
25
+ }
26
+ return {
27
+ ok: true,
28
+ taskId: options.task,
29
+ decision,
30
+ status: result.task.status,
31
+ localReportPath: report.localReportPath,
32
+ obsidianReportPath: report.obsidianReportPath,
33
+ };
34
+ }
@@ -0,0 +1,7 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmRun(options: {
3
+ project: string;
4
+ task?: string;
5
+ dryRun?: boolean;
6
+ runner?: "manual" | "acp" | "subagent";
7
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,39 @@
1
+ import { resolveSwarmPaths } from "../lib/paths.js";
2
+ import { journalRunEntry } from "../reporting/obsidian-journal.js";
3
+ import { writeWorkflowReport } from "../reporting/reporter.js";
4
+ import { createOrchestrator } from "../services/orchestrator.js";
5
+ import { SessionStore } from "../session/session-store.js";
6
+ import { resolveSessionAdapter, resolveStateStore, resolveSubagentAdapter } from "./context.js";
7
+ export async function runSwarmRun(options, context) {
8
+ const stateStore = resolveStateStore(context);
9
+ const sessionStore = context?.sessionStore ?? new SessionStore(stateStore.config);
10
+ const sessionAdapter = resolveSessionAdapter(context);
11
+ const subagentAdapter = resolveSubagentAdapter(context);
12
+ const reportConfig = context?.config ?? stateStore.config;
13
+ const orchestrator = createOrchestrator({ stateStore, sessionStore, sessionAdapter, subagentAdapter });
14
+ const result = await orchestrator.runOnce({
15
+ projectRoot: options.project,
16
+ taskId: options.task,
17
+ dryRun: options.dryRun,
18
+ runnerOverride: options.runner,
19
+ });
20
+ if (!options.dryRun) {
21
+ const workflow = await stateStore.loadWorkflow(options.project);
22
+ const report = await writeWorkflowReport(options.project, workflow, reportConfig);
23
+ // Obsidian journal: run log
24
+ if (result.runIds?.[0]) {
25
+ const runs = await stateStore.loadRuns(options.project);
26
+ const runRecord = runs.find((r) => r.runId === result.runIds?.[0]);
27
+ if (runRecord) {
28
+ const paths = resolveSwarmPaths(options.project, reportConfig);
29
+ await journalRunEntry(paths, stateStore.config.obsidianJournal, runRecord);
30
+ }
31
+ }
32
+ return {
33
+ ...result,
34
+ localReportPath: report.localReportPath,
35
+ obsidianReportPath: report.obsidianReportPath,
36
+ };
37
+ }
38
+ return result;
39
+ }
@@ -0,0 +1,6 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionCancel(options: {
3
+ project: string;
4
+ run: string;
5
+ reason?: string;
6
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,64 @@
1
+ import { applyAcpRunStatusToWorkflow } from "../review/review-gate.js";
2
+ import { writeWorkflowReport } from "../reporting/reporter.js";
3
+ import { buildSessionRecordFromRun } from "../session/session-lifecycle.js";
4
+ import { resolveSessionAdapter, resolveSessionStore, resolveStateStore, resolveSubagentAdapter } from "./context.js";
5
+ function resolveCancelledAt(result) {
6
+ const normalized = result;
7
+ return normalized.cancelledAt ?? normalized.killedAt;
8
+ }
9
+ export async function runSwarmSessionCancel(options, context) {
10
+ const stateStore = resolveStateStore(context);
11
+ const sessionStore = resolveSessionStore(context);
12
+ const sessionAdapter = resolveSessionAdapter(context);
13
+ const reportConfig = context?.config ?? stateStore.config;
14
+ const runRecord = await stateStore.loadRun(options.project, options.run);
15
+ if (!runRecord) {
16
+ throw new Error(`Run record not found: ${options.run}`);
17
+ }
18
+ if (!runRecord.sessionRef?.sessionKey) {
19
+ throw new Error(`Run record has no session key: ${options.run}`);
20
+ }
21
+ const cancelled = runRecord.runner.type === "subagent"
22
+ ? await resolveSubagentAdapter(context).killSubagentRun(runRecord.sessionRef.sessionKey, options.reason)
23
+ : await sessionAdapter.cancelAcpSession(runRecord.sessionRef.sessionKey, options.reason);
24
+ const nextRun = {
25
+ ...runRecord,
26
+ status: "cancelled",
27
+ endedAt: resolveCancelledAt(cancelled) ?? new Date().toISOString(),
28
+ resultSummary: cancelled.message ?? runRecord.resultSummary,
29
+ events: [
30
+ ...(runRecord.events ?? []),
31
+ {
32
+ at: resolveCancelledAt(cancelled) ?? new Date().toISOString(),
33
+ type: "cancelled",
34
+ detail: { reason: options.reason, message: cancelled.message },
35
+ },
36
+ ],
37
+ };
38
+ await stateStore.writeRun(options.project, nextRun);
39
+ const workflow = await stateStore.loadWorkflow(options.project);
40
+ const nextWorkflow = applyAcpRunStatusToWorkflow(workflow, { taskId: nextRun.taskId, runStatus: "cancelled" });
41
+ await stateStore.saveWorkflow(options.project, nextWorkflow);
42
+ const task = nextWorkflow.tasks.find((entry) => entry.taskId === nextRun.taskId);
43
+ const nextSession = buildSessionRecordFromRun(nextWorkflow, nextRun, task);
44
+ if (nextSession) {
45
+ const existing = await sessionStore.loadSession(options.project, nextSession.sessionId);
46
+ await sessionStore.writeSession(options.project, existing
47
+ ? {
48
+ ...existing,
49
+ ...nextSession,
50
+ createdAt: existing.createdAt,
51
+ }
52
+ : nextSession);
53
+ }
54
+ const report = await writeWorkflowReport(options.project, nextWorkflow, reportConfig);
55
+ return {
56
+ ok: true,
57
+ runId: nextRun.runId,
58
+ status: nextRun.status,
59
+ sessionRef: nextRun.sessionRef,
60
+ resultSummary: nextRun.resultSummary,
61
+ localReportPath: report.localReportPath,
62
+ obsidianReportPath: report.obsidianReportPath,
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export type CleanupResult = {
3
+ ok: boolean;
4
+ orphanedCount: number;
5
+ closedSessionIds: string[];
6
+ message: string;
7
+ };
8
+ /**
9
+ * Detect orphaned sessions and close them.
10
+ * A session is considered orphaned if it is in "active" state but its
11
+ * updatedAt is older than the staleness threshold (default 1 hour).
12
+ */
13
+ export declare function runSwarmSessionCleanup(options: {
14
+ project: string;
15
+ staleMinutes?: number;
16
+ }, context?: SwarmCliContext): Promise<CleanupResult>;
@@ -0,0 +1,34 @@
1
+ import { SessionStore } from "../session/session-store.js";
2
+ import { resolveStateStore } from "./context.js";
3
+ /**
4
+ * Detect orphaned sessions and close them.
5
+ * A session is considered orphaned if it is in "active" state but its
6
+ * updatedAt is older than the staleness threshold (default 1 hour).
7
+ */
8
+ export async function runSwarmSessionCleanup(options, context) {
9
+ const stateStore = resolveStateStore(context);
10
+ const sessionStore = context?.sessionStore ?? new SessionStore(stateStore.config);
11
+ const staleMinutes = options.staleMinutes ?? 60;
12
+ const cutoff = new Date(Date.now() - staleMinutes * 60_000).toISOString();
13
+ const sessions = await sessionStore.listSessions(options.project);
14
+ const orphanCandidates = sessions.filter((session) => session.state === "active" && session.updatedAt < cutoff);
15
+ const closedIds = [];
16
+ for (const session of orphanCandidates) {
17
+ const updated = {
18
+ ...session,
19
+ state: "orphaned",
20
+ updatedAt: new Date().toISOString(),
21
+ summary: `Orphaned: stale for >${staleMinutes}m (was active since ${session.updatedAt})`,
22
+ };
23
+ await sessionStore.writeSession(options.project, updated);
24
+ closedIds.push(session.sessionId);
25
+ }
26
+ return {
27
+ ok: true,
28
+ orphanedCount: closedIds.length,
29
+ closedSessionIds: closedIds,
30
+ message: closedIds.length > 0
31
+ ? `Marked ${closedIds.length} stale session(s) as orphaned`
32
+ : "No orphaned sessions found",
33
+ };
34
+ }
@@ -0,0 +1,6 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionClose(options: {
3
+ project: string;
4
+ run: string;
5
+ reason?: string;
6
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,53 @@
1
+ import { writeWorkflowReport } from "../reporting/reporter.js";
2
+ import { buildSessionRecordFromRun } from "../session/session-lifecycle.js";
3
+ import { resolveSessionAdapter, resolveSessionStore, resolveStateStore } from "./context.js";
4
+ export async function runSwarmSessionClose(options, context) {
5
+ const stateStore = resolveStateStore(context);
6
+ const sessionStore = resolveSessionStore(context);
7
+ const sessionAdapter = resolveSessionAdapter(context);
8
+ const reportConfig = context?.config ?? stateStore.config;
9
+ const runRecord = await stateStore.loadRun(options.project, options.run);
10
+ if (!runRecord) {
11
+ throw new Error(`Run record not found: ${options.run}`);
12
+ }
13
+ if (runRecord.runner.type !== "acp" || !runRecord.sessionRef?.sessionKey) {
14
+ throw new Error(`Run record is not an ACP closable session: ${options.run}`);
15
+ }
16
+ const closed = await sessionAdapter.closeAcpSession(runRecord.sessionRef.sessionKey, options.reason);
17
+ const nextRun = {
18
+ ...runRecord,
19
+ resultSummary: closed.message ?? runRecord.resultSummary,
20
+ events: [
21
+ ...(runRecord.events ?? []),
22
+ {
23
+ at: closed.closedAt ?? new Date().toISOString(),
24
+ type: "closed",
25
+ detail: { reason: options.reason, message: closed.message },
26
+ },
27
+ ],
28
+ };
29
+ await stateStore.writeRun(options.project, nextRun);
30
+ const workflow = await stateStore.loadWorkflow(options.project);
31
+ const task = workflow.tasks.find((entry) => entry.taskId === nextRun.taskId);
32
+ const nextSession = buildSessionRecordFromRun(workflow, nextRun, task);
33
+ if (nextSession) {
34
+ const existing = await sessionStore.loadSession(options.project, nextSession.sessionId);
35
+ await sessionStore.writeSession(options.project, existing
36
+ ? {
37
+ ...existing,
38
+ ...nextSession,
39
+ createdAt: existing.createdAt,
40
+ }
41
+ : nextSession);
42
+ }
43
+ const report = await writeWorkflowReport(options.project, workflow, reportConfig);
44
+ return {
45
+ ok: true,
46
+ runId: nextRun.runId,
47
+ status: nextRun.status,
48
+ sessionRef: nextRun.sessionRef,
49
+ resultSummary: nextRun.resultSummary,
50
+ localReportPath: report.localReportPath,
51
+ obsidianReportPath: report.obsidianReportPath,
52
+ };
53
+ }
@@ -0,0 +1,7 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionFollowup(options: {
3
+ project: string;
4
+ session: string;
5
+ task: string;
6
+ runner?: "acp" | "subagent";
7
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,63 @@
1
+ import { writeWorkflowReport } from "../reporting/reporter.js";
2
+ import { createOrchestrator } from "../services/orchestrator.js";
3
+ import { SessionStore } from "../session/session-store.js";
4
+ import { resolveSessionAdapter, resolveStateStore, resolveSubagentAdapter } from "./context.js";
5
+ export async function runSwarmSessionFollowup(options, context) {
6
+ const stateStore = resolveStateStore(context);
7
+ const sessionStore = context?.sessionStore ?? new SessionStore(stateStore.config);
8
+ const sessionAdapter = resolveSessionAdapter(context);
9
+ const subagentAdapter = resolveSubagentAdapter(context);
10
+ const session = await sessionStore.loadSession(options.project, options.session);
11
+ if (!session) {
12
+ return { ok: false, error: `Session not found: ${options.session}` };
13
+ }
14
+ if (session.state !== "active" && session.state !== "idle") {
15
+ return { ok: false, error: `Session ${options.session} is in ${session.state} state, cannot follow up` };
16
+ }
17
+ // Load workflow and find next runnable task, or inject a synthetic one
18
+ const workflow = await stateStore.loadWorkflow(options.project);
19
+ const runnerType = options.runner ?? session.runner;
20
+ // Create a synthetic follow-up task injected into the workflow
21
+ const followupTaskId = `followup-${Date.now()}`;
22
+ const followupTask = {
23
+ taskId: followupTaskId,
24
+ specId: workflow.activeSpecId ?? "followup",
25
+ title: `Follow-up: ${options.task}`,
26
+ description: options.task,
27
+ kind: "coding",
28
+ deps: [],
29
+ status: "ready",
30
+ workspace: { mode: "shared" },
31
+ runner: { type: runnerType, threadRequested: Boolean(session.threadId) },
32
+ review: { required: true },
33
+ session: {
34
+ policy: "require_existing",
35
+ bindingKey: session.scope.bindingKey,
36
+ preferredSessionId: session.sessionId,
37
+ },
38
+ };
39
+ // Add task to workflow
40
+ const updatedWorkflow = {
41
+ ...workflow,
42
+ tasks: [...workflow.tasks, followupTask],
43
+ };
44
+ await stateStore.saveWorkflow(options.project, updatedWorkflow);
45
+ // Dispatch through orchestrator
46
+ const orchestrator = createOrchestrator({ stateStore, sessionStore, sessionAdapter, subagentAdapter });
47
+ const result = await orchestrator.runOnce({
48
+ projectRoot: options.project,
49
+ taskId: followupTaskId,
50
+ runnerOverride: runnerType,
51
+ });
52
+ if (!result.ok) {
53
+ return result;
54
+ }
55
+ const finalWorkflow = await stateStore.loadWorkflow(options.project);
56
+ const report = await writeWorkflowReport(options.project, finalWorkflow, stateStore.config);
57
+ return {
58
+ ...result,
59
+ followupTaskId,
60
+ sessionId: options.session,
61
+ localReportPath: report.localReportPath,
62
+ };
63
+ }
@@ -0,0 +1,5 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionInspect(options: {
3
+ project: string;
4
+ session: string;
5
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,12 @@
1
+ import { resolveSessionStore } from "./context.js";
2
+ export async function runSwarmSessionInspect(options, context) {
3
+ const sessionStore = resolveSessionStore(context);
4
+ const session = await sessionStore.loadSession(options.project, options.session);
5
+ if (!session) {
6
+ throw new Error(`Session record not found: ${options.session}`);
7
+ }
8
+ return {
9
+ ok: true,
10
+ session,
11
+ };
12
+ }
@@ -0,0 +1,4 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionList(options: {
3
+ project: string;
4
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,19 @@
1
+ import { resolveSessionStore } from "./context.js";
2
+ export async function runSwarmSessionList(options, context) {
3
+ const sessionStore = resolveSessionStore(context);
4
+ const sessions = await sessionStore.listSessions(options.project);
5
+ return {
6
+ ok: true,
7
+ sessions: sessions
8
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
9
+ .map((session) => ({
10
+ sessionId: session.sessionId,
11
+ runner: session.runner,
12
+ mode: session.mode,
13
+ state: session.state,
14
+ summary: session.summary,
15
+ lastRunId: session.lastRunId,
16
+ updatedAt: session.updatedAt,
17
+ })),
18
+ };
19
+ }
@@ -0,0 +1,5 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionStatus(options: {
3
+ project: string;
4
+ run: string;
5
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,85 @@
1
+ import { applyAcpRunStatusToWorkflow } from "../review/review-gate.js";
2
+ import { writeWorkflowReport } from "../reporting/reporter.js";
3
+ import { buildSessionRecordFromRun } from "../session/session-lifecycle.js";
4
+ import { resolveSessionAdapter, resolveSessionStore, resolveStateStore, resolveSubagentAdapter } from "./context.js";
5
+ import { syncAcpRunRecord, syncSubagentRunRecord } from "../runtime/session-sync.js";
6
+ function canUseLocalClosedRunFallback(error, runRecord) {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ const hasClosedEvent = (runRecord.events ?? []).some((event) => event.type === "closed");
9
+ return hasClosedEvent && /metadata is missing|Unable to resolve session target/i.test(message);
10
+ }
11
+ export async function runSwarmSessionStatus(options, context) {
12
+ const stateStore = resolveStateStore(context);
13
+ const sessionStore = resolveSessionStore(context);
14
+ const sessionAdapter = resolveSessionAdapter(context);
15
+ const reportConfig = context?.config ?? stateStore.config;
16
+ const runRecord = await stateStore.loadRun(options.project, options.run);
17
+ if (!runRecord) {
18
+ throw new Error(`Run record not found: ${options.run}`);
19
+ }
20
+ if (!runRecord.sessionRef?.sessionKey) {
21
+ throw new Error(`Run record has no session key: ${options.run}`);
22
+ }
23
+ let synced;
24
+ try {
25
+ if (runRecord.runner.type === "acp") {
26
+ const remoteStatus = await sessionAdapter.getAcpSessionStatus(runRecord.sessionRef.sessionKey);
27
+ synced = syncAcpRunRecord(runRecord, remoteStatus);
28
+ }
29
+ else if (runRecord.runner.type === "subagent") {
30
+ const remoteStatus = await resolveSubagentAdapter(context).getSubagentRunStatus(runRecord.sessionRef.sessionKey);
31
+ synced = syncSubagentRunRecord(runRecord, remoteStatus);
32
+ }
33
+ else {
34
+ throw new Error(`Run record is not a session-backed runner: ${options.run}`);
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (!canUseLocalClosedRunFallback(error, runRecord)) {
39
+ throw error;
40
+ }
41
+ synced = {
42
+ runRecord: {
43
+ ...runRecord,
44
+ resultSummary: runRecord.resultSummary ?? "session metadata missing after close; using local ledger",
45
+ },
46
+ remoteStatus: {
47
+ sessionKey: runRecord.sessionRef.sessionKey,
48
+ state: runRecord.status === "cancelled" ? "cancelled" : "completed",
49
+ checkedAt: new Date().toISOString(),
50
+ message: "session metadata missing after close; using local ledger",
51
+ },
52
+ };
53
+ }
54
+ await stateStore.writeRun(options.project, synced.runRecord);
55
+ const workflow = await stateStore.loadWorkflow(options.project);
56
+ const nextWorkflow = applyAcpRunStatusToWorkflow(workflow, {
57
+ taskId: synced.runRecord.taskId,
58
+ runStatus: synced.runRecord.status,
59
+ summary: synced.runRecord.resultSummary,
60
+ at: synced.remoteStatus.checkedAt,
61
+ });
62
+ await stateStore.saveWorkflow(options.project, nextWorkflow);
63
+ const task = nextWorkflow.tasks.find((entry) => entry.taskId === synced.runRecord.taskId);
64
+ const nextSession = buildSessionRecordFromRun(nextWorkflow, synced.runRecord, task);
65
+ if (nextSession) {
66
+ const existing = await sessionStore.loadSession(options.project, nextSession.sessionId);
67
+ await sessionStore.writeSession(options.project, existing
68
+ ? {
69
+ ...existing,
70
+ ...nextSession,
71
+ createdAt: existing.createdAt,
72
+ }
73
+ : nextSession);
74
+ }
75
+ const report = await writeWorkflowReport(options.project, nextWorkflow, reportConfig);
76
+ return {
77
+ ok: true,
78
+ runId: synced.runRecord.runId,
79
+ status: synced.runRecord.status,
80
+ sessionRef: synced.runRecord.sessionRef,
81
+ resultSummary: synced.runRecord.resultSummary,
82
+ localReportPath: report.localReportPath,
83
+ obsidianReportPath: report.obsidianReportPath,
84
+ };
85
+ }
@@ -0,0 +1,6 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export declare function runSwarmSessionSteer(options: {
3
+ project: string;
4
+ session: string;
5
+ message: string;
6
+ }, context?: SwarmCliContext): Promise<unknown>;
@@ -0,0 +1,40 @@
1
+ import { SessionStore } from "../session/session-store.js";
2
+ import { resolveSessionAdapter, resolveStateStore } from "./context.js";
3
+ export async function runSwarmSessionSteer(options, context) {
4
+ const stateStore = resolveStateStore(context);
5
+ const sessionStore = context?.sessionStore ?? new SessionStore(stateStore.config);
6
+ const sessionAdapter = resolveSessionAdapter(context);
7
+ const session = await sessionStore.loadSession(options.project, options.session);
8
+ if (!session) {
9
+ return { ok: false, error: `Session not found: ${options.session}` };
10
+ }
11
+ if (session.state !== "active") {
12
+ return { ok: false, error: `Session ${options.session} is in ${session.state} state, must be active to steer` };
13
+ }
14
+ if (!session.providerRef.sessionKey) {
15
+ return { ok: false, error: `Session ${options.session} has no provider session key` };
16
+ }
17
+ // Use the session adapter to send a follow-up message via spawn with the existing session
18
+ // This leverages the same spawn mechanism with existingSessionKey
19
+ const result = await sessionAdapter.spawnAcpSession({
20
+ task: options.message,
21
+ runtime: "acp",
22
+ agentId: "codex",
23
+ mode: "session",
24
+ thread: true,
25
+ existingSessionKey: session.providerRef.sessionKey,
26
+ threadId: session.threadId,
27
+ });
28
+ // Update session record
29
+ await sessionStore.writeSession(options.project, {
30
+ ...session,
31
+ updatedAt: new Date().toISOString(),
32
+ summary: `Steered: ${options.message.slice(0, 80)}`,
33
+ });
34
+ return {
35
+ ok: true,
36
+ sessionId: options.session,
37
+ sessionKey: result.sessionKey,
38
+ message: `Steering message sent to session ${options.session}`,
39
+ };
40
+ }
@@ -0,0 +1,81 @@
1
+ import { type SwarmCliContext } from "./context.js";
2
+ export type SwarmStatusResult = {
3
+ ok: true;
4
+ workflow: {
5
+ lifecycle: string;
6
+ activeSpecId?: string;
7
+ totalTasks: number;
8
+ readyTasks: number;
9
+ runningTasks: number;
10
+ blockedTasks: number;
11
+ reviewQueueSize: number;
12
+ lastAction?: {
13
+ at: string;
14
+ type: string;
15
+ message?: string;
16
+ };
17
+ };
18
+ reviewQueue: Array<{
19
+ taskId: string;
20
+ title?: string;
21
+ status?: string;
22
+ latestRunId?: string;
23
+ latestRunStatus?: string;
24
+ latestRunSummary?: string;
25
+ recommendedAction?: string;
26
+ }>;
27
+ attention: Array<{
28
+ kind: string;
29
+ taskId: string;
30
+ title?: string;
31
+ message: string;
32
+ latestRunId?: string;
33
+ latestRunStatus?: string;
34
+ latestRunSummary?: string;
35
+ recommendedAction: string;
36
+ }>;
37
+ recentRuns: Array<{
38
+ runId: string;
39
+ taskId: string;
40
+ runner: string;
41
+ status: string;
42
+ resultSummary?: string;
43
+ }>;
44
+ highlights: Array<{
45
+ kind: string;
46
+ runId: string;
47
+ taskId: string;
48
+ runner: string;
49
+ summary?: string;
50
+ recommendedAction: string;
51
+ }>;
52
+ recommendedActions: string[];
53
+ sessions: {
54
+ total: number;
55
+ active: number;
56
+ idle: number;
57
+ closed: number;
58
+ failed: number;
59
+ orphaned: number;
60
+ };
61
+ recentSessions: Array<{
62
+ sessionId: string;
63
+ runner: string;
64
+ mode: string;
65
+ state: string;
66
+ summary?: string;
67
+ lastRunId?: string;
68
+ }>;
69
+ reusableSessionCandidates: Array<{
70
+ taskId: string;
71
+ title?: string;
72
+ policy: string;
73
+ eligible: boolean;
74
+ bindingKey?: string;
75
+ selectedSessionId?: string;
76
+ reason: string;
77
+ }>;
78
+ };
79
+ export declare function runSwarmStatus(options: {
80
+ project: string;
81
+ }, context?: SwarmCliContext): Promise<SwarmStatusResult>;