Working on roster itself
Layout, template classes, and the rules that matter.
git clone <the framework> && cd roster && npm install
npm test # the whole suite
npx tsc --noEmit # typecheckTests are node:test run through tsx. There is no build step for development, and the portal
has no build step at all.
Layout
src/cli.ts command table and help
src/commands/ one file per command
src/lib/ shared: workspace, memory, render, merge, gh, templates, docs
templates/ops/ what a tenant's ops repo is generated from
templates/brain/ what a staff member's repo is generated from
templates/portal/ the portal: a shell, css/, and js/ as ES modules
docs/ these pages
test/ one file per areaThe portal has no build step, on purpose
templates/portal/ is a plain index.html, one stylesheet per area under css/, and one ES
module per screen under js/views/. roster portal serves them from /assets, reading each
file per request. Editing a stylesheet and reloading the page is the whole edit loop.
No bundler, because the browser resolves the module graph itself, and a bundler would be a build step in a tool whose selling point is that it does not have one.
templates/portal/
index.html the shell: <link>s, the sidebar's bones, one <script type="module">
css/base.css tokens, reset, buttons, inputs, chips
css/layout.css the frame: sidebar, main, tree-beside-viewer
css/<area>.css markdown, brain, inbox, graph, diff, health
js/state.js every screen's state in one object, and the URL that mirrors it
js/router.js one indirection so a view can repaint without importing the shell
js/dom.js api.js md.js refresh.js icons.js
js/app.js boot, the sidebar, dispatching a view into <main>
js/views/*.js one per screenjs/icons.js holds two dozen Lucide glyphs as inlined path data, ISC
licensed. Vendored rather than fetched, because the portal is offline-first and an icon that
needs the network is an icon that is sometimes a blank square. They take colour from
currentColor and size from a --ic custom property, so a component styles them in CSS and
never touches the SVG. Nothing in the UI should draw with a text character.
Two rules keep the module graph a tree rather than a ring: state lives in state.js, and
a view never imports app.js. It calls render() from router.js, which the shell
registers into at boot.
The rule that matters
Never fix a generated file in a tenant. compose.mjs, agents.mjs, runner-plan.mjs,
inflight.mjs, run-record.mjs and session.yaml live in templates/ops/. Fix them there and run roster upgrade.
A fix made in the tenant's copy goes unnoticed until the framework next touches that file, and
then it is a conflict. So roster upgrade reports an edit to a framework-owned file whether or
not anything has collided, and roster upgrade --check fails on it.
Template classes
Ownership decides what roster upgrade does, and getting it wrong is how you either lose
somebody's work or never ship an improvement.
| Where | Class | Upgrade behaviour |
|---|---|---|
templates/ops/org/, prompts/ |
seeded |
three-way merge |
templates/ops/ everything else |
managed |
reported even when nothing collided |
templates/brain/.github/workflows/ |
generated |
regenerated wholesale |
templates/brain/ everything else |
scaffold |
added when new, never rewritten |
scaffold is the important one. A working agent rewrites CHARTER.md and memory/INDEX.md
beyond recognition, and merging a template into that would be vandalism.
Tokens in brain templates
templates/brain/ is rendered with %%TOKEN%% substitution, including filenames, which is
why the callers are %%STAFF%%-daily.yaml.
An unfilled token throws rather than being left on the page. A workflow containing a literal
%%SCHEDULE%% is a file GitHub accepts and never runs.
A line containing %%TOKENS%% is a note to whoever reads the template and is dropped on
render, along with the # separator above it. Every line of a multi-line note needs the
marker, or the leftover line is stranded.
How the tests are meant to work
Three habits, each of which guards against a test that passes without testing anything.
Test the harness, not just the code. The portal's state is an exported object, so the tests import the real modules under a DOM shim and drive the same state the page does. A test that sets a property on something the code never reads cannot fail.
Run it against reality when you can. The suite builds a temporary tenant by default, so it
runs for anybody who clones the repo. Set ROSTER_TEST_WORKSPACE=<dir> to run the portal and
doctor tests against a real workspace instead, which is how you catch real data growing a shape
the code cannot handle: templated filenames, for instance, that a comparison forgot to render.
Mutation-test the invariants. For anything asserting "this behaviour must not regress", break it deliberately and check the test fails. If it does not, the assertion is decoration.
Adding a command
src/commands/<name>.ts, exporting<name>Commandand<name>Help.- Add both to the tables in
src/cli.ts. - Document it in
docs/commands.md. A test fails if you do not, and another fails if the flags you document are not ones the parser accepts.
Prefer plan-then-apply. Every command that changes anything prints what it would do and needs
--apply, and that is the convention that makes them safe to run in someone else's org.
Adding an agent preset
templates/ops/agents.mjs. Three fields: install, run, token_env. The run command reads
the prompt from $AGENT_PROMPT_FILE.
Read the flags off the tool's own help or docs. Every preset that ships was verified that
way, and the tests say which flag exists for which reason. docs/agents.md has to quote the
same install line, and a test checks it does.
Before pushing anything that touches a live org
roster prompt cto --kind daily # composed prompt, byte for byte
roster upgrade # what would change, and where
roster doctor # is it still wired upA prompt change with no visible diff in the composed output is the only kind that is safe.