bobs-workshop 0.3.3 → 3.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 (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 +377 -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 +46 -0
  42. package/package.json +34 -66
  43. package/postinstall.js +190 -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 +455 -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 +55 -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,455 @@
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
+ const skillFile = readFileSync(skillPath, "utf8");
121
+ skillContent += `\n\n---\n## Skill: ${skillName}\n\n${skillFile}`;
122
+ }
123
+ }
124
+ }
125
+
126
+ // Add MANUAL content if provided
127
+ if (input.manual_path) {
128
+ const { existsSync, readFileSync } = await import("node:fs");
129
+ if (existsSync(input.manual_path)) {
130
+ const manualContent = readFileSync(input.manual_path, "utf8");
131
+ skillContent = skillContent
132
+ ? `${skillContent}\n\n## MANUAL Context:\n${manualContent}`
133
+ : manualContent;
134
+ }
135
+ }
136
+
137
+ // Fire-and-forget prompt
138
+ this.client.session.prompt({
139
+ path: { id: sessionID },
140
+ body: {
141
+ agent: input.agent,
142
+ ...(input.model ? { model: input.model } : {}),
143
+ system: skillContent || undefined,
144
+ tools: {
145
+ task: false,
146
+ delegate_task: false,
147
+ },
148
+ parts: [{ type: "text", text: input.prompt }],
149
+ },
150
+ } as any).catch((error) => {
151
+ console.error("[background-agent] Prompt error:", error);
152
+ const existingTask = this.tasks.get(task.id);
153
+ if (existingTask && existingTask.status === "running") {
154
+ existingTask.status = "error";
155
+ existingTask.error = error instanceof Error ? error.message : String(error);
156
+ existingTask.completedAt = new Date();
157
+ if (concurrencyKey) {
158
+ this.concurrencyManager.release(concurrencyKey);
159
+ }
160
+ }
161
+ });
162
+ } catch (error) {
163
+ this.concurrencyManager.release(concurrencyKey);
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ getTask(id: string): BackgroundTask | undefined {
169
+ return this.tasks.get(id);
170
+ }
171
+
172
+ getAllTasks(): BackgroundTask[] {
173
+ return Array.from(this.tasks.values());
174
+ }
175
+
176
+ getRunningTasks(): BackgroundTask[] {
177
+ return Array.from(this.tasks.values()).filter((t) => t.status === "running");
178
+ }
179
+
180
+ handleEvent(event: { type: string; properties?: Record<string, unknown> }): void {
181
+ if (event.type === "session.idle") {
182
+ const sessionID = event.properties?.sessionID as string | undefined;
183
+ if (!sessionID) return;
184
+
185
+ const task = Array.from(this.tasks.values()).find((t) => t.sessionID === sessionID);
186
+ if (!task || task.status !== "running") return;
187
+
188
+ const startedAt = task.startedAt;
189
+ if (!startedAt) return;
190
+
191
+ const elapsedMs = Date.now() - startedAt.getTime();
192
+ const MIN_IDLE_TIME_MS = 5000; // 5 seconds
193
+
194
+ if (elapsedMs < MIN_IDLE_TIME_MS) {
195
+ console.log("[background-agent] Ignoring early session.idle");
196
+ return;
197
+ }
198
+
199
+ this.tryCompleteTask(task, "session.idle");
200
+ }
201
+
202
+ if (event.type === "session.deleted") {
203
+ const info = event.properties?.info as { id: string } | undefined;
204
+ if (!info?.id) return;
205
+
206
+ const task = Array.from(this.tasks.values()).find((t) => t.sessionID === info.id);
207
+ if (!task) return;
208
+
209
+ if (task.status === "running") {
210
+ task.status = "cancelled";
211
+ task.completedAt = new Date();
212
+ task.error = "Session deleted";
213
+ }
214
+
215
+ this.tasks.delete(task.id);
216
+ }
217
+ }
218
+
219
+ private async tryCompleteTask(task: BackgroundTask, source: string): Promise<boolean> {
220
+ if (task.status !== "running") {
221
+ return false;
222
+ }
223
+
224
+ // Validate session has output before completing
225
+ if (!task.sessionID) return false;
226
+
227
+ const hasOutput = await this.validateSessionHasOutput(task.sessionID);
228
+ if (!hasOutput) {
229
+ console.log("[background-agent] No valid output yet, waiting");
230
+ return false;
231
+ }
232
+
233
+ task.status = "completed";
234
+ task.completedAt = new Date();
235
+
236
+ // Release concurrency
237
+ if (task.model) {
238
+ this.concurrencyManager.release(task.model);
239
+ } else {
240
+ this.concurrencyManager.release(task.agent);
241
+ }
242
+
243
+ console.log(`[background-agent] Task completed via ${source}:`, task.id);
244
+
245
+ // Auto-remove after 5 minutes
246
+ setTimeout(() => {
247
+ this.tasks.delete(task.id);
248
+ console.log("[background-agent] Removed completed task from memory:", task.id);
249
+ }, 5 * 60 * 1000);
250
+
251
+ return true;
252
+ }
253
+
254
+ private async validateSessionHasOutput(sessionID: string): Promise<boolean> {
255
+ try {
256
+ const response = await this.client.session.messages({
257
+ path: { id: sessionID },
258
+ });
259
+
260
+ const messages = (response.data ?? []) as Array<{
261
+ info?: { role?: string };
262
+ parts?: Array<{ type?: string; text?: string; tool?: string }>;
263
+ }>;
264
+
265
+ const hasAssistantOrToolMessage = messages.some(
266
+ (m) => m.info?.role === "assistant" || m.info?.role === "tool"
267
+ );
268
+
269
+ if (!hasAssistantOrToolMessage) return false;
270
+
271
+ const hasContent = messages.some((m) => {
272
+ if (m.info?.role !== "assistant" && m.info?.role !== "tool") return false;
273
+ const parts = m.parts ?? [];
274
+ return parts.some(
275
+ (p: any) =>
276
+ (p.type === "text" && p.text && p.text.trim().length > 0) ||
277
+ (p.type === "reasoning" && p.text && p.text.trim().length > 0) ||
278
+ p.type === "tool" ||
279
+ (p.type === "tool_result" && p.content !== undefined)
280
+ );
281
+ });
282
+
283
+ return hasContent;
284
+ } catch (error) {
285
+ console.error("[background-agent] Error validating session output:", error);
286
+ return true; // Allow completion on error
287
+ }
288
+ }
289
+
290
+ private startPolling(): void {
291
+ if (this.pollingInterval) return;
292
+
293
+ this.pollingInterval = setInterval(() => {
294
+ this.pollRunningTasks();
295
+ }, 2000);
296
+ this.pollingInterval.unref();
297
+ }
298
+
299
+ private stopPolling(): void {
300
+ if (this.pollingInterval) {
301
+ clearInterval(this.pollingInterval);
302
+ this.pollingInterval = undefined;
303
+ }
304
+ }
305
+
306
+ private async pollRunningTasks(): Promise<void> {
307
+ const now = Date.now();
308
+
309
+ // Prune stale tasks
310
+ for (const [taskId, task] of this.tasks.entries()) {
311
+ const timestamp = task.status === "pending"
312
+ ? task.queuedAt?.getTime()
313
+ : task.startedAt?.getTime();
314
+
315
+ if (!timestamp) continue;
316
+
317
+ const age = now - timestamp;
318
+ if (age > TASK_TTL_MS) {
319
+ const errorMessage = task.status === "pending"
320
+ ? "Task timed out while queued (30 minutes)"
321
+ : "Task timed out after 30 minutes";
322
+
323
+ console.log("[background-agent] Pruning stale task:", { taskId, age });
324
+ task.status = "error";
325
+ task.error = errorMessage;
326
+ task.completedAt = new Date();
327
+ this.tasks.delete(taskId);
328
+ }
329
+ }
330
+
331
+ // Check running tasks
332
+ const statusResult = await this.client.session.status();
333
+ const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
334
+
335
+ for (const task of this.tasks.values()) {
336
+ if (task.status !== "running") continue;
337
+
338
+ const sessionID = task.sessionID;
339
+ if (!sessionID) continue;
340
+
341
+ try {
342
+ const sessionStatus = allStatuses[sessionID];
343
+
344
+ if (sessionStatus?.type === "idle") {
345
+ const hasValidOutput = await this.validateSessionHasOutput(sessionID);
346
+ if (!hasValidOutput) {
347
+ continue;
348
+ }
349
+
350
+ // Re-check status after async operation
351
+ if (task.status !== "running") continue;
352
+
353
+ await this.tryCompleteTask(task, "polling (idle status)");
354
+ continue;
355
+ }
356
+
357
+ // Stability detection
358
+ const messagesResult = await this.client.session.messages({
359
+ path: { id: sessionID },
360
+ });
361
+
362
+ if (!messagesResult.error && messagesResult.data) {
363
+ const messages = messagesResult.data as Array<{
364
+ info?: { role?: string };
365
+ parts?: Array<{ type?: string; tool?: string; name?: string; text?: string }>;
366
+ }>;
367
+
368
+ const assistantMsgs = messages.filter((m) => m.info?.role === "assistant");
369
+
370
+ let toolCalls = 0;
371
+ let lastTool: string | undefined;
372
+
373
+ for (const msg of assistantMsgs) {
374
+ const parts = msg.parts ?? [];
375
+ for (const part of parts) {
376
+ if (part.type === "tool_use" || part.tool) {
377
+ toolCalls++;
378
+ lastTool = part.tool || part.name || "unknown";
379
+ }
380
+ }
381
+ }
382
+
383
+ if (!task.progress) {
384
+ task.progress = { toolCalls: 0, lastUpdate: new Date() };
385
+ }
386
+ task.progress.toolCalls = toolCalls;
387
+ task.progress.lastTool = lastTool;
388
+ task.progress.lastUpdate = new Date();
389
+
390
+ const currentMsgCount = messages.length;
391
+ const startedAt = task.startedAt;
392
+ if (!startedAt) continue;
393
+
394
+ const elapsedMs = Date.now() - startedAt.getTime();
395
+
396
+ if (elapsedMs >= MIN_STABILITY_TIME_MS) {
397
+ if (task.lastMsgCount === currentMsgCount) {
398
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
399
+ if ((task.stablePolls ?? 0) >= 3) {
400
+ const recheckStatus = await this.client.session.status();
401
+ const recheckData = (recheckStatus.data ?? {}) as Record<string, { type: string }>;
402
+ const currentStatus = recheckData[sessionID];
403
+
404
+ if (currentStatus?.type !== "idle") {
405
+ task.stablePolls = 0;
406
+ continue;
407
+ }
408
+
409
+ const hasValidOutput = await this.validateSessionHasOutput(sessionID);
410
+ if (!hasValidOutput) {
411
+ continue;
412
+ }
413
+
414
+ if (task.status !== "running") continue;
415
+
416
+ await this.tryCompleteTask(task, "stability detection");
417
+ continue;
418
+ }
419
+ } else {
420
+ task.stablePolls = 0;
421
+ }
422
+ }
423
+ task.lastMsgCount = currentMsgCount;
424
+ }
425
+ } catch (error) {
426
+ console.error("[background-agent] Poll error:", error);
427
+ }
428
+ }
429
+
430
+ // Stop polling if no running tasks
431
+ if (!this.getRunningTasks().length) {
432
+ this.stopPolling();
433
+ }
434
+ }
435
+
436
+ shutdown(): void {
437
+ if (this.shutdownTriggered) return;
438
+ this.shutdownTriggered = true;
439
+ console.log("[background-agent] Shutting down BackgroundManager");
440
+ this.stopPolling();
441
+
442
+ // Release concurrency for all running tasks
443
+ for (const task of this.tasks.values()) {
444
+ if (task.model) {
445
+ this.concurrencyManager.release(task.model);
446
+ } else {
447
+ this.concurrencyManager.release(task.agent);
448
+ }
449
+ }
450
+
451
+ this.concurrencyManager.clear();
452
+ this.tasks.clear();
453
+ console.log("[background-agent] Shutdown complete");
454
+ }
455
+ }
@@ -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";