@happycastle/oh-my-openclaw 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/helpers/mock-factory.js +5 -0
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +61 -1
- package/dist/hooks/subagent-tracker.d.ts +8 -0
- package/dist/hooks/subagent-tracker.js +62 -0
- package/dist/hooks/todo-reminder.js +6 -0
- package/dist/index.js +18 -0
- package/dist/services/webhook-bridge.d.ts +13 -0
- package/dist/services/webhook-bridge.js +121 -0
- package/dist/tools/omo-delegation.d.ts +2 -0
- package/dist/tools/omo-delegation.js +63 -0
- package/dist/tools/task-delegation.js +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/utils/config.js +5 -0
- package/dist/utils/webhook-client.d.ts +21 -0
- package/dist/utils/webhook-client.js +48 -0
- package/package.json +1 -1
- package/skills/opencode-controller.md +192 -96
|
@@ -49,6 +49,11 @@ export function createMockConfig(overrides) {
|
|
|
49
49
|
checkpoint_dir: 'workspace/checkpoints',
|
|
50
50
|
tmux_socket: '/tmp/openclaw-tmux-sockets/openclaw.sock',
|
|
51
51
|
model_routing: undefined,
|
|
52
|
+
webhook_bridge_enabled: false,
|
|
53
|
+
gateway_url: 'http://127.0.0.1:18789',
|
|
54
|
+
hooks_token: '',
|
|
55
|
+
webhook_reminder_interval_ms: 300000,
|
|
56
|
+
webhook_subagent_stale_threshold_ms: 600000,
|
|
52
57
|
...overrides,
|
|
53
58
|
};
|
|
54
59
|
}
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -41,12 +41,16 @@ export declare function mergeAgentConfigs(existing: Array<{
|
|
|
41
41
|
result: MergeResult;
|
|
42
42
|
};
|
|
43
43
|
export declare function applyProviderToConfigs(configs: OmocAgentConfig[], provider: string): OmocAgentConfig[];
|
|
44
|
+
export declare function readExistingHooksToken(configPath: string): string | undefined;
|
|
45
|
+
export declare function generateHooksToken(): string;
|
|
44
46
|
export interface InteractiveSetupResult {
|
|
45
47
|
provider: string;
|
|
46
48
|
setupMcporter: boolean;
|
|
47
49
|
excludeServers: string[];
|
|
48
50
|
enableTodoEnforcer: boolean;
|
|
49
51
|
enablePlannerGuard: boolean;
|
|
52
|
+
enableWebhookBridge: boolean;
|
|
53
|
+
webhookHooksToken: string;
|
|
50
54
|
}
|
|
51
55
|
export declare function runInteractiveSetup(logger: Logger): Promise<InteractiveSetupResult>;
|
|
52
56
|
export interface SetupOptions {
|
|
@@ -60,6 +64,8 @@ export interface SetupOptions {
|
|
|
60
64
|
excludeServers?: string[];
|
|
61
65
|
enableTodoEnforcer?: boolean;
|
|
62
66
|
enablePlannerGuard?: boolean;
|
|
67
|
+
enableWebhookBridge?: boolean;
|
|
68
|
+
webhookHooksToken?: string;
|
|
63
69
|
interactive?: boolean;
|
|
64
70
|
logger: Logger;
|
|
65
71
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import * as readline from 'node:readline';
|
|
@@ -174,6 +175,20 @@ async function runCustomProviderFlow(rl, logger) {
|
|
|
174
175
|
registerCustomPreset(customName, customPreset);
|
|
175
176
|
return customName;
|
|
176
177
|
}
|
|
178
|
+
export function readExistingHooksToken(configPath) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
181
|
+
const config = JSON5.parse(raw);
|
|
182
|
+
const hooks = config.hooks;
|
|
183
|
+
return hooks?.token && typeof hooks.token === 'string' ? hooks.token : undefined;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function generateHooksToken() {
|
|
190
|
+
return crypto.randomBytes(32).toString('hex');
|
|
191
|
+
}
|
|
177
192
|
export async function runInteractiveSetup(logger) {
|
|
178
193
|
const rl = readline.createInterface({
|
|
179
194
|
input: process.stdin,
|
|
@@ -185,6 +200,8 @@ export async function runInteractiveSetup(logger) {
|
|
|
185
200
|
excludeServers: [],
|
|
186
201
|
enableTodoEnforcer: false,
|
|
187
202
|
enablePlannerGuard: false,
|
|
203
|
+
enableWebhookBridge: false,
|
|
204
|
+
webhookHooksToken: '',
|
|
188
205
|
};
|
|
189
206
|
try {
|
|
190
207
|
logger.info('');
|
|
@@ -257,8 +274,26 @@ export async function runInteractiveSetup(logger) {
|
|
|
257
274
|
const enableTodoEnforcer = todoAnswer.toLowerCase() !== 'n' && todoAnswer.toLowerCase() !== 'no';
|
|
258
275
|
const guardAnswer = await askQuestion(rl, ' Enable planner guard (prevents prometheus from editing code)? (Y/n): ');
|
|
259
276
|
const enablePlannerGuard = guardAnswer.toLowerCase() !== 'n' && guardAnswer.toLowerCase() !== 'no';
|
|
277
|
+
const webhookAnswer = await askQuestion(rl, ' Enable webhook bridge (proactive agent reminders via hooks/wake)? (Y/n): ');
|
|
278
|
+
const enableWebhookBridge = webhookAnswer.toLowerCase() !== 'n' && webhookAnswer.toLowerCase() !== 'no';
|
|
279
|
+
let webhookHooksToken = '';
|
|
280
|
+
if (enableWebhookBridge) {
|
|
281
|
+
const configPath = findConfigPath();
|
|
282
|
+
const existingToken = configPath ? readExistingHooksToken(configPath) : undefined;
|
|
283
|
+
if (existingToken) {
|
|
284
|
+
logger.info(` Found existing hooks.token in config`);
|
|
285
|
+
const reuseAnswer = await askQuestion(rl, ' Reuse existing hooks token? (Y/n): ');
|
|
286
|
+
if (reuseAnswer.toLowerCase() !== 'n' && reuseAnswer.toLowerCase() !== 'no') {
|
|
287
|
+
webhookHooksToken = existingToken;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!webhookHooksToken) {
|
|
291
|
+
webhookHooksToken = generateHooksToken();
|
|
292
|
+
logger.info(` Generated new hooks token`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
260
295
|
logger.info('');
|
|
261
|
-
return { provider, setupMcporter, excludeServers, enableTodoEnforcer, enablePlannerGuard };
|
|
296
|
+
return { provider, setupMcporter, excludeServers, enableTodoEnforcer, enablePlannerGuard, enableWebhookBridge, webhookHooksToken };
|
|
262
297
|
}
|
|
263
298
|
finally {
|
|
264
299
|
rl.close();
|
|
@@ -349,6 +384,25 @@ export function runSetup(options) {
|
|
|
349
384
|
}
|
|
350
385
|
logger.info(`Todo enforcer: ${options.enableTodoEnforcer ? 'enabled' : 'disabled'}`);
|
|
351
386
|
}
|
|
387
|
+
if (options.enableWebhookBridge && options.webhookHooksToken) {
|
|
388
|
+
const pluginSettings = (config.pluginSettings ?? {});
|
|
389
|
+
if (!pluginSettings['oh-my-openclaw']) {
|
|
390
|
+
pluginSettings['oh-my-openclaw'] = {};
|
|
391
|
+
}
|
|
392
|
+
pluginSettings['oh-my-openclaw']['webhook_bridge_enabled'] = true;
|
|
393
|
+
pluginSettings['oh-my-openclaw']['hooks_token'] = options.webhookHooksToken;
|
|
394
|
+
config.pluginSettings = pluginSettings;
|
|
395
|
+
const hooksSection = (config.hooks ?? {});
|
|
396
|
+
hooksSection.enabled = true;
|
|
397
|
+
if (!hooksSection.token) {
|
|
398
|
+
hooksSection.token = options.webhookHooksToken;
|
|
399
|
+
}
|
|
400
|
+
config.hooks = hooksSection;
|
|
401
|
+
if (!dryRun) {
|
|
402
|
+
fs.writeFileSync(configPath, serializeConfig(config), 'utf-8');
|
|
403
|
+
}
|
|
404
|
+
logger.info('Webhook bridge enabled with hooks token configured');
|
|
405
|
+
}
|
|
352
406
|
if (options.setupMcporter) {
|
|
353
407
|
logger.info('');
|
|
354
408
|
logger.info('Setting up mcporter MCP servers...');
|
|
@@ -383,6 +437,8 @@ export function registerSetupCli(ctx) {
|
|
|
383
437
|
let excludeServers = [];
|
|
384
438
|
let enableTodoEnforcer;
|
|
385
439
|
let enablePlannerGuard;
|
|
440
|
+
let enableWebhookBridge;
|
|
441
|
+
let webhookHooksToken;
|
|
386
442
|
if (!provider && process.stdin.isTTY) {
|
|
387
443
|
const result = await runInteractiveSetup(ctx.logger);
|
|
388
444
|
if (!result.provider)
|
|
@@ -392,6 +448,8 @@ export function registerSetupCli(ctx) {
|
|
|
392
448
|
excludeServers = result.excludeServers;
|
|
393
449
|
enableTodoEnforcer = result.enableTodoEnforcer;
|
|
394
450
|
enablePlannerGuard = result.enablePlannerGuard;
|
|
451
|
+
enableWebhookBridge = result.enableWebhookBridge;
|
|
452
|
+
webhookHooksToken = result.webhookHooksToken;
|
|
395
453
|
}
|
|
396
454
|
runSetup({
|
|
397
455
|
configPath: opts.config,
|
|
@@ -403,6 +461,8 @@ export function registerSetupCli(ctx) {
|
|
|
403
461
|
excludeServers,
|
|
404
462
|
enableTodoEnforcer,
|
|
405
463
|
enablePlannerGuard,
|
|
464
|
+
enableWebhookBridge,
|
|
465
|
+
webhookHooksToken,
|
|
406
466
|
logger: ctx.logger,
|
|
407
467
|
});
|
|
408
468
|
ctx.logger.info('');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OmocPluginApi } from '../types.js';
|
|
2
|
+
declare function extractSpawnResult(content: string): {
|
|
3
|
+
runId: string;
|
|
4
|
+
childSessionKey: string;
|
|
5
|
+
task: string;
|
|
6
|
+
} | null;
|
|
7
|
+
export declare function registerSubagentTracker(api: OmocPluginApi): void;
|
|
8
|
+
export { extractSpawnResult };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
+
import { trackSubagentSpawn, clearSubagentTracking } from '../services/webhook-bridge.js';
|
|
3
|
+
const SPAWN_TOOL_NAME = 'sessions_spawn';
|
|
4
|
+
function extractSpawnResult(content) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(content);
|
|
7
|
+
if (parsed.status === 'accepted' && parsed.runId && parsed.childSessionKey) {
|
|
8
|
+
return {
|
|
9
|
+
runId: parsed.runId,
|
|
10
|
+
childSessionKey: parsed.childSessionKey,
|
|
11
|
+
task: parsed.task ?? '',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
const runIdMatch = content.match(/runId["\s:]+["']?([a-zA-Z0-9_-]+)/);
|
|
17
|
+
const sessionKeyMatch = content.match(/childSessionKey["\s:]+["']?([a-zA-Z0-9:_-]+)/);
|
|
18
|
+
if (runIdMatch && sessionKeyMatch) {
|
|
19
|
+
return {
|
|
20
|
+
runId: runIdMatch[1],
|
|
21
|
+
childSessionKey: sessionKeyMatch[1],
|
|
22
|
+
task: '',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function registerSubagentTracker(api) {
|
|
29
|
+
api.registerHook('tool_result_persist', (payload) => {
|
|
30
|
+
if (payload.tool !== SPAWN_TOOL_NAME)
|
|
31
|
+
return undefined;
|
|
32
|
+
const content = typeof payload.content === 'string' ? payload.content : '';
|
|
33
|
+
const spawnResult = extractSpawnResult(content);
|
|
34
|
+
if (spawnResult) {
|
|
35
|
+
trackSubagentSpawn({
|
|
36
|
+
...spawnResult,
|
|
37
|
+
spawnedAt: Date.now(),
|
|
38
|
+
});
|
|
39
|
+
api.logger.info(`${LOG_PREFIX} Tracking sub-agent spawn: runId=${spawnResult.runId}`);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}, {
|
|
43
|
+
name: 'oh-my-openclaw.subagent-tracker',
|
|
44
|
+
description: 'Tracks sessions_spawn results for stale sub-agent detection',
|
|
45
|
+
});
|
|
46
|
+
api.registerHook('message:received', (context) => {
|
|
47
|
+
const content = context?.content ?? '';
|
|
48
|
+
if (!content.includes('Sub-agent') && !content.includes('subagent') && !content.includes('announce')) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const runIdMatch = content.match(/runId["\s:=]+["']?([a-zA-Z0-9_-]+)/);
|
|
52
|
+
if (runIdMatch) {
|
|
53
|
+
clearSubagentTracking(runIdMatch[1]);
|
|
54
|
+
api.logger.info(`${LOG_PREFIX} Cleared sub-agent tracking: runId=${runIdMatch[1]} (announce received)`);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}, {
|
|
58
|
+
name: 'oh-my-openclaw.subagent-announce-detector',
|
|
59
|
+
description: 'Detects sub-agent announce messages and clears stale tracking',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export { extractSpawnResult };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { TOOL_PREFIX, LOG_PREFIX } from '../constants.js';
|
|
2
2
|
import { getIncompleteTodos, resetStore } from '../tools/todo/store.js';
|
|
3
|
+
import { getConfig } from '../utils/config.js';
|
|
4
|
+
import { callHooksWake } from '../utils/webhook-client.js';
|
|
3
5
|
const TODO_TOOL_NAMES = new Set([
|
|
4
6
|
`${TOOL_PREFIX}todo_create`,
|
|
5
7
|
`${TOOL_PREFIX}todo_list`,
|
|
@@ -59,6 +61,10 @@ export function registerAgentEndReminder(api) {
|
|
|
59
61
|
if (sessionKey) {
|
|
60
62
|
api.runtime.system.enqueueSystemEvent(warning, { sessionKey });
|
|
61
63
|
}
|
|
64
|
+
const config = getConfig(api);
|
|
65
|
+
if (config.webhook_bridge_enabled && config.hooks_token) {
|
|
66
|
+
callHooksWake(`⚠️ Agent ended with ${incomplete.length} incomplete todo(s). Resume work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger).catch(() => { });
|
|
67
|
+
}
|
|
62
68
|
api.logger.warn(`${LOG_PREFIX} Agent ended with ${incomplete.length} incomplete todo(s)`);
|
|
63
69
|
}
|
|
64
70
|
catch {
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,10 @@ import { registerCommentChecker } from './hooks/comment-checker.js';
|
|
|
7
7
|
import { registerMessageMonitor } from './hooks/message-monitor.js';
|
|
8
8
|
import { registerStartupHook } from './hooks/startup.js';
|
|
9
9
|
import { registerRalphLoop } from './services/ralph-loop.js';
|
|
10
|
+
import { registerWebhookBridge } from './services/webhook-bridge.js';
|
|
11
|
+
import { registerSubagentTracker } from './hooks/subagent-tracker.js';
|
|
10
12
|
import { registerDelegateTool } from './tools/task-delegation.js';
|
|
13
|
+
import { registerOmoDelegateTool } from './tools/omo-delegation.js';
|
|
11
14
|
import { registerLookAtTool } from './tools/look-at.js';
|
|
12
15
|
import { registerCheckpointTool } from './tools/checkpoint.js';
|
|
13
16
|
import { registerWebSearchTool } from './tools/web-search.js';
|
|
@@ -127,11 +130,26 @@ export default function register(api) {
|
|
|
127
130
|
registry.services.push('ralph-loop');
|
|
128
131
|
api.logger.info(`[${PLUGIN_ID}] Ralph Loop service registered`);
|
|
129
132
|
});
|
|
133
|
+
safeRegister(api, 'webhook-bridge', 'service', () => {
|
|
134
|
+
registerWebhookBridge(api);
|
|
135
|
+
registry.services.push('webhook-bridge');
|
|
136
|
+
api.logger.info(`[${PLUGIN_ID}] Webhook Bridge service registered (enabled: ${config.webhook_bridge_enabled})`);
|
|
137
|
+
});
|
|
138
|
+
safeRegister(api, 'subagent-tracker', 'hook', () => {
|
|
139
|
+
registerSubagentTracker(guarded);
|
|
140
|
+
registry.hooks.push('subagent-tracker', 'subagent-announce-detector');
|
|
141
|
+
api.logger.info(`[${PLUGIN_ID}] Sub-agent tracker hooks registered`);
|
|
142
|
+
});
|
|
130
143
|
safeRegister(api, 'omoc_delegate', 'tool', () => {
|
|
131
144
|
registerDelegateTool(api);
|
|
132
145
|
registry.tools.push('omoc_delegate');
|
|
133
146
|
api.logger.info(`[${PLUGIN_ID}] Delegate tool registered`);
|
|
134
147
|
});
|
|
148
|
+
safeRegister(api, 'omo_delegate', 'tool', () => {
|
|
149
|
+
registerOmoDelegateTool(api);
|
|
150
|
+
registry.tools.push('omo_delegate');
|
|
151
|
+
api.logger.info(`[${PLUGIN_ID}] OmO Delegate tool registered (ACP)`);
|
|
152
|
+
});
|
|
135
153
|
safeRegister(api, 'omoc_look_at', 'tool', () => {
|
|
136
154
|
registerLookAtTool(api);
|
|
137
155
|
registry.tools.push('omoc_look_at');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OmocPluginApi } from '../types.js';
|
|
2
|
+
interface TrackedSubagent {
|
|
3
|
+
runId: string;
|
|
4
|
+
childSessionKey: string;
|
|
5
|
+
task: string;
|
|
6
|
+
spawnedAt: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function trackSubagentSpawn(entry: TrackedSubagent): void;
|
|
9
|
+
export declare function clearSubagentTracking(runId: string): void;
|
|
10
|
+
export declare function getTrackedSubagents(): ReadonlyMap<string, TrackedSubagent>;
|
|
11
|
+
export declare function resetWebhookBridgeState(): void;
|
|
12
|
+
export declare function registerWebhookBridge(api: OmocPluginApi): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LOG_PREFIX, TOOL_PREFIX } from '../constants.js';
|
|
2
|
+
import { getConfig } from '../utils/config.js';
|
|
3
|
+
import { getIncompleteTodos } from '../tools/todo/store.js';
|
|
4
|
+
import { callHooksAgent, callHooksWake } from '../utils/webhook-client.js';
|
|
5
|
+
const trackedSubagents = new Map();
|
|
6
|
+
let reminderTimer = null;
|
|
7
|
+
export function trackSubagentSpawn(entry) {
|
|
8
|
+
trackedSubagents.set(entry.runId, entry);
|
|
9
|
+
}
|
|
10
|
+
export function clearSubagentTracking(runId) {
|
|
11
|
+
trackedSubagents.delete(runId);
|
|
12
|
+
}
|
|
13
|
+
export function getTrackedSubagents() {
|
|
14
|
+
return trackedSubagents;
|
|
15
|
+
}
|
|
16
|
+
export function resetWebhookBridgeState() {
|
|
17
|
+
trackedSubagents.clear();
|
|
18
|
+
if (reminderTimer) {
|
|
19
|
+
clearInterval(reminderTimer);
|
|
20
|
+
reminderTimer = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function buildWebhookConfig(api) {
|
|
24
|
+
const config = getConfig(api);
|
|
25
|
+
return {
|
|
26
|
+
gateway_url: config.gateway_url,
|
|
27
|
+
hooks_token: config.hooks_token,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function checkIncompleteTodos(api) {
|
|
31
|
+
const webhookConfig = buildWebhookConfig(api);
|
|
32
|
+
const config = getConfig(api);
|
|
33
|
+
const allSessionKeys = ['__default__', 'agent:main:main'];
|
|
34
|
+
let totalIncomplete = 0;
|
|
35
|
+
const summaryParts = [];
|
|
36
|
+
for (const sessionKey of allSessionKeys) {
|
|
37
|
+
const incomplete = getIncompleteTodos(sessionKey);
|
|
38
|
+
if (incomplete.length > 0) {
|
|
39
|
+
totalIncomplete += incomplete.length;
|
|
40
|
+
const items = incomplete.map((t) => ` - [${t.status}] ${t.content}`).join('\n');
|
|
41
|
+
summaryParts.push(items);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (totalIncomplete === 0)
|
|
45
|
+
return;
|
|
46
|
+
const summary = summaryParts.join('\n');
|
|
47
|
+
const message = `[OmOC Periodic Reminder] You have ${totalIncomplete} incomplete todo(s):\n${summary}\n\n` +
|
|
48
|
+
`Review with \`${TOOL_PREFIX}todo_list\` and resume work. ` +
|
|
49
|
+
`If blocked, update todo status. If all done, mark them complete.`;
|
|
50
|
+
const result = await callHooksAgent(message, webhookConfig, {
|
|
51
|
+
name: 'OmOC-TodoReminder',
|
|
52
|
+
deliver: false,
|
|
53
|
+
}, api.logger);
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
api.logger.info(`${LOG_PREFIX} Periodic todo reminder sent via hooks/agent (${totalIncomplete} incomplete)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function checkStaleSubagents(api) {
|
|
59
|
+
const webhookConfig = buildWebhookConfig(api);
|
|
60
|
+
const config = getConfig(api);
|
|
61
|
+
const threshold = config.webhook_subagent_stale_threshold_ms;
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const stale = [];
|
|
64
|
+
for (const entry of trackedSubagents.values()) {
|
|
65
|
+
if (now - entry.spawnedAt > threshold) {
|
|
66
|
+
stale.push(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (stale.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
const details = stale
|
|
72
|
+
.map((s) => ` - runId=${s.runId} task="${s.task.substring(0, 80)}" (${Math.round((now - s.spawnedAt) / 60000)}m ago)`)
|
|
73
|
+
.join('\n');
|
|
74
|
+
const message = `[OmOC Sub-agent Alert] ${stale.length} sub-agent(s) may have completed without announce:\n${details}\n\n` +
|
|
75
|
+
`Check sub-agent status with \`/subagents list\` or \`/subagents info <id>\`. ` +
|
|
76
|
+
`If completed, collect results and proceed. If still running, wait.`;
|
|
77
|
+
const result = await callHooksWake(message, webhookConfig, api.logger);
|
|
78
|
+
if (result.ok) {
|
|
79
|
+
api.logger.info(`${LOG_PREFIX} Stale sub-agent alert sent via hooks/wake (${stale.length} stale)`);
|
|
80
|
+
for (const s of stale) {
|
|
81
|
+
trackedSubagents.delete(s.runId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function registerWebhookBridge(api) {
|
|
86
|
+
const config = getConfig(api);
|
|
87
|
+
api.registerService({
|
|
88
|
+
id: 'omoc-webhook-bridge',
|
|
89
|
+
name: 'Webhook Bridge Service',
|
|
90
|
+
description: 'Proactive agent messaging via Gateway webhook hooks',
|
|
91
|
+
start: async () => {
|
|
92
|
+
if (!config.webhook_bridge_enabled) {
|
|
93
|
+
api.logger.info(`${LOG_PREFIX} Webhook bridge disabled (set webhook_bridge_enabled: true to enable)`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!config.hooks_token) {
|
|
97
|
+
api.logger.warn(`${LOG_PREFIX} Webhook bridge enabled but hooks_token is empty — skipping`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const intervalMs = Math.max(config.webhook_reminder_interval_ms, 30_000);
|
|
101
|
+
reminderTimer = setInterval(async () => {
|
|
102
|
+
try {
|
|
103
|
+
await checkIncompleteTodos(api);
|
|
104
|
+
await checkStaleSubagents(api);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
api.logger.warn(`${LOG_PREFIX} Webhook bridge tick error:`, err);
|
|
108
|
+
}
|
|
109
|
+
}, intervalMs);
|
|
110
|
+
api.logger.info(`${LOG_PREFIX} Webhook bridge started (interval=${intervalMs}ms, stale_threshold=${config.webhook_subagent_stale_threshold_ms}ms)`);
|
|
111
|
+
},
|
|
112
|
+
stop: async () => {
|
|
113
|
+
if (reminderTimer) {
|
|
114
|
+
clearInterval(reminderTimer);
|
|
115
|
+
reminderTimer = null;
|
|
116
|
+
}
|
|
117
|
+
trackedSubagents.clear();
|
|
118
|
+
api.logger.info(`${LOG_PREFIX} Webhook bridge stopped`);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { TOOL_PREFIX } from '../types.js';
|
|
3
|
+
import { LOG_PREFIX } from '../constants.js';
|
|
4
|
+
import { toolResponse, toolError } from '../utils/helpers.js';
|
|
5
|
+
const VALID_ACP_AGENTS = ['opencode', 'codex', 'claude', 'gemini', 'pi'];
|
|
6
|
+
const OmoDelegateParamsSchema = Type.Object({
|
|
7
|
+
task: Type.String({ description: 'What OmO (OpenCode) should do — the coding task description. Use @agentname prefix to invoke OpenCode subagents (e.g., "@explore find auth files").' }),
|
|
8
|
+
agent: Type.Optional(Type.String({ description: 'ACP harness agent ID (default: "opencode"). Valid: opencode, codex, claude, gemini, pi' })),
|
|
9
|
+
opencode_agent: Type.Optional(Type.String({ description: 'OpenCode internal agent mode (e.g., "build", "plan", or custom agent name). Only applies when agent is "opencode". Defaults to OpenCode\'s configured primary agent. Uses ACP session mode switching.' })),
|
|
10
|
+
model: Type.Optional(Type.String({ description: 'Override model — only when you need a specific model. Leave empty to use OpenCode\'s own configured default.' })),
|
|
11
|
+
thread: Type.Optional(Type.Boolean({ description: 'Bind to a thread for persistent multi-turn session (default: false)', default: false })),
|
|
12
|
+
label: Type.Optional(Type.String({ description: 'Label for easy identification in /subagents list and /acp sessions' })),
|
|
13
|
+
cwd: Type.Optional(Type.String({ description: 'Working directory for the ACP session' })),
|
|
14
|
+
});
|
|
15
|
+
export function registerOmoDelegateTool(api) {
|
|
16
|
+
api.registerTool({
|
|
17
|
+
name: `${TOOL_PREFIX.replace('omoc_', 'omo_')}delegate`,
|
|
18
|
+
description: 'Delegate a coding task to OmO (OpenCode) via ACP session. Use this for coding work — implementation, bug fixes, refactoring, testing.',
|
|
19
|
+
parameters: OmoDelegateParamsSchema,
|
|
20
|
+
execute: async (_toolCallId, params) => {
|
|
21
|
+
if (!params.task?.trim()) {
|
|
22
|
+
return toolError('Task is required and cannot be empty');
|
|
23
|
+
}
|
|
24
|
+
if (params.task.length > 10000) {
|
|
25
|
+
return toolError('Task too long (max 10000 chars)');
|
|
26
|
+
}
|
|
27
|
+
const agent = params.agent || 'opencode';
|
|
28
|
+
if (!VALID_ACP_AGENTS.includes(agent)) {
|
|
29
|
+
return toolError(`Invalid ACP agent: ${agent}. Valid: ${VALID_ACP_AGENTS.join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
const sessionMode = params.thread ? 'session' : 'run';
|
|
32
|
+
api.logger.info(`${LOG_PREFIX} OmO delegation:`, { agent, opencode_agent: params.opencode_agent, model: params.model, thread: params.thread });
|
|
33
|
+
const instruction = [
|
|
34
|
+
`OmO Delegation → ACP runtime → harness "${agent}"`,
|
|
35
|
+
'',
|
|
36
|
+
'⚡ NOW CALL sessions_spawn with these parameters:',
|
|
37
|
+
` task: "${params.task}"`,
|
|
38
|
+
` runtime: "acp"`,
|
|
39
|
+
` agentId: "${agent}"`,
|
|
40
|
+
` mode: "${sessionMode}"`,
|
|
41
|
+
params.model ? ` model: "${params.model}"` : '',
|
|
42
|
+
params.thread ? ' thread: true' : '',
|
|
43
|
+
params.label ? ` label: "${params.label}"` : '',
|
|
44
|
+
params.cwd ? ` cwd: "${params.cwd}"` : '',
|
|
45
|
+
'',
|
|
46
|
+
'Do NOT just return this metadata. Actually call sessions_spawn NOW.',
|
|
47
|
+
params.opencode_agent ? [
|
|
48
|
+
'',
|
|
49
|
+
`🔀 AFTER session is created, switch OpenCode agent mode to "${params.opencode_agent}":`,
|
|
50
|
+
` Use ACP session mode switching (setSessionMode) to select "${params.opencode_agent}"`,
|
|
51
|
+
' Available modes are returned in the session creation response',
|
|
52
|
+
].join('\n') : '',
|
|
53
|
+
'',
|
|
54
|
+
'⚠️ AFTER the ACP session completes:',
|
|
55
|
+
' 1. Check the announce result immediately',
|
|
56
|
+
' 2. Verify with git status/diff',
|
|
57
|
+
' 3. Proceed to next task — do NOT stop',
|
|
58
|
+
].filter(Boolean).join('\n');
|
|
59
|
+
return toolResponse(instruction);
|
|
60
|
+
},
|
|
61
|
+
optional: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -45,7 +45,7 @@ function getModelForCategory(category, api) {
|
|
|
45
45
|
export function registerDelegateTool(api) {
|
|
46
46
|
api.registerTool({
|
|
47
47
|
name: `${TOOL_PREFIX}delegate`,
|
|
48
|
-
description: 'Delegate a task to
|
|
48
|
+
description: 'Delegate a task to an OpenClaw-native sub-agent with category-based model routing',
|
|
49
49
|
parameters: DelegateParamsSchema,
|
|
50
50
|
execute: async (_toolCallId, params) => {
|
|
51
51
|
const validCategories = Object.keys(DEFAULT_CATEGORY_MODELS);
|
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface PluginConfig {
|
|
|
13
13
|
model: string;
|
|
14
14
|
alternatives?: string[];
|
|
15
15
|
}>>;
|
|
16
|
+
webhook_bridge_enabled: boolean;
|
|
17
|
+
gateway_url: string;
|
|
18
|
+
hooks_token: string;
|
|
19
|
+
webhook_reminder_interval_ms: number;
|
|
20
|
+
webhook_subagent_stale_threshold_ms: number;
|
|
16
21
|
}
|
|
17
22
|
export interface RalphLoopState {
|
|
18
23
|
active: boolean;
|
package/dist/utils/config.js
CHANGED
|
@@ -14,6 +14,11 @@ export function getConfig(api) {
|
|
|
14
14
|
checkpoint_dir: join(wsDir, 'checkpoints'),
|
|
15
15
|
tmux_socket: '/tmp/openclaw-tmux-sockets/openclaw.sock',
|
|
16
16
|
model_routing: undefined,
|
|
17
|
+
webhook_bridge_enabled: false,
|
|
18
|
+
gateway_url: process.env.OPENCLAW_GATEWAY_URL ?? 'http://127.0.0.1:18789',
|
|
19
|
+
hooks_token: process.env.OPENCLAW_HOOKS_TOKEN ?? '',
|
|
20
|
+
webhook_reminder_interval_ms: 5 * 60 * 1000, // 5 minutes
|
|
21
|
+
webhook_subagent_stale_threshold_ms: 10 * 60 * 1000, // 10 minutes
|
|
17
22
|
};
|
|
18
23
|
const config = { ...defaults, ...(api.pluginConfig ?? api.config) };
|
|
19
24
|
const validation = validateConfig(config);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WebhookConfig {
|
|
2
|
+
gateway_url: string;
|
|
3
|
+
hooks_token: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HooksAgentOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
agentId?: string;
|
|
8
|
+
sessionKey?: string;
|
|
9
|
+
deliver?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface WebhookResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
status?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function callHooksWake(text: string, config: WebhookConfig, logger?: {
|
|
17
|
+
warn: (...args: unknown[]) => void;
|
|
18
|
+
}): Promise<WebhookResult>;
|
|
19
|
+
export declare function callHooksAgent(message: string, config: WebhookConfig, options?: HooksAgentOptions, logger?: {
|
|
20
|
+
warn: (...args: unknown[]) => void;
|
|
21
|
+
}): Promise<WebhookResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
+
export async function callHooksWake(text, config, logger) {
|
|
3
|
+
if (!config.hooks_token) {
|
|
4
|
+
return { ok: false, error: 'hooks_token not configured' };
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(`${config.gateway_url}/hooks/wake`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
'Authorization': `Bearer ${config.hooks_token}`,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({ text, mode: 'now' }),
|
|
14
|
+
});
|
|
15
|
+
return { ok: res.ok, status: res.status };
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19
|
+
logger?.warn(`${LOG_PREFIX} hooks/wake failed: ${msg}`);
|
|
20
|
+
return { ok: false, error: msg };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function callHooksAgent(message, config, options, logger) {
|
|
24
|
+
if (!config.hooks_token) {
|
|
25
|
+
return { ok: false, error: 'hooks_token not configured' };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const payload = {
|
|
29
|
+
message,
|
|
30
|
+
wakeMode: 'now',
|
|
31
|
+
...options,
|
|
32
|
+
};
|
|
33
|
+
const res = await fetch(`${config.gateway_url}/hooks/agent`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Authorization': `Bearer ${config.hooks_token}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(payload),
|
|
40
|
+
});
|
|
41
|
+
return { ok: res.ok, status: res.status };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
logger?.warn(`${LOG_PREFIX} hooks/agent failed: ${msg}`);
|
|
46
|
+
return { ok: false, error: msg };
|
|
47
|
+
}
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,125 +1,145 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencode-controller
|
|
3
|
-
description: Control OpenCode sessions via
|
|
3
|
+
description: Control OpenCode sessions via ACP (Agent Client Protocol). Includes session management, model selection, and OmO delegation patterns.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# opencode-controller — OpenCode Session Control
|
|
6
|
+
# opencode-controller — OpenCode Session Control (ACP)
|
|
7
7
|
|
|
8
|
-
OpenClaw is not a direct code executor, but an orchestrator that delegates work to OpenCode, collects results, and verifies them.
|
|
8
|
+
OpenClaw is not a direct code executor, but an orchestrator that delegates work to OpenCode via ACP, collects results, and verifies them.
|
|
9
9
|
|
|
10
10
|
## Core Principles
|
|
11
11
|
|
|
12
12
|
- OpenClaw does not write code directly
|
|
13
|
-
- Coding tasks are delegated to
|
|
13
|
+
- Coding tasks are delegated to OpenCode via ACP sessions (`runtime: "acp"`, `agentId: "opencode"`)
|
|
14
14
|
- OpenClaw is responsible for task decomposition, instruction, monitoring, and result verification
|
|
15
15
|
|
|
16
16
|
## Pre-flight Checklist
|
|
17
17
|
|
|
18
|
-
### 1)
|
|
18
|
+
### 1) ACP Backend Check
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
- Prioritize high-performance models for high-difficulty tasks
|
|
20
|
+
Verify ACP is enabled and the opencode harness is available:
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- If authentication expires, re-login within the session and retry
|
|
22
|
+
```text
|
|
23
|
+
/acp doctor
|
|
24
|
+
```
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
If ACP backend is not configured, install it:
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
openclaw plugins install @openclaw/acpx
|
|
30
|
+
openclaw config set plugins.entries.acpx.enabled true
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
### 2) OpenCode Authentication
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Create session
|
|
41
|
-
tmux -S "$SOCKET" new -d -s opencode -n main
|
|
35
|
+
- OpenCode CLI provider authentication must be completed
|
|
36
|
+
- If authentication expires, re-authenticate and retry
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
tmux -S "$SOCKET" list-sessions
|
|
38
|
+
### 3) ACP Session Status
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
```text
|
|
41
|
+
/acp status
|
|
42
|
+
/acp sessions
|
|
48
43
|
```
|
|
49
44
|
|
|
50
|
-
##
|
|
45
|
+
## OmO Delegation Pattern
|
|
51
46
|
|
|
52
|
-
|
|
47
|
+
### 1) One-Shot Delegation (Default)
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|----------|------|------|
|
|
56
|
-
| Sisyphus | Default implementation/fixes | Default state |
|
|
57
|
-
| Hephaestus | Deep implementation/refactoring | Tab 1x |
|
|
58
|
-
| Prometheus | Planning/strategy | Tab 2x |
|
|
49
|
+
For single tasks that run to completion:
|
|
59
50
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
```json
|
|
52
|
+
sessions_spawn({
|
|
53
|
+
"task": "ultrawork fix payment failure bug. Include reproduction, root cause analysis, test addition, and regression prevention.",
|
|
54
|
+
"runtime": "acp",
|
|
55
|
+
"agentId": "opencode",
|
|
56
|
+
"mode": "run"
|
|
57
|
+
})
|
|
64
58
|
```
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
- `sessions_spawn` returns immediately with `{ status: "accepted", runId, childSessionKey }`
|
|
61
|
+
- OpenCode works autonomously on the task
|
|
62
|
+
- On completion, result is announced back to the requester chat channel
|
|
63
|
+
- Use `/subagents info <id>` or `/subagents log <id>` to inspect details
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
- Complex refactoring/design: Deep reasoning model
|
|
70
|
-
- Planning-only phase: Highest reasoning model first
|
|
65
|
+
### 2) Persistent Session (Thread-Bound)
|
|
71
66
|
|
|
72
|
-
|
|
67
|
+
For multi-turn interactive work in a thread:
|
|
73
68
|
|
|
74
|
-
|
|
69
|
+
```json
|
|
70
|
+
sessions_spawn({
|
|
71
|
+
"task": "Set up for auth module refactoring. Start by analyzing the current structure.",
|
|
72
|
+
"runtime": "acp",
|
|
73
|
+
"agentId": "opencode",
|
|
74
|
+
"mode": "session",
|
|
75
|
+
"thread": true
|
|
76
|
+
})
|
|
77
|
+
```
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
4) Run tests/build/verification
|
|
80
|
-
5) OpenClaw collects results and reports final summary
|
|
79
|
+
- Thread binding routes follow-up messages to the same OpenCode session
|
|
80
|
+
- Use `/acp steer <instruction>` to nudge without replacing context
|
|
81
|
+
- Use `/unfocus` to detach from the thread when done
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
tmux -S "$SOCKET" send-keys -t opencode:0.0 Tab
|
|
86
|
-
sleep 0.2
|
|
87
|
-
tmux -S "$SOCKET" send-keys -t opencode:0.0 -l -- 'Plan: write scope/risk/verification strategy for auth module refactoring'
|
|
88
|
-
tmux -S "$SOCKET" send-keys -t opencode:0.0 Enter
|
|
83
|
+
### 3) Model Override (Use Sparingly)
|
|
84
|
+
|
|
85
|
+
Override only when you need a specific model. By default, OpenCode uses its own configured model — leave `model` empty to use that default.
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
```json
|
|
88
|
+
sessions_spawn({
|
|
89
|
+
"task": "Plan: write scope/risk/verification strategy for auth module refactoring",
|
|
90
|
+
"runtime": "acp",
|
|
91
|
+
"agentId": "opencode",
|
|
92
|
+
"mode": "run",
|
|
93
|
+
"model": "claude-opus-4-6-thinking"
|
|
94
|
+
})
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
### 4) OpenCode Agent Mode Selection
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
OpenCode has internal agents (Build, Plan, custom agents from `.opencode/agents/`). Select which agent handles the task via ACP session mode switching:
|
|
99
100
|
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
```json
|
|
102
|
+
// Use Plan agent (read-only, restricted tools) for planning tasks
|
|
103
|
+
sessions_spawn({
|
|
104
|
+
"task": "Analyze the auth module structure and propose refactoring strategy",
|
|
105
|
+
"runtime": "acp",
|
|
106
|
+
"agentId": "opencode",
|
|
107
|
+
"mode": "run"
|
|
108
|
+
})
|
|
109
|
+
// After session creation, switch mode: setSessionMode("plan")
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
**How it works:**
|
|
113
|
+
- ACP session creation returns available `modes` (primary agents only — not subagents, not hidden)
|
|
114
|
+
- Call `setSessionMode(modeId)` to switch the active OpenCode agent
|
|
115
|
+
- Default mode is OpenCode's configured primary agent (usually "build")
|
|
116
|
+
- Available modes: `build` (full tools), `plan` (restricted), plus any custom primary agents
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|------|----------|------|
|
|
109
|
-
| Default execution | Sisyphus | Default |
|
|
110
|
-
| Deep implementation | Hephaestus | Tab 1x |
|
|
111
|
-
| Planning | Prometheus | Tab 2x |
|
|
118
|
+
### 5) Subagent Invocation via @mention
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
OpenCode subagents (Explore, custom subagents from `.opencode/agents/`) are invoked via `@mention` in the task text:
|
|
114
121
|
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
```json
|
|
123
|
+
sessions_spawn({
|
|
124
|
+
"task": "@explore find all authentication-related files and report their structure",
|
|
125
|
+
"runtime": "acp",
|
|
126
|
+
"agentId": "opencode",
|
|
127
|
+
"mode": "run"
|
|
128
|
+
})
|
|
120
129
|
```
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
**Note:** Subagents are NOT available as session modes. They are invoked within the agent's conversation via `@agentname` prefix.
|
|
132
|
+
|
|
133
|
+
## Model Selection Guide
|
|
134
|
+
|
|
135
|
+
- **Default behavior**: Leave `model` empty — OpenCode uses its own configured model
|
|
136
|
+
- Quick fixes: Speed-first model (only override if OpenCode default is too slow)
|
|
137
|
+
- Complex refactoring/design: Deep reasoning model
|
|
138
|
+
- Planning-only phase: Use `opencode_agent: "plan"` mode instead of model override
|
|
139
|
+
|
|
140
|
+
Follow project standard routing (quick/deep/ultrabrain) at execution time.
|
|
141
|
+
|
|
142
|
+
## Work Templates
|
|
123
143
|
|
|
124
144
|
Feature implementation:
|
|
125
145
|
```text
|
|
@@ -155,17 +175,70 @@ Read [/path/to/research.md] first,
|
|
|
155
175
|
then ultrawork implement [feature] based on research findings.
|
|
156
176
|
```
|
|
157
177
|
|
|
158
|
-
|
|
178
|
+
## Progress Monitoring
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
### Check Active Sessions
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
/acp sessions
|
|
184
|
+
/subagents list
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Inspect Session Output
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
/subagents log <id>
|
|
191
|
+
/subagents info <id>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Steer Active Session
|
|
195
|
+
|
|
196
|
+
Nudge a running session without replacing context:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
/acp steer focus on the failing test case first
|
|
162
200
|
```
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
- Check progress logs every 10-30 seconds
|
|
166
|
-
- Intervene immediately on signs of blockage (repeated identical output, prompt waiting)
|
|
202
|
+
### Session History
|
|
167
203
|
|
|
168
|
-
|
|
204
|
+
After completion, review the full transcript:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
/subagents log <id> 50
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Parallel Delegation
|
|
211
|
+
|
|
212
|
+
Run multiple OpenCode sessions in parallel:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
// Session 1: Fix auth bug
|
|
216
|
+
sessions_spawn({
|
|
217
|
+
"task": "ultrawork fix auth bug in src/auth/login.ts",
|
|
218
|
+
"runtime": "acp",
|
|
219
|
+
"agentId": "opencode",
|
|
220
|
+
"mode": "run",
|
|
221
|
+
"label": "auth-fix"
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Session 2: Enhance payment tests
|
|
225
|
+
sessions_spawn({
|
|
226
|
+
"task": "ultrawork enhance payment module tests",
|
|
227
|
+
"runtime": "acp",
|
|
228
|
+
"agentId": "opencode",
|
|
229
|
+
"mode": "run",
|
|
230
|
+
"label": "payment-tests"
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- Each session runs independently with its own context
|
|
235
|
+
- Results announce back separately on completion
|
|
236
|
+
- Use `/subagents list` to monitor all active sessions
|
|
237
|
+
- Concurrency is governed by `agents.defaults.subagents.maxConcurrent` (default: 8)
|
|
238
|
+
|
|
239
|
+
## Collect Results
|
|
240
|
+
|
|
241
|
+
After announce arrives:
|
|
169
242
|
|
|
170
243
|
```bash
|
|
171
244
|
git status
|
|
@@ -173,25 +246,48 @@ git diff --stat
|
|
|
173
246
|
git diff
|
|
174
247
|
```
|
|
175
248
|
|
|
176
|
-
OpenClaw summarizes changed files
|
|
249
|
+
OpenClaw summarizes changed files, test results, and risks before reporting to user.
|
|
177
250
|
|
|
178
|
-
|
|
251
|
+
## Error Recovery
|
|
179
252
|
|
|
180
|
-
```
|
|
181
|
-
#
|
|
182
|
-
|
|
253
|
+
```text
|
|
254
|
+
# Cancel in-flight session
|
|
255
|
+
/acp cancel <session-key>
|
|
256
|
+
|
|
257
|
+
# Close and unbind thread
|
|
258
|
+
/acp close
|
|
259
|
+
|
|
260
|
+
# Kill specific sub-agent
|
|
261
|
+
/subagents kill <id>
|
|
262
|
+
|
|
263
|
+
# Retry with new session
|
|
264
|
+
sessions_spawn({
|
|
265
|
+
"task": "First solve only the test failure cause from the previous step.",
|
|
266
|
+
"runtime": "acp",
|
|
267
|
+
"agentId": "opencode",
|
|
268
|
+
"mode": "run"
|
|
269
|
+
})
|
|
270
|
+
```
|
|
183
271
|
|
|
184
|
-
|
|
185
|
-
tmux -S "$SOCKET" send-keys -t opencode:0.0 -l -- 'First solve only the test failure cause from the previous step.'
|
|
186
|
-
tmux -S "$SOCKET" send-keys -t opencode:0.0 Enter
|
|
272
|
+
## Session Lifecycle
|
|
187
273
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
274
|
+
| Action | Command |
|
|
275
|
+
|--------|---------|
|
|
276
|
+
| Spawn one-shot | `sessions_spawn` with `mode: "run"` |
|
|
277
|
+
| Spawn persistent | `sessions_spawn` with `mode: "session"`, `thread: true` |
|
|
278
|
+
| Check status | `/acp status`, `/subagents list` |
|
|
279
|
+
| Inspect output | `/subagents log <id>`, `/subagents info <id>` |
|
|
280
|
+
| Steer mid-run | `/acp steer <instruction>` |
|
|
281
|
+
| Cancel turn | `/acp cancel` |
|
|
282
|
+
| Close session | `/acp close` |
|
|
283
|
+
| Kill sub-agent | `/subagents kill <id>` |
|
|
192
284
|
|
|
193
285
|
## Operation Checklist
|
|
194
286
|
|
|
195
|
-
- Verify
|
|
196
|
-
- Always use `
|
|
287
|
+
- Verify ACP health (`/acp doctor`) -> delegate via `sessions_spawn` -> monitor (`/subagents list`) -> collect results (`git diff`) -> report
|
|
288
|
+
- Always use `runtime: "acp"` and `agentId: "opencode"` for OmO delegation
|
|
289
|
+
- `model` is override-only — leave empty to use OpenCode's own configured default
|
|
290
|
+
- Use `opencode_agent` to select OpenCode's internal agent mode (build, plan, custom)
|
|
291
|
+
- Use `@agentname` prefix in task text to invoke OpenCode subagents
|
|
292
|
+
- Use `label` parameter for easy identification of parallel sessions
|
|
197
293
|
- Validate changes with `git status`/`git diff` before reporting results
|