diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cc3b736..9d4eb98 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", diff --git a/README.md b/README.md index ea79670..ea29144 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, 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,14 +112,14 @@ Allium provides five 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. 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. | -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). @@ -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/design/loop-mode.md b/design/loop-mode.md new file mode 100644 index 0000000..f4e0c3f --- /dev/null +++ b/design/loop-mode.md @@ -0,0 +1,126 @@ +# 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. + +## 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 — announce, then proceed + +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. + +- 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) + +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 (§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** — 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-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. + +## 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). +- **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. + +**Still open:** none outstanding from review. Revisit when building the skill — likely the ledger JSON schema and exact decomposition thresholds. + +## 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). diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index 4a21587..1d01aa9 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, loopdocs, hooks, discovery, crosstalk * - * The first six 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. */ -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(), @@ -222,6 +238,8 @@ if (shouldRun("structure")) { pass(`${label} naming`); } } + + console.log(""); } // --------------------------------------------------------------------------- @@ -361,6 +379,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 +428,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" : ""})`); + } + } } // --------------------------------------------------------------------------- @@ -429,6 +508,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/allium/references/driving-the-loop.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/allium/references/driving-the-loop.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 // --------------------------------------------------------------------------- diff --git a/skills/allium/SKILL.md b/skills/allium/SKILL.md index a50dedb..eb98f11 100644 --- a/skills/allium/SKILL.md +++ b/skills/allium/SKILL.md @@ -33,10 +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 | 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") → 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. 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. +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: @@ -322,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/allium/references/driving-the-loop.md b/skills/allium/references/driving-the-loop.md new file mode 100644 index 0000000..bd55ed1 --- /dev/null +++ b/skills/allium/references/driving-the-loop.md @@ -0,0 +1,87 @@ +# Driving the 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. + +## 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 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. 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 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/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: