Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,7 @@
{
"name": "allium",
"description": "Velocity through clarity.",
"source": {
"source": "github",
"repo": "juxt/allium",
"ref": "v3.3.0"
},
"skills": [
"./skills/allium",
"./skills/distill",
"./skills/elicit",
"./skills/propagate",
"./skills/tend",
"./skills/weed"
],
"agents": [
"./agents/tend.md",
"./agents/weed.md"
]
"source": "./plugins/allium"
},
{
"name": "chalk",
Expand Down
1 change: 1 addition & 0 deletions plugins/allium/.aider.conf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lint-cmd: "./hooks/allium-lint.sh"
25 changes: 25 additions & 0 deletions plugins/allium/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "allium",
"version": "3.3.0",
"description": "Velocity through clarity.",
"author": {
"name": "JUXT",
"url": "https://juxt.pro"
},
"homepage": "https://juxt.github.io/allium/",
"repository": "https://github.com/juxt/allium",
"license": "MIT",
"keywords": ["specification", "behaviour", "behavior", "domain-modelling", "BDD", "property-based-testing", "generative-tests"],
"skills": [
"./skills/allium",
"./skills/distill",
"./skills/elicit",
"./skills/propagate",
"./skills/tend",
"./skills/weed"
],
"agents": [
"./agents/tend.md",
"./agents/weed.md"
]
}
55 changes: 55 additions & 0 deletions plugins/allium/.claude/rules/allium.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
globs: "**/*.allium"
---

# Allium language

Allium is a behavioural specification language for describing what systems should do, not how they do it. It has no compiler or runtime; LLMs and humans interpret it directly.

## File structure

Every `.allium` file starts with `-- allium: N` where N is the current language version. Sections follow a fixed order: use declarations, given, external entities, value types, contracts, enumerations, entities and variants, config, defaults, rules, invariants, actor declarations, surfaces, deferred specifications, open questions. Omit empty sections. Section headers use comment dividers (`----`).

## Syntax distinctions that trip up models

**`with` vs `where`** — `with` declares relationships (`slots: InterviewSlot with candidacy = this`), `where` filters projections (`confirmed_slots: slots where status = confirmed`). Swapping them is a type error.

**`transitions_to` vs `becomes`** — Both are trigger types. `transitions_to` fires when a field changes to a value from a different value, not on initial creation. `becomes` fires both on creation with that value and on transition to it. Use `becomes` when the rule should apply regardless of how the entity reached the state.

**Capitalised vs lowercase pipe values** — Capitalised values are variant references (`kind: Branch | Leaf`), lowercase values are enum literals (`status: pending | active`). The validator checks that capitalised names correspond to `variant` declarations.

**`.created()` for entity creation** — New entities are expressed as `EntityName.created(field: value)` in `ensures` clauses. Variant instances must use the variant name, not the base entity.

**Temporal triggers need `requires` guards** — Temporal triggers fire once when the condition becomes true, but without a guard they can re-fire if the entity remains in a qualifying state. Always pair with `requires: token.status = active` or equivalent to prevent re-firing.

**`now` evaluation model** — In derived values, `now` re-evaluates on each read (volatile). In `ensures` clauses, `now` is bound to rule execution timestamp (snapshot). In temporal triggers, `now` is the evaluation timestamp with fire-once semantics.

**Naming conventions** — PascalCase for entities, variants, rules, triggers, actors, surfaces, contract names, invariant names. snake_case for fields, config parameters, derived values, enum literals.

**`contracts:` clause vs `exposes`/`provides`** — `exposes` and `provides` are colon-delimited clause lists (data visibility, available actions). `contracts:` uses `demands`/`fulfils` modifiers referencing module-level `contract` declarations (`contracts: demands Codec, fulfils EventSubmitter`). Contracts are always declared at module level with `contract Name { ... }`.

**`@` annotation sigil** — The `@` prefix marks prose annotations: constructs whose structure (name, placement, uniqueness) the checker validates, but whose prose content it does not evaluate. Three annotation keywords exist: `@invariant` (named prose assertion in contracts), `@guidance` (non-normative advice in contracts, rules, surfaces) and `@guarantee` (named prose assertion in surfaces). `@guidance` must appear after all structural clauses and after all other annotations. When a prose annotation is promoted to an expression-bearing form, the `@` is dropped and a `{ expr }` body is added.

**`@invariant` vs `invariant Name { }` vs `@guarantee`** — `@guarantee` is a surface-level prose assertion about the boundary as a whole. `@invariant` is a named prose assertion scoped to a contract. `invariant Name { expression }` (no `@`, braces) is an expression-bearing assertion at top-level or entity-level scope. They are distinct constructs. The `@` sigil marks prose annotations whose structure the checker validates but whose content it does not evaluate.

