Skip to content

feat(claw-server): self-healing per-host recipes (behind flag) - #1859

Closed
Dani Akash (DaniAkash) wants to merge 4 commits into
mainfrom
feat/recipes-phase-1
Closed

feat(claw-server): self-healing per-host recipes (behind flag)#1859
Dani Akash (DaniAkash) wants to merge 4 commits into
mainfrom
feat/recipes-phase-1

Conversation

@DaniAkash

@DaniAkash Dani Akash (DaniAkash) commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What ships

A convention-based self-healing mechanism for BrowserClaw's MCP dispatch loop: a coding-agent MCP client can leave a per-site Markdown note once and read it on every future visit to the same host. The mechanism is a filesystem convention plus one dispatch effect plus one appended instructions block. No engine, no ML, no schema migration, no UI.

Off by default behind BROWSERCLAW_RECIPES=1 for a careful rollout.

The two commits

1. feat(claw-server): add recipes service + domain-skills-hint effect (behind flag)

  • services/recipes.ts: hostStemFromUrl (hostname after stripping www., then the first dot-separated label, filtered to http: / https:), recipesDirFor, listRecipeFiles (sorted, capped at 10, subdir / non-file safe).
  • mcp/effects/domain-skills-hint.ts: a new NamedToolEffect that fires on navigate when the flag is on and annotates the result's structuredContent with domain_skills.{files, workspace_dir} while also appending a synthetic content[] text block for clients that only pass content to the LLM. Registered in BASE_EFFECTS between tabs-list-view and audit so the annotation lands in resultMeta and the operator sees which recipes each agent was told about.
  • env.ts: BROWSERCLAW_RECIPES flag via a new readBoolFlagDefaultFalse helper.
  • Tests: 12 unit tests on the service (host-stem edge cases, cap enforcement, symlink following, subdir skipping, multi-agent + multi-host isolation) + 10 effect tests (flag gating, non-navigate passthrough, error passthrough, no-identity guard, chrome:// no-op, cold host hint, warm host hint, args-url fallback, structuredContent field preservation, multi-agent isolation).

2. feat(claw-server): append recipe discipline to MCP instructions when flag is on

  • mcp/mcp-prompt.ts: adds a Domain recipes: block that tells the agent to Read every domain_skills.files before acting on the site and to Write new .md notes to workspace_dir when it figures something out. Refactored to getBrowserClawMcpInstructions() so the flag composition happens at call time.
  • mcp/single-server.ts (+1 -1): switched the McpServer constructor to the getter.
  • Tests: 2 unit tests covering flag-off (base only) and flag-on (base + recipes tail) shapes.

Storage layout

Recipes live at <browserclawDir>/recipes/<slug>/<host-stem>/<name>.md, alongside the existing audit.sqlite and replays/. Slug comes from identityService so recipes are scoped per MCP client (claude-code, codex-mcp-client, etc.). Host stem is deliberately the same reduction upstream libraries use, so a folder is portable in either direction.

Writes

Coding-agent MCP clients (Claude Code, Codex, Cursor, VS Code, Zed, OpenCode, Antigravity) use their own Read / Write tools. That is why this PR ships zero new BrowserClaw MCP tools: the write path exists in the client, not the server. Clients that lack filesystem access (Claude Desktop-like) are a no-op today; a dedicated MCP save_recipe tool for those clients is a follow-up if needed.

Full suite

  • bun test in apps/claw-server: 424 pass, 0 fail (was 400, +24 new tests across 3 files)
  • bun run typecheck: clean
  • bunx biome check: clean

What is deliberately NOT in this PR

  • No new MCP tools (list_recipes, read_recipe, save_recipe). Coding-agent clients use their own filesystem tools.
  • No apps/claw-app/ UI. Browsing / deleting recipes is a follow-up.
  • No SQLite schema. Filesystem-first.
  • No cross-agent shared recipe pool. Recipes are per-slug.
  • No auto-heal-on-failure detection.

Rollback

unset BROWSERCLAW_RECIPES (or set to 0 / false). The effect is a cheap early-return with no filesystem I/O; the instructions revert to the base block; existing recipe files stay on disk but are never read by the server until the flag flips back on.

Manual verification

  1. BROWSERCLAW_RECIPES=1 bun run start inside apps/claw-server.
  2. Connect Claude Code via the standard MCP config.
  3. Call navigate("https://news.ycombinator.com"). Confirm the tool result carries structuredContent.domain_skills.workspace_dir pointing at the per-slug per-host directory.
  4. Ask Claude Code: "save a recipe named frontpage-selectors.md at that path with # HN\n- .titleline > a for top story titles".
  5. Navigate again to the same URL. Confirm domain_skills.files includes frontpage-selectors.md and the synthetic text block names it.
  6. Kill the session, start a new one, repeat step 5. Confirm persistence.
  7. Toggle BROWSERCLAW_RECIPES=0, restart, navigate. Confirm no domain_skills field appears and the instructions no longer include the Domain recipes: block.

Test plan

  • apps/claw-server unit tests green locally
  • Typecheck + biome clean
  • CI green
  • Manual end-to-end verification against a real MCP client before the flag flips on-by-default

Follow-ups (separate work)

  • A cockpit UI to browse and delete recipes (shadcn AlertDialog with typed-confirmation for delete, matching the audit-cleanup UX).
  • Optional MCP tools for clients that lack filesystem access.
  • User-facing docs.
  • Interaction check with the audit-cleanup orphan sweep once that lands.

…ehind flag)

