Skip to content

Commit afe47dc

Browse files
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) <noreply@anthropic.com>
1 parent 901362c commit afe47dc

9 files changed

Lines changed: 339 additions & 10 deletions

File tree

plugins/allium/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "allium",
3-
"version": "3.6.0",
3+
"version": "3.7.0",
44
"description": "Velocity through clarity.",
55
"author": {
66
"name": "JUXT",

plugins/allium/.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "allium",
3-
"version": "3.6.0",
3+
"version": "3.7.0",
44
"description": "Velocity through clarity.",
55
"author": {
66
"name": "JUXT",

plugins/allium/README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ npx skills add juxt/allium
8484

8585
**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.
8686

87-
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.
87+
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.
8888

8989
Jump to what [Allium looks like in practice](#what-this-looks-like-in-practice).
9090

@@ -112,14 +112,14 @@ Allium provides five skills, an entry point and two autonomous agents.
112112

113113
| Skill | Purpose |
114114
|---|---|
115-
| `/allium <prompt>` | Entry point. Examines your project or the prompt and routes you to the right skill. |
115+
| `/allium <goal or prompt>` | 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. |
116116
| `/elicit <feature idea>` (or `/allium:elicit`) | Build a spec through structured conversation. |
117117
| `/distill <codebase area>` (or `/allium:distill`) | Extract a spec from existing code. |
118118
| `/propagate <optional constraints>` (or `/allium:propagate`) | Generate tests from a spec. |
119119
| `/tend <optional constraints>` (or `/allium:tend`) | Targeted changes to existing specs. |
120120
| `/weed <optional constraints>` (or `/allium:weed`) | Find and fix divergences between spec and code. |
121121

122-
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.
122+
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.
123123

124124
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).
125125

@@ -260,6 +260,28 @@ A developer starts a new session days later, working on a different feature.
260260

261261
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.
262262

263+
### The loop in motion
264+
265+
A developer asks for a feature and lets the loop run to convergence.
266+
267+
---
268+
269+
> ***> Add gift cards to checkout.***
270+
>
271+
> ✻ 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?
272+
>
273+
> ***> Mutually exclusive.***
274+
>
275+
> ✻ Spec updated. Generated tests with `/propagate` and ran them — 4 new tests, all failing as expected (red). Implementing against them now.
276+
>
277+
> ✻ 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.
278+
>
279+
> ✻ Re-ran: tests pass, `/weed` clean, no open questions. Spec, tests and code agree — converged.
280+
281+
---
282+
283+
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.
284+
263285
## Verification
264286

265287
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.

plugins/allium/scripts/test-skills.mjs

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
* node scripts/test-skills.mjs structure # run one group
1111
* node scripts/test-skills.mjs portability links # run multiple groups
1212
*
13-
* Groups: structure, codex, portability, links, routing, generation, discovery, crosstalk
13+
* Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, discovery, crosstalk
1414
*
15-
* The first six groups are offline (free, fast). The last two require --live
15+
* All groups except discovery and crosstalk are offline (free, fast); those two require --live
1616
* and make Claude API calls.
1717
*/
1818

19-
import { readFileSync, existsSync } from "fs";
19+
import { readFileSync, existsSync, readdirSync } from "fs";
2020
import { execFileSync, execSync } from "child_process";
2121
import path from "path";
2222

@@ -102,6 +102,22 @@ function resolveRelativeLinks(body, fileDir) {
102102
}));
103103
}
104104