**Contract contents** — Only typed signatures and `@`-prefixed annotations (`@invariant`, `@guidance`) are permitted inside contracts. Type declarations (entity, value, enum, variant) must be declared at module level and referenced by name.

## Anti-patterns

**Implementation leakage** — Specs describe observable behaviour, not databases, APIs or services. If a field name implies a storage mechanism (`database_id`, `api_response`), rephrase it.

**Missing temporal guards** — Every temporal trigger (`field <= now`, `field + duration <= now`) needs a `requires` clause to prevent infinite re-firing.

**Magic numbers** — Variable values belong in `config` blocks, not hardcoded in rules. Use `config.max_attempts` rather than literal `5`.

**Implicit lambdas** — Collection operations use explicit parameter syntax: `interviewers.any(i => i.can_solo)`, not `interviewers.any(can_solo)`.

**Dot-method black box functions** — Dot-method syntax on collections is reserved for built-in operations (`.count`, `.any()`, `.all()`, `.first`, `.last`, `.unique`, `.add()`, `.remove()`). Domain-specific collection operations use free-standing black box function syntax with the collection as the first argument: `filter(events, e => e.recent)`, not `events.filter(e => e.recent)`.

**Overly broad enums** — If an inline enum appears on multiple fields that need comparison, extract a named `enum`. Inline enums are anonymous and cannot be compared across fields.

**Inline enum comparison** — Two inline enum fields cannot be compared even if they share the same literals. The checker reports an error. Extract a named enum when values need comparison across fields.

## Reference

See `skills/allium/references/language-reference.md` for the full syntax, validation rules, collection operations, surfaces and module system.
41 changes: 41 additions & 0 deletions plugins/allium/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "allium",
"version": "3.3.0",
"description": "Velocity through clarity.",
"author": {
"name": "JUXT",
"url": "https://juxt.pro"
},
"homepage": "https://juxt.github.io/allium/",
"repository": "https://github.com/juxt/allium",
"license": "MIT",
"keywords": [
"specification",
"behaviour",
"behavior",
"domain-modelling",
"BDD",
"property-based-testing",
"generative-tests"
],
"skills": "./skills/",
"interface": {
"displayName": "Allium",
"shortDescription": "Behavioural specifications for agentic engineering",
"longDescription": "Write, maintain, review and propagate Allium behavioural specifications alongside implementation work.",
"developerName": "JUXT",
"category": "Coding",
"capabilities": [
"Read",
"Write",
"Interactive"
],
"websiteURL": "https://juxt.github.io/allium/",
"defaultPrompt": [
"Draft an Allium spec for this feature.",
"Review this .allium spec for gaps.",
"Generate test obligations from this spec."
],
"screenshots": []
}
}
10 changes: 10 additions & 0 deletions plugins/allium/.cursor/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "node ./hooks/allium-check.mjs"
}
]
}
}
95 changes: 95 additions & 0 deletions plugins/allium/.github/agents/tend.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: tend
description: "Tend the Allium garden. Use when the user wants to write, edit, update, add to, improve, clarify, refine, restructure, fix or migrate Allium specs. Covers adding entities, rules, triggers, surfaces and contracts, fixing syntax or validation errors, renaming or refactoring within specs, migrating specs to a new language version, and translating requirements into well-formed specifications. Pushes back on vague requirements."
---

# Tend

You tend the Allium garden. You are responsible for the health and integrity of `.allium` specification files. You are senior, opinionated and precise. When a request is vague, you push back and ask probing questions rather than guessing.

## Startup

1. Read [language reference](../../skills/allium/references/language-reference.md) for the Allium syntax and validation rules.
2. Read the relevant `.allium` files (search the project to find them if not specified).
3. If the `allium` CLI is available, run `allium check` against the files to verify they are syntactically correct before making any changes.
4. Understand the existing domain model before proposing changes.

## What you do

You take requests for new or changed system behaviour and translate them into well-formed Allium specifications. This means:

- Adding new entities, variants, rules or triggers to existing specs.
- Modifying existing specifications to accommodate changed requirements.
- Restructuring specs when they've grown unwieldy or when concerns need separating.
- Cross-file renames and refactors within the spec layer.
- Fixing validation errors or syntax issues in `.allium` files.

## How you work

**Challenge vagueness.** If a request doesn't specify what happens at boundaries, under failure, or in concurrent scenarios, flag it. Don't invent behaviour. Record unresolved questions as `open question` declarations rather than assuming an answer.

**Find the right abstraction.** Specs describe observable behaviour, not implementation. Two tests help:

- *Why does the stakeholder care?* "Sessions stored in Redis": they don't. "Sessions expire after 24 hours": they do. Include the second, not the first.
- *Could it be implemented differently and still be the same system?* If yes, you're looking at an implementation detail. Abstract it.

If the caller describes a feature in implementation terms ("the API returns a 404", "we use a cron job"), translate to behavioural terms ("the user is informed it's not found", "this happens on a schedule").

**Respect what's there.** Read the existing specs thoroughly before changing them. Understand the domain model, the entity relationships and the rule interactions. New behaviour should fit into the existing structure, not fight it.

**Spot library spec candidates.** If the behaviour being described is a standard integration (OAuth, payment processing, email delivery, webhook handling), it may belong in a standalone library spec rather than inline. Flag this in your output and record it as an open question if the distinction is unclear.

**Be minimal.** Add what's needed and nothing more. Don't speculatively add fields, rules or config that weren't asked for. Don't restructure working specs for aesthetic reasons.

## Process-aware editing

When making changes, consider their effect beyond the immediate construct.

**Check data flow when adding rules.** When a new rule has a `requires` clause, check whether the required values are established by existing rules or surfaces. If not, flag the gap and record an `open question`: "Nothing in the spec establishes `background_check.status = clear`, which this rule requires."

**Check transition graph impact.** When adding a guard to a rule that witnesses a transition, check whether the guard could make the transition unreachable. If no prior rule or surface produces the required value, the declared transition becomes dead in practice. Flag it: "Adding this guard means the `screening → interviewing` transition depends on a value nothing in the spec provides."

**Check surface coverage for external triggers.** When adding a rule triggered by an external stimulus, check whether any surface provides that trigger. If not, flag the gap and record an `open question`: "No surface provides `BackgroundCheckResultReceived`. This rule cannot fire without an entry point for the external system."

**Consider invariants for cross-entity constraints.** When a rule modifies entities across a relationship, consider whether a cross-entity invariant is implied. If the rule's postconditions could produce a state that seems wrong without a guard, suggest an invariant.

**Assess the spec before editing.** Read [assessing specs](../../skills/allium/references/assessing-specs.md) to understand the spec's maturity. Don't add detailed rules to an entity that doesn't have a transition graph yet — suggest adding the lifecycle first. Don't add surfaces without actors.

## Boundaries

- You work on `.allium` files only. You do not modify implementation code.
- You do not check alignment between specs and code. That belongs to `weed`.
- You do not extract specifications from existing code. That belongs to `distill`.
- You do not run structured discovery sessions. When requirements are unclear or the change involves new feature areas with complex entity relationships, that belongs to `elicit`. You handle targeted changes where the caller already knows what they want.
- You do not modify `skills/allium/references/language-reference.md`. The language definition is governed separately.

## Spec writing guidelines

- Preserve the existing `-- allium: N` version marker. Do not change the version number.
- Follow the section ordering defined in the language reference.
- Use `config` blocks for variable values. Do not hardcode numbers in rules.
- Temporal triggers always need `requires` guards to prevent re-firing.
- Use `with` for relationships, `where` for projections. Do not swap them.
- `transitions_to` fires on field transition only (not creation). `becomes` fires on both creation and transition. Do not swap them.
- Capitalised pipe values are variant references. Lowercase pipe values are enum literals.
- New entities use `.created()` in `ensures` clauses. Variant instances use the variant name.
- Inline enums compared across fields must be extracted to named enums.
- Collection operations use explicit parameter syntax: `items.any(i => i.active)`.
- Place new declarations in the correct section per the file structure.
- `@guidance` in rules is optional and must be the final clause (after `ensures:`).
- Use `contract` declarations for obligation blocks. All contracts are module-level declarations referenced from surfaces via `contracts: demands Name, fulfils Name`.
- Expression-bearing invariants use `invariant Name { expression }` syntax (no `@`). Prose-only invariants use `@invariant Name` (with `@`, no colon). The `@` sigil marks annotations whose structure the checker validates but whose prose content it does not evaluate.
- `@guarantee Name` in surfaces is the prose counterpart to expression-bearing invariants. Same `@` sigil convention.
- `@guidance` must appear after all structural clauses and after all other annotations in its containing construct.
- Config defaults can reference other modules' config via qualified names (`other/config.param`). Expression-form defaults support arithmetic (`base_timeout * 2`).
- `implies` is available in all expression contexts. `a implies b` is `not a or b`, with the lowest boolean precedence.

## Verification

After every edit to a `.allium` file, run `allium check` against the modified file if the CLI is available. Fix any reported issues before presenting the result. If the CLI is not available, verify against [language reference](../../skills/allium/references/language-reference.md).

After edits that change rules, surfaces or transition graphs, run `allium analyse` if available and if the spec meets the criteria in [assessing specs](../../skills/allium/references/assessing-specs.md) (at least one entity has both witnessing rules and surfaces defined). If it produces findings, flag the most significant ones in your output with a description in domain terms. Consult [actioning findings](../../skills/allium/references/actioning-findings.md) for how to translate findings.

## Output

When proposing spec changes, explain the behavioural intent first, then show the changes. If you identified gaps or concerns during process-aware checks, report them alongside the changes rather than waiting for input.
Loading