Skip to content

Commit 598bc78

Browse files
committed
fix(claude): align Codencer adapter with real Claude Code CLI
Codencer's previous Claude adapter assumed a worker-style command contract that does not match the real Claude Code source. This change rewrites the adapter to use the actual headless CLI integration model. Highlights: - switch to the real Claude CLI invocation model - pass task content non-interactively instead of relying on nonexistent worker flags - execute in the attempt workspace/worktree - capture raw structured output and synthesize normalized Codencer results - harden poll/cancel/process lifecycle behavior - add fake-binary and normalization tests - update public docs to reflect the real integration contract This keeps Codencer within its bridge-not-brain doctrine while making the Claude executor path materially correct and auditable.
1 parent e6fe537 commit 598bc78

21 files changed

Lines changed: 1253 additions & 32 deletions

.env.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ CODEX_BINARY=codex-agent
1010
# Set to 1 to stub only the Codex agent
1111
CODEX_SIMULATION_MODE=0
1212

13-
# Path to the Claude agent binary (requires npm install -g @anthropic-ai/claude-code)
14-
CLAUDE_BINARY=claude-code
13+
# Path to the Claude agent binary (install via npm: @anthropic-ai/claude-code)
14+
# Codencer invokes the installed `claude` CLI in headless print mode.
15+
CLAUDE_BINARY=claude
1516
CLAUDE_SIMULATION_MODE=0
1617

1718
# Path to the Qwen agent binary

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Codencer: The Tactical Orchestration Bridge
22

3-
Codencer is a tactical orchestration bridge that manages execution, isolation, and high-fidelity audit trails for coding agents. It serves as the **system of record** between a high-level **Planner** (human or LLM) and tactical **Coding Agents** (Codex, Claude-code).
3+
Codencer is a tactical orchestration bridge that manages execution, isolation, and high-fidelity audit trails for coding agents. It serves as the **system of record** between a high-level **Planner** (human or LLM) and tactical **Coding Agents** (Codex, Claude).
44

55
Designed for **local-first, self-hosted developer toolchains**, Codencer provides the missing "relay" layer that ensures every task attempt is isolated, provisioned, and validated before it ever reaches your production branch.
66

@@ -29,7 +29,7 @@ Codencer is a **Tactical Orchestration Bridge**, not a strategic planner. It han
2929
### Core Roles
3030
- **Planner (Brain)**: You, a Chat UI, or an agentic planner. Decides **what** to do.
3131
- **Bridge (Codencer)**: Receives the `TaskSpec`, manages workspace isolation (Git Worktrees), enforces policies, and monitors execution.
32-
- **Coding Agent (Worker)**: The tactical tool performing the actual work (e.g., `codex-agent`, `claude-code`).
32+
- **Coding Agent (Worker)**: The tactical tool performing the actual work (e.g., `codex-agent`, `claude`).
3333

3434
---
3535