105+
// Broader link check for prose docs (README, references, design notes):
106+
// any markdown link to a local path, skipping external URLs and pure anchors.
107+
function resolveDocLinks(body, fileDir) {
108+
const linkPattern = /\[[^\]]*\]\(([^)]+)\)/g;
109+
const out = [];
110+
let m;
111+
while ((m = linkPattern.exec(body)) !== null) {
112+
const raw = m[1].trim().split(/\s+/)[0]; // drop any "title" suffix
113+
if (/^(https?:|mailto:|#)/.test(raw)) continue; // external or pure anchor
114+
const noAnchor = raw.replace(/#.*$/, "");
115+
if (!noAnchor) continue;
116+
out.push({ link: raw, exists: existsSync(path.resolve(fileDir, noAnchor)) });
117+
}
118+
return out;
119+
}
120+
105121
function claudeQuery(prompt, { cwd } = {}) {
106122
const output = execFileSync(
107123
getClaudePath(),
@@ -222,6 +238,8 @@ if (shouldRun("structure")) {
222238
pass(`${label} naming`);
223239
}
224240
}
241+
242+
console.log("");
225243
}
226244

227245
// ---------------------------------------------------------------------------
@@ -361,6 +379,38 @@ if (shouldRun("portability")) {
361379
// Links — all relative markdown links resolve to real files
362380
// ---------------------------------------------------------------------------
363381

382+
if (shouldRun("consistency")) {
383+
console.log("\n── consistency: manifests & registration ──\n");
384+
385+
const claudePluginPath = path.join(ROOT, ".claude-plugin", "plugin.json");
386+
const claude = readJson(claudePluginPath);
387+
const codex = readJson(codexPluginPath);
388+
389+
// Version parity across the two plugin manifests.
390+
if (claude && codex && claude.version && claude.version === codex.version) {
391+
pass(`version parity (${claude.version})`);
392+
} else {
393+
fail("version parity", `claude=${claude?.version} codex=${codex?.version}`);
394+
}
395+
396+
// Registration: skills/ dirs == test skillNames == .claude-plugin skills[].
397+
const skillsRoot = path.join(ROOT, "skills");
398+
const actualDirs = existsSync(skillsRoot)
399+
? readdirSync(skillsRoot).filter((d) => existsSync(path.join(skillsRoot, d, "SKILL.md")))
400+
: [];
401+
const claudeArray = Array.isArray(claude?.skills) ? claude.skills.map((s) => path.basename(s)) : [];
402+
const sortUniq = (a) => [...new Set(a)].sort();
403+
const dirs = sortUniq(actualDirs);
404+
const named = sortUniq(skillNames);
405+
const registered = sortUniq(claudeArray);
406+
const eq = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
407+
if (eq(dirs, named) && eq(dirs, registered)) {
408+
pass(`skill registration consistent (${dirs.length} skills)`);
409+
} else {
410+
fail("skill registration", `dirs=[${dirs}] skillNames=[${named}] claude-plugin=[${registered}]`);
411+
}
412+
}
413+
364414
if (shouldRun("links")) {
365415
console.log("\n── links: relative link resolution ──\n");
366416

@@ -378,6 +428,35 @@ if (shouldRun("links")) {
378428
pass(`${rel(filePath)} (${links.length} link${links.length !== 1 ? "s" : ""})`);
379429
}
380430
}
431+
432+
// Prose docs (README + reference docs + design notes) — broader link check
433+
// that also covers bare relative paths, not just ./ and ../ links.
434+
const proseDocs = [path.join(ROOT, "README.md")];
435+
for (const n of skillNames) {
436+
const refDir = path.join(ROOT, "skills", n, "references");
437+
if (existsSync(refDir)) {
438+
for (const f of readdirSync(refDir)) {
439+
if (f.endsWith(".md")) proseDocs.push(path.join(refDir, f));
440+
}
441+
}
442+
}
443+
const designDir = path.join(ROOT, "design");
444+
if (existsSync(designDir)) {
445+
for (const f of readdirSync(designDir)) {
446+
if (f.endsWith(".md")) proseDocs.push(path.join(designDir, f));
447+
}
448+
}
449+
for (const filePath of proseDocs) {
450+
if (!existsSync(filePath)) continue;
451+
const links = resolveDocLinks(readFileSync(filePath, "utf-8"), path.dirname(filePath));
452+
const broken = links.filter((l) => !l.exists);
453+
for (const { link } of broken) {
454+
fail(`${rel(filePath)}`, `broken link: ${link}`);
455+
}
456+
if (broken.length === 0) {
457+
pass(`${rel(filePath)} (${links.length} link${links.length !== 1 ? "s" : ""})`);
458+
}
459+
}
381460
}
382461

383462
// ---------------------------------------------------------------------------
@@ -429,6 +508,104 @@ if (shouldRun("generation")) {
429508
}
430509
}
431510

511+
// ---------------------------------------------------------------------------
512+
// Loopdocs — the loop constants (caps + phase phrase) stay consistent across
513+
// the docs that restate them. Canonical values live here in the test.
514+
// ---------------------------------------------------------------------------
515+
516+
if (shouldRun("loopdocs")) {
517+
console.log("\n── loopdocs: loop constant drift ──\n");
518+
519+
const HARD_CAP = 6;
520+
const NO_PROGRESS = 2;
521+
const PHASE_PHRASE = "gather context → take action → verify → repeat";
522+
523+
// Files that state the numeric caps.
524+
const capFiles = [
525+
"skills/allium/references/driving-the-loop.md",
526+
"skills/allium/references/recommended-loops.md",
527+
"design/loop-mode.md",
528+
];
529+
for (const rp of capFiles) {
530+
const fp = path.join(ROOT, rp);
531+
if (!existsSync(fp)) continue; // design note may be absent post-release
532+
const src = readFileSync(fp, "utf-8");
533+
const hard = src.match(/hard cap[^\n.]*?\b(\d+)\b/i);
534+
const noProg = src.match(/no-progress[^\n.]*?\b(\d+)\b/i);
535+
if (hard && Number(hard[1]) === HARD_CAP) pass(`${rp} hard cap = ${HARD_CAP}`);
536+
else fail(`${rp} hard cap`, `expected ${HARD_CAP}, found ${hard ? hard[1] : "none"}`);
537+
if (noProg && Number(noProg[1]) === NO_PROGRESS) pass(`${rp} no-progress cap = ${NO_PROGRESS}`);
538+
else fail(`${rp} no-progress cap`, `expected ${NO_PROGRESS}, found ${noProg ? noProg[1] : "none"}`);
539+
}
540+
541+
// Files that state the phase phrase in arrow form.
542+
const phaseFiles = [
543+
"skills/allium/references/driving-the-loop.md",
544+
"skills/allium/references/recommended-loops.md",
545+
"skills/allium/SKILL.md",
546+
"design/loop-mode.md",
547+
];
548+
for (const rp of phaseFiles) {
549+
const fp = path.join(ROOT, rp);
550+
if (!existsSync(fp)) continue;
551+
if (readFileSync(fp, "utf-8").includes(PHASE_PHRASE)) pass(`${rp} phase phrase`);
552+
else fail(`${rp} phase phrase`, `missing "${PHASE_PHRASE}"`);
553+
}
554+
555+
// README states the phases in verb form — check the four appear in order.
556+
const readmePath = path.join(ROOT, "README.md");
557+
if (existsSync(readmePath)) {
558+
const src = readFileSync(readmePath, "utf-8");
559+
const stems = [/gather/i, /take[s]? action/i, /verif/i, /repeat/i];
560+
const idx = stems.map((s) => src.search(s));
561+
if (idx.every((i) => i >= 0) && idx.every((v, i) => i === 0 || v > idx[i - 1])) {
562+
pass("README.md phases in order");
563+
} else {
564+
fail("README.md phases", `not all present and in order: ${idx}`);
565+
}
566+
}
567+
}
568+
569+
// ---------------------------------------------------------------------------
570+
// Hooks — the PostToolUse hook config is valid and points at a real script.
571+
// ---------------------------------------------------------------------------
572+
573+
if (shouldRun("hooks")) {
574+
console.log("\n── hooks: hook config integrity ──\n");
575+
576+
const hooksPath = path.join(ROOT, "hooks", "hooks.json");
577+
if (!existsSync(hooksPath)) {
578+
fail("hooks/hooks.json", "not found");
579+
} else {
580+
const cfg = readJson(hooksPath);
581+
if (!cfg) {
582+
fail("hooks/hooks.json", "invalid JSON");
583+
} else {
584+
pass("hooks/hooks.json valid JSON");
585+
const post = cfg.hooks?.PostToolUse;
586+
if (!Array.isArray(post) || post.length === 0) {
587+
fail("hooks PostToolUse", "missing or empty");
588+
} else {
589+
pass("hooks PostToolUse present");
590+
let matchersOk = true;
591+
let scriptsOk = true;
592+
for (const entry of post) {
593+
if (!entry || !entry.matcher) matchersOk = false;
594+
const cmds = Array.isArray(entry?.hooks) ? entry.hooks : [];
595+
for (const h of cmds) {
596+
const m =
597+
typeof h.command === "string" &&
598+
h.command.match(/\$\{CLAUDE_PLUGIN_ROOT\}\/([^"\s]+)/);
599+
if (m && !existsSync(path.join(ROOT, m[1]))) scriptsOk = false;
600+
}
601+
}
602+
matchersOk ? pass("hooks have matchers") : fail("hooks matcher", "an entry is missing a matcher");
603+
scriptsOk ? pass("hook command scripts exist") : fail("hook command", "referenced script not found");
604+
}
605+
}
606+
}
607+
}
608+
432609
// ---------------------------------------------------------------------------
433610
// Discovery — live Claude Code skill and agent loading
434611
// ---------------------------------------------------------------------------

plugins/allium/skills/allium/SKILL.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,23 @@ Allium does NOT specify programming language or framework choices, database sche
3333
| Modifying an existing spec | `tend` skill | User wants targeted changes to `.allium` files |
3434
| Checking spec-to-code alignment | `weed` skill | User wants to find or fix divergences between spec and implementation |
3535
| Generating tests from a spec | `propagate` skill | User wants to generate tests, PBT properties or state machine tests from a specification |
36+
| 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 <goal>` runs the gather→act→verify→repeat loop autonomously until spec, tests and code agree |
37+
38+
## Responding to `/allium` (loop-first)
39+
40+
`/allium` is the entry point. Bias toward the autonomous path — the whole-loop value is exactly what occasional single-skill use misses:
41+
42+
- **Clear single task** → route straight to that skill (per the routing table); don't make the user wade through a menu.
43+
- **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).
44+
- **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:
45+
46+
> 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.
47+
48+
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.
3649

3750
## The Allium loop (recommended sequencing)
3851

39-
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.
52+
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).
4053

4154
Two entry points, one convergence loop:
4255

@@ -322,4 +335,5 @@ When the `allium` CLI is installed, a hook validates `.allium` files automatical
322335
- [Language reference](./references/language-reference.md) — full syntax for entities, rules, expressions, surfaces, contracts, invariants and validation
323336
- [Test generation](./references/test-generation.md) — generating tests from specifications
324337
- [Recommended loops](./references/recommended-loops.md) — the gather-context → take-action → verify → repeat loop, with spec-first and code-first walkthroughs
338+
- [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)
325339
- [Patterns](./references/patterns.md) — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract

0 commit comments

Comments
 (0)