@agentuity/opencode 1.0.1 → 1.0.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/AGENTS.md +121 -13
- package/README.md +133 -12
- package/dist/agents/architect.d.ts +1 -1
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +2 -2
- package/dist/agents/builder.d.ts +1 -1
- package/dist/agents/builder.d.ts.map +1 -1
- package/dist/agents/builder.js +2 -2
- package/dist/agents/builder.js.map +1 -1
- package/dist/agents/expert-backend.d.ts +4 -0
- package/dist/agents/expert-backend.d.ts.map +1 -0
- package/dist/agents/expert-backend.js +493 -0
- package/dist/agents/expert-backend.js.map +1 -0
- package/dist/agents/expert-frontend.d.ts +4 -0
- package/dist/agents/expert-frontend.d.ts.map +1 -0
- package/dist/agents/expert-frontend.js +480 -0
- package/dist/agents/expert-frontend.js.map +1 -0
- package/dist/agents/expert-ops.d.ts +4 -0
- package/dist/agents/expert-ops.d.ts.map +1 -0
- package/dist/agents/expert-ops.js +375 -0
- package/dist/agents/expert-ops.js.map +1 -0
- package/dist/agents/expert.d.ts +1 -1
- package/dist/agents/expert.d.ts.map +1 -1
- package/dist/agents/expert.js +172 -913
- package/dist/agents/expert.js.map +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +8 -2
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/lead.d.ts +1 -1
- package/dist/agents/lead.d.ts.map +1 -1
- package/dist/agents/lead.js +359 -58
- package/dist/agents/lead.js.map +1 -1
- package/dist/agents/memory/entities.d.ts.map +1 -1
- package/dist/agents/memory/entities.js +5 -2
- package/dist/agents/memory/entities.js.map +1 -1
- package/dist/agents/memory.d.ts +1 -1
- package/dist/agents/memory.d.ts.map +1 -1
- package/dist/agents/memory.js +285 -10
- package/dist/agents/memory.js.map +1 -1
- package/dist/agents/monitor.d.ts +4 -0
- package/dist/agents/monitor.d.ts.map +1 -0
- package/dist/agents/monitor.js +106 -0
- package/dist/agents/monitor.js.map +1 -0
- package/dist/agents/product.d.ts +1 -1
- package/dist/agents/product.d.ts.map +1 -1
- package/dist/agents/product.js +161 -21
- package/dist/agents/product.js.map +1 -1
- package/dist/agents/reasoner.d.ts +1 -1
- package/dist/agents/reasoner.d.ts.map +1 -1
- package/dist/agents/reasoner.js +94 -11
- package/dist/agents/reasoner.js.map +1 -1
- package/dist/agents/scout.d.ts +1 -1
- package/dist/agents/scout.d.ts.map +1 -1
- package/dist/agents/scout.js +6 -4
- package/dist/agents/scout.js.map +1 -1
- package/dist/agents/types.d.ts +6 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/background/manager.d.ts +22 -1
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +218 -1
- package/dist/background/manager.js.map +1 -1
- package/dist/background/types.d.ts +19 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +10 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/plugin/hooks/cadence.d.ts +2 -1
- package/dist/plugin/hooks/cadence.d.ts.map +1 -1
- package/dist/plugin/hooks/cadence.js +66 -3
- package/dist/plugin/hooks/cadence.js.map +1 -1
- package/dist/plugin/hooks/keyword.d.ts.map +1 -1
- package/dist/plugin/hooks/keyword.js +5 -3
- package/dist/plugin/hooks/keyword.js.map +1 -1
- package/dist/plugin/hooks/session-memory.d.ts +2 -1
- package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
- package/dist/plugin/hooks/session-memory.js +57 -5
- package/dist/plugin/hooks/session-memory.js.map +1 -1
- package/dist/plugin/hooks/tools.d.ts.map +1 -1
- package/dist/plugin/hooks/tools.js +28 -5
- package/dist/plugin/hooks/tools.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +119 -68
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +9 -0
- package/dist/services/auth.js.map +1 -1
- package/dist/tmux/executor.d.ts.map +1 -1
- package/dist/tmux/executor.js +13 -4
- package/dist/tmux/executor.js.map +1 -1
- package/dist/tools/background.d.ts +4 -1
- package/dist/tools/background.d.ts.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/agents/architect.ts +2 -2
- package/src/agents/builder.ts +2 -2
- package/src/agents/expert-backend.ts +495 -0
- package/src/agents/expert-frontend.ts +482 -0
- package/src/agents/expert-ops.ts +377 -0
- package/src/agents/expert.ts +172 -913
- package/src/agents/index.ts +8 -2
- package/src/agents/lead.ts +359 -58
- package/src/agents/memory/entities.ts +10 -2
- package/src/agents/memory.ts +285 -10
- package/src/agents/monitor.ts +108 -0
- package/src/agents/product.ts +161 -21
- package/src/agents/reasoner.ts +94 -11
- package/src/agents/scout.ts +6 -4
- package/src/agents/types.ts +6 -0
- package/src/background/manager.ts +259 -2
- package/src/background/types.ts +17 -0
- package/src/config/loader.ts +11 -1
- package/src/plugin/hooks/cadence.ts +79 -3
- package/src/plugin/hooks/keyword.ts +5 -3
- package/src/plugin/hooks/session-memory.ts +68 -6
- package/src/plugin/hooks/tools.ts +40 -14
- package/src/plugin/plugin.ts +128 -70
- package/src/services/auth.ts +10 -0
- package/src/tmux/executor.ts +13 -4
- package/src/tools/index.ts +0 -1
- package/src/types.ts +4 -1
- package/dist/agents/planner.d.ts +0 -4
- package/dist/agents/planner.d.ts.map +0 -1
- package/dist/agents/planner.js +0 -158
- package/dist/agents/planner.js.map +0 -1
- package/dist/tools/delegate.d.ts +0 -45
- package/dist/tools/delegate.d.ts.map +0 -1
- package/dist/tools/delegate.js +0 -72
- package/dist/tools/delegate.js.map +0 -1
- package/src/agents/planner.ts +0 -161
- package/src/tools/delegate.ts +0 -83
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
2
|
import { agents } from '../agents';
|
|
3
3
|
import type { AgentDefinition } from '../agents';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
BackgroundTask,
|
|
6
|
+
BackgroundTaskConfig,
|
|
7
|
+
BackgroundTaskStatus,
|
|
8
|
+
LaunchInput,
|
|
9
|
+
TaskInspection,
|
|
10
|
+
TaskProgress,
|
|
11
|
+
} from './types';
|
|
5
12
|
import { ConcurrencyManager } from './concurrency';
|
|
6
13
|
|
|
7
14
|
const DEFAULT_BACKGROUND_CONFIG: BackgroundTaskConfig = {
|
|
@@ -106,6 +113,225 @@ export class BackgroundManager {
|
|
|
106
113
|
return this.tasks.get(taskId);
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Inspect a background task by fetching its session messages.
|
|
118
|
+
* Useful for seeing what a child Lead or other agent is doing.
|
|
119
|
+
*/
|
|
120
|
+
async inspectTask(taskId: string): Promise<TaskInspection | undefined> {
|
|
121
|
+
const task = this.tasks.get(taskId);
|
|
122
|
+
if (!task?.sessionId) return undefined;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Get session details
|
|
126
|
+
const sessionResponse = await this.ctx.client.session.get({
|
|
127
|
+
path: { id: task.sessionId },
|
|
128
|
+
throwOnError: false,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Get messages from the session
|
|
132
|
+
const messagesResponse = await this.ctx.client.session.messages({
|
|
133
|
+
path: { id: task.sessionId },
|
|
134
|
+
throwOnError: false,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const session = unwrapResponse<unknown>(sessionResponse);
|
|
138
|
+
const rawMessages =
|
|
139
|
+
unwrapResponse<Array<{ info: unknown; parts: unknown[] }>>(messagesResponse);
|
|
140
|
+
// Defensive array coercion (response may be non-array when throwOnError is false)
|
|
141
|
+
const messages = Array.isArray(rawMessages) ? rawMessages : [];
|
|
142
|
+
|
|
143
|
+
// Return structured inspection result
|
|
144
|
+
return {
|
|
145
|
+
taskId: task.id,
|
|
146
|
+
sessionId: task.sessionId,
|
|
147
|
+
status: task.status,
|
|
148
|
+
session,
|
|
149
|
+
messages,
|
|
150
|
+
lastActivity: task.progress?.lastUpdate?.toISOString(),
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
// Session might not exist anymore
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Refresh task statuses from the server.
|
|
160
|
+
* Useful for recovering state after issues or checking on stuck tasks.
|
|
161
|
+
*/
|
|
162
|
+
async refreshStatuses(): Promise<Map<string, BackgroundTaskStatus>> {
|
|
163
|
+
const results = new Map<string, BackgroundTaskStatus>();
|
|
164
|
+
|
|
165
|
+
// Get all our tracked session IDs
|
|
166
|
+
const sessionIds = Array.from(this.tasksBySession.keys());
|
|
167
|
+
if (sessionIds.length === 0) return results;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Fetch children for each unique parent (more efficient than individual gets)
|
|
171
|
+
const parentIds = new Set<string>();
|
|
172
|
+
for (const task of this.tasks.values()) {
|
|
173
|
+
if (task.parentSessionId) {
|
|
174
|
+
parentIds.add(task.parentSessionId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const completionPromises: Promise<void>[] = [];
|
|
179
|
+
|
|
180
|
+
for (const parentId of parentIds) {
|
|
181
|
+
const childrenResponse = await this.ctx.client.session.children({
|
|
182
|
+
path: { id: parentId },
|
|
183
|
+
throwOnError: false,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const children = unwrapResponse<Array<unknown>>(childrenResponse) ?? [];
|
|
187
|
+
for (const child of children) {
|
|
188
|
+
const childSession = child as { id?: string; status?: { type?: string } };
|
|
189
|
+
if (!childSession.id) continue;
|
|
190
|
+
|
|
191
|
+
const matchedTaskId = this.tasksBySession.get(childSession.id);
|
|
192
|
+
if (matchedTaskId) {
|
|
193
|
+
const task = this.tasks.get(matchedTaskId);
|
|
194
|
+
if (task) {
|
|
195
|
+
const newStatus = this.mapSessionStatusToTaskStatus(childSession);
|
|
196
|
+
if (newStatus !== task.status) {
|
|
197
|
+
// Use proper handlers to trigger side effects (concurrency, notifications, etc.)
|
|
198
|
+
if (newStatus === 'completed' && task.status === 'running') {
|
|
199
|
+
completionPromises.push(this.completeTask(task));
|
|
200
|
+
results.set(matchedTaskId, newStatus);
|
|
201
|
+
} else if (newStatus === 'error') {
|
|
202
|
+
this.failTask(task, 'Session ended with error');
|
|
203
|
+
results.set(matchedTaskId, newStatus);
|
|
204
|
+
} else {
|
|
205
|
+
// For other transitions (e.g., pending -> running), direct update is fine
|
|
206
|
+
task.status = newStatus;
|
|
207
|
+
results.set(matchedTaskId, newStatus);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Wait for all completion handlers to finish
|
|
216
|
+
await Promise.all(completionPromises);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Log but don't fail - this is a best-effort refresh
|
|
219
|
+
console.error('Failed to refresh task statuses:', error);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Recover background tasks from existing sessions.
|
|
227
|
+
* Call this on plugin startup to restore state after restart.
|
|
228
|
+
*
|
|
229
|
+
* This method queries all sessions and reconstructs task state from
|
|
230
|
+
* sessions that have JSON-encoded task metadata in their title.
|
|
231
|
+
*
|
|
232
|
+
* @returns The number of tasks recovered
|
|
233
|
+
*/
|
|
234
|
+
async recoverTasks(): Promise<number> {
|
|
235
|
+
let recovered = 0;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Get all sessions
|
|
239
|
+
const sessionsResponse = await this.ctx.client.session.list({
|
|
240
|
+
throwOnError: false,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const sessions = unwrapResponse<Array<unknown>>(sessionsResponse) ?? [];
|
|
244
|
+
|
|
245
|
+
for (const session of sessions) {
|
|
246
|
+
const sess = session as {
|
|
247
|
+
id?: string;
|
|
248
|
+
title?: string;
|
|
249
|
+
parentID?: string;
|
|
250
|
+
status?: { type?: string };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Check if this is one of our background task sessions
|
|
254
|
+
// Our sessions have JSON-encoded task metadata in the title
|
|
255
|
+
if (!sess.title?.startsWith('{')) continue;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const metadata = JSON.parse(sess.title) as {
|
|
259
|
+
taskId?: string;
|
|
260
|
+
agent?: string;
|
|
261
|
+
description?: string;
|
|
262
|
+
createdAt?: string;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Skip if not a valid task metadata (must have taskId starting with 'bg_')
|
|
266
|
+
if (!metadata.taskId || !metadata.taskId.startsWith('bg_')) continue;
|
|
267
|
+
|
|
268
|
+
// Skip if we already have this task
|
|
269
|
+
if (this.tasks.has(metadata.taskId)) continue;
|
|
270
|
+
|
|
271
|
+
// Skip sessions without an ID
|
|
272
|
+
if (!sess.id) continue;
|
|
273
|
+
|
|
274
|
+
// Reconstruct the task
|
|
275
|
+
const agentName = metadata.agent ?? 'unknown';
|
|
276
|
+
const task: BackgroundTask = {
|
|
277
|
+
id: metadata.taskId,
|
|
278
|
+
sessionId: sess.id,
|
|
279
|
+
parentSessionId: sess.parentID ?? '',
|
|
280
|
+
agent: agentName,
|
|
281
|
+
description: metadata.description ?? '',
|
|
282
|
+
prompt: '', // Original prompt not stored in metadata
|
|
283
|
+
status: this.mapSessionStatusToTaskStatus(sess),
|
|
284
|
+
queuedAt: metadata.createdAt ? new Date(metadata.createdAt) : new Date(),
|
|
285
|
+
startedAt: metadata.createdAt ? new Date(metadata.createdAt) : new Date(),
|
|
286
|
+
concurrencyGroup: this.getConcurrencyGroup(agentName),
|
|
287
|
+
progress: {
|
|
288
|
+
toolCalls: 0,
|
|
289
|
+
lastUpdate: new Date(),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Add to our tracking maps
|
|
294
|
+
this.tasks.set(task.id, task);
|
|
295
|
+
this.tasksBySession.set(sess.id, task.id);
|
|
296
|
+
|
|
297
|
+
if (task.parentSessionId) {
|
|
298
|
+
const parentTasks = this.tasksByParent.get(task.parentSessionId) ?? new Set();
|
|
299
|
+
parentTasks.add(task.id);
|
|
300
|
+
this.tasksByParent.set(task.parentSessionId, parentTasks);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
recovered++;
|
|
304
|
+
} catch {
|
|
305
|
+
// Not valid JSON or not our task, skip
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('Failed to recover tasks:', error);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return recovered;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private mapSessionStatusToTaskStatus(session: unknown): BackgroundTaskStatus {
|
|
317
|
+
// Map OpenCode session status to our task status
|
|
318
|
+
// Session status types: 'idle' | 'pending' | 'running' | 'error'
|
|
319
|
+
const status = (session as { status?: { type?: string } })?.status?.type;
|
|
320
|
+
switch (status) {
|
|
321
|
+
case 'idle':
|
|
322
|
+
return 'completed';
|
|
323
|
+
case 'pending':
|
|
324
|
+
return 'pending';
|
|
325
|
+
case 'running':
|
|
326
|
+
return 'running';
|
|
327
|
+
case 'error':
|
|
328
|
+
return 'error';
|
|
329
|
+
default:
|
|
330
|
+
// Unknown session status - default to pending for best-effort recovery
|
|
331
|
+
return 'pending';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
109
335
|
cancel(taskId: string): boolean {
|
|
110
336
|
const task = this.tasks.get(taskId);
|
|
111
337
|
if (!task || task.status === 'completed' || task.status === 'error') {
|
|
@@ -220,10 +446,18 @@ export class BackgroundManager {
|
|
|
220
446
|
}
|
|
221
447
|
|
|
222
448
|
try {
|
|
449
|
+
// Store task metadata in session title for persistence/recovery
|
|
450
|
+
const taskMetadata = JSON.stringify({
|
|
451
|
+
taskId: task.id,
|
|
452
|
+
agent: task.agent,
|
|
453
|
+
description: task.description,
|
|
454
|
+
createdAt: task.queuedAt?.toISOString() ?? new Date().toISOString(),
|
|
455
|
+
});
|
|
456
|
+
|
|
223
457
|
const sessionResult = await this.ctx.client.session.create({
|
|
224
458
|
body: {
|
|
225
459
|
parentID: task.parentSessionId,
|
|
226
|
-
title:
|
|
460
|
+
title: taskMetadata,
|
|
227
461
|
},
|
|
228
462
|
throwOnError: true,
|
|
229
463
|
});
|
|
@@ -334,6 +568,29 @@ export class BackgroundManager {
|
|
|
334
568
|
private async notifyParent(task: BackgroundTask): Promise<void> {
|
|
335
569
|
if (!task.parentSessionId) return;
|
|
336
570
|
|
|
571
|
+
// Prevent duplicate notifications for the same task+status combination
|
|
572
|
+
// This guards against OpenCode firing multiple events for the same status transition
|
|
573
|
+
const notifiedStatuses = task.notifiedStatuses ?? new Set();
|
|
574
|
+
|
|
575
|
+
// Self-healing for tasks created before deduplication was added:
|
|
576
|
+
// If a task is already in a terminal state but has no notification history,
|
|
577
|
+
// assume it was already notified and skip to prevent duplicate notifications.
|
|
578
|
+
if (
|
|
579
|
+
notifiedStatuses.size === 0 &&
|
|
580
|
+
(task.status === 'completed' || task.status === 'error' || task.status === 'cancelled')
|
|
581
|
+
) {
|
|
582
|
+
notifiedStatuses.add(task.status);
|
|
583
|
+
task.notifiedStatuses = notifiedStatuses;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (notifiedStatuses.has(task.status)) {
|
|
588
|
+
return; // Already notified for this status, skip duplicate
|
|
589
|
+
}
|
|
590
|
+
// Mark as notified BEFORE sending to prevent race conditions
|
|
591
|
+
notifiedStatuses.add(task.status);
|
|
592
|
+
task.notifiedStatuses = notifiedStatuses;
|
|
593
|
+
|
|
337
594
|
const statusLine = task.status === 'completed' ? 'completed' : task.status;
|
|
338
595
|
const message = `[BACKGROUND TASK ${statusLine.toUpperCase()}]
|
|
339
596
|
|
package/src/background/types.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface BackgroundTask {
|
|
|
26
26
|
progress?: TaskProgress;
|
|
27
27
|
concurrencyKey?: string; // Active concurrency slot key
|
|
28
28
|
concurrencyGroup?: string; // Persistent key for re-acquiring on resume
|
|
29
|
+
notifiedStatuses?: Set<BackgroundTaskStatus>; // Tracks statuses already notified to prevent duplicates
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export interface LaunchInput {
|
|
@@ -50,3 +51,19 @@ export interface BackgroundTaskConfig {
|
|
|
50
51
|
providerConcurrency?: Record<string, number>;
|
|
51
52
|
modelConcurrency?: Record<string, number>;
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of inspecting a background task's session.
|
|
57
|
+
* Provides access to session details and messages for debugging.
|
|
58
|
+
*/
|
|
59
|
+
export interface TaskInspection {
|
|
60
|
+
taskId: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
status: BackgroundTaskStatus;
|
|
63
|
+
/** Session details from OpenCode SDK */
|
|
64
|
+
session: unknown;
|
|
65
|
+
/** Messages from the session */
|
|
66
|
+
messages: Array<{ info: unknown; parts: unknown[] }>;
|
|
67
|
+
/** Last activity timestamp from task progress */
|
|
68
|
+
lastActivity?: string;
|
|
69
|
+
}
|
package/src/config/loader.ts
CHANGED
|
@@ -35,6 +35,16 @@ interface CLIConfig {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function getProfilePath(): Promise<string> {
|
|
38
|
+
// Check AGENTUITY_PROFILE env var first (matches CLI behavior)
|
|
39
|
+
if (process.env.AGENTUITY_PROFILE) {
|
|
40
|
+
const envProfilePath = join(CONFIG_DIR, `${process.env.AGENTUITY_PROFILE}.yaml`);
|
|
41
|
+
const envFile = Bun.file(envProfilePath);
|
|
42
|
+
if (await envFile.exists()) {
|
|
43
|
+
return envProfilePath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Then check profile file
|
|
38
48
|
const profileFile = Bun.file(join(CONFIG_DIR, 'profile'));
|
|
39
49
|
|
|
40
50
|
if (await profileFile.exists()) {
|
|
@@ -74,7 +84,7 @@ export async function getConfigPath(): Promise<string> {
|
|
|
74
84
|
* {
|
|
75
85
|
* "agent": {
|
|
76
86
|
* "Agentuity Coder Architect": {
|
|
77
|
-
* "model": "openai/gpt-5.
|
|
87
|
+
* "model": "openai/gpt-5.3-codex",
|
|
78
88
|
* "reasoningEffort": "xhigh"
|
|
79
89
|
* }
|
|
80
90
|
* }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
2
|
import type { CoderConfig } from '../../types';
|
|
3
|
+
import type { BackgroundManager } from '../../background';
|
|
3
4
|
|
|
4
5
|
/** Compacting hook input/output types */
|
|
5
6
|
type CompactingInput = { sessionID: string };
|
|
@@ -15,6 +16,23 @@ export interface CadenceHooks {
|
|
|
15
16
|
|
|
16
17
|
const COMPLETION_PATTERN = /<promise>\s*DONE\s*<\/promise>/i;
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Get the current git branch name.
|
|
21
|
+
*/
|
|
22
|
+
async function getCurrentBranch(): Promise<string> {
|
|
23
|
+
try {
|
|
24
|
+
const proc = Bun.spawn(['git', 'branch', '--show-current'], {
|
|
25
|
+
stdout: 'pipe',
|
|
26
|
+
stderr: 'pipe',
|
|
27
|
+
});
|
|
28
|
+
const stdout = await new Response(proc.stdout).text();
|
|
29
|
+
await proc.exited;
|
|
30
|
+
return stdout.trim() || 'unknown';
|
|
31
|
+
} catch {
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
// Ultrawork trigger keywords - case insensitive matching
|
|
19
37
|
const ULTRAWORK_TRIGGERS = [
|
|
20
38
|
'ultrawork',
|
|
@@ -48,7 +66,11 @@ interface CadenceSessionState {
|
|
|
48
66
|
* 4. Trigger continuation after compaction (session.compacted)
|
|
49
67
|
* 5. Clean up on session abort/error
|
|
50
68
|
*/
|
|
51
|
-
export function createCadenceHooks(
|
|
69
|
+
export function createCadenceHooks(
|
|
70
|
+
ctx: PluginInput,
|
|
71
|
+
_config: CoderConfig,
|
|
72
|
+
backgroundManager?: BackgroundManager
|
|
73
|
+
): CadenceHooks {
|
|
52
74
|
const activeCadenceSessions = new Map<string, CadenceSessionState>();
|
|
53
75
|
|
|
54
76
|
const log = (msg: string) => {
|
|
@@ -170,6 +192,9 @@ export function createCadenceHooks(ctx: PluginInput, _config: CoderConfig): Cade
|
|
|
170
192
|
log(`Compaction completed for Cadence session ${sessionId} - saving and continuing`);
|
|
171
193
|
showToast(ctx, '🔄 Compaction saved, resuming Cadence...');
|
|
172
194
|
|
|
195
|
+
// Get current git branch
|
|
196
|
+
const branch = await getCurrentBranch();
|
|
197
|
+
|
|
173
198
|
try {
|
|
174
199
|
await ctx.client.session?.prompt?.({
|
|
175
200
|
path: { id: sessionId },
|
|
@@ -181,6 +206,8 @@ export function createCadenceHooks(ctx: PluginInput, _config: CoderConfig): Cade
|
|
|
181
206
|
|
|
182
207
|
The compaction summary above contains our Cadence session context.
|
|
183
208
|
|
|
209
|
+
Current branch: ${branch}
|
|
210
|
+
|
|
184
211
|
1. Have @Agentuity Coder Memory save this compaction:
|
|
185
212
|
- Get existing session: \`agentuity cloud kv get agentuity-opencode-memory "session:${sessionId}" --json --region use\`
|
|
186
213
|
- Append compaction to \`compactions\` array with timestamp
|
|
@@ -269,6 +296,42 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
|
|
|
269
296
|
log(`Injecting Cadence context during compaction for session ${sessionId}`);
|
|
270
297
|
showToast(ctx, '💾 Compacting Cadence context...');
|
|
271
298
|
|
|
299
|
+
// Get current git branch
|
|
300
|
+
const branch = await getCurrentBranch();
|
|
301
|
+
|
|
302
|
+
// Get active background tasks for this session
|
|
303
|
+
const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
|
|
304
|
+
let backgroundTaskContext = '';
|
|
305
|
+
|
|
306
|
+
if (tasks.length > 0) {
|
|
307
|
+
const taskList = tasks
|
|
308
|
+
.map(
|
|
309
|
+
(t) =>
|
|
310
|
+
`- **${t.id}**: ${t.description || 'No description'} (session: ${t.sessionId ?? 'pending'}, status: ${t.status})`
|
|
311
|
+
)
|
|
312
|
+
.join('\n');
|
|
313
|
+
|
|
314
|
+
backgroundTaskContext = `
|
|
315
|
+
|
|
316
|
+
## Active Background Tasks
|
|
317
|
+
|
|
318
|
+
This session has ${tasks.length} background task(s) running in separate sessions:
|
|
319
|
+
${taskList}
|
|
320
|
+
|
|
321
|
+
**CRITICAL:** Task IDs and session IDs persist across compaction - these tasks are still running.
|
|
322
|
+
Use \`agentuity_background_output({ task_id: "..." })\` to check their status.
|
|
323
|
+
|
|
324
|
+
**Tip:** If you spawned child Leads for parallel work, delegate monitoring to BackgroundMonitor:
|
|
325
|
+
\`\`\`typescript
|
|
326
|
+
agentuity_background_task({
|
|
327
|
+
agent: "monitor",
|
|
328
|
+
task: "Monitor these background tasks and report when all complete:\\n${tasks.map((t) => `- ${t.id}`).join('\\n')}",
|
|
329
|
+
description: "Monitor child tasks"
|
|
330
|
+
})
|
|
331
|
+
\`\`\`
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
272
335
|
output.context.push(`
|
|
273
336
|
## CADENCE MODE ACTIVE
|
|
274
337
|
|
|
@@ -277,6 +340,7 @@ This session is running in Cadence mode (long-running autonomous loop).
|
|
|
277
340
|
**Cadence State:**
|
|
278
341
|
- Session ID: ${sessionId}
|
|
279
342
|
- Loop ID: ${state.loopId ?? 'unknown'}
|
|
343
|
+
- Branch: ${branch}
|
|
280
344
|
- Started: ${state.startedAt}
|
|
281
345
|
- Iteration: ${state.iteration} / ${state.maxIterations}
|
|
282
346
|
- Last activity: ${state.lastActivity}
|
|
@@ -284,8 +348,20 @@ This session is running in Cadence mode (long-running autonomous loop).
|
|
|
284
348
|
**Session Record Location:**
|
|
285
349
|
\`session:${sessionId}\` in agentuity-opencode-memory
|
|
286
350
|
|
|
287
|
-
|
|
288
|
-
|
|
351
|
+
**Planning State:**
|
|
352
|
+
If this session has planning active, the session record contains:
|
|
353
|
+
- \`planning.prdKey\` - Link to the PRD being executed
|
|
354
|
+
- \`planning.objective\` - What we're trying to accomplish
|
|
355
|
+
- \`planning.phases\` - Current phases with status and notes
|
|
356
|
+
- \`planning.current\` - Current phase
|
|
357
|
+
- \`planning.findings\` - Discoveries made during work
|
|
358
|
+
- \`planning.errors\` - Failures to avoid repeating
|
|
359
|
+
${backgroundTaskContext}
|
|
360
|
+
After compaction:
|
|
361
|
+
1. Memory will save this summary and update the session record
|
|
362
|
+
2. Memory should update planning.progress with this compaction
|
|
363
|
+
3. Lead will continue the loop from iteration ${state.iteration}
|
|
364
|
+
4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?
|
|
289
365
|
`);
|
|
290
366
|
},
|
|
291
367
|
|
|
@@ -17,9 +17,10 @@ You are now using the Agentuity Coder agent team from Agentuity.
|
|
|
17
17
|
- **@Agentuity Coder Architect**: Senior implementer - complex autonomous tasks, Cadence mode
|
|
18
18
|
- **@Agentuity Coder Reviewer**: Quality checker - reviews changes, applies fixes
|
|
19
19
|
- **@Agentuity Coder Memory**: Context keeper - remembers decisions, stores checkpoints
|
|
20
|
+
- **@Agentuity Coder Reasoner**: Conclusion extractor - resolves conflicts, surfaces corrections
|
|
20
21
|
- **@Agentuity Coder Expert**: Agentuity specialist - knows CLI commands and cloud services
|
|
21
|
-
- **@Agentuity Coder Planner**: Strategic advisor - complex architecture, deep planning
|
|
22
22
|
- **@Agentuity Coder Runner**: Command executor - runs lint/build/test, returns structured results
|
|
23
|
+
- **@Agentuity Coder Product**: Requirements definer - clarifies scope, validates features, drives clarity
|
|
23
24
|
|
|
24
25
|
## Agentuity Cloud Services Available
|
|
25
26
|
When genuinely helpful, use these via the CLI:
|
|
@@ -34,8 +35,9 @@ Run \`agentuity ai schema show\` to see all available CLI commands.
|
|
|
34
35
|
## Guidelines
|
|
35
36
|
1. Break complex tasks into subtasks
|
|
36
37
|
2. Use @Agentuity Coder Scout before implementing to understand context
|
|
37
|
-
3.
|
|
38
|
-
4.
|
|
38
|
+
3. Use @Agentuity Coder Product for new features or unclear requirements
|
|
39
|
+
4. Have @Agentuity Coder Reviewer check @Agentuity Coder Builder's work
|
|
40
|
+
5. Use cloud services only when they genuinely help
|
|
39
41
|
</coder-mode>
|
|
40
42
|
`;
|
|
41
43
|
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
2
|
import type { CoderConfig } from '../../types';
|
|
3
|
+
import type { BackgroundManager } from '../../background';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the current git branch name.
|
|
7
|
+
*/
|
|
8
|
+
async function getCurrentBranch(): Promise<string> {
|
|
9
|
+
try {
|
|
10
|
+
const proc = Bun.spawn(['git', 'branch', '--show-current'], {
|
|
11
|
+
stdout: 'pipe',
|
|
12
|
+
stderr: 'pipe',
|
|
13
|
+
});
|
|
14
|
+
const stdout = await new Response(proc.stdout).text();
|
|
15
|
+
await proc.exited;
|
|
16
|
+
return stdout.trim() || 'unknown';
|
|
17
|
+
} catch {
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
export interface SessionMemoryHooks {
|
|
5
23
|
onEvent: (input: {
|
|
@@ -20,7 +38,8 @@ export interface SessionMemoryHooks {
|
|
|
20
38
|
*/
|
|
21
39
|
export function createSessionMemoryHooks(
|
|
22
40
|
ctx: PluginInput,
|
|
23
|
-
_config: CoderConfig
|
|
41
|
+
_config: CoderConfig,
|
|
42
|
+
backgroundManager?: BackgroundManager
|
|
24
43
|
): SessionMemoryHooks {
|
|
25
44
|
const log = (msg: string) => {
|
|
26
45
|
ctx.client.app.log({
|
|
@@ -52,6 +71,8 @@ export function createSessionMemoryHooks(
|
|
|
52
71
|
log(`Compaction complete for session ${sessionId} - triggering memory save`);
|
|
53
72
|
|
|
54
73
|
try {
|
|
74
|
+
const branch = await getCurrentBranch();
|
|
75
|
+
|
|
55
76
|
await ctx.client.session.prompt({
|
|
56
77
|
path: { id: sessionId },
|
|
57
78
|
body: {
|
|
@@ -61,12 +82,14 @@ export function createSessionMemoryHooks(
|
|
|
61
82
|
text: `[COMPACTION COMPLETE]
|
|
62
83
|
|
|
63
84
|
The compaction summary above contains our session context.
|
|
85
|
+
Current branch: ${branch}
|
|
64
86
|
|
|
65
87
|
Have @Agentuity Coder Memory save this compaction:
|
|
66
88
|
1. Get existing session record (or create new): \`agentuity cloud kv get agentuity-opencode-memory "session:${sessionId}" --json --region use\`
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
89
|
+
2. Ensure branch field is set to "${branch}"
|
|
90
|
+
3. Append this compaction summary to the \`compactions\` array with timestamp
|
|
91
|
+
4. Save back: \`agentuity cloud kv set agentuity-opencode-memory "session:${sessionId}" '{...}' --region use\`
|
|
92
|
+
5. Upsert to Vector for semantic search: \`agentuity cloud vector upsert agentuity-opencode-sessions "session:${sessionId}" --document "..." --metadata '...' --region use\`
|
|
70
93
|
|
|
71
94
|
After saving the compaction:
|
|
72
95
|
1. Read back the session record from KV
|
|
@@ -129,13 +152,52 @@ Then continue with the current task if there is one.`,
|
|
|
129
152
|
const sessionId = input.sessionID;
|
|
130
153
|
log(`Compacting session ${sessionId}`);
|
|
131
154
|
|
|
155
|
+
// Get current git branch
|
|
156
|
+
const branch = await getCurrentBranch();
|
|
157
|
+
|
|
158
|
+
// Get active background tasks for this session
|
|
159
|
+
const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
|
|
160
|
+
let backgroundTaskContext = '';
|
|
161
|
+
|
|
162
|
+
if (tasks.length > 0) {
|
|
163
|
+
const taskList = tasks
|
|
164
|
+
.map(
|
|
165
|
+
(t) =>
|
|
166
|
+
`- **${t.id}**: ${t.description || 'No description'} (session: ${t.sessionId ?? 'pending'}, status: ${t.status})`
|
|
167
|
+
)
|
|
168
|
+
.join('\n');
|
|
169
|
+
|
|
170
|
+
backgroundTaskContext = `
|
|
171
|
+
|
|
172
|
+
## Active Background Tasks
|
|
173
|
+
|
|
174
|
+
This session has ${tasks.length} background task(s) running in separate sessions:
|
|
175
|
+
${taskList}
|
|
176
|
+
|
|
177
|
+
**CRITICAL:** Task IDs and session IDs persist across compaction - these tasks are still running.
|
|
178
|
+
Use \`agentuity_background_output({ task_id: "..." })\` to check their status.
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
132
182
|
output.context.push(`
|
|
133
183
|
## Session Memory
|
|
134
184
|
|
|
135
185
|
This session's context is being saved to persistent memory.
|
|
136
186
|
Session record location: \`session:${sessionId}\` in agentuity-opencode-memory
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
Current branch: ${branch}
|
|
188
|
+
|
|
189
|
+
**Planning State (if active):**
|
|
190
|
+
If this session has planning active (user requested "track progress" or similar), the session record contains:
|
|
191
|
+
- \`planning.prdKey\` - Link to PRD if one exists
|
|
192
|
+
- \`planning.objective\` - What we're trying to accomplish
|
|
193
|
+
- \`planning.phases\` - Current phases with status and notes
|
|
194
|
+
- \`planning.findings\` - Discoveries made during work
|
|
195
|
+
- \`planning.errors\` - Failures to avoid repeating
|
|
196
|
+
${backgroundTaskContext}
|
|
197
|
+
After compaction:
|
|
198
|
+
1. Memory will save this summary to the session record
|
|
199
|
+
2. If planning is active, Memory should update planning.progress with this compaction
|
|
200
|
+
3. Memory will consider triggering Reasoner if significant patterns/corrections emerged
|
|
139
201
|
`);
|
|
140
202
|
},
|
|
141
203
|
};
|
|
@@ -15,6 +15,15 @@ const CLOUD_TOOL_PREFIXES = [
|
|
|
15
15
|
'agentuity.sandbox',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Escape a string for safe use in shell commands.
|
|
20
|
+
* Wraps in single quotes and escapes any internal single quotes.
|
|
21
|
+
*/
|
|
22
|
+
function shellEscape(str: string): string {
|
|
23
|
+
// Replace single quotes with '\'' (end quote, escaped quote, start quote)
|
|
24
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Get the Agentuity profile to use for CLI commands.
|
|
20
29
|
* Defaults to 'production' for safety, but can be overridden via AGENTUITY_CODER_PROFILE.
|
|
@@ -74,21 +83,38 @@ export function createToolHooks(ctx: PluginInput, config: CoderConfig): ToolHook
|
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
// Inject AGENTUITY_PROFILE and AGENTUITY_OPENCODE_SESSION environment variables
|
|
87
|
+
const profile = getCoderProfile();
|
|
88
|
+
const sessionId = (input as { sessionID?: string }).sessionID;
|
|
89
|
+
|
|
90
|
+
// Escape values for safe shell interpolation
|
|
91
|
+
const escapedProfile = shellEscape(profile);
|
|
92
|
+
const escapedSessionId = sessionId ? shellEscape(sessionId) : undefined;
|
|
93
|
+
|
|
94
|
+
let modifiedCommand: string;
|
|
95
|
+
|
|
96
|
+
// Check if AGENTUITY_PROFILE already exists (anywhere in the command)
|
|
97
|
+
if (/AGENTUITY_PROFILE=(?:'[^']*'|\S+)/.test(command)) {
|
|
98
|
+
// Replace all existing AGENTUITY_PROFILE occurrences to enforce our profile
|
|
99
|
+
modifiedCommand = command.replace(
|
|
100
|
+
/AGENTUITY_PROFILE=(?:'[^']*'|\S+)/g,
|
|
101
|
+
() => `AGENTUITY_PROFILE=${escapedProfile}`
|
|
102
|
+
);
|
|
103
|
+
// Add session ID and agent mode if not already present
|
|
104
|
+
if (escapedSessionId && !modifiedCommand.includes('AGENTUITY_OPENCODE_SESSION=')) {
|
|
105
|
+
modifiedCommand = `AGENTUITY_OPENCODE_SESSION=${escapedSessionId} ${modifiedCommand}`;
|
|
91
106
|
}
|
|
107
|
+
if (!modifiedCommand.includes('AGENTUITY_AGENT_MODE=')) {
|
|
108
|
+
modifiedCommand = `AGENTUITY_AGENT_MODE=opencode ${modifiedCommand}`;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Build environment variable prefix
|
|
112
|
+
let envVars = `AGENTUITY_PROFILE=${escapedProfile} AGENTUITY_AGENT_MODE=opencode`;
|
|
113
|
+
if (escapedSessionId) {
|
|
114
|
+
envVars += ` AGENTUITY_OPENCODE_SESSION=${escapedSessionId}`;
|
|
115
|
+
}
|
|
116
|
+
modifiedCommand = `${envVars} ${command}`;
|
|
117
|
+
}
|
|
92
118
|
setBashCommand(input, modifiedCommand);
|
|
93
119
|
|
|
94
120
|
// Show toast for cloud service usage
|