opencodekit 0.14.1 → 0.14.2
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/dist/index.js +1 -1
- package/dist/template/.opencode/.background-tasks.json +96 -0
- package/dist/template/.opencode/.ralph-state.json +12 -0
- package/dist/template/.opencode/AGENTS.md +76 -5
- package/dist/template/.opencode/agent/build.md +16 -7
- package/dist/template/.opencode/agent/looker.md +124 -0
- package/dist/template/.opencode/agent/rush.md +18 -6
- package/dist/template/.opencode/agent/scout.md +0 -1
- package/dist/template/.opencode/agent/vision.md +0 -1
- package/dist/template/.opencode/command/implement.md +51 -10
- package/dist/template/.opencode/command/new-feature.md +68 -10
- package/dist/template/.opencode/command/plan.md +59 -10
- package/dist/template/.opencode/command/ralph-loop.md +97 -0
- package/dist/template/.opencode/command/start.md +13 -10
- package/dist/template/.opencode/memory/{project/beads-workflow.md → beads-workflow.md} +53 -0
- package/dist/template/.opencode/memory/project/conventions.md +53 -3
- package/dist/template/.opencode/opencode.json +4 -0
- package/dist/template/.opencode/package.json +1 -0
- package/dist/template/.opencode/plugin/lsp.ts +299 -0
- package/dist/template/.opencode/plugin/ralph-wiggum.ts +182 -0
- package/dist/template/.opencode/tool/background.ts +461 -0
- package/dist/template/.opencode/tool/ralph.ts +203 -0
- package/package.json +1 -1
- /package/dist/template/.opencode/memory/{project/README.md → README.md} +0 -0
- /package/dist/template/.opencode/plugin/{notification.ts → notification.ts.bak} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Wiggum Plugin for OpenCode
|
|
3
|
+
*
|
|
4
|
+
* Handles the session.idle event to continue the Ralph loop.
|
|
5
|
+
* Tools are defined separately in .opencode/tool/ralph.ts
|
|
6
|
+
*
|
|
7
|
+
* Based on: https://ghuntley.com/ralph/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
12
|
+
|
|
13
|
+
const STATE_FILE = ".opencode/.ralph-state.json";
|
|
14
|
+
const IDLE_DEBOUNCE_MS = 2000;
|
|
15
|
+
let lastIdleTime = 0;
|
|
16
|
+
|
|
17
|
+
interface RalphState {
|
|
18
|
+
active: boolean;
|
|
19
|
+
sessionID: string | null;
|
|
20
|
+
iteration: number;
|
|
21
|
+
maxIterations: number;
|
|
22
|
+
completionPromise: string;
|
|
23
|
+
task: string;
|
|
24
|
+
prdFile: string | null;
|
|
25
|
+
progressFile: string;
|
|
26
|
+
startedAt: number | null;
|
|
27
|
+
mode: "hitl" | "afk";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadState(): Promise<RalphState | null> {
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(STATE_FILE, "utf-8");
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function saveState(state: RalphState): Promise<void> {
|
|
40
|
+
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resetState(): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
await fs.unlink(STATE_FILE);
|
|
46
|
+
} catch {
|
|
47
|
+
// File doesn't exist, that's fine
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const RalphWiggum: Plugin = async ({ client }) => {
|
|
52
|
+
const log = async (
|
|
53
|
+
message: string,
|
|
54
|
+
level: "info" | "warn" | "error" = "info",
|
|
55
|
+
) => {
|
|
56
|
+
await client.app
|
|
57
|
+
.log({
|
|
58
|
+
body: { service: "ralph-wiggum", level, message },
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const showToast = async (
|
|
64
|
+
title: string,
|
|
65
|
+
message: string,
|
|
66
|
+
variant: "info" | "success" | "warning" | "error" = "info",
|
|
67
|
+
) => {
|
|
68
|
+
await client.tui
|
|
69
|
+
.showToast({
|
|
70
|
+
body: {
|
|
71
|
+
title: `Ralph: ${title}`,
|
|
72
|
+
message,
|
|
73
|
+
variant,
|
|
74
|
+
duration: variant === "error" ? 8000 : 5000,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
.catch(() => {});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const buildContinuationPrompt = (state: RalphState): string => {
|
|
81
|
+
const prdRef = state.prdFile ? `@${state.prdFile} ` : "";
|
|
82
|
+
const progressRef = `@${state.progressFile}`;
|
|
83
|
+
|
|
84
|
+
return `
|
|
85
|
+
${prdRef}${progressRef}
|
|
86
|
+
|
|
87
|
+
## Ralph Wiggum Loop - Iteration ${state.iteration}/${state.maxIterations}
|
|
88
|
+
|
|
89
|
+
You are in an autonomous loop. Continue working on the task.
|
|
90
|
+
|
|
91
|
+
**Task:** ${state.task}
|
|
92
|
+
|
|
93
|
+
**Instructions:**
|
|
94
|
+
1. Review the PRD/task list and progress file
|
|
95
|
+
2. Choose the highest-priority INCOMPLETE task
|
|
96
|
+
3. Implement ONE feature/change only
|
|
97
|
+
4. Run feedback loops: typecheck, test, lint
|
|
98
|
+
5. Commit if all pass
|
|
99
|
+
6. Update ${state.progressFile}
|
|
100
|
+
7. If ALL tasks complete, output: ${state.completionPromise}
|
|
101
|
+
|
|
102
|
+
**Constraints:** ONE feature per iteration. Quality over speed.
|
|
103
|
+
`.trim();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleSessionIdle = async (sessionID: string): Promise<void> => {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (now - lastIdleTime < IDLE_DEBOUNCE_MS) return;
|
|
109
|
+
lastIdleTime = now;
|
|
110
|
+
|
|
111
|
+
const state = await loadState();
|
|
112
|
+
if (!state?.active || state.sessionID !== sessionID) return;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const messagesResponse = await client.session.messages({
|
|
116
|
+
path: { id: sessionID },
|
|
117
|
+
});
|
|
118
|
+
const messages = messagesResponse.data || [];
|
|
119
|
+
const lastMessage = messages[messages.length - 1];
|
|
120
|
+
|
|
121
|
+
const lastText =
|
|
122
|
+
lastMessage?.parts
|
|
123
|
+
?.filter((p) => p.type === "text")
|
|
124
|
+
.map((p) => ("text" in p ? (p.text as string) : ""))
|
|
125
|
+
.join("") || "";
|
|
126
|
+
|
|
127
|
+
if (lastText.includes(state.completionPromise)) {
|
|
128
|
+
const duration = state.startedAt
|
|
129
|
+
? Math.round((Date.now() - state.startedAt) / 1000 / 60)
|
|
130
|
+
: 0;
|
|
131
|
+
await showToast(
|
|
132
|
+
"Complete!",
|
|
133
|
+
`Finished in ${state.iteration} iterations (${duration} min)`,
|
|
134
|
+
"success",
|
|
135
|
+
);
|
|
136
|
+
await log(`Loop completed in ${state.iteration} iterations`);
|
|
137
|
+
await resetState();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
state.iteration++;
|
|
142
|
+
if (state.iteration >= state.maxIterations) {
|
|
143
|
+
await showToast(
|
|
144
|
+
"Stopped",
|
|
145
|
+
`Max iterations (${state.maxIterations}) reached`,
|
|
146
|
+
"warning",
|
|
147
|
+
);
|
|
148
|
+
await log(`Max iterations reached: ${state.maxIterations}`, "warn");
|
|
149
|
+
await resetState();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await saveState(state);
|
|
154
|
+
|
|
155
|
+
await client.session.prompt({
|
|
156
|
+
path: { id: sessionID },
|
|
157
|
+
body: {
|
|
158
|
+
parts: [{ type: "text", text: buildContinuationPrompt(state) }],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await log(`Iteration ${state.iteration}/${state.maxIterations}`);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await log(`Error in Ralph loop: ${error}`, "error");
|
|
165
|
+
await resetState();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
event: async ({ event }) => {
|
|
171
|
+
if (event.type === "session.idle") {
|
|
172
|
+
const sessionID = (event as { properties?: { sessionID?: string } })
|
|
173
|
+
.properties?.sessionID;
|
|
174
|
+
if (sessionID) {
|
|
175
|
+
await handleSessionIdle(sessionID);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default RalphWiggum;
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
6
|
+
|
|
7
|
+
const TASKS_FILE = ".opencode/.background-tasks.json";
|
|
8
|
+
|
|
9
|
+
interface BackgroundTask {
|
|
10
|
+
taskId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
parentSessionId: string; // Track parent for debugging
|
|
13
|
+
agent: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
started: number;
|
|
16
|
+
status: "running" | "completed" | "cancelled";
|
|
17
|
+
// Beads integration
|
|
18
|
+
beadId?: string;
|
|
19
|
+
autoCloseBead?: boolean; // Only allowed for safe agents (explore, scout)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TasksStore {
|
|
23
|
+
tasks: Record<string, BackgroundTask>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadTasks(): Promise<TasksStore> {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(TASKS_FILE, "utf-8");
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
} catch {
|
|
31
|
+
return { tasks: {} };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function saveTasks(store: TasksStore): Promise<void> {
|
|
36
|
+
await fs.mkdir(path.dirname(TASKS_FILE), { recursive: true });
|
|
37
|
+
await fs.writeFile(TASKS_FILE, JSON.stringify(store, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createClient() {
|
|
41
|
+
return createOpencodeClient({ baseUrl: "http://localhost:4096" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the bd binary path dynamically
|
|
46
|
+
*/
|
|
47
|
+
function findBdPath(): string {
|
|
48
|
+
try {
|
|
49
|
+
// Try to find bd in PATH using shell
|
|
50
|
+
const result = execSync("which bd || command -v bd", {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
shell: "/bin/sh",
|
|
54
|
+
}).trim();
|
|
55
|
+
if (result) return result;
|
|
56
|
+
} catch {
|
|
57
|
+
// Fallback to common locations
|
|
58
|
+
const commonPaths = [
|
|
59
|
+
`${process.env.HOME}/.local/bin/bd`,
|
|
60
|
+
`${process.env.HOME}/.bun/bin/bd`,
|
|
61
|
+
"/usr/local/bin/bd",
|
|
62
|
+
"/opt/homebrew/bin/bd",
|
|
63
|
+
];
|
|
64
|
+
for (const p of commonPaths) {
|
|
65
|
+
try {
|
|
66
|
+
execSync(`test -x "${p}"`, { timeout: 1000 });
|
|
67
|
+
return p;
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Last resort - assume it's in PATH
|
|
74
|
+
return "bd";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Cache the bd path
|
|
78
|
+
let bdPath: string | null = null;
|
|
79
|
+
function getBdPath(): string {
|
|
80
|
+
if (!bdPath) {
|
|
81
|
+
bdPath = findBdPath();
|
|
82
|
+
}
|
|
83
|
+
return bdPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Helper to run beads CLI commands
|
|
88
|
+
*/
|
|
89
|
+
async function runBeadsCommand(
|
|
90
|
+
args: string[],
|
|
91
|
+
): Promise<{ success: boolean; output: string }> {
|
|
92
|
+
try {
|
|
93
|
+
// Quote arguments that contain spaces
|
|
94
|
+
const quotedArgs = args.map((arg) =>
|
|
95
|
+
arg.includes(" ") ? `"${arg}"` : arg,
|
|
96
|
+
);
|
|
97
|
+
// Use dynamically detected bd path with shell for proper PATH resolution
|
|
98
|
+
const output = execSync(`${getBdPath()} ${quotedArgs.join(" ")}`, {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
timeout: 30000,
|
|
101
|
+
shell: "/bin/sh",
|
|
102
|
+
env: { ...process.env },
|
|
103
|
+
});
|
|
104
|
+
return { success: true, output };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const error = e as { stderr?: string; message?: string };
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
output: error.stderr || error.message || String(e),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Allowed agents for background delegation
|
|
115
|
+
// - Subagents: explore, scout, review, planner, vision, looker (stateless workers)
|
|
116
|
+
// - Primary: rush (autonomous execution)
|
|
117
|
+
// - NOT allowed: build (build is the orchestrator that uses this tool)
|
|
118
|
+
const ALLOWED_AGENTS = [
|
|
119
|
+
"explore",
|
|
120
|
+
"scout",
|
|
121
|
+
"review",
|
|
122
|
+
"planner",
|
|
123
|
+
"vision",
|
|
124
|
+
"looker",
|
|
125
|
+
"rush",
|
|
126
|
+
] as const;
|
|
127
|
+
type AllowedAgent = (typeof ALLOWED_AGENTS)[number];
|
|
128
|
+
|
|
129
|
+
// Agents safe for autoCloseBead (pure research, no side effects)
|
|
130
|
+
// These only return information, don't make changes that need verification
|
|
131
|
+
const SAFE_AUTOCLOSE_AGENTS: readonly string[] = [
|
|
132
|
+
"explore",
|
|
133
|
+
"scout",
|
|
134
|
+
"looker",
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start a background subagent task.
|
|
139
|
+
* Creates a child session that runs independently.
|
|
140
|
+
*/
|
|
141
|
+
export const start = tool({
|
|
142
|
+
description:
|
|
143
|
+
"Start a background subagent task. Returns a task_id to collect results later. Use for parallel research/exploration or executing beads subtasks. NOTE: Cannot delegate to 'build' agent (build is the orchestrator).",
|
|
144
|
+
args: {
|
|
145
|
+
agent: tool.schema
|
|
146
|
+
.string()
|
|
147
|
+
.describe(
|
|
148
|
+
"Agent type: explore, scout, review, planner, vision, looker, rush (NOT build - build is the orchestrator)",
|
|
149
|
+
),
|
|
150
|
+
prompt: tool.schema.string().describe("Task prompt for the agent"),
|
|
151
|
+
title: tool.schema
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Optional task title for identification"),
|
|
155
|
+
beadId: tool.schema
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Bead ID to associate with this task (e.g., bd-abc123)"),
|
|
159
|
+
autoCloseBead: tool.schema
|
|
160
|
+
.boolean()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe(
|
|
163
|
+
"Auto-close bead on completion. Only allowed for safe agents (explore, scout). Blocked for rush/review/planner/vision/looker.",
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
execute: async (args, context) => {
|
|
167
|
+
// Validate agent type - build cannot delegate to itself
|
|
168
|
+
if (args.agent === "build") {
|
|
169
|
+
return JSON.stringify({
|
|
170
|
+
error:
|
|
171
|
+
"Cannot delegate to 'build' agent. Build is the orchestrator that uses this tool. Use subagents (explore, scout, review, planner, vision, looker) or rush instead.",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!ALLOWED_AGENTS.includes(args.agent as AllowedAgent)) {
|
|
176
|
+
return JSON.stringify({
|
|
177
|
+
error: `Invalid agent type: ${args.agent}. Allowed: ${ALLOWED_AGENTS.join(", ")}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate autoCloseBead - only allowed for safe agents
|
|
182
|
+
if (args.autoCloseBead && !SAFE_AUTOCLOSE_AGENTS.includes(args.agent)) {
|
|
183
|
+
return JSON.stringify({
|
|
184
|
+
error: `autoCloseBead not allowed for '${args.agent}' agent. Only safe for: ${SAFE_AUTOCLOSE_AGENTS.join(", ")}. Build agent must verify output from ${args.agent} before closing beads.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const client = createClient();
|
|
189
|
+
const taskId = `bg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
190
|
+
const title = args.title || `bg-${args.agent}-${Date.now()}`;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Create child session linked to parent (build agent's session)
|
|
194
|
+
// This enables context inheritance from the main session
|
|
195
|
+
const session = await client.session.create({
|
|
196
|
+
body: {
|
|
197
|
+
title,
|
|
198
|
+
parentID: context.sessionID, // Link to parent session for context inheritance
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!session.data?.id) {
|
|
203
|
+
return JSON.stringify({ error: "Failed to create session" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fire the prompt (this returns immediately, session runs async)
|
|
207
|
+
// Use the agent field and AgentPartInput to properly route to the specified agent
|
|
208
|
+
await client.session.prompt({
|
|
209
|
+
path: { id: session.data.id },
|
|
210
|
+
body: {
|
|
211
|
+
agent: args.agent, // Specify agent type directly in body
|
|
212
|
+
parts: [
|
|
213
|
+
{
|
|
214
|
+
type: "agent" as const,
|
|
215
|
+
name: args.agent, // AgentPartInput triggers agent routing
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "text" as const,
|
|
219
|
+
text: args.prompt,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Persist task info
|
|
226
|
+
const store = await loadTasks();
|
|
227
|
+
store.tasks[taskId] = {
|
|
228
|
+
taskId,
|
|
229
|
+
sessionId: session.data.id,
|
|
230
|
+
parentSessionId: context.sessionID,
|
|
231
|
+
agent: args.agent,
|
|
232
|
+
prompt: args.prompt,
|
|
233
|
+
started: Date.now(),
|
|
234
|
+
status: "running",
|
|
235
|
+
beadId: args.beadId,
|
|
236
|
+
autoCloseBead: args.autoCloseBead,
|
|
237
|
+
};
|
|
238
|
+
await saveTasks(store);
|
|
239
|
+
|
|
240
|
+
// If beadId provided, mark it as in_progress
|
|
241
|
+
if (args.beadId) {
|
|
242
|
+
await runBeadsCommand([
|
|
243
|
+
"update",
|
|
244
|
+
args.beadId,
|
|
245
|
+
"--status",
|
|
246
|
+
"in_progress",
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return JSON.stringify({
|
|
251
|
+
taskId,
|
|
252
|
+
sessionId: session.data.id,
|
|
253
|
+
agent: args.agent,
|
|
254
|
+
beadId: args.beadId,
|
|
255
|
+
status: "started",
|
|
256
|
+
message: `Background task started. Use background_output(taskId="${taskId}") to get results.`,
|
|
257
|
+
});
|
|
258
|
+
} catch (e) {
|
|
259
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
260
|
+
return JSON.stringify({
|
|
261
|
+
error: `Failed to start background task: ${error}`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get output from a background task.
|
|
269
|
+
* Retrieves the last assistant message from the child session.
|
|
270
|
+
*/
|
|
271
|
+
export const output = tool({
|
|
272
|
+
description:
|
|
273
|
+
"Get output from a background task. Returns the agent's response or 'still running' if not complete.",
|
|
274
|
+
args: {
|
|
275
|
+
taskId: tool.schema.string().describe("Task ID from background_start"),
|
|
276
|
+
},
|
|
277
|
+
execute: async (args) => {
|
|
278
|
+
const client = createClient();
|
|
279
|
+
const store = await loadTasks();
|
|
280
|
+
const task = store.tasks[args.taskId];
|
|
281
|
+
|
|
282
|
+
if (!task) {
|
|
283
|
+
return JSON.stringify({
|
|
284
|
+
error: `Task not found: ${args.taskId}`,
|
|
285
|
+
availableTasks: Object.keys(store.tasks),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const messages = await client.session.messages({
|
|
291
|
+
path: { id: task.sessionId },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!messages.data?.length) {
|
|
295
|
+
return JSON.stringify({
|
|
296
|
+
taskId: args.taskId,
|
|
297
|
+
status: "running",
|
|
298
|
+
message: "No messages yet - task still initializing",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Find last assistant message
|
|
303
|
+
const assistantMessages = messages.data.filter(
|
|
304
|
+
(m) => m.info?.role === "assistant",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (!assistantMessages.length) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
taskId: args.taskId,
|
|
310
|
+
status: "running",
|
|
311
|
+
message: "Task running - no response yet",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
316
|
+
const textParts = lastMessage.parts
|
|
317
|
+
?.filter((p) => p.type === "text")
|
|
318
|
+
.map((p) => p.text)
|
|
319
|
+
.join("\n");
|
|
320
|
+
|
|
321
|
+
// Update status
|
|
322
|
+
task.status = "completed";
|
|
323
|
+
await saveTasks(store);
|
|
324
|
+
|
|
325
|
+
// Build result object
|
|
326
|
+
const result: Record<string, unknown> = {
|
|
327
|
+
taskId: args.taskId,
|
|
328
|
+
agent: task.agent,
|
|
329
|
+
status: "completed",
|
|
330
|
+
output: textParts || "(empty response)",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Handle bead closing
|
|
334
|
+
if (task.beadId) {
|
|
335
|
+
result.beadId = task.beadId;
|
|
336
|
+
|
|
337
|
+
// Auto-close for safe agents (explore, scout)
|
|
338
|
+
if (task.autoCloseBead && SAFE_AUTOCLOSE_AGENTS.includes(task.agent)) {
|
|
339
|
+
const closeResult = await runBeadsCommand([
|
|
340
|
+
"close",
|
|
341
|
+
task.beadId,
|
|
342
|
+
"--reason",
|
|
343
|
+
`Auto-closed: ${task.agent} task completed (${task.taskId})`,
|
|
344
|
+
]);
|
|
345
|
+
result.beadClosed = closeResult.success;
|
|
346
|
+
if (!closeResult.success) {
|
|
347
|
+
result.beadCloseError = closeResult.output;
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// For unsafe agents or when autoClose not requested, remind to verify
|
|
351
|
+
result.beadAction = `VERIFY output, then run: bd close ${task.beadId} --reason "..." `;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return JSON.stringify(result);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
358
|
+
return JSON.stringify({
|
|
359
|
+
taskId: args.taskId,
|
|
360
|
+
error: `Failed to get output: ${error}`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Cancel background tasks.
|
|
368
|
+
* Aborts running sessions and cleans up task records.
|
|
369
|
+
*/
|
|
370
|
+
export const cancel = tool({
|
|
371
|
+
description:
|
|
372
|
+
"Cancel background tasks. Use all=true to cancel all, or specify taskId.",
|
|
373
|
+
args: {
|
|
374
|
+
all: tool.schema
|
|
375
|
+
.boolean()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("Cancel all background tasks"),
|
|
378
|
+
taskId: tool.schema
|
|
379
|
+
.string()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("Specific task ID to cancel"),
|
|
382
|
+
},
|
|
383
|
+
execute: async (args) => {
|
|
384
|
+
const client = createClient();
|
|
385
|
+
const store = await loadTasks();
|
|
386
|
+
const cancelled: string[] = [];
|
|
387
|
+
const errors: string[] = [];
|
|
388
|
+
|
|
389
|
+
const tasksToCancel = args.all
|
|
390
|
+
? Object.values(store.tasks).filter((t) => t.status === "running")
|
|
391
|
+
: args.taskId && store.tasks[args.taskId]
|
|
392
|
+
? [store.tasks[args.taskId]]
|
|
393
|
+
: [];
|
|
394
|
+
|
|
395
|
+
if (!tasksToCancel.length) {
|
|
396
|
+
return JSON.stringify({
|
|
397
|
+
message: args.all
|
|
398
|
+
? "No running tasks to cancel"
|
|
399
|
+
: `Task not found: ${args.taskId}`,
|
|
400
|
+
activeTasks: Object.values(store.tasks)
|
|
401
|
+
.filter((t) => t.status === "running")
|
|
402
|
+
.map((t) => t.taskId),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const task of tasksToCancel) {
|
|
407
|
+
try {
|
|
408
|
+
await client.session.abort({ path: { id: task.sessionId } });
|
|
409
|
+
task.status = "cancelled";
|
|
410
|
+
cancelled.push(task.taskId);
|
|
411
|
+
} catch (e) {
|
|
412
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
413
|
+
errors.push(`${task.taskId}: ${error}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await saveTasks(store);
|
|
418
|
+
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
cancelled,
|
|
421
|
+
errors: errors.length ? errors : undefined,
|
|
422
|
+
remaining: Object.values(store.tasks)
|
|
423
|
+
.filter((t) => t.status === "running")
|
|
424
|
+
.map((t) => t.taskId),
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* List all background tasks with their status.
|
|
431
|
+
*/
|
|
432
|
+
export const list = tool({
|
|
433
|
+
description: "List all background tasks with their status.",
|
|
434
|
+
args: {
|
|
435
|
+
status: tool.schema
|
|
436
|
+
.enum(["running", "completed", "cancelled", "all"])
|
|
437
|
+
.optional()
|
|
438
|
+
.default("all")
|
|
439
|
+
.describe("Filter by status"),
|
|
440
|
+
},
|
|
441
|
+
execute: async (args) => {
|
|
442
|
+
const store = await loadTasks();
|
|
443
|
+
const tasks = Object.values(store.tasks);
|
|
444
|
+
|
|
445
|
+
const filtered =
|
|
446
|
+
args.status === "all"
|
|
447
|
+
? tasks
|
|
448
|
+
: tasks.filter((t) => t.status === args.status);
|
|
449
|
+
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
total: filtered.length,
|
|
452
|
+
tasks: filtered.map((t) => ({
|
|
453
|
+
taskId: t.taskId,
|
|
454
|
+
agent: t.agent,
|
|
455
|
+
status: t.status,
|
|
456
|
+
started: new Date(t.started).toISOString(),
|
|
457
|
+
prompt: t.prompt.slice(0, 100) + (t.prompt.length > 100 ? "..." : ""),
|
|
458
|
+
})),
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
});
|