Skip to content

Commit ed6af27

Browse files
committed
Add backend preflight and generic agent skills
1 parent cf2277f commit ed6af27

6 files changed

Lines changed: 144 additions & 10 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
## Workflow
1212
- Always run `dune build` after edits and fix all errors before moving on.
13-
- A Claude Code hook runs `dune build` automatically after Edit/Write — check its output.
13+
- An agent hook may run `dune build` automatically after edits — check its output.
1414
- All warnings except 44 (open-shadow) and 70 (missing-mli) are fatal errors.
1515
- The pre-commit hook runs build + test + format check — commits will be rejected if any fail.
1616

@@ -86,7 +86,7 @@ Pitfalls when reading a bundle:
8686
trust the `agent_before`/`agent_after` fields embedded in each event for
8787
ground-truth agent state at that timestamp.
8888
- `activity_log.events` (inside the snapshot) carries the human-readable
89-
per-session error strings — `"Session failed (Claude) — exit 1: …"`,
89+
per-session error strings — `"Session failed (<backend>) — exit 1: …"`,
9090
rebase failure detail, etc. Check it before concluding that `complete`
9191
events are opaque; `complete.result` only carries the structured
9292
`session_result` (with `detail : string option` post-error-capture-fix).

bin/main.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4093,6 +4093,14 @@ let run_with_config ~no_lock (config : config) gameplan existing_snapshot =
40934093
Printf.eprintf "Error: %s\n" msg;
40944094
Stdlib.exit 1
40954095
in
4096+
(match
4097+
Backend_preflight.validate ~default_backend:config.backend
4098+
~cli_model:cli_model_opt ~repo_config ()
4099+
with
4100+
| Ok () -> ()
4101+
| Error errs ->
4102+
Base.List.iter errs ~f:(fun e -> Printf.eprintf "Error: %s\n" e);
4103+
Stdlib.exit 1);
40964104
let registry =
40974105
Backend_registry.create ~process_mgr ~clock ~timeout:session_timeout
40984106
~setsid_exec

lib/backend_preflight.ml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
open Base
2+
3+
let command_for_backend = function
4+
| "claude" -> Some "claude"
5+
| "codex" -> Some "codex"
6+
| "opencode" -> Some "opencode"
7+
| "pi" -> Some "pi"
8+
| "gemini" -> Some "gemini"
9+
| "patch-agent" -> Some "patch-agent"
10+
| _ -> None
11+
12+
let path_dirs getenv_opt =
13+
match getenv_opt "PATH" with
14+
| None | Some "" -> []
15+
| Some path ->
16+
String.split path ~on:':'
17+
|> List.map ~f:(fun dir -> if String.is_empty dir then "." else dir)
18+
19+
let is_executable_file path =
20+
try
21+
match (Unix.stat path).Unix.st_kind with
22+
| Unix.S_REG ->
23+
Unix.access path [ Unix.X_OK ];
24+
true
25+
| Unix.S_DIR | Unix.S_CHR | Unix.S_BLK | Unix.S_LNK | Unix.S_FIFO
26+
| Unix.S_SOCK ->
27+
false
28+
with Unix.Unix_error _ -> false
29+
30+
let find_executable ?(getenv_opt = Stdlib.Sys.getenv_opt)
31+
?(is_executable = is_executable_file) command =
32+
if String.contains command '/' then
33+
if is_executable command then Some command else None
34+
else
35+
path_dirs getenv_opt
36+
|> List.find_map ~f:(fun dir ->
37+
let path = Stdlib.Filename.concat dir command in
38+
if is_executable path then Some path else None)
39+
40+
let check_backend ?getenv_opt ?is_executable backend =
41+
match command_for_backend backend with
42+
| None ->
43+
Error
44+
(Printf.sprintf "unknown backend %S; cannot run backend preflight"
45+
backend)
46+
| Some command -> (
47+
match find_executable ?getenv_opt ?is_executable command with
48+
| Some _ -> Ok ()
49+
| None ->
50+
Error
51+
(Printf.sprintf
52+
"backend %S requires executable %S on PATH, but it was not \
53+
found or is not executable. Install that backend CLI, fix \
54+
PATH, or choose another backend with --backend."
55+
backend command))
56+
57+
let selected_backends ~default_backend ~cli_model ~(repo_config : Repo_config.t)
58+
=
59+
let add acc backend =
60+
if List.mem acc backend ~equal:String.equal then acc else backend :: acc
61+
in
62+
let acc = add [] default_backend in
63+
let uses_routes =
64+
match cli_model with
65+
| Some model -> Backend_routing.is_auto_model (Some model)
66+
| None -> false
67+
in
68+
let acc =
69+
if uses_routes then
70+
List.fold repo_config.complexity_routes ~init:acc
71+
~f:(fun acc (_, route) -> add acc route.Repo_config.backend)
72+
else acc
73+
in
74+
List.rev acc
75+
76+
let validate ?getenv_opt ?is_executable ~default_backend ~cli_model ~repo_config
77+
() =
78+
let backends = selected_backends ~default_backend ~cli_model ~repo_config in
79+
let errors =
80+
List.filter_map backends ~f:(fun backend ->
81+
match check_backend ?getenv_opt ?is_executable backend with
82+
| Ok () -> None
83+
| Error msg -> Some msg)
84+
in
85+
match errors with [] -> Ok () | _ :: _ -> Error errors
86+
87+
let%test "selected_backends ignores routes unless cli model is auto" =
88+
let repo_config =
89+
{
90+
Repo_config.complexity_routes =
91+
[ (1, { backend = "codex"; model = None }) ];
92+
review_backends = [];
93+
}
94+
in
95+
List.equal String.equal
96+
(selected_backends ~default_backend:"claude" ~cli_model:None ~repo_config)
97+
[ "claude" ]
98+
99+
let%test "selected_backends includes routes for auto model" =
100+
let repo_config =
101+
{
102+
Repo_config.complexity_routes =
103+
[
104+
(1, { backend = "codex"; model = None });
105+
(2, { backend = "claude"; model = Some "sonnet" });
106+
];
107+
review_backends = [];
108+
}
109+
in
110+
List.equal String.equal
111+
(selected_backends ~default_backend:"claude" ~cli_model:(Some "auto")
112+
~repo_config)
113+
[ "claude"; "codex" ]
114+
115+
let%test "validate reports missing backend executable" =
116+
let repo_config = Repo_config.empty in
117+
match
118+
validate
119+
~getenv_opt:(fun _ -> Some "/bin")
120+
~is_executable:(fun _ -> false)
121+
~default_backend:"codex" ~cli_model:None ~repo_config ()
122+
with
123+
| Error [ msg ] ->
124+
String.is_substring msg ~substring:"codex"
125+
&& String.is_substring msg ~substring:"not found or is not executable"
126+
| Ok () | Error _ -> false

