@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.
Files changed (138) hide show
  1. package/AGENTS.md +121 -13
  2. package/README.md +133 -12
  3. package/dist/agents/architect.d.ts +1 -1
  4. package/dist/agents/architect.d.ts.map +1 -1
  5. package/dist/agents/architect.js +2 -2
  6. package/dist/agents/builder.d.ts +1 -1
  7. package/dist/agents/builder.d.ts.map +1 -1
  8. package/dist/agents/builder.js +2 -2
  9. package/dist/agents/builder.js.map +1 -1
  10. package/dist/agents/expert-backend.d.ts +4 -0
  11. package/dist/agents/expert-backend.d.ts.map +1 -0
  12. package/dist/agents/expert-backend.js +493 -0
  13. package/dist/agents/expert-backend.js.map +1 -0
  14. package/dist/agents/expert-frontend.d.ts +4 -0
  15. package/dist/agents/expert-frontend.d.ts.map +1 -0
  16. package/dist/agents/expert-frontend.js +480 -0
  17. package/dist/agents/expert-frontend.js.map +1 -0
  18. package/dist/agents/expert-ops.d.ts +4 -0
  19. package/dist/agents/expert-ops.d.ts.map +1 -0
  20. package/dist/agents/expert-ops.js +375 -0
  21. package/dist/agents/expert-ops.js.map +1 -0
  22. package/dist/agents/expert.d.ts +1 -1
  23. package/dist/agents/expert.d.ts.map +1 -1
  24. package/dist/agents/expert.js +172 -913
  25. package/dist/agents/expert.js.map +1 -1
  26. package/dist/agents/index.d.ts.map +1 -1
  27. package/dist/agents/index.js +8 -2
  28. package/dist/agents/index.js.map +1 -1
  29. package/dist/agents/lead.d.ts +1 -1
  30. package/dist/agents/lead.d.ts.map +1 -1
  31. package/dist/agents/lead.js +359 -58
  32. package/dist/agents/lead.js.map +1 -1
  33. package/dist/agents/memory/entities.d.ts.map +1 -1
  34. package/dist/agents/memory/entities.js +5 -2
  35. package/dist/agents/memory/entities.js.map +1 -1
  36. package/dist/agents/memory.d.ts +1 -1
  37. package/dist/agents/memory.d.ts.map +1 -1
  38. package/dist/agents/memory.js +285 -10
  39. package/dist/agents/memory.js.map +1 -1
  40. package/dist/agents/monitor.d.ts +4 -0
  41. package/dist/agents/monitor.d.ts.map +1 -0
  42. package/dist/agents/monitor.js +106 -0
  43. package/dist/agents/monitor.js.map +1 -0
  44. package/dist/agents/product.d.ts +1 -1
  45. package/dist/agents/product.d.ts.map +1 -1
  46. package/dist/agents/product.js +161 -21
  47. package/dist/agents/product.js.map +1 -1
  48. package/dist/agents/reasoner.d.ts +1 -1
  49. package/dist/agents/reasoner.d.ts.map +1 -1
  50. package/dist/agents/reasoner.js +94 -11
  51. package/dist/agents/reasoner.js.map +1 -1
  52. package/dist/agents/scout.d.ts +1 -1
  53. package/dist/agents/scout.d.ts.map +1 -1
  54. package/dist/agents/scout.js +6 -4
  55. package/dist/agents/scout.js.map +1 -1
  56. package/dist/agents/types.d.ts +6 -0
  57. package/dist/agents/types.d.ts.map +1 -1
  58. package/dist/background/manager.d.ts +22 -1
  59. package/dist/background/manager.d.ts.map +1 -1
  60. package/dist/background/manager.js +218 -1
  61. package/dist/background/manager.js.map +1 -1
  62. package/dist/background/types.d.ts +19 -0
  63. package/dist/background/types.d.ts.map +1 -1
  64. package/dist/config/loader.d.ts +1 -1
  65. package/dist/config/loader.d.ts.map +1 -1
  66. package/dist/config/loader.js +10 -1
  67. package/dist/config/loader.js.map +1 -1
  68. package/dist/plugin/hooks/cadence.d.ts +2 -1
  69. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  70. package/dist/plugin/hooks/cadence.js +66 -3
  71. package/dist/plugin/hooks/cadence.js.map +1 -1
  72. package/dist/plugin/hooks/keyword.d.ts.map +1 -1
  73. package/dist/plugin/hooks/keyword.js +5 -3
  74. package/dist/plugin/hooks/keyword.js.map +1 -1
  75. package/dist/plugin/hooks/session-memory.d.ts +2 -1
  76. package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
  77. package/dist/plugin/hooks/session-memory.js +57 -5
  78. package/dist/plugin/hooks/session-memory.js.map +1 -1
  79. package/dist/plugin/hooks/tools.d.ts.map +1 -1
  80. package/dist/plugin/hooks/tools.js +28 -5
  81. package/dist/plugin/hooks/tools.js.map +1 -1
  82. package/dist/plugin/plugin.d.ts.map +1 -1
  83. package/dist/plugin/plugin.js +119 -68
  84. package/dist/plugin/plugin.js.map +1 -1
  85. package/dist/services/auth.d.ts.map +1 -1
  86. package/dist/services/auth.js +9 -0
  87. package/dist/services/auth.js.map +1 -1
  88. package/dist/tmux/executor.d.ts.map +1 -1
  89. package/dist/tmux/executor.js +13 -4
  90. package/dist/tmux/executor.js.map +1 -1
  91. package/dist/tools/background.d.ts +4 -1
  92. package/dist/tools/background.d.ts.map +1 -1
  93. package/dist/tools/index.d.ts +0 -1
  94. package/dist/tools/index.d.ts.map +1 -1
  95. package/dist/tools/index.js +0 -1
  96. package/dist/tools/index.js.map +1 -1
  97. package/dist/types.d.ts +4 -1
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +4 -1
  100. package/dist/types.js.map +1 -1
  101. package/package.json +3 -3
  102. package/src/agents/architect.ts +2 -2
  103. package/src/agents/builder.ts +2 -2
  104. package/src/agents/expert-backend.ts +495 -0
  105. package/src/agents/expert-frontend.ts +482 -0
  106. package/src/agents/expert-ops.ts +377 -0
  107. package/src/agents/expert.ts +172 -913
  108. package/src/agents/index.ts +8 -2
  109. package/src/agents/lead.ts +359 -58
  110. package/src/agents/memory/entities.ts +10 -2
  111. package/src/agents/memory.ts +285 -10
  112. package/src/agents/monitor.ts +108 -0
  113. package/src/agents/product.ts +161 -21
  114. package/src/agents/reasoner.ts +94 -11
  115. package/src/agents/scout.ts +6 -4
  116. package/src/agents/types.ts +6 -0
  117. package/src/background/manager.ts +259 -2
  118. package/src/background/types.ts +17 -0
  119. package/src/config/loader.ts +11 -1
  120. package/src/plugin/hooks/cadence.ts +79 -3
  121. package/src/plugin/hooks/keyword.ts +5 -3
  122. package/src/plugin/hooks/session-memory.ts +68 -6
  123. package/src/plugin/hooks/tools.ts +40 -14
  124. package/src/plugin/plugin.ts +128 -70
  125. package/src/services/auth.ts +10 -0
  126. package/src/tmux/executor.ts +13 -4
  127. package/src/tools/index.ts +0 -1
  128. package/src/types.ts +4 -1
  129. package/dist/agents/planner.d.ts +0 -4
  130. package/dist/agents/planner.d.ts.map +0 -1
  131. package/dist/agents/planner.js +0 -158
  132. package/dist/agents/planner.js.map +0 -1
  133. package/dist/tools/delegate.d.ts +0 -45
  134. package/dist/tools/delegate.d.ts.map +0 -1
  135. package/dist/tools/delegate.js +0 -72
  136. package/dist/tools/delegate.js.map +0 -1
  137. package/src/agents/planner.ts +0 -161
  138. 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 { BackgroundTask, BackgroundTaskConfig, LaunchInput, TaskProgress } from './types';
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: task.description,
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
 