@@ -85,11 +85,15 @@ Choose your execution tier in `.env` (Simulation is enabled by default in `.env.
8585
# Start in Simulation Mode (Background)
8686
make start-sim
8787

88-
# OR Start in Real Mode (Requires agent binaries like codex-agent)
88+
# OR Start in Real Mode (Requires agent binaries like codex-agent or claude)
8989
# Edit .env: ALL_ADAPTERS_SIMULATION_MODE=0
9090
make start
9191
```
9292

93+
For Claude, Codencer invokes the installed CLI as `claude -p --output-format json`, sends the step prompt on `stdin`, and runs from the isolated attempt workspace as the process `cwd`.
94+
95+
Current support level for the Claude adapter is **Supported (Beta)**: the wrapper contract is implemented and covered by prompt, normalization, lifecycle, fake-binary integration, and simulation conformance tests, but the repo test suite does not run a live authenticated Claude service call.
96+
9397
### 3. Run Your First Tactical Task
9498
Submit a task and wait for the bridge to report results. For the full auditing sequence, see the **[Canonical Local Runbook](docs/EXAMPLES.md)**.
9599

@@ -168,6 +172,12 @@ Every task execution leaves a permanent audit trail:
168172
3. **Artifacts**: Every modified file and diff is stored in `.codencer/artifacts/`. Use `./bin/orchestratorctl step artifacts <id>` to see the exact paths and SHA-256 hashes.
169173
4. **Validations**: Run `./bin/orchestratorctl step validations <id>` to see specific test/lint results.
170174

175+
For Claude attempts specifically, the standard evidence set is:
176+
- `prompt.txt`: the exact prompt Codencer built and sent to Claude
177+
- `stdout.log`: raw Claude JSON output
178+
- `stderr.log`: raw Claude stderr
179+
- `result.json`: synthesized normalized Codencer result
180+
171181
## 🧾 Submission Inputs
172182

173183
Codencer supports two submit styles:

cmd/orchestratorctl/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ func runDoctor() {
986986
env string
987987
}{
988988
{"Codex", "codex-agent", "CODEX_BINARY"},
989-
{"Claude", "claude-code", "CLAUDE_BINARY"},
989+
{"Claude", "claude", "CLAUDE_BINARY"},
990990
{"Qwen", "qwen-local", "QWEN_BINARY"},
991991
{"OpenClaw", "acpx", "OPENCLAW_ACPX_BINARY"},
992992
}

docs/06_adapters_and_ide.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ Why:
2121
- strong local CLI and IDE support
2222
- first-class fit for MVP
2323

24-
### 3. Claude Code second
24+
### 3. Claude second
2525
Why:
2626
- mature terminal-native coding agent
2727
- strong second adapter
2828
- good contrast for adapter-neutral design
2929

30+
Status:
31+
- **Supported (Beta)**: the v1 CLI wrapper contract is implemented and covered by fake-binary integration tests, prompt/normalization unit tests, lifecycle tests, and simulation conformance tests.
32+
- **Not covered in repo tests**: live authenticated Claude service calls.
33+
34+
Current v1 contract:
35+
- binary: `claude` (configurable via `CLAUDE_BINARY`)
36+
- invocation: `claude -p --output-format json`
37+
- prompt path: `stdin`
38+
- workspace semantics: process `cwd` is the attempt workspace root
39+
- evidence model: `prompt.txt`, raw `stdout.log`, raw `stderr.log`, plus a synthesized `result.json`
40+
3041
### 4. OpenClaw ACPX
3142
- **Status**: 🧪 **Experimental (Alpha)**
3243
- **Description**: Standardized ACP (Agent Control Protocol) bridge to the OpenClaw ecosystem.

docs/AI_OPERATOR_GUIDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ Always verify the daemon's identity to ensure you are targeting the correct repo
4040

4141
Use `submit --wait --json` for a synchronous hand-off. This simplifies your control flow by blocking until a terminal state is reached.
4242

43+
Runtime note:
44+
- In real mode, Codencer expects the installed agent CLIs to be reachable through their configured binaries.
45+
- For Claude specifically, the bridge runs `claude -p --output-format json`, sends the submitted prompt on `stdin`, and executes from the isolated attempt workspace.
46+
- Claude evidence is file-backed: review `prompt.txt`, `stdout.log`, `stderr.log`, and the synthesized `result.json` through normal artifact inspection.
47+
4348
### Pattern: The Direct Input Loop
4449
Ideal for human-in-the-loop or iterative assistant tasks.
4550

@@ -110,6 +115,7 @@ Follow this sequence for every tactical mission:
110115
```bash
111116
./bin/orchestratorctl step result <UUID> --json
112117
./bin/orchestratorctl step validations <UUID> --json
118+
./bin/orchestratorctl step artifacts <UUID>
113119
```
114120

115121
---

docs/EXAMPLES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ Exclude the test files.
8080
EOF
8181
```
8282

83+
### 5.5 Claude Headless Execution
84+
Use the Claude adapter when the `claude` CLI is installed locally and reachable through `CLAUDE_BINARY` or `$PATH`.
85+
```bash
86+
cat <<'EOF' | ./bin/orchestratorctl submit my-run --stdin --title "Claude Audit" --adapter claude --wait --json
87+
Review the auth package, explain the failing behavior, and propose a minimal fix.
88+
EOF
89+
```
90+
91+
For Claude attempts, the standard evidence set includes:
92+
- `prompt.txt`
93+
- `stdout.log`
94+
- `stderr.log`
95+
- `result.json`
96+
8397
### OpenClaw ACPX (Experimental / Alpha)
8498
Relay tasks to an OpenClaw-compatible executor via the standardized ACP bridge. Use `--wait --json` for synchronous machine-safe handoffs.
8599
```bash
@@ -117,3 +131,9 @@ See exactly how the workspace was prepared.
117131
```bash
118132
./bin/orchestratorctl step logs <HANDLE>
119133
```
134+
135+
### Inspecting Claude-Specific Evidence
136+
```bash
137+
./bin/orchestratorctl step artifacts <HANDLE>
138+
./bin/orchestratorctl step result <HANDLE> --json | jq '.raw_output_ref, .artifacts["prompt.txt"], .artifacts["stderr.log"]'
139+
```

docs/SETUP.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ make start-sim
5151
```
5252

5353
### 3.2 Real Mode (Tactical Execution)
54-
Use this mode for real-world tasks. It requires agents like `codex-agent` or `claude-code` to be installed.
54+
Use this mode for real-world tasks. It requires agents like `codex-agent` or `claude` to be installed.
5555
```bash
5656
# Edit .env to set ALL_ADAPTERS_SIMULATION_MODE=0
5757
make start
5858
```
5959

60+
Claude is executed in headless print mode as `claude -p --output-format json`. Codencer builds the task prompt, writes it to `prompt.txt`, delivers it on `stdin`, and runs the process from the attempt workspace root.
61+
6062
---
6163

6264
## 4. Daemon Management & Targeting
@@ -89,9 +91,15 @@ Use the provided script to start and build a daemon instance for a specific proj
8991
### 4.4 Environment Variables
9092
Codencer uses these variables to locate agent binaries and target the daemon:
9193
- `CODEX_BINARY`: Path to the `codex-agent` binary.
94+
- `CLAUDE_BINARY`: Path to the `claude` binary. Defaults to `claude`.
9295
- `OPENCLAW_ACPX_BINARY`: Path to the `acpx` CLI (for OpenClaw support).
9396
- `ORCHESTRATORD_URL`: URL of the daemon (default: `http://localhost:8085`).
9497

98+
### 4.5 Claude Adapter Notes
99+
- Install the Claude CLI so the `claude` binary is available on your `$PATH`, or point `CLAUDE_BINARY` at the full path.
100+
- Codencer does not pass a workspace flag to Claude. The attempt workspace is supplied via process `cwd`.
101+
- Claude raw output is preserved in `stdout.log`; Codencer parses that JSON and synthesizes the normalized `result.json`.
102+
95103
---
96104

97105
## 5. OpenClaw Setup (Experimental / Alpha)

docs/TROUBLESHOOTING.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@ Always start here to verify your environment:
2323

2424
### 2.2 "Agent Binary Not Found" (`failed_adapter`)
2525
**Symptoms**: Submitting a task fails immediately; `doctor` shows `MISSING` for an adapter.
26-
- **Cause**: The bridge cannot find the `codex-agent` or `claude-code` binary in your `$PATH`.
26+
- **Cause**: The bridge cannot find the `codex-agent` or `claude` binary in your `$PATH`.
2727
- **Fix**:
2828
- Export the specific path: `export CODEX_BINARY=/path/to/codex-agent`.
29+
- Or export `CLAUDE_BINARY=/path/to/claude` if the Claude CLI is installed outside your default `$PATH`.
2930
- Or ensure the binary is in your global `$PATH`.
3031

32+
### 2.2.1 "Malformed or Missing Claude Result Output" (`failed_terminal`)
33+
**Symptoms**: Claude starts, but the final result summary mentions malformed or missing Claude output.
34+
- **Cause**: The `claude` process did not emit a final JSON `result` object on stdout, or another tool/script polluted stdout.
35+
- **Fix**:
36+
- Inspect `./bin/orchestratorctl step logs <UUID>` for the raw stdout payload.
37+
- Inspect `./bin/orchestratorctl step artifacts <UUID>` and review `stderr.log` for CLI/auth/runtime errors.
38+
- Re-run `claude -p --output-format json` directly in the repo if you suspect local CLI/environment issues.
39+
3140
### 2.3 "Workspace Creation Failed" (`failed_bridge`)
3241
**Symptoms**: `failed_bridge` reported during attempt.
3342
- **Cause**: Git worktree conflict or permission issue.
@@ -133,6 +142,7 @@ Codencer uses specific states to distinguish between **instructional failure** (
133142
| `failed_adapter` | **Infrastructure** | The agent binary crashed (e.g. API error, OOM). | Check `step logs`. |
134143
| `failed_bridge` | **Infrastructure** | Codencer failed (e.g. Disk Full, Git Error, Provisioning). | Check daemon logs. |
135144
| `timeout` | **Infrastructure** | Task exceeded `timeout_seconds` and was killed. | Increase timeout. |
145+
| `cancelled` | **Infrastructure / Operator Action** | Task was explicitly interrupted before completion. | Re-run or submit a follow-up task. |
136146

137147
---
138148

docs/internal/GAP_AUDIT.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
| **CLI & MCP Layer** |**Ready (Stable)** | Native | Human-readable hints, logs, and structured JSON. |
2020
| **Codex Adapter** |**Ready (Stable)** | CLI Wrapper | High-fidelity relay with artifact harvesting. |
2121
| **OpenClaw Adapter** | 🧪 **Experimental (Alpha)** | ACPX Wrapper | Functional alpha; basic lifecycle tracking. |
22-
| **Claude/Qwen Adapters** | 🟡 **Functional** | CLI Wrapper | Basic subprocess wrappers; lacks deep extraction. |
22+
| **Claude Adapter** | 🟢 **Supported (Beta)** | CLI Wrapper | Uses `claude -p --output-format json` with stdin prompt delivery, cwd-based execution, synthesized result mapping, and fake-binary integration coverage. Live authenticated Claude service calls are not exercised in repo tests. |
23+
| **Qwen Adapter** | 🟡 **Functional** | CLI Wrapper | Basic subprocess wrapper; narrower evidence extraction than Codex/Claude. |
2324
| **Simulation Mode** |**Ready (Stable)** | Native | Robust stubs for orchestrator validation. |
2425
| **Adaptive Routing** | 🧪 **Prototype** | Heuristic | Static fallback chain; not yet benchmark-driven. |
2526
| **Governance** |**Ready (Stable)** | Manual | MIT Licensed; `CONTRIBUTING.md` authored. |

internal/adapters/claude/adapter.go

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,38 @@ package claude
22

33
import (
44
"context"
5+
"fmt"
56
"log/slog"
7+
"os"
8+
"os/exec"
69
"sync"
710

811
"agent-bridge/internal/adapters/common"
912
"agent-bridge/internal/domain"
1013
)
1114

15+
const (
16+
binaryName = "claude"
17+
binaryEnvVar = "CLAUDE_BINARY"
18+
)
19+
20+
type processState struct {
21+
cancel context.CancelFunc
22+
done chan struct{}
23+
24+
mu sync.Mutex
25+
cmd *exec.Cmd
26+
}
27+
1228
// Adapter implements domain.Adapter for the Claude agent.
1329
type Adapter struct {
14-
processes map[string]*context.CancelFunc
1530
mu sync.Mutex
31+
processes map[string]*processState
1632
}
1733

1834
func NewAdapter() *Adapter {
1935
return &Adapter{
20-
processes: make(map[string]*context.CancelFunc),
36+
processes: make(map[string]*processState),
2137
}
2238
}
2339

@@ -31,27 +47,40 @@ func (a *Adapter) Capabilities() []string {
3147

3248
func (a *Adapter) Start(ctx context.Context, step *domain.Step, attempt *domain.Attempt, workspaceRoot, attemptArtifactRoot string) error {
3349
a.mu.Lock()
34-
defer a.mu.Unlock()
35-
3650
if _, exists := a.processes[attempt.ID]; exists {
51+
a.mu.Unlock()
3752
return nil
3853
}
3954

55+
state := &processState{done: make(chan struct{})}
56+
a.processes[attempt.ID] = state
57+
a.mu.Unlock()
58+
59+
isSimulation := common.IsSimulationEnabled(a.Name())
60+
if !isSimulation {
61+
if _, err := resolveBinary(); err != nil {
62+
a.mu.Lock()
63+
delete(a.processes, attempt.ID)
64+
a.mu.Unlock()
65+
return err
66+
}
67+
}
68+
4069
execCtx, cancel := context.WithCancel(context.Background())
41-
a.processes[attempt.ID] = &cancel
70+
state.cancel = cancel
4271

4372
go func() {
73+
defer close(state.done)
4474
defer cancel()
45-
opts := common.ExecutionOptions{
46-
AdapterName: a.Name(),
47-
BinaryName: "claude-code",
48-
BinaryEnvVar: "CLAUDE_BINARY",
49-
Args: []string{"run", "--workspace", workspaceRoot, "--output", attemptArtifactRoot},
50-
Workspace: workspaceRoot,
51-
ArtifactRoot: attemptArtifactRoot,
75+
76+
var err error
77+
if isSimulation {
78+
err = common.RunSimulation(execCtx, step, attempt, attemptArtifactRoot, workspaceRoot)
79+
} else {
80+
err = runAttempt(execCtx, state, step, attempt, workspaceRoot, attemptArtifactRoot)
5281
}
5382

54-
if err := common.InvokeLocal(execCtx, step, attempt, opts); err != nil {
83+
if err != nil {
5584
slog.Error("Claude Adapter: Execution failed", "attemptID", attempt.ID, "error", err)
5685
}
5786

@@ -65,22 +94,34 @@ func (a *Adapter) Start(ctx context.Context, step *domain.Step, attempt *domain.
6594

6695
func (a *Adapter) Poll(ctx context.Context, attemptID string) (bool, error) {
6796
a.mu.Lock()
68-
defer a.mu.Unlock()
69-
_, running := a.processes[attemptID]
70-
return running, nil
97+
state, exists := a.processes[attemptID]
98+
a.mu.Unlock()
99+
if !exists {
100+
return false, nil
101+
}
102+
103+
select {
104+
case <-state.done:
105+
return false, nil
106+
default:
107+
return true, nil
108+
}
71109
}
72110

73111
func (a *Adapter) Cancel(ctx context.Context, attemptID string) error {
74112
a.mu.Lock()
75-
defer a.mu.Unlock()
76-
77-
cancelFunc, exists := a.processes[attemptID]
113+
state, exists := a.processes[attemptID]
114+
a.mu.Unlock()
78115
if !exists {
79116
return nil
80117
}
81118

82-
(*cancelFunc)()
83-
delete(a.processes, attemptID)
119+
if state.cancel != nil {
120+
state.cancel()
121+
}
122+
if err := stopProcess(state); err != nil {
123+
return err
124+
}
84125
return nil
85126
}
86127

@@ -89,5 +130,20 @@ func (a *Adapter) CollectArtifacts(ctx context.Context, attemptID string, attemp
89130
}
90131

91132
func (a *Adapter) NormalizeResult(ctx context.Context, attemptID string, artifacts []*domain.Artifact) (*domain.ResultSpec, error) {
92-
return common.NormalizeStandardResult(attemptID, artifacts)
133+
isSimulation := common.IsSimulationEnabled(a.Name())
134+
return NormalizeCore(attemptID, artifacts, a.Name(), isSimulation)
135+
}
136+
137+
func resolveBinary() (string, error) {
138+
binary := os.Getenv(binaryEnvVar)
139+
if binary == "" {
140+
binary = binaryName
141+
}
142+
143+
binaryPath, err := exec.LookPath(binary)
144+
if err != nil {
145+
return "", fmt.Errorf("claude binary %q not found or not executable. Set %s to a valid path or enable simulation with %s_SIMULATION_MODE=1: %w", binary, binaryEnvVar, "CLAUDE", err)
146+
}
147+
148+
return binaryPath, nil
93149
}

0 commit comments

Comments
 (0)