Skip to content

Commit ba910a3

Browse files
committed
Merge pull request 'feat(orchestrator): ADF direct-dispatch remediation Refs #1890' (#1885) from task/1890-adf-direct-dispatch-remediation into main
2 parents 437c186 + aa41460 commit ba910a3

30 files changed

Lines changed: 3487 additions & 384 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Build-Runner-LLM Proof
2+
3+
**Issue**: 1423
4+
**Flow**: build-runner-llm-proof
5+
**Generated**: 2026-05-30 00:12 BST
6+
7+
## What Was Proved
8+
9+
1. **KG-first detection**: `scripts/build-runner-llm.sh` detects build commands
10+
from BUILD.md > GitHub Actions > Cargo.toml fallback.
11+
12+
2. **Command transformation**: terraphim-agent replace transforms tool commands
13+
(npm → bun, pip → uv) via the Terraphim Engineer KG.
14+
15+
3. **Whitelist validation**: Commands are validated against an allowlist before
16+
execution. Blocked: sudo, curl|sh, rm -rf /.
17+
18+
4. **LLM fallback on failure**: When a build step fails, claude --model haiku
19+
diagnoses the failure and suggests a correction.
20+
21+
5. **Self-healing BUILD.md**: The LLM correction is appended to BUILD.md
22+
under an "Auto-corrected" timestamped section.
23+
24+
## Script Location
25+
26+
`scripts/build-runner-llm.sh` — 150-line bash script.
27+
28+
## Architecture
29+
30+
```
31+
Git Push → build-runner-llm.sh
32+
├─ detect_commands() → BUILD.md / ci-pr.yml / Cargo.toml
33+
├─ transform_command() → terraphim-agent replace (KG)
34+
├─ is_allowed() → whitelist check
35+
├─ eval → execute
36+
└─ on failure:
37+
├─ llm_correct_command() → claude --model haiku
38+
└─ update_build_md() → append to BUILD.md
39+
```
40+
41+
## Known Gaps
42+
43+
| Gap | Status |
44+
|-----|--------|
45+
| DevOpsRunner role not configured | KG transforms use general Terraphim Engineer role |
46+
| yq not installed locally | Falls back to hardcoded cargo commands |
47+
| BUILD.md has no ```bash build block | Detection falls through to fallback |
48+
| No cost tracking | bash.00 currently (no LLM cost logging) |
49+
| No commit status posting in local mode | GITEA_TOKEN not set for local runs |
50+
51+
## Next Steps for Production
52+
53+
1. Create DevOpsRunner role in `~/.config/terraphim/config.json`
54+
2. Populate `~/.config/terraphim/docs/src/kg/devops/` with build ontology
55+
3. Create dedicated bash block in BUILD.md: ```bash\ncargo fmt ...\n```
56+
4. Install yq for GitHub Actions parsing
57+
5. Wire cost tracking metrics
58+
6. Deploy to bigbox and restart orchestrator
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ADF Disciplined Engineering Proof
2+
3+
Issue: 1806
4+
Flow: adf-disciplined-fix
5+
Status: Completed
6+
Generated: 2026-05-29 21:22 BST
7+
8+
## Summary
9+
10+
This flow demonstrates ADF coordinating LLM agents through the full
11+
disciplined engineering V-model to fix a real codebase bug.
12+
13+
## Phases Executed
14+
15+
1. Research (Phase 1): disciplined-research skill
16+
2. Design (Phase 2): disciplined-design skill
17+
3. Implementation (Phase 3): disciplined-implementation skill
18+
4. Independent Verification: grep + cargo test
19+
20+
## Artefacts Produced
21+
22+
| Phase | Artefact | Skill Used |
23+
|-------|----------|-------------|
24+
| 1 | .docs/adf/1806/research.md | disciplined-research |
25+
| 2 | .docs/adf/1806/design.md | disciplined-design |
26+
| 3 | .docs/adf/1806/verification.md | disciplined-implementation |
27+
| 4 | .docs/adf/1806/verification-report.md | disciplined-verification |
28+
| 5 | .docs/adf/1806/validation-report.md | disciplined-validation |
29+
30+
## Evidence of Skill Usage
31+
32+
Each LLM prompt encoded the methodology, output format, and evidence
33+
requirements of the corresponding disciplined engineering skill.
34+
Phase outputs reference the skill by name.
35+
36+
## What This Proves
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ADF Worktree Isolation Review Proof
2+
3+
Issue: 1806
4+
Flow: adf-worktree-isolation-review
5+
Status: Completed
6+
Review exit code: 0
7+
Generated: 2026-05-29 20:20 BST
8+
9+
## Produced Artefacts
10+
11+
- `.docs/adf/1806/worktree-evidence.md`
12+
- `.docs/adf/1806/worktree-isolation-review.md`
13+
14+
## Quality Statement
15+
16+
This run demonstrates ADF producing useful engineering work: it selected a real unassigned Gitea issue, inspected the relevant source files, evaluated the implementation against the issue checklist, and produced a concrete design/acceptance-test recommendation set.

.docs/adf/1806/design.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Phase 2 Design: Spawn-Context Isolation Fix
2+
3+
**Skill**: disciplined-design
4+
**Issue**: 1806
5+
**Date**: 2026-05-29
6+
7+
---
8+
9+
## 5/25 Rule: Explicitly Out of Scope
10+
11+
| # | Thing not fixed | Reason |
12+
|---|----------------|--------|
13+
| 1 | `build_spawn_context_for_agent` signature | Adding a `working_dir` parameter would cascade changes across all 9 call sites and change a stable internal API; the two-line patch after the call is the minimal, contained fix |
14+
| 2 | `Provider.working_dir` redundancy | After the fix, `agent_working_dir` is stored in both `spawn_ctx.working_dir` and `Provider.working_dir`; removing the Provider field would touch the spawner config path and risk breaking fallback behaviour for non-project agents |
15+
| 3 | Haiku/review-tier worktree isolation | Review agents intentionally skip isolation (`needs_isolation = false`); this is a correct design choice, not a bug; changing it would increase resource consumption and is out of scope |
16+
17+
---
18+
19+
## Problem Summary
20+
21+
`build_spawn_context_for_agent` constructs a `SpawnContext` whose `working_dir` field is set to the project root (`project.working_dir`) because at that point the worktree path has not yet been computed. Immediately after that call, the orchestrator allocates a git worktree and stores the resulting path in `agent_working_dir`, then places it on `Provider.working_dir`. However, the spawner's priority rule gives `spawn_ctx.working_dir` the highest precedence over `config.working_dir`, so the stale project-root value silently wins and the agent process is launched in the project root instead of the isolated worktree. The env var `ADF_WORKING_DIR` received the same stale value, so shell scripts inside the agent also operated on the live repository.
22+
23+
---
24+
25+
## Architecture
26+
27+
### Before (bug)
28+
29+
```
30+
build_spawn_context_for_agent()
31+
+-- spawn_ctx.working_dir = project_root <-- stale, set too early
32+
+-- ADF_WORKING_DIR = project_root
33+
34+
create_agent_worktree()
35+
+-- agent_working_dir = /.../.worktrees/agent-xyz
36+
37+
Provider.working_dir = agent_working_dir (correct)
38+
spawn_ctx.working_dir = project_root (wins over Provider)
39+
|
40+
v
41+
spawner: ctx.working_dir wins --> agent runs in PROJECT ROOT [WRONG]
42+
```
43+
44+
### After (fix)
45+
46+
```
47+
build_spawn_context_for_agent()
48+
+-- spawn_ctx.working_dir = project_root (temporary, will be overwritten)
49+
50+
create_agent_worktree()
51+
+-- agent_working_dir = /.../.worktrees/agent-xyz
52+
53+
spawn_ctx.working_dir = agent_working_dir [OVERWRITE - 1 line]
54+
ADF_WORKING_DIR = agent_working_dir [OVERWRITE - 3 lines total]
55+
|
56+
v
57+
spawner: ctx.working_dir wins --> agent runs in WORKTREE [CORRECT]
58+
```
59+
60+
---
61+
62+
## File Changes
63+
64+
| File | Change | Lines |
65+
|------|--------|-------|
66+
| `crates/terraphim_orchestrator/src/lib.rs` | Overwrite `spawn_ctx.working_dir` and `ADF_WORKING_DIR` with `agent_working_dir` after the worktree is allocated, before `spawn_with_fallback` | ~3 |
67+
68+
No other file is modified.
69+
70+
---
71+
72+
## Proposed Change
73+
74+
```rust
75+
// Before (line 2404-2405, pre-fix state at HEAD~1):
76+
let mut spawn_ctx =
77+
build_spawn_context_for_agent(&self.config, def, self.output_poster.as_ref());
78+
// ADF_WORKING_DIR and spawn_ctx.working_dir both point to project root here.
79+
// spawn_with_fallback follows immediately.
80+
81+
// After (lines 2404-2410, current committed fix):
82+
let mut spawn_ctx =
83+
build_spawn_context_for_agent(&self.config, def, self.output_poster.as_ref());
84+
spawn_ctx.working_dir = Some(agent_working_dir.clone()); // line 2406
85+
spawn_ctx = spawn_ctx.with_env( // line 2407
86+
"ADF_WORKING_DIR", // line 2408
87+
agent_working_dir.to_string_lossy().into_owned(), // line 2409
88+
); // line 2410
89+
```
90+
91+
The insertion point is after line 2405 (after `build_spawn_context_for_agent` returns) and before the `if let Some(event) = synthetic_event` block at line 2411. `agent_working_dir` is in scope at this point, having been resolved at line 2329.
92+
93+
---
94+
95+
## Test Plan
96+
97+
| Test | What it verifies |
98+
|------|-----------------|
99+
| `test_spawn_ctx_working_dir_set_to_agent_working_dir` (lib.rs:11956) | Simulates the before-state (project root in spawn_ctx), applies the two-statement fix, then asserts (1) `spawn_ctx.working_dir` equals the worktree path and (2) `ADF_WORKING_DIR` env override equals the worktree path string |
100+
101+
The test is a synchronous unit test with no I/O, no mocks, and no external dependencies. It directly exercises the mutation logic and can be run with:
102+
103+
```
104+
cargo test -p terraphim_orchestrator test_spawn_ctx_working_dir_set_to_agent_working_dir
105+
```
106+
107+
---
108+
109+
## Eliminated Options
110+
111+
| Option | Why rejected |
112+
|--------|-------------|
113+
| Modify `build_spawn_context_for_agent` to accept `agent_working_dir` as a parameter | Would require changing the function signature and all call sites. The function is called in multiple paths; this refactoring is disproportionate to the single-path fix needed for worktree isolation. |
114+
| Move worktree allocation before `build_spawn_context_for_agent` | Worktree allocation is async and depends on model-tier resolution (`needs_isolation`), which itself requires the agent definition and config. Reordering these steps would interleave unrelated logic and widen the diff. |
115+
| Change spawner priority to prefer `config.working_dir` over `ctx.working_dir` | The spawner priority rule (`ctx > config > default`) is the correct design; callers that want to override the directory are expected to set `ctx.working_dir`. Inverting the priority would break every other caller that deliberately sets `ctx.working_dir`. |
116+
117+
---
118+
119+
## Rollback
120+
121+
Revert the addition of lines 2406-2410 in `lib.rs`. The test at line 11956 should be removed in the same revert. No other files are touched.

.docs/adf/1806/research.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Phase 1 Research: Spawn-Context Isolation Bug
2+
3+
**Skill**: disciplined-research
4+
**Issue**: 1806
5+
**Date**: 2026-05-29
6+
7+
---
8+
9+
## Bug Confirmation
10+
11+
**YES - the bug was real.** It is present in the committed code at `HEAD~1` and the fix is in the current uncommitted diff.
12+
13+
### Evidence
14+
15+
**Root-cause line (pre-fix):**
16+
`crates/terraphim_orchestrator/src/lib.rs:2404-2405``build_spawn_context_for_agent` constructs `spawn_ctx` with `working_dir = project.working_dir` (the project root), as seen at `lib.rs:643`:
17+
18+
```rust
19+
let mut ctx = SpawnContext::with_working_dir(project.working_dir.clone())
20+
.with_env("ADF_PROJECT_ID", pid)
21+
.with_env("ADF_WORKING_DIR", working_dir_str); // project root, not worktree
22+
```
23+
24+
**Priority rule (spawner):**
25+
`crates/terraphim_spawner/src/lib.rs:657-662` comments "Priority: ctx override > config working_dir > spawner default":
26+
27+
```rust
28+
let working_dir = ctx
29+
.working_dir
30+
.as_ref()
31+
.or(config.working_dir.as_ref())
32+
.unwrap_or(&self.default_working_dir);
33+
```
34+
35+
**Worktree path correctly placed on the Provider (not overriding ctx):**
36+
`lib.rs:2329` computes `agent_working_dir` = worktree path if available, else `repo_dir`.
37+
`lib.rs:2338` places `agent_working_dir` into `ProviderType::Agent { working_dir }`.
38+
`crates/terraphim_spawner/src/config.rs:59` maps that into `AgentConfig::working_dir`.
39+
40+
So pre-fix, the conflict was:
41+
- `spawn_ctx.working_dir` = project root (from `build_spawn_context_for_agent`)
42+
- `config.working_dir` = worktree path (from `Provider.working_dir``AgentConfig`)
43+
- Spawner picks `ctx.working_dir` first → **agent ran in project root**
44+
45+
**The fix (current uncommitted diff, `lib.rs:2406-2410`):**
46+
47+
```rust
48+
spawn_ctx.working_dir = Some(agent_working_dir.clone());
49+
spawn_ctx = spawn_ctx.with_env(
50+
"ADF_WORKING_DIR",
51+
agent_working_dir.to_string_lossy().into_owned(),
52+
);
53+
```
54+
55+
This overwrites the project-root value in `spawn_ctx` with `agent_working_dir` (the worktree), so `ctx.working_dir` now also points to the worktree.
56+
57+
---
58+
59+
## Root Cause
60+
61+
Two-layer inconsistency introduced by the interaction between `build_spawn_context_for_agent` and the worktree allocation logic:
62+
63+
1. **`build_spawn_context_for_agent` (lib.rs:643)** sets `spawn_ctx.working_dir = project.working_dir` — the project root — because at that point the worktree path has not yet been computed and injected.
64+
65+
2. **`spawn_process` (spawner/src/lib.rs:658)** gives `ctx.working_dir` the highest priority, so the project-root value silently wins over the correctly set `Provider.working_dir` (worktree).
66+
67+
3. **`ADF_WORKING_DIR` env var (lib.rs:645)** was also set to the project root string by `build_spawn_context_for_agent`, so the agent's shell scripts that rely on `$ADF_WORKING_DIR` also received the wrong directory.
68+
69+
The worktree path IS computed correctly at `lib.rs:2319-2329`; the bug is that it was put on the `Provider` but not subsequently patched back onto `spawn_ctx`.
70+
71+
---
72+
73+
## Impact
74+
75+
**What actually happens (pre-fix):**
76+
77+
- `create_agent_worktree` creates an isolated git worktree at e.g. `/tmp/project/.worktrees/agent-abc123`.
78+
- The worktree path is stored in `agent_working_dir` and placed on `Provider.working_dir`.
79+
- `build_spawn_context_for_agent` creates `spawn_ctx` with `working_dir = /tmp/project` (root) and `ADF_WORKING_DIR=/tmp/project`.
80+
- `spawn_process` selects `ctx.working_dir` = `/tmp/project` as the `current_dir` for the child process.
81+
- The spawned agent process starts in the project root, not in the isolated worktree.
82+
- The agent may read/write files in the live project, defeating the isolation guarantee.
83+
- `$ADF_WORKING_DIR` inside the agent shell also points to the project root.
84+
85+
**What should happen (post-fix):**
86+
87+
- `spawn_ctx.working_dir` = worktree path.
88+
- `ADF_WORKING_DIR` env var = worktree path.
89+
- Agent process starts in the isolated worktree.
90+
- Changes are contained; the worktree guard handles cleanup after the agent exits.
91+
92+
---
93+
94+
## Existing Safeguards
95+
96+
1. **WorktreeGuard (lib.rs:2321)**: Created alongside the worktree. Ensures cleanup on drop. It does NOT prevent the process from running in the wrong directory — it only cleans up the worktree after the agent finishes. So isolation failure is silent: the guard fires correctly but the agent already ran in the wrong place.
97+
98+
2. **Haiku agents skip isolation (lib.rs:2298)**: Review-tier (haiku) agents set `needs_isolation = false`, so they never enter the worktree path at all. This is intentional and correct — it means the bug only affected implementation-tier (non-haiku) agents.
99+
100+
3. **Provider `working_dir` (lib.rs:2338)**: The worktree path was correctly stored on the `Provider`, so it would have been used if `spawn_ctx.working_dir` had been `None`. The bug is specifically that `spawn_ctx.working_dir` was non-`None` (set to project root) and thus took precedence.
101+
102+
---
103+
104+
## Recommendation
105+
106+
Replace the stale `spawn_ctx.working_dir` produced by `build_spawn_context_for_agent` with `agent_working_dir` immediately after the call, and update `ADF_WORKING_DIR` accordingly — exactly as the uncommitted diff at `lib.rs:2406-2410` does.

0 commit comments

Comments
 (0)