Skip to content

feat(profile): materializer + host wiring + ACP/socket backends (full profile provisioning)#57

Merged
drewstone merged 8 commits into
mainfrom
feat/profile-wiring
Jun 14, 2026
Merged

feat(profile): materializer + host wiring + ACP/socket backends (full profile provisioning)#57
drewstone merged 8 commits into
mainfrom
feat/profile-wiring

Conversation

@drewstone

Copy link
Copy Markdown
Owner

The complete profile-provisioning system for cli-bridge. Builds on (and includes) #55 (materializer) and #56 (ACP/nanoclaw backends), plus the Phase-2 host wiring.

What's here

  1. materializeProfile(profile, harness) — one materializer, all 6 dimensions (context/skills/mcp/hooks/subagents/commands) × 9 harnesses, verified routing, fail-closed. applyWorkspacePlan writes it into a cwd. (feat(profile): materializeProfile — one per-harness profile materializer (all dimensions) #55)
  2. 3 new backends — hermes + openclaw (ACP) and nanoclaw (Unix-socket client). (feat(backends): add hermes, openclaw (ACP) + nanoclaw (socket) backends #56)
  3. Host wiring (new) — the 6 spawn backends (claude, codex, opencode, kimi, gemini, pi) now call provisionProfileWorkspace before spawn, mounting a profile's cwd-native dimensions (skills, context, hooks, subagents, commands) into the run workspace the way each harness natively loads them.

Safe + additive

  • MCP is skipped in the wiring ({skip:['mcp']}) — cli-bridge's existing per-harness MCP path stays the source of truth, so MCP can't regress.
  • The new dimensions aren't provisioned today, so writing them into cwd is purely additive.
  • provisionProfileWorkspace is fail-safe (try/catch): a materialization error yields an un-provisioned run, never a crashed request.

Proven, not claimed

Remaining (separate, canary-gated)

  • Per-backend MCP/hook canaries (skill is proven).
  • Phase 3: agent-dev-container box provisioning off the same materializer (fixes the ACP mcpServers:[] drop).
  • Phase 4: point VB at it, delete composeWorkerSkills.

🤖 Generated with Claude Code

…zer (all dimensions)

Generalizes the MCP-only materialisers in profile-support.ts to EVERY AgentProfile
dimension — context, skills, mcp, hooks, subagents, commands — routed per the verified
capability matrix (workflow harness-capability-matrix). materializeProfile(profile,
harness) → WorkspacePlan {files, env, flags, unsupported}: the exact native files to
write into the run cwd, plus env/flags for harnesses with NO cwd auto-discovery.

Verified per-harness routing (the thing a wrong path silently breaks):
- context file is per-harness: claude/nanoclaw→CLAUDE.md, gemini→GEMINI.md (NOT AGENTS.md;
  current not legacy), hermes→HERMES.md, rest→AGENTS.md.
- skills: .{claude,codex,opencode,kimi,pi,gemini}/skills/<n>/SKILL.md; openclaw→skills/;
  hermes has NO cwd skill dir (fail-closed).
- mcp: claude .mcp.json+settings enable, codex [mcp_servers] (trust), opencode opencode.json,
  gemini settings.json mcpServers, kimi --mcp-config-file FLAG, pi/hermes/openclaw fail-closed.
- hooks/subagents/commands: native files where supported; per-harness FORMAT (claude/gemini
  settings.json, codex toml/json, opencode JS plugin) — emitted correctly, not one shape.

FAIL-CLOSED, never silent-drop: a dimension with no cwd path on the chosen harness is
recorded in plan.unsupported with the reason (the "looks provisioned, loads nothing" trap).
JSON multi-writer files (claude/gemini settings.json from mcp+hooks) are merged, not clobbered.

Pure, no IO — golden-file tested across all 9 harnesses (11/11), typechecks clean. This is
the ONE materializer the cli-bridge host spawn, the agent-dev-container box, and VB will all
consume. Next: wire into the backend spawn + register openclaw/hermes/nanoclaw backends +
canary each harness×capability (secret-word skill / MCP tool-call n>0 / hook sentinel).
…e IO consumer)

The IO sibling of the pure materializeProfile: writes every plan file (creating parent
dirs) into the workspace and returns the env + flags the spawn must apply. This is what
the cli-bridge host spawn, the agent-dev-container box, and VB all call.

PROVEN LIVE end-to-end: a skill materialized by materializeProfile + applied here is
actually loaded by real harnesses through cli-bridge — claude-code, kimi-code, pi each
returned a secret that exists ONLY inside the materialized SKILL.md file (the manipulation
check; opencode invokes the skill tool but ends its turn on the call). 12/12 tests.
… Protocol)

