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
its restart checklist, then continue." Report any session you could not
reach so the operator nudges it by hand; then resume your own loop.
(CLI executor seats survive app restarts and usually need no nudge -
their dead watchers re-invoke them; the fan-out is the safety net.)
- You never edit task files, never claim, never merge, never touch git
beyond the janitor's read-only look (law 4).
Role: OperatorTasks (the operator's console; run with the operator present)
One session, on demand, on the OPERATOR SEAT or in the primary checkout
(--operator claims are allowed from both). It turns the operator lane into
an interactive inbox: what is waiting on the operator, answered in
conversation, resolved in the queue. It ends when the inbox is clear - a
standing session adds nothing, since every resolution needs the operator
anyway.
- Inbox:
scripts/factory.sh list --operator, then show <id> each.
Present a numbered digest - per task: what it is, the EXACT question or
action needed, and what it unblocks (grep the queue for after: citing
its id; more dependents = present it earlier).
- Discuss: the operator answers, decides, or performs auth steps in
chat. Push back on ambiguity: once embedded, the answer must meet the
self-containedness bar (law 6) - the eventual consumer has no access to
this conversation.
- Resolve, per shape:
- INPUT task (needs an answer):
factory.sh claim --operator --id <id>,
then edit the file you now own - replace ## Needs from operator with
## Operator answer (<date>) recording the decision verbatim, and
DELETE the lane: operator line. factory.sh requeue <id> [band]
returns it to the consumer queue with the answer riding in the file.
- EXECUTION task (engine/subnet rollout, a push): keep the lane; execute
it here and now with the operator present for auth - from the PRIMARY
checkout on merged main, foregrounded so the
icp reauth URL is
visible - then factory.sh done <id> done "<what ran, where>".
- Obsolete / answered elsewhere:
claim --operator --id <id> then
done <id> superseded "why".
- Law 9 always: never edit a queued file in place - claim-edit-requeue is
the only safe edit. Never touch consumer ordering beyond the requeue
band hint; the prioritizer owns the queue order. New work the discussion
surfaces is filed via the producer procedure, not bolted onto existing
tasks.
Role: TaskConsumer
The worker doctrine (adopted 2026-08-20 from the open-saas factory's
2026-08-19 operator decision): workers are CLI sessions running EXECUTOR
MODE - always. Boot workers with
the claude CLI in a Terminal (never as desktop-app sessions - the desktop is
the OPERATOR's surface), and always in executor mode (below; no longer a
variant). Why: CLI seats survive desktop-app restarts (upstream lost its
whole fleet twice to app restarts killing desktop workers mid-turn), the
whole lifecycle is scriptable (boot loop, spawn-successor, signal files - no
chips, no clicks, no message-delivery uncertainty), and executor seats stay
clean for hundreds of cycles. Session names carry the PROJECT PREFIX:
<prefix>-executor-<seat> (non-executor boots: <prefix>-worker-<seat>),
where <prefix> is FACTORY_PROJECT if set, else the primary checkout's
directory basename with any -ng/-main suffix dropped - here multidex
-> multidex, so multidex-executor-<seat>. Derived, never hardcoded, so
forks inherit correct names for free; several projects run this factory
pattern on one machine (open-saas among them), and the prefix is what keeps
claude --resume pickers and process listings unambiguous.
spawn-successor mints it automatically; operator hand-boots pass the same
--name shape. Existing sessions are never force-renamed - they converge at
their next handoff. Chips and message-activated warm spares are desktop-era
mechanisms: RETIRED for workers (the sections below remain for the
operator's desktop surfaces and the transition).
Setup once: work in your own git worktree on a claude/<name> branch -
never in the primary checkout (law 5). If you were started in the primary
checkout, create/enter a worktree first (EnterWorktree, or
git worktree add .claude/worktrees/<name> -b claude/<name> from the
primary). Your worker identity is your branch name with / flattened to -.
PIN IT ONCE, as your first act on the seat:
cd <your-worktree> && scripts/factory.sh worker-pin
That writes <worktree-root>/.factory-worker (one line, the flattened
branch; gitignored runtime state). Every later claim/requeue/done run
from ANYWHERE inside that tree - executor subagents included, since they run
inside it by construction - then resolves the seat's own name instead of
deriving one from whatever branch the cwd happens to sit on. Derivation from
the cwd is what mints wrong-worker claims (upstream: twice in one hour on
2026-08-04; again on 2026-08-19 from a drifted executor subagent), each
costing a requeue + re-claim bounce, and leaving the board and
tasks/claimed/ lying about who holds what in between. Belt and braces for
commands that may run OUTSIDE your worktree (harness-BACKGROUNDED ones
inherit the harness's DEFAULT cwd, which can be another seat entirely):
pass the worker explicitly (factory.sh claim <worker>) or export
FACTORY_WORKER=<worker> on the command. Precedence is explicit arg >
FACTORY_WORKER > the .factory-worker pin > cwd branch; claim prints the
effective worker on stderr (factory: worker: <name>) - check it in every
claim's output - and WARNS whenever the pin and the name actually used
disagree, which is drift caught in the act. factory.sh worker prints what
the current directory resolves to and why. (requeue/done locate your claimed
file by id across all workers, so they are cwd-proof; with a pin present they
also warn if the claim you are closing belongs to another seat.)
If your harness offers session titling (set_session_title), title this
session MULTI/DEX worker — <seat> — <UTC datetime> where <seat> is your
worktree's basename (seat-1, seat-2, ...; legacy adjective-named
worktrees use that basename) and the timestamp is
date -u '+%Y-%m-%d %H:%M'. Seat-first titles make the operator's session
list scannable by seat, and successive sessions on one seat share a prefix -
honest start times tell them apart. Handoff successors overwrite any staged
chip title this way too. Some harnesses REFUSE to rename the calling
session (Claude Desktop does): when self-titling errors, read your own id
from $CLAUDE_CODE_HOST_SESSION_ID and have another fleet session apply
the exact title for you - the cross-titling procedure, in "The warm-spare
pool" below. Never block on a title: keep working and let the fleet
converge on it. (CLI seats booted with --name already list unambiguously;
the title is the desktop-facing half.)
Seat venue - bring-up (once per seat)
Your worktree runs its OWN local venue, fully isolated from the primary's
:8000 venue, from other seats, and from the machine's global icp identity
store. The recipe, in order, from the worktree root:
- Isolated icp store:
export ICP_HOME="$PWD/.icp-home" - and export
it in EVERY shell that runs icp or any script (children inherit it;
a call without it acts on the GLOBAL store, whose ~300 identities also
risk the CMC mint-cap abort at network start). Create the identities the
suites use: icp identity new alice --storage plaintext (and bob,
charlie when a task's suites need them). The fresh store contains only
anonymous, which locally IS a controller - that is expected.
- Own gateway port: in the worktree's
icp.yaml, give the local
network gateway: { port: 0 } (the OS picks a free port). This edit
stays UNCOMMITTED for the seat's whole life - commit by explicit paths
(never git add . / -a), and the posture law refuses the merge if it
ever reaches a commit. Never point at :8000: that port is
machine-global, first-come, and is normally the primary's live venue.
The signature "Error: the local network for this project is not running"
while curl 127.0.0.1:8000/api/v2/status succeeds means :8000 is
SOMEONE ELSE'S gateway and your own master is dead or never started -
never "fix" strays there.
- Build prerequisites:
mops install, then
mops generate candid backend - src/backend/backend.did is a
gitignored intermediate, absent in a fresh worktree, and icp deploy
fails its Candid compatibility check without it.
- Start the network: overlapping
icp network starts wedge on a loaded
machine, so before your FIRST start (or any start after a stop) check
pgrep -f "icp network start" and wait for in-flight starts to clear
(healthy starts take ~20s) - this is the ONE machine-wide wait in the
whole protocol. Then icp network start --background and watchdog your
own start: no port descriptor within ~90s means wedged - kill both
halves of YOUR start (captured PIDs, never a pattern), remove your
worktree's network lock, retry.
- Deploy + seed on #dev: flip
DEPLOY_MODE to #dev in
src/backend/main.mo, run
bash scripts/cold_start.sh --mode full --no-simulate, then RESTORE
#play in the tree immediately (see the posture rhythm below).
--no-simulate is non-negotiable on a seat: sim bots crash the local
pocket-ic master silently within minutes, and N seats running fleets
would flatten the machine besides. cold_start's stray-master reaper
resolves ownership via icp network status (worktree-safe by
construction since W1-06); if it nonetheless kills your OWN master,
stop and re-verify rather than editing the reaper.
- Trust, then verify the controllers: cold_start's "✓ alice promoted"
line can lie (the settings update is
||-swallowed). Before running
anything, confirm
icp canister status backend --identity anonymous | grep Controllers
lists 2vxsx-fae. If not:
icp canister settings update backend --add-controller 2vxsx-fae --force --identity alice.
The posture rhythm - the venue runs #dev, the tree stays #play: the
integration suite requires #dev-gated hooks (setAmmRefPrice and friends
trap on #play by design), so every DEPLOY to your venue happens with the
flip in place - flip #play -> #dev, bash scripts/deploy.sh local (or the
cold_start above), restore #play in the tree the moment the deploy
finishes. Never let the flip sit in the tree between deploys: a lingering
flip turns git merge main into a conflict magnet on main.mo and a slip
away from committing #dev - which the merge gate would refuse, at the cost
of a redo. A wall of red on suites whose fixtures fail at setup is the
posture signature: check
echo y | icp canister call backend getDeployMode '()' --query --identity anonymous
before blaming your change.
Never stop your replica while you idle. icp network stop/start does
NOT resume state - a restart comes up freshly seeded with no canisters, and
the only restore is a full cold_start + reseed (minutes). The cheap idle is
the replica you never stopped. Stop it only when RETIRING the seat (wind-up)
or when the operator asks for machine headroom - and budget the re-bring-up.
Loop - and every cycle STARTS by re-reading this skill from the PRIMARY
checkout (<primary>/.claude/skills/mdex-software-factory/SKILL.md;
primary = git worktree list --porcelain | sed -n 's/^worktree //p' | head -1).
The protocol evolves while sessions run; your boot-time copy and your
worktree's checked-out copy both go stale, and the primary's copy is
authoritative. One Read per task is cheap; obeying a superseded protocol is
not.
Every cycle also verifies the model. Consumer seats run on Fable 5, and
the platform sometimes flips a session (fast mode, usage fallback). Check
your CURRENT system prompt's environment section - it names the model
actually powering you now; trust it over anything earlier in the
conversation. A session cannot flip itself back, so if it does not say
Fable 5, make the first line of your next message:
!!! MODEL - running on <model>, flip me to Fable 5 !!!
then continue working normally - never block or park over model drift, and
repeat the banner once per claim cycle while it persists (the flip back is
the operator's, via the app's model picker).
- Claim:
scripts/factory.sh claim <your-worker> --affinity <a,b,c> -
always name your worker (or export FACTORY_WORKER); a backgrounded
claim without it, from a seat that never ran worker-pin, derives the
worker from the harness cwd's branch and can mint another seat's
identity (see Setup). The affinity list is the touches: of your last
task or two - your warm context. Affinity reorders WITHIN the head
priority band only: among equal-priority work you get the task you
already know the ground for (and same-area work serializes onto one seat
instead of ping-ponging), but higher-priority work can never be skipped
past. Omit --affinity on a fresh seat. Beyond that, claim picks the
best eligible task - skipping tasks whose after: ids are still live,
soft-skipping tasks whose touches: overlap another session's active
claim (to keep merge conflicts rare; if everything overlaps it takes the
head anyway) - moves it to tasks/claimed/<you>/ and prints the path.
Announce it immediately (the === CLAIMED TASK === banner below) before
starting work. EMPTY means idle - and it arrives itemized: EMPTY (queue N: X dep-gated, Y operator-lane, Z overlap-deferred), or EMPTY (queue 0) when queue/ is literally empty. RELAY the breakdown in your
idle report - "idle: 16 dep-gated (behind 1785898191), 4 operator-lane"
tells the operator the truth, and naming the gating prerequisite beats
the bare count. Say "queue is empty" ONLY on a literal EMPTY (queue 0).
A claim that REFUSES with an operator signal is waiting for '<you>' is
not an error either: the operator has given you an order, printed right
there in the refusal - carry it out, acknowledge it with
factory.sh signal-clear <you>, then claim again (Operator signals,
below). On idle: factory.sh watch (background it on this harness) and
claim again on wake - a signal wakes that watch too. If you instead wake
to a "background task stopped - no completion record" notice for your
watcher, the HARNESS RESTARTED: that notice is your resume signal, not
an anomaly - announce "restart detected - resuming", follow the restart
seat checklist (Operator guide), and never end your turn having only
narrated the dead task. And KEEP your replica running while you idle
(above).
- Sync, then sanity-check. First
git merge main into your branch:
other seats land work continuously, and an after: dependency being
archived only promises the prerequisite's code is on MAIN - a lagging
branch can be missing exactly what your task depends on. (Clean tree
first: the posture rhythm means no lingering main.mo flip.) If the task
needs the live venue, redeploy your venue per the posture rhythm. Then
sanity-check the task against current reality: cited files/lines still
exist? Already fixed by someone else? The corpus README's "Already
resolved - do not redo" table? If stale:
factory.sh done <id> superseded "why" and claim again.
- Split when splitting beats doing: work discovered to be multi-session
sized, separable, or blocked on a missing prerequisite becomes NEW tasks
(producer procedure: draft +
new, with after:/split_from: wiring),
then factory.sh done <id> split "-> <new ids>". When mid-task work is
partially done and coherent, integrate what stands (below), then split
the remainder.
- Execute in your worktree. The execution rules that are multidex
physics, not style:
- Deploys go to your worktree's OWN venue only (law 8).
ICP_HOME
exported, --identity on every icp call (law 10) - tests/_lib.sh
and scripts/seed.sh already enforce the pin at the helper layer;
match them in ad hoc calls.
- The harness shell cwd is treacherous: after ANY foreground Bash call
is timeout-moved to the background, the NEXT call's working directory
RESETS to the session's default cwd - which can be a DIFFERENT
worktree. Prefix EVERY deploy/icp/mops/test invocation with an
explicit absolute
cd <your-worktree> && ..., and re-check pwd
before trusting any relative path after a backgrounding event (a
stray relative deploy once seeded a whole venue into the wrong
worktree).
- Deploys against your RUNNING replica contend with nothing of other
seats': never wait for machine-wide quiet, never poll for other
sessions' deploys, never build wedge detectors - deploy immediately.
The one start-line exception is bring-up step 4.
- Mid-task discoveries you will not do now: file them (law 7). Doctrine
questions (posture, transparency, disclosure) are operator-INPUT
tasks - park into the lane rather than deciding unilaterally.
- Never edit a running script in place (a live fleet's bash re-parses
mid-run); atomic-rename or stop-first. Restore accidentally clobbered
files from copies, never
git checkout over uncommitted work.
- Commit at verified checkpoints, by explicit paths (the seat's
uncommitted icp.yaml port edit must never ride along).
- Verify what the task's "how to verify" says, plus the narrowest
honest gate set for what you touched:
- Unit (no venue):
mops test for the Motoko suites in
tests/*.test.mo.
- Static (no venue):
bash tests/test_deploy_hygiene.sh and the
other source-reading suites - run hygiene whenever you touched
scripts/, deploy paths, or anything a hygiene section pins.
- Integration (your #dev venue): the specific
tests/test_*.sh the
task names, or bash tests/run_all.sh [--filter X] - from the
worktree, ICP_HOME exported. Suite physics: it is NOT idempotent
across runs (test_state_reset wipes the venue near the end), and a
FAILING run can strand global state that reds OTHER suites - so
reseed (cold_start --mode full --no-simulate, posture rhythm)
between full runs and before re-diagnosing a surprising red. Cheap
wipe check first:
echo y | icp canister call backend getAmmPools '()' --query --identity anonymous
returning (vec {}) means you are reading noise, reseed. On a
freshly seeded seat venue, feed/anchor-leaning suites (the archive
tr
…(truncated)
1---2name: mdex-software-factory3description: 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.4---56# Skill: the MULTI/DEX software factory78Several Claude sessions work one shared task queue continuously: **TaskConsumers**9(typically 3-5, each a CLI executor session in its own git worktree with its10OWN local venue - the worker doctrine below) claim and execute tasks; **one11TaskPrioritizer** reorders the queue and keeps the board; **TaskProducers**12(ad hoc) turn instructions, audits, and community issues into tasks; and one13standing **operator seat** hosts operator-directed work. The operator14(Dominic) boots sessions and is the escalation path. There is no message bus15and no coordinator process: the queue directory IS the coordination, and every16mutation is an atomic same-filesystem rename, so races have exactly one winner17and losers retry. The operator's control channel to a running seat is a18FILE too - `tasks/signal/` (Operator signals, below) - not a chat message.1920Three things make this factory different from a generic one, and they are all21load-bearing:22231. **This machine hosts live systems.** Long-lived bot fleets drive the LIVE24 subnet at multidex.ai and the cloud engine, and the primary checkout's25 :8000 local network is the operator's standing demo venue. Seat isolation26 (own worktree, own `ICP_HOME`, own OS-assigned port) and the27 `mdex-process-safety` skill are how the factory coexists with all of that.282. **Nothing is ever pushed.** Merges land on the primary checkout's `main`29 LOCALLY. Pushing to origin or the public mirror is the operator's act, on30 explicit instruction only, sequenced by the outward-batch policy - the31 factory has no push step anywhere.323. **Task production stays as it is today.** `docs/tasks/` (tracked) remains33 the authored task corpus - W-style briefs, `done/`, the README queue table.34 The factory adds a RUNTIME queue (`tasks/`, gitignored) that carries35 execution state: claims, priorities, dependencies, the board.3637`scripts/factory.sh` is the ONLY way to touch the queue. It works from the38primary checkout or any worktree (it resolves the queue root to the PRIMARY39checkout's `tasks/` either way - `git worktree list` puts the primary first).40Run `scripts/factory.sh help` for the command table;41`scripts/factory-selftest.sh` exercises it against a throwaway queue.4243## The queue on disk4445```46tasks/queue/ pending tasks, one md file each: PPPPPPPPPP-IIIIIIIIII.md47tasks/claimed/<worker>/ in-flight tasks, one subdir per consumer48tasks/archive/ terminal tasks: <id>.md (status stamped inside)49tasks/factory/ coordination surface: index.md (the board), merge.lock/,50 .ids/ (the append-only id allocator - never touch, never clean)51tasks/handoff/ one <worktree-basename>.md per seat waiting for a successor52 session: the boot prompt, on disk, where an app restart53 cannot reach it (`factory.sh handoff-list`)54tasks/signal/ operator ORDERS to a seat: <worker>.md, plus all.md (the55 broadcast). Wakes every `watch`, blocks that seat's next56 `claim` until acknowledged (Operator signals, below)57```5859All six are gitignored runtime state. The tracked corpus in `docs/tasks/` is60unaffected and keeps its current conventions exactly. One further piece of61runtime state lives outside `tasks/`: each seat's `<worktree>/.factory-worker`62(gitignored too), the one-line identity pin factory.sh reads instead of63guessing a worker from the cwd's branch - see the TaskConsumer setup.6465**Naming.** `P` (10 digits) is the priority key; `I` (10 digits) is the66immutable id. Both start as the UTC epoch seconds of creation, so an untouched67queue lists in FIFO order and `ls tasks/queue` IS the execution order68(ascending). The prioritizer renames ONLY the P half. The id is the task's69identity forever - in `after:` dependencies, in archive filenames, in board70notes - and the allocator guarantees it is never minted twice.7172**Lifecycle.** `queue/ -> claimed/<worker>/ -> archive/` - claim and archive73are `mv`s performed by factory.sh; there are no other states. A task in74`queue/` is claimable by definition; if it must not run yet, that is expressed75by `after:` dependencies or a `90`-band parking, not by side agreements.7677**The two layers.** A queue task either inlines its whole brief (small,78self-contained work), or names a tracked brief with `docfile:79docs/tasks/<file>.md` plus a one-paragraph gist. The brief rides git, so every80worktree sees it and law 6 (self-containedness) is satisfied transitively.81Closing a `docfile:` task INCLUDES the corpus bookkeeping, on your branch so82it rides your merge: move the brief to `docs/tasks/done/` with the83`**done <date>**: ...` completion note in the current style, and strike its84row in `docs/tasks/README.md` (`~~[W..]~~ ... — **done <date>**: ...`).85`factory.sh done` refuses to archive while the brief still sits at its86original path - the **bookkeeping law**, mechanical so forgetting is87impossible. Each seat edits only its OWN task's README row; rows are one line88each precisely so parallel strikes merge clean.8990**The operator lane.** `lane: operator` in a task's front matter means the91task is WAITING ON THE OPERATOR and no consumer seat may claim it. It holds92two shapes of work: operator-EXECUTED tasks (engine/subnet rollouts, pushes,93anything needing human-held auth - law 8), which the operator resolves with94`done`; and operator-INPUT tasks (a decision, credential, or clarification a95consumer needs), which pass THROUGH the lane - the answer is embedded in the96task file and it is released back to the consumer queue (the OperatorTasks97role below). `factory.sh claim` skips lane tasks entirely, including as a98last-resort fallback, so a seat gets an itemized `EMPTY` rather than forbidden99work; `factory.sh claim --operator` claims only them. `list --operator` /100`list --consumer` filter, and the board gives them their own section - being101SEEN at rollout time is the point of the lane, not being worked. This is a102different axis from priority: band 90 only DELAYS a claim, and on a contended103queue "nothing else remains" arrives routinely; a task no consumer may run104must be unclaimable, not merely parked.105106### The laws (every role)1071081. Only `factory.sh` mutates queue state. Never `mv`/`rm`/Write task files by109 hand, and never operate on a worktree's own `tasks/` copy.1102. The id half of a filename never changes. Only the prioritizer renames the111 priority half, only via `reprioritize`, only for QUEUED tasks.1123. A claimed task belongs to its worker: nobody else edits or moves it. The113 prioritizer flags stale claims on the board but never requeues them itself -114 that is the operator's call (or the owning session's, via `requeue`).1154. The prioritizer never edits task content, never claims, and never touches116 git history or refs (no commits, merges, checkouts). Its only git contact117 is the janitor's read-only liveness look at a stale claim's worktree.1185. Nobody DEVELOPS in the primary checkout. It hosts four things only: the119 runtime queue, the canonical `:8000` local venue (the operator's - see120 "The operator seat"), the untracked rollout config121 (`scripts/.subnet.conf`, `scripts/.cloud-engine.conf`, the API-key files),122 and merges performed under the merge lock. Consumers' only writes there123 are through `factory.sh` and that final merge; operator-directed124 development happens on the operator seat, not in the primary.1256. Every task is self-contained: a session with no memory of the conversation126 that produced it can act on it (a `docfile:` brief counts - it is tracked127 in every worktree). If it is not, it is not a task yet.1287. Follow-up work any session discovers is filed as a queue task - never a129 suggested-task chip, never a note in some doc.1308. Consumer seats never touch a remote target and never push. Concretely131 out of bounds: `scripts/deploy.sh engine|subnet`, `deploy_to_engine.sh`,132 `deploy_to_subnet.sh`, `start_bots_*`/`stop_bots_*` for engine or subnet,133 `topup*.sh` against remote targets, anything that reads134 `scripts/.subnet.conf` or `scripts/.cloud-engine.conf`, and `git push`135 to ANY remote (origin and the public mirror are operator acts under the136 outward-batch policy). Live verification happens on the seat's OWN local137 venue. Remote rollout is a different trajectory with its own human-held138 auth - `icp` reauth is a web flow that WEDGES in a non-interactive139 session (piped output swallows its URL and the command parks forever),140 so a consumer that tries either blocks for hours or, worse, an expired141 delegation fakes success. When a task's remote part matters: do the142 local part, note the remote part as DEFERRED in the completion note, and143 file a follow-up task carrying `lane: operator`. Sole exception: the144 task text itself explicitly makes a remote deploy the deliverable - and145 such a task is operator-lane by construction.1469. Nobody edits a QUEUED task file in place - not even its producer. To147 change task content, own it first: consumers edit only their claimed148 files; the operator takes a specific task with149 `claim --operator --id <id>`, edits, and requeues. An in-place Write150 racing a claim recreates the moved file as a ghost duplicate of a task151 someone else now owns.15210. **`mdex-process-safety` is binding in every role.** No pattern kills153 (`pkill -f` took down the live subnet fleet three times), no `pgrep`-based154 waits (they match the waiting shell itself), kill only PIDs you captured155 or via the repo's `stop_bots_*` machinery, never leave an156 `icp network status` poll unattended without a kill-after watchdog, and157 EVERY `icp` invocation carries `--identity <name>` - never158 `icp identity default`, whose global state other sessions and connectors159 mutate mid-run. Read that skill before your first process or `icp`160 operation; it is short and it is the machine's #1 hazard list.16111. **The posture law.** A branch merges into main only on the committed162 defaults: `src/backend/main.mo` AND `src/bridge/main.mo` resolve to163 `#play` (read by `mdx_posture_of`, the one comment-aware reader), and164 `icp.yaml` carries no `port: 0` line. `factory.sh merge-lock acquire165 <branch>` enforces it mechanically. The seat-local `#dev` flip and the166 port-0 gateway edit are legitimate WORKING-TREE state (that is how a167 seat's venue runs); the law is what lets them exist without ever168 reaching main.169170## Task file format171172```markdown173---174producer: <who filed it: branch, "dominic", "audit-2026-08-17", ...>175class: security-high | security-medium | architecture | prerequisite |176 feature | hygiene | docs | parked (advisory - the prioritizer's input)177touches: backend, tests (see the vocabulary below; "suite" = repo-wide)178docfile: docs/tasks/W4-23-clamp-amm-half-spread.md (the tracked brief, when one exists)179after: 1785709403, 1785709404 (ids that must archive first)180split_from: 1785709400 (lineage, when split)181lane: operator (waiting on the operator - execution OR input)182---183# <imperative title - what will have happened when this is done>184185Goal, context, and exact pointers: paths, file:line, the shape of the change,186and HOW TO VERIFY (which suites, which gates, what to look at). Out-of-scope187notes if the edges are temptingly fuzzy. With a docfile: a one-paragraph gist188plus anything the brief lacks (the brief carries the details).189190## Factory log <- appended by factory.sh; never write this yourself191```192193Only the `# title` line is mandatory (factory.sh refuses drafts without one);194`class` and `touches` should almost always be present, `docfile` whenever a195tracked brief exists, `after` whenever order matters, and `lane: operator`196whenever law 8 puts the work out of a consumer's reach. Fields are greppable197plain lines - keep them single-line, comma lists.198199**`touches:` vocabulary** (the affinity and overlap currency - use these,200not ad hoc strings): `backend` (src/backend), `bridge` (src/bridge), `arb`201(src/arb), `archive` (ArchiveCanister + archive design), `frontend`202(src/frontend), `verifier` (scripts/verify_ledger.mjs + its fixtures),203`deploy` (scripts/), `tests` (tests/), `docs` (docs/), `candid`, and `suite`204for repo-wide waves.205206## Priority bands207208The prioritizer expresses policy by renaming into bands: new priority =209`BB * 10^8 + (id mod 10^8)` - `factory.sh reprioritize <id> <BB>` computes210this - which preserves FIFO order among tasks in the same band.211212| Band | Meaning |213|------|---------|214| 00 | emergency - drop everything (operator-directed, mostly) |215| 01 | security high - live venue / pipeline safety (the W1 shape) |216| 02 | security medium - tape & verifier integrity, #production hardening (W2/W3 shapes) |217| 03 | architecture / doctrine changes that would force rework of tasks landing after them |218| 04 | prerequisites - tasks other queued tasks name in `after:` |219| 05-09 | expedited, prioritizer's discretion |220| 17-21 | the natural zone: untouched creation keys (epoch seconds land here through 2036) = plain FIFO |221| 30 | hygiene, gate/instrument debt, low-severity findings (the W5 shape) |222| 40 | nice-to-have, cosmetic, docs polish |223| 90 | parked - claim only when nothing else remains |224225Rationale in one breath: security before features so it is baked in, not226bolted on; the tape and the verifier are REQUIREMENTS under the transparency227doctrine, not features - rank them accordingly; architecture before the work228it would rework; prerequisites before their dependents; everything else FIFO.229230Band 90 is not a way to say "never": it delays a claim, it does not refuse231one. Work a consumer must NOT run belongs in the operator lane232(`lane: operator`), a separate axis - a task can be band 01 and operator-lane233at once.234235## Role: TaskProducer2362371. Collect the work: the operator's instruction, an audit round, community238 issues, or existing findings. One task per independently completable item.2392. Dedupe first: `scripts/factory.sh list`, the board240 (`tasks/factory/index.md`), a grep of `tasks/archive/`, AND the corpus -241 `docs/tasks/README.md`'s queue table plus `docs/tasks/done/`. Extend or242 skip rather than duplicate; the "Already resolved - do not redo" table in243 the README exists because reporters re-report fixed things.2443. Author to the current corpus standard. Substantial work gets a W-style245 brief in `docs/tasks/` exactly as today (verified against the working246 tree by symbol lookup, citations, the how-to-verify section, pushback247 recorded where a reporter is wrong) and a THIN queue task pointing at it248 via `docfile:`. Small self-contained items inline everything in the queue249 task. Either way the self-containedness bar is law 6, and sources are250 cited (file:line, issue numbers, commit shas).2514. `scripts/factory.sh new <draft>` - it allocates the id and prints the252 queue path. Draft in your scratchpad; the draft file stays where it was,253 inert. (New briefs in `docs/tasks/` are ordinary tracked edits - commit254 them through the normal branch flow, not by writing into the primary.)255 **File in dependency order**: an `after:` id must be one `new` has256 ALREADY printed (every filing prints its id) - never a guessed or257 predicted one. `new` refuses a draft whose `after:` names an id that258 was never minted, because a nonexistent gate gates nothing: `claim`259 treats unknown ids as satisfied (live-ids-only, deliberately - in the260 open-saas factory five guessed-id gates once let four "sequential"261 tasks run concurrently). File prerequisites first, then the dependents262 citing the printed ids.2635. Report the filed ids/titles. Do not set priorities - that is the264 prioritizer's job; your `class:` field is its input. (Genuine265 emergencies: say so to the operator rather than guessing a band.)266267Set `lane: operator` on anything a consumer seat is forbidden to execute -268engine/subnet rollouts, pushes, anything needing human-held auth or a269decision only Dominic can make. That is a statement of fact about the work,270not a priority call, so it IS yours to set. Leaving it off does not merely271mis-sort the task: seats will claim it, discover they must not act, and272requeue it, indefinitely.273274**Seeding the queue from the corpus:** the standing instruction "work the275docs/tasks queue" translates to one thin `docfile:` task per open row of276`docs/tasks/README.md`, skipping rows marked IN FLIGHT in another session,277with `after:` wiring where a brief names an ordering. Do this once at factory278boot and again whenever a new triage round lands in the corpus.279280## Role: TaskPrioritizer (exactly one session)281282Loop forever: **wake -> triage -> reorder -> board -> janitor -> re-arm** -283and keep this skill fresh MECHANICALLY, not by feel: every pass, stat the284primary copy285(`stat -f %m <primary>/.claude/skills/mdex-software-factory/SKILL.md` - one286token-free shell call, foldable into the same Bash call as your watch);287RE-READ the file whenever that mtime moved past your last read, and in any288case at least once an hour. The mtime gate means a protocol change reaches289you within one pass; the hourly floor exists because even an unchanged skill290needs occasional re-reading - summarization quietly erodes the copy in your291context.292293- **Wake**: `scripts/factory.sh watch 500` blocks until294 queue/claimed/archive/signal change (prints CHANGED) or times out295 (TIMEOUT) - act on either, then re-arm. In a harness with background296 tasks, run `watch 3600` via a background Bash call and you will be297 re-invoked on completion; otherwise call it in the foreground repeatedly.298 Either way each pass costs nothing when idle.299- **Triage**: `factory.sh list`; read (`factory.sh show <id>`) tasks you have300 not seen before (the board is your memory of what you have triaged). Judge301 class and content per the band table - and check `after:` chains: a302 dependent must sort behind its prerequisites (band 04 the prerequisites303 forward rather than parking the dependent, when both matter).304- **Reorder**: `factory.sh reprioritize <id> <band>`. If it errors, the task305 was claimed mid-rename - that is fine, drop it. Do not churn: rename only306 when the current position is actually wrong.307- **Rank bands, not tasks - keep the head band BROAD.** A band is a statement308 that its members are equally important, and that interchangeability is what309 consumer affinity (`claim --affinity`) converts into warm-context310 throughput: a seat may take any same-band task it knows the ground for,311 never anything past the band. A head band holding one task disables312 affinity exactly where most claims happen. So when you promote several313 tasks of comparable urgency, put them in the SAME band (within-band FIFO is314 preserved by the formula); split finer only when one genuinely must precede315 another - and a true must-precede is usually an `after:` dependency, not a316 band distinction.317- **Board**: `factory.sh index` regenerates `tasks/factory/index.md` - the318 operator's one-glance view. Refresh it every pass. Its **Seats awaiting319 successor** section lists `tasks/handoff/` - seats that handed off and are320 sitting unmanned. Call any entry older than an hour out in your pass321 summary: a shrinking fleet is invisible otherwise (a CLI seat's322 self-spawn can fail; a desktop chip can die with an app restart). Its323 **Operator signals** section lists unacknowledged orders - each line is a324 seat that CANNOT claim until it acknowledges, so an old entry means a325 stopped or dead seat: call those out too.326- **Janitor**: the index marks claims idle >4h as STALE. Investigate liveness327 read-only (is the worktree still there? recent commits?328 `git -C <worktree> log --oneline main..`), REPORT on the board via your329 pass summary, but do not requeue (law 3). ONE exception the operator has330 standing approval to act on, and which you should therefore state plainly331 rather than merely flag: a claim whose seat has been silent for HOURS332 **and** whose worktree hosts no live session (no recent writes, no333 successor Terminal, no handoff file being worked) is requeue-eligible even334 mid-band - an app quit kills desktop sessions MID-TURN and such claims335 strand their tasks for many hours. Say which claims qualify and why; the336 requeue itself is still the operator's call, and its note must say what337 partial work sits on the seat's branch so the next claimant does not redo338 it. Also `factory.sh merge-lock status`: a wedged lock older than 30 min339 will be broken by the next acquirer automatically; mention it. And title340 hygiene: retitle any fleet session whose title has drifted from this341 skill's conventions (a consumer still wearing a boot auto-title, a342 spare never cross-titled) - you may rename every session except your343 own (cross-titling, in the warm-spare pool section), and this sweep344 is what keeps the resume fan-out's title match honest.345- **Resume fan-out**: when the operator reports a harness restart, nudge346 every fleet session (titles matching `MULTI/DEX worker — *`, plus the347 legacy `MULTI/DEX — worker started *` until the fleet rolls over) via348 the session-management tools (list_sessions + send_message, where the349 harness offers them): "Harness restarted - re-read the skill and follow350 its restart checklist, then continue." Report any session you could not351 reach so the operator nudges it by hand; then resume your own loop.352 (CLI executor seats survive app restarts and usually need no nudge -353 their dead watchers re-invoke them; the fan-out is the safety net.)354- You never edit task files, never claim, never merge, never touch git355 beyond the janitor's read-only look (law 4).356357## Role: OperatorTasks (the operator's console; run with the operator present)358359One session, on demand, on the OPERATOR SEAT or in the primary checkout360(`--operator` claims are allowed from both). It turns the operator lane into361an interactive inbox: what is waiting on the operator, answered in362conversation, resolved in the queue. It ends when the inbox is clear - a363standing session adds nothing, since every resolution needs the operator364anyway.3653661. **Inbox**: `scripts/factory.sh list --operator`, then `show <id>` each.367 Present a numbered digest - per task: what it is, the EXACT question or368 action needed, and what it unblocks (grep the queue for `after:` citing369 its id; more dependents = present it earlier).3702. **Discuss**: the operator answers, decides, or performs auth steps in371 chat. Push back on ambiguity: once embedded, the answer must meet the372 self-containedness bar (law 6) - the eventual consumer has no access to373 this conversation.3743. **Resolve, per shape**:375 - INPUT task (needs an answer): `factory.sh claim --operator --id <id>`,376 then edit the file you now own - replace `## Needs from operator` with377 `## Operator answer (<date>)` recording the decision verbatim, and378 DELETE the `lane: operator` line. `factory.sh requeue <id> [band]`379 returns it to the consumer queue with the answer riding in the file.380 - EXECUTION task (engine/subnet rollout, a push): keep the lane; execute381 it here and now with the operator present for auth - from the PRIMARY382 checkout on merged main, foregrounded so the `icp` reauth URL is383 visible - then `factory.sh done <id> done "<what ran, where>"`.384 - Obsolete / answered elsewhere: `claim --operator --id <id>` then385 `done <id> superseded "why"`.3864. Law 9 always: never edit a queued file in place - claim-edit-requeue is387 the only safe edit. Never touch consumer ordering beyond the requeue388 band hint; the prioritizer owns the queue order. New work the discussion389 surfaces is filed via the producer procedure, not bolted onto existing390 tasks.391392## Role: TaskConsumer393394**The worker doctrine (adopted 2026-08-20 from the open-saas factory's3952026-08-19 operator decision): workers are CLI sessions running EXECUTOR396MODE - always.** Boot workers with397the claude CLI in a Terminal (never as desktop-app sessions - the desktop is398the OPERATOR's surface), and always in executor mode (below; no longer a399variant). Why: CLI seats survive desktop-app restarts (upstream lost its400whole fleet twice to app restarts killing desktop workers mid-turn), the401whole lifecycle is scriptable (boot loop, spawn-successor, signal files - no402chips, no clicks, no message-delivery uncertainty), and executor seats stay403clean for hundreds of cycles. Session names carry the PROJECT PREFIX:404`<prefix>-executor-<seat>` (non-executor boots: `<prefix>-worker-<seat>`),405where `<prefix>` is `FACTORY_PROJECT` if set, else the primary checkout's406directory basename with any `-ng`/`-main` suffix dropped - here `multidex`407-> `multidex`, so `multidex-executor-<seat>`. Derived, never hardcoded, so408forks inherit correct names for free; several projects run this factory409pattern on one machine (open-saas among them), and the prefix is what keeps410`claude --resume` pickers and process listings unambiguous.411`spawn-successor` mints it automatically; operator hand-boots pass the same412`--name` shape. Existing sessions are never force-renamed - they converge at413their next handoff. Chips and message-activated warm spares are desktop-era414mechanisms: RETIRED for workers (the sections below remain for the415operator's desktop surfaces and the transition).416417**Setup once**: work in your own git worktree on a `claude/<name>` branch -418never in the primary checkout (law 5). If you were started in the primary419checkout, create/enter a worktree first (EnterWorktree, or420`git worktree add .claude/worktrees/<name> -b claude/<name>` from the421primary). Your worker identity is your branch name with `/` flattened to `-`.422423**PIN IT ONCE, as your first act on the seat:**424425```426cd <your-worktree> && scripts/factory.sh worker-pin427```428429That writes `<worktree-root>/.factory-worker` (one line, the flattened430branch; gitignored runtime state). Every later `claim`/`requeue`/`done` run431from ANYWHERE inside that tree - executor subagents included, since they run432inside it by construction - then resolves the seat's own name instead of433deriving one from whatever branch the cwd happens to sit on. Derivation from434the cwd is what mints wrong-worker claims (upstream: twice in one hour on4352026-08-04; again on 2026-08-19 from a drifted executor subagent), each436costing a requeue + re-claim bounce, and leaving the board and437`tasks/claimed/` lying about who holds what in between. Belt and braces for438commands that may run OUTSIDE your worktree (harness-BACKGROUNDED ones439inherit the harness's DEFAULT cwd, which can be another seat entirely):440pass the worker explicitly (`factory.sh claim <worker>`) or export441`FACTORY_WORKER=<worker>` on the command. Precedence is explicit arg >442`FACTORY_WORKER` > the `.factory-worker` pin > cwd branch; claim prints the443effective worker on stderr (`factory: worker: <name>`) - check it in every444claim's output - and WARNS whenever the pin and the name actually used445disagree, which is drift caught in the act. `factory.sh worker` prints what446the current directory resolves to and why. (requeue/done locate your claimed447file by id across all workers, so they are cwd-proof; with a pin present they448also warn if the claim you are closing belongs to another seat.)449If your harness offers session titling (`set_session_title`), title this450session `MULTI/DEX worker — <seat> — <UTC datetime>` where `<seat>` is your451worktree's basename (`seat-1`, `seat-2`, ...; legacy adjective-named452worktrees use that basename) and the timestamp is453`date -u '+%Y-%m-%d %H:%M'`. Seat-first titles make the operator's session454list scannable by seat, and successive sessions on one seat share a prefix -455honest start times tell them apart. Handoff successors overwrite any staged456chip title this way too. Some harnesses REFUSE to rename the calling457session (Claude Desktop does): when self-titling errors, read your own id458from `$CLAUDE_CODE_HOST_SESSION_ID` and have another fleet session apply459the exact title for you - the cross-titling procedure, in "The warm-spare460pool" below. Never block on a title: keep working and let the fleet461converge on it. (CLI seats booted with `--name` already list unambiguously;462the title is the desktop-facing half.)463464### Seat venue - bring-up (once per seat)465466Your worktree runs its OWN local venue, fully isolated from the primary's467:8000 venue, from other seats, and from the machine's global icp identity468store. The recipe, in order, from the worktree root:4694701. **Isolated icp store**: `export ICP_HOME="$PWD/.icp-home"` - and export471 it in EVERY shell that runs `icp` or any script (children inherit it;472 a call without it acts on the GLOBAL store, whose ~300 identities also473 risk the CMC mint-cap abort at network start). Create the identities the474 suites use: `icp identity new alice --storage plaintext` (and `bob`,475 `charlie` when a task's suites need them). The fresh store contains only476 `anonymous`, which locally IS a controller - that is expected.4772. **Own gateway port**: in the worktree's `icp.yaml`, give the local478 network `gateway: { port: 0 }` (the OS picks a free port). This edit479 stays UNCOMMITTED for the seat's whole life - commit by explicit paths480 (never `git add .` / `-a`), and the posture law refuses the merge if it481 ever reaches a commit. Never point at `:8000`: that port is482 machine-global, first-come, and is normally the primary's live venue.483 The signature "Error: the local network for this project is not running"484 while `curl 127.0.0.1:8000/api/v2/status` succeeds means :8000 is485 SOMEONE ELSE'S gateway and your own master is dead or never started -486 never "fix" strays there.4873. **Build prerequisites**: `mops install`, then488 `mops generate candid backend` - `src/backend/backend.did` is a489 gitignored intermediate, absent in a fresh worktree, and `icp deploy`490 fails its Candid compatibility check without it.4914. **Start the network**: overlapping `icp network start`s wedge on a loaded492 machine, so before your FIRST start (or any start after a stop) check493 `pgrep -f "icp network start"` and wait for in-flight starts to clear494 (healthy starts take ~20s) - this is the ONE machine-wide wait in the495 whole protocol. Then `icp network start --background` and watchdog your496 own start: no port descriptor within ~90s means wedged - kill both497 halves of YOUR start (captured PIDs, never a pattern), remove your498 worktree's network lock, retry.4995. **Deploy + seed on #dev**: flip `DEPLOY_MODE` to `#dev` in500 `src/backend/main.mo`, run501 `bash scripts/cold_start.sh --mode full --no-simulate`, then RESTORE502 `#play` in the tree immediately (see the posture rhythm below).503 `--no-simulate` is non-negotiable on a seat: sim bots crash the local504 pocket-ic master silently within minutes, and N seats running fleets505 would flatten the machine besides. cold_start's stray-master reaper506 resolves ownership via `icp network status` (worktree-safe by507 construction since W1-06); if it nonetheless kills your OWN master,508 stop and re-verify rather than editing the reaper.5096. **Trust, then verify the controllers**: cold_start's "✓ alice promoted"510 line can lie (the settings update is `||`-swallowed). Before running511 anything, confirm512 `icp canister status backend --identity anonymous | grep Controllers`513 lists `2vxsx-fae`. If not:514 `icp canister settings update backend --add-controller 2vxsx-fae --force --identity alice`.515516**The posture rhythm** - the venue runs `#dev`, the tree stays `#play`: the517integration suite requires `#dev`-gated hooks (`setAmmRefPrice` and friends518trap on `#play` by design), so every DEPLOY to your venue happens with the519flip in place - flip `#play -> #dev`, `bash scripts/deploy.sh local` (or the520cold_start above), restore `#play` in the tree the moment the deploy521finishes. Never let the flip sit in the tree between deploys: a lingering522flip turns `git merge main` into a conflict magnet on main.mo and a slip523away from committing `#dev` - which the merge gate would refuse, at the cost524of a redo. A wall of red on suites whose fixtures fail at setup is the525posture signature: check526`echo y | icp canister call backend getDeployMode '()' --query --identity anonymous`527before blaming your change.528529**Never stop your replica while you idle.** `icp network stop`/`start` does530NOT resume state - a restart comes up freshly seeded with no canisters, and531the only restore is a full cold_start + reseed (minutes). The cheap idle is532the replica you never stopped. Stop it only when RETIRING the seat (wind-up)533or when the operator asks for machine headroom - and budget the re-bring-up.534535**Loop** - and every cycle STARTS by re-reading this skill from the PRIMARY536checkout (`<primary>/.claude/skills/mdex-software-factory/SKILL.md`;537primary = `git worktree list --porcelain | sed -n 's/^worktree //p' | head -1`).538The protocol evolves while sessions run; your boot-time copy and your539worktree's checked-out copy both go stale, and the primary's copy is540authoritative. One Read per task is cheap; obeying a superseded protocol is541not.542543Every cycle also verifies the model. Consumer seats run on **Fable 5**, and544the platform sometimes flips a session (fast mode, usage fallback). Check545your CURRENT system prompt's environment section - it names the model546actually powering you now; trust it over anything earlier in the547conversation. A session cannot flip itself back, so if it does not say548Fable 5, make the first line of your next message:549550```551!!! MODEL - running on <model>, flip me to Fable 5 !!!552```553554then continue working normally - never block or park over model drift, and555repeat the banner once per claim cycle while it persists (the flip back is556the operator's, via the app's model picker).5575581. **Claim**: `scripts/factory.sh claim <your-worker> --affinity <a,b,c>` -559 always name your worker (or export `FACTORY_WORKER`); a backgrounded560 claim without it, from a seat that never ran `worker-pin`, derives the561 worker from the harness cwd's branch and can mint another seat's562 identity (see Setup). The affinity list is the `touches:` of your last563 task or two - your warm context. Affinity reorders WITHIN the head564 priority band only: among equal-priority work you get the task you565 already know the ground for (and same-area work serializes onto one seat566 instead of ping-ponging), but higher-priority work can never be skipped567 past. Omit `--affinity` on a fresh seat. Beyond that, claim picks the568 best eligible task - skipping tasks whose `after:` ids are still live,569 soft-skipping tasks whose `touches:` overlap another session's active570 claim (to keep merge conflicts rare; if everything overlaps it takes the571 head anyway) - moves it to `tasks/claimed/<you>/` and prints the path.572 Announce it immediately (the `=== CLAIMED TASK ===` banner below) before573 starting work. `EMPTY` means idle - and it arrives itemized: `EMPTY574 (queue N: X dep-gated, Y operator-lane, Z overlap-deferred)`, or `EMPTY575 (queue 0)` when `queue/` is literally empty. RELAY the breakdown in your576 idle report - "idle: 16 dep-gated (behind 1785898191), 4 operator-lane"577 tells the operator the truth, and naming the gating prerequisite beats578 the bare count. Say "queue is empty" ONLY on a literal `EMPTY (queue 0)`.579 A claim that REFUSES with `an operator signal is waiting for '<you>'` is580 not an error either: the operator has given you an order, printed right581 there in the refusal - carry it out, acknowledge it with582 `factory.sh signal-clear <you>`, then claim again (Operator signals,583 below). On idle: `factory.sh watch` (background it on this harness) and584 claim again on wake - a signal wakes that watch too. If you instead wake585 to a "background task stopped - no completion record" notice for your586 watcher, the HARNESS RESTARTED: that notice is your resume signal, not587 an anomaly - announce "restart detected - resuming", follow the restart588 seat checklist (Operator guide), and never end your turn having only589 narrated the dead task. And KEEP your replica running while you idle590 (above).5912. **Sync, then sanity-check.** First `git merge main` into your branch:592 other seats land work continuously, and an `after:` dependency being593 archived only promises the prerequisite's code is on MAIN - a lagging594 branch can be missing exactly what your task depends on. (Clean tree595 first: the posture rhythm means no lingering main.mo flip.) If the task596 needs the live venue, redeploy your venue per the posture rhythm. Then597 sanity-check the task against current reality: cited files/lines still598 exist? Already fixed by someone else? The corpus README's "Already599 resolved - do not redo" table? If stale:600 `factory.sh done <id> superseded "why"` and claim again.6013. **Split when splitting beats doing**: work discovered to be multi-session602 sized, separable, or blocked on a missing prerequisite becomes NEW tasks603 (producer procedure: draft + `new`, with `after:`/`split_from:` wiring),604 then `factory.sh done <id> split "-> <new ids>"`. When mid-task work is605 partially done and coherent, integrate what stands (below), then split606 the remainder.6074. **Execute** in your worktree. The execution rules that are multidex608 physics, not style:609 - Deploys go to your worktree's OWN venue only (law 8). `ICP_HOME`610 exported, `--identity` on every `icp` call (law 10) - `tests/_lib.sh`611 and `scripts/seed.sh` already enforce the pin at the helper layer;612 match them in ad hoc calls.613 - The harness shell cwd is treacherous: after ANY foreground Bash call614 is timeout-moved to the background, the NEXT call's working directory615 RESETS to the session's default cwd - which can be a DIFFERENT616 worktree. Prefix EVERY deploy/icp/mops/test invocation with an617 explicit absolute `cd <your-worktree> && ...`, and re-check `pwd`618 before trusting any relative path after a backgrounding event (a619 stray relative deploy once seeded a whole venue into the wrong620 worktree).621 - Deploys against your RUNNING replica contend with nothing of other622 seats': never wait for machine-wide quiet, never poll for other623 sessions' deploys, never build wedge detectors - deploy immediately.624 The one start-line exception is bring-up step 4.625 - Mid-task discoveries you will not do now: file them (law 7). Doctrine626 questions (posture, transparency, disclosure) are operator-INPUT627 tasks - park into the lane rather than deciding unilaterally.628 - Never edit a running script in place (a live fleet's bash re-parses629 mid-run); atomic-rename or stop-first. Restore accidentally clobbered630 files from copies, never `git checkout` over uncommitted work.631 - Commit at verified checkpoints, by explicit paths (the seat's632 uncommitted icp.yaml port edit must never ride along).6335. **Verify** what the task's "how to verify" says, plus the narrowest634 honest gate set for what you touched:635 - **Unit** (no venue): `mops test` for the Motoko suites in636 `tests/*.test.mo`.637 - **Static** (no venue): `bash tests/test_deploy_hygiene.sh` and the638 other source-reading suites - run hygiene whenever you touched639 scripts/, deploy paths, or anything a hygiene section pins.640 - **Integration** (your #dev venue): the specific `tests/test_*.sh` the641 task names, or `bash tests/run_all.sh [--filter X]` - from the642 worktree, `ICP_HOME` exported. Suite physics: it is NOT idempotent643 across runs (`test_state_reset` wipes the venue near the end), and a644 FAILING run can strand global state that reds OTHER suites - so645 reseed (`cold_start --mode full --no-simulate`, posture rhythm)646 between full runs and before re-diagnosing a surprising red. Cheap647 wipe check first:648 `echo y | icp canister call backend getAmmPools '()' --query --identity anonymous`649 returning `(vec {})` means you are reading noise, reseed. On a650 freshly seeded seat venue, feed/anchor-leaning suites (the archive651 tr652653…(truncated)