| name | jobcard-system |
| description | Use when working in the jobcard repository, dispatching agents, writing specs, creating cards, or understanding the card lifecycle. This is a multi-agent task orchestration system — NOT the transit data standard. |
Jobcard System (bop)
Naming — COMPLETE (2026-03-01)
| Role | Name |
|---|
| CLI binary | bop |
| Quick Look bundle | sh.bop.ql |
| Host app bundle | sh.bop |
| Card UTI | sh.bop.card |
| launchd labels | sh.bop.dispatcher, sh.bop.merge-gate |
bop new implement feat-auth
bop status
bop dispatcher --once
bop merge-gate --once
bop inspect <id>
bop approve <id>
bop kill <id>
bop logs <id>
bop poker open <id>
bop poker submit <id> [--glyph 🂻] [--name alice]
bop poker reveal <id>
bop poker consensus <id> --glyph 🂻
bop factory install
bop factory status
bop factory start
bop factory stop
bop factory uninstall
bop icons
bop gantt
bop gantt -w 80
bop gantt --html
bop gantt -o
bop events
bop events --card <id>
bop factory install also sets up the icon watcher alongside dispatcher and merge-gate.
MICROCLAW Principle
Fewer files. Sharper precision. Minimum viable factory.
- Every file has exactly one job
- Agents can participate with just
mkdir + echo '{}' > meta.json
- The FORMAT is the product; the CLI is one thin reference impl
- Like Apache Iceberg (table format, any engine reads/writes)
Core Framing
The filesystem IS the state machine. Playing cards ARE the identity system.
The FORMAT is the product.
mkdir -p .cards/pending/my-task.jobcard
echo '{"id":"my-task","stage":"implement","created":"2026-03-01T00:00:00Z"}' \
> .cards/pending/my-task.jobcard/meta.json
echo "# My Task\nDo X" > .cards/pending/my-task.jobcard/spec.md
Architecture
.cards/
stages/ ← stage instruction files (spec.md, plan.md, implement.md, qa.md)
templates/ ← factory recipes (implement, full, cheap, qa-only)
system_context.md ← prepended to every agent prompt
<team>/<state>/<id>.jobcard/
meta.json ← card identity, stage pipeline, glyph, labels, subtasks
spec.md ← what to build
prompt.md ← rendered from template, sent to agent
logs/ ← stdout.log, stderr.log, pid, lease.json
output/ ← result.md, cards.yaml (child card seeds)
QuickLook/ ← Thumbnail.png for Finder preview
State Machine
pending/ → running/ → done/ → merged/
↓
failed/
Transitions are atomic fs::rename. Dispatcher owns pending/→running/.
Merge-gate owns done/→merged/ (runs acceptance_criteria first).
Filename Glyph Lifecycle (Poker)
Use filename prefix for poker visibility while keeping meta.id canonical.
- Before consensus:
🂠-feat-auth.jobcard
- After consensus:
🂻-feat-auth.jobcard
- Keep
meta.id stable as feat-auth (CLI queries stay stable)
Safe rename flow (filesystem-first, same volume):
- Only rename outside
running/ (prefer pending/; allow done//failed/ for cleanup).
- Hold card lock / ownership gate; assert destination path does not exist.
- Apply atomic rename (
fs::rename).
- If poker consensus finalized a glyph, write
meta.glyph in the same critical section.
- Refresh visual artifacts (
QuickLook/Thumbnail.png + Finder icon).
Stage Pipeline (Factory Engine)
Templates define the stage pipeline via these meta.json fields:
| Field | Type | Example |
|---|
stage_chain | Vec<String> | ["implement", "qa"] |
stage_models | Map<String,String> | {"implement":"opus","qa":"sonnet"} |
stage_providers | Map<String,String> | {"implement":"claude","qa":"codex"} |
stage_budgets | Map<String,u64> | {"implement":32000,"qa":8000} |
When a stage finishes (exit 0), dispatcher reads stage_chain, creates next-stage
child card in pending/ inheriting spec + glyph + prior output. Final stage → done/.
All four fields are on the Meta struct (landed 2026-03-01). render_prompt substitutes
{{stage_instructions}}, {{stage_index}}, {{stage_count}}, {{prior_stage_output}}.
Dispatcher auto-advances stages via maybe_advance_stage().
Stage Instruction Files
.cards/stages/ — one markdown file per stage, ~50 tokens each:
| File | Agent's job |
|---|
spec.md | Write a spec under 500 words. No code. |
plan.md | Read spec, produce ordered steps under 800 words. |
implement.md | Write code, run tests, exit 0 when green. |
qa.md | Review as a different agent. Be skeptical. |
Custom stages: create .cards/stages/<name>.md, add to template's stage_chain.
Context Window Architecture
Every token earns its slot. Dispatcher builds prompts by layering:
system_context.md ~150 tokens domain fix
stages/<stage>.md ~50 tokens stage behavior
card header ~20 tokens id + glyph + stage index
spec.md variable the work
prior_stage_output variable previous stage result
acceptance_criteria ~10 tokens shell gates
Template variables in prompt.md:
{{system_context}} — .cards/system_context.md (WORKS)
{{stage_instructions}} — .cards/stages/<stage>.md (WORKS)
{{stage_index}} / {{stage_count}} — position in chain (WORKS)
{{prior_stage_output}} — previous stage's output/result.md (WORKS)
{{spec}}, {{plan}}, {{acceptance_criteria}}, {{memory}} — WORK
Templates
Live in .cards/templates/. Four shipping templates:
| Template | Stages | Models | Cost | Use When |
|---|
| implement | implement → qa | opus → sonnet | $$ | Clear requirements |
| full | spec → plan → implement → qa | sonnet → sonnet → opus → sonnet | $$$ | Complex/ambiguous |
| cheap | implement | ollama-local | free | Small fix, local model |
| qa-only | qa | sonnet | $ | Code review, audit |
Create your own: cp -c -R .cards/templates/implement.jobcard .cards/templates/mine.jobcard
then edit meta.json (stage_chain, stage_models, stage_providers, stage_budgets).
See plan.md §8 for annotated meta.json reference.
COW-clone: cp -c (macOS APFS) or cp --reflink=auto (Linux).
APFS Copy, Dedupe, Compression (macOS)
- Fast template clone:
cp -c -R (clonefile/COW on APFS).
- Metadata-safe bundle copy:
ditto --clone --extattr --acl --qtn --preserveHFSCompression <src> <dst>.
- Keep default atomic replacement behavior; avoid
ditto --nonAtomicCopies for card bundles.
- Cold-card storage option:
ditto --hfsCompression (or preserve existing compression with default --preserveHFSCompression).
- For archive export/import, use
ditto -c -k / ditto -x -k; prefer staging + atomic move into .cards/.
Filename Tooling Compatibility (macOS 26.3 validation)
With 🂠-feat-auth.jobcard on APFS, these worked in tests:
rg, tail, eza, bat
bop inspect feat-auth
bop logs feat-auth
bop logs -f feat-auth when the card is in running/
exa is no longer a Homebrew formula; standardize on eza.
Filesystem Hardening
-
Dispatcher lock: .cards/.locks/dispatcher.lock/owner.json
- Acquire with atomic
mkdir; reject if live owner PID exists
- Reclaim stale lock (dead PID) and continue
-
Run lease per card: logs/lease.json
- Fields:
run_id, pid, pid_start_time, started_at, heartbeat_at, host
- Dispatcher heartbeats every 5s; stale floor is 30s
-
Orphan reaping: reap when PID dead OR lease stale
- Normalize stale stage metadata (
running → pending/failed)
-
Idempotent controls:
bop kill <id> succeeds even with stale PID
bop retry <id> clears failure reason + normalizes stage
Meta.json Fields
| Field | Meaning |
|---|
glyph | SMP playing card (suit=team, rank=priority) |
token | BMP symbol for terminals/filenames |
stage | Current stage: spec / plan / implement / qa |
stage_chain | Full pipeline: ["implement","qa"] |
stage_models | Model per stage: {"implement":"opus"} |
stage_providers | Adapter per stage: {"implement":"claude"} |
stage_budgets | Token budget per stage: {"implement":32000} |
stages.<s>.status | pending / running / done / blocked / failed |
zellij_session | Zellij session name for running card |
zellij_pane | Zellij pane ID for running card |
worktree_branch | Git branch — never touch main |
acceptance_criteria | Shell commands merge-gate runs |
decision_required | true = merge-gate holds; bop approve clears |
provider_chain | Failover order: ["claude","codex","ollama-local"] |
timeout_seconds | Kill agent after this many seconds |
estimates | Planning poker: {"alice":"🂻","bob":"🂺"} |
poker_round | "open" / "revealed" / absent |
labels | [{"name":"High Impact","kind":"effort"}] |
subtasks | [{"id":"s1","title":"Do X","done":false}] |
progress | 0–100 overall completion % |
Adapter Contract
adapter.zsh <workdir> <prompt_file> <stdout_log> <stderr_log> [timeout]
| Exit | Meaning | Action |
|---|
| 0 | Success | → done/ |
| 75 | Rate-limited | → pending/, rotate provider |
| other | Failure | → failed/ |
Available: claude.zsh, codex.zsh, ollama-local.zsh, goose.zsh, aider.zsh, opencode.zsh, mock.zsh.
Planning Poker
Suit → perspective: ♠=complexity ♥=effort ♦=risk ♣=value.
Rank → magnitude: Ace=1, 2-10=face, Jack=13, Queen=21, King=40.
Joker (🃟/🃏) → needs breakdown, blocks consensus.
Scripts
| Script | Purpose |
|---|
scripts/land_safe.zsh | JJ/git safe-landing with gate checks |
scripts/bop_bop.zsh | Goal→card→Zellij session bootstrap |
scripts/bop_focus.zsh | Navigate Zellij panes by card id |
scripts/macos_cards_maintenance.zsh | Thumbnail refresh, cold card compression |
scripts/render_card_thumbnail.swift | Generate QuickLook/Thumbnail.png |
scripts/set_card_icon.swift | Stage-coloured Finder icons for .jobcard bundles |
All zsh scripts use ROOT=${0:A:h:h}. setopt NULL_GLOB for .jobcard globs.
Never use done as a variable (zsh reserved — use n_done).
Quick Look / macOS
- Xcode project:
macos/macos.xcodeproj
- Extension:
macos/bop/PreviewViewController.swift
- Host app:
macos/JobCardHost/ (registers bop:// URL scheme)
- Build: Xcode → Product → Build
- Register:
lsregister -f ~/Applications/JobCardHost.app
- Bundle IDs:
sh.bop (host), sh.bop.ql (extension), sh.bop.card (UTI)
Three Steering Layers
Every dispatched agent is shaped by (inner wins):
- Skills (
~/.claude/skills/) — HOW to approach work
- CLAUDE.md — project constraints, shell rules, architecture
system_context.md — runtime domain fix ("NOT GTFS transit data")