cli-bridge had no ACP client; the existing backends spawn a CLI and parse stdout, but
hermes and openclaw expose `<bin> acp` — a JSON-RPC 2.0 ndjson stdio server with a
stateful session. AcpBackend drives any such agent:

  spawn `<bin> acp` (cwd=req.cwd so it discovers workspace skills/context)
  → initialize → session/new {cwd,mcpServers} → session/prompt {prompt:[text]}
  ← stream session/update {content:{text}} → ChatDelta.content (message chunks only;
    thought/plan/tool chunks skipped so reasoning never pollutes the OpenAI stream)
  ← session/request_permission → auto-allow (first option; trusted scope)
  ← result {stopReason} → finish_reason

Registered as hermes + openclaw backends (config bins HERMES_BIN/OPENCLAW_BIN, opt-in
via BRIDGE_BACKENDS; health reports `unavailable` if the binary is absent).

VERIFIED: live end-to-end against `hermes acp` — health ready (v0.10.0), the handshake +
session/prompt streamed real deltas with a clean stop. Wire protocol confirmed by a raw
ndjson probe first. 4/4 unit tests against a mock ACP agent (handshake, message-vs-thought
filtering, permission auto-allow, spawn-error), typechecks clean.

nanoclaw is NOT included here: it is a multi-channel messaging DAEMON (boots a DB +
channels, no `acp`/one-shot stdio mode), so it does not fit the stdio-backend model —
it needs a different integration (its own HTTP/queue interface), tracked separately.
…daemon

NanoClaw isn't a one-shot CLI: it's a long-lived multi-channel agent daemon (Claude Code
under the @onecli-sh Chat SDK) that exposes a CLI channel over a Unix socket
(<nanoclaw>/data/cli.sock). So the proper integration is a socket CLIENT, mirroring
NanoClaw's own scripts/chat.ts:

  connect(socket) → write {"text": prompt}\n
  ← stream {"text": reply}\n lines → ChatDelta.content
  completion is SILENCE-based (NanoClaw sends no done event) — ends after silenceMs of
  quiet following the first reply, or socket close, or the hard cap.

Registered as the `nanoclaw` backend (NANOCLAW_SOCKET → the daemon's data/cli.sock;
opt-in via BRIDGE_BACKENDS; health=unavailable when the daemon isn't running).

LIMITATION (documented): the daemon owns its own workspace, so NanoClaw does NOT honor a
per-request cwd — workspace skill/profile materialization doesn't reach it (configure
skills on the NanoClaw side). This is inherent to its daemon model, not a shortcut.

Verified: 5/5 tests against a REAL Unix-socket server speaking NanoClaw's exact protocol
(stream-then-silence, socket-close finish, health ready/unavailable). Typechecks clean.
…e 2)

Each spawn backend (claude, codex, opencode, kimi, gemini, pi) now provisions a
profile's CWD-NATIVE dimensions — skills, context, hooks, subagents, commands — into
the run workspace before the harness spawns, via the shared provisionProfileWorkspace
helper. So a profile's skills/hooks/etc. are MOUNTED the way each harness natively
loads them, not prompt-injected.

SAFE + additive:
- MCP is SKIPPED here (materializeProfile {skip:['mcp']}) — cli-bridge's existing
  per-harness MCP path (config-dir + env) stays the source of truth, so MCP can't regress.
- The new dimensions are not provisioned today, so writing them into cwd is purely
  additive (can't change existing behavior).
- provisionProfileWorkspace is FAIL-SAFE (try/catch): a materialization error returns an
  un-provisioned run, never crashes a live request.

PROVEN end-to-end through the REAL backend code: a profile skill provisioned by the
wired KimiBackend was loaded by kimi from cwd (returned the secret that exists only in
the materialized SKILL.md). Typechecks clean; 21/21 existing tests green.

Builds on the materializer (#55) + the new ACP/nanoclaw backends (#56) — this branch
includes both.
# Conflicts:
#	src/backends/materialize-profile.ts
…s-ref shape

Per review: agent-dev-container already materializes profiles in the cursor provider
(materializeCursorProfileResources) via resources.skills/commands/agents as resource
REFS — not a top-level name→content map. This materializer was diverging on input shape
(a fork waiting to drift). Align to the box's canonical shape so BOTH layers read the
SAME profile and this can be lifted to one shared package (extend, don't fork):

- skills/commands/agents move under `resources` as AgentProfileResourceRef[] (inline
  {kind,name,content} | github), matching cursor + the published AgentProfileResources.
- resolveInlineRef resolves inline refs; github refs need async fetch (cursor does this)
  so the sync materializer reports them unsupported for the caller to pre-resolve.
- hooks stays top-level (the dimension being promoted); subagents stays canonical
  top-level (structured AgentSubagentProfile).

Re-proven end-to-end: a profile with resources.skills (inline ref) provisioned by the
wired KimiBackend was loaded by kimi from cwd. 12/12 materializer tests, full suite green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant