| name | mdex-software-factory |
| description | Operate the MULTI/DEX software factory - a filesystem task queue at tasks/queue with atomic claims, priority-by-rename, operator signal files, and session roles, where every consumer seat is a CLI executor session in its own git worktree with its own isolated local venue, plus a dedicated operator seat for operator-directed work. Ingest when told "you are a TaskConsumer / TaskPrioritizer / TaskProducer / the OperatorTasks console", when booting or taking over a factory seat, and whenever filing follow-up work into the queue. |
| metadata | {"title":"The software factory","category":"Operations"} |
Skill: the MULTI/DEX software factory
Several Claude sessions work one shared task queue continuously: TaskConsumers
(typically 3-5, each a CLI executor session in its own git worktree with its
OWN local venue - the worker doctrine below) claim and execute tasks; one
TaskPrioritizer reorders the queue and keeps the board; TaskProducers
(ad hoc) turn instructions, audits, and community issues into tasks; and one
standing operator seat hosts operator-directed work. The operator
(Dominic) boots sessions and is the escalation path. There is no message bus
and no coordinator process: the queue directory IS the coordination, and every
mutation is an atomic same-filesystem rename, so races have exactly one winner
and losers retry. The operator's control channel to a running seat is a
FILE too - tasks/signal/ (Operator signals, below) - not a chat message.
Three things make this factory different from a generic one, and they are all
load-bearing:
- This machine hosts live systems. Long-lived bot fleets drive the LIVE
subnet at multidex.ai and the cloud engine, and the primary checkout's
:8000 local network is the operator's standing demo venue. Seat isolation
(own worktree, own
ICP_HOME, own OS-assigned port) and the
mdex-process-safety skill are how the factory coexists with all of that.
- Nothing is ever pushed. Merges land on the primary checkout's
main
LOCALLY. Pushing to origin or the public mirror is the operator's act, on
explicit instruction only, sequenced by the outward-batch policy - the
factory has no push step anywhere.
- Task production stays as it is today.
docs/tasks/ (tracked) remains
the authored task corpus - W-style briefs, done/, the README queue table.
The factory adds a RUNTIME queue (tasks/, gitignored) that carries
execution state: claims, priorities, dependencies, the board.
scripts/factory.sh is the ONLY way to touch the queue. It works from the
primary checkout or any worktree (it resolves the queue root to the PRIMARY
checkout's tasks/ either way - git worktree list puts the primary first).
Run scripts/factory.sh help for the command table;
scripts/factory-selftest.sh exercises it against a throwaway queue.
The queue on disk
tasks/queue/ pending tasks, one md file each: PPPPPPPPPP-IIIIIIIIII.md
tasks/claimed/<worker>/ in-flight tasks, one subdir per consumer
tasks/archive/ terminal tasks: <id>.md (status stamped inside)
tasks/factory/ coordination surface: index.md (the board), merge.lock/,
.ids/ (the append-only id allocator - never touch, never clean)
tasks/handoff/ one <worktree-basename>.md per seat waiting for a successor
session: the boot prompt, on disk, where an app restart
cannot reach it (`factory.sh handoff-list`)
tasks/signal/ operator ORDERS to a seat: <worker>.md, plus all.md (the
broadcast). Wakes every `watch`, blocks that seat's next
`claim` until acknowledged (Operator signals, below)
All six are gitignored runtime state. The tracked corpus in docs/tasks/ is
unaffected and keeps its current conventions exactly. One further piece of
runtime state lives outside tasks/: each seat's <worktree>/.factory-worker
(gitignored too), the one-line identity pin factory.sh reads instead of
guessing a worker from the cwd's branch - see the TaskConsumer setup.
Naming. P (10 digits) is the priority key; I (10 digits) is the
immutable id. Both start as the UTC epoch seconds of creation, so an untouched
queue lists in FIFO order and ls tasks/queue IS the execution order
(ascending). The prioritizer renames ONLY the P half. The id is the task's
identity forever - in after: dependencies, in archive filenames, in board
notes - and the allocator guarantees it is never minted twice.
Lifecycle. queue/ -> claimed/<worker>/ -> archive/ - claim and archive
are mvs performed by factory.sh; there are no other states. A task in
queue/ is claimable by definition; if it must not run yet, that is expressed
by after: dependencies or a 90-band parking, not by side agreements.
The two layers. A queue task either inlines its whole brief (small,
self-contained work), or names a tracked brief with docfile: docs/tasks/<file>.md plus a one-paragraph gist. The brief rides git, so every
worktree sees it and law 6 (self-containedness) is satisfied transitively.
Closing a docfile: task INCLUDES the corpus bookkeeping, on your branch so
it rides your merge: move the brief to docs/tasks/done/ with the
**done <date>**: ... completion note in the current style, and strike its
row in docs/tasks/README.md (~~[W..]~~ ... — **done <date>**: ...).
factory.sh done refuses to archive while the brief still sits at its
original path - the bookkeeping law, mechanical so forgetting is
impossible. Each seat edits only its OWN task's README row; rows are one line
each precisely so parallel strikes merge clean.
The operator lane. lane: operator in a task's front matter means the
task is WAITING ON THE OPERATOR and no consumer seat may claim it. It holds
two shapes of work: operator-EXECUTED tasks (engine/subnet rollouts, pushes,
anything needing human-held auth - law 8), which the operator resolves with
done; and operator-INPUT tasks (a decision, credential, or clarification a
consumer needs), which pass THROUGH the lane - the answer is embedded in the
task file and it is released back to the consumer queue (the OperatorTasks
role below). factory.sh claim skips lane tasks entirely, including as a
last-resort fallback, so a seat gets an itemized EMPTY rather than forbidden
work; factory.sh claim --operator claims only them. list --operator /
list --consumer filter, and the board gives them their own section - being
SEEN at rollout time is the point of the lane, not being worked. This is a
different axis from priority: band 90 only DELAYS a claim, and on a contended
queue "nothing else remains" arrives routinely; a task no consumer may run
must be unclaimable, not merely parked.
The laws (every role)
- Only
factory.sh mutates queue state. Never mv/rm/Write task files by
hand, and never operate on a worktree's own tasks/ copy.
- The id half of a filename never changes. Only the prioritizer renames the
priority half, only via
reprioritize, only for QUEUED tasks.
- A claimed task belongs to its worker: nobody else edits or moves it. The
prioritizer flags stale claims on the board but never requeues them itself -
that is the operator's call (or the owning session's, via
requeue).
- The prioritizer never edits task content, never claims, and never touches
git history or refs (no commits, merges, checkouts). Its only git contact
is the janitor's read-only liveness look at a stale claim's worktree.
- Nobody DEVELOPS in the primary checkout. It hosts four things only: the
runtime queue, the canonical
:8000 local venue (the operator's - see
"The operator seat"), the untracked rollout config
(scripts/.subnet.conf, scripts/.cloud-engine.conf, the API-key files),
and merges performed under the merge lock. Consumers' only writes there
are through factory.sh and that final merge; operator-directed
development happens on the operator seat, not in the primary.
- Every task is self-contained: a session with no memory of the conversation
that produced it can act on it (a
docfile: brief counts - it is tracked
in every worktree). If it is not, it is not a task yet.
- Follow-up work any session discovers is filed as a queue task - never a
suggested-task chip, never a note in some doc.
- Consumer seats never touch a remote target and never push. Concretely
out of bounds:
scripts/deploy.sh engine|subnet, deploy_to_engine.sh,
deploy_to_subnet.sh, start_bots_*/stop_bots_* for engine or subnet,
topup*.sh against remote targets, anything that reads
scripts/.subnet.conf or scripts/.cloud-engine.conf, and git push
to ANY remote (origin and the public mirror are operator acts under the
outward-batch policy). Live verification happens on the seat's OWN local
venue. Remote rollout is a different trajectory with its own human-held
auth - icp reauth is a web flow that WEDGES in a non-interactive
session (piped output swallows its URL and the command parks forever),
so a consumer that tries either blocks for hours or, worse, an expired
delegation fakes success. When a task's remote part matters: do the
local part, note the remote part as DEFERRED in the completion note, and
file a follow-up task carrying lane: operator. Sole exception: the
task text itself explicitly makes a remote deploy the deliverable - and
such a task is operator-lane by construction.
- Nobody edits a QUEUED task file in place - not even its producer. To
change task content, own it first: consumers edit only their claimed
files; the operator takes a specific task with
claim --operator --id <id>, edits, and requeues. An in-place Write
racing a claim recreates the moved file as a ghost duplicate of a task
someone else now owns.
mdex-process-safety is binding in every role. No pattern kills
(pkill -f took down the live subnet fleet three times), no pgrep-based
waits (they match the waiting shell itself), kill only PIDs you captured
or via the repo's stop_bots_* machinery, never leave an
icp network status poll unattended without a kill-after watchdog, and
EVERY icp invocation carries --identity <name> - never
icp identity default, whose global state other sessions and connectors
mutate mid-run. Read that skill before your first process or icp
operation; it is short and it is the machine's #1 hazard list.
- The posture law. A branch merges into main only on the committed
defaults:
src/backend/main.mo AND src/bridge/main.mo resolve to
#play (read by mdx_posture_of, the one comment-aware reader), and
icp.yaml carries no port: 0 line. factory.sh merge-lock acquire <branch> enforces it mechanically. The seat-local #dev flip and the
port-0 gateway edit are legitimate WORKING-TREE state (that is how a
seat's venue runs); the law is what lets them exist without ever
reaching main.
Task file format
---
producer: <who filed it: branch, "dominic", "audit-2026-08-17", ...>
class: security-high | security-medium | architecture | prerequisite |
feature | hygiene | docs | parked (advisory - the prioritizer's input)
touches: backend, tests (see the vocabulary below; "suite" = repo-wide)
docfile: docs/tasks/W4-23-clamp-amm-half-spread.md (the tracked brief, when one exists)
after: 1785709403, 1785709404 (ids that must archive first)
split_from: 1785709400 (lineage, when split)
lane: operator (waiting on the operator - execution OR input)
---
# <imperative title - what will have happened when this is done>
Goal, context, and exact pointers: paths, file:line, the shape of the change,
and HOW TO VERIFY (which suites, which gates, what to look at). Out-of-scope
notes if the edges are temptingly fuzzy. With a docfile: a one-paragraph gist
plus anything the brief lacks (the brief carries the details).
## Factory log <- appended by factory.sh; never write this yourself
Only the # title line is mandatory (factory.sh refuses drafts without one);
class and touches should almost always be present, docfile whenever a
tracked brief exists, after whenever order matters, and lane: operator
whenever law 8 puts the work out of a consumer's reach. Fields are greppable
plain lines - keep them single-line, comma lists.
touches: vocabulary (the affinity and overlap currency - use these,
not ad hoc strings): backend (src/backend), bridge (src/bridge), arb
(src/arb), archive (ArchiveCanister + archive design), frontend
(src/frontend), verifier (scripts/verify_ledger.mjs + its fixtures),
deploy (scripts/), tests (tests/), docs (docs/), candid, and suite
for repo-wide waves.
Priority bands
The prioritizer expresses policy by renaming into bands: new priority =
BB * 10^8 + (id mod 10^8) - factory.sh reprioritize <id> <BB> computes
this - which preserves FIFO order among tasks in the same band.
| Band | Meaning |
|---|
| 00 | emergency - drop everything (operator-directed, mostly) |
| 01 | security high - live venue / pipeline safety (the W1 shape) |
| 02 | security medium - tape & verifier integrity, #production hardening (W2/W3 shapes) |
| 03 | architecture / doctrine changes that would force rework of tasks landing after them |
| 04 | prerequisites - tasks other queued tasks name in after: |
| 05-09 | expedited, prioritizer's discretion |
| 17-21 | the natural zone: untouched creation keys (epoch seconds land here through 2036) = plain FIFO |
| 30 | hygiene, gate/instrument debt, low-severity findings (the W5 shape) |
| 40 | nice-to-have, cosmetic, docs polish |
| 90 | parked - claim only when nothing else remains |
Rationale in one breath: security before features so it is baked in, not
bolted on; the tape and the verifier are REQUIREMENTS under the transparency
doctrine, not features - rank them accordingly; architecture before the work
it would rework; prerequisites before their dependents; everything else FIFO.
Band 90 is not a way to say "never": it delays a claim, it does not refuse
one. Work a consumer must NOT run belongs in the operator lane
(lane: operator), a separate axis - a task can be band 01 and operator-lane
at once.
Role: TaskProducer
- Collect the work: the operator's instruction, an audit round, community
issues, or existing findings. One task per independently completable item.
- Dedupe first:
scripts/factory.sh list, the board
(tasks/factory/index.md), a grep of tasks/archive/, AND the corpus -
docs/tasks/README.md's queue table plus docs/tasks/done/. Extend or
skip rather than duplicate; the "Already resolved - do not redo" table in
the README exists because reporters re-report fixed things.
- Author to the current corpus standard. Substantial work gets a W-style
brief in
docs/tasks/ exactly as today (verified against the working
tree by symbol lookup, citations, the how-to-verify section, pushback
recorded where a reporter is wrong) and a THIN queue task pointing at it
via docfile:. Small self-contained items inline everything in the queue
task. Either way the self-containedness bar is law 6, and sources are
cited (file:line, issue numbers, commit shas).
scripts/factory.sh new <draft> - it allocates the id and prints the
queue path. Draft in your scratchpad; the draft file stays where it was,
inert. (New briefs in docs/tasks/ are ordinary tracked edits - commit
them through the normal branch flow, not by writing into the primary.)
File in dependency order: an after: id must be one new has
ALREADY printed (every filing prints its id) - never a guessed or
predicted one. new refuses a draft whose after: names an id that
was never minted, because a nonexistent gate gates nothing: claim
treats unknown ids as satisfied (live-ids-only, deliberately - in the
open-saas factory five guessed-id gates once let four "sequential"
tasks run concurrently). File prerequisites first, then the dependents
citing the printed ids.
- Report the filed ids/titles. Do not set priorities - that is the
prioritizer's job; your
class: field is its input. (Genuine
emergencies: say so to the operator rather than guessing a band.)
Set lane: operator on anything a consumer seat is forbidden to execute -
engine/subnet rollouts, pushes, anything needing human-held auth or a
decision only Dominic can make. That is a statement of fact about the work,
not a priority call, so it IS yours to set. Leaving it off does not merely
mis-sort the task: seats will claim it, discover they must not act, and
requeue it, indefinitely.
Seeding the queue from the corpus: the standing instruction "work the
docs/tasks queue" translates to one thin docfile: task per open row of
docs/tasks/README.md, skipping rows marked IN FLIGHT in another session,
with after: wiring where a brief names an ordering. Do this once at factory
boot and again whenever a new triage round lands in the corpus.
Role: TaskPrioritizer (exactly one session)
Loop forever: wake -> triage -> reorder -> board -> janitor -> re-arm -
and keep this skill fresh MECHANICALLY, not by feel: every pass, stat the
primary copy
(stat -f %m <primary>/.claude/skills/mdex-software-factory/SKILL.md - one
token-free shell call, foldable into the same Bash call as your watch);
RE-READ the file whenever that mtime moved past your last read, and in any
case at least once an hour. The mtime gate means a protocol change reaches
you within one pass; the hourly floor exists because even an unchanged skill
needs occasional re-reading - summarization quietly erodes the copy in your
context.
- Wake:
scripts/factory.sh watch 500 blocks until
queue/claimed/archive/signal change (prints CHANGED) or times out
(TIMEOUT) - act on either, then re-arm. In a harness with background
tasks, run watch 3600 via a background Bash call and you will be
re-invoked on completion; otherwise call it in the foreground repeatedly.
Either way each pass costs nothing when idle.
- Triage:
factory.sh list; read (factory.sh show <id>) tasks you have
not seen before (the board is your memory of what you have triaged). Judge
class and content per the band table - and check after: chains: a
dependent must sort behind its prerequisites (band 04 the prerequisites
forward rather than parking the dependent, when both matter).
- Reorder:
factory.sh reprioritize <id> <band>. If it errors, the task
was claimed mid-rename - that is fine, drop it. Do not churn: rename only
when the current position is actually wrong.
- Rank bands, not tasks - keep the head band BROAD. A band is a statement
that its members are equally important, and that interchangeability is what
consumer affinity (
claim --affinity) converts into warm-context
throughput: a seat may take any same-band task it knows the ground for,
never anything past the band. A head band holding one task disables
affinity exactly where most claims happen. So when you promote several
tasks of comparable urgency, put them in the SAME band (within-band FIFO is
preserved by the formula); split finer only when one genuinely must precede
another - and a true must-precede is usually an after: dependency, not a
band distinction.
- Board:
factory.sh index regenerates tasks/factory/index.md - the
operator's one-glance view. Refresh it every pass. Its Seats awaiting
successor section lists tasks/handoff/ - seats that handed off and are
sitting unmanned. Call any entry older than an hour out in your pass
summary: a shrinking fleet is invisible otherwise (a CLI seat's
self-spawn can fail; a desktop chip can die with an app restart). Its
Operator signals section lists unacknowledged orders - each line is a
seat that CANNOT claim until it acknowledges, so an old entry means a
stopped or dead seat: call those out too.
- Janitor: the index marks claims idle >4h as STALE. Investigate liveness
read-only (is the worktree still there? recent commits?
git -C <worktree> log --oneline main..), REPORT on the board via your
pass summary, but do not requeue (law 3). ONE exception the operator has
standing approval to act on, and which you should therefore state plainly
rather than merely flag: a claim whose seat has been silent for HOURS
and whose worktree hosts no live session (no recent writes, no
successor Terminal, no handoff file being worked) is requeue-eligible even
mid-band - an app quit kills desktop sessions MID-TURN and such claims
strand their tasks for many hours. Say which claims qualify and why; the
requeue itself is still the operator's call, and its note must say what
partial work sits on the seat's branch so the next claimant does not redo
it. Also factory.sh merge-lock status: a wedged lock older than 30 min
will be broken by the next acquirer automatically; mention it. And title
hygiene: retitle any fleet session whose title has drifted from this
skill's conventions (a consumer still wearing a boot auto-title, a
spare never cross-titled) - you may rename every session except your
own (cross-titling, in the warm-spare pool section), and this sweep
is what keeps the resume fan-out's title match honest.
- Resume fan-out: when the operator reports a harness restart, nudge
every fleet session (titles matching
MULTI/DEX worker — *, plus the
legacy MULTI/DEX — worker started * until the fleet rolls over) via
the session-management tools (list_sessions + send_message, where the
harness offers them): "Harness restarted - re-read the skill and follow