@happycastle/oh-my-openclaw 0.19.0 → 0.20.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.
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ export declare function registerGuardrailInjector(api: OmocPluginApi): void;
@@ -0,0 +1,37 @@
1
+ import { LOG_PREFIX } from '../constants.js';
2
+ const GUARDRAIL_RULES = `
3
+ <anti-hallucination-guardrails>
4
+ ## Anti-Hallucination Rules (MANDATORY)
5
+
6
+ These rules are NON-NEGOTIABLE. Violating them is a critical failure.
7
+
8
+ ### Rule 1: No Fake Tool Calls
9
+ - If you say "I read the file", "I checked the code", "I confirmed in the source", or similar — there MUST be a corresponding \`read\`, \`exec\`, \`grep\`, or equivalent tool call in THE SAME turn.
10
+ - If you did NOT make a tool call, you MUST say: "I haven't verified this directly — this is based on my prior knowledge/context."
11
+ - Phrases that REQUIRE a preceding tool call: "확인했다", "읽었다", "봤다", "코드에서", "소스를 보면", "파일을 열어보니", "checked", "verified", "confirmed", "read the file", "looked at the code"
12
+
13
+ ### Rule 2: No Fabricated Results
14
+ - Never invent file contents, command outputs, or API responses.
15
+ - If you're unsure what a file contains, READ IT. Don't guess.
16
+ - If a tool call fails, report the failure — don't make up what the result "would have been."
17
+
18
+ ### Rule 3: Distinguish Memory from Verification
19
+ - Information from previous sessions or context = "이전 세션 기억 기반으로는..." or "Based on prior context..."
20
+ - Information from THIS session's tool calls = state it directly
21
+ - NEVER present memory/context as if you just verified it with a tool call.
22
+
23
+ ### Rule 4: Sub-agent Delegation Honesty
24
+ - If asked to delegate to a sub-agent, you MUST actually call \`sessions_spawn\` or \`omoc_delegate\`.
25
+ - Saying "서브에이전트 호출 완료" without a tool call = CRITICAL VIOLATION.
26
+ - If the spawn fails, report the failure honestly.
27
+ </anti-hallucination-guardrails>
28
+ `.trim();
29
+ export function registerGuardrailInjector(api) {
30
+ api.on('before_prompt_build', (_event, _ctx) => {
31
+ api.logger.info(`${LOG_PREFIX} Guardrail rules injected via before_prompt_build`);
32
+ return {
33
+ prependContext: GUARDRAIL_RULES,
34
+ };
35
+ }, { priority: 90 } // Between persona (100) and context-injector (50)
36
+ );
37
+ }
@@ -1,5 +1,5 @@
1
1
  import { LOG_PREFIX } from '../constants.js';
2
- import { trackSubagentSpawn, clearSubagentTracking, getCallerSessionKey } from '../services/webhook-bridge.js';
2
+ import { trackSubagentSpawn, clearSubagentTracking, getCallerSessionKey, getTrackedSubagents } from '../services/webhook-bridge.js';
3
3
  import { callHooksWake } from '../utils/webhook-client.js';
4
4
  import { getConfig } from '../utils/config.js';
5
5
  const SPAWN_TOOL_NAME = 'sessions_spawn';
@@ -27,6 +27,43 @@ function extractSpawnResult(content) {
27
27
  }
28
28
  return null;
29
29
  }
