diff --git a/.claude/settings.json b/.claude/settings.json index 23d968e8..1221ff5a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,6 +6,8 @@ "WebFetch(domain:anthropic.com)", "WebFetch(domain:platform.claude.com)", "WebFetch(domain:docs.anthropic.com)", + "WebFetch(domain:docs.openclaw.ai)", + "WebFetch(domain:github.com)", "Bash(ls:*)", "Bash(grep:*)", "Bash(find:*)", diff --git a/README.md b/README.md index a9a43140..a1d2828a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,18 @@ For bonus points, try `/deepwork learn` after running your workflow as well, and +### OpenClaw + +OpenClaw support ships as a [Codex bundle](https://docs.openclaw.ai/plugins/bundles) in [`plugins/openclaw/`](./plugins/openclaw/). The bundle launches the DeepWork runtime via `uvx`, so you do not need to install `deepwork` separately — just have `uv` on your PATH. + +```bash +git clone https://github.com/Unsupervisedcom/deepwork.git +openclaw plugins install ./deepwork/plugins/openclaw +openclaw gateway restart +``` + +Start a new OpenClaw session after the restart. See [plugins/openclaw/README.md](./plugins/openclaw/README.md) for the short install guide and [doc/openclaw_design.md](./doc/openclaw_design.md) for how the integration works, limitations, and troubleshooting. + --- ## The Problem diff --git a/doc/openclaw_design.md b/doc/openclaw_design.md new file mode 100644 index 00000000..62c0715a --- /dev/null +++ b/doc/openclaw_design.md @@ -0,0 +1,124 @@ +# DeepWork on OpenClaw — Design Doc + +This document describes how DeepWork integrates with [OpenClaw](https://openclaw.ai), why the integration is shaped the way it is, and how the moving parts fit together. + +Primary reference: [OpenClaw — Building Plugins](https://docs.openclaw.ai/plugins/building-plugins). Related pages: [Plugin Bundles](https://docs.openclaw.ai/plugins/bundles), [Hooks](https://docs.openclaw.ai/automation/hooks), [Skills](https://docs.openclaw.ai/tools/skills), [mcp CLI](https://docs.openclaw.ai/cli/mcp), [Agent Bootstrapping](https://docs.openclaw.ai/start/bootstrapping). + +## Goal + +Let an OpenClaw session use DeepWork's workflow engine and review system through MCP, with no DeepWork-specific pre-install on the host, and no Claude-specific machinery. + +## Why a Codex bundle, not a native OpenClaw plugin + +OpenClaw supports two plugin formats: + +- **Native plugins** — npm package with `package.json`, `openclaw.plugin.json`, and a TypeScript entry point that calls `api.registerTool(...)`, `api.registerHook(...)`, etc. Plugins run in-process in the OpenClaw gateway. +- **Codex bundles** — content packs that OpenClaw maps onto its native features. A bundle is identified by a `.codex-plugin/plugin.json` marker and may ship `skills/`, `hooks/`, `.mcp.json`, and `.app.json`. No entry point, no npm package, no TypeScript. See [Plugin Bundles](https://docs.openclaw.ai/plugins/bundles). + +DeepWork is an out-of-process MCP server written in Python. It does not need to run inside the OpenClaw gateway. The Codex bundle format fits exactly: declare the MCP server in `.mcp.json`, ship skill instructions the agent reads when the user invokes `/deepwork` or `/review`, and ship one bootstrap hook so the agent sees the right `session_id` up front. This is strictly less machinery than a native plugin and gives the same runtime behavior. + +## File layout + +``` +plugins/openclaw/ +├── .codex-plugin/ +│ └── plugin.json # Codex bundle marker +├── .mcp.json # Registers the DeepWork MCP server +├── skills/ +│ ├── deepwork/SKILL.md # /deepwork skill — workflow execution +│ └── review/SKILL.md # /review skill — DeepWork reviews +└── hooks/ + └── deepwork-openclaw-bootstrap/ + ├── HOOK.md # Declares the agent:bootstrap hook + └── handler.ts # Injects session/resume context +``` + +Nothing else in this bundle is required. No `package.json`. No `index.ts`. No `openclaw.plugin.json`. Those belong to native plugins, not Codex bundles. + +## Install flow (what the user actually does) + +1. Install `uv` (so `uvx` is on PATH). +2. Clone DeepWork, or otherwise get the bundle on disk. +3. `openclaw plugins install /path/to/deepwork/plugins/openclaw` +4. Restart the OpenClaw gateway and start a new session. + +No `uv tool install deepwork`. No `pip install`. The MCP config uses `uvx deepwork serve --platform openclaw`, so `uvx` fetches and caches the published `deepwork` package on first launch and auto-resolves updates on subsequent runs. Users get updates by letting `uvx` refresh the cache (or by running `uv cache clean` if they want to force a refresh); they do not need to re-run an install step. + +The "bundle still has to be on disk" part is the one remaining pre-install step, and the right fix is publishing the bundle to ClawHub or npm so `openclaw plugins install clawhub:unsupervisedcom/deepwork` works. Until then, pointing `openclaw plugins install` at the cloned repo path is the intended flow. + +## Runtime contract + +DeepWork's MCP tools require a `session_id` on every call. In OpenClaw, we map that to the current session's `sessionId`. In Claude Code, we map it to `CLAUDE_CODE_SESSION_ID`. The MCP server itself is unchanged between platforms — `--platform openclaw` only affects the adapter that formats review output, not the tool shapes. + +Session state lives under the workspace at: + +``` +.deepwork/tmp/sessions/openclaw/session-/state.json +``` + +This scoping means resume works inside a single OpenClaw session. Spawned child sessions get their own session directories; they do not yet share a DeepWork workflow stack with their parent the way Claude Code does. That shared-root model depends on parent/root session metadata that OpenClaw's current bootstrap hook surface does not expose. We'll revisit when it does. + +## The bootstrap hook + +Skills tell the agent what to do when the user invokes `/deepwork` or `/review`. The bootstrap hook exists because the agent also needs to know *which* `session_id` to use before it makes any MCP call — and the agent cannot read that from the skill file alone. + +`HOOK.md` registers a hook on the [`agent:bootstrap`](https://docs.openclaw.ai/automation/hooks) event. `handler.ts` receives the event, reads `context.sessionId`, `context.sessionKey`, `context.agentId`, and `context.workspaceDir`, and appends a synthetic bootstrap file to `context.bootstrapFiles` telling the agent: + +- the exact `session_id` to pass to every DeepWork MCP call +- whether DeepWork state already exists for this session (detected by checking for `state.json` on disk) — if so, prompt the agent to call `deepwork__get_active_workflow` before starting a new workflow +- the review-spawn conventions specific to OpenClaw (`sessions_spawn` + `sessions_yield`, no `timeoutSeconds` unless `0`, workspace-relative instruction paths) + +The handler also writes the same note to disk as a best-effort fallback, but the in-memory `bootstrapFiles` injection is the load-bearing mechanism. + +## Reviews on OpenClaw + +DeepWork quality gates can return review tasks that the agent needs to run in parallel. Claude Code runs these as sub-tasks via the Task tool. OpenClaw runs them as parallel sub-agents via [`sessions_spawn`](https://docs.openclaw.ai/) + [`sessions_yield`](https://docs.openclaw.ai/). + +The review instructions returned by the MCP server are platform-neutral. The OpenClaw-specific skill file (`skills/review/SKILL.md`) tells the agent how to dispatch them as OpenClaw sub-agents. The Claude formatter path in the DeepWork runtime is separate and unchanged. + +## Troubleshooting + +### DeepWork tools don't appear + +- Confirm `openclaw plugins inspect deepwork` shows a bundle with an MCP server. +- Restart the OpenClaw gateway and start a fresh session. +- Verify `uvx deepwork --version` works in your shell. If `uvx` is not on PATH, install `uv`. + +### OpenClaw launches the wrong DeepWork binary + +Add a top-level `mcp.servers.deepwork` override in OpenClaw's config that points at an exact executable: + +```json +{ + "mcp": { + "servers": { + "deepwork": { + "command": "/absolute/path/to/deepwork", + "args": ["serve", "--platform", "openclaw"] + } + } + } +} +``` + +Restart the gateway after the change. This is a rare case — `uvx` resolution is normally correct. + +### Testing unreleased DeepWork changes from a local checkout + +Register the checkout as an editable `uv` tool so `uvx deepwork` resolves to your local source: + +```bash +uv tool install -e /path/to/deepwork +``` + +If you want the gateway to launch a specific binary regardless of `PATH`, use the top-level `mcp.servers.deepwork` override above. + +### Agent starts a second workflow instead of resuming + +Tell the agent to call `deepwork__get_active_workflow` first. The bootstrap hook already prompts this when it detects prior state on disk; if the hook did not run, the skill file will still produce the correct behavior when the agent reads it. + +## Known limitations + +- **Session-scoped state.** Parent/child OpenClaw sessions do not yet share one DeepWork workflow stack. Revisit when OpenClaw exposes parent/root session metadata in the bootstrap context. +- **Bundle distribution.** Until the bundle is published to ClawHub or npm, the install step requires a local clone of DeepWork. +- **No auto-update for MCP servers.** OpenClaw does not refresh plugin-declared MCP servers automatically. `uvx`'s own cache handles version updates, but a gateway restart may be needed to pick up a new binary. diff --git a/plugins/openclaw/.codex-plugin/plugin.json b/plugins/openclaw/.codex-plugin/plugin.json new file mode 100644 index 00000000..ab58c699 --- /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.14.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..19a05b13 --- /dev/null +++ b/plugins/openclaw/README.md @@ -0,0 +1,48 @@ +# DeepWork for OpenClaw + +This directory is an OpenClaw [Codex bundle](https://docs.openclaw.ai/plugins/bundles) that lets OpenClaw sessions use DeepWork's workflow and review MCP tools. + +For how the integration works, why it is shaped this way, and troubleshooting beyond the basics, see [`doc/openclaw_design.md`](../../doc/openclaw_design.md). + +## Prerequisites + +- OpenClaw with bundle support +- [`uv`](https://docs.astral.sh/uv/) installed (so `uvx` is on PATH) +- a Git repository for the target project (DeepWork stores job definitions under `.deepwork/` and may create work branches) + +You do **not** need to install the `deepwork` CLI separately. The bundle launches it via `uvx deepwork serve --platform openclaw`, which auto-fetches the published package on first use. + +## Install + +```bash +git clone https://github.com/Unsupervisedcom/deepwork.git +openclaw plugins install ./deepwork/plugins/openclaw +openclaw gateway restart +``` + +Then start a new OpenClaw session. Verify with: + +```bash +openclaw plugins inspect deepwork +``` + +You should see bundle subtype `codex`, skill roots from `skills/`, a hook pack from `hooks/`, and an MCP server named `deepwork`. + +## First run + +Typical 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 bundled bootstrap hook will have already told the agent the correct `session_id` to use for DeepWork MCP calls and whether prior DeepWork state exists for the session. + +## Runtime layout + +- `.codex-plugin/plugin.json` — Codex bundle marker +- `.mcp.json` — declares the `deepwork` MCP server (`uvx deepwork serve --platform openclaw`) +- `skills/deepwork/SKILL.md`, `skills/review/SKILL.md` — agent-facing instructions for `/deepwork` and `/review` +- `hooks/deepwork-openclaw-bootstrap/` — `agent:bootstrap` hook that injects session and resume guidance + +See the [design doc](../../doc/openclaw_design.md) for how these pieces fit together, known limitations (notably: session-scoped state across spawned sub-sessions), and troubleshooting for pinning a specific `deepwork` binary. 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