@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.
@@ -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
  }
@@ -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,2 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ export declare function registerOmoDelegateTool(api: OmocPluginApi): void;
@@ -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 a sub-agent with category-based model routing',
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;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.15.3",
3
+ "version": "0.16.1",
4
4
  "description": "Oh-My-OpenClaw plugin — multi-agent orchestration, todo enforcer, ralph loop, and custom tools for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,125 +1,145 @@
1
1
  ---
2
2
  name: opencode-controller
3
- description: Control OpenCode sessions via tmux. Includes session management, model selection, agent switching (Plan/Build), and OmO delegation patterns.
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 the tmux `opencode` session
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) Provider/Model Selection
18
+ ### 1) ACP Backend Check
19
19
 
20
- - Select OpenCode model based on task difficulty (quick/deep/ultrabrain)
21
- - Prioritize high-performance models for high-difficulty tasks
20
+ Verify ACP is enabled and the opencode harness is available:
22
21
 
23
- ### 2) Authentication Status
24
-
25
- - OpenCode CLI provider authentication must be completed
26
- - If authentication expires, re-login within the session and retry
22
+ ```text
23
+ /acp doctor
24
+ ```
27
25
 
28
- ### 3) tmux Session Check
26
+ If ACP backend is not configured, install it:
29
27
 
30
28
  ```bash
31
- SOCKET="/tmp/openclaw-tmux-sockets/openclaw.sock"
32
- tmux -S "$SOCKET" has-session -t opencode
29
+ openclaw plugins install @openclaw/acpx
30
+ openclaw config set plugins.entries.acpx.enabled true
33
31
  ```
34
32
 
35
- ## Session Management
33
+ ### 2) OpenCode Authentication
36
34
 
37
- ```bash
38
- SOCKET="/tmp/openclaw-tmux-sockets/openclaw.sock"
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
- # Session status
44
- tmux -S "$SOCKET" list-sessions
38
+ ### 3) ACP Session Status
45
39
 
46
- # Check session output
47
- tmux -S "$SOCKET" capture-pane -p -J -t opencode:0.0 -S -200
40
+ ```text
41
+ /acp status
42
+ /acp sessions
48
43
  ```
49
44
 
50
- ## Agent Control
45
+ ## OmO Delegation Pattern
51
46
 
52
- OpenCode agent switching is performed via Tab.
47
+ ### 1) One-Shot Delegation (Default)
53
48
 
54
- | Agent | Purpose | Switch |
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
- ```bash
61
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Tab
62
- sleep 1
63
- tmux -S "$SOCKET" capture-pane -p -J -t opencode:0.0 -S -20
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
- ## Model Selection Guide
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
- - Quick fixes: Speed-first model
69
- - Complex refactoring/design: Deep reasoning model
70
- - Planning-only phase: Highest reasoning model first
65
+ ### 2) Persistent Session (Thread-Bound)
71
66
 
72
- Follow project standard routing (quick/deep/ultrabrain) at execution time.
67
+ For multi-turn interactive work in a thread:
73
68
 
74
- ## Plan -> Build Workflow
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
- 1) Establish plan with Prometheus
77
- 2) Approve/refine plan
78
- 3) Switch to Sisyphus or Hephaestus for implementation
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
- ```bash
83
- # Plan phase
84
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Tab
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
- # Build phase switch (e.g., return to Sisyphus for implementation)
91
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Escape
92
- tmux -S "$SOCKET" send-keys -t opencode:0.0 -l -- 'ultrawork implement based on above plan, complete through testing'
93
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Enter
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
- ## OmO Delegation Pattern
97
+ ### 4) OpenCode Agent Mode Selection
97
98
 
98
- ### 1) Validate tmux Session
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
- ```bash
101
- SOCKET="/tmp/openclaw-tmux-sockets/openclaw.sock"
102
- tmux -S "$SOCKET" has-session -t opencode
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
- ### 2) Agent Selection Table
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
- | Purpose | Agent | Switch |
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
- ### 3) Send Work (`send-keys -l` + separate Enter)
120
+ OpenCode subagents (Explore, custom subagents from `.opencode/agents/`) are invoked via `@mention` in the task text:
114
121
 
115
- ```bash
116
- TASK='ultrawork fix payment failure bug. Include reproduction, root cause analysis, test addition, and regression prevention.'
117
- tmux -S "$SOCKET" send-keys -t opencode:0.0 -l -- "$TASK"
118
- sleep 0.1
119
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Enter
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
- ### 4) Work Templates
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
- ### 5) Progress Monitoring (`capture-pane`)
178
+ ## Progress Monitoring
159
179
 
160
- ```bash
161
- tmux -S "$SOCKET" capture-pane -p -J -t opencode:0.0 -S -200
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
- Recommendations:
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
- ### 6) Collect Results (`git status`/`git diff`)
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/test results/risks and reports to user.
249
+ OpenClaw summarizes changed files, test results, and risks before reporting to user.
177
250
 
178
- ### 7) Error Recovery
251
+ ## Error Recovery
179
252
 
180
- ```bash
181
- # Stop current operation
182
- tmux -S "$SOCKET" send-keys -t opencode:0.0 Escape
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
- # Resend fix instruction
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
- # Restart session
189
- tmux -S "$SOCKET" kill-session -t opencode
190
- tmux -S "$SOCKET" new -d -s opencode -n main
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 session alive select agent send work monitor collect results
196
- - Always use `send-keys -l` + separate Enter
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