bobs-workshop 0.3.3 → 3.1.1

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 (200) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +199 -210
  3. package/bin/bobs-workshop.js +109 -0
  4. package/config/agents.json +27 -0
  5. package/dist/plugins/bobs-workshop.js +34 -0
  6. package/dist/tools/background-agent/cancel.d.ts +3 -0
  7. package/dist/tools/background-agent/cancel.d.ts.map +1 -0
  8. package/dist/tools/background-agent/cancel.js +52 -0
  9. package/dist/tools/background-agent/concurrency.d.ts +15 -0
  10. package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
  11. package/dist/tools/background-agent/concurrency.js +61 -0
  12. package/dist/tools/background-agent/index.d.ts +8 -0
  13. package/dist/tools/background-agent/index.d.ts.map +1 -0
  14. package/dist/tools/background-agent/index.js +7 -0
  15. package/dist/tools/background-agent/launch.d.ts +6 -0
  16. package/dist/tools/background-agent/launch.d.ts.map +1 -0
  17. package/dist/tools/background-agent/launch.js +33 -0
  18. package/dist/tools/background-agent/list.d.ts +7 -0
  19. package/dist/tools/background-agent/list.d.ts.map +1 -0
  20. package/dist/tools/background-agent/list.js +40 -0
  21. package/dist/tools/background-agent/manager.d.ts +29 -0
  22. package/dist/tools/background-agent/manager.d.ts.map +1 -0
  23. package/dist/tools/background-agent/manager.js +388 -0
  24. package/dist/tools/background-agent/output.d.ts +3 -0
  25. package/dist/tools/background-agent/output.d.ts.map +1 -0
  26. package/dist/tools/background-agent/output.js +41 -0
  27. package/dist/tools/background-agent/types.d.ts +46 -0
  28. package/dist/tools/background-agent/types.d.ts.map +1 -0
  29. package/dist/tools/background-agent/types.js +1 -0
  30. package/dist/tools/index.d.ts +9 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +8 -0
  33. package/dist/tools/manual/index.d.ts +3 -0
  34. package/dist/tools/manual/index.d.ts.map +1 -0
  35. package/dist/tools/manual/index.js +2 -0
  36. package/dist/tools/manual/manual-update.d.ts +4 -0
  37. package/dist/tools/manual/manual-update.d.ts.map +1 -0
  38. package/dist/tools/manual/manual-update.js +190 -0
  39. package/dist/tools/manual/verify-manual.d.ts +4 -0
  40. package/dist/tools/manual/verify-manual.d.ts.map +1 -0
  41. package/dist/tools/manual/verify-manual.js +51 -0
  42. package/package.json +34 -66
  43. package/postinstall.js +193 -0
  44. package/src/agents/alice.md +466 -0
  45. package/src/agents/bob-rev.md +493 -0
  46. package/src/agents/bob-send.md +277 -0
  47. package/src/agents/bob.md +442 -0
  48. package/src/agents/trace.md +451 -0
  49. package/src/plugins/bobs-workshop.ts +45 -0
  50. package/src/skills/api-patterns/SKILL.md +376 -0
  51. package/src/skills/architecture/SKILL.md +271 -0
  52. package/src/skills/bobs-workshop/performance/icon.svg +3 -0
  53. package/src/skills/brainstorming/SKILL.md +210 -0
  54. package/src/skills/clean-code/SKILL.md +151 -0
  55. package/src/skills/code-review-checklist/SKILL.md +220 -0
  56. package/src/skills/database-design/SKILL.md +271 -0
  57. package/src/skills/exploration/SKILL.md +257 -0
  58. package/src/skills/frontend-ui-ux/SKILL.md +78 -0
  59. package/src/skills/git-master/SKILL.md +1105 -0
  60. package/src/skills/performance/SKILL.md +144 -0
  61. package/src/skills/performance/icon.svg +3 -0
  62. package/src/skills/plan-writing/SKILL.md +225 -0
  63. package/src/skills/security/SKILL.md +410 -0
  64. package/src/skills/simplification/SKILL.md +238 -0
  65. package/src/skills/systematic-debugging/SKILL.md +175 -0
  66. package/src/skills/testing-patterns/SKILL.md +305 -0
  67. package/src/skills/verification/SKILL.md +286 -0
  68. package/src/tools/background-agent/cancel.ts +67 -0
  69. package/src/tools/background-agent/concurrency.ts +71 -0
  70. package/src/tools/background-agent/index.ts +7 -0
  71. package/src/tools/background-agent/launch.ts +39 -0
  72. package/src/tools/background-agent/list.ts +50 -0
  73. package/src/tools/background-agent/manager.ts +466 -0
  74. package/src/tools/background-agent/output.ts +57 -0
  75. package/src/tools/background-agent/types.ts +55 -0
  76. package/src/tools/index.ts +8 -0
  77. package/src/tools/manual/index.ts +2 -0
  78. package/src/tools/manual/manual-update.ts +197 -0
  79. package/src/tools/manual/verify-manual.ts +60 -0
  80. package/uninstall.js +64 -0
  81. package/Claude.md +0 -162
  82. package/bin/bobs-mcp-server.js +0 -11
  83. package/bin/bobs-mcp.js +0 -130
  84. package/dist/api/taskLogger.js +0 -106
  85. package/dist/api/taskLogger.js.map +0 -1
  86. package/dist/cli/checker.js +0 -401
  87. package/dist/cli/checker.js.map +0 -1
  88. package/dist/cli/cleanup.js +0 -131
  89. package/dist/cli/cleanup.js.map +0 -1
  90. package/dist/cli/debug.js +0 -157
  91. package/dist/cli/debug.js.map +0 -1
  92. package/dist/cli/health.js +0 -97
  93. package/dist/cli/health.js.map +0 -1
  94. package/dist/cli/setup.js +0 -81
  95. package/dist/cli/setup.js.map +0 -1
  96. package/dist/cli/workshop.js +0 -42
  97. package/dist/cli/workshop.js.map +0 -1
  98. package/dist/dashboard/server.js +0 -1203
  99. package/dist/dashboard/server.js.map +0 -1
  100. package/dist/index.js +0 -960
  101. package/dist/index.js.map +0 -1
  102. package/dist/prompts/architect.js +0 -221
  103. package/dist/prompts/architect.js.map +0 -1
  104. package/dist/prompts/debugger.js +0 -257
  105. package/dist/prompts/debugger.js.map +0 -1
  106. package/dist/prompts/engineer.js +0 -249
  107. package/dist/prompts/engineer.js.map +0 -1
  108. package/dist/prompts/orchestrator.js +0 -304
  109. package/dist/prompts/orchestrator.js.map +0 -1
  110. package/dist/prompts/reviewer.js +0 -289
  111. package/dist/prompts/reviewer.js.map +0 -1
  112. package/dist/services/activitySummarizer.js +0 -388
  113. package/dist/services/activitySummarizer.js.map +0 -1
  114. package/dist/services/changeValidator.js +0 -396
  115. package/dist/services/changeValidator.js.map +0 -1
  116. package/dist/services/claudeOrchestrator.js +0 -343
  117. package/dist/services/claudeOrchestrator.js.map +0 -1
  118. package/dist/services/fileMonitor.js +0 -250
  119. package/dist/services/fileMonitor.js.map +0 -1
  120. package/dist/services/implementationSummarizer.js +0 -306
  121. package/dist/services/implementationSummarizer.js.map +0 -1
  122. package/dist/services/liveMonitor.js +0 -315
  123. package/dist/services/liveMonitor.js.map +0 -1
  124. package/dist/services/mcpAuditLogger.js +0 -104
  125. package/dist/services/mcpAuditLogger.js.map +0 -1
  126. package/dist/services/mcpLogger.js +0 -223
  127. package/dist/services/mcpLogger.js.map +0 -1
  128. package/dist/services/tmuxManager.js +0 -541
  129. package/dist/services/tmuxManager.js.map +0 -1
  130. package/dist/tools/approvalTools.js +0 -244
  131. package/dist/tools/approvalTools.js.map +0 -1
  132. package/dist/tools/autoDebugger.js +0 -147
  133. package/dist/tools/autoDebugger.js.map +0 -1
  134. package/dist/tools/cleanupService.js +0 -221
  135. package/dist/tools/cleanupService.js.map +0 -1
  136. package/dist/tools/dashboardTools.js +0 -342
  137. package/dist/tools/dashboardTools.js.map +0 -1
  138. package/dist/tools/developmentNudges.js +0 -336
  139. package/dist/tools/developmentNudges.js.map +0 -1
  140. package/dist/tools/gitTools.js +0 -741
  141. package/dist/tools/gitTools.js.map +0 -1
  142. package/dist/tools/orchestratorTools.js +0 -832
  143. package/dist/tools/orchestratorTools.js.map +0 -1
  144. package/dist/tools/searchCache.js +0 -64
  145. package/dist/tools/searchCache.js.map +0 -1
  146. package/dist/tools/searchTools.js +0 -1107
  147. package/dist/tools/searchTools.js.map +0 -1
  148. package/dist/tools/semgrep-patterns.js +0 -296
  149. package/dist/tools/semgrep-patterns.js.map +0 -1
  150. package/dist/tools/specTools.js +0 -332
  151. package/dist/tools/specTools.js.map +0 -1
  152. package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
  153. package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
  154. package/dist/tools/structural/cache.js +0 -226
  155. package/dist/tools/structural/cache.js.map +0 -1
  156. package/dist/tools/structural/engines/python/index.js +0 -118
  157. package/dist/tools/structural/engines/python/index.js.map +0 -1
  158. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
  159. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
  160. package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
  161. package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
  162. package/dist/tools/structural/engines/typescript/index.js +0 -381
  163. package/dist/tools/structural/engines/typescript/index.js.map +0 -1
  164. package/dist/tools/structural/engines/typescript/utils.js +0 -279
  165. package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
  166. package/dist/tools/structural/index.js +0 -248
  167. package/dist/tools/structural/index.js.map +0 -1
  168. package/dist/tools/structural/types.js +0 -18
  169. package/dist/tools/structural/types.js.map +0 -1
  170. package/dist/tools/tmuxTools.js +0 -100
  171. package/dist/tools/tmuxTools.js.map +0 -1
  172. package/dist/tools/workRecorder.js +0 -215
  173. package/dist/tools/workRecorder.js.map +0 -1
  174. package/dist/tools/worktreeTools.js +0 -705
  175. package/dist/tools/worktreeTools.js.map +0 -1
  176. package/dist/utils/__tests__/integration.test.js +0 -57
  177. package/dist/utils/__tests__/integration.test.js.map +0 -1
  178. package/dist/utils/__tests__/serverDetection.test.js +0 -151
  179. package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
  180. package/dist/utils/errorHandling.js +0 -336
  181. package/dist/utils/errorHandling.js.map +0 -1
  182. package/dist/utils/processManager.js +0 -172
  183. package/dist/utils/processManager.js.map +0 -1
  184. package/dist/utils/reliability.js +0 -263
  185. package/dist/utils/reliability.js.map +0 -1
  186. package/dist/utils/responseFormatter.js +0 -250
  187. package/dist/utils/responseFormatter.js.map +0 -1
  188. package/dist/utils/serverDetection.js +0 -133
  189. package/dist/utils/serverDetection.js.map +0 -1
  190. package/dist/utils/specMigration.js +0 -105
  191. package/dist/utils/specMigration.js.map +0 -1
  192. package/dist/validation/schemas.js +0 -299
  193. package/dist/validation/schemas.js.map +0 -1
  194. package/public/.well-known/mcp/manifest.json +0 -473
  195. package/public/index.html +0 -3157
  196. package/public/index.html.backup +0 -2805
  197. package/public/index.html.backup2 +0 -1292
  198. package/scripts/cleanup-system-logs.ts +0 -121
  199. package/scripts/init-workspace.js +0 -63
  200. package/scripts/install-search-tools.js +0 -116