Ports the storage + surfacing halves of browser-use/browser-harness's
'domain skills' convention to BrowserClaw so a coding-agent MCP
client can leave a per-site Markdown note once and read it on every
future visit. Self-healing loop as a filesystem convention, not an
engine.

Storage. Files live at <browserclawDir>/recipes/<slug>/<host-stem>/
<name>.md. Slug comes from identityService so recipes are scoped per
MCP client (claude-code, codex-mcp-client, etc.). Host stem matches
the upstream's helpers.py:134 reduction (strip www., take first
label) so a folder is portable between browser-harness and
BrowserClaw in either direction.

Surfacing. A new NamedToolEffect fires on navigate when the flag is
on. It annotates the tool result's structuredContent with
domain_skills.{files, workspace_dir} and appends a synthetic text
block for clients that only pass content[] to the LLM. Runs BEFORE
audit so the annotation lands in resultMeta and the operator sees
which recipes each agent was told about. Never touches error
results or non-navigate calls. No-op with zero I/O when the flag is
off.

Writes. Deliberately no save_recipe / list_recipes MCP tool in v1.
The coding agent uses its own Read/Write tools, matching how
browser-harness itself works upstream. If Claude Desktop-like
clients turn out to matter, Phase 2 adds the tool.

Feature-flagged behind BROWSERCLAW_RECIPES=1. Off by default in
v1 for careful rollout; flip to on-by-default after production
soak. Adds a shared readBoolFlagDefaultFalse helper in env.ts for
the opt-in shape.

Tests: 12 unit tests on the recipes service (host-stem edge cases
across http/https/chrome/about/file/data, IPs, localhost, cap
enforcement, symlink following, subdir skipping, multi-agent +
multi-host isolation) + 10 effect tests (flag gating, non-navigate
passthrough, error passthrough, no-identity guard, chrome:// no-op,
cold host hint, warm host hint, args-url fallback, structuredContent
field preservation, multi-agent isolation). Full claw-server suite:
422 pass, 0 fail.
…flag is on

Adds a Domain recipes block to BROWSERCLAW_MCP_INSTRUCTIONS when
BROWSERCLAW_RECIPES is on. Tells the calling agent to Read every
domain_skills file before acting on the site and to Write new .md
notes to the workspace_dir when it figures something out. Uses the
agent's own filesystem tools (matches how browser-harness's
SKILL.md discipline works upstream).

