diff --git a/README.md b/README.md index a9a43140..b1d79acc 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,19 @@ For bonus points, try `/deepwork learn` after running your workflow as well, and +### OpenClaw + +OpenClaw support is installed from the bundle in [`plugins/openclaw/`](./plugins/openclaw/), while the DeepWork runtime comes from an installed `deepwork` CLI. + +See [plugins/openclaw/README.md](./plugins/openclaw/README.md) for the full instructions. The short version is: + +1. Clone this repo. +2. Install a DeepWork runtime that supports `deepwork serve --platform openclaw`. +3. Install the OpenClaw bundle from `plugins/openclaw/`. +4. Restart the OpenClaw gateway. + +Most users should stop there. If you are testing unreleased DeepWork changes or OpenClaw is launching the wrong DeepWork binary, the OpenClaw guide also covers the exact `mcp.servers.deepwork` override to use. + --- ## The Problem diff --git a/plugins/openclaw/.codex-plugin/plugin.json b/plugins/openclaw/.codex-plugin/plugin.json new file mode 100644 index 00000000..8ee8fecc --- /dev/null +++ b/plugins/openclaw/.codex-plugin/plugin.json @@ -0,0 +1,7 @@ +{ + "name": "deepwork", + "description": "Framework for AI-powered multi-step workflows with quality gates in OpenClaw", + "version": "0.11.0", + "skills": ["skills"], + "hooks": ["hooks"] +} diff --git a/plugins/openclaw/.mcp.json b/plugins/openclaw/.mcp.json new file mode 100644 index 00000000..cfb60b09 --- /dev/null +++ b/plugins/openclaw/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "deepwork": { + "command": "uvx", + "args": ["deepwork", "serve", "--platform", "openclaw"] + } + } +} diff --git a/plugins/openclaw/README.md b/plugins/openclaw/README.md new file mode 100644 index 00000000..9e324edb --- /dev/null +++ b/plugins/openclaw/README.md @@ -0,0 +1,216 @@ +# DeepWork for OpenClaw + +This bundle lets OpenClaw use DeepWork's workflow and review MCP tools without relying on Claude-specific hooks. + +It packages three things together: + +- OpenClaw skills for DeepWork workflows and reviews +- an OpenClaw bootstrap hook that injects DeepWork session guidance +- an MCP config that starts `uvx deepwork serve --platform openclaw` + +## What This Bundle Adds + +When OpenClaw installs this directory as a bundle, it loads: + +- [`.codex-plugin/plugin.json`](./.codex-plugin/plugin.json) so the directory is detected as a Codex bundle +- [`skills/deepwork/SKILL.md`](./skills/deepwork/SKILL.md) for workflow execution guidance +- [`skills/review/SKILL.md`](./skills/review/SKILL.md) for review execution guidance +- [`hooks/deepwork-openclaw-bootstrap/HOOK.md`](./hooks/deepwork-openclaw-bootstrap/HOOK.md) and [`handler.ts`](./hooks/deepwork-openclaw-bootstrap/handler.ts) to inject session and resume context into OpenClaw bootstrap +- [`.mcp.json`](./.mcp.json) so OpenClaw exposes DeepWork MCP tools as `deepwork__` + +This is an OpenClaw-native bundle shape. It does not depend on Claude's `hooks.json` automation. + +## Prerequisites + +- OpenClaw with bundle support +- a DeepWork runtime that supports `deepwork serve --platform openclaw` +- `uv` installed, because the bundle launches DeepWork with `uvx` +- a Git repository for the target project + +DeepWork stores job definitions under `.deepwork/` and may create work branches while executing workflows. If the target project is not already a Git repository, initialize it first. + +## Install + +### 1. Clone DeepWork + +OpenClaw installs the bundle from this directory, so start by cloning the DeepWork repo: + +```bash +git clone https://github.com/Unsupervisedcom/deepwork.git +cd deepwork +``` + +If you are testing an unreleased branch, check out that branch before continuing. Otherwise stay on the default branch or a released tag. + +### 2. Install DeepWork + +```bash +uv tool install deepwork +``` + +This installs the `deepwork` CLI that the OpenClaw bundle launches over MCP. + +Verify that the installed runtime supports OpenClaw: + +```bash +deepwork serve --platform openclaw +``` + +If that command fails, upgrade to a newer DeepWork release before installing the bundle. + +### 3. Install the OpenClaw bundle + +Install the bundle from this checkout: + +```bash +openclaw plugins install /path/to/deepwork/plugins/openclaw +openclaw plugins inspect deepwork +openclaw gateway restart +``` + +Expected output from `openclaw plugins inspect deepwork`: + +- `Format: bundle` +- bundle subtype `codex` +- skill roots from `skills/` +- a hook pack from `hooks/` +- an MCP server named `deepwork` + +## First Run + +Start a new OpenClaw session after installing the bundle. + +Typical user prompts: + +- `Use DeepWork to create a workflow for shipping release notes.` +- `Use DeepWork to run the tutorial_writer workflow.` +- `Use DeepWork review on this change set.` + +The agent should then: + +1. Read the injected DeepWork bootstrap note. +2. Use the current OpenClaw `sessionId` as DeepWork `session_id`. +3. Call `deepwork__get_active_workflow` before starting something new if prior DeepWork state exists. +4. Use `deepwork__get_workflows` and `deepwork__start_workflow` to execute workflows. +5. Before `deepwork__finished_step`, compare your payload to `step_expected_outputs` or call `deepwork__validate_step_outputs`. +6. Use `deepwork__get_review_instructions` and launch review tasks with `sessions_spawn`. + +## Runtime Contract + +This bundle deliberately keeps the DeepWork runtime mostly unchanged and adapts OpenClaw to it. + +- DeepWork `session_id` maps to the current OpenClaw `sessionId` +- DeepWork `agent_id` is usually left unset in OpenClaw +- the bootstrap hook writes a synthetic note into bootstrap context so the agent sees the correct `session_id` +- if DeepWork state already exists for the current session, the hook tells the agent to call `deepwork__get_active_workflow` + +DeepWork session state for this bundle lives under: + +```text +.deepwork/tmp/sessions/openclaw/session-/state.json +``` + +## Reviews in OpenClaw + +DeepWork quality gates can return review tasks. In OpenClaw, those should be run as parallel sub-agents with `sessions_spawn`. + +That is why this bundle includes a separate OpenClaw review skill and platform-specific quality-gate formatting. The Claude formatter path is still separate and unchanged. + +## Current Limitation + +This first OpenClaw adapter scopes DeepWork state to the current OpenClaw session. + +That means: + +- resume works reliably inside the same OpenClaw session +- `deepwork__get_active_workflow` works after compaction or reset for that same session +- spawned OpenClaw child sessions do not yet share one root DeepWork workflow stack the way Claude Code can + +This is a host-context limitation, not a workflow-engine limitation. The current OpenClaw bootstrap hook surface does not expose enough parent/root session metadata to recreate DeepWork's Claude-style shared root session model. + +## Troubleshooting + +### The DeepWork tools do not appear in OpenClaw + +Check: + +- the bundle was installed from this directory, not the repo root +- `openclaw plugins inspect deepwork` shows a bundle with an MCP server +- you restarted the OpenClaw gateway after installation +- you started a fresh OpenClaw session after the restart +- the runtime itself starts cleanly: + +```bash +uvx deepwork --version +uvx deepwork serve --platform openclaw +``` + +If `uvx` works in your shell but OpenClaw still does not expose the tools, use the pinned top-level `mcp.servers.deepwork` override shown in the troubleshooting steps below so the gateway launches an exact binary instead of relying on `PATH` or `uvx` resolution. + +### I am testing unreleased DeepWork changes from a local checkout + +Register the checkout as an editable `uv` tool: + +```bash +uv tool install -e /path/to/deepwork +``` + +This makes `uvx deepwork` resolve to your checkout instead of whatever version is published. + +If OpenClaw is still launching the wrong binary, add a top-level `mcp.servers.deepwork` override that points to the exact `deepwork` executable you want: + +```json +{ + "mcp": { + "servers": { + "deepwork": { + "command": "/absolute/path/to/deepwork", + "args": ["serve", "--platform", "openclaw"] + } + } + } +} +``` + +Restart the OpenClaw gateway after changing the override. + +### OpenClaw is launching the wrong DeepWork binary + +Check both launch paths explicitly: + +```bash +uvx deepwork --version +deepwork --version +``` + +Then point OpenClaw at the exact binary you want with a top-level `mcp.servers.deepwork` override: + +```json +{ + "mcp": { + "servers": { + "deepwork": { + "command": "/absolute/path/to/deepwork", + "args": ["serve", "--platform", "openclaw"] + } + } + } +} +``` + +Restart the OpenClaw gateway after changing the override. + +### The agent starts a second workflow instead of resuming + +Tell the agent to call `deepwork__get_active_workflow` first. That is the intended resume path for OpenClaw after session restore, compaction, or ambiguity about current state. + +## Note for Existing Claude Users + +If you already use the Claude Code DeepWork plugin on this machine, you may already have a working DeepWork runtime via `uvx`. Before installing DeepWork separately, check: + +```bash +uvx deepwork --version +uvx deepwork serve --platform openclaw +``` + +If that works, you can use the existing runtime. If it fails, install DeepWork explicitly with `uv tool install deepwork` and then continue with the steps above. diff --git a/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/HOOK.md b/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/HOOK.md new file mode 100644 index 00000000..fad13e75 --- /dev/null +++ b/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/HOOK.md @@ -0,0 +1,19 @@ +--- +name: deepwork-openclaw-bootstrap +description: "Inject DeepWork session and resume guidance into OpenClaw bootstrap context" +metadata: + { + "openclaw": + { + "emoji": "🧭", + "events": ["agent:bootstrap"], + "install": [{ "id": "deepwork", "kind": "bundled", "label": "Bundled with DeepWork OpenClaw plugin" }], + }, + } +--- + +# DeepWork OpenClaw Bootstrap + +Injects a small synthetic bootstrap note that tells the agent which `session_id` +to use for DeepWork MCP tools in the current OpenClaw session, and whether it +should try `deepwork__get_active_workflow` to restore prior DeepWork state. diff --git a/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/handler.ts b/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/handler.ts new file mode 100644 index 00000000..4282aca2 --- /dev/null +++ b/plugins/openclaw/hooks/deepwork-openclaw-bootstrap/handler.ts @@ -0,0 +1,109 @@ +import fs from "node:fs"; +import path from "node:path"; + +const SYNTHETIC_NOTES = [ + { + name: "BOOTSTRAP.md", + relativePath: ".deepwork/tmp/openclaw/DEEPWORK_OPENCLAW_BOOTSTRAP.md", + }, + { + name: "TOOLS.md", + relativePath: ".deepwork/tmp/openclaw/DEEPWORK_OPENCLAW.md", + }, +] as const; + +function readTrimmedString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +const handler = async (event: any) => { + if (event?.type !== "agent" || event?.action !== "bootstrap") { + return; + } + + const context = event.context ?? {}; + if (!Array.isArray(context.bootstrapFiles)) { + return; + } + + const workspaceDir = readTrimmedString(context.workspaceDir); + const sessionId = readTrimmedString(context.sessionId); + if (!workspaceDir || !sessionId) { + return; + } + + const sessionKey = readTrimmedString(context.sessionKey) || "(unknown)"; + const agentId = readTrimmedString(context.agentId) || "(unknown)"; + const syntheticNotes = SYNTHETIC_NOTES.map((note) => ({ + ...note, + path: path.join(workspaceDir, note.relativePath), + })); + const statePath = path.join( + workspaceDir, + ".deepwork", + "tmp", + "sessions", + "openclaw", + `session-${sessionId}`, + "state.json", + ); + const hasActiveState = fs.existsSync(statePath); + + const content = `# DeepWork OpenClaw Runtime + +Use these values when calling DeepWork MCP tools from this OpenClaw session: + +- session_id: \`${sessionId}\` +- session_key: \`${sessionKey}\` +- agent_id: \`${agentId}\` +- workspace_dir: \`${workspaceDir}\` + +Guidance: + +- Use the current OpenClaw session's \`sessionId\` as DeepWork \`session_id\`. +- Ignore any stale \`BOOTSTRAP.md\` files or hardcoded \`session_id\` values elsewhere in the workspace. The current OpenClaw session values above win. +- In OpenClaw, leave DeepWork \`agent_id\` unset unless you intentionally want a separate agent-scoped DeepWork state file. +- DeepWork relative paths are rooted at \`workspace_dir\`, not the plugin bundle directory. +- ${ + hasActiveState + ? "DeepWork state already exists for this session. Call `deepwork__get_active_workflow` before starting a new workflow unless you are sure you want a second one." + : "No DeepWork state has been detected for this session yet." + } +- Before \`deepwork__finished_step\`, compare your outputs to \`step_expected_outputs\` or call \`deepwork__validate_step_outputs\`. +- For review work returned by DeepWork quality gates, prefer parallel OpenClaw sub-agents via \`sessions_spawn\`. +- Spawn all requested review sub-agents before waiting for completions. +- Keep DeepWork instruction paths workspace-relative; do not rewrite them as absolute host paths. +- Omit \`timeoutSeconds\` on review spawns so the runtime default applies. If a timeout value is required, use \`0\`. +- After all review spawns are accepted, use \`sessions_yield\` while you wait for completion events. +`; + + for (const note of syntheticNotes) { + try { + fs.mkdirSync(path.dirname(note.path), { recursive: true }); + fs.writeFileSync(note.path, content, "utf8"); + } catch { + // Best-effort persistence for runtime session hints. The in-memory bootstrap + // injection below still gives the model the same guidance if disk writes fail. + } + } + + context.bootstrapFiles = context.bootstrapFiles + .filter((file: any) => { + const filePath = readTrimmedString(file?.path); + const fileName = readTrimmedString(file?.name); + if (fileName === "BOOTSTRAP.md") { + return false; + } + return !syntheticNotes.some((note) => note.path === filePath); + }) + .concat( + syntheticNotes.map((note) => ({ + name: note.name, + path: note.path, + content, + missing: false, + })), + ); +}; + +export default handler; diff --git a/plugins/openclaw/skills/deepwork/SKILL.md b/plugins/openclaw/skills/deepwork/SKILL.md new file mode 100644 index 00000000..f7db86cb --- /dev/null +++ b/plugins/openclaw/skills/deepwork/SKILL.md @@ -0,0 +1,48 @@ +--- +name: deepwork +description: "Start or continue DeepWork workflows in OpenClaw using MCP tools" +--- + +# DeepWork Workflow Manager + +Execute multi-step DeepWork workflows in OpenClaw. + +## Runtime Contract + +- Read the injected DeepWork OpenClaw bootstrap note before using the tools. +- Use the `session_id` shown there for all DeepWork MCP calls in the current OpenClaw session. +- If the bootstrap note says prior DeepWork state may exist, call `deepwork__get_active_workflow` first to restore context before starting anything new. + +## How to Use + +1. Call `deepwork__get_workflows` to discover available workflows. +2. If resuming, call `deepwork__get_active_workflow`. +3. Call `deepwork__start_workflow` with `goal`, `job_name`, `workflow_name`, and `session_id`. +4. Follow the returned step instructions. +5. If `begin_step.step_inputs` shows any required input with `value: null`, stop before doing step work. +6. If the missing input is already clear from the user's request, restart the workflow with `deepwork__start_workflow(..., inputs={...})` so the value is populated from the beginning. +7. If the missing input is not clear, ask the user instead of fabricating outputs or calling `deepwork__finished_step`. +8. Never call `deepwork__finished_step` for a step whose required inputs are still missing. +9. Before submitting outputs, compare them to `step_expected_outputs` or call `deepwork__validate_step_outputs`. +10. Call `deepwork__finished_step` with your outputs when the step is done. +11. Handle the response: `needs_work`, `next_step`, or `workflow_complete`. + +## Quality Gates + +- DeepWork may require reviews before a step can advance. +- In OpenClaw, prefer launching those reviews as parallel sub-agents with `sessions_spawn`, then use `sessions_yield` to wait for completions. +- Spawn all review sub-agents before waiting, keep instruction paths workspace-relative, and do not set `timeoutSeconds` on review spawns unless you must use `0`. +- After applying any fixes, call `deepwork__finished_step` again. + +## Navigation + +- Use `deepwork__abort_workflow` if a workflow cannot be completed. +- Use `deepwork__go_to_step` to revisit an earlier step and clear later progress. + +## Intent Parsing + +When the user invokes `/deepwork`: + +1. Always call `deepwork__get_workflows`. +2. If the request clearly matches one workflow, start it. +3. If multiple workflows could fit, summarize the closest matches and ask the user which one they want. diff --git a/plugins/openclaw/skills/review/SKILL.md b/plugins/openclaw/skills/review/SKILL.md new file mode 100644 index 00000000..e7a8422a --- /dev/null +++ b/plugins/openclaw/skills/review/SKILL.md @@ -0,0 +1,25 @@ +--- +name: review +description: "Run DeepWork Reviews in OpenClaw using `.deepreview` rules" +--- + +# DeepWork Review + +Run project reviews using DeepWork review rules. + +## How to Run + +1. Call `deepwork__get_configured_reviews` first and summarize the active rules for the user. +2. Call `deepwork__get_review_instructions`. +3. Launch the returned review tasks as parallel OpenClaw sub-agents with `sessions_spawn`. +4. Spawn all requested review sub-agents before waiting, keep instruction paths workspace-relative, and do not set `timeoutSeconds` unless you must use `0`. +5. Collect the findings and apply obvious low-risk fixes immediately. +6. For anything with tradeoffs, summarize the finding and ask the user how they want to proceed. + +## Iterate + +After making changes: + +1. Run `deepwork__get_review_instructions` again. +2. Re-run only the review tasks that still matter, unless the change set was large enough to justify a full rerun. +3. Repeat until the review run is clean or the user explicitly chooses to stop. diff --git a/tests/unit/plugins/test_openclaw_plugin.py b/tests/unit/plugins/test_openclaw_plugin.py new file mode 100644 index 00000000..981e0b8b --- /dev/null +++ b/tests/unit/plugins/test_openclaw_plugin.py @@ -0,0 +1,96 @@ +"""Tests for the OpenClaw bundle scaffold.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import yaml + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent +PLUGIN_DIR = PROJECT_ROOT / "plugins" / "openclaw" +SKILLS_DIR = PLUGIN_DIR / "skills" +HOOKS_DIR = PLUGIN_DIR / "hooks" + + +def _parse_yaml_frontmatter(path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + assert text.startswith("---"), f"{path} must start with YAML frontmatter" + end = text.index("---", 3) + result: dict[str, Any] = yaml.safe_load(text[3:end]) + return result + + +class TestPluginManifest: + manifest_path = PLUGIN_DIR / ".codex-plugin" / "plugin.json" + + def test_manifest_exists(self) -> None: + assert self.manifest_path.exists() + + def test_manifest_declares_skills_and_hooks(self) -> None: + data = json.loads(self.manifest_path.read_text()) + assert data["name"] == "deepwork" + assert data["skills"] == ["skills"] + assert data["hooks"] == ["hooks"] + + +class TestMcpRegistration: + mcp_json_path = PLUGIN_DIR / ".mcp.json" + + def test_mcp_json_exists(self) -> None: + assert self.mcp_json_path.exists() + + def test_mcp_json_uses_openclaw_platform(self) -> None: + data = json.loads(self.mcp_json_path.read_text()) + server = data["mcpServers"]["deepwork"] + assert server["command"] == "uvx" + assert server["args"] == ["deepwork", "serve", "--platform", "openclaw"] + + +class TestDeepworkSkill: + skill_path = SKILLS_DIR / "deepwork" / "SKILL.md" + + def test_skill_exists(self) -> None: + assert self.skill_path.exists() + + def test_skill_frontmatter(self) -> None: + frontmatter = _parse_yaml_frontmatter(self.skill_path) + assert frontmatter["name"] == "deepwork" + + def test_skill_mentions_resume_and_mcp_tools(self) -> None: + content = self.skill_path.read_text(encoding="utf-8") + for token in ( + "deepwork__get_workflows", + "deepwork__get_active_workflow", + "deepwork__start_workflow", + "deepwork__validate_step_outputs", + "deepwork__finished_step", + ): + assert token in content + + +class TestReviewSkill: + skill_path = SKILLS_DIR / "review" / "SKILL.md" + + def test_skill_exists(self) -> None: + assert self.skill_path.exists() + + def test_skill_mentions_openclaw_review_flow(self) -> None: + content = self.skill_path.read_text(encoding="utf-8") + assert "deepwork__get_review_instructions" in content + assert "sessions_spawn" in content + assert "timeoutSeconds" in content + + +class TestBootstrapHook: + hook_dir = HOOKS_DIR / "deepwork-openclaw-bootstrap" + + def test_hook_files_exist(self) -> None: + assert (self.hook_dir / "HOOK.md").exists() + assert (self.hook_dir / "handler.ts").exists() + + def test_hook_is_registered_for_agent_bootstrap(self) -> None: + content = (self.hook_dir / "HOOK.md").read_text(encoding="utf-8") + assert "agent:bootstrap" in content + assert "DeepWork session" in content