Skip to content

Commit 14f5bea

Browse files
fix hook-managed worktree routing
1 parent 4d558b0 commit 14f5bea

8 files changed

Lines changed: 624 additions & 45 deletions

File tree

docs/working/hook-harness-design-spec.md

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ This document defines a deterministic worktree harness for OpenCode using plugin
88

99
The current worktree workflow relies on skills and orchestrator prompts to convince agents to call `worktree_prepare` and `worktree_cleanup` at the right time. That behavior is not reliable enough because skills are still prompt text, and agents can skip or misapply them. The result is nondeterministic workspace selection and inconsistent cleanup behavior.
1010

11+
## Validated Runtime Constraints
12+
13+
Validation against current OpenCode runtime behavior showed an important host boundary:
14+
15+
- `tool.execute.before` does fire for built-in tools
16+
- hook payload mutation is only reliable when mutating the existing object in place
17+
- replacing `output.args` or `output.parts` wholesale is not a safe assumption
18+
- the documented plugin model is centered on the host-selected `worktree`, not on plugins acting as a general multi-worktree router for every built-in tool
19+
20+
This means the harness can still use hooks as an enforcement layer, but only for capabilities it can actually control. Transparent routing of all built-in tools across multiple task worktrees is not a host-guaranteed primitive today.
21+
1122
## Goals
1223

1324
- Make worktree creation deterministic for non-trivial editable work.
@@ -16,6 +27,7 @@ The current worktree workflow relies on skills and orchestrator prompts to convi
1627
- Keep the primary orchestrator as the only authority that can create, switch, fork, or cleanup-manage worktrees.
1728
- Keep the skill policy-oriented and move enforcement into plugin hooks.
1829
- Keep the existing `worktree_prepare` and `worktree_cleanup` tool contract as the public capability surface.
30+
- Preserve an enforceable routing story for mutating work even if OpenCode does not provide a fully general built-in tool redirection contract.
1931

2032
## Non-Goals
2133

@@ -74,6 +86,8 @@ Recommended OpenCode hooks:
7486
- `command.execute.before`: slash-command normalization and parity
7587
- `experimental.chat.system.transform`: model-facing context injection only
7688

89+
Hooks remain the right place for classification, state adoption, prompt context, and live payload mutation. They should not be treated as proof that the plugin can transparently re-home every built-in tool into an arbitrary task worktree.
90+
7791
### Runtime State Layer
7892

7993
The authoritative runtime state should live outside the repo in plugin-managed storage keyed by canonical repo root and `sessionID`. Optional diagnostic mirrors inside `.opencode/` are acceptable, but they must not be authoritative.
@@ -182,11 +196,13 @@ Responsibilities:
182196
- classify the call as read-only, mutating, or delegation
183197
- resolve task continuity
184198
- create or select a bound worktree if isolation is required
185-
- rewrite execution context and repo-scoped paths into the bound worktree
199+
- rewrite execution context and repo-scoped paths into the bound worktree when the runtime contract safely permits it
186200
- block when safe provisioning or safe rewriting is not possible
187201

188202
This hook is the main choke point because it runs immediately before the action happens.
189203

204+
For hard guarantees, this hook should prefer plugin-controlled execution paths over assumptions about undocumented built-in routing semantics.
205+
190206
### `tool.execute.after`
191207

192208
Responsibilities:
@@ -204,6 +220,8 @@ Responsibilities:
204220
- route `/wt-new` and `/wt-clean` through the same state model
205221
- keep slash commands as human UX only, not a separate control plane
206222

223+
This hook should mutate existing prompt parts in place when possible. It should not assume replacing `output.parts` wholesale is honored by the host.
224+
207225
### `experimental.chat.system.transform`
208226

209227
Responsibilities:
@@ -214,6 +232,8 @@ Responsibilities:
214232

215233
This hook improves model behavior but is not part of enforcement.
216234

235+
It should be treated as advisory context only, never as the mechanism that makes routing correct.
236+
217237
## Delegation Policy
218238

219239
Task-tool calls are the canonical delegation boundary.
@@ -287,16 +307,34 @@ Provenance should affect messaging and cleanup recommendations, but not whether
287307

288308
## Path and Workdir Rewriting
289309

290-
The harness should rewrite execution into the bound worktree by default.
310+
The harness should rewrite execution into the bound worktree only where the runtime contract is actually enforceable.
291311

292312
### Rewrite Rules
293313