Refactor. mcp-prompt.ts now exposes getBrowserClawMcpInstructions()
which reads env.recipesEnabled at call time so the composition
reacts to flag flips without a re-import. single-server.ts is
switched to the getter. The old BROWSERCLAW_MCP_INSTRUCTIONS constant
stays exported as the base (flag-off) shape so the existing
server-identity test's snapshot stays pinned.

Test coverage: 2 unit tests on the getter (flag off returns base,
flag on appends the recipes block with the full tail text). Full
suite: 424 pass, 0 fail.
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

✅ Tests passed — 1986/1990

Suite Passed Failed Skipped
agent 311/311 0 0
build 40/40 0 0
claw-app 179/179 0 0
⚠️ claw-onboard 0/0 0 0
claw-server 423/423 0 0
server-agent 301/301 0 0
server-api 155/155 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 265/266 0 1
server-root 38/41 0 3
server-tools 254/254 0 0

View workflow run

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Introduces Phase 1 of self-healing recipes: a filesystem-based per-agent per-host Markdown note store that surfaces relevant files to the LLM on every navigate, ported from browser-harness's "domain skills" convention. The entire feature is behind BROWSERCLAW_RECIPES=1 (off by default) with a clean early-return path that avoids all I/O when the flag is off.

  • New services/recipes.ts: Pure path derivation and readdirSync-based directory listing, capped at 10 files, with slug sanitized to [a-z0-9-] and host stem taken from the URL hostname label — both safe for path.join construction.
  • New domain-skills-hint effect: Fires on navigate results, annotates structuredContent.domain_skills and appends a synthetic content[] text block; runs before applyAudit so operator logs capture which recipes each agent was told about.
  • mcp-prompt.ts refactor: Static export replaced by getBrowserClawMcpInstructions() getter so each new MCP session reads the live flag; +24 tests bring the suite to 424 passing.

Confidence Score: 4/5

Safe to merge — the feature is entirely behind a default-off flag, all early-return guards are well-tested, and the path construction is secure thanks to slug sanitization on the identity side.

The logic is solid and the 24 new tests cover edge cases thoroughly. The two remaining concerns are both style-level: statSync uses a template-literal path instead of path.join, and BROWSERCLAW_MCP_INSTRUCTIONS is re-exported as production dead code maintained only for an out-of-PR snapshot test. Neither affects runtime correctness.

