From f503eb89a04d4492845a77271898f76184cefc98 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 16:49:47 +0300 Subject: [PATCH 01/20] Add "driving the loop" prompt template and a loop-in-motion transcript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recommended-loops.md: a single-prompt invocation for spec-first and code-first, with the stop conditions (6-iteration hard cap, 2-iteration no-progress cap, escalate-on-open-question). Notes there is no separate "loop mode" — it's a structured prompt over the existing skills. - README: a "loop in motion" roleplay transcript under "What this looks like in practice", matching the existing style. Staged on a branch; not released yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 22 +++++++++++++++++ skills/allium/references/recommended-loops.md | 24 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/README.md b/README.md index ea79670..85b65e9 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,28 @@ A developer starts a new session days later, working on a different feature. The developer never mentioned invoicing or payment method capture. The Allium did, from a session that ended days ago. Without it, the LLM would have implemented the trial with no awareness of these constraints, and the gaps would have surfaced in production. +### The loop in motion + +A developer asks for a feature and lets the loop run to convergence. + +--- + +> ***> Add gift cards to checkout.*** +> +> ✻ I'll capture this as a spec first (`/elicit`). One thing to pin down before building: can a gift card be combined with a promo code, or are they mutually exclusive? +> +> ***> Mutually exclusive.*** +> +> ✻ Spec updated. Generated tests with `/propagate` and ran them — 4 new tests, all failing as expected (red). Implementing against them now. +> +> ✻ Tests green. `/weed` flagged a divergence: the code lets a gift card pay for shipping, but the spec scopes it to goods only. Fixing the code to match the spec. +> +> ✻ Re-ran: tests pass, `/weed` clean, no open questions. Spec, tests and code agree — converged. + +--- + +The loop did the work: elicitation forced the promo-code decision up front, the red step proved the tests were real, and verification (`/weed`) caught a drift the passing tests didn't. The agent stops when the three artefacts agree, not when the code merely runs. + ## Verification When the CLI is installed, `.allium` files are validated automatically after every write or edit. Diagnostics appear inline and the model fixes issues in the same turn. diff --git a/skills/allium/references/recommended-loops.md b/skills/allium/references/recommended-loops.md index b055c55..b677766 100644 --- a/skills/allium/references/recommended-loops.md +++ b/skills/allium/references/recommended-loops.md @@ -132,6 +132,30 @@ Both loops can be driven autonomously — `/tend` and `/weed` already ship as st **State worth tracking across ticks:** tests status (pass/fail counts), `/weed` verdict, count of open questions, and — for code-first — whether the last `/distill` pass found anything new. Convergence is all four trending to zero/clean. +## Driving the loop with one prompt + +You don't have to invoke the skills one at a time. Hand a single agentic session the goal, the entry point and the stop conditions, and let it run the phases itself. There is no separate "loop mode" to enable — this is a well-structured prompt; Allium supplies the spec, the tests and the verification signal the loop iterates against. + +**Spec-first:** + +> Goal: ``. Run the Allium spec-first loop (see this reference). +> 1. `/elicit` to capture the behaviour as a spec. Surface open questions to me before proceeding — don't guess. +> 2. `/propagate` tests, then confirm they fail before implementing; flag any that pass as already-covered or vacuous. +> 3. Implement against the spec + tests. Do not edit tests to make them pass. +> 4. Run tests → `/weed` → `allium` checks. +> 5. Repeat until tests pass, `/weed` is clean, and no open questions remain. +> Stop and ask me if a decision is needed, after **6 iterations**, or after **2 iterations with no progress**. Print a one-line state summary each iteration (tests, weed verdict, open questions). + +**Code-first:** replace step 1 with `/distill ` followed by a review separating intended from accidental behaviour, and add "a fresh `/distill` finds nothing new" to the exit condition in step 5. + +**Stop conditions matter as much as the steps.** Three keep an autonomous run timely and honest: + +- **Hard cap** — stop after **6 iterations**. +- **No-progress cap** — stop after **2 iterations** with no measurable change (test pass count, weed verdict, open-question count). This catches thrashing against a test the agent can't satisfy. +- **Escalate on open question** — a decision goes to the human, never a silent guess. + +The loop can also be driven by the autonomous `tend` and `weed` agents, or by a harness loop primitive (for example a self-paced `/loop` in Claude Code). Those supply the "keep going" mechanism; the procedure and the exit conditions above are unchanged. + ## The "produce the code" prompt There is no skill for implementation. Point the model at the spec and the propagated tests: From 3b418cd60924bac88085c432eef5c06036518df0 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 17:06:43 +0300 Subject: [PATCH 02/20] Add design note for /allium:loop (loop mode) Persist the loop-mode design sketch: the two layers (in-session procedure vs unattended re-invocation), entry-point detection, the tick, the verification harness, stop conditions, and a ledger in a dotfile. Captures two design considerations raised in review: - Nested loops: elicit's Q&A is an inner loop of the gather phase (and distill multi-pass / TDD are inner loops too); the model must support loops-within-loops with per-phase exit conditions. - Open-question scheduling: classify blocking/direction-changing (escalate eagerly, avoid throwaway) vs non-blocking/peripheral (park and batch for autonomy); elicit's inner loop front-loads the structural questions. Status: proposed, not implemented. Living doc. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 105 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 design/loop-mode.md diff --git a/design/loop-mode.md b/design/loop-mode.md new file mode 100644 index 0000000..c4d9e37 --- /dev/null +++ b/design/loop-mode.md @@ -0,0 +1,105 @@ +# Loop mode (`/allium:loop`) — design note + +> **Status: Proposed, not implemented.** Living document — update as the design evolves. +> Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. + +## 1. Purpose + +Drive the Allium loop — gather context → take action → verify → repeat — to convergence on a stated goal, with a trustworthy (behavioural) verification signal and safe termination. Allium already owns the two hardest ingredients of a trustworthy autonomous loop: **durable context** (the spec persists) and a **behavioural verification signal** (`propagate` tests + `weed` + CLI checks trace back to intent, not to the agent's own output). Loop mode packages those into a driver. + +## 2. Two layers (substrate-agnostic by design) + +- **Layer 1 — in-session procedure.** `/allium:loop` is a skill the agent follows within a single session, iterating by calling tools until convergence or a cap. Needs nothing special from the harness; portable across all skills-capable editors. This is the foundation and the MVP. +- **Layer 2 — unattended re-invocation.** A harness primitive (a self-paced `/loop`, cron, a background agent, a Ralph-style `while` script) re-fires the *same* skill across sessions/time until the goal is met. The substrate is editor-specific; Allium plugs into it rather than reimplementing it. + +The skill is designed so the identical procedure serves both: run once and it self-iterates in-session; re-fire it and it resumes from the ledger (§10). Full autonomy is the destination; Layer 1 is how we get there portably. + +## 3. Invocation + +- `/allium:loop ` — e.g. `/allium:loop add gift cards to checkout`. +- Optional: entry-point override (`from-intent` | `from-code`), iteration cap, target area/scope. +- No goal → infer from context or ask one question. + +## 4. Entry-point detection (decision tree) + +Pick the starting mode; confirm with the user only if ambiguous: + +- Spec for the area **absent**, behaviour described → **spec-first** (`elicit`). +- Spec absent, **existing code** in the area → **code-first** (`distill`). +- Spec **present**, requirements changed → **tend**, then continue. +- Spec present, code may have drifted → **weed-first** maintenance loop. + +## 5. The tick (one iteration) + +1. **Gather/refresh context** — elicit/distill/tend, *only if* the spec needs to change this tick (each may be an inner loop; see §6). +2. **Act** — `propagate` (if spec changed) → **red-check** new tests → implement. +3. **Verify** — run the project's test command; `weed`; `allium check`/`analyse`. Executed and parsed, never narrated. +4. **Evaluate** the convergence invariant: tests pass ∧ weed clean ∧ no open questions (or only parked, non-blocking ones; §7) ∧ (code-first) distill finds nothing new. +5. **Route the fix** — test fails → fix code; test wrong → tend spec + re-propagate; weed says spec wrong → tend; open question → classify and handle (§7). +6. **Record state** — one-line ledger entry: `tick n · tests x/y · weed: clean/dirty · openQ: blocking k / parked m`. + +## 6. Nested loops (inner loops within phases) + +The outer convergence loop is **not flat** — several phases are themselves loops, each with its own local exit condition that must be met before the outer loop advances. The design (and eventually the skill) must explicitly model loops-within-loops; treating a phase as a single atomic call is wrong. + +- **Elicitation inner loop (`/elicit:loop`)** — question ↔ user answer until the model judges the spec covers the edge cases ("I have enough to specify this"). This is the gather-context phase's inner loop, and it **front-loads structural decisions** before any code is written. +- **Distillation** — multi-pass (Ralph-style) until a pass surfaces nothing new. +- **TDD inner loop** — implement ↔ run tests until green. +- **Weed/verify clarification** — may spawn its own small clarification loop. + +The ledger (§10) records which inner loop is active so an unattended resume (§2, Layer 2) re-enters at the right place. + +## 7. Open questions: scheduling & escalation + +**The tension.** Maximal autonomy says: do everything you can on your own and **park** open questions to batch at the end (fewer interruptions — our goal). But a late answer can **change direction** and make completed work throwaway. Neither "always ask now" nor "always defer" is right. + +**Resolution — classify, don't pick a single policy.** + +- **Blocking / direction-changing** — the answer reshapes the spec, and therefore the tests and code. → **Escalate eagerly**, before doing dependent work. Deferring these is what creates throwaway. +- **Non-blocking / peripheral** — doesn't affect what's already built or what's next. → **Park** in the spec's `open questions` section + the ledger, and batch at the end. This is where deferring is safe and buys autonomy. + +**Decision rule:** a question is *blocking* iff the next unit of work depends on its answer (proceeding would require assuming one). The loop does all work independent of unresolved questions, and escalates a question only when it gates further independent progress. + +**Minimise throwaway when proceeding past a parked question:** +- Log the assumption explicitly (in the spec / ledger). +- Prefer **cheap-to-revise** work; defer expensive or irreversible work that hangs off an unresolved structural question. + +**Natural front-loading.** Elicit's inner loop (§6) already resolves the structural questions up front, so in spec-first the direction-changers are largely settled before propagate/implement. Park-and-batch then mostly applies to the smaller questions that surface later during distill, weed or verify — which is exactly the safe case. + +## 8. Verification harness (load-bearing) + +- **Discover the test command** — reuse `propagate`'s discovery checklist (framework, runner, test paths). Unknown → ask once. +- **CLI presence** — needs `allium` on PATH for structural checks. Absent → run with a reduced signal and *say so* (this is why the first-run install notice matters). +- If verification can't actually run, the loop **degrades loudly to assisted mode** — it never narrates a pass it didn't execute. + +## 9. Stop conditions & safety + +- **Hard cap** — default **6** iterations. +- **No-progress cap** — default **2** ticks with no measurable change (test pass count, weed verdict, open-question count). Catches thrashing against a test it can't satisfy. +- **Escalate on blocking open question** (§7). +- **Anti-cheat (non-negotiable)** — never edit a generated test to pass; a wrong-looking test means tend the spec + re-propagate, logged. +- Honour `config` (no magic numbers); decompose a too-big goal into sub-goals rather than blowing the context budget. + +## 10. State & resumability (ledger) + +- The **loop ledger lives in a dotfile** (e.g. `.allium-loop/.json`): goal, mode, tick count, active inner loop, last verdicts, and parked (non-blocking) open questions. Blocking questions are escalated, not parked. +- The ledger is what makes the same skill resumable unattended (§2, Layer 2): a fresh context reads it and continues idempotently. +- Open follow-on: should the dotfile be `.gitignore`-d by default? (See §12.1 — mirrors the install-notice marker debate.) + +## 11. Output / reporting + +- Per-tick one-line state summary. +- Final report: what converged, what was escalated, residual parked questions, test/weed status. + +## 12. Open design questions + +1. **Ledger dotfile + `.gitignore`** — decided it lives in a dotfile (§10); still to settle whether loop mode offers to gitignore it. +2. **Auto-commit per converged tick**, or leave the working tree to the user? +3. **Relationship to the `tend`/`weed` agents** — does `loop` *call* them, or inline their procedures? +4. **Auto-decompose** large goals vs. surface a plan and ask. +5. **Configurable caps** — flags, a `config` block, or fixed defaults? + +## 13. Out of scope / risks + +- **Out of scope (for the MVP):** building an Allium-owned scheduler/runner for Layer 2 — lean on the harness. +- **Risks:** verification not runnable (degrade loudly); context limits on large goals (decompose); cheating toward green (anti-cheat rule); runaway cost (caps); throwaway from deferred structural questions (classification, §7). From 3fc4538dc4680abe54260076cef94a86e1a1ab6a Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 17:14:02 +0300 Subject: [PATCH 03/20] Record loop-mode design decisions (ledger gitignore, delegation, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve open questions from review: - Ledger dotfile is git-ignored (auto-added to .gitignore on first use). - No auto-commit by default — leave the working tree to the user. - Orchestration delegates phases to skills/agents (not inlined) for DRY, agentic composition, and context isolation that keeps long runs in budget. - Auto-decompose large goals and run autonomously, with one consolidated summary at the end; blocking questions still escalate mid-run. - Caps are overridable defaults (per-invocation flags and config). Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/design/loop-mode.md b/design/loop-mode.md index c4d9e37..abe8a54 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -84,20 +84,22 @@ The ledger (§10) records which inner loop is active so an unattended resume (§ - The **loop ledger lives in a dotfile** (e.g. `.allium-loop/.json`): goal, mode, tick count, active inner loop, last verdicts, and parked (non-blocking) open questions. Blocking questions are escalated, not parked. - The ledger is what makes the same skill resumable unattended (§2, Layer 2): a fresh context reads it and continues idempotently. -- Open follow-on: should the dotfile be `.gitignore`-d by default? (See §12.1 — mirrors the install-notice marker debate.) +- The ledger dir `.allium-loop/` is **git-ignored** — loop mode appends it to `.gitignore` on first use. It is transient, per-developer, per-run state (like a build artefact), not shared source; same reasoning as the install-notice marker. ## 11. Output / reporting - Per-tick one-line state summary. - Final report: what converged, what was escalated, residual parked questions, test/weed status. -## 12. Open design questions +## 12. Decisions -1. **Ledger dotfile + `.gitignore`** — decided it lives in a dotfile (§10); still to settle whether loop mode offers to gitignore it. -2. **Auto-commit per converged tick**, or leave the working tree to the user? -3. **Relationship to the `tend`/`weed` agents** — does `loop` *call* them, or inline their procedures? -4. **Auto-decompose** large goals vs. surface a plan and ask. -5. **Configurable caps** — flags, a `config` block, or fixed defaults? +- **Ledger** — dotfile `.allium-loop/.json`, git-ignored (§10). +- **No auto-commit** — loop mode never commits on its own; it leaves the working tree for the user (who can always instruct the agent to commit). Committing is a user prerogative with too many valid styles to default. +- **Orchestration: delegate, don't inline** — loop *delegates* each phase to the existing skills/agents rather than re-implementing them. This is DRY (phase logic stays in one place), more agentic (an orchestrator over sub-agents — the trees-of-agents pattern), and — because `tend`/`weed` already run in their own context — delegation provides **context isolation**: the orchestrator holds only the ledger + convergence state while each phase runs in a clean context, which is what lets a long loop stay within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger. *Implication:* every phase should be spawnable as an isolated sub-agent — today only `tend`/`weed` are agents, so `elicit`/`distill`/`propagate` would need to be agent-invocable (or loop spawns a generic sub-agent that loads the relevant skill). +- **Auto-decompose, summarise at the end** — a goal too big for one tick is decomposed into sub-goals and run autonomously (no blocking to confirm a plan), with a single consolidated summary at the very end. Blocking open questions still escalate mid-run per §7. +- **Caps: defaults, overridable** — sensible fixed defaults (hard cap 6, no-progress 2; §9), overridable per-invocation (flags) and via a `config` block. + +**Still open:** decomposition heuristics (how to split a large goal and roll sub-goal results into the final summary); whether phases pass anything beyond the on-disk artefacts + ledger; `.gitignore` mechanics for repos that have none or unusual setups. ## 13. Out of scope / risks From 67198b93f2dca236e29a0e84f989fd20c3ca6a58 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 20:04:33 +0300 Subject: [PATCH 04/20] Record loop-mode design note lifecycle in its header Internal design note, not user-facing: retire when /allium:loop ships (content graduates into the skill + recommended-loops reference), and keep it out of the vendored plugin until then. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 1 + 1 file changed, 1 insertion(+) diff --git a/design/loop-mode.md b/design/loop-mode.md index abe8a54..764f92c 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -1,6 +1,7 @@ # Loop mode (`/allium:loop`) — design note > **Status: Proposed, not implemented.** Living document — update as the design evolves. +> **Lifecycle:** internal design note, not user-facing. To be retired when `/allium:loop` ships — its content graduates into the skill itself and the recommended-loops reference. Keep it out of the user-facing vendored plugin until then (split onto its own branch, or exclude `design/` from the marketplace sync, before any release that would otherwise ship it). > Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. ## 1. Purpose From fb41dfc44333820202c82961ce58f7ab0672abf3 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 20:09:56 +0300 Subject: [PATCH 05/20] Resolve loop-mode open questions (decomposition, interface, gitignore) - New "Decomposition & roll-up" section: split along Allium's structural seams (entity lifecycle / surface / rule chain), order by the data-flow graph, add a whole-spec integration pass so cross-slice seams converge, and roll sub-goal mini-reports into one final summary. - Decision: phases share state through artefacts + ledger only (derived data is regenerable/cached; sub-agent return values must be persisted). - Expand the ledger .gitignore mechanics for non-repos, monorepos, already-ignored, and read-only cases (best-effort, mention once). - "Still open" is now cleared pending the build. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/design/loop-mode.md b/design/loop-mode.md index 764f92c..74465f9 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -79,30 +79,41 @@ The ledger (§10) records which inner loop is active so an unattended resume (§ - **No-progress cap** — default **2** ticks with no measurable change (test pass count, weed verdict, open-question count). Catches thrashing against a test it can't satisfy. - **Escalate on blocking open question** (§7). - **Anti-cheat (non-negotiable)** — never edit a generated test to pass; a wrong-looking test means tend the spec + re-propagate, logged. -- Honour `config` (no magic numbers); decompose a too-big goal into sub-goals rather than blowing the context budget. +- Honour `config` (no magic numbers); decompose a too-big goal into sub-goals rather than blowing the context budget (§12). ## 10. State & resumability (ledger) - The **loop ledger lives in a dotfile** (e.g. `.allium-loop/.json`): goal, mode, tick count, active inner loop, last verdicts, and parked (non-blocking) open questions. Blocking questions are escalated, not parked. - The ledger is what makes the same skill resumable unattended (§2, Layer 2): a fresh context reads it and continues idempotently. -- The ledger dir `.allium-loop/` is **git-ignored** — loop mode appends it to `.gitignore` on first use. It is transient, per-developer, per-run state (like a build artefact), not shared source; same reasoning as the install-notice marker. +- The ledger dir `.allium-loop/` is **git-ignored** — transient, per-developer, per-run state (like a build artefact), not shared source; same reasoning as the install-notice marker. Mechanics: resolve the repo root with `git rev-parse --show-toplevel` (**not a git repo → skip**, don't create a `.gitignore`); ensure `.allium-loop/` is ignored at that root — create `.gitignore` if absent, append if missing, **no-op if already ignored** (check `git check-ignore` first so global excludes or existing entries aren't duplicated); in a monorepo or worktree use the nearest enclosing project root. **Best-effort**: if `.gitignore` can't be written, continue (the ledger still works locally) and say so. Mention the change once; don't prompt. ## 11. Output / reporting - Per-tick one-line state summary. - Final report: what converged, what was escalated, residual parked questions, test/weed status. -## 12. Decisions +## 12. Decomposition & roll-up + +A goal too big for one tick is split into sub-goals, each run as its own loop (with its own delegated, context-isolated phases per §13). Decomposition follows Allium's own structural seams rather than arbitrary task slicing: + +- **Unit** — one sub-goal per **entity lifecycle**, **surface**, or **independent rule / data-flow chain**. The spec defines the seams; `allium plan`/`analyse` enumerate obligations per construct. +- **Ordering** — topological by the spec's **data-flow / trigger graph**: producers before consumers (the same graph `propagate` uses for data-flow and cross-module tests). +- **When to split** — when the goal spans more than one independent behavioural slice. Judgment guided by the seams, not a magic obligation count. +- **Integration pass** — per-slice convergence is not enough: decomposing by entity splits cross-entity processes and data-flow chains. After the slices converge, run a **whole-spec integration pass** — the cross-entity / data-flow / reachability tests plus a full `weed` — so the seams *between* slices converge too. +- **Roll-up** — each sub-goal loop returns a structured mini-report (converged? tests, escalations, residual parked questions). The orchestrator aggregates into one final summary: per-slice status, totals, consolidated and deduped parked questions, overall convergence. The ledger records which sub-goals are done, so an unattended resume skips completed ones. + +## 13. Decisions - **Ledger** — dotfile `.allium-loop/.json`, git-ignored (§10). - **No auto-commit** — loop mode never commits on its own; it leaves the working tree for the user (who can always instruct the agent to commit). Committing is a user prerogative with too many valid styles to default. - **Orchestration: delegate, don't inline** — loop *delegates* each phase to the existing skills/agents rather than re-implementing them. This is DRY (phase logic stays in one place), more agentic (an orchestrator over sub-agents — the trees-of-agents pattern), and — because `tend`/`weed` already run in their own context — delegation provides **context isolation**: the orchestrator holds only the ledger + convergence state while each phase runs in a clean context, which is what lets a long loop stay within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger. *Implication:* every phase should be spawnable as an isolated sub-agent — today only `tend`/`weed` are agents, so `elicit`/`distill`/`propagate` would need to be agent-invocable (or loop spawns a generic sub-agent that loads the relevant skill). - **Auto-decompose, summarise at the end** — a goal too big for one tick is decomposed into sub-goals and run autonomously (no blocking to confirm a plan), with a single consolidated summary at the very end. Blocking open questions still escalate mid-run per §7. - **Caps: defaults, overridable** — sensible fixed defaults (hard cap 6, no-progress 2; §9), overridable per-invocation (flags) and via a `config` block. +- **Interface: artefacts + ledger only** — phases share state *exclusively* through on-disk artefacts (spec, tests, code) and the ledger; nothing essential passes only in memory. This is what guarantees context isolation and Layer-2 resumability — anything not reconstructable from disk would break an unattended restart. Derived data (`allium plan`/`model` JSON) is regenerable from the spec, so it is at most *cached* in `.allium-loop/`, never a separate source of truth. A sub-agent may *return* a structured result for immediate orchestration, but it must also be written to the ledger — the return value is a convenience, never the source of truth. -**Still open:** decomposition heuristics (how to split a large goal and roll sub-goal results into the final summary); whether phases pass anything beyond the on-disk artefacts + ledger; `.gitignore` mechanics for repos that have none or unusual setups. +**Still open:** none outstanding from review. Revisit when building the skill — likely the ledger JSON schema and exact decomposition thresholds. -## 13. Out of scope / risks +## 14. Out of scope / risks - **Out of scope (for the MVP):** building an Allium-owned scheduler/runner for Layer 2 — lean on the harness. - **Risks:** verification not runnable (degrade loudly); context limits on large goals (decompose); cheating toward green (anti-cheat rule); runaway cost (caps); throwaway from deferred structural questions (classification, §7). From 3d8393cd0e1ae3e9085d9a35823ab37d1136e5ef Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 20:13:59 +0300 Subject: [PATCH 06/20] Add /allium:loop skill (Layer 1 MVP) built against the design note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New skills/loop/SKILL.md drives the loop to convergence: entry-point detection, the per-tick procedure (gather → act → verify → route), convergence invariant, stop conditions (6 / 2 / escalate, anti-cheat), open-question park-vs-escalate, decomposition + integration pass, phase isolation, the git-ignored ledger, and real-verification guardrails. - Register ./skills/loop in .claude-plugin (codex picks it up via ./skills/). - Add loop to the router routing table + a pointer from the loop section. - Add loop to the test-skills skill registry. - Mark the design note status as MVP built. Validation: node scripts/test-skills.mjs -> 60 passed, 0 failed; generate-multi-editor.mjs --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/plugin.json | 1 + design/loop-mode.md | 2 +- scripts/test-skills.mjs | 2 +- skills/allium/SKILL.md | 3 +- skills/loop/SKILL.md | 85 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 skills/loop/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cc3b736..13c56f6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -14,6 +14,7 @@ "./skills/allium", "./skills/distill", "./skills/elicit", + "./skills/loop", "./skills/propagate", "./skills/tend", "./skills/weed" diff --git a/design/loop-mode.md b/design/loop-mode.md index 74465f9..28a2a11 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -1,6 +1,6 @@ # Loop mode (`/allium:loop`) — design note -> **Status: Proposed, not implemented.** Living document — update as the design evolves. +> **Status: MVP built** — implemented as the `loop` skill (`skills/loop/SKILL.md`); Layer 1 (in-session) only. Living document — update as the design evolves. > **Lifecycle:** internal design note, not user-facing. To be retired when `/allium:loop` ships — its content graduates into the skill itself and the recommended-loops reference. Keep it out of the user-facing vendored plugin until then (split onto its own branch, or exclude `design/` from the marketplace sync, before any release that would otherwise ship it). > Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index 4a21587..e8c5a00 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -132,7 +132,7 @@ function claudeQuery(prompt, { cwd } = {}) { } // Known paths -const skillNames = ["allium", "distill", "elicit", "propagate", "tend", "weed"]; +const skillNames = ["allium", "distill", "elicit", "loop", "propagate", "tend", "weed"]; const skillPaths = skillNames.map((n) => path.join(ROOT, "skills", n, "SKILL.md")); const agentPaths = ["tend", "weed"].map((n) => path.join(ROOT, "agents", `${n}.md`)); const vscodeAgentPaths = ["tend", "weed"].map((n) => path.join(ROOT, ".github", "agents", `${n}.agent.md`)); diff --git a/skills/allium/SKILL.md b/skills/allium/SKILL.md index a50dedb..bdb6e5f 100644 --- a/skills/allium/SKILL.md +++ b/skills/allium/SKILL.md @@ -33,10 +33,11 @@ Allium does NOT specify programming language or framework choices, database sche | Modifying an existing spec | `tend` skill | User wants targeted changes to `.allium` files | | Checking spec-to-code alignment | `weed` skill | User wants to find or fix divergences between spec and implementation | | Generating tests from a spec | `propagate` skill | User wants to generate tests, PBT properties or state machine tests from a specification | +| Driving the whole loop to convergence | `loop` skill | User wants to build or reconcile a feature end to end — run the gather→act→verify→repeat loop autonomously until spec, tests and code agree | ## The Allium loop (recommended sequencing) -The skills are not one-shot commands; they compose into an autonomous-style loop — **gather context → take action → verify → repeat** — that drives three artefacts to agreement: the **spec** (intent), the **tests** (contract), and the **code** (implementation). Gather context with `/elicit` or `/distill` (the spec is durable context); take action with `/propagate` then implementation (in spec-first work, confirm the new tests fail first — a test already green before you implement is already-covered or vacuous); verify by running the tests, then `/weed`, then CLI structural checks; repeat until converged. Verification is the phase that matters most, and the spec-plus-tests-plus-weed signal is what makes the loop trustworthy. After invoking one skill, proactively suggest the next step rather than waiting to be asked. +The skills are not one-shot commands; they compose into an autonomous-style loop — **gather context → take action → verify → repeat** — that drives three artefacts to agreement: the **spec** (intent), the **tests** (contract), and the **code** (implementation). Gather context with `/elicit` or `/distill` (the spec is durable context); take action with `/propagate` then implementation (in spec-first work, confirm the new tests fail first — a test already green before you implement is already-covered or vacuous); verify by running the tests, then `/weed`, then CLI structural checks; repeat until converged. Verification is the phase that matters most, and the spec-plus-tests-plus-weed signal is what makes the loop trustworthy. After invoking one skill, proactively suggest the next step rather than waiting to be asked. To run the whole loop to convergence in one go, use the `loop` skill (`/allium:loop `). Two entry points, one convergence loop: diff --git a/skills/loop/SKILL.md b/skills/loop/SKILL.md new file mode 100644 index 0000000..449ba07 --- /dev/null +++ b/skills/loop/SKILL.md @@ -0,0 +1,85 @@ +--- +name: loop +description: "Drive the Allium loop to convergence — gather context, take action, verify, repeat — running elicit or distill, propagate, implementation and weed as phases until the spec, tests and code agree. Use when the user wants to build or reconcile a feature end to end, run the spec-to-tests-to-code loop autonomously, or asks to loop on a goal." +--- + +# Loop + +Drive a goal to convergence by running the Allium loop yourself: **gather context → take action → verify → repeat**, until the spec, tests and code agree. You orchestrate; each phase is an existing skill (`elicit`, `distill`, `propagate`, `tend`, `weed`) plus ordinary implementation. What makes the loop trustworthy is the verification signal — you stop when behaviour is proven against intent, not when the code merely runs. + +For the conceptual model and worked walkthroughs, see [recommended loops](../allium/references/recommended-loops.md). + +## 1. Detect the entry point + +Pick the starting mode (confirm with the user only if genuinely ambiguous): + +- No spec for the area, behaviour described → **spec-first**: start with `elicit`. +- No spec, but existing code in the area → **code-first**: start with `distill`. +- Spec present, requirements changed → start with `tend`. +- Spec present, code may have drifted → start with `weed`. + +## 2. Run the loop (one tick) + +1. **Gather context** — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. +2. **Take action** — `propagate` to (re)generate tests when the spec changed, then implement. + - **Spec-first: confirm the new tests FAIL before implementing.** A generated test that is already green is already covered (reference it, don't duplicate) or vacuous (fix the spec or test). + - Never edit a generated test to make it pass. +3. **Verify** — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium check`/`analyse`. Parse the results; never narrate a pass you didn't execute. +4. **Route the outcome:** + - test fails → fix the code; + - a test is wrong → `tend` the spec, then `propagate` again; + - `weed` says the spec is wrong → `tend` the spec; + - open question → classify and handle (§5). +5. **Record state** in the ledger (§8) and print a one-line summary: `tick n · tests x/y · weed clean/dirty · openQ blocking k / parked m`. + +## 3. Convergence (when to stop) + +Stop when **all** hold: + +- tests pass, +- `weed` reports no divergence, +- no blocking open questions remain (only parked, non-blocking ones), +- (code-first) a fresh `distill` pass finds nothing new. + +## 4. Stop conditions & safety + +- **Hard cap** — stop after **6** iterations. +- **No-progress cap** — stop after **2** iterations with no change in tests / weed verdict / open-question count (catches thrashing against a test you can't satisfy). +- **Escalate** on a blocking open question (§5). +- **Anti-cheat (non-negotiable)** — never weaken or edit a generated test to pass; honour `config` (no magic numbers in code the spec parameterises). +- On hitting a cap or an unrecoverable error, **stop and report** — don't spin. + +Caps default to 6 / 2 and may be overridden per invocation or via a `config` block. + +## 5. Open questions: park or escalate + +Classify every question the loop surfaces: + +- **Blocking / direction-changing** — the answer reshapes the spec, and therefore the tests and code. → **Escalate to the user now**, before doing dependent work. Deferring these creates throwaway. +- **Non-blocking / peripheral** — doesn't affect what's already built or what's next. → **Park** it (spec `open questions` section + ledger) and continue; batch all parked questions into the final report. + +Rule: a question is *blocking* iff the next unit of work depends on its answer. Do everything independent of unresolved questions first. If you must proceed past a parked question, log the assumption and prefer cheap-to-revise work — never expensive or irreversible work that hangs off an unresolved structural question. + +## 6. Large goals: decompose, then integrate + +If the goal spans more than one independent behavioural slice, decompose along the spec's seams — one sub-goal per **entity lifecycle**, **surface**, or **independent rule / data-flow chain**. Order sub-goals topologically by the data-flow / trigger graph (producers before consumers). Run each sub-goal as its own loop. + +After the slices converge, run a **whole-spec integration pass** — cross-entity / data-flow / reachability tests plus a full `weed` — so the seams *between* slices converge too. Run this autonomously without blocking to confirm the plan, and produce one consolidated summary at the end. + +## 7. Run phases in isolation + +Where the harness allows, run each phase as an isolated sub-agent (`tend` and `weed` are already agents) so this orchestrator holds only the loop state and each phase gets a clean context — that is what keeps a long run within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger; do not rely on in-memory state surviving between phases. + +## 8. The ledger + +Keep loop state in `.allium-loop/.json`: goal, mode, tick count, active inner loop, last verdicts, completed sub-goals, and parked (non-blocking) open questions. This makes the loop resumable — a fresh run reads it and continues where it left off. + +Git-ignore it: resolve the repo root (`git rev-parse --show-toplevel`; skip if not a git repo), then ensure `.allium-loop/` is ignored there — create `.gitignore` if absent, append if missing, no-op if already ignored (`git check-ignore` first). Best-effort: if it can't be written, continue and say so. Mention it once; don't prompt. + +## 9. Verification must be real + +The loop is only as good as its verification. Discover the project's test command (framework, runner, test paths — reuse the `propagate` discovery checklist). If the `allium` CLI is not on PATH, run with the reduced signal you have and say so. If verification cannot actually run, **degrade loudly to assisted mode** — tell the user; never claim a pass you did not execute. + +## 10. Report + +End with: what converged, per–sub-goal status, tests and weed verdict, anything escalated, and all parked questions consolidated. From 5add819e2a7de497c907fc04865e3b7ef80ae5a0 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 20:27:42 +0300 Subject: [PATCH 07/20] Record the worker/orchestrator split for interactive phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit elicit and distill are human-in-the-loop, so each splits into the existing user-facing interactive skill plus an internal isolated worker the loop spawns. The worker runs in its own context and returns open questions; the orchestrator escalates them, writes answers to the spec/ledger, and re-spawns the worker. propagate stays a plain non-interactive worker. Workers are loop-internal by convention (not enforced — no per-caller ACL). Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 1 + 1 file changed, 1 insertion(+) diff --git a/design/loop-mode.md b/design/loop-mode.md index 28a2a11..713b694 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -107,6 +107,7 @@ A goal too big for one tick is split into sub-goals, each run as its own loop (w - **Ledger** — dotfile `.allium-loop/.json`, git-ignored (§10). - **No auto-commit** — loop mode never commits on its own; it leaves the working tree for the user (who can always instruct the agent to commit). Committing is a user prerogative with too many valid styles to default. - **Orchestration: delegate, don't inline** — loop *delegates* each phase to the existing skills/agents rather than re-implementing them. This is DRY (phase logic stays in one place), more agentic (an orchestrator over sub-agents — the trees-of-agents pattern), and — because `tend`/`weed` already run in their own context — delegation provides **context isolation**: the orchestrator holds only the ledger + convergence state while each phase runs in a clean context, which is what lets a long loop stay within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger. *Implication:* every phase should be spawnable as an isolated sub-agent — today only `tend`/`weed` are agents, so `elicit`/`distill`/`propagate` would need to be agent-invocable (or loop spawns a generic sub-agent that loads the relevant skill). +- **Interactive phases — worker/orchestrator split.** `propagate` is non-interactive and runs as a plain isolated worker. `elicit` and `distill` are human-in-the-loop (they need the user's judgment on intent), so split each into: (a) the existing **user-facing interactive skill** (unchanged, main context), and (b) an **internal isolated worker** the loop spawns. The worker does all autonomous work in its own context and **returns its open questions** instead of conversing; the **orchestrator** (main context) escalates the blocking ones (§7), writes the answers into the spec/ledger, and **re-spawns the worker** with a fresh context that reads the updated spec. The inner elicitation loop (§6) thus runs *orchestrator ↔ user* with a stateless worker re-spawned each round — preserving interactivity, keeping the context-isolation budget win, and fitting the artefacts+ledger-only interface. Workers are kept **loop-internal by convention** — not registered as user skills, absent from the routing table, no `/command` or auto-trigger, described as internal. *Caveat:* this is discoverability, not a permission boundary — the skill/agent model has no per-caller ACL, so a determined user could still invoke a worker; it is simply not surfaced. - **Auto-decompose, summarise at the end** — a goal too big for one tick is decomposed into sub-goals and run autonomously (no blocking to confirm a plan), with a single consolidated summary at the very end. Blocking open questions still escalate mid-run per §7. - **Caps: defaults, overridable** — sensible fixed defaults (hard cap 6, no-progress 2; §9), overridable per-invocation (flags) and via a `config` block. - **Interface: artefacts + ledger only** — phases share state *exclusively* through on-disk artefacts (spec, tests, code) and the ledger; nothing essential passes only in memory. This is what guarantees context isolation and Layer-2 resumability — anything not reconstructable from disk would break an unattended restart. Derived data (`allium plan`/`model` JSON) is regenerable from the spec, so it is at most *cached* in `.allium-loop/`, never a separate source of truth. A sub-agent may *return* a structured result for immediate orchestration, but it must also be written to the ledger — the return value is a convenience, never the source of truth. From 41b22ef74af5b36937c39a0e251d443c0129833b Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 21:54:04 +0300 Subject: [PATCH 08/20] Add consistency checks and broaden link coverage in test-skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New 'consistency' group: - version parity between .claude-plugin and .codex-plugin manifests - skill registration is identical across skills/ dirs, the test skillNames registry, and the .claude-plugin skills[] array (catches a skill that is built but never registered — would otherwise ship broken with tests green) Broadened the 'links' group to also check prose docs (README, all references/*.md, design notes) via a permissive resolver that catches bare relative paths, not just ./ and ../ links. All three verified to fail on mutation and pass after revert. Suite: 78 passed. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-skills.mjs | 83 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index e8c5a00..6f4da51 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -10,13 +10,13 @@ * node scripts/test-skills.mjs structure # run one group * node scripts/test-skills.mjs portability links # run multiple groups * - * Groups: structure, codex, portability, links, routing, generation, discovery, crosstalk + * Groups: structure, codex, consistency, portability, links, routing, generation, discovery, crosstalk * - * The first six groups are offline (free, fast). The last two require --live + * The first seven groups are offline (free, fast). The last two require --live * and make Claude API calls. */ -import { readFileSync, existsSync } from "fs"; +import { readFileSync, existsSync, readdirSync } from "fs"; import { execFileSync, execSync } from "child_process"; import path from "path"; @@ -102,6 +102,22 @@ function resolveRelativeLinks(body, fileDir) { })); } +// Broader link check for prose docs (README, references, design notes): +// any markdown link to a local path, skipping external URLs and pure anchors. +function resolveDocLinks(body, fileDir) { + const linkPattern = /\[[^\]]*\]\(([^)]+)\)/g; + const out = []; + let m; + while ((m = linkPattern.exec(body)) !== null) { + const raw = m[1].trim().split(/\s+/)[0]; // drop any "title" suffix + if (/^(https?:|mailto:|#)/.test(raw)) continue; // external or pure anchor + const noAnchor = raw.replace(/#.*$/, ""); + if (!noAnchor) continue; + out.push({ link: raw, exists: existsSync(path.resolve(fileDir, noAnchor)) }); + } + return out; +} + function claudeQuery(prompt, { cwd } = {}) { const output = execFileSync( getClaudePath(), @@ -361,6 +377,38 @@ if (shouldRun("portability")) { // Links — all relative markdown links resolve to real files // --------------------------------------------------------------------------- +if (shouldRun("consistency")) { + console.log("\n── consistency: manifests & registration ──\n"); + + const claudePluginPath = path.join(ROOT, ".claude-plugin", "plugin.json"); + const claude = readJson(claudePluginPath); + const codex = readJson(codexPluginPath); + + // Version parity across the two plugin manifests. + if (claude && codex && claude.version && claude.version === codex.version) { + pass(`version parity (${claude.version})`); + } else { + fail("version parity", `claude=${claude?.version} codex=${codex?.version}`); + } + + // Registration: skills/ dirs == test skillNames == .claude-plugin skills[]. + const skillsRoot = path.join(ROOT, "skills"); + const actualDirs = existsSync(skillsRoot) + ? readdirSync(skillsRoot).filter((d) => existsSync(path.join(skillsRoot, d, "SKILL.md"))) + : []; + const claudeArray = Array.isArray(claude?.skills) ? claude.skills.map((s) => path.basename(s)) : []; + const sortUniq = (a) => [...new Set(a)].sort(); + const dirs = sortUniq(actualDirs); + const named = sortUniq(skillNames); + const registered = sortUniq(claudeArray); + const eq = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]); + if (eq(dirs, named) && eq(dirs, registered)) { + pass(`skill registration consistent (${dirs.length} skills)`); + } else { + fail("skill registration", `dirs=[${dirs}] skillNames=[${named}] claude-plugin=[${registered}]`); + } +} + if (shouldRun("links")) { console.log("\n── links: relative link resolution ──\n"); @@ -378,6 +426,35 @@ if (shouldRun("links")) { pass(`${rel(filePath)} (${links.length} link${links.length !== 1 ? "s" : ""})`); } } + + // Prose docs (README + reference docs + design notes) — broader link check + // that also covers bare relative paths, not just ./ and ../ links. + const proseDocs = [path.join(ROOT, "README.md")]; + for (const n of skillNames) { + const refDir = path.join(ROOT, "skills", n, "references"); + if (existsSync(refDir)) { + for (const f of readdirSync(refDir)) { + if (f.endsWith(".md")) proseDocs.push(path.join(refDir, f)); + } + } + } + const designDir = path.join(ROOT, "design"); + if (existsSync(designDir)) { + for (const f of readdirSync(designDir)) { + if (f.endsWith(".md")) proseDocs.push(path.join(designDir, f)); + } + } + for (const filePath of proseDocs) { + if (!existsSync(filePath)) continue; + const links = resolveDocLinks(readFileSync(filePath, "utf-8"), path.dirname(filePath)); + const broken = links.filter((l) => !l.exists); + for (const { link } of broken) { + fail(`${rel(filePath)}`, `broken link: ${link}`); + } + if (broken.length === 0) { + pass(`${rel(filePath)} (${links.length} link${links.length !== 1 ? "s" : ""})`); + } + } } // --------------------------------------------------------------------------- From eec2cf20fa6fc45d90430854d8cb3112f8911972 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 22:44:20 +0300 Subject: [PATCH 09/20] Add loopdocs (constant drift) and hooks integrity checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loopdocs group: the loop caps (6 / 2) and the phase phrase "gather context → take action → verify → repeat" must stay consistent across the docs that restate them (loop skill, recommended-loops, router, design note) and README (verb form, checked in order). Canonical values live in the test (Option A). - hooks group: hooks/hooks.json is valid JSON, has a PostToolUse entry with a matcher, and its command points at a script that exists. Both verified to fail on mutation and pass after revert. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-skills.mjs | 102 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index 6f4da51..b28e3e0 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -10,9 +10,9 @@ * node scripts/test-skills.mjs structure # run one group * node scripts/test-skills.mjs portability links # run multiple groups * - * Groups: structure, codex, consistency, portability, links, routing, generation, discovery, crosstalk + * Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, discovery, crosstalk * - * The first seven groups are offline (free, fast). The last two require --live + * All groups except discovery and crosstalk are offline (free, fast); those two require --live * and make Claude API calls. */ @@ -506,6 +506,104 @@ if (shouldRun("generation")) { } } +// --------------------------------------------------------------------------- +// Loopdocs — the loop constants (caps + phase phrase) stay consistent across +// the docs that restate them. Canonical values live here in the test. +// --------------------------------------------------------------------------- + +if (shouldRun("loopdocs")) { + console.log("\n── loopdocs: loop constant drift ──\n"); + + const HARD_CAP = 6; + const NO_PROGRESS = 2; + const PHASE_PHRASE = "gather context → take action → verify → repeat"; + + // Files that state the numeric caps. + const capFiles = [ + "skills/loop/SKILL.md", + "skills/allium/references/recommended-loops.md", + "design/loop-mode.md", + ]; + for (const rp of capFiles) { + const fp = path.join(ROOT, rp); + if (!existsSync(fp)) continue; // design note may be absent post-release + const src = readFileSync(fp, "utf-8"); + const hard = src.match(/hard cap[^\n.]*?\b(\d+)\b/i); + const noProg = src.match(/no-progress[^\n.]*?\b(\d+)\b/i); + if (hard && Number(hard[1]) === HARD_CAP) pass(`${rp} hard cap = ${HARD_CAP}`); + else fail(`${rp} hard cap`, `expected ${HARD_CAP}, found ${hard ? hard[1] : "none"}`); + if (noProg && Number(noProg[1]) === NO_PROGRESS) pass(`${rp} no-progress cap = ${NO_PROGRESS}`); + else fail(`${rp} no-progress cap`, `expected ${NO_PROGRESS}, found ${noProg ? noProg[1] : "none"}`); + } + + // Files that state the phase phrase in arrow form. + const phaseFiles = [ + "skills/loop/SKILL.md", + "skills/allium/references/recommended-loops.md", + "skills/allium/SKILL.md", + "design/loop-mode.md", + ]; + for (const rp of phaseFiles) { + const fp = path.join(ROOT, rp); + if (!existsSync(fp)) continue; + if (readFileSync(fp, "utf-8").includes(PHASE_PHRASE)) pass(`${rp} phase phrase`); + else fail(`${rp} phase phrase`, `missing "${PHASE_PHRASE}"`); + } + + // README states the phases in verb form — check the four appear in order. + const readmePath = path.join(ROOT, "README.md"); + if (existsSync(readmePath)) { + const src = readFileSync(readmePath, "utf-8"); + const stems = [/gather/i, /take[s]? action/i, /verif/i, /repeat/i]; + const idx = stems.map((s) => src.search(s)); + if (idx.every((i) => i >= 0) && idx.every((v, i) => i === 0 || v > idx[i - 1])) { + pass("README.md phases in order"); + } else { + fail("README.md phases", `not all present and in order: ${idx}`); + } + } +} + +// --------------------------------------------------------------------------- +// Hooks — the PostToolUse hook config is valid and points at a real script. +// --------------------------------------------------------------------------- + +if (shouldRun("hooks")) { + console.log("\n── hooks: hook config integrity ──\n"); + + const hooksPath = path.join(ROOT, "hooks", "hooks.json"); + if (!existsSync(hooksPath)) { + fail("hooks/hooks.json", "not found"); + } else { + const cfg = readJson(hooksPath); + if (!cfg) { + fail("hooks/hooks.json", "invalid JSON"); + } else { + pass("hooks/hooks.json valid JSON"); + const post = cfg.hooks?.PostToolUse; + if (!Array.isArray(post) || post.length === 0) { + fail("hooks PostToolUse", "missing or empty"); + } else { + pass("hooks PostToolUse present"); + let matchersOk = true; + let scriptsOk = true; + for (const entry of post) { + if (!entry || !entry.matcher) matchersOk = false; + const cmds = Array.isArray(entry?.hooks) ? entry.hooks : []; + for (const h of cmds) { + const m = + typeof h.command === "string" && + h.command.match(/\$\{CLAUDE_PLUGIN_ROOT\}\/([^"\s]+)/); + if (m && !existsSync(path.join(ROOT, m[1]))) scriptsOk = false; + } + } + matchersOk ? pass("hooks have matchers") : fail("hooks matcher", "an entry is missing a matcher"); + scriptsOk ? pass("hook command scripts exist") : fail("hook command", "referenced script not found"); + } + } + } +} + // --------------------------------------------------------------------------- // Discovery — live Claude Code skill and agent loading // --------------------------------------------------------------------------- From a7e9b6672795b4fe939f09f362b8769f7dc3ca88 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 22:46:35 +0300 Subject: [PATCH 10/20] Doc review: list the loop skill in the README Adding the loop skill left the README stale: bump "five skills" to six and add a /loop row to the Skills and agents table so it appears in the human-facing menu, not just the routing table. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85b65e9..98ea619 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ See the [allium-tools repo](https://github.com/juxt/allium-tools) for details. ## Skills and agents -Allium provides five skills, an entry point and two autonomous agents. +Allium provides six skills, an entry point and two autonomous agents. | Skill | Purpose | |---|---| @@ -118,6 +118,7 @@ Allium provides five skills, an entry point and two autonomous agents. | `/propagate ` (or `/allium:propagate`) | Generate tests from a spec. | | `/tend ` (or `/allium:tend`) | Targeted changes to existing specs. | | `/weed ` (or `/allium:weed`) | Find and fix divergences between spec and code. | +| `/loop ` (or `/allium:loop`) | Drive the whole loop to convergence — gather context, take action, verify, repeat — running the other skills as phases. | How skills appear depends on your editor. Some show the fully qualified form (`/allium:weed`), others show the short form (`/weed`), and some support both. If one form isn't recognised, try the other. Skills also auto-trigger when you open or edit `.allium` files. From 489febdbfb21d26b7ea9af591866376cb11ddac1 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 22:53:28 +0300 Subject: [PATCH 11/20] Make /allium loop-first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The biggest adoption gap is that the full value needs orchestrating several skills, so people dabble in one or two. Bias the entry point toward the autonomous path: recommend driving the whole loop (/allium:loop) by default, direct-route clear single tasks, and only present the menu-with-hints when the request is bare or ambiguous — leading with the loop and offering the individual skills as the control path. - Router: new "Responding to /allium (loop-first)" section. - README: entry-point row and getting-started prose reflect loop-first. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- skills/allium/SKILL.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98ea619..16e96bd 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ npx skills add juxt/allium **Other editors:** If your editor doesn't read from `.agents/skills/`, symlink the installed skills into wherever it does look (e.g. `ln -s .agents/skills/allium .continue/rules/allium`, or `mklink /J` on Windows). Use a symlink rather than copying; the skill files contain relative links to reference material that a copy would break. -Once installed, type `/allium` to get started. Allium examines your project and guides you toward the right skill, whether that's distilling a spec from existing code or building one through conversation. Once you're familiar with the individual skills, you'll likely invoke them directly. +Once installed, type `/allium` to get started. Allium examines your project and points you at the best next move — usually driving the whole loop end to end (`/allium:loop`), or a single skill like distilling a spec from existing code or building one through conversation. Once you're familiar with the individual skills, you'll likely invoke them directly. Jump to what [Allium looks like in practice](#what-this-looks-like-in-practice). @@ -112,7 +112,7 @@ Allium provides six skills, an entry point and two autonomous agents. | Skill | Purpose | |---|---| -| `/allium ` | Entry point. Examines your project or the prompt and routes you to the right skill. | +| `/allium ` | Entry point. Examines your project and either drives the whole loop (`/allium:loop`) or routes you to the right single skill. | | `/elicit ` (or `/allium:elicit`) | Build a spec through structured conversation. | | `/distill ` (or `/allium:distill`) | Extract a spec from existing code. | | `/propagate ` (or `/allium:propagate`) | Generate tests from a spec. | diff --git a/skills/allium/SKILL.md b/skills/allium/SKILL.md index bdb6e5f..c5d6700 100644 --- a/skills/allium/SKILL.md +++ b/skills/allium/SKILL.md @@ -35,6 +35,18 @@ Allium does NOT specify programming language or framework choices, database sche | Generating tests from a spec | `propagate` skill | User wants to generate tests, PBT properties or state machine tests from a specification | | Driving the whole loop to convergence | `loop` skill | User wants to build or reconcile a feature end to end — run the gather→act→verify→repeat loop autonomously until spec, tests and code agree | +## Responding to `/allium` (loop-first) + +`/allium` is the entry point. Bias toward the autonomous path — the whole-loop value is exactly what occasional single-skill use misses: + +- **Clear single task** → route straight to that skill (per the routing table); don't make the user wade through a menu. +- **A goal or feature** (e.g. "add gift cards", "get password reset working") → recommend driving the whole loop end to end with `/allium:loop`, rather than running one phase. +- **Bare or ambiguous** → orient the user loop-first: recommend the loop as the default, then list the individual skills as the control path with a one-line hint each, and suggest a concrete starting point from the project state (existing `.allium` specs? code but no spec? drift to reconcile?). For example: + + > Tell me a goal and I'll drive the whole loop — spec → tests → code, until they agree (`/allium:loop`). Or run one step yourself: `elicit` (spec from intent), `distill` (spec from existing code), `propagate` (tests from a spec), `tend` (edit a spec), `weed` (fix spec↔code drift). You have code but no `.allium` yet, so I'd start by distilling — or just give me the goal and I'll take it end to end. + +Lead with the loop; keep the individual skills one step away for users who want manual control. And once a single skill finishes, proactively suggest the next phase rather than waiting to be asked. + ## The Allium loop (recommended sequencing) The skills are not one-shot commands; they compose into an autonomous-style loop — **gather context → take action → verify → repeat** — that drives three artefacts to agreement: the **spec** (intent), the **tests** (contract), and the **code** (implementation). Gather context with `/elicit` or `/distill` (the spec is durable context); take action with `/propagate` then implementation (in spec-first work, confirm the new tests fail first — a test already green before you implement is already-covered or vacuous); verify by running the tests, then `/weed`, then CLI structural checks; repeat until converged. Verification is the phase that matters most, and the spec-plus-tests-plus-weed signal is what makes the loop trustworthy. After invoking one skill, proactively suggest the next step rather than waiting to be asked. To run the whole loop to convergence in one go, use the `loop` skill (`/allium:loop `). From fb7b1a933d492abf16b32b10f3cc8b26e66a5055 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Tue, 23 Jun 2026 22:56:13 +0300 Subject: [PATCH 12/20] Bump plugin version to 3.7.0 Minor bump for loop mode: the /allium:loop skill and the loop-first entry point, plus the loop docs and test hardening. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 13c56f6..7cc273a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "allium", - "version": "3.6.0", + "version": "3.7.0", "description": "Velocity through clarity.", "author": { "name": "JUXT", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 45621d8..fab6067 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "allium", - "version": "3.6.0", + "version": "3.7.0", "description": "Velocity through clarity.", "author": { "name": "JUXT", From 67250bebe2d7d4eb6be72bcd710c186eba24f081 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Wed, 24 Jun 2026 08:46:35 +0300 Subject: [PATCH 13/20] Loop entry detection: announce-and-proceed with override (no confirm) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry path is detected from project state AND goal intent, announced in one line (naming the override), then run without a blocking confirmation — the user interrupts/redirects if it's wrong, or passes an explicit entry. Clarify that intent (not just file state) decides distill-vs-elicit and tend-vs-weed. The no-confirm applies to the entry path only; blocking open questions still pause and escalate. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 15 +++++++++------ skills/loop/SKILL.md | 17 +++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/design/loop-mode.md b/design/loop-mode.md index 713b694..aedefdf 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -21,14 +21,17 @@ The skill is designed so the identical procedure serves both: run once and it se - Optional: entry-point override (`from-intent` | `from-code`), iteration cap, target area/scope. - No goal → infer from context or ask one question. -## 4. Entry-point detection (decision tree) +## 4. Entry-point detection — announce, then proceed -Pick the starting mode; confirm with the user only if ambiguous: +Choose the starting mode from project state **and goal intent**, then **announce the chosen path in one line (naming the override) and proceed — no blocking confirmation.** The user can interrupt and redirect; an explicit entry argument (e.g. `/allium:loop distill `) skips detection. -- Spec for the area **absent**, behaviour described → **spec-first** (`elicit`). -- Spec absent, **existing code** in the area → **code-first** (`distill`). -- Spec **present**, requirements changed → **tend**, then continue. -- Spec present, code may have drifted → **weed-first** maintenance loop. +- No spec, no code → **spec-first** (`elicit`). +- No spec, code present, goal captures/verifies existing behaviour → **code-first** (`distill`). +- No spec, code present, goal adds **new** behaviour → **spec-first** (`elicit`) — distilling would capture what exists, not what you're adding. +- Spec present, goal **changes** behaviour → **tend**, then continue the loop. +- Spec present, code may have **drifted** → **weed**-first. + +State answers "spec? code?"; **intent** answers capture-vs-add and change-vs-reconcile — both matter, so read the goal, not just the file tree. The announce-and-proceed applies to the *entry path only*; genuine blocking open questions still pause and escalate (§7). ## 5. The tick (one iteration) diff --git a/skills/loop/SKILL.md b/skills/loop/SKILL.md index 449ba07..89a974a 100644 --- a/skills/loop/SKILL.md +++ b/skills/loop/SKILL.md @@ -9,14 +9,19 @@ Drive a goal to convergence by running the Allium loop yourself: **gather contex For the conceptual model and worked walkthroughs, see [recommended loops](../allium/references/recommended-loops.md). -## 1. Detect the entry point +## 1. Detect the entry point — announce, then proceed -Pick the starting mode (confirm with the user only if genuinely ambiguous): +Choose the starting mode from the project state **and the goal's intent**, then **announce the chosen path in one line, name the override, and proceed — do not wait for confirmation.** The user can interrupt and redirect if it's wrong; the entry choice is not a gate. -- No spec for the area, behaviour described → **spec-first**: start with `elicit`. -- No spec, but existing code in the area → **code-first**: start with `distill`. -- Spec present, requirements changed → start with `tend`. -- Spec present, code may have drifted → start with `weed`. +- No spec and no code → **spec-first**: start with `elicit`. +- No spec, code exists, goal captures/verifies existing behaviour → **code-first**: start with `distill`. +- No spec, code exists, goal adds **new** behaviour → **spec-first**: `elicit` the new behaviour (don't distill — distilling captures what's there, not what you're adding). +- Spec exists, goal **changes** behaviour → start with `tend`. +- Spec exists, code may have **drifted** from it → start with `weed`. + +State answers "is there a spec / code?"; the **goal's intent** answers capture-vs-add (`distill` vs `elicit`) and change-vs-reconcile (`tend` vs `weed`) — so read the goal, not just the file tree. If the user gives an explicit entry (`/allium:loop distill `, or just "tend the spec"), use it and skip detection. + +Announce like: *"No spec here, code present, goal reads as new behaviour → starting with elicit. (Say 'distill' or 'tend' to switch.)"* This announce-and-proceed applies to the **entry path only** — genuine blocking open questions still pause and escalate (§5). ## 2. Run the loop (one tick) From 80be547dcaf436f4db7d6775dfea91c61019255a Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Wed, 24 Jun 2026 08:52:31 +0300 Subject: [PATCH 14/20] Loop: add phase-boundary narration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Announce each phase as it begins with a one-line marker (→ Gather / → Act / → Verify) so a multi-tick run stays legible, while leaving command-level detail to the harness. Keeps the per-tick summary and final report. Mirrored in the design note reporting section. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode.md | 1 + skills/loop/SKILL.md | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/design/loop-mode.md b/design/loop-mode.md index aedefdf..6f18a15 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -92,6 +92,7 @@ The ledger (§10) records which inner loop is active so an unattended resume (§ ## 11. Output / reporting +- Per-phase one-line marker as each phase begins (e.g. `→ Verify: tests → weed → allium check`) so the run stays legible; the harness shows the underlying commands, so don't narrate every command — just the phase boundaries. - Per-tick one-line state summary. - Final report: what converged, what was escalated, residual parked questions, test/weed status. diff --git a/skills/loop/SKILL.md b/skills/loop/SKILL.md index 89a974a..d6edddd 100644 --- a/skills/loop/SKILL.md +++ b/skills/loop/SKILL.md @@ -25,11 +25,13 @@ Announce like: *"No spec here, code present, goal reads as new behaviour → sta ## 2. Run the loop (one tick) -1. **Gather context** — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. -2. **Take action** — `propagate` to (re)generate tests when the spec changed, then implement. +Announce each phase as it begins with a one-line marker (shown in parentheses below) so the run stays legible across ticks. Let the harness show the underlying commands — don't narrate every command, just the phase boundaries. + +1. **Gather context** *(`→ Gather: elicit/distill/tend the spec`)* — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. +2. **Take action** *(`→ Act: propagate tests, then implement`)* — `propagate` to (re)generate tests when the spec changed, then implement. - **Spec-first: confirm the new tests FAIL before implementing.** A generated test that is already green is already covered (reference it, don't duplicate) or vacuous (fix the spec or test). - Never edit a generated test to make it pass. -3. **Verify** — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium check`/`analyse`. Parse the results; never narrate a pass you didn't execute. +3. **Verify** *(`→ Verify: run tests → weed → allium check`)* — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium check`/`analyse`. Parse the results; never narrate a pass you didn't execute. 4. **Route the outcome:** - test fails → fix the code; - a test is wrong → `tend` the spec, then `propagate` again; From 619fe756ba450e612705ff3b32e01b6839e89f46 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Wed, 24 Jun 2026 09:04:24 +0300 Subject: [PATCH 15/20] Loop: align CLI checking with Allium's continuous-validation guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the Allium guidance, .allium specs are checked by allium check on every edit (hook/LSP), not as a late step. Make the loop faithful: the Gather phase notes the spec is CLI-checked on edit and must be valid before propagating (tests are generated from it); Verify focuses on the behavioural checks — tests, weed (spec↔code drift), and allium analyse (semantic gaps) — rather than re-running the structural check. Co-Authored-By: Claude Opus 4.8 (1M context) --- skills/loop/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/loop/SKILL.md b/skills/loop/SKILL.md index d6edddd..f6b242d 100644 --- a/skills/loop/SKILL.md +++ b/skills/loop/SKILL.md @@ -27,11 +27,11 @@ Announce like: *"No spec here, code present, goal reads as new behaviour → sta Announce each phase as it begins with a one-line marker (shown in parentheses below) so the run stays legible across ticks. Let the harness show the underlying commands — don't narrate every command, just the phase boundaries. -1. **Gather context** *(`→ Gather: elicit/distill/tend the spec`)* — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. +1. **Gather context** *(`→ Gather: elicit/distill/tend the spec`)* — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. The spec is CLI-checked on every edit (the hook / LSP run `allium check`); **resolve any reported issues before propagating** — tests are generated from the spec, so it must be valid first. 2. **Take action** *(`→ Act: propagate tests, then implement`)* — `propagate` to (re)generate tests when the spec changed, then implement. - **Spec-first: confirm the new tests FAIL before implementing.** A generated test that is already green is already covered (reference it, don't duplicate) or vacuous (fix the spec or test). - Never edit a generated test to make it pass. -3. **Verify** *(`→ Verify: run tests → weed → allium check`)* — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium check`/`analyse`. Parse the results; never narrate a pass you didn't execute. +3. **Verify** *(`→ Verify: run tests → weed → allium analyse`)* — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium analyse` for semantic gaps (dead ends, unreachable states). The spec's *structure* is already validated on every edit (§2.1), so verify adds the behavioural checks: tests, drift, and semantic analysis. Parse the results; never narrate a pass you didn't execute. 4. **Route the outcome:** - test fails → fix the code; - a test is wrong → `tend` the spec, then `propagate` again; From 7a1fff14722656f8ab5f7a30a80a75bf7099948f Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Wed, 24 Jun 2026 18:03:19 +0300 Subject: [PATCH 16/20] Loop: add a reviewer recipe for running /allium:loop locally Document how to load the plugin from a local PR checkout via --plugin-dir (no marketplace needed, live working-tree edits) and drive /allium:loop against a throwaway target project. Includes a spec-first and a bounded track, what to watch for, targeted feedback prompts, and a known-good reference spec. Linked from the design note. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode-local-testing.md | 258 ++++++++++++++++++++++++++++++ design/loop-mode.md | 1 + 2 files changed, 259 insertions(+) create mode 100644 design/loop-mode-local-testing.md diff --git a/design/loop-mode-local-testing.md b/design/loop-mode-local-testing.md new file mode 100644 index 0000000..31d94f0 --- /dev/null +++ b/design/loop-mode-local-testing.md @@ -0,0 +1,258 @@ +# Trying loop mode locally (for PR reviewers) + +> Companion to the [loop-mode design note](./loop-mode.md). This is a reviewer +> recipe: run `/allium:loop` from **this PR branch** against a throwaway project, +> watch it drive a goal to convergence, and feed back. Like the rest of `design/` +> it is internal and not shipped to end users. + +`/allium:loop` is a plugin skill, so it only exists once Claude Code loads the +plugin. This repo is the plugin's **source**, not an installed plugin — and it +contains nothing to loop on. So testing has two parts: **load the plugin from +your checkout**, then **point it at a target project**. + +--- + +## Prerequisites + +- **Claude Code ≥ 2.1** (needs the `--plugin-dir` flag — `claude --version`). +- **The `allium` CLI** (strongly recommended). The loop's verification signal + leans on it (`allium check` gates propagation; `allium plan` derives test + obligations; `allium analyse` finds semantic gaps). Without it the loop + **degrades to assisted mode** with a weaker signal — fine for a smoke test, + but you won't see the CLI-driven phases. + ``` + brew tap juxt/allium && brew install allium + ``` +- `allium-lsp` is optional (live diagnostics); without it checking falls back to + CLI `allium check`, which is all the loop needs. + +--- + +## 1. Load the plugin from this branch + +`--plugin-dir` loads the plugin **directly from the working tree** — no +marketplace, no cache, nothing persisted. Point it at your checkout, but launch +Claude Code from inside your *target* project: + +```bash +# in one terminal: check out the PR +git -C /path/to/allium-pr-review checkout loop-mode + +# then start Claude Code in a SCRATCH project, loading the plugin from the checkout +mkdir -p /tmp/loop-demo && cd /tmp/loop-demo +claude --plugin-dir /path/to/allium-pr-review +``` + +Inside the session, `/allium:loop` (and `allium:distill`, `tend`, `weed`, …) +are now available. Quick non-interactive sanity check: + +```bash +claude --plugin-dir /path/to/allium-pr-review \ + -p "List the skills starting with 'allium', one per line." +# → allium:allium, allium:distill, allium:elicit, allium:loop, allium:propagate, allium:tend, allium:weed +``` + +**Editing the skill while testing?** `--plugin-dir` reads live from the working +tree — run `/reload-plugins` to pick up your edits in the same session. + +> Don't run the loop *inside* `allium-pr-review` itself — it's the plugin source, +> there's no spec or product code to loop on. Always use a separate target dir. + +--- + +## 2. Give it something to loop on + +Pick one track. Both run inside the scratch project from step 1. + +### Track A — spec-first, from scratch (most representative) + +An empty dir + a new-behaviour goal exercises the whole loop (entry detection → +elicit → propagate → implement → verify): + +``` +/allium:loop build a coffee loyalty stamp card: a stamp per purchase; every +10th stamp issues a reward that expires 30 days after it's earned +``` + +Elicit is interactive — expect it to ask you a couple of edge-case questions +(reopen a closed card? claim flow? expiry cascade?) before it propagates. + +### Track B — bounded run from a ready spec + +For a shorter, more deterministic pass, drop the reference spec from the +[appendix](#appendix-reference-spec) into the dir as `loyalty.allium` and run: + +``` +/allium:loop propagate tests and an implementation for loyalty.allium, then +reconcile until tests, spec and code agree +``` + +--- + +## 3. What to watch for (so feedback is targeted) + +The loop should make each of these observable — flag any that's missing or wrong: + +- **Entry detection (announce-and-proceed).** One line naming the chosen path + *and* the override, then it proceeds **without** waiting for confirmation. +- **Phase markers.** `→ Gather` / `→ Act` / `→ Verify` boundaries, not a wall of + narrated commands. +- **Spec checked before propagating.** `allium check` runs on every spec edit; + errors/warnings resolved before tests are generated. +- **Fail-first (anti-cheat).** Newly generated tests are shown **RED before** + implementation; a generated test is **never** edited to make it pass; `config` + values aren't hard-coded as magic numbers in the code. +- **Verify is real.** Tests actually run; `weed` runs for drift; `allium analyse` + runs for semantic gaps — and an analyse/weed finding **routes back into a + `tend`**, not a shrug. +- **Caps & ledger.** Hard cap **6** ticks; **no-progress** cap of **2** identical + ticks; resumable ledger under `.allium-loop/.json` (git-ignored). +- **Convergence report** at the end: what converged, tests/weed verdict, escalated + vs parked open questions. + +--- + +## 4. Feedback worth gathering + +- Did **entry detection** pick the right starting mode for your goal? +- Were the **caps** (6 / 2) hit appropriately, or did it stop too early / spin? +- Did **escalation vs parking** of open questions feel right (§7 of the design note)? +- Two spots the author already flagged as possibly under-specified: + - §2.1 says "resolve any reported issues before propagating," but `allium check` + emits **info-level** notes and **by-design advisories** (e.g. the external-entity + reminder) that arguably shouldn't block. Does the skill draw that line clearly? + - The **no-progress** signal keys on tests / weed / open-question count — it does + **not** include `allium analyse` finding count. A loop that only moves the + analyse needle could trip the cap despite genuinely converging. + +File feedback on the PR; quote the tick summary line where relevant. + +--- + +## Cleanup + +`--plugin-dir` persists nothing, so there's nothing to uninstall. Remove the +scratch dir (`rm -rf /tmp/loop-demo`); `.allium-loop/` is git-ignored if you +looped inside a repo. + +--- + +## Appendix: reference spec + +A known-good converged spec for Track B. Driving the loop manually against this +(spec-first) reached convergence in **2 ticks** — tests 13/13, `weed` clean, +`allium analyse` 0 findings (one by-design external-entity advisory on `Barista` +remains). Tick 1 implemented the rules; tick 2 added the surfaces after `analyse` +flagged the triggers as having no boundary. + +```allium +-- allium: 3 +-- Coffee loyalty card: a stamp per purchase; every N stamps issues a +-- reward that expires after a fixed validity window. + +------------------------------------------------------------ +-- External Entities +------------------------------------------------------------ + +external entity Barista {} + +------------------------------------------------------------ +-- Config +------------------------------------------------------------ + +config { + stamps_per_reward: Integer = 10 + reward_validity: Duration = 30.days +} + +------------------------------------------------------------ +-- Entities and Variants +------------------------------------------------------------ + +entity LoyaltyCard { + holder: String + stamps: Integer + status: active | closed +} + +entity Reward { + card: LoyaltyCard + issued_at: Timestamp + expires_at: Timestamp + status: unclaimed | expired +} + +------------------------------------------------------------ +-- Rules +------------------------------------------------------------ + +rule IssueCard { + when: IssuesCard(staff, holder) + ensures: LoyaltyCard.created(holder: holder, stamps: 0, status: active) +} + +rule CloseCard { + when: ClosesCard(staff, card) + requires: card.status = active + ensures: card.status = closed +} + +rule RecordPurchase { + when: RecordsPurchase(staff, card) + requires: card.status = active + + let new_count = card.stamps + 1 + + ensures: + if new_count >= config.stamps_per_reward: + card.stamps = new_count - config.stamps_per_reward + Reward.created( + card: card, + issued_at: now, + expires_at: now + config.reward_validity, + status: unclaimed + ) + if new_count < config.stamps_per_reward: + card.stamps = new_count +} + +rule RewardExpires { + when: reward: Reward.expires_at <= now + requires: reward.status = unclaimed + ensures: reward.status = expired +} + +------------------------------------------------------------ +-- Invariants +------------------------------------------------------------ + +invariant StampsWithinCycle { + for card in LoyaltyCards: + card.stamps >= 0 and card.stamps < config.stamps_per_reward +} + +------------------------------------------------------------ +-- Surfaces +------------------------------------------------------------ + +surface CardRegistration { + facing staff: Barista + + provides: + IssuesCard(staff, holder) +} + +surface CardActions { + facing staff: Barista + + context card: LoyaltyCard where status = active + + exposes: + card.holder + card.stamps + + provides: + RecordsPurchase(staff, card) + ClosesCard(staff, card) +} +``` diff --git a/design/loop-mode.md b/design/loop-mode.md index 6f18a15..47fc786 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -3,6 +3,7 @@ > **Status: MVP built** — implemented as the `loop` skill (`skills/loop/SKILL.md`); Layer 1 (in-session) only. Living document — update as the design evolves. > **Lifecycle:** internal design note, not user-facing. To be retired when `/allium:loop` ships — its content graduates into the skill itself and the recommended-loops reference. Keep it out of the user-facing vendored plugin until then (split onto its own branch, or exclude `design/` from the marketplace sync, before any release that would otherwise ship it). > Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. +> **Reviewers:** to run `/allium:loop` from this branch on your own machine, follow [Trying loop mode locally](./loop-mode-local-testing.md). ## 1. Purpose From 0008af1609d8d2e3c22469c04235ec9acb67a9fd Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Mon, 29 Jun 2026 17:31:34 +0300 Subject: [PATCH 17/20] Loop: expose /allium-loop as a GitHub Copilot prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop is a skill, so unlike tend/weed it had no editor variant — Copilot users couldn't run it. Generate .github/prompts/allium-loop.prompt.md from skills/loop/SKILL.md so VS Code Copilot Chat (Agent mode) surfaces it as /allium-loop. Two adaptations beyond adaptBody(), since Copilot lacks two mechanisms the skill assumes: - no edit hook / LSP, so the implicit "checked on every edit" becomes an explicit "run allium check yourself" instruction; - reference links are absolutised so the prompt stays valid when copied out of this repo into a target project. Generated, not hand-authored, and covered by test-skills.mjs (structure + portability + the existing up-to-date guard), so it can't drift from the skill. Other tools (Claude Code, Codex, Cursor, …) read the unmodified skill and are unaffected. Docs updated: a Copilot reviewer section in the local testing recipe and a /allium-loop pointer in the README. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/prompts/allium-loop.prompt.md | 92 +++++++++++++++++++++++++++ README.md | 2 +- design/loop-mode-local-testing.md | 51 +++++++++++++++ scripts/generate-multi-editor.mjs | 41 ++++++++++++ scripts/test-skills.mjs | 29 +++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 .github/prompts/allium-loop.prompt.md diff --git a/.github/prompts/allium-loop.prompt.md b/.github/prompts/allium-loop.prompt.md new file mode 100644 index 0000000..df605f5 --- /dev/null +++ b/.github/prompts/allium-loop.prompt.md @@ -0,0 +1,92 @@ +--- +description: "Drive the Allium loop to convergence — gather context, take action, verify, repeat — running elicit or distill, propagate, implementation and weed as phases until the spec, tests and code agree. Use when the user wants to build or reconcile a feature end to end, run the spec-to-tests-to-code loop autonomously, or asks to loop on a goal." +mode: agent +--- + +# Loop + +Drive a goal to convergence by running the Allium loop yourself: **gather context → take action → verify → repeat**, until the spec, tests and code agree. You orchestrate; each phase is an existing skill (`elicit`, `distill`, `propagate`, `tend`, `weed`) plus ordinary implementation. What makes the loop trustworthy is the verification signal — you stop when behaviour is proven against intent, not when the code merely runs. + +For the conceptual model and worked walkthroughs, see [recommended loops](https://github.com/juxt/allium/blob/main/skills/allium/references/recommended-loops.md). + +## 1. Detect the entry point — announce, then proceed + +Choose the starting mode from the project state **and the goal's intent**, then **announce the chosen path in one line, name the override, and proceed — do not wait for confirmation.** The user can interrupt and redirect if it's wrong; the entry choice is not a gate. + +- No spec and no code → **spec-first**: start with `elicit`. +- No spec, code exists, goal captures/verifies existing behaviour → **code-first**: start with `distill`. +- No spec, code exists, goal adds **new** behaviour → **spec-first**: `elicit` the new behaviour (don't distill — distilling captures what's there, not what you're adding). +- Spec exists, goal **changes** behaviour → start with `tend`. +- Spec exists, code may have **drifted** from it → start with `weed`. + +State answers "is there a spec / code?"; the **goal's intent** answers capture-vs-add (`distill` vs `elicit`) and change-vs-reconcile (`tend` vs `weed`) — so read the goal, not just the file tree. If the user gives an explicit entry (`/allium:loop distill `, or just "tend the spec"), use it and skip detection. + +Announce like: *"No spec here, code present, goal reads as new behaviour → starting with elicit. (Say 'distill' or 'tend' to switch.)"* This announce-and-proceed applies to the **entry path only** — genuine blocking open questions still pause and escalate (§5). + +## 2. Run the loop (one tick) + +Announce each phase as it begins with a one-line marker (shown in parentheses below) so the run stays legible across ticks. Let the harness show the underlying commands — don't narrate every command, just the phase boundaries. + +1. **Gather context** *(`→ Gather: elicit/distill/tend the spec`)* — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. Run `allium check` yourself after every spec edit; **resolve any reported issues before propagating** — tests are generated from the spec, so it must be valid first. +2. **Take action** *(`→ Act: propagate tests, then implement`)* — `propagate` to (re)generate tests when the spec changed, then implement. + - **Spec-first: confirm the new tests FAIL before implementing.** A generated test that is already green is already covered (reference it, don't duplicate) or vacuous (fix the spec or test). + - Never edit a generated test to make it pass. +3. **Verify** *(`→ Verify: run tests → weed → allium analyse`)* — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium analyse` for semantic gaps (dead ends, unreachable states). The spec's *structure* is already validated on every edit (§2.1), so verify adds the behavioural checks: tests, drift, and semantic analysis. Parse the results; never narrate a pass you didn't execute. +4. **Route the outcome:** + - test fails → fix the code; + - a test is wrong → `tend` the spec, then `propagate` again; + - `weed` says the spec is wrong → `tend` the spec; + - open question → classify and handle (§5). +5. **Record state** in the ledger (§8) and print a one-line summary: `tick n · tests x/y · weed clean/dirty · openQ blocking k / parked m`. + +## 3. Convergence (when to stop) + +Stop when **all** hold: + +- tests pass, +- `weed` reports no divergence, +- no blocking open questions remain (only parked, non-blocking ones), +- (code-first) a fresh `distill` pass finds nothing new. + +## 4. Stop conditions & safety + +- **Hard cap** — stop after **6** iterations. +- **No-progress cap** — stop after **2** iterations with no change in tests / weed verdict / open-question count (catches thrashing against a test you can't satisfy). +- **Escalate** on a blocking open question (§5). +- **Anti-cheat (non-negotiable)** — never weaken or edit a generated test to pass; honour `config` (no magic numbers in code the spec parameterises). +- On hitting a cap or an unrecoverable error, **stop and report** — don't spin. + +Caps default to 6 / 2 and may be overridden per invocation or via a `config` block. + +## 5. Open questions: park or escalate + +Classify every question the loop surfaces: + +- **Blocking / direction-changing** — the answer reshapes the spec, and therefore the tests and code. → **Escalate to the user now**, before doing dependent work. Deferring these creates throwaway. +- **Non-blocking / peripheral** — doesn't affect what's already built or what's next. → **Park** it (spec `open questions` section + ledger) and continue; batch all parked questions into the final report. + +Rule: a question is *blocking* iff the next unit of work depends on its answer. Do everything independent of unresolved questions first. If you must proceed past a parked question, log the assumption and prefer cheap-to-revise work — never expensive or irreversible work that hangs off an unresolved structural question. + +## 6. Large goals: decompose, then integrate + +If the goal spans more than one independent behavioural slice, decompose along the spec's seams — one sub-goal per **entity lifecycle**, **surface**, or **independent rule / data-flow chain**. Order sub-goals topologically by the data-flow / trigger graph (producers before consumers). Run each sub-goal as its own loop. + +After the slices converge, run a **whole-spec integration pass** — cross-entity / data-flow / reachability tests plus a full `weed` — so the seams *between* slices converge too. Run this autonomously without blocking to confirm the plan, and produce one consolidated summary at the end. + +## 7. Run phases in isolation + +Where the harness allows, run each phase as an isolated sub-agent (`tend` and `weed` are already agents) so this orchestrator holds only the loop state and each phase gets a clean context — that is what keeps a long run within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger; do not rely on in-memory state surviving between phases. + +## 8. The ledger + +Keep loop state in `.allium-loop/.json`: goal, mode, tick count, active inner loop, last verdicts, completed sub-goals, and parked (non-blocking) open questions. This makes the loop resumable — a fresh run reads it and continues where it left off. + +Git-ignore it: resolve the repo root (`git rev-parse --show-toplevel`; skip if not a git repo), then ensure `.allium-loop/` is ignored there — create `.gitignore` if absent, append if missing, no-op if already ignored (`git check-ignore` first). Best-effort: if it can't be written, continue and say so. Mention it once; don't prompt. + +## 9. Verification must be real + +The loop is only as good as its verification. Discover the project's test command (framework, runner, test paths — reuse the `propagate` discovery checklist). If the `allium` CLI is not on PATH, run with the reduced signal you have and say so. If verification cannot actually run, **degrade loudly to assisted mode** — tell the user; never claim a pass you did not execute. + +## 10. Report + +End with: what converged, per–sub-goal status, tests and weed verdict, anything escalated, and all parked questions consolidated. diff --git a/README.md b/README.md index 16e96bd..295ca8c 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ codex plugin add allium@juxt-plugins npx skills add juxt/allium ``` -**GitHub Copilot** reads skills and agents from the repository automatically. No installation needed. +**GitHub Copilot** reads skills and agents from the repository automatically. No installation needed. The end-to-end loop is also wired as a Copilot prompt — invoke it in VS Code Copilot Chat (Agent mode) with `/allium-loop `. **Other editors:** If your editor doesn't read from `.agents/skills/`, symlink the installed skills into wherever it does look (e.g. `ln -s .agents/skills/allium .continue/rules/allium`, or `mklink /J` on Windows). Use a symlink rather than copying; the skill files contain relative links to reference material that a copy would break. diff --git a/design/loop-mode-local-testing.md b/design/loop-mode-local-testing.md index 31d94f0..0620339 100644 --- a/design/loop-mode-local-testing.md +++ b/design/loop-mode-local-testing.md @@ -60,6 +60,57 @@ tree — run `/reload-plugins` to pick up your edits in the same session. --- +## 1b. Or: try it in GitHub Copilot + +Copilot has no `--plugin-dir`. It reads agents and prompts from `.github/` in the +**workspace that's open in VS Code**, so there's nothing to "load" — you drop the +loop's prompt file into your target project and invoke it. The loop ships as a +prompt (not an agent) because it's interactive — it asks elicitation questions and +escalates open questions — and prompt files run in the main Chat thread where that +Q&A is natural. It's generated from `skills/loop/SKILL.md`, so it stays in lockstep +with the canonical skill. + +```bash +# from your branch checkout, copy the one generated file into your TARGET project: +mkdir -p /path/to/target/.github/prompts +cp /path/to/allium-pr-review/.github/prompts/allium-loop.prompt.md \ + /path/to/target/.github/prompts/ + +# optional: copy the tend/weed agents too, so the loop's delegation steps +# (§7) have sub-agents to hand off to +mkdir -p /path/to/target/.github/agents +cp /path/to/allium-pr-review/.github/agents/{tend,weed}.agent.md \ + /path/to/target/.github/agents/ +``` + +Then open the target project in VS Code, switch Copilot Chat to **Agent mode** +(the loop edits files and runs the test / `allium` commands — Ask/Edit modes +can't), and run: + +``` +/allium-loop build a coffee loyalty stamp card: a stamp per purchase; every +10th stamp issues a reward that expires 30 days after it's earned +``` + +The command name is the filename minus `.prompt.md` — `/allium-loop`, **not** +`/allium:loop` (that colon form is Claude Code plugin syntax). Everything after it +is the goal. + +Two differences from the Claude path worth knowing while reviewing: + +- **No auto-check.** Claude Code's hook/LSP checks the spec on every edit; Copilot + has neither, so the prompt instructs the model to **run `allium check` itself** + after each spec edit. You should see explicit `allium check` calls rather than + silent inline diagnostics. (Install the [CLI](#prerequisites) for this; without + it the loop degrades to assisted mode exactly as under Claude Code.) +- **No live reload.** `--plugin-dir` reads the working tree live; here, if you edit + `skills/loop/SKILL.md`, regenerate (`node scripts/generate-multi-editor.mjs`) and + re-copy the prompt file into the target project. + +> Same rule as above: run it in a *target* project, not in `allium-pr-review`. + +--- + ## 2. Give it something to loop on Pick one track. Both run inside the scratch project from step 1. diff --git a/scripts/generate-multi-editor.mjs b/scripts/generate-multi-editor.mjs index 0665b75..1bfa518 100644 --- a/scripts/generate-multi-editor.mjs +++ b/scripts/generate-multi-editor.mjs @@ -98,6 +98,40 @@ ${adapted}`; return agent; } +// --------------------------------------------------------------------------- +// Copilot / VS Code prompt generation (the loop orchestrator) +// --------------------------------------------------------------------------- +// +// The loop is a skill, not an agent, so it has no agents/ source — the Copilot +// variant is projected directly from skills/loop/SKILL.md into a VS Code prompt +// file (invokable as /allium-loop). Two adaptations beyond adaptBody(), because +// Copilot lacks two Claude Code mechanisms the skill assumes: +// 1. No edit hook / LSP, so the spec is not auto-checked — the implicit +// "checked on every edit" becomes an explicit "run allium check yourself". +// 2. Reference links are absolutised so the prompt stays valid when copied out +// of this repo into a target project (its relative ../ paths would break). + +function adaptLoopBody(body) { + return adaptBody(body) + .replace( + "The spec is CLI-checked on every edit (the hook / LSP run `allium check`); **resolve any reported issues before propagating**", + "Run `allium check` yourself after every spec edit; **resolve any reported issues before propagating**" + ) + .replace( + "[recommended loops](../allium/references/recommended-loops.md)", + "[recommended loops](https://github.com/juxt/allium/blob/main/skills/allium/references/recommended-loops.md)" + ); +} + +function generateCopilotPrompt() { + const { frontmatter, body } = parseFrontmatter(read("skills/loop/SKILL.md")); + return `--- +description: "${frontmatter.description}" +mode: agent +--- +${adaptLoopBody(body)}`; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -113,6 +147,13 @@ for (const name of AGENTS) { } } +if (write(".github/prompts/allium-loop.prompt.md", generateCopilotPrompt())) { + console.log( + `${CHECK ? "out of date" : "wrote"}: .github/prompts/allium-loop.prompt.md` + ); + dirty = true; +} + if (CHECK && dirty) { console.error( "\nGenerated files are out of date. Run: node scripts/generate-multi-editor.mjs" diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index b28e3e0..f5bc36c 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -152,6 +152,7 @@ const skillNames = ["allium", "distill", "elicit", "loop", "propagate", "tend", const skillPaths = skillNames.map((n) => path.join(ROOT, "skills", n, "SKILL.md")); const agentPaths = ["tend", "weed"].map((n) => path.join(ROOT, "agents", `${n}.md`)); const vscodeAgentPaths = ["tend", "weed"].map((n) => path.join(ROOT, ".github", "agents", `${n}.agent.md`)); +const vscodePromptPath = path.join(ROOT, ".github", "prompts", "allium-loop.prompt.md"); const codexPluginPath = path.join(ROOT, ".codex-plugin", "plugin.json"); const portableSkillNames = ["tend", "weed"]; @@ -238,6 +239,33 @@ if (shouldRun("structure")) { pass(`${label} naming`); } } + + console.log(""); + + // VS Code / Copilot prompt (the loop orchestrator, projected from the skill) + { + const label = rel(vscodePromptPath); + if (!existsSync(vscodePromptPath)) { + fail(label, "file not found"); + } else { + const parsed = parseFrontmatter(readFileSync(vscodePromptPath, "utf-8")); + if (!parsed) { + fail(label, "no valid frontmatter"); + } else { + const { frontmatter } = parsed; + if (!frontmatter.description) fail(label, "missing 'description'"); + else pass(label); + // A prompt drives interactive orchestration, so it must run in agent mode. + if (frontmatter.mode !== "agent") fail(`${label} mode`, "must be mode: agent"); + else pass(`${label} mode`); + } + if (!path.basename(vscodePromptPath).endsWith(".prompt.md")) { + fail(`${label} naming`, "must end with .prompt.md"); + } else { + pass(`${label} naming`); + } + } + } } // --------------------------------------------------------------------------- @@ -358,6 +386,7 @@ if (shouldRun("portability")) { const portableArtifacts = [ ...portableSkillNames.map((n) => path.join(ROOT, "skills", n, "SKILL.md")), ...vscodeAgentPaths, + vscodePromptPath, ]; for (const filePath of portableArtifacts) { if (!existsSync(filePath)) continue; From faa5ed4c415d41b9eefb87dcf0c914d05ccfc817 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Fri, 3 Jul 2026 13:37:10 +0300 Subject: [PATCH 18/20] Loop: fold /allium:loop into /allium The loop is no longer a separate command. The loop skill merges into the allium entry-point skill, so /allium drives the whole loop to convergence; its procedure moves to skills/allium/references/driving-the-loop.md. Removes the standalone loop skill, its plugin.json entry, and the redundant /allium-loop Copilot prompt (plus its generation and tests). Repoints the loopdocs drift checks, updates the README command table, and adds superseded banners to the internal design notes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/plugin.json | 1 - .github/prompts/allium-loop.prompt.md | 92 ------------------- README.md | 9 +- design/loop-mode-local-testing.md | 2 + design/loop-mode.md | 4 +- scripts/generate-multi-editor.mjs | 41 --------- scripts/test-skills.mjs | 33 +------ skills/allium/SKILL.md | 11 ++- .../references/driving-the-loop.md} | 11 +-- 9 files changed, 21 insertions(+), 183 deletions(-) delete mode 100644 .github/prompts/allium-loop.prompt.md rename skills/{loop/SKILL.md => allium/references/driving-the-loop.md} (92%) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7cc273a..9d4eb98 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -14,7 +14,6 @@ "./skills/allium", "./skills/distill", "./skills/elicit", - "./skills/loop", "./skills/propagate", "./skills/tend", "./skills/weed" diff --git a/.github/prompts/allium-loop.prompt.md b/.github/prompts/allium-loop.prompt.md deleted file mode 100644 index df605f5..0000000 --- a/.github/prompts/allium-loop.prompt.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -description: "Drive the Allium loop to convergence — gather context, take action, verify, repeat — running elicit or distill, propagate, implementation and weed as phases until the spec, tests and code agree. Use when the user wants to build or reconcile a feature end to end, run the spec-to-tests-to-code loop autonomously, or asks to loop on a goal." -mode: agent ---- - -# Loop - -Drive a goal to convergence by running the Allium loop yourself: **gather context → take action → verify → repeat**, until the spec, tests and code agree. You orchestrate; each phase is an existing skill (`elicit`, `distill`, `propagate`, `tend`, `weed`) plus ordinary implementation. What makes the loop trustworthy is the verification signal — you stop when behaviour is proven against intent, not when the code merely runs. - -For the conceptual model and worked walkthroughs, see [recommended loops](https://github.com/juxt/allium/blob/main/skills/allium/references/recommended-loops.md). - -## 1. Detect the entry point — announce, then proceed - -Choose the starting mode from the project state **and the goal's intent**, then **announce the chosen path in one line, name the override, and proceed — do not wait for confirmation.** The user can interrupt and redirect if it's wrong; the entry choice is not a gate. - -- No spec and no code → **spec-first**: start with `elicit`. -- No spec, code exists, goal captures/verifies existing behaviour → **code-first**: start with `distill`. -- No spec, code exists, goal adds **new** behaviour → **spec-first**: `elicit` the new behaviour (don't distill — distilling captures what's there, not what you're adding). -- Spec exists, goal **changes** behaviour → start with `tend`. -- Spec exists, code may have **drifted** from it → start with `weed`. - -State answers "is there a spec / code?"; the **goal's intent** answers capture-vs-add (`distill` vs `elicit`) and change-vs-reconcile (`tend` vs `weed`) — so read the goal, not just the file tree. If the user gives an explicit entry (`/allium:loop distill `, or just "tend the spec"), use it and skip detection. - -Announce like: *"No spec here, code present, goal reads as new behaviour → starting with elicit. (Say 'distill' or 'tend' to switch.)"* This announce-and-proceed applies to the **entry path only** — genuine blocking open questions still pause and escalate (§5). - -## 2. Run the loop (one tick) - -Announce each phase as it begins with a one-line marker (shown in parentheses below) so the run stays legible across ticks. Let the harness show the underlying commands — don't narrate every command, just the phase boundaries. - -1. **Gather context** *(`→ Gather: elicit/distill/tend the spec`)* — run the entry skill (or `tend`) only if the spec needs to change this tick. Treat elicitation as an *inner loop*: keep asking the user questions until the spec covers the edge cases, then continue. Distillation may take several passes. Run `allium check` yourself after every spec edit; **resolve any reported issues before propagating** — tests are generated from the spec, so it must be valid first. -2. **Take action** *(`→ Act: propagate tests, then implement`)* — `propagate` to (re)generate tests when the spec changed, then implement. - - **Spec-first: confirm the new tests FAIL before implementing.** A generated test that is already green is already covered (reference it, don't duplicate) or vacuous (fix the spec or test). - - Never edit a generated test to make it pass. -3. **Verify** *(`→ Verify: run tests → weed → allium analyse`)* — actually run it: the project's test command, then `weed` for spec↔code alignment, then `allium analyse` for semantic gaps (dead ends, unreachable states). The spec's *structure* is already validated on every edit (§2.1), so verify adds the behavioural checks: tests, drift, and semantic analysis. Parse the results; never narrate a pass you didn't execute. -4. **Route the outcome:** - - test fails → fix the code; - - a test is wrong → `tend` the spec, then `propagate` again; - - `weed` says the spec is wrong → `tend` the spec; - - open question → classify and handle (§5). -5. **Record state** in the ledger (§8) and print a one-line summary: `tick n · tests x/y · weed clean/dirty · openQ blocking k / parked m`. - -## 3. Convergence (when to stop) - -Stop when **all** hold: - -- tests pass, -- `weed` reports no divergence, -- no blocking open questions remain (only parked, non-blocking ones), -- (code-first) a fresh `distill` pass finds nothing new. - -## 4. Stop conditions & safety - -- **Hard cap** — stop after **6** iterations. -- **No-progress cap** — stop after **2** iterations with no change in tests / weed verdict / open-question count (catches thrashing against a test you can't satisfy). -- **Escalate** on a blocking open question (§5). -- **Anti-cheat (non-negotiable)** — never weaken or edit a generated test to pass; honour `config` (no magic numbers in code the spec parameterises). -- On hitting a cap or an unrecoverable error, **stop and report** — don't spin. - -Caps default to 6 / 2 and may be overridden per invocation or via a `config` block. - -## 5. Open questions: park or escalate - -Classify every question the loop surfaces: - -- **Blocking / direction-changing** — the answer reshapes the spec, and therefore the tests and code. → **Escalate to the user now**, before doing dependent work. Deferring these creates throwaway. -- **Non-blocking / peripheral** — doesn't affect what's already built or what's next. → **Park** it (spec `open questions` section + ledger) and continue; batch all parked questions into the final report. - -Rule: a question is *blocking* iff the next unit of work depends on its answer. Do everything independent of unresolved questions first. If you must proceed past a parked question, log the assumption and prefer cheap-to-revise work — never expensive or irreversible work that hangs off an unresolved structural question. - -## 6. Large goals: decompose, then integrate - -If the goal spans more than one independent behavioural slice, decompose along the spec's seams — one sub-goal per **entity lifecycle**, **surface**, or **independent rule / data-flow chain**. Order sub-goals topologically by the data-flow / trigger graph (producers before consumers). Run each sub-goal as its own loop. - -After the slices converge, run a **whole-spec integration pass** — cross-entity / data-flow / reachability tests plus a full `weed` — so the seams *between* slices converge too. Run this autonomously without blocking to confirm the plan, and produce one consolidated summary at the end. - -## 7. Run phases in isolation - -Where the harness allows, run each phase as an isolated sub-agent (`tend` and `weed` are already agents) so this orchestrator holds only the loop state and each phase gets a clean context — that is what keeps a long run within budget. The shared interface between phases is the on-disk artefacts (spec, tests, code) plus the ledger; do not rely on in-memory state surviving between phases. - -## 8. The ledger - -Keep loop state in `.allium-loop/.json`: goal, mode, tick count, active inner loop, last verdicts, completed sub-goals, and parked (non-blocking) open questions. This makes the loop resumable — a fresh run reads it and continues where it left off. - -Git-ignore it: resolve the repo root (`git rev-parse --show-toplevel`; skip if not a git repo), then ensure `.allium-loop/` is ignored there — create `.gitignore` if absent, append if missing, no-op if already ignored (`git check-ignore` first). Best-effort: if it can't be written, continue and say so. Mention it once; don't prompt. - -## 9. Verification must be real - -The loop is only as good as its verification. Discover the project's test command (framework, runner, test paths — reuse the `propagate` discovery checklist). If the `allium` CLI is not on PATH, run with the reduced signal you have and say so. If verification cannot actually run, **degrade loudly to assisted mode** — tell the user; never claim a pass you did not execute. - -## 10. Report - -End with: what converged, per–sub-goal status, tests and weed verdict, anything escalated, and all parked questions consolidated. diff --git a/README.md b/README.md index 295ca8c..f02c5a3 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ codex plugin add allium@juxt-plugins npx skills add juxt/allium ``` -**GitHub Copilot** reads skills and agents from the repository automatically. No installation needed. The end-to-end loop is also wired as a Copilot prompt — invoke it in VS Code Copilot Chat (Agent mode) with `/allium-loop `. +**GitHub Copilot** reads skills and agents from the repository automatically. No installation needed. **Other editors:** If your editor doesn't read from `.agents/skills/`, symlink the installed skills into wherever it does look (e.g. `ln -s .agents/skills/allium .continue/rules/allium`, or `mklink /J` on Windows). Use a symlink rather than copying; the skill files contain relative links to reference material that a copy would break. -Once installed, type `/allium` to get started. Allium examines your project and points you at the best next move — usually driving the whole loop end to end (`/allium:loop`), or a single skill like distilling a spec from existing code or building one through conversation. Once you're familiar with the individual skills, you'll likely invoke them directly. +Once installed, type `/allium` to get started. Allium examines your project and points you at the best next move — usually driving the whole loop end to end, or a single skill like distilling a spec from existing code or building one through conversation. Once you're familiar with the individual skills, you'll likely invoke them directly. Jump to what [Allium looks like in practice](#what-this-looks-like-in-practice). @@ -108,17 +108,16 @@ See the [allium-tools repo](https://github.com/juxt/allium-tools) for details. ## Skills and agents -Allium provides six skills, an entry point and two autonomous agents. +Allium provides five skills, an entry point and two autonomous agents. | Skill | Purpose | |---|---| -| `/allium ` | Entry point. Examines your project and either drives the whole loop (`/allium:loop`) or routes you to the right single skill. | +| `/allium ` | Entry point. Give it a goal and it drives the whole loop to convergence — gather context, take action, verify, repeat — running the other skills as phases; give it a single task and it routes you to the right skill. | | `/elicit ` (or `/allium:elicit`) | Build a spec through structured conversation. | | `/distill ` (or `/allium:distill`) | Extract a spec from existing code. | | `/propagate ` (or `/allium:propagate`) | Generate tests from a spec. | | `/tend ` (or `/allium:tend`) | Targeted changes to existing specs. | | `/weed ` (or `/allium:weed`) | Find and fix divergences between spec and code. | -| `/loop ` (or `/allium:loop`) | Drive the whole loop to convergence — gather context, take action, verify, repeat — running the other skills as phases. | How skills appear depends on your editor. Some show the fully qualified form (`/allium:weed`), others show the short form (`/weed`), and some support both. If one form isn't recognised, try the other. Skills also auto-trigger when you open or edit `.allium` files. diff --git a/design/loop-mode-local-testing.md b/design/loop-mode-local-testing.md index 0620339..f658251 100644 --- a/design/loop-mode-local-testing.md +++ b/design/loop-mode-local-testing.md @@ -1,5 +1,7 @@ # Trying loop mode locally (for PR reviewers) +> **Superseded (2026-07):** the loop folded into the `allium` entry-point skill, so it now runs as `/allium ` (not `/allium:loop`), and the `/allium-loop` Copilot prompt was removed. The specific commands, filenames and generation steps below predate that rename and are retained only as a historical record — substitute `/allium` for `/allium:loop` when running it. +> > Companion to the [loop-mode design note](./loop-mode.md). This is a reviewer > recipe: run `/allium:loop` from **this PR branch** against a throwaway project, > watch it drive a goal to convergence, and feed back. Like the rest of `design/` diff --git a/design/loop-mode.md b/design/loop-mode.md index 47fc786..31b6377 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -1,5 +1,7 @@ -# Loop mode (`/allium:loop`) — design note +# Loop mode (`/allium`) — design note +> **Superseded (2026-07):** the loop is no longer a separate `/allium:loop` command. The `loop` skill was folded into the `allium` entry-point skill, so `/allium ` drives the loop directly; its procedure now lives in [`skills/allium/references/driving-the-loop.md`](../skills/allium/references/driving-the-loop.md). The standalone `loop` skill and the `/allium-loop` Copilot prompt were removed. The design below is kept as a historical record — command names and file paths in it predate the rename. +> > **Status: MVP built** — implemented as the `loop` skill (`skills/loop/SKILL.md`); Layer 1 (in-session) only. Living document — update as the design evolves. > **Lifecycle:** internal design note, not user-facing. To be retired when `/allium:loop` ships — its content graduates into the skill itself and the recommended-loops reference. Keep it out of the user-facing vendored plugin until then (split onto its own branch, or exclude `design/` from the marketplace sync, before any release that would otherwise ship it). > Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. diff --git a/scripts/generate-multi-editor.mjs b/scripts/generate-multi-editor.mjs index 1bfa518..0665b75 100644 --- a/scripts/generate-multi-editor.mjs +++ b/scripts/generate-multi-editor.mjs @@ -98,40 +98,6 @@ ${adapted}`; return agent; } -// --------------------------------------------------------------------------- -// Copilot / VS Code prompt generation (the loop orchestrator) -// --------------------------------------------------------------------------- -// -// The loop is a skill, not an agent, so it has no agents/ source — the Copilot -// variant is projected directly from skills/loop/SKILL.md into a VS Code prompt -// file (invokable as /allium-loop). Two adaptations beyond adaptBody(), because -// Copilot lacks two Claude Code mechanisms the skill assumes: -// 1. No edit hook / LSP, so the spec is not auto-checked — the implicit -// "checked on every edit" becomes an explicit "run allium check yourself". -// 2. Reference links are absolutised so the prompt stays valid when copied out -// of this repo into a target project (its relative ../ paths would break). - -function adaptLoopBody(body) { - return adaptBody(body) - .replace( - "The spec is CLI-checked on every edit (the hook / LSP run `allium check`); **resolve any reported issues before propagating**", - "Run `allium check` yourself after every spec edit; **resolve any reported issues before propagating**" - ) - .replace( - "[recommended loops](../allium/references/recommended-loops.md)", - "[recommended loops](https://github.com/juxt/allium/blob/main/skills/allium/references/recommended-loops.md)" - ); -} - -function generateCopilotPrompt() { - const { frontmatter, body } = parseFrontmatter(read("skills/loop/SKILL.md")); - return `--- -description: "${frontmatter.description}" -mode: agent ---- -${adaptLoopBody(body)}`; -} - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -147,13 +113,6 @@ for (const name of AGENTS) { } } -if (write(".github/prompts/allium-loop.prompt.md", generateCopilotPrompt())) { - console.log( - `${CHECK ? "out of date" : "wrote"}: .github/prompts/allium-loop.prompt.md` - ); - dirty = true; -} - if (CHECK && dirty) { console.error( "\nGenerated files are out of date. Run: node scripts/generate-multi-editor.mjs" diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index f5bc36c..1d01aa9 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -148,11 +148,10 @@ function claudeQuery(prompt, { cwd } = {}) { } // Known paths -const skillNames = ["allium", "distill", "elicit", "loop", "propagate", "tend", "weed"]; +const skillNames = ["allium", "distill", "elicit", "propagate", "tend", "weed"]; const skillPaths = skillNames.map((n) => path.join(ROOT, "skills", n, "SKILL.md")); const agentPaths = ["tend", "weed"].map((n) => path.join(ROOT, "agents", `${n}.md`)); const vscodeAgentPaths = ["tend", "weed"].map((n) => path.join(ROOT, ".github", "agents", `${n}.agent.md`)); -const vscodePromptPath = path.join(ROOT, ".github", "prompts", "allium-loop.prompt.md"); const codexPluginPath = path.join(ROOT, ".codex-plugin", "plugin.json"); const portableSkillNames = ["tend", "weed"]; @@ -241,31 +240,6 @@ if (shouldRun("structure")) { } console.log(""); - - // VS Code / Copilot prompt (the loop orchestrator, projected from the skill) - { - const label = rel(vscodePromptPath); - if (!existsSync(vscodePromptPath)) { - fail(label, "file not found"); - } else { - const parsed = parseFrontmatter(readFileSync(vscodePromptPath, "utf-8")); - if (!parsed) { - fail(label, "no valid frontmatter"); - } else { - const { frontmatter } = parsed; - if (!frontmatter.description) fail(label, "missing 'description'"); - else pass(label); - // A prompt drives interactive orchestration, so it must run in agent mode. - if (frontmatter.mode !== "agent") fail(`${label} mode`, "must be mode: agent"); - else pass(`${label} mode`); - } - if (!path.basename(vscodePromptPath).endsWith(".prompt.md")) { - fail(`${label} naming`, "must end with .prompt.md"); - } else { - pass(`${label} naming`); - } - } - } } // --------------------------------------------------------------------------- @@ -386,7 +360,6 @@ if (shouldRun("portability")) { const portableArtifacts = [ ...portableSkillNames.map((n) => path.join(ROOT, "skills", n, "SKILL.md")), ...vscodeAgentPaths, - vscodePromptPath, ]; for (const filePath of portableArtifacts) { if (!existsSync(filePath)) continue; @@ -549,7 +522,7 @@ if (shouldRun("loopdocs")) { // Files that state the numeric caps. const capFiles = [ - "skills/loop/SKILL.md", + "skills/allium/references/driving-the-loop.md", "skills/allium/references/recommended-loops.md", "design/loop-mode.md", ]; @@ -567,7 +540,7 @@ if (shouldRun("loopdocs")) { // Files that state the phase phrase in arrow form. const phaseFiles = [ - "skills/loop/SKILL.md", + "skills/allium/references/driving-the-loop.md", "skills/allium/references/recommended-loops.md", "skills/allium/SKILL.md", "design/loop-mode.md", diff --git a/skills/allium/SKILL.md b/skills/allium/SKILL.md index c5d6700..eb98f11 100644 --- a/skills/allium/SKILL.md +++ b/skills/allium/SKILL.md @@ -33,23 +33,23 @@ Allium does NOT specify programming language or framework choices, database sche | Modifying an existing spec | `tend` skill | User wants targeted changes to `.allium` files | | Checking spec-to-code alignment | `weed` skill | User wants to find or fix divergences between spec and implementation | | Generating tests from a spec | `propagate` skill | User wants to generate tests, PBT properties or state machine tests from a specification | -| Driving the whole loop to convergence | `loop` skill | User wants to build or reconcile a feature end to end — run the gather→act→verify→repeat loop autonomously until spec, tests and code agree | +| Driving the whole loop to convergence | this skill (see [driving the loop](./references/driving-the-loop.md)) | User wants to build or reconcile a feature end to end — `/allium ` runs the gather→act→verify→repeat loop autonomously until spec, tests and code agree | ## Responding to `/allium` (loop-first) `/allium` is the entry point. Bias toward the autonomous path — the whole-loop value is exactly what occasional single-skill use misses: - **Clear single task** → route straight to that skill (per the routing table); don't make the user wade through a menu. -- **A goal or feature** (e.g. "add gift cards", "get password reset working") → recommend driving the whole loop end to end with `/allium:loop`, rather than running one phase. -- **Bare or ambiguous** → orient the user loop-first: recommend the loop as the default, then list the individual skills as the control path with a one-line hint each, and suggest a concrete starting point from the project state (existing `.allium` specs? code but no spec? drift to reconcile?). For example: +- **A goal or feature** (e.g. "add gift cards", "get password reset working") → drive the whole loop end to end yourself, rather than running one phase. Follow [driving the loop](./references/driving-the-loop.md). +- **Bare or ambiguous** → orient the user loop-first: offer to drive the loop as the default, then list the individual skills as the control path with a one-line hint each, and suggest a concrete starting point from the project state (existing `.allium` specs? code but no spec? drift to reconcile?). For example: - > Tell me a goal and I'll drive the whole loop — spec → tests → code, until they agree (`/allium:loop`). Or run one step yourself: `elicit` (spec from intent), `distill` (spec from existing code), `propagate` (tests from a spec), `tend` (edit a spec), `weed` (fix spec↔code drift). You have code but no `.allium` yet, so I'd start by distilling — or just give me the goal and I'll take it end to end. + > Tell me a goal and I'll drive the whole loop — spec → tests → code, until they agree. Or run one step yourself: `elicit` (spec from intent), `distill` (spec from existing code), `propagate` (tests from a spec), `tend` (edit a spec), `weed` (fix spec↔code drift). You have code but no `.allium` yet, so I'd start by distilling — or just give me the goal and I'll take it end to end. Lead with the loop; keep the individual skills one step away for users who want manual control. And once a single skill finishes, proactively suggest the next phase rather than waiting to be asked. ## The Allium loop (recommended sequencing) -The skills are not one-shot commands; they compose into an autonomous-style loop — **gather context → take action → verify → repeat** — that drives three artefacts to agreement: the **spec** (intent), the **tests** (contract), and the **code** (implementation). Gather context with `/elicit` or `/distill` (the spec is durable context); take action with `/propagate` then implementation (in spec-first work, confirm the new tests fail first — a test already green before you implement is already-covered or vacuous); verify by running the tests, then `/weed`, then CLI structural checks; repeat until converged. Verification is the phase that matters most, and the spec-plus-tests-plus-weed signal is what makes the loop trustworthy. After invoking one skill, proactively suggest the next step rather than waiting to be asked. To run the whole loop to convergence in one go, use the `loop` skill (`/allium:loop `). +The skills are not one-shot commands; they compose into an autonomous-style loop — **gather context → take action → verify → repeat** — that drives three artefacts to agreement: the **spec** (intent), the **tests** (contract), and the **code** (implementation). Gather context with `/elicit` or `/distill` (the spec is durable context); take action with `/propagate` then implementation (in spec-first work, confirm the new tests fail first — a test already green before you implement is already-covered or vacuous); verify by running the tests, then `/weed`, then CLI structural checks; repeat until converged. Verification is the phase that matters most, and the spec-plus-tests-plus-weed signal is what makes the loop trustworthy. After invoking one skill, proactively suggest the next step rather than waiting to be asked. To run the whole loop to convergence in one go, just give `/allium` a goal — it drives the loop for you, following [driving the loop](./references/driving-the-loop.md). Two entry points, one convergence loop: @@ -335,4 +335,5 @@ When the `allium` CLI is installed, a hook validates `.allium` files automatical - [Language reference](./references/language-reference.md) — full syntax for entities, rules, expressions, surfaces, contracts, invariants and validation - [Test generation](./references/test-generation.md) — generating tests from specifications - [Recommended loops](./references/recommended-loops.md) — the gather-context → take-action → verify → repeat loop, with spec-first and code-first walkthroughs +- [Driving the loop](./references/driving-the-loop.md) — the procedure `/allium` follows to drive a goal to convergence (entry detection, the tick, stop conditions, the ledger) - [Patterns](./references/patterns.md) — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract diff --git a/skills/loop/SKILL.md b/skills/allium/references/driving-the-loop.md similarity index 92% rename from skills/loop/SKILL.md rename to skills/allium/references/driving-the-loop.md index f6b242d..bd55ed1 100644 --- a/skills/loop/SKILL.md +++ b/skills/allium/references/driving-the-loop.md @@ -1,14 +1,9 @@ ---- -name: loop -description: "Drive the Allium loop to convergence — gather context, take action, verify, repeat — running elicit or distill, propagate, implementation and weed as phases until the spec, tests and code agree. Use when the user wants to build or reconcile a feature end to end, run the spec-to-tests-to-code loop autonomously, or asks to loop on a goal." ---- +# Driving the loop -# Loop +This reference is the procedure `/allium` follows when you hand it a goal: it drives the Allium loop to convergence on your behalf. For the conceptual model and worked walkthroughs, see [recommended loops](./recommended-loops.md). Drive a goal to convergence by running the Allium loop yourself: **gather context → take action → verify → repeat**, until the spec, tests and code agree. You orchestrate; each phase is an existing skill (`elicit`, `distill`, `propagate`, `tend`, `weed`) plus ordinary implementation. What makes the loop trustworthy is the verification signal — you stop when behaviour is proven against intent, not when the code merely runs. -For the conceptual model and worked walkthroughs, see [recommended loops](../allium/references/recommended-loops.md). - ## 1. Detect the entry point — announce, then proceed Choose the starting mode from the project state **and the goal's intent**, then **announce the chosen path in one line, name the override, and proceed — do not wait for confirmation.** The user can interrupt and redirect if it's wrong; the entry choice is not a gate. @@ -19,7 +14,7 @@ Choose the starting mode from the project state **and the goal's intent**, then - Spec exists, goal **changes** behaviour → start with `tend`. - Spec exists, code may have **drifted** from it → start with `weed`. -State answers "is there a spec / code?"; the **goal's intent** answers capture-vs-add (`distill` vs `elicit`) and change-vs-reconcile (`tend` vs `weed`) — so read the goal, not just the file tree. If the user gives an explicit entry (`/allium:loop distill `, or just "tend the spec"), use it and skip detection. +State answers "is there a spec / code?"; the **goal's intent** answers capture-vs-add (`distill` vs `elicit`) and change-vs-reconcile (`tend` vs `weed`) — so read the goal, not just the file tree. If the user gives an explicit entry (`/allium distill `, or just "tend the spec"), use it and skip detection. Announce like: *"No spec here, code present, goal reads as new behaviour → starting with elicit. (Say 'distill' or 'tend' to switch.)"* This announce-and-proceed applies to the **entry path only** — genuine blocking open questions still pause and escalate (§5). From 7d112d2d433b8dd51dfd7592b93ba2e42ac28ed3 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Fri, 3 Jul 2026 13:56:00 +0300 Subject: [PATCH 19/20] Loop: note the /allium entry point's qualified form in the README Clarify that /allium surfaces as /allium:allium (plugin name == skill name) so it reads as expected rather than a mistake. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f02c5a3..ea29144 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Allium provides five skills, an entry point and two autonomous agents. | `/tend ` (or `/allium:tend`) | Targeted changes to existing specs. | | `/weed ` (or `/allium:weed`) | Find and fix divergences between spec and code. | -How skills appear depends on your editor. Some show the fully qualified form (`/allium:weed`), others show the short form (`/weed`), and some support both. If one form isn't recognised, try the other. Skills also auto-trigger when you open or edit `.allium` files. +How skills appear depends on your editor. Some show the fully qualified form (`/allium:weed`), others show the short form (`/weed`), and some support both. If one form isn't recognised, try the other. The entry point is `/allium` — because its name matches the plugin, its fully qualified form is `/allium:allium` (the same `plugin:skill` rule as every other skill); either invokes it. Skills also auto-trigger when you open or edit `.allium` files. Tend and weed are also available as autonomous **agents** that run in their own context, keeping Allium syntax out of your main session. Claude Code picks up agents from `agents/`, Copilot from `.github/agents/`. How editors discover skills and agents is still settling; we make these available in the most portable formats we can and expect to consolidate as conventions stabilise. If your editor doesn't pick something up, [raise an issue](https://github.com/juxt/allium/issues). From f40852d18fea5c6e0873dff958adfe50cb3ede64 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Fri, 3 Jul 2026 15:21:33 +0300 Subject: [PATCH 20/20] Loop: remove the obsolete local-testing reviewer note Its recipe referenced the removed /allium-loop Copilot prompt and generation steps. Drop it and the now-dangling link from the design note. Co-Authored-By: Claude Opus 4.8 (1M context) --- design/loop-mode-local-testing.md | 311 ------------------------------ design/loop-mode.md | 1 - 2 files changed, 312 deletions(-) delete mode 100644 design/loop-mode-local-testing.md diff --git a/design/loop-mode-local-testing.md b/design/loop-mode-local-testing.md deleted file mode 100644 index f658251..0000000 --- a/design/loop-mode-local-testing.md +++ /dev/null @@ -1,311 +0,0 @@ -# Trying loop mode locally (for PR reviewers) - -> **Superseded (2026-07):** the loop folded into the `allium` entry-point skill, so it now runs as `/allium ` (not `/allium:loop`), and the `/allium-loop` Copilot prompt was removed. The specific commands, filenames and generation steps below predate that rename and are retained only as a historical record — substitute `/allium` for `/allium:loop` when running it. -> -> Companion to the [loop-mode design note](./loop-mode.md). This is a reviewer -> recipe: run `/allium:loop` from **this PR branch** against a throwaway project, -> watch it drive a goal to convergence, and feed back. Like the rest of `design/` -> it is internal and not shipped to end users. - -`/allium:loop` is a plugin skill, so it only exists once Claude Code loads the -plugin. This repo is the plugin's **source**, not an installed plugin — and it -contains nothing to loop on. So testing has two parts: **load the plugin from -your checkout**, then **point it at a target project**. - ---- - -## Prerequisites - -- **Claude Code ≥ 2.1** (needs the `--plugin-dir` flag — `claude --version`). -- **The `allium` CLI** (strongly recommended). The loop's verification signal - leans on it (`allium check` gates propagation; `allium plan` derives test - obligations; `allium analyse` finds semantic gaps). Without it the loop - **degrades to assisted mode** with a weaker signal — fine for a smoke test, - but you won't see the CLI-driven phases. - ``` - brew tap juxt/allium && brew install allium - ``` -- `allium-lsp` is optional (live diagnostics); without it checking falls back to - CLI `allium check`, which is all the loop needs. - ---- - -## 1. Load the plugin from this branch - -`--plugin-dir` loads the plugin **directly from the working tree** — no -marketplace, no cache, nothing persisted. Point it at your checkout, but launch -Claude Code from inside your *target* project: - -```bash -# in one terminal: check out the PR -git -C /path/to/allium-pr-review checkout loop-mode - -# then start Claude Code in a SCRATCH project, loading the plugin from the checkout -mkdir -p /tmp/loop-demo && cd /tmp/loop-demo -claude --plugin-dir /path/to/allium-pr-review -``` - -Inside the session, `/allium:loop` (and `allium:distill`, `tend`, `weed`, …) -are now available. Quick non-interactive sanity check: - -```bash -claude --plugin-dir /path/to/allium-pr-review \ - -p "List the skills starting with 'allium', one per line." -# → allium:allium, allium:distill, allium:elicit, allium:loop, allium:propagate, allium:tend, allium:weed -``` - -**Editing the skill while testing?** `--plugin-dir` reads live from the working -tree — run `/reload-plugins` to pick up your edits in the same session. - -> Don't run the loop *inside* `allium-pr-review` itself — it's the plugin source, -> there's no spec or product code to loop on. Always use a separate target dir. - ---- - -## 1b. Or: try it in GitHub Copilot - -Copilot has no `--plugin-dir`. It reads agents and prompts from `.github/` in the -**workspace that's open in VS Code**, so there's nothing to "load" — you drop the -loop's prompt file into your target project and invoke it. The loop ships as a -prompt (not an agent) because it's interactive — it asks elicitation questions and -escalates open questions — and prompt files run in the main Chat thread where that -Q&A is natural. It's generated from `skills/loop/SKILL.md`, so it stays in lockstep -with the canonical skill. - -```bash -# from your branch checkout, copy the one generated file into your TARGET project: -mkdir -p /path/to/target/.github/prompts -cp /path/to/allium-pr-review/.github/prompts/allium-loop.prompt.md \ - /path/to/target/.github/prompts/ - -# optional: copy the tend/weed agents too, so the loop's delegation steps -# (§7) have sub-agents to hand off to -mkdir -p /path/to/target/.github/agents -cp /path/to/allium-pr-review/.github/agents/{tend,weed}.agent.md \ - /path/to/target/.github/agents/ -``` - -Then open the target project in VS Code, switch Copilot Chat to **Agent mode** -(the loop edits files and runs the test / `allium` commands — Ask/Edit modes -can't), and run: - -``` -/allium-loop build a coffee loyalty stamp card: a stamp per purchase; every -10th stamp issues a reward that expires 30 days after it's earned -``` - -The command name is the filename minus `.prompt.md` — `/allium-loop`, **not** -`/allium:loop` (that colon form is Claude Code plugin syntax). Everything after it -is the goal. - -Two differences from the Claude path worth knowing while reviewing: - -- **No auto-check.** Claude Code's hook/LSP checks the spec on every edit; Copilot - has neither, so the prompt instructs the model to **run `allium check` itself** - after each spec edit. You should see explicit `allium check` calls rather than - silent inline diagnostics. (Install the [CLI](#prerequisites) for this; without - it the loop degrades to assisted mode exactly as under Claude Code.) -- **No live reload.** `--plugin-dir` reads the working tree live; here, if you edit - `skills/loop/SKILL.md`, regenerate (`node scripts/generate-multi-editor.mjs`) and - re-copy the prompt file into the target project. - -> Same rule as above: run it in a *target* project, not in `allium-pr-review`. - ---- - -## 2. Give it something to loop on - -Pick one track. Both run inside the scratch project from step 1. - -### Track A — spec-first, from scratch (most representative) - -An empty dir + a new-behaviour goal exercises the whole loop (entry detection → -elicit → propagate → implement → verify): - -``` -/allium:loop build a coffee loyalty stamp card: a stamp per purchase; every -10th stamp issues a reward that expires 30 days after it's earned -``` - -Elicit is interactive — expect it to ask you a couple of edge-case questions -(reopen a closed card? claim flow? expiry cascade?) before it propagates. - -### Track B — bounded run from a ready spec - -For a shorter, more deterministic pass, drop the reference spec from the -[appendix](#appendix-reference-spec) into the dir as `loyalty.allium` and run: - -``` -/allium:loop propagate tests and an implementation for loyalty.allium, then -reconcile until tests, spec and code agree -``` - ---- - -## 3. What to watch for (so feedback is targeted) - -The loop should make each of these observable — flag any that's missing or wrong: - -- **Entry detection (announce-and-proceed).** One line naming the chosen path - *and* the override, then it proceeds **without** waiting for confirmation. -- **Phase markers.** `→ Gather` / `→ Act` / `→ Verify` boundaries, not a wall of - narrated commands. -- **Spec checked before propagating.** `allium check` runs on every spec edit; - errors/warnings resolved before tests are generated. -- **Fail-first (anti-cheat).** Newly generated tests are shown **RED before** - implementation; a generated test is **never** edited to make it pass; `config` - values aren't hard-coded as magic numbers in the code. -- **Verify is real.** Tests actually run; `weed` runs for drift; `allium analyse` - runs for semantic gaps — and an analyse/weed finding **routes back into a - `tend`**, not a shrug. -- **Caps & ledger.** Hard cap **6** ticks; **no-progress** cap of **2** identical - ticks; resumable ledger under `.allium-loop/.json` (git-ignored). -- **Convergence report** at the end: what converged, tests/weed verdict, escalated - vs parked open questions. - ---- - -## 4. Feedback worth gathering - -- Did **entry detection** pick the right starting mode for your goal? -- Were the **caps** (6 / 2) hit appropriately, or did it stop too early / spin? -- Did **escalation vs parking** of open questions feel right (§7 of the design note)? -- Two spots the author already flagged as possibly under-specified: - - §2.1 says "resolve any reported issues before propagating," but `allium check` - emits **info-level** notes and **by-design advisories** (e.g. the external-entity - reminder) that arguably shouldn't block. Does the skill draw that line clearly? - - The **no-progress** signal keys on tests / weed / open-question count — it does - **not** include `allium analyse` finding count. A loop that only moves the - analyse needle could trip the cap despite genuinely converging. - -File feedback on the PR; quote the tick summary line where relevant. - ---- - -## Cleanup - -`--plugin-dir` persists nothing, so there's nothing to uninstall. Remove the -scratch dir (`rm -rf /tmp/loop-demo`); `.allium-loop/` is git-ignored if you -looped inside a repo. - ---- - -## Appendix: reference spec - -A known-good converged spec for Track B. Driving the loop manually against this -(spec-first) reached convergence in **2 ticks** — tests 13/13, `weed` clean, -`allium analyse` 0 findings (one by-design external-entity advisory on `Barista` -remains). Tick 1 implemented the rules; tick 2 added the surfaces after `analyse` -flagged the triggers as having no boundary. - -```allium --- allium: 3 --- Coffee loyalty card: a stamp per purchase; every N stamps issues a --- reward that expires after a fixed validity window. - ------------------------------------------------------------- --- External Entities ------------------------------------------------------------- - -external entity Barista {} - ------------------------------------------------------------- --- Config ------------------------------------------------------------- - -config { - stamps_per_reward: Integer = 10 - reward_validity: Duration = 30.days -} - ------------------------------------------------------------- --- Entities and Variants ------------------------------------------------------------- - -entity LoyaltyCard { - holder: String - stamps: Integer - status: active | closed -} - -entity Reward { - card: LoyaltyCard - issued_at: Timestamp - expires_at: Timestamp - status: unclaimed | expired -} - ------------------------------------------------------------- --- Rules ------------------------------------------------------------- - -rule IssueCard { - when: IssuesCard(staff, holder) - ensures: LoyaltyCard.created(holder: holder, stamps: 0, status: active) -} - -rule CloseCard { - when: ClosesCard(staff, card) - requires: card.status = active - ensures: card.status = closed -} - -rule RecordPurchase { - when: RecordsPurchase(staff, card) - requires: card.status = active - - let new_count = card.stamps + 1 - - ensures: - if new_count >= config.stamps_per_reward: - card.stamps = new_count - config.stamps_per_reward - Reward.created( - card: card, - issued_at: now, - expires_at: now + config.reward_validity, - status: unclaimed - ) - if new_count < config.stamps_per_reward: - card.stamps = new_count -} - -rule RewardExpires { - when: reward: Reward.expires_at <= now - requires: reward.status = unclaimed - ensures: reward.status = expired -} - ------------------------------------------------------------- --- Invariants ------------------------------------------------------------- - -invariant StampsWithinCycle { - for card in LoyaltyCards: - card.stamps >= 0 and card.stamps < config.stamps_per_reward -} - ------------------------------------------------------------- --- Surfaces ------------------------------------------------------------- - -surface CardRegistration { - facing staff: Barista - - provides: - IssuesCard(staff, holder) -} - -surface CardActions { - facing staff: Barista - - context card: LoyaltyCard where status = active - - exposes: - card.holder - card.stamps - - provides: - RecordsPurchase(staff, card) - ClosesCard(staff, card) -} -``` diff --git a/design/loop-mode.md b/design/loop-mode.md index 31b6377..f4e0c3f 100644 --- a/design/loop-mode.md +++ b/design/loop-mode.md @@ -5,7 +5,6 @@ > **Status: MVP built** — implemented as the `loop` skill (`skills/loop/SKILL.md`); Layer 1 (in-session) only. Living document — update as the design evolves. > **Lifecycle:** internal design note, not user-facing. To be retired when `/allium:loop` ships — its content graduates into the skill itself and the recommended-loops reference. Keep it out of the user-facing vendored plugin until then (split onto its own branch, or exclude `design/` from the marketplace sync, before any release that would otherwise ship it). > Companion to the shipped [recommended loops](../skills/allium/references/recommended-loops.md) reference, which documents the loop *pattern*. This note designs a skill that *drives* it. -> **Reviewers:** to run `/allium:loop` from this branch on your own machine, follow [Trying loop mode locally](./loop-mode-local-testing.md). ## 1. Purpose