30
+ /**
31
+ * Tries to find a tracked sub-agent from the message content.
32
+ * Uses multiple strategies: runId match, childSessionKey match.
33
+ * Falls back to keyword detection for single-tracked-agent case
34
+ * only when multiple strong announce indicators are present.
35
+ */
36
+ function findTrackedSubagentInContent(content) {
37
+ const tracked = getTrackedSubagents();
38
+ if (tracked.size === 0)
39
+ return null;
40
+ // Strategy 1: Direct runId match in content
41
+ const runIdMatch = content.match(/runId["\s:=]+["']?([a-zA-Z0-9_-]+)/);
42
+ if (runIdMatch && tracked.has(runIdMatch[1])) {
43
+ return runIdMatch[1];
44
+ }
45
+ // Strategy 2: childSessionKey match in content
46
+ for (const [runId, entry] of tracked) {
47
+ if (content.includes(entry.childSessionKey)) {
48
+ return runId;
49
+ }
50
+ }
51
+ // Strategy 3: Strong announce indicators (require at least 2)
52
+ // Only used for unambiguous single-tracked-agent case
53
+ if (tracked.size === 1) {
54
+ const strongIndicators = [
55
+ 'Sub-agent', 'subagent', 'sub_agent',
56
+ 'Result:', 'Summary:',
57
+ ];
58
+ const matchCount = strongIndicators.filter((kw) => content.includes(kw)).length;
59
+ // Require at least 2 strong indicators to avoid false positives
60
+ if (matchCount >= 2) {
61
+ const [onlyRunId] = tracked.keys();
62
+ return onlyRunId;
63
+ }
64
+ }
65
+ return null;
66
+ }
30
67
  export function registerSubagentTracker(api) {
31
68
  api.registerHook('tool_result_persist', (payload) => {
32
69
  if (payload.tool !== SPAWN_TOOL_NAME)
@@ -34,7 +71,6 @@ export function registerSubagentTracker(api) {
34
71
  const content = typeof payload.content === 'string' ? payload.content : '';
35
72
  const spawnResult = extractSpawnResult(content);
36
73
  if (spawnResult) {
37
- // Capture the caller's session key from the payload context
38
74
  const callerSessionKey = typeof payload.sessionId === 'string'
39
75
  ? payload.sessionId
40
76
  : undefined;
@@ -50,34 +86,63 @@ export function registerSubagentTracker(api) {
50
86
  name: 'oh-my-openclaw.subagent-tracker',
51
87
  description: 'Tracks sessions_spawn results for stale sub-agent detection',
52
88
  });
89
+ api.on('subagent_ended', async (event, ctx) => {
90
+ const runId = typeof event?.runId === 'string' ? event.runId : undefined;
91
+ if (!runId)
92
+ return;
93
+ const tracked = getTrackedSubagents();
94
+ const wasTracked = tracked.has(runId);
95
+ const callerSession = getCallerSessionKey(runId);
96
+ clearSubagentTracking(runId);
97
+ if (!wasTracked)
98
+ return;
99
+ api.logger.info(`${LOG_PREFIX} subagent_ended received: runId=${runId} (callerSession=${callerSession ?? 'unknown'})`);
100
+ const config = getConfig(api);
101
+ if (config.webhook_bridge_enabled && config.gateway_url && config.hooks_token) {
102
+ const requesterSessionKey = typeof ctx?.requesterSessionKey === 'string'
103
+ ? (ctx.requesterSessionKey)
104
+ : undefined;
105
+ const wakeMessage = requesterSessionKey
106
+ ? `[System] Sub-agent completed (runId=${runId}, requester=${requesterSessionKey}). Process the result and continue pending work.`
107
+ : `[System] Sub-agent completed (runId=${runId}). Process the result and continue pending work.`;
108
+ const result = await callHooksWake(wakeMessage, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger);
109
+ if (result.ok) {
110
+ api.logger.info(`${LOG_PREFIX} Wake sent from subagent_ended: runId=${runId}`);
111
+ }
112
+ else {
113
+ api.logger.warn(`${LOG_PREFIX} Wake from subagent_ended failed: ${result.error ?? `status ${result.status}`}`);
114
+ }
115
+ }
116
+ }, { priority: 120 });
53
117
  api.registerHook('message:received', (context) => {
54
118
  const content = context?.content ?? '';
55
- if (!content.includes('Sub-agent') && !content.includes('subagent') && !content.includes('announce')) {
119
+ // Skip empty/short messages
120
+ if (content.length < 10)
56
121
  return undefined;
57
- }
58
- const runIdMatch = content.match(/runId["\s:=]+["']?([a-zA-Z0-9_-]+)/);
59
- if (runIdMatch) {
60
- // Get caller session key before clearing tracking
61
- const callerSession = getCallerSessionKey(runIdMatch[1]);
62
- clearSubagentTracking(runIdMatch[1]);
63
- api.logger.info(`${LOG_PREFIX} Cleared sub-agent tracking: runId=${runIdMatch[1]} (announce received, callerSession=${callerSession ?? 'unknown'})`);
64
- // Send wake to ensure the main agent processes the announce and continues work
65
- const config = getConfig(api);
66
- if (config.webhook_bridge_enabled && config.gateway_url && config.hooks_token) {
67
- void callHooksWake(`[System] Sub-agent completed (runId=${runIdMatch[1]}). Process the announce result and continue any pending work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger).then((result) => {
68
- if (result.ok) {
69
- api.logger.info(`${LOG_PREFIX} Wake sent after sub-agent announce: runId=${runIdMatch[1]}`);
70
- }
71
- else {
72
- api.logger.warn(`${LOG_PREFIX} Wake after announce failed: ${result.error ?? `status ${result.status}`}`);
73
- }
74
- });
75
- }
122
+ // Try to find a tracked sub-agent in this message
123
+ const matchedRunId = findTrackedSubagentInContent(content);
124
+ if (!matchedRunId)
125
+ return undefined;
126
+ // Found a match — this is likely a sub-agent announce
127
+ const callerSession = getCallerSessionKey(matchedRunId);
128
+ clearSubagentTracking(matchedRunId);
129
+ api.logger.info(`${LOG_PREFIX} Sub-agent announce detected: runId=${matchedRunId} (callerSession=${callerSession ?? 'unknown'})`);
130
+ // Send wake to ensure the main agent processes the announce and continues work
131
+ const config = getConfig(api);
132
+ if (config.webhook_bridge_enabled && config.gateway_url && config.hooks_token) {
133
+ void callHooksWake(`[System] Sub-agent completed (runId=${matchedRunId}). Process the announce result and continue any pending work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger).then((result) => {
134
+ if (result.ok) {
135
+ api.logger.info(`${LOG_PREFIX} Wake sent after sub-agent announce: runId=${matchedRunId}`);
136
+ }
137
+ else {
138
+ api.logger.warn(`${LOG_PREFIX} Wake after announce failed: ${result.error ?? `status ${result.status}`}`);
139
+ }
140
+ });
76
141
  }
77
142
  return undefined;
78
143
  }, {
79
144
  name: 'oh-my-openclaw.subagent-announce-detector',
80
- description: 'Detects sub-agent announce messages, clears stale tracking, and wakes main agent',
145
+ description: 'Detects sub-agent announce messages via multi-strategy matching, clears stale tracking, and wakes main agent',
81
146
  });
82
147
  }
83
148
  export { extractSpawnResult };
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerRalphCommands } from './commands/ralph-commands.js';
18
18
  import { registerStatusCommands } from './commands/status-commands.js';
19
19
  import { registerPersonaCommands } from './commands/persona-commands.js';
20
20
  import { registerContextInjector } from './hooks/context-injector.js';
21
+ import { registerGuardrailInjector } from './hooks/guardrail-injector.js';
21
22
  import { registerSessionSync } from './hooks/session-sync.js';
22
23
  import { registerSpawnGuard } from './hooks/spawn-guard.js';
23
24
  import { registerKeywordDetector } from './hooks/keyword-detector/hook.js';
@@ -95,6 +96,11 @@ export default function register(api) {
95
96
  registry.hooks.push('context-injector');
96
97
  api.logger.info(`[${PLUGIN_ID}] Context injector hook registered (before_prompt_build)`);
97
98
  });
99
+ safeRegister(api, 'guardrail-injector', 'hook', () => {
100
+ registerGuardrailInjector(guarded);
101
+ registry.hooks.push('guardrail-injector');
102
+ api.logger.info(`[${PLUGIN_ID}] Guardrail injector hook registered (before_prompt_build, priority 90)`);
103
+ });
98
104
  safeRegister(api, 'session-sync', 'hook', () => {
99
105
  registerSessionSync(api);
100
106
  registry.hooks.push('session-sync');
@@ -34,7 +34,7 @@ const DelegateParamsSchema = Type.Object({
34
34
  skills: Type.Optional(Type.Array(Type.String(), { description: 'Skill names to load' })),
35
35
  background: Type.Optional(Type.Boolean({ description: 'Run in background (default: false)', default: false })),
36
36
  });
37
- function getModelForCategory(category, api) {
37
+ function getRecommendedModelForCategory(category, api) {
38
38
  const config = getConfig(api);
39
39
  const override = config.model_routing?.[category];
40
40
  if (override?.model) {
@@ -59,7 +59,7 @@ export function registerDelegateTool(api) {
59
59
  return toolError(`Invalid category: ${params.category}. Valid: ${validCategories.join(', ')}`);
60
60
  }
61
61
  const category = params.category;
62
- const { model, alternatives } = getModelForCategory(category, api);
62
+ const { model, alternatives } = getRecommendedModelForCategory(category, api);
63
63
  const agentId = params.agent_id || DEFAULT_CATEGORY_AGENTS[category];
64
64
  api.logger.info(`${LOG_PREFIX} Delegating task:`, { category, model, agentId });
65
65
  const instruction = [
@@ -68,12 +68,12 @@ export function registerDelegateTool(api) {
68
68
  '⚡ NOW CALL sessions_spawn with these parameters:',
69
69
  ` task: "${params.task_description}"`,
70
70
  ` mode: "run"`,
71
- ` model: "${model}"`,
72
71
  ` agentId: "${agentId}"`,
73
- alternatives?.length ? ` fallback_models: ${JSON.stringify(alternatives)}` : '',
74
- alternatives?.length ? ` If "${model}" is unavailable, try: ${alternatives.join(', ')}` : '',
72
+ ` # recommended model (do NOT pass to sessions_spawn): ${model}`,
73
+ alternatives?.length ? ` Recommended fallback models (informational only): ${alternatives.join(', ')}` : '',
75
74
  params.background ? ' (background execution — results will arrive via push notification)' : '',
76
75
  '',
76
+ 'Do NOT set sessions_spawn model unless explicitly asked by user.',
77
77
  'Do NOT just return this metadata. Actually call sessions_spawn NOW.',
78
78
  '',
79
79
  '⚠️ AFTER the subagent completes:',
@@ -2,7 +2,7 @@
2
2
  "id": "oh-my-openclaw",
3
3
  "name": "Oh-My-OpenClaw",
4
4
  "description": "Multi-agent orchestration plugin — 11 agents, category-based model routing, todo enforcer, ralph loop, agent setup CLI, and custom tools",
5
- "version": "0.19.0",
5
+ "version": "0.20.1",
6
6
  "skills": [
7
7
  "skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",