services/recipes.ts for the path.join nit and sync I/O note; mcp/mcp-prompt.ts for the stale re-export.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/claw-server/src/services/recipes.ts New service providing hostStemFromUrl, recipesDirFor, and listRecipeFiles. Slug and host-stem are sanitized to alphanumeric+hyphen, making path construction safe. Minor: statSync uses template-literal path instead of path.join, and sync I/O runs on every navigate.
packages/browseros-agent/apps/claw-server/src/mcp/effects/domain-skills-hint.ts New NamedToolEffect annotating navigate results with domain_skills. Early-return guards (flag-off, error, non-navigate, no-identity, no host stem) are thorough; URL fallback from args.url is covered by tests.
packages/browseros-agent/apps/claw-server/src/mcp/mcp-prompt.ts Refactored from a module-load constant to a getBrowserClawMcpInstructions() getter. BROWSERCLAW_MCP_INSTRUCTIONS is re-exported as dead production code purely to support an existing snapshot test.
packages/browseros-agent/apps/claw-server/src/env.ts Adds readBoolFlagDefaultFalse helper and recipesEnabled field, symmetric with the existing readBoolFlagDefaultTrue, defaulting correctly to false.
packages/browseros-agent/apps/claw-server/src/mcp/dispatch.ts Wires applyDomainSkillsHint into BASE_EFFECTS between tabs-list-view and audit, matching the documented ordering requirement.
packages/browseros-agent/apps/claw-server/src/mcp/single-server.ts Minimal change: switches from the static constant to the getBrowserClawMcpInstructions() getter so each new session picks up the current flag value.
packages/browseros-agent/apps/claw-server/tests/mcp/effects/domain-skills-hint.test.ts 10 unit tests covering flag-gating, non-navigate passthrough, error passthrough, no-identity guard, chrome:// no-op, cold/warm host hints, args-url fallback, structuredContent preservation, and multi-agent isolation.
packages/browseros-agent/apps/claw-server/tests/services/recipes.test.ts 12 tests covering host-stem edge cases, cap enforcement, .md filtering, subdir skipping, symlink following, and agent/host isolation. Comprehensive.
packages/browseros-agent/apps/claw-server/tests/mcp/mcp-prompt.test.ts 2 unit tests verifying flag-off (base only) and flag-on (base + recipes block) shapes of getBrowserClawMcpInstructions(). Simple and correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant LLM as LLM Agent
    participant MCP as BrowserClaw MCP
    participant Disp as Dispatch Loop
    participant DSH as domain-skills-hint effect
    participant FS as Filesystem (recipes dir)
    participant Audit as applyAudit effect

    LLM->>MCP: navigate("https://example.com")
    MCP->>Disp: dispatchTool(call)
    Disp->>Disp: applyOwnershipClaims
    Disp->>Disp: applyTabsListView
    Disp->>DSH: applyDomainSkillsHint(call, result)
    alt BROWSERCLAW_RECIPES off
        DSH-->>Disp: undefined (no-op)
    else flag on + navigate + http/https + identity
        DSH->>FS: readdirSync(recipes/slug/hostStem)
        FS-->>DSH: [invitation.md, login.md]
        DSH-->>Disp: result + structuredContent.domain_skills + content[] hint
    end
    Disp->>Audit: applyAudit(call, result)
    Note over Audit: logs domain_skills in resultMeta
    Disp-->>LLM: ToolResult with domain_skills annotation
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant LLM as LLM Agent
    participant MCP as BrowserClaw MCP
    participant Disp as Dispatch Loop
    participant DSH as domain-skills-hint effect
    participant FS as Filesystem (recipes dir)
    participant Audit as applyAudit effect

    LLM->>MCP: navigate("https://example.com")
    MCP->>Disp: dispatchTool(call)
    Disp->>Disp: applyOwnershipClaims
    Disp->>Disp: applyTabsListView
    Disp->>DSH: applyDomainSkillsHint(call, result)
    alt BROWSERCLAW_RECIPES off
        DSH-->>Disp: undefined (no-op)
    else flag on + navigate + http/https + identity
        DSH->>FS: readdirSync(recipes/slug/hostStem)
        FS-->>DSH: [invitation.md, login.md]
        DSH-->>Disp: result + structuredContent.domain_skills + content[] hint
    end
    Disp->>Audit: applyAudit(call, result)
    Note over Audit: logs domain_skills in resultMeta
    Disp-->>LLM: ToolResult with domain_skills annotation
Loading

Comments Outside Diff (1)

  1. packages/browseros-agent/apps/claw-server/src/services/recipes.ts, line 344-365 (link)

    P2 Synchronous I/O blocks the event loop on every navigate call

    readdirSync followed by up to N statSync calls executes synchronously in the MCP dispatch loop. For most local SSDs this is negligible, but if the recipes directory grows large (many files, slow network-mounted home dir, or antivirus scanning), every navigation call will stall the event loop for the duration. Given that ToolEffect already has a sync signature this is an architectural constraint rather than a bug, but it's worth a follow-up to switch to async stat once the effect system supports it.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/browseros-agent/apps/claw-server/src/services/recipes.ts
    Line: 344-365
    
    Comment:
    Synchronous I/O blocks the event loop on every `navigate` call
    
    `readdirSync` followed by up to N `statSync` calls executes synchronously in the MCP dispatch loop. For most local SSDs this is negligible, but if the recipes directory grows large (many files, slow network-mounted home dir, or antivirus scanning), every navigation call will stall the event loop for the duration. Given that `ToolEffect` already has a sync signature this is an architectural constraint rather than a bug, but it's worth a follow-up to switch to async stat once the effect system supports it.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
