From afe47dc11b50861525e3e9a3dd2458102bfae0c8 Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Fri, 3 Jul 2026 15:27:14 +0300 Subject: [PATCH] Bump vendored allium to v3.7.0 Re-vendor plugins/allium from juxt/allium@v3.7.0: /allium now drives the whole loop to convergence (the standalone /allium:loop command folded into the entry point). Also exclude design/ (internal, non-user-facing notes) from the sync so they don't ship to the marketplace. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/allium/.claude-plugin/plugin.json | 2 +- plugins/allium/.codex-plugin/plugin.json | 2 +- plugins/allium/README.md | 28 ++- plugins/allium/scripts/test-skills.mjs | 183 +++++++++++++++++- plugins/allium/skills/allium/SKILL.md | 16 +- .../allium/references/driving-the-loop.md | 87 +++++++++ .../allium/references/recommended-loops.md | 24 +++ scripts/allium-ref.txt | 2 +- scripts/sync-allium.sh | 5 + 9 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 plugins/allium/skills/allium/references/driving-the-loop.md diff --git a/plugins/allium/.claude-plugin/plugin.json b/plugins/allium/.claude-plugin/plugin.json index cc3b736..9d4eb98 100644 --- a/plugins/allium/.claude-plugin/plugin.json +++ b/plugins/allium/.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/plugins/allium/.codex-plugin/plugin.json b/plugins/allium/.codex-plugin/plugin.json index 45621d8..fab6067 100644 --- a/plugins/allium/.codex-plugin/plugin.json +++ b/plugins/allium/.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/plugins/allium/README.md b/plugins/allium/README.md index ea79670..ea29144 100644 --- a/plugins/allium/README.md +++ b/plugins/allium/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/plugins/allium/scripts/test-skills.mjs b/plugins/allium/scripts/test-skills.mjs index 4a21587..1d01aa9 100644 --- a/plugins/allium/scripts/test-skills.mjs +++ b/plugins/allium/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/plugins/allium/skills/allium/SKILL.md b/plugins/allium/skills/allium/SKILL.md index a50dedb..eb98f11 100644 --- a/plugins/allium/skills/allium/SKILL.md +++ b/plugins/allium/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/plugins/allium/skills/allium/references/driving-the-loop.md b/plugins/allium/skills/allium/references/driving-the-loop.md new file mode 100644 index 0000000..bd55ed1 --- /dev/null +++ b/plugins/allium/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/plugins/allium/skills/allium/references/recommended-loops.md b/plugins/allium/skills/allium/references/recommended-loops.md index b055c55..b677766 100644 --- a/plugins/allium/skills/allium/references/recommended-loops.md +++ b/plugins/allium/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: diff --git a/scripts/allium-ref.txt b/scripts/allium-ref.txt index 130165b..d1e9cf3 100644 --- a/scripts/allium-ref.txt +++ b/scripts/allium-ref.txt @@ -1 +1 @@ -v3.6.0 +v3.7.0 diff --git a/scripts/sync-allium.sh b/scripts/sync-allium.sh index a308051..fd11160 100755 --- a/scripts/sync-allium.sh +++ b/scripts/sync-allium.sh @@ -33,6 +33,11 @@ git clone --quiet https://github.com/juxt/allium "$tmp/allium" git -C "$tmp/allium" checkout --quiet "$REF" rm -rf "$tmp/allium/.git" +# design/ holds internal, non-user-facing design notes; keep them out of the +# vendored marketplace payload. Stripped from the source snapshot here so that +# both `sync` and `--check` operate on the same design-less tree. +rm -rf "$tmp/allium/design" + if [ "$MODE" = "--check" ]; then if [ ! -d "$DEST" ]; then echo "plugins/allium not present yet — nothing to check (vendor it with: scripts/sync-allium.sh)."