Design choices¶
The choices below shape most changes to this repo. Each one names what it costs, and links
the decision record that argues it in full. The records live under docs/adr/. A choice
that was later reversed links the record that reversed it.
One plugin, updated as a whole¶
Every skill ships in one plugin, lab, from one marketplace, liulab. The skills lean on
each other: lab-jupyter takes a job line word for word from lab-containers, and both
defer to lab-hpc for the safety rules. A machine running two skills at two different
versions is a mix nobody tests. That mix fails as a wrong command on a real cluster, which is
the worst place to find out.
One plugin makes an update all or nothing. The cost is that a lab member who wants one skill installs all of them, and every skill's description takes a little of every agent's context. That pressure is why descriptions stay short and dense with trigger words.
Read the record: The repo root is the plugin, and one plugin holds every skill.
A version names one tree¶
Every change merged to main bumps the version, even a change to a test or a comment. The
whole tree ships, so two different trees must never answer to one number. tests/lint.sh
fails a tagged version whose files have moved since the tag.
flowchart LR
merge["A change merges to main"] --> ver["A new version,<br/>tagged on main after the merge"]
ver --> cc["Claude Code:<br/>autoUpdate on the marketplace"]
ver --> codex["Codex:<br/>upgrades at startup"]
ver --> kimi["Kimi Code:<br/>reads Claude Code's clone"]
Versions are calendar dates, YEAR.MONTH.PATCH. What ships is prose an agent reads, with no
API to break, so the only thing a version needs to say is how old a copy is.
Installed copies update on their own. Releasing is pushing, and a fixed safety rule reaches every machine at its next launch. So does a bad release, with nobody choosing to take it.
Read the records: The version names one tree, CalVer, and Installed copies update on their own, which reversed the manual-update half of the CalVer record.
Facts and procedures live apart¶
New content goes where its kind says, not where the person asking for it happens to work.
flowchart LR
q{"What are you<br/>adding?"}
q -->|"how a cluster behaves"| a["skills/lab-hpc/references/<br/>checked on a compute node, dated"]
q -->|"how a vendor platform behaves"| b["the owning skill's references/<br/>under a provenance block"]
q -->|"a repeatable procedure"| c["its own lab-* skill,<br/>pointing at the facts"]
q -->|"a one-line tip"| d["a bullet in lab-hpc"]
q -->|"what a word means"| e["GLOSSARY.md"]
q -->|"a hard-to-reverse decision"| f["docs/adr/"]
q -->|"open work"| g["a GitHub issue"]
A fact copied into two skills goes stale in one of them, and no check can tell which copy is
right. On a cluster that is not a stale sentence. It is an agent typing squeue --me on a
Slurm version that lacks it.
Two rules keep facts honest:
- Record configuration, never measurements. Configuration is what an agent must type: a
partition's time limit, a port, a module name. A measurement is free disk space, a queue
wait or a node count. It is true for one afternoon, and the agent cannot tell when it went
stale. Write the guidance and the command that gives a live value instead: "
/large_storagefills up, checkdf -h", not "81% used". - Facts carry their source. A vendor fact cannot be checked again for free, so each
lab-edisonreference page opens with a provenance block. The block gives its source, the date it was checked, and one of three tiers. Verified means we ran it. Read means it came from the package source or a vendor page. Unverified means it was inferred. Lint fails a page without one.
Read the records: Facts versus recipes, Jupyter and containers split at the job line, and Facts carry their provenance.
No secrets, by construction¶
The repo is public, so it holds no connection details at all. Skills name hosts only by the lab's ssh alias names, and read everything else from the user's machine when they run.
flowchart LR
subgraph pub["Public repo"]
skill["Skill text<br/>alias names only:<br/>arc, ircbc, ..."]
end
subgraph mine["The user's machine, never committed"]
direction TB
ssh["~/.ssh/config<br/>hosts, usernames, keys"]
pers["~/.claude/lab/personal.md<br/>folders, ports, job defaults"]
key["~/.claude/lab/edison.env<br/>the Edison key"]
end
skill -->|"resolves aliases at run time"| ssh
skill -->|"reads into the agent's context"| pers
skill -->|"edison-cli sources it into<br/>its process, never into context"| key
tests/lint.sh sweeps every file for a leaked address, hostname or username. It reads your
own ssh config to know which names to look for, so the sweep is complete only on a machine
with the lab's ssh aliases. CI cannot run it in full, which is why you run the gate before you push.
The Edison key gets a file of its own because personal.md is read into the agent's
context. That is its job. A key written there would land in the transcript of every session.
The key file is sourced by a shell instead, so the key reaches the client without ever
appearing as text.
Read the record: The Edison key gets its own file.
Rules become structure¶
A rule an agent must remember at the moment it matters fails sooner or later. Where a rule guards something costly, the repo turns it into something that runs.
- Preflights refuse.
lab-hpcchecks the machine's ssh setup before any cluster work, and refuses on a machine that is not set up rather than asking for an address. - Spending goes through a command.
lab-edisononce taught an agent to submit a task, print its id, and only then poll. A real session lost a paid run's id anyway, and it had to be dug out of the task history. Nowedison-clidoes it in order:
flowchart TB
typed["A person types /lab-edison"] --> pre["edison-cli runs its preflight: no key file, no spend"]
pre --> file["The question goes into a file, which the person sees in full"]
file --> submit["Submit from that file: the task id is the first line out"]
submit --> poll["Poll, then fetch the answer"]
- Lint checks the conventions. Names, version tags, eval case lists and the no-secrets
sweep are all checks in
tests/lint.sh, not reminders in a document. - Some steps always wait for a person.
hq-mode's always-pause list, such as submitting a Slurm job or merging an unreviewed pull request, outranks anything said in the session, "land it" included.
Read the records: The Edison skill ships its spend path and The spend path became a package.
Two kinds of page¶
Every page is written either for an agent that has to act, or for a person reading this site. The gate checks each kind for different things.
| For agents | For people | |
|---|---|---|
| Files | AGENTS.md, every SKILL.md and references/ page, docs/adr/, docs/agents/ |
README.md, CHANGELOG.md, the pages in this site's menu |
| Checked for | A word cap for each kind of file | No jargon from the lab's list, and a reading grade of 11 or below |
| On this site | Out of the menu and search, still reachable by URL | In the menu and search |
A cap that fires means the text belongs somewhere else, such as a references/ page, not that
the cap should rise. Upstream text in an adopted skill is exempt, so it passes unchanged. docs/agents/writing.md has the caps
and how to measure against them.
Upstream skills keep upstream text¶
The lab takes general engineering skills from mattpocock/skills and pstack instead of writing its own. A tracked skill only ever gains text, and keeps its upstream name, so each sync merges cleanly. Skills from mattpocock and pstack has the whole story.
The docs site runs on an alpha¶
This site is built by zensical, an early 0.0.x builder from the makers of Material for
MkDocs. The lab wants every repo on one builder, and this repo is the cheapest place to try
it: a few hand-written pages, nothing generated. The pin is exact, and moving back to mkdocs
is a short change to pyproject.toml and mkdocs.yml.
Read the record: The docs site is built by zensical.