8.6 KiB
Porting Allium to Pi.dev: Keeping Upstream by Reference
The below (from 'As the agent said')was generated by a coding agent, after implementation. The agent has not picked up on that we could replicate the behaviour of claude code hooks inside a Pi extension. I didn't go for that, yet, as I wanted to make it work, then make it right.
There are a couple of third party skills I want to take with me from Claude Code to Pi. Anything markdown and scripts is a matter of moving things around, as Pi supports the agents skill standard.
Plugins with agents are a bit different. The mapping is not one-on-one, and that is by design. Pi follows the unix philosophy of small, composable tools, Pi itself being one - invoke with Pi -p or --json for non-interactive use. The interactive prompt with its' fast /reload feels more like a Lisp REPL or Smalltalk workspace.
I come from Claude Code. I use it in a variety of ways. Sometimes quickly building out a prototype in as close to production quality as possible, taking bigger steps after iterative brainstorming. Sometimes in-the-loop with short cycles when I want something small and focused, or am exploring how a particular flow with agents or llms' works.
Surprisingly Allium adds value for both cases. Allium to me is conversational formal verification. The conversation can be more detailed, while staying playful and iterative. I can generate tests, or take an implementation, and have it find gaps.
Read on for a brief explanation of a gap found in pi-turn-limit. I made pi-turn-limit exploring how far /reload and a local model would go in making an extensoin. Allium + the same local model found a gap, and helped make a new feature with tests. Directed by claude code. That non-interactive mode of Pi makes iterating in a loop easy.
Enjoy,
Willem van en Ende
P.S. I see I need to rename the skills. Will probably be skill:allium-* instead of skill:*
As the agent said...
(agent was Pi with Qwen3.6-27b)
How to adapt an AI agent skill framework for a new platform without forking the source.
Allium is a formal language for specifying software behavior at the domain level. It sits between informal feature descriptions and implementation code, giving AI agents something more structured to work with than raw Markdown prompts.
Allium ships as a Claude Code plugin with six skills: elicit, distill, propagate, tend, and weed (plus a root skill for routing). Each skill is a SKILL.md file with frontmatter and a body of instructions, supported by reference documents.
When I wanted to use Allium with pi.dev, I faced a choice: fork the entire repository and maintain a parallel copy, or find a way to reference the upstream content directly. I chose the latter.
The symlink strategy
The key insight: most of Allium's content is universal — the language reference, the elicitation methodology, the distillation workflow. What's platform-specific is the frontmatter (metadata format) and the invocation paths (/allium:elicit vs /skill:elicit).
So the port structure is:
.pi/skills/elicit/
├── SKILL.md ← thin adapter: pi frontmatter + invocation paths
└── references/ ← symlinks to upstream
├── language-reference.md → ../../../../allium-main/references/language-reference.md
└── library-spec-signals.md → ../../../../allium-main/skills/elicit/references/library-spec-signals.md
The SKILL.md files are thin adapters. They change the YAML frontmatter from Claude Code format to pi.dev format, update skill invocation references, and include a few pi-specific instructions. Everything else flows through symlinks.
Result: when upstream Allium updates, I run git pull in allium-main/ and the port is current. No merge conflicts, no duplication, no drift.
What changed
Frontmatter
Claude Code plugins use name, description, version, and auto_trigger. Pi.dev skills use name, description, disable-model-invocation, license, and metadata:
---
name: elicit
description: Elicit requirements and design decisions
disable-model-invocation: true
license: MIT
metadata:
upstream: https://github.com/juxt/allium
version: 3
---
Invocation paths
Every /allium:X becomes /skill:X. The routing table in the root skill maps tasks to the correct skill:
| Task | Claude Code | Pi.dev |
|---|---|---|
| Root | /allium |
/skill:allium |
| Elicit | /allium:elicit |
/skill:elicit |
| Distill | /allium:distill |
/skill:distill |
| Propagate | /allium:propagate |
/skill:propagate |
Agents become skills
Allium's tend and weed are Claude Code agents — they specify a model (Opus) and tool permissions (Read, Glob, Grep, Edit, Write, Bash). Pi.dev doesn't have native agent support with model selection or tool scoping.
The pragmatic solution: port them as regular skills with disable-model-invocation: true. They run on whatever model you're using, with whatever tools are available. The methodology is the same; only the enforcement differs.
Rules become references
Claude Code's .claude/rules/allium.md contains syntax rules and anti-patterns that activate on glob patterns. Pi.dev has no glob-based rule activation.
Solution: symlink the rules file into references/allium-rules.md and add a "Syntax rules" section to each skill instructing the model to read it before writing .allium files. Same content, different delivery.
Hooks become instructions
The Claude Code version runs allium-check.mjs as a PostToolUse hook — every time the model writes or edits a file, validation runs automatically. Pi.dev has no equivalent hook system.
Solution: add a "Verification" section to skills that write .allium files, instructing the model to run allium check <file> after writing. It's not automatic, but it's explicit.
Testing with a real project
I tested the port against pi-turn-limit, a pi.dev extension that limits conversation turns.
The workflow:
- Distill (
/skill:distill) — extracted a.alliumspec from existing TypeScript code. The spec captured turn counting, enable/disable, config persistence, and UI separation. - Propagate (
/skill:propagate) — generated 30 test obligations: 10 unit tests for pure functions, 8 config/command tests, 3 entity state tests, and 5 rule tests. - Elicit (
/skill:elicit) — explored a new "disable turn limit" feature. Three design decisions emerged: unlimited means no boundary check, hard reset on re-enable, and config value is not entity state. - TDD cycle — wrote 8 new tests from propagate output (red: 8 failing), implemented unlimited mode, ran tests (green: 29/29 passing).
The ported skills produced the same quality of output as the Claude Code originals.
Trade-offs
The symlink strategy works well but has trade-offs:
| Advantage | Trade-off |
|---|---|
| Zero content duplication | Symlinks must resolve — repo structure matters |
| Automatic upstream sync | No automatic sync — you still need to git pull |
| Minimal maintenance surface | Platform gaps (agents, hooks, rules) require workarounds |
| Clear attribution | Upstream content remains under JUXT's copyright |
Known limitations
The port doesn't replicate three Claude Code features (yet):
- Model selection — tend/weed run on your default model, not Opus
- Tool scoping — skills can use any available tool, not a restricted set
- Automatic validation —
allium checkis instructed, not enforced
Conclusion
The symlink-based port pattern works well for adapting AI agent skills across platforms when:
- The core methodology is platform-agnostic
- Platform differences are mostly in metadata and invocation
- You want to stay in sync with upstream without forking
It's not a perfect solution — the agent and hook workarounds are manual. But it's maintainable, and it produces results.
The port is available at github.com/.../pi-allium-port (link TBD).
Allium is developed by JUXT Ltd and licensed under MIT. This port adapts Allium for pi.dev while keeping upstream content by reference.