feat: add development and hf-hangman-lab skills
development: absorbed openclaw-plugin-dev as a workflow, with reference docs for plugin structure, hooks, tools, state, config. hf-hangman-lab: hf-wakeup workflow — full agent wakeup lifecycle: set busy → check due slots → select & defer → identify task → plan work → create work channel → execute. All branches end with status reset to idle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
development/SKILL.md
Normal file
12
development/SKILL.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: development
|
||||
description: Software development workflows — plugin development, code review, and implementation patterns. Use when building OpenClaw plugins, implementing features, or following development processes.
|
||||
---
|
||||
|
||||
## Workflows
|
||||
|
||||
- `{baseDir}/workflows/openclaw-plugin-development.md` — When creating or modifying OpenClaw plugins. Covers project structure, entry points, hooks, tools, state management, config, and debugging.
|
||||
|
||||
## Reference
|
||||
|
||||
- `{baseDir}/docs/` — Plugin development reference docs (structure, hooks, tools, state, config, debugging).
|
||||
61
development/docs/config.md
Normal file
61
development/docs/config.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Config Schema & Install Scripts
|
||||
|
||||
## openclaw.plugin.json
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openclaw.ai/schemas/plugin-config.json",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"myToken": { "type": "string" },
|
||||
"myFlag": { "type": "boolean", "default": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- `additionalProperties: false` is mandatory — OpenClaw validates config against schema
|
||||
- Removing a config field requires removing it from schema too (or gateway fails to start)
|
||||
- Sensitive fields (tokens, keys): no `default`, user must configure manually
|
||||
|
||||
## Install Script (scripts/install.mjs)
|
||||
|
||||
### Install does:
|
||||
|
||||
1. Build dist (compile TypeScript)
|
||||
2. Clean and copy to `~/.openclaw/plugins/<id>/`
|
||||
3. Update `openclaw.json`:
|
||||
- Add to `plugins.allow`
|
||||
- Add to `plugins.load.paths`
|
||||
- Set `plugins.entries.<id>.enabled = true`
|
||||
- Set default config via `setIfMissing` (never overwrite existing values)
|
||||
4. Never set sensitive fields (tokens) — add comments for manual configuration
|
||||
|
||||
### Uninstall does:
|
||||
|
||||
1. Remove from `plugins.allow`
|
||||
2. Delete `plugins.entries.<id>`
|
||||
3. Remove from `plugins.load.paths`
|
||||
4. Delete install directory
|
||||
|
||||
### setIfMissing pattern
|
||||
|
||||
```javascript
|
||||
function setIfMissing(obj, key, value) {
|
||||
if (obj[key] === undefined || obj[key] === null) {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Config field changes
|
||||
|
||||
When renaming or removing config fields:
|
||||
1. Update `openclaw.plugin.json` schema first (source of truth)
|
||||
2. Update `normalizeConfig()` in index.ts
|
||||
3. Update install script `setIfMissing` calls
|
||||
4. Document migration steps for users
|
||||
46
development/docs/debugging.md
Normal file
46
development/docs/debugging.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Development & Debugging
|
||||
|
||||
## Dev Loop
|
||||
|
||||
```bash
|
||||
# 1. Modify code in plugin/
|
||||
# 2. Reinstall
|
||||
node scripts/install.mjs --install
|
||||
|
||||
# 3. Restart gateway
|
||||
openclaw gateway restart
|
||||
|
||||
# 4. Watch logs
|
||||
openclaw logs --follow
|
||||
|
||||
# 5. Test
|
||||
```
|
||||
|
||||
## Log Keywords
|
||||
|
||||
| Keyword | Meaning |
|
||||
|---------|---------|
|
||||
| `plugin registered` | register() completed |
|
||||
| `startSideCar called` / `already running` | Sidecar status |
|
||||
| `must NOT have additional properties` | Schema vs config mismatch |
|
||||
| `EADDRINUSE` | Port conflict (sidecar or HTTP server) |
|
||||
|
||||
## Checklist Before Deploy
|
||||
|
||||
### General
|
||||
|
||||
- [ ] All hook handlers have event dedup (globalThis, not module-level)
|
||||
- [ ] Gateway lifecycle events protected by globalThis flag
|
||||
- [ ] Business state on globalThis
|
||||
- [ ] `openclaw.plugin.json` schema matches actual config fields
|
||||
- [ ] Install script doesn't set schema-absent fields
|
||||
- [ ] Sensitive fields not set by install script
|
||||
- [ ] Install script cleans old dist before copying
|
||||
|
||||
### Connection-type plugins
|
||||
|
||||
- [ ] Start flag on globalThis (not module-level `let`)
|
||||
- [ ] Runtime reference on globalThis
|
||||
- [ ] Shared registries/callbacks on globalThis, init once
|
||||
- [ ] Cross-plugin API object overwritten every register() but runtime started once
|
||||
- [ ] Consumer plugins defend against provider not loaded
|
||||
49
development/docs/entry-point.md
Normal file
49
development/docs/entry-point.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Plugin Entry Point
|
||||
|
||||
## Format
|
||||
|
||||
```typescript
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
|
||||
const _G = globalThis as Record<string, unknown>;
|
||||
const LIFECYCLE_KEY = "_myPluginGatewayLifecycleRegistered";
|
||||
|
||||
export default {
|
||||
id: "my-plugin",
|
||||
name: "My Plugin",
|
||||
register(api: OpenClawPluginApi) {
|
||||
const config = normalizeConfig(api);
|
||||
|
||||
// Gateway lifecycle: only once
|
||||
if (!_G[LIFECYCLE_KEY]) {
|
||||
_G[LIFECYCLE_KEY] = true;
|
||||
// Start sidecars, init global resources
|
||||
api.on("gateway_stop", () => { _G[LIFECYCLE_KEY] = false; });
|
||||
}
|
||||
|
||||
// Agent session hooks: every register() call (dedup inside handler)
|
||||
registerMyHook(api, config);
|
||||
|
||||
// Tools
|
||||
registerMyTools(api, config);
|
||||
|
||||
api.logger.info("my-plugin: registered");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Why globalThis?
|
||||
|
||||
OpenClaw may hot-reload plugins. Module-level variables reset on reload, but `globalThis` persists. All mutable state must be on `globalThis`:
|
||||
|
||||
- Startup flags → prevent double initialization
|
||||
- Dedup sets → prevent double hook execution
|
||||
- Runtime references → keep connections alive across reloads
|
||||
- Shared registries → preserve cross-plugin state
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```
|
||||
_<pluginId>PluginXxx # Internal state (e.g., _prismFacetRouters)
|
||||
__<pluginId> # Cross-plugin public API (e.g., __yonexusClient)
|
||||
```
|
||||
62
development/docs/hooks.md
Normal file
62
development/docs/hooks.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Hook Registration
|
||||
|
||||
## Available Hooks
|
||||
|
||||
| Hook | Purpose | Dedup Method |
|
||||
|------|---------|-------------|
|
||||
| `before_model_resolve` | Override model/provider before LLM call | WeakSet on event |
|
||||
| `before_prompt_build` | Inject into system prompt | WeakSet on event |
|
||||
| `before_agent_start` | Legacy prompt injection | WeakSet on event |
|
||||
| `agent_end` | Post-conversation actions | Set on runId |
|
||||
| `message_received` | React to inbound messages | — |
|
||||
| `llm_input` / `llm_output` | Observe LLM traffic | — |
|
||||
| `before_tool_call` / `after_tool_call` | Observe tool execution | — |
|
||||
| `gateway_start` / `gateway_stop` | Gateway lifecycle | globalThis flag |
|
||||
|
||||
## Dedup Patterns
|
||||
|
||||
### WeakSet (for event-object hooks)
|
||||
|
||||
```typescript
|
||||
const _G = globalThis as Record<string, unknown>;
|
||||
const DEDUP_KEY = "_myPluginHookDedup";
|
||||
if (!(_G[DEDUP_KEY] instanceof WeakSet)) _G[DEDUP_KEY] = new WeakSet<object>();
|
||||
const dedup = _G[DEDUP_KEY] as WeakSet<object>;
|
||||
|
||||
api.on("before_model_resolve", async (event, ctx) => {
|
||||
if (dedup.has(event as object)) return;
|
||||
dedup.add(event as object);
|
||||
// ... handler logic
|
||||
});
|
||||
```
|
||||
|
||||
### Set with runId (for agent_end)
|
||||
|
||||
```typescript
|
||||
const DEDUP_KEY = "_myPluginAgentEndDedup";
|
||||
if (!(_G[DEDUP_KEY] instanceof Set)) _G[DEDUP_KEY] = new Set<string>();
|
||||
const dedup = _G[DEDUP_KEY] as Set<string>;
|
||||
|
||||
api.on("agent_end", async (event, ctx) => {
|
||||
const runId = (event as any).runId as string;
|
||||
if (runId) {
|
||||
if (dedup.has(runId)) return;
|
||||
dedup.add(runId);
|
||||
if (dedup.size > 500) dedup.delete(dedup.values().next().value!);
|
||||
}
|
||||
// ... handler logic
|
||||
});
|
||||
```
|
||||
|
||||
## Prompt Injection Hooks
|
||||
|
||||
`before_prompt_build` and `before_agent_start` can return prompt mutation fields:
|
||||
|
||||
| Field | Caching | Use for |
|
||||
|-------|---------|---------|
|
||||
| `prependSystemContext` | Cached | Static role/identity prompts |
|
||||
| `appendSystemContext` | Cached | Static supplementary guidance |
|
||||
| `prependContext` | Not cached | Per-turn dynamic context |
|
||||
| `systemPrompt` | — | Full system prompt replacement (rarely used) |
|
||||
|
||||
Requires `plugins.entries.<id>.hooks.allowPromptInjection: true` in openclaw.json.
|
||||
48
development/docs/state.md
Normal file
48
development/docs/state.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# State Management
|
||||
|
||||
## Where to Store What
|
||||
|
||||
| Data Type | Location | Reason |
|
||||
|-----------|----------|--------|
|
||||
| Business state (maps, sets, caches) | `globalThis` | Module vars reset on hot reload |
|
||||
| Event dedup (WeakSet/Set) | `globalThis` | Same |
|
||||
| Gateway lifecycle flags | `globalThis` | Prevent double init |
|
||||
| Connection flags (WebSocket/TCP) | `globalThis` | Prevent duplicate connections |
|
||||
| Runtime references | `globalThis` | Closures need living instance |
|
||||
| Cross-plugin API objects (`__pluginId`) | `globalThis` | Other plugins access via globalThis |
|
||||
| Pure utility functions | Module-level | No state needed |
|
||||
| Persistent data | File + memory cache | Survives gateway restart |
|
||||
|
||||
## Cross-Plugin API Pattern
|
||||
|
||||
### Provider side
|
||||
|
||||
```typescript
|
||||
const _G = globalThis as Record<string, unknown>;
|
||||
|
||||
// Init shared objects once
|
||||
if (!(_G["_myRegistry"] instanceof MyRegistry)) {
|
||||
_G["_myRegistry"] = new MyRegistry();
|
||||
}
|
||||
|
||||
// Overwrite public API every register() (updates closures)
|
||||
_G["__myPlugin"] = {
|
||||
registry: _G["_myRegistry"],
|
||||
send: (msg) => (_G["_myRuntime"] as Runtime)?.send(msg) ?? false,
|
||||
};
|
||||
```
|
||||
|
||||
### Consumer side
|
||||
|
||||
```typescript
|
||||
const provider = (globalThis as any)["__myPlugin"];
|
||||
if (!provider) {
|
||||
console.error("[consumer] __myPlugin not found");
|
||||
return;
|
||||
}
|
||||
provider.registry.register("my_rule", handler);
|
||||
```
|
||||
|
||||
### Load Order
|
||||
|
||||
Provider must be listed before consumer in `plugins.allow`. Consumer must defend against provider not being loaded.
|
||||
26
development/docs/structure.md
Normal file
26
development/docs/structure.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Plugin Project Structure
|
||||
|
||||
```
|
||||
proj-root/
|
||||
plugin/ # Installable plugin (copied to ~/.openclaw/plugins/<id>/)
|
||||
index.ts # Entry: export default { id, name, register }
|
||||
openclaw.plugin.json # Config schema declaration
|
||||
package.json # name, version, type: module
|
||||
hooks/ # Hook handlers (one file per hook)
|
||||
tools/ # Tool registrations
|
||||
core/ # Pure business logic (no plugin-sdk imports)
|
||||
web/ # HTTP routes (optional)
|
||||
services/ # Sidecar processes (optional, installed alongside plugin)
|
||||
skills/ # OpenClaw skills provided by this plugin (optional)
|
||||
routers/ # Drop-in modules (plugin-specific, e.g., PrismFacet routers)
|
||||
scripts/
|
||||
install.mjs # --install / --uninstall / --update
|
||||
docs/ # Documentation
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- File names: kebab-case (`before-model-resolve.ts`)
|
||||
- Export names: camelCase (`registerBeforeModelResolve`)
|
||||
- `plugin/core/` must not import from `openclaw/plugin-sdk` — keeps logic unit-testable
|
||||
- Hook registration logic goes in `plugin/hooks/`, not in `index.ts`
|
||||
40
development/docs/tools.md
Normal file
40
development/docs/tools.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Tool Registration
|
||||
|
||||
## Interface
|
||||
|
||||
```typescript
|
||||
// Without agent context
|
||||
api.registerTool({
|
||||
name: "my-tool",
|
||||
description: "Does something",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
param: { type: "string", description: "..." },
|
||||
},
|
||||
required: ["param"],
|
||||
},
|
||||
execute: async (toolCallId, params) => {
|
||||
const { param } = params as { param: string };
|
||||
return { result: "ok" };
|
||||
},
|
||||
});
|
||||
|
||||
// With agent context (factory function)
|
||||
api.registerTool((ctx) => ({
|
||||
name: "my-contextual-tool",
|
||||
description: "...",
|
||||
inputSchema: { /* ... */ },
|
||||
execute: async (toolCallId, params) => {
|
||||
const agentId = ctx.agentId;
|
||||
return { result: agentId };
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Interface is `execute: async (toolCallId, params)`, NOT `handler:`
|
||||
- Use `inputSchema` (JSON Schema), NOT `parameters`
|
||||
- Return `{ result: "..." }` object
|
||||
- Factory form `(ctx) => ({...})` gives access to agent context (agentId, sessionKey, etc.)
|
||||
128
development/workflows/openclaw-plugin-development.md
Normal file
128
development/workflows/openclaw-plugin-development.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# OpenClaw Plugin Development
|
||||
|
||||
When creating or modifying an OpenClaw plugin.
|
||||
|
||||
> Reference docs in `{baseDir}/docs/` cover each topic in detail.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Scaffold
|
||||
|
||||
Create the project structure:
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
plugin/
|
||||
index.ts # export default { id, name, register }
|
||||
openclaw.plugin.json # config schema (additionalProperties: false)
|
||||
package.json # name, version, type: module
|
||||
hooks/ # one file per hook handler
|
||||
tools/ # tool registrations
|
||||
core/ # pure logic (no plugin-sdk imports)
|
||||
scripts/
|
||||
install.mjs # --install / --uninstall
|
||||
package.json # dev dependencies
|
||||
tsconfig.plugin.json
|
||||
.gitignore
|
||||
```
|
||||
|
||||
See `{baseDir}/docs/structure.md` for conventions.
|
||||
|
||||
### 2. Entry Point
|
||||
|
||||
Follow the globalThis lifecycle pattern:
|
||||
|
||||
```typescript
|
||||
const _G = globalThis as Record<string, unknown>;
|
||||
const LIFECYCLE_KEY = "_myPluginLifecycleRegistered";
|
||||
|
||||
export default {
|
||||
id: "my-plugin",
|
||||
name: "My Plugin",
|
||||
register(api) {
|
||||
if (!_G[LIFECYCLE_KEY]) {
|
||||
_G[LIFECYCLE_KEY] = true;
|
||||
// gateway-level init (once)
|
||||
api.on("gateway_stop", () => { _G[LIFECYCLE_KEY] = false; });
|
||||
}
|
||||
// agent-level hooks (every register call, dedup inside)
|
||||
registerMyHook(api);
|
||||
registerMyTools(api);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
See `{baseDir}/docs/entry-point.md` for details.
|
||||
|
||||
### 3. Hooks
|
||||
|
||||
Each hook in its own file under `hooks/`. Must use dedup on globalThis:
|
||||
|
||||
- `before_model_resolve`, `before_prompt_build` → WeakSet on event object
|
||||
- `agent_end` → Set on runId with size cap 500
|
||||
- `gateway_start/stop` → globalThis flag
|
||||
|
||||
If returning `prependSystemContext`/`appendSystemContext`, set `allowPromptInjection: true` in config.
|
||||
|
||||
See `{baseDir}/docs/hooks.md`.
|
||||
|
||||
### 4. Tools
|
||||
|
||||
Interface is `inputSchema` + `execute` (not `parameters` + `handler`):
|
||||
|
||||
```typescript
|
||||
api.registerTool({
|
||||
name: "my-tool",
|
||||
inputSchema: { type: "object", properties: { ... }, required: [...] },
|
||||
execute: async (toolCallId, params) => { return { result: "ok" }; },
|
||||
});
|
||||
```
|
||||
|
||||
For agent context access, use factory form: `api.registerTool((ctx) => ({ ... }))`.
|
||||
|
||||
See `{baseDir}/docs/tools.md`.
|
||||
|
||||
### 5. State Management
|
||||
|
||||
All mutable state on `globalThis`, not module-level variables (hot reload resets modules).
|
||||
|
||||
- Business state, dedup sets, lifecycle flags → `globalThis`
|
||||
- Cross-plugin API → `globalThis.__pluginId`
|
||||
- Pure functions → module-level is fine
|
||||
|
||||
See `{baseDir}/docs/state.md`.
|
||||
|
||||
### 6. Config Schema
|
||||
|
||||
`openclaw.plugin.json` must have `additionalProperties: false`. Every config field in install script must exist in schema. Never set sensitive fields (tokens) in install script.
|
||||
|
||||
See `{baseDir}/docs/config.md`.
|
||||
|
||||
### 7. Install Script
|
||||
|
||||
```bash
|
||||
node scripts/install.mjs --install # build, copy to ~/.openclaw/plugins/<id>, update config
|
||||
node scripts/install.mjs --uninstall # remove from config and filesystem
|
||||
```
|
||||
|
||||
See `{baseDir}/docs/config.md`.
|
||||
|
||||
### 8. Build & Test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
node scripts/install.mjs --install
|
||||
openclaw gateway restart
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
### 9. Checklist Before Deploy
|
||||
|
||||
- [ ] Hook handlers have dedup on globalThis
|
||||
- [ ] Gateway lifecycle protected by globalThis flag
|
||||
- [ ] Business state on globalThis
|
||||
- [ ] Schema matches actual config fields
|
||||
- [ ] Install script uses setIfMissing, no sensitive fields
|
||||
- [ ] Clean dist before copy (`rmSync`)
|
||||
|
||||
See `{baseDir}/docs/debugging.md` for full checklist.
|
||||
Reference in New Issue
Block a user