Junhanliu-dev/espalier-engineering

72 stars · Last commit 2026-08-17

Train your AI coders the way you'd train a vine — discover your codebase's actual patterns, then encode them as Claude Code rules, skills, agents, hooks, and a guided pipeline so generated code lands inside your conventions on the first try, not the fifth.

README preview

# Espalier Engineering

> An espalier trains a fruit tree to grow flat along a wall — pruned, wired, productive, and impossible to mistake for a wild one. **Espalier does the same thing for your AI coding agents:** it discovers the patterns already in your codebase, then encodes them as constraints so generated code grows along your conventions on the first try, not the fifth.

```text
/plugin marketplace add Junhanliu-dev/espalier-engineering
/plugin install espalier-engineering@espalier-engineering
/espalier-init
```

> **v0.20.0 — slice PRs: the run becomes reviewable one ticket at a time.** The field run that birthed v0.19 ended in a single integration PR of **103 files / +14,051 lines** — mergeable, not reviewable. v0.20.0 fixes review without touching the run's merge topology: with an **opt-in `pr` key** in a map's plan.json, the master's merge step becomes **open → merge → sync** — push the ticket branch, open its PR *against the integration branch* before the local merge, then merge locally exactly as before (union-merge rules intact) and push; the forge sees the PR's head contained in its base and **closes the slice PR as merged on its own**, preserving a one-ticket diff plus its CI verdict as the review surface. Review is advisory-after-merge (a PASSED slice already survived grill + reviewer + security agents); at completion `maprun-pr.sh final` opens the assembly PR whose body links every slice PR — the human sign-off gate. The obvious alternative — **stacked PRs** (one branch per ticket, based on its dependency) — was designed in full and rejected on review: squash-merge strategies break retargeted children, base derivation goes stateful when mid-stack branches delete, the rework loop was unspecified, and sibling conflicts would surface at run end instead of at each merge; the findings are recorded in the design doc. Also fixed on the way: `maprun-integration.sh` wrote its push block into the **shared** `.git/config` (blocking the operator's own pushes — latent in v0.19, fatal once the master must push); now worktree-scoped with a self-heal, plus `setup`'s push-permission probe and a scan for `pull_request` workflows whose `branches:` filter would silently skip CI on slice PRs. Without a `pr` config the lane is byte-for-byte v0.19 behavior: nothing is ever pushed. The lane also gains a second execution mode — **`worker_mode: inline`**: dispatch prepares the isolated worktree and spawns nothing; the master works each ticket in-session (stage agents spawned directly, questions asked live, no headless CLI required — Copilot-only installs included), one ticket at a time, while the headless modes keep the `watch` dashboard and their outlive-the-master workers. New fixture `run-07-pr-flow`; bootstrap Test 27 (stub-gh + bare-remote e2e, inline-dispatch asserts); migration #27. Design: [`docs/maprun-pr-lane-plan.md`](./docs/maprun-pr-lane-plan.md).

> **v0.19.0 — the run lane: a cleared map executes itself, survivably.** v0.18 ended with a handoff: a cleared map files FILED slices and `/espalier` adopts them one hand-driven session at a time. **`/espalier-maprun`** closes the loop — one interactive **master** pass reaps finished workers, merges what passed into an integration branch, relays parked worker questions to the human, grills and dispatches up to N headless `/espalier` pipelines (stages 1–6) into isolated **push-blocked git worktrees**, then stops; the workers keep grinding after your session ends. Everything the run knows lives on disk (`plan.json` + `state.json` beside the map), so the design survives the failure it was built for: being killed without warning — quota exhaustion reaps as resumable `QUOTA`, a dead worker's false stage claim is caught by a git-state check ("the worktree is the fact; the state file is the claim"), a recycled pid is a SUSPECT not a worker, and a resumed worktree first absorbs everything that landed on the integration branch. Long-horizon context hygiene is structural: masters run one pass per fresh session with subagent-read escalation excerpts, workers park questions inside their own change folder (no access to the master's state at all), and `worker_mode: staged` bounds a long ticket to one fresh session per stage group. The engine ships repo-agnostic (workspaces, verify commands, env seeds, union-merge paths all declared in `plan.json`, seeded from what init discovered) with first-class optional ClickUp/Harvest sync that never blocks the build. Field-built inside a production repo (a 14-slice Keystone schema map) before generalization; validated by a new **`eval/maprun/`** harness (six planted-hazard fixtures incl. a "just resolve the conflict yourself" human bait, 0.80 catch-rate gate — first full run 6/6, catch-rate 1.00, zero judge-verdict mismatches; provisional until judge validation + shadow fixtures) plus stub-CLI session-mode AND staged-mode dispatch e2e in the bootstrap suite. Suites: bootstrap 225/225, hooks 146/146; validation 52/57/62; migration #26. Design: [`docs/maprun-lane-plan.md`](./docs/maprun-lane-plan.md); migration: [`docs/migrating-v0.18-to-v0.19.md`](./docs/migrating-v0.18-to-v0.19.md).