294-
- always rewrite repo-scoped workdir or cwd into the active worktree
295-
- always rewrite repo-scoped file paths into the active worktree
314+
- mutate live path-like arguments in place when the runtime exposes a stable mutable payload
315+
- prefer plugin-owned mutating tools when execution location must be guaranteed
316+
- rewrite repo-scoped workdir or cwd only when the called tool actually honors those fields as execution inputs
317+
- rewrite repo-scoped file paths only when the path is an explicit structured argument that can be rewritten safely
296318
- never rewrite clearly external absolute paths
297319
- block rather than guess when a path cannot be safely classified
298320

299-
This should make workspace isolation deterministic without depending on agents to remember worktree-local paths.
321+
This can make parts of workspace isolation deterministic without depending on agents to remember worktree-local paths, but only for tools whose execution semantics are actually under plugin control.
322+
323+
### Enforceable Routing Boundary
324+
325+
The harness should distinguish three classes of execution:
326+
327+
1. Plugin-owned tools
328+
2. Built-in tools with safe in-place mutable structured arguments
329+
3. Built-in tools whose effective execution context is owned by OpenCode
330+
331+
For class 1, the plugin can guarantee execution in the bound task worktree.
332+
333+
For class 2, the plugin can often steer execution by mutating the live payload in place, but must still treat that as host-contract-sensitive behavior.
334+
335+
For class 3, the plugin may only provide context, advisory blocking, or explicit failure rather than claim transparent routing.
336+
337+
If hard routing guarantees are required for mutating work, the plugin should own the relevant mutating tools rather than rely on undocumented built-in routing semantics.
300338

301339
## Privilege Model
302340

@@ -421,14 +459,15 @@ This makes workspace context portable and inspectable across fresh-context subag
421459

422460
## Acceptance Criteria
423461

424-
- A mutating tool call in a non-trivial task does not execute in repo root when no bound worktree exists.
462+
- A mutating task that requires hard workspace isolation is either routed through a plugin-controlled execution path or blocked when no safe enforcement path exists.
425463
- A Task-tool editable delegation causes worktree binding before delegation.
426464
- Planner, implementer, and reviewer reuse the same worktree for one linear task unless the orchestrator explicitly forks.
427465
- Manual `worktree_prepare` is adopted into managed runtime state.
428466
- Manual `worktree_cleanup` passes through the same harness policy layer.
429467
- Cleanup advice appears only at defined advisory moments.
430468
- Provisioning or rewrite failures block execution unless the user explicitly overrides.
431469
- Model prompt injection improves context but is not required for enforcement correctness.
470+
- The plugin does not claim transparent built-in multi-worktree routing beyond what the validated host/runtime contract can enforce.
432471

433472
## Test Matrix
434473

@@ -445,6 +484,8 @@ This makes workspace context portable and inspectable across fresh-context subag
445484
- rewrite ambiguity blocks execution
446485
- explicit override allows the scoped root fallback and is visibly recorded
447486
- cleanup advice fires at completion but not on every successful command
487+
- hook payload rewrites mutate existing `args` and `parts` objects in place
488+
- plugin-owned mutating tools execute against the bound task worktree
448489

449490
## Recommended Implementation Sequence
450491

@@ -453,12 +494,13 @@ This makes workspace context portable and inspectable across fresh-context subag
453494
3. Implement task continuity and binding APIs.
454495
4. Add `tool.execute.before` enforcement.
455496
5. Add Task-tool delegation handling.
456-
6. Add repo-scoped path and workdir rewriting.
457-
7. Add `tool.execute.after` adoption and advisory signaling.
458-
8. Add `command.execute.before` parity handling.
459-
9. Extend the handoff schema with workspace fields.
460-
10. Add `experimental.chat.system.transform` context injection.
461-
11. Add invariant-focused tests.
497+
6. Add repo-scoped path and workdir rewriting for safe mutable payloads.
498+
7. Add plugin-owned mutating tools for hard routing guarantees.
499+
8. Add `tool.execute.after` adoption and advisory signaling.
500+
9. Add `command.execute.before` parity handling.
501+
10. Extend the handoff schema with workspace fields.
502+
11. Add `experimental.chat.system.transform` context injection.
503+
12. Add invariant-focused tests.
462504

463505
## Suggested GitHub Tracking Breakdown
464506

@@ -476,7 +518,8 @@ Track the work as separate but ordered issues:
476518
## Definition of Done
477519

478520
- The harness, not the skill, is the authoritative enforcement layer for workspace isolation.
479-
- Orchestrated editable work reliably lands in a task-scoped worktree.
521+
- Orchestrated editable work reliably lands in a task-scoped worktree when routed through an enforceable plugin-controlled path.
480522
- Linear flows reuse one worktree by default.
481523
- Cleanup remains advisory-only.
482524
- Public tools and slash commands still work, but all paths converge on one shared lifecycle model.
525+
- The documented architecture clearly distinguishes advisory hook steering from plugin-owned hard routing.

0 commit comments

Comments
 (0)