@happycastle/oh-my-openclaw 0.1.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.
- package/dist/commands/ralph-commands.d.ts +2 -0
- package/dist/commands/ralph-commands.js +62 -0
- package/dist/commands/workflow-commands.d.ts +2 -0
- package/dist/commands/workflow-commands.js +46 -0
- package/dist/hooks/comment-checker.d.ts +2 -0
- package/dist/hooks/comment-checker.js +86 -0
- package/dist/hooks/message-monitor.d.ts +11 -0
- package/dist/hooks/message-monitor.js +36 -0
- package/dist/hooks/todo-enforcer.d.ts +2 -0
- package/dist/hooks/todo-enforcer.js +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +95 -0
- package/dist/services/ralph-loop.d.ts +17 -0
- package/dist/services/ralph-loop.js +127 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +125 -0
- package/dist/tools/look-at.d.ts +2 -0
- package/dist/tools/look-at.js +67 -0
- package/dist/tools/task-delegation.d.ts +2 -0
- package/dist/tools/task-delegation.js +61 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +4 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +44 -0
- package/dist/utils/state.d.ts +3 -0
- package/dist/utils/state.js +29 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/validation.js +26 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +51 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { startLoop, stopLoop, getStatus } from '../services/ralph-loop.js';
|
|
2
|
+
import { getMessageCount } from '../hooks/message-monitor.js';
|
|
3
|
+
import { getConfig } from '../utils/config.js';
|
|
4
|
+
export function registerRalphCommands(api) {
|
|
5
|
+
// /ralph-loop command
|
|
6
|
+
api.registerCommand({
|
|
7
|
+
name: 'ralph-loop',
|
|
8
|
+
description: 'Start the Ralph Loop self-completion mechanism',
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
const args = (ctx.args || '').trim().split(/\s+/).filter(Boolean);
|
|
11
|
+
const config = getConfig(api);
|
|
12
|
+
const maxIterations = args[0] ? parseInt(args[0], 10) : config.max_ralph_iterations;
|
|
13
|
+
const taskFile = args[1] || '';
|
|
14
|
+
if (isNaN(maxIterations) || maxIterations < 1) {
|
|
15
|
+
return { text: 'Error: First argument must be a positive number (max iterations)' };
|
|
16
|
+
}
|
|
17
|
+
const result = await startLoop(taskFile, maxIterations);
|
|
18
|
+
return {
|
|
19
|
+
text: result.success
|
|
20
|
+
? `✅ Ralph Loop started\n- Max iterations: ${result.state.maxIterations}\n- Task file: ${result.state.taskFile || 'none'}\n- Started: ${result.state.startedAt}`
|
|
21
|
+
: `❌ ${result.message}`,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
// /ralph-stop command
|
|
26
|
+
api.registerCommand({
|
|
27
|
+
name: 'ralph-stop',
|
|
28
|
+
description: 'Stop the active Ralph Loop',
|
|
29
|
+
handler: async () => {
|
|
30
|
+
const result = await stopLoop();
|
|
31
|
+
return {
|
|
32
|
+
text: `Ralph Loop stopped\n- Final iteration: ${result.state.iteration}/${result.state.maxIterations}\n- Was active: ${result.state.active}`,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
// /omoc-status command
|
|
37
|
+
api.registerCommand({
|
|
38
|
+
name: 'omoc-status',
|
|
39
|
+
description: 'Show Oh-My-OpenClaw plugin status',
|
|
40
|
+
handler: async () => {
|
|
41
|
+
const config = getConfig(api);
|
|
42
|
+
const ralphState = await getStatus();
|
|
43
|
+
const messageCount = getMessageCount();
|
|
44
|
+
const lines = [
|
|
45
|
+
'# Oh-My-OpenClaw Status',
|
|
46
|
+
'',
|
|
47
|
+
`## Ralph Loop: ${ralphState.active ? 'ACTIVE' : 'INACTIVE'}`,
|
|
48
|
+
ralphState.active ? ` Iteration: ${ralphState.iteration}/${ralphState.maxIterations}` : '',
|
|
49
|
+
ralphState.active ? ` Task: ${ralphState.taskFile || 'none'}` : '',
|
|
50
|
+
ralphState.active ? ` Started: ${ralphState.startedAt}` : '',
|
|
51
|
+
'',
|
|
52
|
+
`## Todo Enforcer: ${config.todo_enforcer_enabled ? 'ENABLED' : 'DISABLED'}`,
|
|
53
|
+
` Cooldown: ${config.todo_enforcer_cooldown_ms}ms`,
|
|
54
|
+
'',
|
|
55
|
+
`## Comment Checker: ${config.comment_checker_enabled ? 'ENABLED' : 'DISABLED'}`,
|
|
56
|
+
'',
|
|
57
|
+
`## Messages Monitored: ${messageCount}`,
|
|
58
|
+
].filter((l) => l !== '');
|
|
59
|
+
return { text: lines.join('\n') };
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
function readWorkflow(workflowName) {
|
|
4
|
+
try {
|
|
5
|
+
const workflowPath = join(process.cwd(), 'workflows', `${workflowName}.md`);
|
|
6
|
+
return readFileSync(workflowPath, 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return `Error: Could not read workflow file 'workflows/${workflowName}.md'. Make sure the oh-my-openclaw skill directory is accessible.`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function registerWorkflowCommands(api) {
|
|
13
|
+
api.registerCommand({
|
|
14
|
+
name: 'ultrawork',
|
|
15
|
+
description: 'Full planning \u2192 execution \u2192 verification workflow',
|
|
16
|
+
handler: (ctx) => {
|
|
17
|
+
const taskDescription = ctx.args || 'No task specified';
|
|
18
|
+
const workflow = readWorkflow('ultrawork');
|
|
19
|
+
return {
|
|
20
|
+
text: `# Ultrawork Mode\n\n**Task**: ${taskDescription}\n\n---\n\n${workflow}`,
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
api.registerCommand({
|
|
25
|
+
name: 'plan',
|
|
26
|
+
description: 'Create a structured execution plan',
|
|
27
|
+
handler: (ctx) => {
|
|
28
|
+
const topic = ctx.args || 'No topic specified';
|
|
29
|
+
const workflow = readWorkflow('plan');
|
|
30
|
+
return {
|
|
31
|
+
text: `# Planning Mode\n\n**Topic**: ${topic}\n\n---\n\n${workflow}`,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
api.registerCommand({
|
|
36
|
+
name: 'start-work',
|
|
37
|
+
description: 'Execute an approved plan',
|
|
38
|
+
handler: (ctx) => {
|
|
39
|
+
const planPath = ctx.args || 'most recent plan';
|
|
40
|
+
const workflow = readWorkflow('start-work');
|
|
41
|
+
return {
|
|
42
|
+
text: `# Start Work Mode\n\n**Plan**: ${planPath}\n\n---\n\n${workflow}`,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getConfig } from '../utils/config.js';
|
|
2
|
+
const NON_CODE_EXTENSIONS = ['.md', '.json', '.yaml', '.yml', '.txt'];
|
|
3
|
+
const AI_SLOP_PATTERNS = [
|
|
4
|
+
/^\s*\/\/\s*Import\s/i,
|
|
5
|
+
/^\s*\/\/\s*Define\s/i,
|
|
6
|
+
/^\s*\/\/\s*Return\s/i,
|
|
7
|
+
/^\s*\/\/\s*Export\s/i,
|
|
8
|
+
/^\s*\/\/\s*Set\s.*\sto\s/i,
|
|
9
|
+
/^\s*\/\/\s*Loop\s/i,
|
|
10
|
+
/^\s*\/\/\s*Initialize\s/i,
|
|
11
|
+
/^\s*\/\/\s*Create\s(a|an|the|new)\s/i,
|
|
12
|
+
/^\s*\/\/\s*This\s(function|method|class|module|component)\s/i,
|
|
13
|
+
/^\s*\/\/\s*Handle\s(the|an?)?\s?(error|exception|response|request|event)/i,
|
|
14
|
+
/^\s*\/\/\s*Check\s(if|whether)\s/i,
|
|
15
|
+
];
|
|
16
|
+
function hasNonCodeExtension(value) {
|
|
17
|
+
const lowered = value.toLowerCase();
|
|
18
|
+
return NON_CODE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
|
|
19
|
+
}
|
|
20
|
+
function extractFileHint(payload) {
|
|
21
|
+
if (typeof payload.file === 'string') {
|
|
22
|
+
return payload.file;
|
|
23
|
+
}
|
|
24
|
+
if (typeof payload.filename === 'string') {
|
|
25
|
+
return payload.filename;
|
|
26
|
+
}
|
|
27
|
+
if (typeof payload.path === 'string') {
|
|
28
|
+
return payload.path;
|
|
29
|
+
}
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
function contentLooksNonCode(content) {
|
|
33
|
+
const extensionMatch = content.match(/\b[^\s"']+\.(md|json|ya?ml|txt)\b/i);
|
|
34
|
+
return extensionMatch !== null;
|
|
35
|
+
}
|
|
36
|
+
function findViolations(content, file) {
|
|
37
|
+
const violations = [];
|
|
38
|
+
const lines = content.split(/\r?\n/);
|
|
39
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
40
|
+
const lineText = lines[index];
|
|
41
|
+
const isViolation = AI_SLOP_PATTERNS.some((pattern) => pattern.test(lineText));
|
|
42
|
+
if (isViolation) {
|
|
43
|
+
violations.push({
|
|
44
|
+
file,
|
|
45
|
+
line: index + 1,
|
|
46
|
+
content: lineText.trim(),
|
|
47
|
+
reason: 'AI slop: obvious/narrating comment',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return violations;
|
|
52
|
+
}
|
|
53
|
+
function appendViolationSummary(content, violations) {
|
|
54
|
+
const details = violations
|
|
55
|
+
.map((violation) => ` - Line ${violation.line}: "${violation.content}" → ${violation.reason}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
return `${content}\n\n---\n⚠️ [OMOC Comment Checker] Found ${violations.length} AI slop comment(s):\n${details}\n\nConsider removing these obvious/narrating comments to keep code clean.`;
|
|
58
|
+
}
|
|
59
|
+
export function registerCommentChecker(api) {
|
|
60
|
+
api.registerHook('tool_result_persist', (payload) => {
|
|
61
|
+
const config = getConfig(api);
|
|
62
|
+
if (!config.comment_checker_enabled) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const { content } = payload;
|
|
66
|
+
if (typeof content !== 'string' || content.trim().length === 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const fileHint = extractFileHint(payload);
|
|
70
|
+
if (hasNonCodeExtension(fileHint) || contentLooksNonCode(content)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const violations = findViolations(content, fileHint);
|
|
74
|
+
if (violations.length === 0) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const updatedContent = appendViolationSummary(content, violations);
|
|
78
|
+
return {
|
|
79
|
+
...payload,
|
|
80
|
+
content: updatedContent,
|
|
81
|
+
};
|
|
82
|
+
}, {
|
|
83
|
+
name: 'oh-my-openclaw.comment-checker',
|
|
84
|
+
description: 'Detects AI slop comments in code',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OmocPluginApi } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registers the message monitor hook
|
|
4
|
+
* Logs message events for audit purposes without modifying messages
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerMessageMonitor(api: OmocPluginApi): void;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the current message count
|
|
9
|
+
* Useful for status reporting
|
|
10
|
+
*/
|
|
11
|
+
export declare function getMessageCount(): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Module-level state for tracking messages
|
|
2
|
+
let messageCount = 0;
|
|
3
|
+
/**
|
|
4
|
+
* Registers the message monitor hook
|
|
5
|
+
* Logs message events for audit purposes without modifying messages
|
|
6
|
+
*/
|
|
7
|
+
export function registerMessageMonitor(api) {
|
|
8
|
+
api.registerHook('message:sent', (context) => {
|
|
9
|
+
// Extract relevant info from event context
|
|
10
|
+
const content = context?.content || '';
|
|
11
|
+
const preview = content.substring(0, 100);
|
|
12
|
+
const channelId = context?.channelId || 'unknown';
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
// Log the message event
|
|
15
|
+
api.logger.info('[omoc] Message sent:', {
|
|
16
|
+
preview,
|
|
17
|
+
channelId,
|
|
18
|
+
timestamp,
|
|
19
|
+
messageCount: messageCount + 1
|
|
20
|
+
});
|
|
21
|
+
// Increment message counter
|
|
22
|
+
messageCount++;
|
|
23
|
+
// Return undefined to not modify the message
|
|
24
|
+
return undefined;
|
|
25
|
+
}, {
|
|
26
|
+
name: 'oh-my-openclaw.message-monitor',
|
|
27
|
+
description: 'Monitors message events for audit logging'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns the current message count
|
|
32
|
+
* Useful for status reporting
|
|
33
|
+
*/
|
|
34
|
+
export function getMessageCount() {
|
|
35
|
+
return messageCount;
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getConfig } from '../utils/config.js';
|
|
2
|
+
const DIRECTIVE_TEXT = `[SYSTEM DIRECTIVE: OH-MY-OPENCLAW - TODO CONTINUATION]
|
|
3
|
+
You MUST continue working on incomplete todos.
|
|
4
|
+
- Do NOT stop until all tasks are marked complete
|
|
5
|
+
- Do NOT ask for permission to continue
|
|
6
|
+
- Mark each task complete immediately when finished
|
|
7
|
+
- If blocked, document the blocker and move to next task`;
|
|
8
|
+
export function registerTodoEnforcer(api) {
|
|
9
|
+
api.registerHook('agent:bootstrap', (event) => {
|
|
10
|
+
const config = getConfig(api);
|
|
11
|
+
if (!config.todo_enforcer_enabled) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!event.context.bootstrapFiles) {
|
|
15
|
+
event.context.bootstrapFiles = [];
|
|
16
|
+
}
|
|
17
|
+
event.context.bootstrapFiles.push({
|
|
18
|
+
path: 'omoc://todo-enforcer',
|
|
19
|
+
content: DIRECTIVE_TEXT,
|
|
20
|
+
});
|
|
21
|
+
api.logger.info('[omoc] Todo enforcer directive injected');
|
|
22
|
+
}, {
|
|
23
|
+
name: 'oh-my-openclaw.todo-enforcer',
|
|
24
|
+
description: 'Injects TODO continuation directive into agent bootstrap',
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { PLUGIN_ID } from './types.js';
|
|
2
|
+
import { getConfig } from './utils/config.js';
|
|
3
|
+
import { registerTodoEnforcer } from './hooks/todo-enforcer.js';
|
|
4
|
+
import { registerCommentChecker } from './hooks/comment-checker.js';
|
|
5
|
+
import { registerMessageMonitor } from './hooks/message-monitor.js';
|
|
6
|
+
import { registerRalphLoop } from './services/ralph-loop.js';
|
|
7
|
+
import { registerDelegateTool } from './tools/task-delegation.js';
|
|
8
|
+
import { registerLookAtTool } from './tools/look-at.js';
|
|
9
|
+
import { registerCheckpointTool } from './tools/checkpoint.js';
|
|
10
|
+
import { registerWorkflowCommands } from './commands/workflow-commands.js';
|
|
11
|
+
import { registerRalphCommands } from './commands/ralph-commands.js';
|
|
12
|
+
export default function register(api) {
|
|
13
|
+
const config = getConfig(api);
|
|
14
|
+
api.logger.info(`[${PLUGIN_ID}] Initializing plugin v0.1.0`);
|
|
15
|
+
try {
|
|
16
|
+
registerTodoEnforcer(api);
|
|
17
|
+
api.logger.info(`[${PLUGIN_ID}] Todo Enforcer hook registered (enabled: ${config.todo_enforcer_enabled})`);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Todo Enforcer:`, err);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
registerCommentChecker(api);
|
|
24
|
+
api.logger.info(`[${PLUGIN_ID}] Comment Checker hook registered (enabled: ${config.comment_checker_enabled})`);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Comment Checker:`, err);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
registerMessageMonitor(api);
|
|
31
|
+
api.logger.info(`[${PLUGIN_ID}] Message Monitor hook registered`);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Message Monitor:`, err);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
registerRalphLoop(api);
|
|
38
|
+
api.logger.info(`[${PLUGIN_ID}] Ralph Loop service registered`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Ralph Loop:`, err);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
registerDelegateTool(api);
|
|
45
|
+
api.logger.info(`[${PLUGIN_ID}] Delegate tool registered`);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Delegate tool:`, err);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
registerLookAtTool(api);
|
|
52
|
+
api.logger.info(`[${PLUGIN_ID}] Look-At tool registered`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Look-At tool:`, err);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
registerCheckpointTool(api);
|
|
59
|
+
api.logger.info(`[${PLUGIN_ID}] Checkpoint tool registered`);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Checkpoint tool:`, err);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
registerWorkflowCommands(api);
|
|
66
|
+
api.logger.info(`[${PLUGIN_ID}] Workflow commands registered (ultrawork, plan, start-work)`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Workflow commands:`, err);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
registerRalphCommands(api);
|
|
73
|
+
api.logger.info(`[${PLUGIN_ID}] Ralph commands registered (ralph-loop, ralph-stop, omoc-status)`);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Ralph commands:`, err);
|
|
77
|
+
}
|
|
78
|
+
api.registerGatewayMethod('oh-my-openclaw.status', () => {
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
plugin: PLUGIN_ID,
|
|
82
|
+
version: '0.1.0',
|
|
83
|
+
hooks: ['todo-enforcer', 'comment-checker', 'message-monitor'],
|
|
84
|
+
services: ['ralph-loop'],
|
|
85
|
+
tools: ['omoc_delegate', 'omoc_look_at', 'omoc_checkpoint'],
|
|
86
|
+
commands: ['ultrawork', 'plan', 'start-work', 'ralph-loop', 'ralph-stop', 'omoc-status'],
|
|
87
|
+
config: {
|
|
88
|
+
todo_enforcer_enabled: config.todo_enforcer_enabled,
|
|
89
|
+
comment_checker_enabled: config.comment_checker_enabled,
|
|
90
|
+
max_ralph_iterations: config.max_ralph_iterations,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
api.logger.info(`[${PLUGIN_ID}] Plugin initialization complete`);
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OmocPluginApi, RalphLoopState } from '../types.js';
|
|
2
|
+
export declare function registerRalphLoop(api: OmocPluginApi): void;
|
|
3
|
+
export declare function startLoop(taskFile: string, maxIterations: number): Promise<{
|
|
4
|
+
success: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
state: RalphLoopState;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function stopLoop(): Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
state: RalphLoopState;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function getStatus(): Promise<RalphLoopState>;
|
|
14
|
+
export declare function incrementIteration(): Promise<{
|
|
15
|
+
continue: boolean;
|
|
16
|
+
state: RalphLoopState;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { ABSOLUTE_MAX_RALPH_ITERATIONS, PLUGIN_ID, } from '../types.js';
|
|
4
|
+
import { readState, writeState } from '../utils/state.js';
|
|
5
|
+
import { clampIterations } from '../utils/validation.js';
|
|
6
|
+
const DEFAULT_STATE = {
|
|
7
|
+
active: false,
|
|
8
|
+
iteration: 0,
|
|
9
|
+
maxIterations: 10,
|
|
10
|
+
taskFile: '',
|
|
11
|
+
startedAt: '',
|
|
12
|
+
};
|
|
13
|
+
let apiRef = null;
|
|
14
|
+
let stateFilePath = '';
|
|
15
|
+
let currentState = { ...DEFAULT_STATE };
|
|
16
|
+
function getApi() {
|
|
17
|
+
if (!apiRef) {
|
|
18
|
+
throw new Error('Ralph Loop service is not registered');
|
|
19
|
+
}
|
|
20
|
+
return apiRef;
|
|
21
|
+
}
|
|
22
|
+
async function loadStateFromFile() {
|
|
23
|
+
if (!stateFilePath) {
|
|
24
|
+
currentState = { ...DEFAULT_STATE };
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const loadedState = await readState(stateFilePath);
|
|
28
|
+
if (loadedState) {
|
|
29
|
+
currentState = loadedState;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let hasExistingFile = false;
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(stateFilePath);
|
|
35
|
+
hasExistingFile = true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
hasExistingFile = false;
|
|
39
|
+
}
|
|
40
|
+
currentState = { ...DEFAULT_STATE };
|
|
41
|
+
if (hasExistingFile) {
|
|
42
|
+
getApi().logger.warn(`[${PLUGIN_ID}] Ralph Loop state was corrupted; recovering with default state`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function saveStateToFile() {
|
|
46
|
+
if (!stateFilePath) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await writeState(stateFilePath, currentState);
|
|
50
|
+
}
|
|
51
|
+
export function registerRalphLoop(api) {
|
|
52
|
+
apiRef = api;
|
|
53
|
+
stateFilePath = join(api.config.checkpoint_dir, 'ralph-loop-state.json');
|
|
54
|
+
api.registerService({
|
|
55
|
+
id: 'omoc-ralph-loop',
|
|
56
|
+
name: 'Ralph Loop Service',
|
|
57
|
+
description: 'Self-referential completion mechanism with configurable iterations',
|
|
58
|
+
start: async () => {
|
|
59
|
+
await loadStateFromFile();
|
|
60
|
+
},
|
|
61
|
+
stop: async () => {
|
|
62
|
+
await saveStateToFile();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export async function startLoop(taskFile, maxIterations) {
|
|
67
|
+
const api = getApi();
|
|
68
|
+
if (currentState.active) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
message: 'Ralph Loop is already running',
|
|
72
|
+
state: currentState,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const clampedIterations = clampIterations(maxIterations, ABSOLUTE_MAX_RALPH_ITERATIONS);
|
|
76
|
+
currentState = {
|
|
77
|
+
active: true,
|
|
78
|
+
iteration: 0,
|
|
79
|
+
maxIterations: clampedIterations,
|
|
80
|
+
taskFile,
|
|
81
|
+
startedAt: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
await saveStateToFile();
|
|
84
|
+
api.logger.info('[omoc] Ralph Loop started');
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
message: 'Ralph Loop started',
|
|
88
|
+
state: currentState,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function stopLoop() {
|
|
92
|
+
const api = getApi();
|
|
93
|
+
currentState = {
|
|
94
|
+
...currentState,
|
|
95
|
+
active: false,
|
|
96
|
+
};
|
|
97
|
+
await saveStateToFile();
|
|
98
|
+
api.logger.info('[omoc] Ralph Loop stopped');
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
message: 'Ralph Loop stopped',
|
|
102
|
+
state: currentState,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function getStatus() {
|
|
106
|
+
return currentState;
|
|
107
|
+
}
|
|
108
|
+
export async function incrementIteration() {
|
|
109
|
+
if (!currentState.active) {
|
|
110
|
+
return {
|
|
111
|
+
continue: false,
|
|
112
|
+
state: currentState,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const nextIteration = currentState.iteration + 1;
|
|
116
|
+
const reachedLimit = nextIteration >= currentState.maxIterations;
|
|
117
|
+
currentState = {
|
|
118
|
+
...currentState,
|
|
119
|
+
iteration: nextIteration,
|
|
120
|
+
active: reachedLimit ? false : currentState.active,
|
|
121
|
+
};
|
|
122
|
+
await saveStateToFile();
|
|
123
|
+
return {
|
|
124
|
+
continue: currentState.active,
|
|
125
|
+
state: currentState,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { readState, writeState, ensureDir } from '../utils/state.js';
|
|
5
|
+
import { getConfig } from '../utils/config.js';
|
|
6
|
+
export function registerCheckpointTool(api) {
|
|
7
|
+
api.registerTool({
|
|
8
|
+
name: 'omoc_checkpoint',
|
|
9
|
+
description: 'Save, load, or list session checkpoints for crash recovery',
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
action: Type.Union([Type.Literal('save'), Type.Literal('load'), Type.Literal('list')], {
|
|
12
|
+
description: 'Checkpoint operation',
|
|
13
|
+
}),
|
|
14
|
+
task: Type.Optional(Type.String({ description: 'Current task name (for save)' })),
|
|
15
|
+
step: Type.Optional(Type.String({ description: 'Current step name (for save)' })),
|
|
16
|
+
changed_files: Type.Optional(Type.Array(Type.String(), { description: 'Files modified since last checkpoint' })),
|
|
17
|
+
next_action: Type.Optional(Type.String({ description: 'What to do after restore' })),
|
|
18
|
+
}),
|
|
19
|
+
execute: async (params) => {
|
|
20
|
+
const config = getConfig(api);
|
|
21
|
+
const checkpointDir = config.checkpoint_dir;
|
|
22
|
+
await ensureDir(checkpointDir);
|
|
23
|
+
if (params.action === 'save') {
|
|
24
|
+
const checkpoint = {
|
|
25
|
+
type: 'session-checkpoint',
|
|
26
|
+
session_id: `checkpoint-${Date.now()}`,
|
|
27
|
+
task: params.task || 'unknown',
|
|
28
|
+
step: params.step || 'unknown',
|
|
29
|
+
changed_files: params.changed_files || [],
|
|
30
|
+
verification: { diagnostics: 'not-run', tests: 'not-run', build: 'not-run' },
|
|
31
|
+
next_action: params.next_action || '',
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
const filename = `checkpoint-${Date.now()}.json`;
|
|
35
|
+
await writeState(join(checkpointDir, filename), checkpoint);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
text: JSON.stringify({ saved: filename, checkpoint }, null, 2),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (params.action === 'load') {
|
|
46
|
+
try {
|
|
47
|
+
const files = await fs.readdir(checkpointDir);
|
|
48
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json')).sort().reverse();
|
|
49
|
+
if (jsonFiles.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: 'text', text: 'No checkpoints found' }],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const mostRecent = jsonFiles[0];
|
|
55
|
+
const checkpoint = await readState(join(checkpointDir, mostRecent));
|
|
56
|
+
if (!checkpoint) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: `Failed to load checkpoint: ${mostRecent}` }],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: JSON.stringify({ loaded: mostRecent, checkpoint }, null, 2),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `Error loading checkpoint: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (params.action === 'list') {
|
|
82
|
+
try {
|
|
83
|
+
const files = await fs.readdir(checkpointDir);
|
|
84
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json')).sort().reverse();
|
|
85
|
+
if (jsonFiles.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: 'text', text: 'No checkpoints found' }],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const checkpoints = await Promise.all(jsonFiles.map(async (file) => {
|
|
91
|
+
const checkpoint = await readState(join(checkpointDir, file));
|
|
92
|
+
return {
|
|
93
|
+
file,
|
|
94
|
+
timestamp: checkpoint?.timestamp || 'unknown',
|
|
95
|
+
task: checkpoint?.task || 'unknown',
|
|
96
|
+
step: checkpoint?.step || 'unknown',
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: JSON.stringify({ checkpoints }, null, 2),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: `Error listing checkpoints: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: 'Invalid action' }],
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
optional: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import { TOOL_PREFIX } from '../types.js';
|
|
5
|
+
const TMUX_SOCKET = '/tmp/openclaw-tmux-sockets/openclaw.sock';
|
|
6
|
+
const TMUX_SESSION_TARGET = 'gemini:0.0';
|
|
7
|
+
const GEMINI_TIMEOUT_MS = 60_000;
|
|
8
|
+
function escapeShellArg(arg) {
|
|
9
|
+
return arg.replace(/'/g, "'\\''");
|
|
10
|
+
}
|
|
11
|
+
export function registerLookAtTool(api) {
|
|
12
|
+
api.registerTool({
|
|
13
|
+
name: `${TOOL_PREFIX}look_at`,
|
|
14
|
+
description: 'Analyze files (PDF, images, video) using Gemini CLI via tmux',
|
|
15
|
+
parameters: Type.Object({
|
|
16
|
+
file_path: Type.String({ description: 'Path to the file to analyze' }),
|
|
17
|
+
goal: Type.String({ description: 'What to analyze or look for' }),
|
|
18
|
+
model: Type.Optional(Type.String({
|
|
19
|
+
description: 'Gemini model to use',
|
|
20
|
+
default: 'gemini-2.5-flash',
|
|
21
|
+
})),
|
|
22
|
+
}),
|
|
23
|
+
execute: async (params) => {
|
|
24
|
+
const tempFile = `/tmp/omoc-look-at-${Date.now()}.md`;
|
|
25
|
+
try {
|
|
26
|
+
const model = params.model ?? 'gemini-2.5-flash';
|
|
27
|
+
const escapedFilePath = escapeShellArg(params.file_path);
|
|
28
|
+
const escapedGoal = escapeShellArg(params.goal);
|
|
29
|
+
const escapedModel = escapeShellArg(model);
|
|
30
|
+
const command = `gemini -m ${escapedModel} --prompt '${escapedGoal}' -f '${escapedFilePath}' -o text > ${tempFile} 2>&1`;
|
|
31
|
+
const tmuxCommand = `tmux -S ${TMUX_SOCKET} send-keys -t ${TMUX_SESSION_TARGET} -l -- '${command}' && sleep 0.1 && tmux -S ${TMUX_SOCKET} send-keys -t ${TMUX_SESSION_TARGET} Enter`;
|
|
32
|
+
execSync(tmuxCommand, { timeout: 5000 });
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
while (Date.now() - startTime < GEMINI_TIMEOUT_MS) {
|
|
35
|
+
try {
|
|
36
|
+
const stat = await fs.stat(tempFile);
|
|
37
|
+
if (stat.size > 0) {
|
|
38
|
+
const result = await fs.readFile(tempFile, 'utf-8');
|
|
39
|
+
await fs.unlink(tempFile);
|
|
40
|
+
return { content: [{ type: 'text', text: result }] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await fs.unlink(tempFile);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{ type: 'text', text: 'Error: Gemini CLI timed out after 60 seconds' },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
try {
|
|
58
|
+
await fs.unlink(tempFile);
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }] };
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
optional: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { TOOL_PREFIX } from '../types.js';
|
|
3
|
+
import { isValidCategory } from '../utils/validation.js';
|
|
4
|
+
const CATEGORY_MODELS = {
|
|
5
|
+
quick: 'cliproxy/claude-sonnet-4-6',
|
|
6
|
+
deep: 'cliproxy/claude-opus-4-6-thinking',
|
|
7
|
+
ultrabrain: 'cliproxy/gpt-5.3-codex',
|
|
8
|
+
'visual-engineering': 'cliproxy/gemini-3.1-pro',
|
|
9
|
+
multimodal: 'gemini-2.5-flash',
|
|
10
|
+
artistry: 'cliproxy/claude-opus-4-6-thinking',
|
|
11
|
+
'unspecified-low': 'cliproxy/claude-sonnet-4-6',
|
|
12
|
+
'unspecified-high': 'cliproxy/claude-opus-4-6-thinking',
|
|
13
|
+
writing: 'cliproxy/claude-sonnet-4-6',
|
|
14
|
+
};
|
|
15
|
+
const DelegateParamsSchema = Type.Object({
|
|
16
|
+
task_description: Type.String({ description: 'What the sub-agent should do' }),
|
|
17
|
+
category: Type.String({ description: 'Task category for model routing (quick, deep, ultrabrain, etc.)' }),
|
|
18
|
+
skills: Type.Optional(Type.Array(Type.String(), { description: 'Skill names to load' })),
|
|
19
|
+
background: Type.Optional(Type.Boolean({ description: 'Run in background (default: false)', default: false })),
|
|
20
|
+
});
|
|
21
|
+
export function registerDelegateTool(api) {
|
|
22
|
+
api.registerTool({
|
|
23
|
+
name: `${TOOL_PREFIX}delegate`,
|
|
24
|
+
description: 'Delegate a task to a sub-agent with category-based model routing',
|
|
25
|
+
parameters: DelegateParamsSchema,
|
|
26
|
+
execute: async (params) => {
|
|
27
|
+
const validCategories = Object.keys(CATEGORY_MODELS);
|
|
28
|
+
if (!isValidCategory(params.category)) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
error: `Invalid category: ${params.category}`,
|
|
35
|
+
valid_categories: validCategories,
|
|
36
|
+
}, null, 2),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const model = CATEGORY_MODELS[params.category];
|
|
42
|
+
api.logger.info('[omoc] Delegating task:', { category: params.category, model });
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
action: 'delegate',
|
|
49
|
+
task_description: params.task_description,
|
|
50
|
+
category: params.category,
|
|
51
|
+
model,
|
|
52
|
+
skills: params.skills || [],
|
|
53
|
+
background: params.background || false,
|
|
54
|
+
}, null, 2),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
optional: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export declare const ABSOLUTE_MAX_RALPH_ITERATIONS = 100;
|
|
2
|
+
export declare const TOOL_PREFIX = "omoc_";
|
|
3
|
+
export declare const PLUGIN_ID = "oh-my-openclaw";
|
|
4
|
+
export interface PluginConfig {
|
|
5
|
+
max_ralph_iterations: number;
|
|
6
|
+
todo_enforcer_enabled: boolean;
|
|
7
|
+
todo_enforcer_cooldown_ms: number;
|
|
8
|
+
todo_enforcer_max_failures: number;
|
|
9
|
+
comment_checker_enabled: boolean;
|
|
10
|
+
notepad_dir: string;
|
|
11
|
+
plans_dir: string;
|
|
12
|
+
checkpoint_dir: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RalphLoopState {
|
|
15
|
+
active: boolean;
|
|
16
|
+
iteration: number;
|
|
17
|
+
maxIterations: number;
|
|
18
|
+
taskFile: string;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CheckpointData {
|
|
22
|
+
type: 'session-checkpoint';
|
|
23
|
+
session_id: string;
|
|
24
|
+
task: string;
|
|
25
|
+
step: string;
|
|
26
|
+
changed_files: string[];
|
|
27
|
+
verification: {
|
|
28
|
+
diagnostics: 'pass' | 'fail' | 'not-run';
|
|
29
|
+
tests: 'pass' | 'fail' | 'not-run';
|
|
30
|
+
build: 'pass' | 'fail' | 'not-run';
|
|
31
|
+
};
|
|
32
|
+
next_action: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}
|
|
35
|
+
export interface TodoItem {
|
|
36
|
+
id: string;
|
|
37
|
+
content: string;
|
|
38
|
+
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
|
39
|
+
priority: string;
|
|
40
|
+
}
|
|
41
|
+
export interface CommentViolation {
|
|
42
|
+
file: string;
|
|
43
|
+
line: number;
|
|
44
|
+
content: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
}
|
|
47
|
+
export interface DelegationParams {
|
|
48
|
+
task: string;
|
|
49
|
+
category: string;
|
|
50
|
+
agentId?: string;
|
|
51
|
+
skills?: string[];
|
|
52
|
+
context?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface CategoryConfig {
|
|
55
|
+
model: string;
|
|
56
|
+
description: string;
|
|
57
|
+
agents?: string[];
|
|
58
|
+
alternatives?: string[];
|
|
59
|
+
tool?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface OmocPluginApi {
|
|
62
|
+
config: PluginConfig;
|
|
63
|
+
logger: {
|
|
64
|
+
info: (...args: any[]) => void;
|
|
65
|
+
warn: (...args: any[]) => void;
|
|
66
|
+
error: (...args: any[]) => void;
|
|
67
|
+
};
|
|
68
|
+
registerHook: (event: string, handler: any, meta?: any) => void;
|
|
69
|
+
registerTool: (config: any) => void;
|
|
70
|
+
registerCommand: (config: any) => void;
|
|
71
|
+
registerService: (config: any) => void;
|
|
72
|
+
registerGatewayMethod: (name: string, handler: any) => void;
|
|
73
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ABSOLUTE_MAX_RALPH_ITERATIONS } from '../types.js';
|
|
2
|
+
export function getConfig(api) {
|
|
3
|
+
const defaults = {
|
|
4
|
+
max_ralph_iterations: 10,
|
|
5
|
+
todo_enforcer_enabled: true,
|
|
6
|
+
todo_enforcer_cooldown_ms: 2000,
|
|
7
|
+
todo_enforcer_max_failures: 5,
|
|
8
|
+
comment_checker_enabled: true,
|
|
9
|
+
notepad_dir: 'workspace/notepads',
|
|
10
|
+
plans_dir: 'workspace/plans',
|
|
11
|
+
checkpoint_dir: 'workspace/checkpoints',
|
|
12
|
+
};
|
|
13
|
+
const config = { ...defaults, ...api.config };
|
|
14
|
+
const validation = validateConfig(config);
|
|
15
|
+
if (!validation.valid) {
|
|
16
|
+
api.logger.warn(`Config validation failed: ${validation.errors.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
if (config.max_ralph_iterations > ABSOLUTE_MAX_RALPH_ITERATIONS) {
|
|
19
|
+
config.max_ralph_iterations = ABSOLUTE_MAX_RALPH_ITERATIONS;
|
|
20
|
+
}
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
export function validateConfig(config) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (config.max_ralph_iterations !== undefined) {
|
|
26
|
+
if (config.max_ralph_iterations < 0 || config.max_ralph_iterations > ABSOLUTE_MAX_RALPH_ITERATIONS) {
|
|
27
|
+
errors.push(`max_ralph_iterations must be between 0 and ${ABSOLUTE_MAX_RALPH_ITERATIONS}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (config.todo_enforcer_cooldown_ms !== undefined) {
|
|
31
|
+
if (config.todo_enforcer_cooldown_ms <= 0) {
|
|
32
|
+
errors.push('todo_enforcer_cooldown_ms must be greater than 0');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (config.todo_enforcer_max_failures !== undefined) {
|
|
36
|
+
if (config.todo_enforcer_max_failures < 1) {
|
|
37
|
+
errors.push('todo_enforcer_max_failures must be at least 1');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
valid: errors.length === 0,
|
|
42
|
+
errors,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
export async function readState(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
6
|
+
return JSON.parse(data);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error.code === 'ENOENT') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function writeState(filePath, data) {
|
|
16
|
+
await ensureDir(dirname(filePath));
|
|
17
|
+
const jsonContent = JSON.stringify(data, null, 2);
|
|
18
|
+
await fs.writeFile(filePath, jsonContent, 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
export async function ensureDir(dirPath) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'EEXIST') {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TOOL_PREFIX, ABSOLUTE_MAX_RALPH_ITERATIONS } from '../types.js';
|
|
2
|
+
const VALID_CATEGORIES = [
|
|
3
|
+
'quick',
|
|
4
|
+
'deep',
|
|
5
|
+
'ultrabrain',
|
|
6
|
+
'visual-engineering',
|
|
7
|
+
'multimodal',
|
|
8
|
+
'artistry',
|
|
9
|
+
'unspecified-low',
|
|
10
|
+
'unspecified-high',
|
|
11
|
+
'writing',
|
|
12
|
+
];
|
|
13
|
+
export function isValidCategory(cat) {
|
|
14
|
+
return VALID_CATEGORIES.includes(cat);
|
|
15
|
+
}
|
|
16
|
+
export function sanitizeToolName(name) {
|
|
17
|
+
let sanitized = name.toLowerCase();
|
|
18
|
+
sanitized = sanitized.replace(/[^a-z0-9_]/g, '_');
|
|
19
|
+
if (!sanitized.startsWith(TOOL_PREFIX)) {
|
|
20
|
+
sanitized = TOOL_PREFIX + sanitized;
|
|
21
|
+
}
|
|
22
|
+
return sanitized;
|
|
23
|
+
}
|
|
24
|
+
export function clampIterations(n, max = ABSOLUTE_MAX_RALPH_ITERATIONS) {
|
|
25
|
+
return Math.max(0, Math.min(n, max));
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "oh-my-openclaw",
|
|
3
|
+
"name": "Oh-My-OpenClaw",
|
|
4
|
+
"description": "Multi-agent orchestration plugin with todo enforcer, ralph loop, comment checker, and custom delegation tools",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"skills": ["../skills"],
|
|
7
|
+
"tools": ["omoc_delegate", "omoc_look_at", "omoc_checkpoint"],
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"max_ralph_iterations": {
|
|
13
|
+
"type": "number",
|
|
14
|
+
"default": 10,
|
|
15
|
+
"description": "Maximum ralph loop iterations before auto-stop"
|
|
16
|
+
},
|
|
17
|
+
"todo_enforcer_enabled": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": true,
|
|
20
|
+
"description": "Enable todo continuation enforcement on session idle"
|
|
21
|
+
},
|
|
22
|
+
"todo_enforcer_cooldown_ms": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"default": 2000,
|
|
25
|
+
"description": "Cooldown in ms before injecting continuation prompt"
|
|
26
|
+
},
|
|
27
|
+
"todo_enforcer_max_failures": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"default": 5,
|
|
30
|
+
"description": "Max consecutive failures before extended pause"
|
|
31
|
+
},
|
|
32
|
+
"comment_checker_enabled": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"default": true,
|
|
35
|
+
"description": "Enable AI slop comment detection"
|
|
36
|
+
},
|
|
37
|
+
"notepad_dir": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "workspace/notepads",
|
|
40
|
+
"description": "Directory for wisdom notepad files"
|
|
41
|
+
},
|
|
42
|
+
"plans_dir": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"default": "workspace/plans",
|
|
45
|
+
"description": "Directory for plan files"
|
|
46
|
+
},
|
|
47
|
+
"checkpoint_dir": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"default": "workspace/checkpoints",
|
|
50
|
+
"description": "Directory for checkpoint files"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@happycastle/oh-my-openclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Oh-My-OpenClaw plugin — multi-agent orchestration, todo enforcer, ralph loop, and custom tools for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"test": "node --experimental-vm-modules node_modules/.bin/vitest run",
|
|
12
|
+
"test:watch": "node --experimental-vm-modules node_modules/.bin/vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"lint": "tsc --noEmit",
|
|
15
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"openclaw": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"dist/index.js"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"openclaw.plugin.json",
|
|
35
|
+
"skills"
|
|
36
|
+
],
|
|
37
|
+
"keywords": [
|
|
38
|
+
"openclaw",
|
|
39
|
+
"plugin",
|
|
40
|
+
"multi-agent",
|
|
41
|
+
"orchestration"
|
|
42
|
+
],
|
|
43
|
+
"license": "UNLICENSED",
|
|
44
|
+
"author": "happycastle",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@sinclair/typebox": "^0.34.0",
|
|
47
|
+
"@types/node": "^25.3.0",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"vitest": "^3.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|