> **v0.18.0 — the map lane: planning above one session, and a road into greenfield.** Espalier gains a third altitude. `/espalier <feature>` plans one session's work; **`/espalier-map <idea>`** now plans the work *above* that — an epic, a product on a boilerplate, a greenfield build — as a **decision map** under `espalier/maps/`: a named destination, decision tickets (grilling / research / prototype / task), a fog-of-war section for what can't be phrased yet, and a frontier you work **one ticket per session**. Adapted from Matt Pocock's `wayfinder` (MIT) with Espalier-grade enforcement bolted on: a **map-guard hook** machine-blocks writes outside the map while a session is active (the "agent started building mid-plan" failure structurally removed), a `max-open-tickets` cap stops waterfall over-charting, and every decision runs the grill's rules/wiki cross-check before it locks. A cleared map hands back FILED change skeletons the normal pipeline adopts one at a time — decision → ticket → change → commit stays one audit chain. And `/espalier-init` learns **greenfield**: a near-empty repo takes the two-pass *Decide, Then Bind* path (skeleton install → chart the conventions as decisions → init binds them as rules citing `decided_in:` tickets); boilerplate repos run init first, then map the product against the discovered conventions. Lane quality is measured, not asserted: a shipped read-only **stats hook** (`bash espalier/hooks/espalier-stats.sh`) reports review-round/rollback distributions, the grill-verdict mix, per-map ticket/fog state, and the charted-vs-uncharted cohort comparison (including the fix echo via `caused_by` links), while a new **`eval/map/` harness** (grill-harness pattern: planted decisions, simulated users, LLM judge, 0.80 catch-rate gate) validates the lane itself before release. Validation grows to 50/55/60; migration #25. Suites: bootstrap 197/197, hooks 146/146. Design: [`docs/map-lane-plan.md`](./docs/map-lane-plan.md); migration: [`docs/migrating-v0.17-to-v0.18.md`](./docs/migrating-v0.17-to-v0.18.md).

> **v0.17.0 — the gardener takes over (multi-dev maintenance, Release B-team).** Maintenance becomes a **scheduled singleton**: one rotating gardener runs doctor + prune once per interval in a single weekly maintenance PR, and everyone else's pre-flight defaults to "Proceed". Two small shared files carry the facts that matter through git: the tracked one-line **`espalier/.doctor-stamp`** (only a `clean` stamp satisfies the team; a `dirty:N` stamp satisfies just its writer; skewed stamps rejected; the doctor restamps `clean` when the session's prune cleared everything) and **conventions file-per-key** under `espalier/conventions/` (the towncrier trick: different keys can't conflict at all; a same-key double decision surfaces as an ordinary 5-line git conflict — the conflict *is* the race detection; append collisions resolve by keeping both lines, deduped at read time). No event logs, no fold algorithms, no format markers. Migration #24 is pure-copy only. Suites: bootstrap 177/177, hooks 127/127 with two-clone git sims. Plain-language tour: [`docs/multi-dev-maintenance-how-it-works.md`](./docs/multi-dev-maintenance-how-it-works.md); migration: [`docs/migrating-v0.16-to-v0.17.md`](./docs/migrating-v0.16-to-v0.17.md).

> **v0.16.0 — maintenance goes team-shaped (Release A: the compatibility floor).** With several developers on one repo, the single-dev maintenance loop breaks four ways: every clone sees a different staleness picture, nobody owns the shared upkeep, the bookkeeping files merge with conflicts, and rule changes diverge silently across branches. v0.16.0 ships the floor that fixes the last two and prepares the first two: an executable conventions reader (`conv_fold` — folds the legacy `.conventions.tsv` and the upcoming per-key `espalier/conventions/` files, width-tolerant, deduped, with clock-free status precedence), per-mechanism **maintenance lanes** (doctor and routine prunes ride one weekly maintenance PR; promotions ride the deciding feature branch as their own isolated commit behind a CODEOWNERS merge gate), a corrected promotion **race guard**, canonical-ref config keys, the one `.gitattributes` union entry (`.ask-gaps.tsv`), optional **CODEOWNERS generation**, worktree-correct hook install, an enforced migrate barrier, and conflict/slug-collision recipes. Validation grows to 48/53/58 checks (57-58). Suites: bootstrap 161/161, hooks 99/99. Design: [`docs/multi-dev-maintenance-implementation-plan.md`](./docs/multi-dev-maintenance-implementation-plan.md); plain-language tour: [`docs/multi-dev-maintenance-how-it-works.md`](./docs/multi-dev-maintenance-how-it-works.md).
>

View full repository on GitHub →