@@ -0,0 +1,466 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ import type {
3
+ BackgroundTask,
4
+ LaunchInput,
5
+ ResumeInput,
6
+ BackgroundTaskStatus,
7
+ } from "./types";
8
+ import { ConcurrencyManager } from "./concurrency";
9
+
10
+ const TASK_TTL_MS = 30 * 60 * 1000; // 30 minutes
11
+ const MIN_STABILITY_TIME_MS = 10 * 1000; // 10 seconds before stability detection
12
+
13
+ type OpencodeClient = PluginInput["client"];
14
+
15
+ export class BackgroundManager {
16
+ private tasks: Map<string, BackgroundTask>;
17
+ private client: OpencodeClient;
18
+ private directory: string;
19
+ private pollingInterval?: ReturnType<typeof setInterval>;
20
+ private concurrencyManager: ConcurrencyManager;
21
+ private shutdownTriggered = false;
22
+ private static instance: BackgroundManager | null = null;
23
+
24
+ static getInstance(ctx: PluginInput): BackgroundManager {
25
+ if (!BackgroundManager.instance) {
26
+ BackgroundManager.instance = new BackgroundManager(ctx);
27
+ }
28
+ return BackgroundManager.instance;
29
+ }
30
+
31
+ private constructor(ctx: PluginInput) {
32
+ this.tasks = new Map();
33
+ this.client = ctx.client;
34
+ this.directory = ctx.directory;
35
+ this.concurrencyManager = new ConcurrencyManager();
36
+ }
37
+
38
+ async launch(input: LaunchInput): Promise<BackgroundTask> {
39
+ console.log("[background-agent] Launching task:", {
40
+ agent: input.agent,
41
+ model: input.model,
42
+ description: input.description,
43
+ });
44
+
45
+ if (!input.agent || input.agent.trim() === "") {
46
+ throw new Error("Agent parameter is required");
47
+ }
48
+
49
+ const task: BackgroundTask = {
50
+ id: `bg_${crypto.randomUUID().slice(0, 8)}`,
51
+ status: "pending",
52
+ queuedAt: new Date(),
53
+ description: input.description,
54
+ prompt: input.prompt,
55
+ agent: input.agent,
56
+ parentSessionID: input.parentSessionID,
57
+ model: input.model,
58
+ skills: input.skills,
59
+ };
60
+
61
+ this.tasks.set(task.id, task);
62
+
63
+ // Trigger processing
64
+ this.startTask(task, input).catch((error) => {
65
+ console.error("[background-agent] Error starting task:", error);
66
+ task.status = "error";
67
+ task.error = error instanceof Error ? error.message : String(error);
68
+ task.completedAt = new Date();
69
+ });
70
+
71
+ return task;
72
+ }
73
+
74
+ private async startTask(task: BackgroundTask, input: LaunchInput): Promise<void> {
75
+ const concurrencyKey = input.model || input.agent;
76
+ await this.concurrencyManager.acquire(concurrencyKey);
77
+
78
+ try {
79
+ const createResult = await this.client.session.create({
80
+ body: {
81
+ parentID: input.parentSessionID,
82
+ title: `Background: ${input.description}`,
83
+ },
84
+ query: {
85
+ directory: this.directory,
86
+ },
87
+ });
88
+
89
+ if (createResult.error) {
90
+ throw new Error(`Failed to create background session: ${createResult.error}`);
91
+ }
92
+
93
+ const sessionID = (createResult.data as { id: string }).id;
94
+
95
+ task.status = "running";
96
+ task.startedAt = new Date();
97
+ task.sessionID = sessionID;
98
+ task.progress = {
99
+ toolCalls: 0,
100
+ lastUpdate: new Date(),
101
+ };
102
+
103
+ this.startPolling();
104
+
105
+ console.log("[background-agent] Launching task:", {
106
+ taskId: task.id,
107
+ sessionID,
108
+ agent: input.agent,
109
+ });
110
+
111
+ // Build skill content
112
+ let skillContent = input.skillContent || "";
113
+ if (input.skills && input.skills.length > 0) {
114
+ const { join } = await import("node:path");
115
+ const { readFileSync, existsSync } = await import("node:fs");
116
+
117
+ for (const skillName of input.skills) {
118
+ const skillPath = join(this.directory, ".opencode", "skill", "bobs-workshop", skillName, "SKILL.md");
119
+ if (existsSync(skillPath)) {
120
+ let skillFile = readFileSync(skillPath, "utf8");
121
+ // Sanitize content to prevent JSON parsing issues with kimi model
122
+ skillFile = skillFile
123
+ .replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F]/g, '') // Remove control chars except \n, \r, \t
124
+ .replace(/\u2028/g, '\n') // Replace line separator with newline
125
+ .replace(/\u2029/g, '\n'); // Replace paragraph separator with newline
126
+ skillContent += `\n\n---\n## Skill: ${skillName}\n\n${skillFile}`;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Add MANUAL content if provided
132
+ if (input.manual_path) {
133
+ const { existsSync, readFileSync } = await import("node:fs");
134
+ if (existsSync(input.manual_path)) {
135
+ let manualContent = readFileSync(input.manual_path, "utf8");
136
+ // Sanitize content to prevent JSON parsing issues with kimi model
137
+ // Remove control characters and ensure proper escaping
138
+ manualContent = manualContent
139
+ .replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F]/g, '') // Remove control chars except \n, \r, \t
140
+ .replace(/\u2028/g, '\n') // Replace line separator with newline
141
+ .replace(/\u2029/g, '\n'); // Replace paragraph separator with newline
142
+ skillContent = skillContent
143
+ ? `${skillContent}\n\n## MANUAL Context:\n${manualContent}`
144
+ : manualContent;
145
+ }
146
+ }
147
+
148
+ // Fire-and-forget prompt
149
+ this.client.session.prompt({
150
+ path: { id: sessionID },
151
+ body: {
152
+ agent: input.agent,
153
+ ...(input.model ? { model: input.model } : {}),
154
+ system: skillContent || undefined,
155
+ tools: {
156
+ task: false,
157
+ delegate_task: false,
158
+ },
159
+ parts: [{ type: "text", text: input.prompt }],
160
+ },
161
+ } as any).catch((error) => {
162
+ console.error("[background-agent] Prompt error:", error);
163
+ const existingTask = this.tasks.get(task.id);
164
+ if (existingTask && existingTask.status === "running") {
165
+ existingTask.status = "error";
166
+ existingTask.error = error instanceof Error ? error.message : String(error);
167
+ existingTask.completedAt = new Date();
168
+ if (concurrencyKey) {
169
+ this.concurrencyManager.release(concurrencyKey);
170
+ }
171
+ }
172
+ });
173
+ } catch (error) {
174
+ this.concurrencyManager.release(concurrencyKey);
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ getTask(id: string): BackgroundTask | undefined {
180
+ return this.tasks.get(id);
181
+ }
182
+
183
+ getAllTasks(): BackgroundTask[] {
184
+ return Array.from(this.tasks.values());
185
+ }
186
+
187
+ getRunningTasks(): BackgroundTask[] {
188
+ return Array.from(this.tasks.values()).filter((t) => t.status === "running");
189
+ }
190
+
191
+ handleEvent(event: { type: string; properties?: Record<string, unknown> }): void {
192
+ if (event.type === "session.idle") {
193
+ const sessionID = event.properties?.sessionID as string | undefined;
194
+ if (!sessionID) return;
195
+
196
+ const task = Array.from(this.tasks.values()).find((t) => t.sessionID === sessionID);
197
+ if (!task || task.status !== "running") return;
198
+
199
+ const startedAt = task.startedAt;
200
+ if (!startedAt) return;
201
+
202
+ const elapsedMs = Date.now() - startedAt.getTime();
203
+ const MIN_IDLE_TIME_MS = 5000; // 5 seconds
204
+
205
+ if (elapsedMs < MIN_IDLE_TIME_MS) {
206
+ console.log("[background-agent] Ignoring early session.idle");
207
+ return;
208
+ }
209
+
210
+ this.tryCompleteTask(task, "session.idle");
211
+ }
212
+
213
+ if (event.type === "session.deleted") {
214
+ const info = event.properties?.info as { id: string } | undefined;
215
+ if (!info?.id) return;
216
+
217
+ const task = Array.from(this.tasks.values()).find((t) => t.sessionID === info.id);
218
+ if (!task) return;
219
+
220
+ if (task.status === "running") {
221
+ task.status = "cancelled";
222
+ task.completedAt = new Date();
223
+ task.error = "Session deleted";
224
+ }
225
+
226
+ this.tasks.delete(task.id);
227
+ }
228
+ }
229
+
230
+ private async tryCompleteTask(task: BackgroundTask, source: string): Promise<boolean> {
231
+ if (task.status !== "running") {
232
+ return false;
233
+ }
234
+
235
+ // Validate session has output before completing
236
+ if (!task.sessionID) return false;
237
+
238
+ const hasOutput = await this.validateSessionHasOutput(task.sessionID);
239
+ if (!hasOutput) {
240
+ console.log("[background-agent] No valid output yet, waiting");
241
+ return false;
242
+ }
243
+
244
+ task.status = "completed";
245
+ task.completedAt = new Date();
246
+
247
+ // Release concurrency
248
+ if (task.model) {
249
+ this.concurrencyManager.release(task.model);
250
+ } else {
251
+ this.concurrencyManager.release(task.agent);
252
+ }
253
+
254
+ console.log(`[background-agent] Task completed via ${source}:`, task.id);
255
+
256
+ // Auto-remove after 5 minutes
257
+ setTimeout(() => {
258
+ this.tasks.delete(task.id);
259
+ console.log("[background-agent] Removed completed task from memory:", task.id);
260
+ }, 5 * 60 * 1000);
261
+
262
+ return true;
263
+ }
264
+
265
+ private async validateSessionHasOutput(sessionID: string): Promise<boolean> {
266
+ try {
267
+ const response = await this.client.session.messages({
268
+ path: { id: sessionID },
269
+ });
270
+
271
+ const messages = (response.data ?? []) as Array<{
272
+ info?: { role?: string };
273
+ parts?: Array<{ type?: string; text?: string; tool?: string }>;
274
+ }>;
275
+
276
+ const hasAssistantOrToolMessage = messages.some(
277
+ (m) => m.info?.role === "assistant" || m.info?.role === "tool"
278
+ );
279
+
280
+ if (!hasAssistantOrToolMessage) return false;
281
+
282
+ const hasContent = messages.some((m) => {
283
+ if (m.info?.role !== "assistant" && m.info?.role !== "tool") return false;
284
+ const parts = m.parts ?? [];
285
+ return parts.some(
286
+ (p: any) =>
287
+ (p.type === "text" && p.text && p.text.trim().length > 0) ||
288
+ (p.type === "reasoning" && p.text && p.text.trim().length > 0) ||
289
+ p.type === "tool" ||
290
+ (p.type === "tool_result" && p.content !== undefined)
291
+ );
292
+ });
293
+
294
+ return hasContent;
295
+ } catch (error) {
296
+ console.error("[background-agent] Error validating session output:", error);
297
+ return true; // Allow completion on error
298
+ }
299
+ }
300
+
301
+ private startPolling(): void {
302
+ if (this.pollingInterval) return;
303
+
304
+ this.pollingInterval = setInterval(() => {
305
+ this.pollRunningTasks();
306
+ }, 2000);
307
+ this.pollingInterval.unref();
308
+ }
309
+
310
+ private stopPolling(): void {
311
+ if (this.pollingInterval) {
312
+ clearInterval(this.pollingInterval);
313
+ this.pollingInterval = undefined;
314
+ }
315
+ }
316
+
317
+ private async pollRunningTasks(): Promise<void> {
318
+ const now = Date.now();
319
+
320
+ // Prune stale tasks
321
+ for (const [taskId, task] of this.tasks.entries()) {
322
+ const timestamp = task.status === "pending"
323
+ ? task.queuedAt?.getTime()
324
+ : task.startedAt?.getTime();
325
+
326
+ if (!timestamp) continue;
327
+
328
+ const age = now - timestamp;
329
+ if (age > TASK_TTL_MS) {
330
+ const errorMessage = task.status === "pending"
331
+ ? "Task timed out while queued (30 minutes)"
332
+ : "Task timed out after 30 minutes";
333
+
334
+ console.log("[background-agent] Pruning stale task:", { taskId, age });
335
+ task.status = "error";
336
+ task.error = errorMessage;
337
+ task.completedAt = new Date();
338
+ this.tasks.delete(taskId);
339
+ }
340
+ }
341
+
342
+ // Check running tasks
343
+ const statusResult = await this.client.session.status();
344
+ const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
345
+
346
+ for (const task of this.tasks.values()) {
347
+ if (task.status !== "running") continue;
348
+
349
+ const sessionID = task.sessionID;
350
+ if (!sessionID) continue;
351
+
352
+ try {
353
+ const sessionStatus = allStatuses[sessionID];
354
+
355
+ if (sessionStatus?.type === "idle") {
356
+ const hasValidOutput = await this.validateSessionHasOutput(sessionID);
357
+ if (!hasValidOutput) {
358
+ continue;
359
+ }
360
+
361
+ // Re-check status after async operation
362
+ if (task.status !== "running") continue;
363
+
364
+ await this.tryCompleteTask(task, "polling (idle status)");
365
+ continue;
366
+ }
367
+
368
+ // Stability detection
369
+ const messagesResult = await this.client.session.messages({
370
+ path: { id: sessionID },
371
+ });
372
+
373
+ if (!messagesResult.error && messagesResult.data) {
374
+ const messages = messagesResult.data as Array<{
375
+ info?: { role?: string };
376
+ parts?: Array<{ type?: string; tool?: string; name?: string; text?: string }>;
377
+ }>;
378
+
379
+ const assistantMsgs = messages.filter((m) => m.info?.role === "assistant");
380
+
381
+ let toolCalls = 0;
382
+ let lastTool: string | undefined;
383
+
384
+ for (const msg of assistantMsgs) {
385
+ const parts = msg.parts ?? [];
386
+ for (const part of parts) {
387
+ if (part.type === "tool_use" || part.tool) {
388
+ toolCalls++;
389
+ lastTool = part.tool || part.name || "unknown";
390
+ }
391
+ }
392
+ }
393
+
394
+ if (!task.progress) {
395
+ task.progress = { toolCalls: 0, lastUpdate: new Date() };
396
+ }
397
+ task.progress.toolCalls = toolCalls;
398
+ task.progress.lastTool = lastTool;
399
+ task.progress.lastUpdate = new Date();
400
+
401
+ const currentMsgCount = messages.length;
402
+ const startedAt = task.startedAt;
403
+ if (!startedAt) continue;
404
+
405
+ const elapsedMs = Date.now() - startedAt.getTime();
406
+
407
+ if (elapsedMs >= MIN_STABILITY_TIME_MS) {
408
+ if (task.lastMsgCount === currentMsgCount) {
409
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
410
+ if ((task.stablePolls ?? 0) >= 3) {
411
+ const recheckStatus = await this.client.session.status();
412
+ const recheckData = (recheckStatus.data ?? {}) as Record<string, { type: string }>;
413
+ const currentStatus = recheckData[sessionID];
414
+
415
+ if (currentStatus?.type !== "idle") {
416
+ task.stablePolls = 0;
417
+ continue;
418
+ }
419
+
420
+ const hasValidOutput = await this.validateSessionHasOutput(sessionID);
421
+ if (!hasValidOutput) {
422
+ continue;
423
+ }
424
+
425
+ if (task.status !== "running") continue;
426
+
427
+ await this.tryCompleteTask(task, "stability detection");
428
+ continue;
429
+ }
430
+ } else {
431
+ task.stablePolls = 0;
432
+ }
433
+ }
434
+ task.lastMsgCount = currentMsgCount;
435
+ }
436
+ } catch (error) {
437
+ console.error("[background-agent] Poll error:", error);
438
+ }
439
+ }
440
+
441
+ // Stop polling if no running tasks
442
+ if (!this.getRunningTasks().length) {
443
+ this.stopPolling();
444
+ }
445
+ }
446
+
447
+ shutdown(): void {
448
+ if (this.shutdownTriggered) return;
449
+ this.shutdownTriggered = true;
450
+ console.log("[background-agent] Shutting down BackgroundManager");
451
+ this.stopPolling();
452
+
453
+ // Release concurrency for all running tasks
454
+ for (const task of this.tasks.values()) {
455
+ if (task.model) {
456
+ this.concurrencyManager.release(task.model);
457
+ } else {
458
+ this.concurrencyManager.release(task.agent);
459
+ }
460
+ }
461
+
462
+ this.concurrencyManager.clear();
463
+ this.tasks.clear();
464
+ console.log("[background-agent] Shutdown complete");
465
+ }
466
+ }
@@ -0,0 +1,57 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import type { PluginInput } from "@opencode-ai/plugin";
3
+ import { BackgroundManager } from "./manager.js";
4
+
5
+ const BackgroundOutputTool: any = tool({
6
+ description: "Collect output from a specific background agent session",
7
+ args: {
8
+ task_id: tool.schema.string().describe("Background task ID to retrieve output from"),
9
+ },
10
+ async execute(args, context) {
11
+ const ctx = context as unknown as PluginInput & { sessionID: string };
12
+ const manager = BackgroundManager.getInstance(ctx);
13
+
14
+ const task = manager.getTask(args.task_id);
15
+
16
+ if (!task) {
17
+ return `❌ Task ${args.task_id} not found`;
18
+ }
19
+
20
+ if (task.status === "pending") {
21
+ return `⏳ Task ${args.task_id} is still queued...`;
22
+ }
23
+
24
+ if (task.status === "running") {
25
+ return `🔄 Task ${args.task_id} still running...`;
26
+ }
27
+
28
+ if (task.status === "error") {
29
+ return `❌ Task ${args.task_id} failed: ${task.error}`;
30
+ }
31
+
32
+ if (!task.sessionID) {
33
+ return `❌ Task ${args.task_id} has no session`;
34
+ }
35
+
36
+ const messagesResult = await ctx.client.session.messages({
37
+ path: { id: task.sessionID },
38
+ });
39
+
40
+ if (messagesResult.error) {
41
+ return `❌ Failed to retrieve output: ${messagesResult.error}`;
42
+ }
43
+
44
+ const messages = messagesResult.data as any[];
45
+ const assistantMessages = messages.filter(
46
+ (m) => m.info?.role === "assistant"
47
+ );
48
+
49
+ const output = assistantMessages
50
+ .map((m) => m.parts?.map((p: any) => p.text).join("\n"))
51
+ .join("\n\n---\n\n");
52
+
53
+ return `📥 Output from ${args.task_id}:\n\n${output}`;
54
+ },
55
+ });
56
+
57
+ export default BackgroundOutputTool;
@@ -0,0 +1,55 @@
1
+ export type BackgroundTaskStatus =
2
+ | "pending"
3
+ | "running"
4
+ | "completed"
5
+ | "error"
6
+ | "cancelled";
7
+
8
+ export interface TaskProgress {
9
+ toolCalls: number;
10
+ lastTool?: string;
11
+ lastUpdate: Date;
12
+ lastMessage?: string;
13
+ lastMessageAt?: Date;
14
+ }
15
+
16
+ export interface BackgroundTask {
17
+ id: string;
18
+ sessionID?: string;
19
+ parentSessionID: string;
20
+ description: string;
21
+ prompt: string;
22
+ agent: string;
23
+ status: BackgroundTaskStatus;
24
+ queuedAt?: Date;
25
+ startedAt?: Date;
26
+ completedAt?: Date;
27
+ error?: string;
28
+ progress?: TaskProgress;
29
+ model?: string;
30
+ skills?: string[];
31
+ lastMsgCount?: number;
32
+ stablePolls?: number;
33
+ }
34
+
35
+ export interface LaunchInput {
36
+ description: string;
37
+ prompt: string;
38
+ agent: string;
39
+ parentSessionID: string;
40
+ model?: string;
41
+ skills?: string[];
42
+ skillContent?: string;
43
+ manual_path?: string;
44
+ }
45
+
46
+ export interface ModelConfig {
47
+ providerID: string;
48
+ modelID: string;
49
+ }
50
+
51
+ export interface ResumeInput {
52
+ sessionId: string;
53
+ prompt: string;
54
+ parentSessionID: string;
55
+ }
@@ -0,0 +1,8 @@
1
+ export { default as background_agent } from "./background-agent/launch.js";
2
+ export { default as list_background_tasks } from "./background-agent/list.js";
3
+ export { default as background_output } from "./background-agent/output.js";
4
+ export { default as background_cancel } from "./background-agent/cancel.js";
5
+ export { default as manual_update } from "./manual/manual-update.js";
6
+ export { default as verify_manual } from "./manual/verify-manual.js";
7
+ export * from "./background-agent/index.js";
8
+ export * from "./manual/index.js";
@@ -0,0 +1,2 @@
1
+ export { default as manual_update } from "./manual-update.js";
2
+ export { default as verify_manual } from "./verify-manual.js";