@@ -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
+ }
@@ -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.2-codex",
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(ctx: PluginInput, _config: CoderConfig): CadenceHooks {
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
- After compaction, Memory will save this summary and update the cadence state.
288
- Then Lead will continue the loop from iteration ${state.iteration}.
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. Have @Agentuity Coder Reviewer check @Agentuity Coder Builder's work
38
- 4. Use cloud services only when they genuinely help
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. Append this compaction summary to the \`compactions\` array with timestamp
68
- 3. Save back: \`agentuity cloud kv set agentuity-opencode-memory "session:${sessionId}" '{...}' --region use\`
69
- 4. Upsert to Vector for semantic search: \`agentuity cloud vector upsert agentuity-opencode-sessions "session:${sessionId}" --document "..." --metadata '...' --region use\`
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
- After compaction, Memory will automatically save this summary for future recovery.
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
- // Inject AGENTUITY_PROFILE environment variable
78
- const profile = getCoderProfile();
79
- let modifiedCommand: string;
80
-
81
- // Check if AGENTUITY_PROFILE already exists (anywhere in the command)
82
- if (/AGENTUITY_PROFILE=\S+/.test(command)) {
83
- // Replace all existing AGENTUITY_PROFILE occurrences to enforce our profile
84
- modifiedCommand = command.replace(
85
- /AGENTUITY_PROFILE=\S+/g,
86
- `AGENTUITY_PROFILE=${profile}`
87
- );
88
- } else {
89
- // Prepend AGENTUITY_PROFILE
90
- modifiedCommand = `AGENTUITY_PROFILE=${profile} ${command}`;
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