packages/browseros-agent/apps/claw-server/src/services/recipes.ts:358
`path.join` missing in `statSync` call

`statSync(\`${dir}/${entry}\`)` builds the path via a template literal, but `path.join` is the idiomatic and OS-safe approach used everywhere else in this package (e.g. in `browserclaw-dir.ts`). The `node:path` module isn't imported in this file — adding it and using `join(dir, entry)` would keep the path logic consistent.

### Issue 2 of 5
packages/browseros-agent/apps/claw-server/src/mcp/mcp-prompt.ts:240-248
Re-exported constant now only consumed by a test

`BROWSERCLAW_MCP_INSTRUCTIONS` is re-exported solely so `tests/mcp/server-identity.test.ts` can pin the base wire format independent of the flag. The same guarantee can be achieved by calling `getBrowserClawMcpInstructions()` with `env.recipesEnabled = false` in that test, which is exactly what the new `mcp-prompt.test.ts` does. Keeping an exported constant whose only production consumer (`single-server.ts`) was just migrated away from it leaves dead code in the public surface of the module.

### Issue 3 of 5
packages/browseros-agent/apps/claw-server/src/services/recipes.ts:344-365
Synchronous I/O blocks the event loop on every `navigate` call

`readdirSync` followed by up to N `statSync` calls executes synchronously in the MCP dispatch loop. For most local SSDs this is negligible, but if the recipes directory grows large (many files, slow network-mounted home dir, or antivirus scanning), every navigation call will stall the event loop for the duration. Given that `ToolEffect` already has a sync signature this is an architectural constraint rather than a bug, but it's worth a follow-up to switch to async stat once the effect system supports it.

### Issue 4 of 5
packages/browseros-agent/apps/claw-server/src/services/recipes.ts:17
Use `path.join` instead of template-literal path concatenation for the `statSync` call. `node:path` isn't currently imported in this file; add it alongside the existing `node:fs` import.

```suggestion
import { readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'
```

### Issue 5 of 5
packages/browseros-agent/apps/claw-server/src/services/recipes.ts:82
Replace the template-literal path with `join(dir, entry)` to stay consistent with the rest of the codebase and avoid potential OS-specific edge cases.

```suggestion
      if (!statSync(join(dir, entry)).isFile()) continue
```

Reviews (1): Last reviewed commit: "feat(claw-server): append recipe discipl..." | Re-trigger Greptile

@DaniAkash Dani Akash (DaniAkash) changed the title feat(claw-server): self-healing recipes (Phase 1, behind flag) feat(claw-server): self-healing per-host recipes (behind flag) Jul 17, 2026
…branch

Removes the env-var gate on the recipes surfacing and the composed
MCP instructions. The dispatch effect and the Domain-recipes
discipline block are now always active on this branch. The branch
serves as an epic that will grow the full feature (UI, docs, cleanup
integration) before it merges to main; the flag was a rollout
mechanism for a straight-to-main path we are no longer taking.

- env.ts: drop the recipesEnabled field and the readBoolFlagDefaultFalse
  helper (only user was recipes).
- mcp/mcp-prompt.ts: BROWSERCLAW_MCP_INSTRUCTIONS is now a single
  composed constant with base + recipes. getBrowserClawMcpInstructions()
  is gone; single-server.ts uses the constant directly again.
- mcp/effects/domain-skills-hint.ts: drop the env.recipesEnabled guard.
- Tests updated: 423 pass, 0 fail.
@DaniAkash

Copy link
Copy Markdown
Contributor Author

Converting this branch into an epic. Future work will target feat/recipes-phase-1 directly, then the whole thing merges to main as one PR when complete. Closing this PR now; the branch stays live.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant