@happycastle/oh-my-openclaw 0.19.0 → 0.20.0
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,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
|
-
|
|
119
|
+
// Skip empty/short messages
|
|
120
|
+
if (content.length < 10)
|
|
56
121
|
return undefined;
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
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');
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.20.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills"
|
|
8
8
|
],
|
package/package.json
CHANGED