scripts/install-hooks.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# install-hooks — install onton's git hooks into the repo's hooks dir
33
#
44
# Currently installs:
5-
# - post-checkout: runs sync-skills.sh after branch switches so ~/.claude/skills
5+
# - post-checkout: runs sync-skills.sh after branch switches so ~/.agents/skills
66
# always points at the current checkout
77
# - pre-commit: runs dune build / runtest / fmt against the project's local
88
# opam switch (5.4.0), not whatever the user's default switch happens to be

scripts/sync-skills.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/bin/sh
2-
# sync-skills — symlink repo skills/ into ~/.claude/skills/ for cross-project discovery
2+
# sync-skills — symlink repo skills/ into ~/.agents/skills/ for discovery
33
#
44
# Run once after cloning (or whenever onton's skills change) to make the skills
5-
# globally available to Claude Code regardless of which repo you're working in.
5+
# globally available to coding agents regardless of which repo you're working in.
66
# Also invoked automatically by the post-checkout hook installed via
77
# scripts/install-hooks.sh.
88
#
@@ -19,7 +19,7 @@ set -e
1919
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
2020
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
2121
SKILLS_DIR="$REPO_ROOT/skills"
22-
TARGET_DIR="$HOME/.claude/skills"
22+
TARGET_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
2323

2424
[ -d "$SKILLS_DIR" ] || exit 0
2525

skills/write-workstream/references/workstream-format.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@
4444

4545
## Complete Example: Onton Orchestrator
4646

47-
Below is a real-world workstream demonstrating all sections. Onton is an OCaml orchestrator that manages parallel Claude Code agents executing gameplan patches.
47+
Below is a real-world workstream demonstrating all sections. Onton is an OCaml orchestrator that manages parallel coding agents executing gameplan patches.
4848

4949
# Workstream: Onton Orchestrator
5050

5151
## Vision
5252

53-
Build a standalone OCaml binary that parses gameplans, builds dependency graphs, and spawns concurrent Claude Code agents in isolated git worktrees. It polls GitHub for PR status and reacts to merges, reviews, and CI. The type system and a formal specification (Pantagruel) serve as correctness tools, with property-based tests derived from the spec.
53+
Build a standalone OCaml binary that parses gameplans, builds dependency graphs, and spawns concurrent coding agents in isolated git worktrees. It polls GitHub for PR status and reacts to merges, reviews, and CI. The type system and a formal specification (Pantagruel) serve as correctness tools, with property-based tests derived from the spec.
5454

5555
## Current State
5656

@@ -85,7 +85,7 @@ The OCaml project has a build skeleton (dune 3.21, OCaml 5.4, Jane Street ppx ec
8585

8686
**Definition of Done**:
8787
- QCheck2 property tests for patch_agent, graph, poller, orchestrator, reconciler
88-
- GitHub HTTP wiring (cohttp-eio), Claude subprocess spawning (Eio.Process)
88+
- GitHub HTTP wiring (cohttp-eio), backend subprocess spawning (Eio.Process)
8989
- Full TUI with status table, detail pane, ANSI rendering
9090
- Three-fiber Eio event loop (TUI renderer, poller, runner)
9191
- JSON state persistence
@@ -113,7 +113,7 @@ The OCaml project has a build skeleton (dune 3.21, OCaml 5.4, Jane Street ppx ec
113113
### Milestone 4: onton-completeness-pt-4
114114

115115
**Definition of Done**:
116-
- Claude stream-JSON parsing for live activity and PR auto-detection
116+
- Backend stream parsing for live activity and PR auto-detection
117117
- PR number persistence (survives restarts)
118118
- Persistence migration for forward-compatible snapshots
119119
- GitHub comment ID tracking for dedup

0 commit comments

Comments
 (0)