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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/src/cli/context.d.ts +18 -0
- package/dist/src/cli/context.js +60 -0
- package/dist/src/cli/output.d.ts +1 -0
- package/dist/src/cli/output.js +9 -0
- package/dist/src/cli/register-swarm-cli.d.ts +6 -0
- package/dist/src/cli/register-swarm-cli.js +130 -0
- package/dist/src/cli/swarm-doctor.d.ts +40 -0
- package/dist/src/cli/swarm-doctor.js +69 -0
- package/dist/src/cli/swarm-init.d.ts +9 -0
- package/dist/src/cli/swarm-init.js +10 -0
- package/dist/src/cli/swarm-plan.d.ts +13 -0
- package/dist/src/cli/swarm-plan.js +39 -0
- package/dist/src/cli/swarm-report.d.ts +9 -0
- package/dist/src/cli/swarm-report.js +14 -0
- package/dist/src/cli/swarm-review.d.ts +8 -0
- package/dist/src/cli/swarm-review.js +34 -0
- package/dist/src/cli/swarm-run.d.ts +7 -0
- package/dist/src/cli/swarm-run.js +39 -0
- package/dist/src/cli/swarm-session-cancel.d.ts +6 -0
- package/dist/src/cli/swarm-session-cancel.js +64 -0
- package/dist/src/cli/swarm-session-cleanup.d.ts +16 -0
- package/dist/src/cli/swarm-session-cleanup.js +34 -0
- package/dist/src/cli/swarm-session-close.d.ts +6 -0
- package/dist/src/cli/swarm-session-close.js +53 -0
- package/dist/src/cli/swarm-session-followup.d.ts +7 -0
- package/dist/src/cli/swarm-session-followup.js +63 -0
- package/dist/src/cli/swarm-session-inspect.d.ts +5 -0
- package/dist/src/cli/swarm-session-inspect.js +12 -0
- package/dist/src/cli/swarm-session-list.d.ts +4 -0
- package/dist/src/cli/swarm-session-list.js +19 -0
- package/dist/src/cli/swarm-session-status.d.ts +5 -0
- package/dist/src/cli/swarm-session-status.js +85 -0
- package/dist/src/cli/swarm-session-steer.d.ts +6 -0
- package/dist/src/cli/swarm-session-steer.js +40 -0
- package/dist/src/cli/swarm-status.d.ts +81 -0
- package/dist/src/cli/swarm-status.js +56 -0
- package/dist/src/config.d.ts +159 -0
- package/dist/src/config.js +292 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +24 -0
- package/dist/src/lib/json-file.d.ts +5 -0
- package/dist/src/lib/json-file.js +42 -0
- package/dist/src/lib/paths.d.ts +25 -0
- package/dist/src/lib/paths.js +41 -0
- package/dist/src/planning/planner.d.ts +3 -0
- package/dist/src/planning/planner.js +39 -0
- package/dist/src/planning/task-graph.d.ts +8 -0
- package/dist/src/planning/task-graph.js +59 -0
- package/dist/src/reporting/obsidian-journal.d.ts +7 -0
- package/dist/src/reporting/obsidian-journal.js +126 -0
- package/dist/src/reporting/operator-summary.d.ts +32 -0
- package/dist/src/reporting/operator-summary.js +124 -0
- package/dist/src/reporting/reporter.d.ts +10 -0
- package/dist/src/reporting/reporter.js +128 -0
- package/dist/src/review/review-gate.d.ts +15 -0
- package/dist/src/review/review-gate.js +116 -0
- package/dist/src/runtime/acp-mapping.d.ts +23 -0
- package/dist/src/runtime/acp-mapping.js +50 -0
- package/dist/src/runtime/acp-runner.d.ts +11 -0
- package/dist/src/runtime/acp-runner.js +83 -0
- package/dist/src/runtime/bridge-errors.d.ts +8 -0
- package/dist/src/runtime/bridge-errors.js +59 -0
- package/dist/src/runtime/bridge-manifest.d.ts +30 -0
- package/dist/src/runtime/bridge-manifest.js +87 -0
- package/dist/src/runtime/bridge-openclaw-session-adapter.d.ts +48 -0
- package/dist/src/runtime/bridge-openclaw-session-adapter.js +142 -0
- package/dist/src/runtime/bridge-openclaw-subagent-adapter.d.ts +33 -0
- package/dist/src/runtime/bridge-openclaw-subagent-adapter.js +149 -0
- package/dist/src/runtime/manual-runner.d.ts +9 -0
- package/dist/src/runtime/manual-runner.js +53 -0
- package/dist/src/runtime/openclaw-exec-bridge.d.ts +211 -0
- package/dist/src/runtime/openclaw-exec-bridge.js +498 -0
- package/dist/src/runtime/openclaw-session-adapter.d.ts +48 -0
- package/dist/src/runtime/openclaw-session-adapter.js +14 -0
- package/dist/src/runtime/openclaw-subagent-adapter.d.ts +42 -0
- package/dist/src/runtime/openclaw-subagent-adapter.js +11 -0
- package/dist/src/runtime/public-api-seams.d.ts +23 -0
- package/dist/src/runtime/public-api-seams.js +79 -0
- package/dist/src/runtime/real-openclaw-session-adapter.d.ts +83 -0
- package/dist/src/runtime/real-openclaw-session-adapter.js +91 -0
- package/dist/src/runtime/retry-engine.d.ts +7 -0
- package/dist/src/runtime/retry-engine.js +29 -0
- package/dist/src/runtime/runner-registry.d.ts +6 -0
- package/dist/src/runtime/runner-registry.js +25 -0
- package/dist/src/runtime/session-sync.d.ts +9 -0
- package/dist/src/runtime/session-sync.js +165 -0
- package/dist/src/runtime/subagent-mapping.d.ts +9 -0
- package/dist/src/runtime/subagent-mapping.js +31 -0
- package/dist/src/runtime/subagent-runner.d.ts +9 -0
- package/dist/src/runtime/subagent-runner.js +63 -0
- package/dist/src/runtime/task-runner.d.ts +38 -0
- package/dist/src/runtime/task-runner.js +1 -0
- package/dist/src/schemas/run.schema.json +51 -0
- package/dist/src/schemas/spec.schema.json +30 -0
- package/dist/src/schemas/task.schema.json +48 -0
- package/dist/src/schemas/workflow-state.schema.json +46 -0
- package/dist/src/services/orchestrator.d.ts +47 -0
- package/dist/src/services/orchestrator.js +224 -0
- package/dist/src/session/session-lifecycle.d.ts +6 -0
- package/dist/src/session/session-lifecycle.js +84 -0
- package/dist/src/session/session-selector.d.ts +12 -0
- package/dist/src/session/session-selector.js +72 -0
- package/dist/src/session/session-store.d.ts +14 -0
- package/dist/src/session/session-store.js +84 -0
- package/dist/src/spec/spec-importer.d.ts +4 -0
- package/dist/src/spec/spec-importer.js +80 -0
- package/dist/src/state/state-store.d.ts +22 -0
- package/dist/src/state/state-store.js +187 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.js +116 -0
- package/dist/src/types.d.ts +151 -0
- package/dist/src/types.js +1 -0
- package/dist/src/workspace/workspace-manager.d.ts +8 -0
- package/dist/src/workspace/workspace-manager.js +18 -0
- package/openclaw.plugin.json +121 -0
- package/package.json +62 -0
- package/scripts/openclaw-exec-bridge.mjs +4 -0
- 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,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,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,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,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,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,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,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,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>;
|