meta-orchestration
Host-aware Tillandsias recurring runtime loop: sync remote state, drain claimable plan work, run eligible e2e smoke gates, coordinate integrations on mutable Linux, release when warranted, update plan, commit, and push before exit.
- License 许可
- GPL-3.0
- 最近更新
- 2026-11-12
- Stars
- ★ 6
- Forks
- 0
01 前言 Front Matter
- name
- meta-orchestration
- description
- Host-aware Tillandsias recurring runtime loop: sync remote state, drain claimable plan work, run eligible e2e smoke gates, coordinate integrations on mutable Linux, release when warranted, update plan, commit, and push before exit.
03 SKILL.md 正文
Meta Orchestration
This is the top-level unattended loop intended for:
./repeat --prompt "Use the /meta-orchestration skill"
It composes the worker, coordination, e2e, and release skills without replacing their detailed runbooks.
Invocation Modes — check FIRST, before anything else
The skill is parameterized by the invoking prompt. Before doing any work —
before host classification, before git fetch, before reading the plan —
inspect the prompt that invoked this skill and pick the mode. Full cycles
are token-expensive; running one when a smoke pass was requested wastes a
rate-limited provider budget (operator directive, 2026-07-11).
- Full mode (default): the prompt is a bare "Use the /meta-orchestration skill". Run the complete cycle described in the rest of this document.
- Smoke Mode (early break): the prompt contains the word
smoke(e.g. "Use the /meta-orchestration skill in smoke mode (verify-only)"). You are inside a short e2e smoke test with a hard budget of a few minutes. Do NOT run a full cycle. Follow "Smoke Mode Runbook" below and exit. - Targeted verify (a variant of Smoke Mode): the prompt names specific packets, orders, specs, or files (e.g. "…smoke mode, verify order 283" or "…smoke: spec git-mirror-service"). Same rules as Smoke Mode, but the verification set is the named targets' verifiable closures instead of the generic checks.
Smoke Mode Runbook
Purpose: give fast, cheap feedback that the forge lane and the work under
test are healthy — WITHOUT advancing the plan or consuming a full-cycle
token budget. Hosted e2e gates (litmus:opencode-prompt-e2e-shape) invoke
this mode whenever the full cycle is rate-limited
(scripts/forge-e2e-rate-limit.sh, one full cycle per 4h per host).
Hard rules:
-
Do NOT claim ledger nodes, drain plan work, file routine findings, run nested e2e gates, release, commit, or push. A smoke run leaves the repo untouched. (Exception: a real defect discovered during verification may be reported in your final output text — the HOST files it, not you.)
-
Keep it small: prefer
scripts/run-litmus-test.sh --size instant --phase pre-build --compactscoped with a spec filter when targets are named, plus cheap direct checks (parseplan/index.yaml,git status,git fetch --dry-runreachability, the targets' named verifiable closures). Skip anything that builds, launches containers, or exceeds the budget. 2a. WHAT DECIDES THE VERDICT (order 822-4vwa). Rule 2's last sentence and its first are in tension INSIDE A FORGE, because a large fraction of pre-build litmus needs vault, podman or vsock — none of which a forge has. An agent that runs the unscoped suite there gets a near-total failure that says nothing about the lane. So:- The VERDICT is the forge SUBSTRATE: the checkout resolves and is on the expected branch, the plan ledger parses, the plan/methodology experts answer, and the harness came up. If those hold, the lane is healthy.
- Litmus results in a forge are a REPORT, on the same footing as the
credential verdict in 3a. Quote the pass rate; do NOT convert it into
MO-SMOKE: FAILon its own. - When the invoking prompt NAMES targets, their scoped closures ARE part of the verdict — that is the whole point of a targeted verify.
MEASURED on macuahuitl 2026-08-18, the same prompt on the same host 25 minutes apart with a verified-fresh seed, disagreeing with itself: run 20260818T053811Z returned
MO-SMOKE: PASS(post-build 11/11), and run 20260818T063931Z returnedMO-SMOKE: FAIL litmus pre-build 0% pass rate (1/252); infrastructure-dependent failures expected but unconfirmed. The second agent was right to refuse to bless what it could not confirm; the first agent's PASS is the one that should worry you. A gate whose result depends on how much the agent chose to run is not a gate. -
Budget: finish well inside ~5 minutes of wall time. 3a. The Credential Channel Guard is a REPORT here, never a gate (order 818-cgpn). That guard is a precondition for COMMITTABLE work, and rule 1 says a smoke run performs none — it claims nothing, commits nothing and pushes nothing, so an absent or stale push credential cannot harm it. Report the verdict in your output and carry on verifying; do NOT return
MO-SMOKE: FAILfor it.MEASURED on macuahuitl 2026-08-18, twice: with the mirror's upstream-auth verdict stale, the FULL lane blocked (correctly — it would have pushed) and the SMOKE lane returned
MO-SMOKE: FAIL credential channel blocked (upstream-auth-stale, 22880s > 900s max). Smoke exists precisely as the cheap path for when the expensive one is unavailable; gating it on the same credential leaves NO path, and takes the substrate-idempotence e2e (§2/§3, which never touch a credential) down with it — see 808-zrzz. -
Verdict grammar: your FINAL output line MUST be exactly one of
MO-SMOKE: PASSorMO-SMOKE: FAIL <one-line reason>. The launching litmus greps for this marker; a smoke run that exits without it is a failure by definition.
Sub-agent and token budget (operator directive 2026-09-11; packet 1119-6wn6)
Sub-agents spend the same rate-limited pool as the main loop, and that pool ran out a week early last month. This checkout's 2026-09-06 cycle died mid-finalization for the same reason, with two finished fragments sitting uncommitted for five days. MEASURED on macuahuitl 2026-09-11: one read-only reconciliation Workflow of 42 agents at the main model's tier spent 4,480,590 tokens in 21 minutes — roughly twenty times the main context of the entire cycle. The output was good; the price was invisible until the operator asked.
The loop has timing:, recur: and skippable: for CPU seconds and nothing
for tokens, so an expensive repeatable delegation cannot be seen the way the
low-end hosts made the CPU bottlenecks visible. Until the counter in
1119-6wn6 lands and replaces these rules with numbers:
-
Count. At most 8 sub-agents per full cycle by default, at most 3 running at once. A Workflow (fan-out orchestration) only when the invoking prompt opts in ("ultracode", "use a workflow"), and then at most 15 agents.
-
Tier by task class, and pass
modelandeffortexplicitly every time. Lookups, greps, "does commit X exist / touch file Y", ancestry and existence checks: haiku, effort low. Summarising a diff or a fragment, drafting event prose: sonnet, effort medium. Judgment (a verdict on a packet, a review finding, a design choice): opus, effort medium — high only when the error is unrecoverable. The main model's tier is for the main loop, never for refuters or fan-out. -
Refuters. At most one per verdict, one tier below the verdict's author. Prefer one agent with a schema over N parallel ones when the items are cheap.
-
Packets carry their tier too (trims 2026-09-27, umbrella 1437-62g8). Every filed packet declares
size: S|M|Landimplementer_tier: haiku|sonnet|opus(as scalars once 1437-khnx lands; as two lines insidenotes:until then). S = mechanical, fully specified → haiku; M = a few files, clear design → sonnet; L = cross-cutting, judgment, or gate-integrity-sensitive → opus. Anything that changes what the land gate or the pre-push hook refuses is at least sonnet, opus if it could let a red tree land. An UNTAGGED row is opus (operator 2026-09-27: "No size tags get Opus") and the tier is a floor. A session states its tier to the selector (scripts/select-work-batch.sh <role> --tier <t>, 1437-vdz5) and runs a cheaper packet throughscripts/claude-delegate.sh implement <order>(1437-yjf6) or a dedicated./repeat --model haikusession (1437-m5yx); the host session keeps verify and commit. A host may also run an all-day Haiku ORCHESTRATOR (/haiku-orchestrate, 1443-hbgt) that delegates by tier and accepts only a closure it measured withscripts/verify-closure.sh(1443-qwpj) — never a delegate's "met". Canonical:methodology/distributed-work.yaml→cycle_batch_triage.model_tier_routing. -
Messages to peers: budget ON HOLD (operator question pending, 2026-09-27; 1437-arjg blocked). The shape in
distributed-work.yaml→sibling_heads_up_protocol.size_budgetstays written and is not enforced until the operator answers. -
Never delegate a read an expert answers.
plan_status,plan_answer,methodology_askand the project-info tools cost nothing next to an agent. -
Report it, and LOG it — from the instrument.
scripts/cycle-metrics.sh --emit-tokensexists (1119-6wn6); with--from-transcript(1437-3pj7) it reads this session's own harness transcript and fillsmain_ctx,main_ctx_cumulative,subagent_tokens,agentsandby_modelitself, or recordssource=absent. Hand-passed numbers remain accepted for a harness with no transcript. The typed form was:scripts/cycle-metrics.sh --emit-tokens host=<h> cycle=<id> \ main_ctx=<n> subagent_tokens=<n> agents=<n> \ by_model=<opus:3,haiku:39> label=<what the delegation was for>The handoff still carries the
tokens:line —scripts/cycle-metrics.shrenders it, with a rolling per-cycle average, andsource=absentuntil a record exists. A cycle that spawned nothing writessubagent_tokens=0 agents=0and is correctly absent fromtoken_recur:.labelis the field that earns the log its keep. It names the WORK, not the cycle, so the same expensive delegation recurring across days ranks as one entry intoken_recur:instead of scattering into unrelated rows. Only labels seen MORE THAN ONCE are ranked: a one-off 4.5M sweep is a cost, not a recurrence, and letting it top the list would bury the cheap thing paid fifty times — which is the one the operator asked to find and simplify.Only the agent can observe these numbers, so this stays an attestation (the
check-mcp-surface.shshape); what changed is that it is now structured and comparable across cycles. An unmeasured spend is the order-531 shape one level up: it reads as free and is not.
Full-Mode Terminal Attestation (order 614-2gqx)
Smoke mode has a machine-grepped verdict (MO-SMOKE:); full mode did not, so
a normal provider exit between tool calls could discard local commits while
every outer launcher returned zero (the 4a1410a2 breach; see
plan/issues/meta-orchestration-full-mode-exit-attestation-gap-2026-08-05.md).
Full mode therefore MUST emit a typed terminal marker as its FINAL output
line, and MAY emit it ONLY after every finalization obligation below has
passed (boundary verification, ./build.sh --check, commit, push):
MO-FULL: <DISPOSITION> <LOCAL_SHA> <BRANCH> <REMOTE_SHA>
DISPOSITION∈ {COMPLETE,BLOCKED} — the cycle's terminal disposition.LOCAL_SHA= final local HEAD (git rev-parse HEAD) at emission time.BRANCH= the working branch the cycle committed to.REMOTE_SHA= the remote branch head after the push.
Invariants the outer gate (scripts/mo-full-attest.sh, wired into
scripts/litmus-opencode-e2e-launch.sh for litmus:opencode-prompt-e2e-shape)
enforces before it accepts exit zero:
LOCAL_SHA == REMOTE_SHA— a marker may never follow an unpushed local commit. If the push failed after three rebase retries, do NOT emit the marker; mark the packetblocked/failed-retryable, include the push output, and stop — the missing marker is itself the loud failure.BRANCHmust match the host's current branch.- The claimed
REMOTE_SHAmust actually converge ongit ls-remote origin refs/heads/<BRANCH>within the bounded relay window. - A startup boundary must have been snapshotted at Start Of Cycle and verified
at Finalization, at the HEAD being attested (order 717-3bvv). The guard writes
cycle-scoped stamps into
$GIT_DIR;selfreads them. Skipping the snapshot therefore costs you the marker — which is the point: cycle 16 on windows skipped it,verifyanswered with a missing-directory message about the PREVIOUS cycle's already-removed temp path, and a valid marker printed anyway. MO-SMOKE:grammar and the shared full-cycle rate limit are unchanged; a smoke run never emitsMO-FULL:.
DERIVE the marker, never type it — and let the self-check emit it. A
hand-typed SHA, or one half-copied from git push output, must be structurally
impossible. Do NOT assemble the line yourself. Run the self-attestation, which
reads every field from live git state, verifies local HEAD is durably on the
remote, and prints the verified line FOR you:
scripts/mo-full-attest.sh self # prints the marker on success; MO-FULL: FAIL + non-zero otherwise
Emit its stdout verbatim as your final line. self derives the same values the
raw form would —
printf 'MO-FULL: COMPLETE %s %s %s\n' \
"$(git rev-parse HEAD)" \
"$(git symbolic-ref --short HEAD)" \
"$(git ls-remote origin "refs/heads/$(git symbolic-ref --short HEAD)" | cut -f1)"
— but ALSO refuses to print anything when local HEAD is not the converged
remote head, so an unpushed or fabricated value is caught at emission, in every
lane. (MO_FULL_DISPOSITION=BLOCKED scripts/mo-full-attest.sh self for a
blocked-but-pushed cycle.)
Make the marker durable — the transcript is not the record (order 651-2x5s).
self verifies at emission time, but the emitted line lands in a transcript
nothing parses unless the litmus launcher runs; an operator-prompt or ./repeat
cycle emits it into the void. record closes that: it runs the SAME
verification and appends the verified line to the per-host ledger under
plan/mo-full-attestations.d/<host>.md, a committed, machine-parseable log the
local gate (scripts/check-mo-full-attestations.sh, wired into
./build.sh --check) re-verifies. Automation consumes the ledger, never the
transcript. The ledger line attests the cycle's WORK head; committing the
ledger moves the head, so the terminal marker is re-derived at the head that
CONTAINS the record (see Finalization step 9 and
methodology/mo-full-attestation.yaml). Do not hand-write a ledger line: only
record may append, and only after the verification passes.
This is not pedantry. On 2026-08-10 a full-mode cycle on this host emitted a
marker whose first eight characters came from the git push output and whose
remaining 32 were invented to look like a SHA. The work was genuinely
committed, pushed and green; only the proof was false (651-2x5s).
It went unnoticed because scripts/mo-full-attest.sh check — which would have
rejected it instantly, since it requires git ls-remote to converge on the
claimed value — is wired into exactly one caller, the
litmus:opencode-prompt-e2e-shape launcher. A cycle driven by an operator
prompt, a ./repeat loop, or a cron emits the marker into a transcript nothing
parses, so that check never runs. Without a check the marker is decorative,
and an unverified attestation is worse than none: it reads as proof and carries
none. The self mode closes that gap by moving the same convergence
verification to emission time: EVERY lane self-checks, because the cycle proves
its own marker in the one command that prints it.
Any full-mode run that exits without a valid, converging MO-FULL: marker
has not completed its exit contract — regardless of the process exit code.
scripts/mo-full-attest.sh fixture / scripts/test-mo-full-attest.sh
reproduce the breach shapes hermetically (missing marker, malformed, unpushed
local commit, branch mismatch, remote-head mismatch, a well-formed but
fabricated SHA that matches nothing on the remote, clean pass — 7/7).
Non-Negotiable Exit Contract
Local state is volatile. Before a successful exit, every meaningful result must be committed and pushed to the correct remote branch.
- Exit cleanliness is relative to the startup worktree boundary. No uncommitted change created by this cycle may remain.
- Pre-existing tracked changes and status-visible untracked files are
sibling/operator work, not temporary artifacts. Every path recorded by the
startup boundary MUST remain byte-identical on every exit, including blocked
exits. Never run broad
git clean,git checkout --,git restore, orgit resetagainst a worktree that was dirty at startup. Ignored trees are not hashed because they may contain an arbitrarily large build cache; they are not thereby cycle-owned and must not be overwritten. - Automated finalization must never delete or restore worktree paths. Put every
disposable diagnostic or scratch artifact under the unique external
$boundary_dir/tmp/directory. Files created in the worktree are meaningful cycle output: commit and push them, or stop with a blocker and leave them for explicit operator disposition. - Ensure any
tillandsiasbackground binaries or test processes are fully terminated. - No local-only commits.
- No completed work without a
plan/event or finding. - No e2e pass/fail without a dated plan report.
- No blocked state without a blocker, owner if known, and smallest next action.
- Explicitly log things that make you slower (e.g., repeated steps, invalidated caches, uncoordinated scripts) into
plan/issues/.
SALVAGE BEFORE YOU REFUSE (order 872-c9nd). This is not optional and it comes FIRST — before the handoff, before the blocker, before anything else.
scripts/salvage-dirty-worktree.sh <slug> # -> ok:salvaged:<ref>:<sha>
The script handles a DIRTY tree OR a CLEAN tree whose HEAD is on no origin
ref (order 1146-8j7i). For a dirty tree, it pushes a COPY — tracked
modifications, deletions and untracked files — to
salvage/<host>/<yyyymmdd>-<slug> on origin, and it CANNOT touch the
worktree: it builds the commit through a temporary GIT_INDEX_FILE and
plumbing (write-tree / commit-tree), never a stash, an add against the
real index, or a checkout. Verified: worktree status and .git/index are
byte-identical afterwards, and git show <sha>:<path> returns an untracked
file's full content.
THE SAME SCRIPT ALSO COVERS A CLEAN TREE THAT IS NOT A SAFE ONE (order
1146-8j7i): if git status is empty but HEAD itself is not reachable from any
origin ref — a finished, gate-passing commit stranded when a later trunk merge
reds the gate and every push is then refused — it pushes HEAD to the same
salvage ref and answers ok:salvaged-commits:<ref>:<sha> instead of
ok:salvage-not-needed. A host whose gate just went red on an otherwise-clean
tree should run this script, not open a fresh work/<order> ref that the same
red gate will refuse for the same reason the commit is stranded.
A tool whose name describes its original case will not be found by someone
in its extended case: salvage-dirty-worktree.sh is also the hand-off lane
for a host that cannot gate (1177-k4jq); the work/ lane is the GATED
hand-off and demands the stamp. A host whose own gate cannot complete —
memory-ceilinged, credential-dead, or otherwise unable to produce a green —
cannot use work/<order> at all: the relay-ref lane still requires the full
gate on first push, which is exactly what such a host cannot supply. Salvage
the finished commit to salvage/<host>/<yyyymmdd>-<order> instead and name it
explicitly as an UNGATED hand-off in the report; the host or orchestrator that
relays it gates it there, next pass (drill: the lenovinha entries of 2026-09-14).
WHY THIS RULE EXISTS, and it is the most expensive lesson in this file. On 2026-08-23 a host wedged with 16 modified paths and one untracked litmus file belonging to two claimed packets. Three consecutive cycles refused the dirty tree, verified all 17 paths byte-identical to their boundary snapshots, and wrote increasingly detailed prose about the diff. On 2026-08-24T06:09Z the checkout was replaced by a fresh clone. Four hours of finished work are unrecoverable; the untracked file's name appears in no commit on any branch.
The boundary guard did its job perfectly and protected a directory that someone then deleted wholesale. A guard that forbids the AGENT from touching the work does not forbid anything else from touching it, and a prose description of a diff is not a copy of it. The refusal path was a place work could sit indefinitely; it is now a place work passes THROUGH on its way to origin.
A dirty-start preflight refusal is not a work cycle and is the sole exception
to in-checkout blocker filing: touching plan/ would itself violate the
startup boundary. Report blocked: dirty-start-worktree, owner, the exact
status paths, the salvage ref and sha, and the smallest next action in the
final handoff so the clean host/orchestrator can file it durably. Do not create
then delete a blocker file inside either the shared checkout or $boundary_dir.
If the salvage itself fails, say so loudly and do NOT soften the refusal: an unsalvaged dirty tree is the exact state that cost four hours, and the operator needs to know the copy does not exist before deciding what to do with the directory.
AN OPERATOR LICENCE CAN GO STALE, AND A CLEAN TREE IS HOW YOU KNOW. When a
prompt authorises you to land dirt — order 833-fpe7's resumable: verdict
or an operator sentence naming specific work to review
and land — CHECK THAT THE DIRT IS STILL THERE before acting on it. If
git status --porcelain --untracked-files=all is empty, the premise of the
licence is gone: answer "the premise is gone", say what the licence expected
to find, and stop. Do NOT reconstruct what you think it referred to and land
that instead. This is how 872-c9nd was discovered: an operator relaunched a host
with a verbatim unblock prompt hours after the checkout had been re-cloned, and
the honest answer was that there was nothing left to land.
If a push fails after three fetch/rebase retries, mark the active plan item
blocked or failed-retryable, include the failed push output, and stop.
Host Classification
Detect host at the start of every cycle:
forge: Inside the Tillandsias developer forge container (typically detected by checking ifTILLANDSIAS_HOST_KINDis set toforge).linux_immutable: Linux with/run/ostree-bootedpresent orrpm-ostreeon PATH.linux_mutable: Linux without the immutable marker (and not inside the forge container).macos: Darwin.windows: Windows, MSYS, MINGW, or PowerShell host.
Canonical branches:
- Linux shared/integration:
linux-next - macOS code:
osx-next - Windows code:
windows-next - Release:
mainthrough PR only
All plan/, methodology/, openspec/, and cheatsheets/ files consider linux-next their canonical home. However, agents working on platform branches (windows-next, osx-next) MUST commit and push all edits (including plan updates) directly to their active platform branch. The Linux coordinator will merge these branches back into linux-next during the /multihost-orchestration pass.
Start Of Day / Post-Restart Gate (run FIRST, once per day; methodology development_environment_lifecycle)
Every host restarts with the prompt "Use the ./skills/meta-orchestration skill", so THIS gate is how a restart or a new day gets its maintenance and verification. It is idempotent and cheap when already done today, and the marker that makes it so is now REAL and CHECKABLE (order 801-qasc) — run the check, do not eyeball it:
scripts/check-daily-maintenance.sh check # exit 0 = today's gate ran; skip the body
Read the push guards too, every time, even when the day's gate is current
(see "Push Guards Armed" below): tillandsias-plan script run scripts/lua/check-hooks-armed.lua.
It prints exactly one line matching
^(ok:daily-maintenance-(current|stamped):[0-9]{4}-[0-9]{2}-[0-9]{2}|skip:forge-exempt|due:(no-marker|unreadable-marker|stale:[0-9]{4}-[0-9]{2}-[0-9]{2}))$
and exits 0 only when today's gate is recorded. due:* means run the body;
a corrupt or missing marker is due:, never a pass. From 2026-08-13 to
2026-08-17 the marker this section names existed only as prose — nothing wrote
it, nothing read it — so "did today's gate run" had no answer at all, one cycle
inherited a false "already stamped" premise from its brief and skipped the body,
and the next found nothing to confirm. An unobservable gate is indistinguishable
from one that never runs, and the cheapest way to satisfy it is to skip it.
On a durable bare-metal DEVELOPMENT host (not an ephemeral forge):
-
Post-restart verification (only when the stack looks freshly booted — MCP experts just rebuilt, images/binary possibly toolchain-bumped): run the durable checklist
plan/issues/fleet-restart-post-restart-checklist-2026-08-13.md(order 718-nuvm). Do NOT trustplan_answer/project_answeruntil the MCP experts servesource_commit == git rev-parse HEAD; verify clock/NTP sync (a skewed clock breaks SSH-CA cert TTLs + freshness stamps); rebuild forge images at the installed VERSION before launching any forge (version-skew DOA); reinstall per-checkout git hooks; ensure the router sidecar is staged as a build artifact (710-w9kc). -
Daily maintenance (methodology
development_environment_lifecycle.start_of_day_maintenance): build-cache GC perbuild_cache_hygiene(cargo clean when bloated/stale),scripts/nix-toolbox.sh gc— never a barenix store gc, which would delete the whole persistent cache rather than prune it (build_cache_hygiene.nix_store_policy) —podman image pruneof superseded versioned images, the nix-lane guards on hosts with nix (scripts/test-nix-toolbox.sh,scripts/check-nix-deps-stability.sh— daily, not per-commit: each is a flake evaluation), the enclave nix binary cache on Linux hosts with a running enclave (scripts/nix-cache-service.sh ensurethenscripts/test-nix-cache-service.sh— order 801-kqme; serves the persistent 795-h8er store to the enclave so a disposable forge lands warm without any host path being mounted into it), reap defunct delegate handles (delegate-outcome.sh sweep), verify the dev-environment expert containers are up + fresh (dev_environment_experts— the same ephemeral RAG experts + commit-hook RAG retraining the forge runs), and confirm this host is visible in the capability matrix (order 850-bif2):scripts/check-capability-row.sh # ok:capability-row-current:<host> | due:no-capability-row:<host> # | stale:capability-row-drifted:<host>:row-only=…,probe-only=… # | stale:capability-row-expired:<host>:age=<n>sOn Linux, confirm this host is not carrying the orphaned enclave proxy block (order 923-rmtw):
scripts/check-containers-conf-proxy-env.sh # ok:… | drift:… | unavailable:…[engine] envin containers.conf is injected into EVERY container on every network, andproxyresolves only inside the enclave. Init used to write that block and could never converge it, so hosts provisioned before 801-kqme kept a no_proxy list withoutnix-cache— that was four days of phantom 883-ncrs "cache RSTs" and one broken nix e2e, hand-repaired on macuahuitl and lenovinha. Ondrift:containers-conf-proxy-env-present, runtillandsias --init: it now REMOVES the block, and containers get proxy env per-container. A commented-out block still counts as drift — that is how yoga ended up with no proxy env at all while init reported success.Report scratchpad headroom in the same breath (order 915-wkm2) — one line, ADVISORY, exits 0 on every path:
tillandsias-plan script run scripts/lua/check-scratchpad-headroom.lua # ok:/degraded:scratchpad-headroom-low:/skip:The agent scratchpad is tmpfs; systemd sizes /tmp at 50% of RAM but a usrquota can sit far below that, so
dfreports gigabytes free while every write returns EDQUOT — the host stays healthy and only the agent's tooling dies. That wedged macuahuitl for several cycles: it presents as "my shell is broken", not "I filled a disk". Ondegraded:do NOT build in the scratchpad; build in the checkout. The threshold is absolute (one cold Rusttarget/, 2.6-4.4 GB) rather than a percentage, because the fleet's quotas span 4.6x — lenovinha warns at 29% used while macuahuitl is fine at 62%, so any "warn above N%" rule passes the host that will wedge and warns the one that will not.On
due:, publish the row THIS cycle — the matrix was silent for 5 of 7 hosts because nothing ever asked, and capability routing (847-wgy4) cannot route to hardware it cannot see:bash scripts/host-capability-probe.sh --fragment \ > "plan/index.d/$(date -u +%Y%m%dt%H%M%Sz)-capability-row-$(hostname -s | tr 'A-Z' 'a-z').yaml"then commit it with the cycle's plan changes. Never hand-assemble a row (the unquoted-heredoc incident on 850-bif2 is why the generator exists).
-
Stamp the marker, NAMING what actually ran — the stamp is refused without
--steps, because a stamp that records "something happened" without recording what restores the same unfalsifiability one level up:scripts/check-daily-maintenance.sh stamp --host <host> \ --steps 'delegate-sweep:<result>,podman-prune:<result>,nix-gc:<result>,cargo-gc:<result>'Record
skipped-absent/deferred-<reason>honestly for steps this host cannot or did not run; a partial pass that says so is worth more than a green that says nothing.scripts/check-daily-maintenance.sh showprints the last claim. Ephemeral forges skip this whole gate (they discard their substrate on teardown) and readskip:forge-exempt.
Then favor the expert system for the cycle's work-pull/triage/debug/research
(methodology expert_first_work): ask the experts first; on a gap, fall back
with a loud warning naming the gap, never silently.
How to invoke the gate (read this before your first ./build.sh)
Two environment variables are not optional and neither is discoverable by
reading build.sh. They were carried in operator prompt text for ten cycles
before anyone noticed the skill never mentioned them — which meant the
bootstrap contract ("Use the ./skills/meta-orchestration skill." and nothing
else) was quietly false for any fresh host.
TILLANDSIAS_SKIP_VERSION_BUMP=1 ./build.sh --check # every time
TILLANDSIAS_SKIP_VERSION_BUMP=1 TILLANDSIAS_FORCE_CHECK=1 ./build.sh --check # to VERIFY
TILLANDSIAS_SKIP_VERSION_BUMP=1— always. Without it the gate bumps VERSION as a side effect and the pre-push hook then refuses YOUR OWN push, because VERSION may not change on a platform branch (643-64bx). The blast radius is six files — VERSION, Cargo.lock and four crate Cargo.tomls — so a remedy that reverts only VERSION leaves the tree dirty and the push still refused. Measured live on linux-next 2026-08-20.TILLANDSIAS_FORCE_CHECK=1whenever the green matters. Since 765-tkq2 a plain--checkMEMOISES: on an unchanged tree it printsok:gate-freshand re-runs nothing, in 0.4s instead of 10s. That is correct for the inner loop and useless as evidence. A green you did not force proves only that the tree has not changed since some earlier green.
Killing the gate does not kill the gate (1132-r4mt). ./build.sh --check
re-execs inside the tillandsias-builder toolbox via podman exec, so
killing the host-side wrapper reaps only the wrapper: measured on pirria, the
container-side build.sh kept running 12 minutes after it was "stopped",
concurrently with the gate started after it — and two gates racing one
checkout is the leading mechanism for the archiver-fixture flake that cost
two full gates in a night (reproduced independently on yoga). SIGTERM to the
wrapper is inert; only SIGKILL to the container-side pid and its child ends
it. The discriminator is NOT conmon parentage — every container-side
build.sh under toolbox dispatch has parent conmon while its launcher
chain (toolbox run → podman exec) is alive, so that reading would record
every healthy gate as a stray. A stray is a container-side pid whose
HOST-SIDE podman exec no longer exists (measured by yoga on 2026-09-13 on a
healthy running gate). Before relaunching any gate or land you stopped — or
that a harness reaped — check for a survivor by that test, and force the
relaunch: a stamped ok:gate-fresh no-op proves nothing about strays. Match
ps output by NAME, never by field index — an index match once wrote an
empty file and read as "no stray processes" (drill: plan/issues/fleet-restart-2026-09-12.md, yoga: 1132-r4mt advanced at 191d606db (MO-FULL 90dcbbe07), claim released, criteria 2 and 3 open).
Where standing direction lives
Ask, do not guess:
tillandsias-plan answer "what is the current Direction?"
tillandsias-plan next <role>
The Direction section is operator-owned and is the one place a theme is
declared. Per-row carry-forward lives in each row's next_action, which
next prints beside the row it belongs to.
A caveat learned the hard way, 2026-08-22. Cross-cutting direction parked in
the next_action of a CLAIMED row is invisible: claiming removes the row from
selection, so next never shows it and a fresh host cannot find it. That is
R1 working as designed, and it means a claimed row is the wrong home for
anything a future host must read. Standing direction belongs in the Direction
section or in this skill; next_action is for the row's own next step.
Joining the fleet (read this if you are not macuahuitl)
As of 2026-08-23 the fleet is FIVE working hosts, not one: macuahuitl
(linux_mutable, coordinator), lenovinha and yoga (Silverblue, linux-next),
tlatoanis-macbook-air (osx-next), and yolanda (Windows 11, windows-next).
All five have completed attested cycles. The staged rejoin this section used to
describe is DONE; the three mechanisms it was gating on — claiming, the
first-attestation lane, and capability rows — all landed on 2026-08-22/23.
NOT EVERY HOST IS A DRAIN HOST. Machines with roughly four cores are the
fleet's deliberate LOWER BOUND and take profiling and characterization work in
their own tier, never the general queue (operator mandate 2026-08-16 for
esmeraldinha, extended 2026-08-23 to its Silverblue cousin). A slow host in the
general queue produces slow duplicates of work a faster host already claimed.
The SELECTOR now enforces this (847-wgy4): it probes physical cores locally,
routes a <=4-core host to low-end-tagged work only, refuses loudly
(refused:no-tier-work) rather than falling back to the general queue, and
subtracts that tier from everyone else's pool — a fast host running the
floor's profiling work would measure the wrong machine. Override with
TILLANDSIAS_HOST_TIER when the probe misreads a host. Separation between
general hosts is ordinal: your rank in the capability-matrix roster picks
your epic (route=rank:<r>/<n> in the batch header), so publishing your row
(850-bif2, above) is what buys you a collision-free lane; a rowless host
falls back to the seeded pick and R1 claiming.
The criteria that gated the original rejoin, kept because they are still what "working" means:
- Claiming is live.
expire-claimsreports a non-zeroin_progress, and a claimed row is genuinely hidden fromnext <role>. Verified on a real claim, both halves. What one host CANNOT prove is the cross-host half — a row claimed HERE being skipped THERE. That is the second host's first job, and it is worth doing deliberately before it drains anything. - The selector separates hosts.
select-work-batch.sh <role> --seed <host>returns disjoint batches for different seeds — different epic, different packets, andurgent=no longer forces every host to one head. This FAILED on 2026-08-19 (three seeds, one byte-identical batch) and the--seedflag did not exist then. Pass your hostname as--seed. The default is${TILLANDSIAS_HOST_KIND:-host}-$(date -u +%Y%m%d)(scripts/select-work-batch.sh:232), which varies by host KIND and date — not by host. Two Linux boxes therefore derive the same seed every day whether or not that variable is set, so separation is available but NOT automatic: a second host that forgets the flag collides exactly as the fleet did before.
The third is a fleet property no single host can close: the queue must stop growing faster than it drains. One host currently measures well inside the bound, but that reading says nothing about what a second host does to it — which is precisely why hosts rejoin one at a time and the number is re-measured after each.
On immutable Linux, run the update-skew probe at cycle start and carry its verdict into the handoff (order 1165-g6wx):
scripts/probe-silverblue-update-skew.sh # -> skew: | ok:no-skew | could-not-run:
Read-only: it runs no rpm-ostree command that can mutate a deployment, and it
exists so a cycle SAYS "skew" instead of a host guessing. A skew: verdict means
this host cannot apply the offered update at all — akmods is layered, the base
kernel has outrun the repo's kernel-devel-matched, depsolve fails and nothing
stages, while --check and GNOME Software keep reporting "ready, requires
restart". Do not treat it as a cycle failure and do not act on it: the three
remedies all change what is installed on someone's workstation and are the
operator's decision. Report the verdict and carry on. could-not-run: is not a
clean verdict either — it means the question could not be asked.
See cheatsheets/runtime/silverblue-updates.md.
If you are on immutable Linux (Silverblue/Kinoite), two things differ.
First, ./build.sh transparently re-execs inside the tillandsias-builder
toolbox (scripts/with-tillandsias-builder.sh), creating it with
toolbox create --assumeyes on first use. So unlike a mutable host, your gate
NEEDS a working podman — a Silverblue box with podman broken cannot run
--check at all, where a mutable box happily would. The first --check of a
fresh checkout spends its opening minutes building the toolbox — measured on
yoga (Silverblue, Ryzen AI 5 340, ~250 Mbit/s registry link, 2026-08-23; filed
as an event on 850-bif2): image pull 11.5s (355 MiB compressed), toolbox create 0.1s, then 131.9s for the first forced --check (dnf 17s, rustup 14s,
gate ~100s), with a warm ~/.cargo — a truly pristine home also pays crates.io
downloads on top. That is the toolbox being built, not a hang. (The previous
"minutes" figure here was an unmeasured inference written on a mutable host
that never executes this path — b629bb379.)
Second, scripts/e2e-preflight.sh carries NO immutability logic and will report
eligible to you. Do not believe it: the skill's E2E table routes immutable
Linux to /smoke-curl-install-and-test-e2e (test a PUBLISHED release), never to
/build-install-and-smoke-test-e2e (test a LOCAL build). The preflight is
answering a narrower question than the one you are asking it.
If you are on macOS (Darwin), most defaults hold — these are the parts that
do not (851-gpb5, learned by the first Mac to join). Your branch is
osx-next: commit and push everything there, and before EVERY push merge
origin/linux-next into it — methodology's pre-push gate
(pull_merge_cadence.pre_push_gate; Finalization step 6). Run
scripts/install-hooks.sh once so the v5 pre-push hook actually enforces that
merge on your checkout. ./build.sh --check/--test work natively, but run
scripts/check-host-tools.sh FIRST — it prints every missing host tool with its
brew install line (order 989-ykks). That replaces the list of host tools this
paragraph used to carry, and the replacement is not tidying: the list was wrong
in two ways in one week (it omitted coreutils, a hard requirement of the
credential gate per 988-7kxf, and pkg-config, without which --check cannot
start), and a list is only ever written by someone whose machine already works.
The host that HAS a tool cannot detect that it is undocumented; it reports ok:
and lands all night while the host that lacks it debugs an unrelated failure
hours later. A probe cannot drift that way. --install is REFUSED by design
(723-whrx): the macOS build path is scripts/build-macos-tray.sh — wrapped by
/build-macos-tray, which also files findings to
plan/issues/macos-build-findings-<DATE>.md — and local-build e2e
(/build-install-and-smoke-test-e2e) destroys and re-provisions the
Virtualization.framework VM directory, not a podman store. The system shell is
bash 3.2 with BSD userland: no GNU-only flags in anything a Mac must run (no
xargs -r, no suffix-less sed -i), and sha256sum only exists on macOS
13+ — use the sha256sum-or-shasum -a 256 dispatch
(scripts/build-sidecar.sh, scripts/gate-stamp.sh). Expect the MCP experts
to be DOWN on first boot (down:forge-plan, degraded(not-built)):
scripts/cycle-preflight.sh builds ./target/release/tillandsias-plan, the
same binary the MCP wrapper serves — work through it by path and record the
outage, per mcp_first_read_path. And like every joining host, pass your
hostname as --seed to select-work-batch.sh.
Publish your capability row before you drain anything (order 850-bif2).
scripts/check-capability-row.sh answers whether the matrix can see you AND
whether what it sees is still true (order 889-ewvt); on due:, stale:…drifted
or stale:…expired generate and commit a row with
scripts/host-capability-probe.sh --fragment (Linux/macOS/forge loci; the
windows-host locus keeps its own generator). drifted is the urgent one: the
committed row disagrees with a live probe, and routing that consumes it will
send work to hardware this host does not have — that is how the release gate
was routed to yoga for a gpu/container/ollama engine yoga never had. A joining host that drains work
while publishing nothing is how the matrix went silent for 5 of 7 hosts —
and capability-aware routing (847-wgy4) cannot route to hardware the matrix
cannot see.
Expect to be offered other hosts' abandoned work. Claims expire on a 24h lease, and expired rows return to the pool with their history intact — a row you are offered may have been started elsewhere and dropped. Read the row's events before assuming it is fresh.
The bootstrap prompt is Use the ./skills/meta-orchestration skill. and nothing
else. If you needed something said to you beyond that sentence, the thing you
needed was missing from this skill or the ledger, and THAT is the defect worth
filing — not the prompt.
Start Of Cycle
-
Rebuild the instrument before using it (operator directive 2026-08-13):
scripts/cycle-preflight.sh # -> ok:cycle-preflight:<plan>:<inference>The project believes in idempotency and ephemerality — everything should be safe to destroy and relaunch at any moment, Erlang style — so a cycle never inherits a component from the previous one and hopes it is current. This rebuilds
tillandsias-plan(the binary every expert call, the batch selector, every ledger write and every closure check goes through) and re-establishes the dev inference endpoint. Both are idempotent; the common path costs a no-opcargo buildand one HTTP round trip, measured at ~2.8s.The
<plan>segment names WHICH instrument the cycle is running on, andexistingis a legitimate verdict (order 1004-ws5q):verdict meaning rebuiltcargo was present and the instrument was built or confirmed current existingno cargo on this host, but a runnable binary resolved — the cycle proceeds on it skippedthe build step was skipped by CYCLE_PREFLIGHT_SKIP_BUILD=1blocked:preflight:plan:cargo-absentno cargo AND no runnable binary — no instrument, and the cycle must not start existingexists because the old order of checks was backwards: preflight declared the COMPILER absent before ever looking for a BINARY, so a host doing compile-free work lost its whole slot with a healthy instrument on disk. Measured on pirria 2026-09-04, where a floor-tier release smoke — which compiles nothing — was refused a 4h slot.CYCLE_PREFLIGHT_SKIP_BUILD=1is for compile-free work on a host that cannot compile, and nothing else. It is NOT a way past a red build on a host that can compile: that is a broken instrument, and selecting work with one is the failure this whole step exists to prevent. Since 1004-ws5q a host with no cargo but a working binary no longer needs the variable at all — preflight answersexistingon its own — so reaching for it should now be rare enough to be worth a second thought.It rebuilds the INSTRUMENT, not the product:
./build.sh --checkalready compiles what it validates, and rebuilding everything on a schedule is a heavier decision than this step is making.Confirm the rebuilt instrument is CURRENT before minting or writing, and again after any merge that touches crates/tillandsias-plan/src (a relay of a plan-binary change makes the binary you built an hour ago stale; the next
next-orderthen refuses withstale-plan-binary) (1513-pppk):bash scripts/check-plan-binary-current.sh # -> ends ok:plan-binary-current; any stale:* or blocked:* line is a stop: # cargo build --release -p tillandsias-plan, then re-runA
blocked:preflight:*verdict means do not start the cycle — selecting work with an unverified instrument is the one failure the loop cannot reason its way out of, because the tool it would reason WITH is the stale thing. That is not hypothetical: a selector change on 2026-08-13 added a subcommand every host's binary predated, and this checkout went on refusing until someone rebuilt by hand. Inference is a REPORT inside that verdict, never a gate — the deterministic expert tiers work without it, and a host with no network is degraded, not broken. When inference cannot be established, the report segment readsdegraded:<reason>(neverblocked:*— the gate word is reserved forblocked:preflight:*); continue the cycle.
0b. Name yourself before the first ledger write (orders 756-hn3a, 885-zvzu):
agent_id="$(scripts/agent-identity.sh id <backend>)" || exit 1 # claude|codex|opencode|gemini
Every append-event, set-field reason and loop-status entry this cycle
writes carries an agent_id, and the plan binary REFUSES a non-canonical
one (874-idnt): <platform>-<workstation>-<backend>-<utc-timestamp>,
sanitised to [a-z0-9-]. Never hand-compose it. The helper resolves it
from stable sources and itself refuses
(refused:agent-identity:empty-<component>, empty stdout) rather than mint
an incomplete id; on that refusal append, claim and push nothing.
WHY IT IS HERE: this skill is the whole bootstrap contract, and it never
named the helper. Measured on macuahuitl 2026-08-25: the first
append-event of a cycle driven by the documented prompt hand-wrote an id
and was refused, and the cycle recovered only by reading the error text.
Same class as the two gate variables in "How to invoke the gate", which
lived in operator prompt text for ten cycles.
-
Record UTC time, host kind, current branch, worktree path, and sibling heads. Report this host's scheduler posture in the same breath — it is one line and it answers the question an operator otherwise has to read a transcript for:
tillandsias-plan script run scripts/lua/check-cycle-scheduler.lua # armed? last fire? next due?ADVISORY, NEVER A GATE (order 856-s56y exit criterion 5, wired 865-j3kd).
blocked:cycle-scheduler:not-installed(exit 1;due:not-installedbefore the 1624-bv73 Lua port) is a true and acceptable answer: macuahuitl is driven by an external hourly loop rather than a systemd timer, and a host with no durable timer is differently-scheduled, not broken. What the line buys is that "no scheduler is armed here" becomes a stated fact rather than something nobody discovers until a host has been quietly not-cycling for a day — which is the 856-s56y failure, and the same shape as a wedged host reading as silent (864-t4nq).It is wired here because the guard shipped invoked by nothing and audit-guard-activation therefore failed
./build.sh --ci-full, one of three checks that left the trunk unreleasable for a week (865-n8vq). The auditor greps for the guard's NAME, so a mention would have cleared the verdict while leaving the guard as dead as it was; an orphaned guard is fixed by invoking it. Arming an actual scheduler is 856-s56y's own work and remains yoga's. -
git fetch origin --prune, then run the Credential Channel Guard and the Committable Branch Guard below before any committable work. Run the MCP Expert Health Probe here too — it is advisory and never blocks, but it must run BEFORE the cycle's first expert read, or an outage during that read has no recorded baseline to be visible against. 2b. Acquire the checkout lock — EVERY lane, before the boundary snapshot (order 873-zcim):TILLANDSIAS_CYCLE_HOLDER_PID=$PPID scripts/cycle-checkout-lock.sh acquire \ --lane <how-this-cycle-was-launched> --source "<prompt source, one line>"The no-stacking lock used to be taken only by the driver lane (tillandsias-cycle-driver.sh), so a cycle launched by an operator prompt, a /loop cron, or a cloud schedule acquired nothing and STACKED on a running driver in the same worktree — measured on yoga 2026-08-24, 21 minutes into a driver cycle, duplicating its claims. The lock guarded the driver lane; the thing two agents contend for is the CHECKOUT.
On
skip:overlap-lock-held:<holder>DO NOT PROCEED: the verdict names who holds the checkout (lane, pid, start, source). Report it as the cycle's final output and exit — this is the designed outcome, not a failure, and the refusal is recorded durably OUTSIDE the checkout in~/.cache/tillandsias/overlap-refusals.jsonl, so a refused-for-overlap cycle is distinguishable in the record from a cycle that ran and found nothing (873-zcim criterion 3). Do not retry in a loop; the next scheduled fire retries on its own clock.TILLANDSIAS_CYCLE_HOLDER_PID=$PPIDmust be evaluated in YOUR shell — it anchors liveness to the agent-harness process that spans the whole cycle. The script's own default is one shell too deep and dies with the tool call.DECIDED (873-zcim criterion 4): a second agent NEVER works in a locked checkout. It WAITS for the lock: concurrent work on one host is serialized, never moved to a sibling git worktree or a temp clone (operator direction 2026-10-11, methodology multi-host-development.yaml single_checkout_branching; order 1620-67ji). The worktree/clone technique yoga once used here is retired: every sibling checkout sits outside the session's workspace and puts its cleanup in front of the operator. Release at Finalization (step 9b) with
TILLANDSIAS_CYCLE_HOLDER_PID=$PPID scripts/cycle-checkout-lock.sh release.IF YOU RELOCATE, TAKE THE BOUNDARY IN THE WORKTREE YOU RELOCATE TO (order 983-xha6). This is one line and its absence cost the fleet visibility three separate times on 2026-09-03:
cd <the clone or worktree you will actually work in> boundary_dir="$(mktemp -d "${TMPDIR:-/tmp}/meta-orchestration-boundary.XXXXXX")" scripts/meta-orchestration-worktree-guard.sh snapshot "$boundary_dir"The guard's stamps live in
$(git rev-parse --git-dir), so they are ALREADY per-checkout and a relocated worktree can hold its own boundary — verified in a fresh clone: snapshot, commit,verify->ok: startup worktree boundary preserved. Nothing needed building. What was missing was anyone saying to do it, so step 3 below snapshotted the checkout the cycle then correctly stopped using, the clone never got a boundary, andmo-full-attestrefused at Finalization withno verified startup boundary for this cycle— AFTER a green gate with every commit pushed.The consequence is not a missing line of output.
MO-FULL:is how automation learns a cycle finished, so a lawfully relocated cycle is INDISTINGUISHABLE FROM ONE THAT DIED: the trunk carries its commits while the fleet record says the host never reported. A forge hits this by default — its checkout is a clone, it is often seeded frommain(965-rb3v), andmainis refused bycheck-committable-branch.sh— so relocating is the NORMAL forge path, not an exception.Snapshotting a fresh clone is not a formality and not vacuous: the clone starts clean, so the boundary proves the cycle left nothing uncommitted behind it. What IS vacuous — and must never be done to obtain a marker — is snapshotting AFTER the work and verifying immediately: that compares a tree against itself and emits a valid-looking proof of nothing (651-2x5s). If you reach Finalization having never snapshotted, the honest outcome is a BLOCKED exit with no marker; do not manufacture one.
-
Snapshot the startup boundary before classifying or changing any path:
boundary_dir="$(mktemp -d "${TMPDIR:-/tmp}/meta-orchestration-boundary.XXXXXX")" scripts/meta-orchestration-worktree-guard.sh snapshot "$boundary_dir"The guard records
git status --porcelain=v1 -z --untracked-files=allplus content hashes for every status-visible dirty path. If the worktree is dirty, treat every recorded path as immutable sibling/operator work unless the operator explicitly identifies it as disposable in the current prompt. Refuse the cycle and do not begin committable work. Report the dirty-start blocker through the final handoff as defined above. Do not commit, discard, restore, reset, or clean unknown startup dirt.Before that refusal, run
scripts/salvage-dirty-worktree.sh <slug>(order 872-c9nd, full rationale in the Exit Contract above). Refusing protects the work from YOU; it does not protect it from a fresh clone, and on 2026-08-24 a fresh clone is exactly what took it. The salvage cannot touch the worktree — temporary index and plumbing only — so it is safe to run on dirt you have just been forbidden to alter, and it must run BEFORE the two detectors below: whether the dirt turns out to beresumable:changes what you may LAND, never whether a copy should exist. -
Launch-generated opsx dirt is a refusal (deterministic, order 1440-w8g8): before refusing on startup dirt, name WHICH dirt it is:
scripts/check-opsx-generated-dirt.shIt prints exactly one line matching
^(ok:clean-tree|launch-dirt:opsx-only|non-opsx:.*)$.launch-dirt:opsx-only(exit 5) means every dirty path is the opsx/openspec command/skill set the openspec CLI generates — a forge launch rewrote tracked files, which 1422-w3p8 made impossible. That is a REGRESSION to report, not content to land: refuse the cycle exactly as for any dirty start (salvage first, as above), name 1422-w3p8 and the verdict line in the handoff, and do not commit, discard, restore or clean the dirt. Order 540 (2026-07-31) used to commit it as achore(opsx)sync; the operator reversed that on 2026-09-27 (the dirt "should not exist"), and 64cceb25d is what the old step produced. Moving the generated set to a new openspec version is a deliberateopenspec update+ commit under its own packet, never a cycle's reaction to dirt.non-opsx:(exit 3) is real sibling/operator dirt: fall through to 4b, then to the dirty-start refusal.ok:clean-tree(exit 4) means there is nothing to name. The checker is a falsifiable machine decision; do not substitute prose judgment for it. 4b. Resumable claim dirt (deterministic, order 833-fpe7): when the dirt isnon-opsx:, run the second detector before refusing:scripts/check-resumable-claim-dirt.shIt prints exactly one line matching
^(resumable:<order>(,<order>)*|ok:clean-tree|unattributable:.*)$and exits0only when EVERY dirty path is tracked, the folded ledger names a live claim OWNED BY THIS HOST (tillandsias-plan expire-claims --list-live: claim-convention event host, both claim and activity inside the TTL), and every dirty path's mtime postdates the oldest such claim. That is the signature of this host's OWN previous cycle interrupted between implement and commit — the dirt the refusal would otherwise deadlock on until expire-claims launders finished work into lost work (the 833-fpe7 shape; 814-iyu7 arriving by a second route).resumable:is a licence to REVIEW AND LAND, never to auto-commit: read the diff against each named order's packet, land what implements it as its own commit(s) citing the orders, then re-anchor with the guard'sre-snapshot(scripts/meta-orchestration-worktree-guard.sh). Whether an edit IMPLEMENTS the packet beside it is the agent's judgment; the detector only removes the deadlock. Anyunattributable:verdict falls through to the dirty-start refusal exactly as written. Pinned bylitmus:resumable-claim-dirt-shape. -
Update the active local branch from remote with fast-forward or an explicit merge from
origin/linux-nextinto the platform branch when appropriate.
5a. NOW check for sibling overlap (methodology sibling_heads_up_protocol).
Hosts can message each other directly — ListAgents to see who is live,
SendMessage to reach them. The capability is new and you will not
discover it on your own; the fleet coordinated exclusively through the
ledger and the trunk for months before anyone noticed it existed.
tillandsias-plan query --status in_progress # who is holding what, right now
AFTER THE MERGE, NOT BEFORE, AND THIS ORDERING IS THE WHOLE STEP. This check first shipped as step 2a — before the branch update — and was MEASURED BROKEN on the next cycle by its own author:
before the merge: 831-ezea only <- the other host's claim INVISIBLE
after the merge: 642-fedr + 831-ezea <- yoga's fragments.rs claim
git fetch at step 2 updates remote-tracking refs; it does NOT update the
worktree ledger that tillandsias-plan reads. So a check placed before the
merge answers about the claims as of your LAST merge, not as of now.
THE STALENESS IS NOT ONE-DIRECTIONAL, and the earlier wording here said it was. This passage used to end "a stale ledger always shows FEWER claims, never more." Measured on yoga 2026-08-26T07:26Z, one cycle after that sentence landed, running the same query either side of the merge:
before the merge: 799-tb7q + 831-ezea <- 799-tb7q was a PHANTOM
after the merge: 831-ezea only <- another host had released it
799-tb7q was in_progress in the stale ledger and ready at the new head:
a sibling finished a slice and released it in commits this host had fetched
but not merged. So a pre-merge check can show a claim that no longer exists
as easily as it can hide one that does — fewer claims when siblings have
been claiming, more when they have been releasing.
Both directions are harmful and they fail differently. Missing a live claim fails toward "no overlap", which is the safe-LOOKING answer and the one that costs a union. Seeing a released claim fails toward a needless heads-up, or toward skipping a packet that is free — cheaper, but it teaches the reader that the step produces noise, which is how a step stops being run.
expire-claims --list-live is the tool again as of 905-wjfj: it now prints
one row per in_progress packet and a rows: accounting line whose total must
equal the summary's in_progress=N. Before that fix it printed the count and
no rows — two hosts hit it on the same day — and the workaround here was
query --status in_progress, which still works and is a fine cross-check.
READ THE ROW TYPE, because the two mean opposite things.
live-claim <order> <pid> <host> <claimed-at> <last>— a sibling holds this. If it touches your surface, message that host.unclaimed-in-progress <order> <pid> - - <last>— in_progress, recent enough that no reaper will touch it, and nobody to send a heads-up to. The dashes are the actionable part. This row is not the quiet case; it is the one where the protocol has no addressee, so if it touches your surface you inspect the diff yourself rather than assuming silence means free.attention:list-live-partition-mismatch— a packet is in no bucket or in two. The enumeration is under-reporting; do not trust ano overlapverdict derived from it.
MEASURED 2026-08-26: 831-ezea sat in the unclaimed row for at least a day —
in_progress, with a gate step wired into ./build.sh --check, last activity
18h old and therefore inside the 24h TTL, so neither the reaper nor the
live-claim list would ever surface it. Exactly the row an overlap check
exists to show you, and the only one it could not.
If a live claim touches the SAME SURFACE your batch touches — a wire type, a
shared struct, a script another lane runs, a schema, a guard wired into
--check — send one HEADS UP before implementing: what you are touching, the
shape of the change, any sibling call sites you know of, and the ask. They
reply ACK plus comments. Two short messages.
WHY, structurally: THE UNION IS A STATE NO SINGLE BRANCH'S GATE OBSERVES.
Every branch can be green alone and the merge still broken, because no
branch's --check compiles the other's configuration. Care cannot close
that; only a second lane looking can. MEASURED 2026-08-26: macbook's
731-eupn was green on both branches and E0063 + E0308 in the union — a red
gate and ~30 minutes of coordinator cycle.
Prefer the message to the resolve-to-be-careful: a heads-up is a falsifiable act, care is not. "I considered the siblings" cannot be checked by anyone, including you an hour later.
DO NOT broadcast routine drain. Most work touches only your own lane, and a channel that carries everything is one nobody reads — the recipient is mid-cycle, holding a checkout lock, with expensive context. The heads-up is for what the ledger cannot carry in time: intent, before the fact.
Credential Channel Guard
Run immediately after git fetch and before any worker drain or committable
work.
Close the interactive escape hatches around EVERY push (order 860-g798, exit criterion 3):
export GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never
A credential problem must fail FAST AND LOUDLY, not hang. Measured on
esmeraldinha: the guard reported green (gh auth status held a token), the
cycle entered committable work, and the first push sat >10 minutes behind Git
Credential Manager's interactive prompt — a hang with no output, presenting as
a slow build on the host least able to afford the misdiagnosis. With the
prompts closed the same failure surfaces in seconds as an auth error the guard
now names (blocked:interactive-credential-helper, remedy printed). The guard
itself verifies the PUSH path since 860-g798 — ok:gh-keyring-push-verified
means a bounded non-interactive dry-run push actually authenticated, not
merely that a token exists somewhere. In SMOKE mode it is a REPORT, not a gate — a smoke run
performs no committable work, so a stale push credential cannot harm it (order
818-cgpn; see Smoke Mode Runbook rule 3a). The Cowork scheduled-task runtime can inherit dangling session sockets
(DBUS_SESSION_BUS_ADDRESS, SSH_AUTH_SOCK pointing into a non-existent
/run/user/<uid>) so anonymous reads succeed while every git push silently
fails for lack of a credential. See
plan/issues/cowork-headless-credential-isolation-2026-06-20.md.
Run the executable guard instead of re-deriving the check in prose.
(On Windows: ensure you run this via Git Bash, e.g. & "C:\Program Files\Git\bin\bash.exe" scripts/check-credential-channel.sh. PowerShell's bash alias defaults to an isolated WSL session that lacks host credentials).
scripts/check-credential-channel.sh
It prints exactly one line matching the falsifiable grammar
^(ok:[a-z0-9-]+|blocked:[a-z0-9-]+|missing:no-credential-channel)$ and exits
0 when a usable git-push credential channel is present, non-zero when it is
absent or blocked. A usable channel is present when ANY of these holds (the
script checks them in order):
<common-git-dir>/.gh-credentials(absolutegit rev-parse --path-format=absolute --git-common-dir, so every linked worktree shares it; 1409-65d5) exists and is non-empty (repo-local store helper), orGH_TOKENorGITHUB_TOKENis set in the environment, orgh auth statussucceeds (reachable, unlocked keyring), orTILLANDSIAS_HOST_KIND=forgeis set AND the enclave git mirror is reachable fororiginAND the mirror's published upstream write-authorization verdict (refs/tillandsias/upstream-auth/<state>/<epoch>, refreshed non-mutatingly byimages/git/probe-upstream-auth.sh) is fresh andauthorized(orlocal-only). Order 756-2jnj: reachability alone saidokon 2026-08-15 while GitHub 403'd the mirror's credential, and a forge lost two commits it discovered only at first push.ok:forge-git-mirrornow means BOTH halves — forge→mirror reachable AND mirror→upstream currently write-authorized.
Pinned by litmus:credential-channel-check-shape and
litmus:forge-upstream-auth-gate. A non-zero exit (verdict
missing:no-credential-channel, or any blocked:* verdict — e.g.
blocked:upstream-push-unauthorized for the 403 state,
blocked:upstream-no-credential, blocked:upstream-auth-stale,
blocked:upstream-auth-unpublished) fails the cycle on its own; do NOT proceed
into worker drain or any committable work. Instead
fail loud: file or update a blocker in plan/issues/ recording the exact
verdict, the owner (operator), and the smallest next
action (re-seed .git/.gh-credentials via the gh token, inject GH_TOKEN
into the task environment, or — for blocked:* — repair the mirror's Vault
GitHub token / its repo push permission), then stop. Accreting local-only
commits that cannot be pushed violates the Non-Negotiable Exit Contract and is
the precise velocity-killer this guard prevents.
Reads (git fetch/git ls-remote) succeeding is NOT evidence of a credential
channel — public-repo reads are anonymous. Verify write capability explicitly.
Bound every git push with a timeout; an empty push log is a refusal, not
slowness (macneo, two land stalls of 3000 s each). land: attempt 1 — push
was the last line written, the push log was ZERO bytes, and a stack sample
showed git-credential-osxkeychain blocked in CSSM_DecryptDataFinal — an
ACL confirmation dialog no non-GUI session can answer. The guard's
GIT_TERMINAL_PROMPT=0/GCM_INTERACTIVE=never above close git's own
prompts, not an OS helper below them; macbookair pushed six times on the same
helper and repo from inside a GUI session, so the variable is the session,
not the config. A genuine credential fault refuses WITH output; a keychain
hang produces none. Wrap the push (timeout <n> git push …), stat the log
twice before calling anything slow, and report
blocked:credential-helper-hung with the probe (drill: plan/issues/fleet-restart-2026-09-12.md, A push with no timeout emits nothing).
Committable Branch Guard
Run alongside the Credential Channel Guard, before any committable work.
Direct commits/pushes to main are forbidden — main advances only through
PR merges. Breach record: 34e60965, an antigravity forge cycle running ON
main that pushed a ~42k-line plan/index.yaml reserialization straight to
origin/main (reverted via PR #81; see
plan/issues/main-branch-direct-push-guard-2026-07-24.md).
Run the executable guard instead of re-deriving the check in prose:
scripts/check-committable-branch.sh
It prints exactly one line matching the falsifiable grammar
^(ok:branch-[A-Za-z0-9._/-]+|blocked:(committable-cycle-on-main|detached-head|not-a-git-repo))$
and exits 0 when HEAD is on a committable branch (any named branch except
main), non-zero when the checkout is on main, detached, or not a git
repository (fail closed). On any blocked:* verdict do NOT proceed into
worker drain or any committable work: switch to the host's canonical branch
(linux-next, windows-next, osx-next) or run the cycle read-only.
Read-only/inspection cycles on a main checkout remain allowed — the guard
gates committable cycles only. Pinned by
litmus:committable-branch-guard-shape.
Forge Expert Base Guard
Run before launching a forge from this checkout. Run the executable guard instead of re-deriving the check in prose:
tillandsias-plan script run scripts/lua/check-forge-expert-base.lua
It prints exactly one line matching
^(ok:(expert-base-ready|no-plan-crate)|blocked:(expert-sources-absent|not-a-git-repo))$
and exits 0 when a forge seeded from this checkout can build a working plan
expert, non-zero when it cannot.
Why this is a separate guard from the branch guard above: the launcher seeds a
forge's branch from this checkout's current branch
(read_host_project_current_branch → TILLANDSIAS_FORGE_SEED_BRANCH), so
whatever branch you are parked on becomes the next forge's base. main carries
no crates/tillandsias-plan/src/answer.rs, so a forge seeded from it builds a
pre-expert binary and then reports experts: ready truthfully while every
plan_answer / methodology_path returns confidence=unsupported. Truthful
state, wrong artifact. Breach record: 2026-07-30, an in-forge session found both
experts refusing the milestone's own exemplar question; two earlier cycles had
absorbed the same condition by noticing and hand-rebasing
(plan/loop_status.md:203, :243). Order 531.
The test is source presence, not a branch name — deliberately. The names
linux-next/windows-next/osx-next/main are Tillandsias's own conventions
and are explicitly not a runtime convention of the product
(methodology/multi-host-development.yaml:21-27), so a forge on an end user's
project must not assume them. Source presence is a fact about the checkout, and
it also catches the case a branch-name test cannot: when the seed and the actual
branch agree because this checkout was itself parked on a pre-expert base,
nothing looks wrong.
On blocked:expert-sources-absent, do NOT launch a forge expecting expert
answers. Switch this checkout to a branch carrying the expert sources first. On
ok:no-plan-crate the target project simply has no plan expert, which is normal
off-Tillandsias.
Push Guards Armed (read-only; Start Of Day and Start Of Cycle, order 1255-s4im)
tillandsias-plan script run scripts/lua/check-hooks-armed.lua # ok:hooks-armed:<marker>
It answers whether THIS checkout's pre-push hook carries the current
tillandsias-pre-push-v* marker, and it installs nothing:
ok:hooks-armed:<marker> (exit 0), degraded:hooks:absent or
degraded:hooks:stale:<have>-><want> (exit 1), refused:hooks:foreign-pre-push:*
or refused:hooks:<scope>-hooks-path:* (exit 3). On degraded: run
bash scripts/ensure-hooks.sh before any push: until then a push from here
runs none of the push guards, the release freeze among them. The plan-lane
scripts arm the hooks on their own (ensure-hooks.sh --prelude), but a host
whose first push is a WORK REF never runs one of them, which is why this is
read at the start of the day and of every cycle.
MCP Expert Health Probe (advisory — record, never block)
Run at Start-Of-Cycle, alongside the guards above:
scripts/check-mcp-expert-health.sh
It speaks the MCP initialize handshake to each expected expert and prints
exactly one line matching
^(ok:experts-healthy|down:[a-z0-9-]+(,[a-z0-9-]+)*|absent:not-registered|skip:[a-z0-9-]+)$,
appending one JSONL record per server to $TILLANDSIAS_EXPERT_HEALTH_LOG
(default /tmp/forge-expert-health.jsonl). ~40ms when healthy.
This is the concrete referent for mcp_first_read_path's "fall back and keep
going, THEN RECORD IT". That rule named no destination, and on 2026-08-14 two
hosts skipped the recording half on the same day: windows lost all 28 MCP tools
mid-session and fell back to tillandsias-plan.exe silently; linux ran an entire
cycle whose session had never loaded the servers at all. Both outages were
invisible in the ledger and would have stayed invisible (order 737-zcj5).
Unlike the guards above, a non-zero exit here does not fail the cycle — an unavailable expert is a degraded read path, never a blocked one. Record and continue:
down:<csv>/absent:not-registered— keep working through./target/release/tillandsias-plan(the same binary the MCP wrapper wraps) and let themcp_outage:line carry it into the handoff. Name the fallback reason, as the rule has always required.ok:experts-healthy— say nothing further. The probe is silent by design when healthy; a signal that fires every cycle is one nobody reads.
The handshake is not the surface (order 801-m9tk)
ok:experts-healthy means the registered servers answered when the PROBE
launched them. It does not mean the tools reached YOUR session. For three
consecutive windows cycles the probe printed ok:experts-healthy while not one
mcp__forge-plan__* / mcp__project-info__* tool was bound to the agent, so
every read silently fell back to ./target/release/tillandsias-plan. A server
that is up but not exposed is exactly the outage worth catching, and the
handshake probe calls it healthy — the order-531 shape again.
A script cannot see your tool surface; nothing in this repo can. You are the
only observer of that fact, so the surface half is attested, not measured —
and an unattested cycle reads unattested:no-surface-claim, never ok:.
Immediately after the health probe, look at your own tool surface and say so:
scripts/check-mcp-surface.sh attest exposed # mcp__* tools resolved this session
scripts/check-mcp-surface.sh attest unexposed # they did not — every read is a fallback
scripts/check-mcp-surface.sh check # the joined verdict
check joins the measured handshake with the attested surface and prints
ok:surface-exposed / unexposed:handshake-ok / down:<csv> /
absent:not-registered / unattested:* / skip:no-health-log. Advisory, never
a gate — like the probe it extends. cycle-metrics.sh folds a fresh unexposed
attestation into mcp: health=ok-unexposed and fires the mcp_outage: line, so
the degraded read path reaches the ledger without anyone choosing to write it
down. An exposed attestation leaves health=ok unchanged, and a claim older
than the freshness window labels nothing: a previous cycle's marker never
vouches for this one.
And the third question: is the server you reach running current code? (order 823-u3k9). The handshake probe launches its OWN instance from the registration, so it validates the FILE. It cannot see that your session's long-lived MCP process was started before a fix landed and is still serving the old text — measured on macuahuitl 2026-08-18, where 799-j4xd's fix was in the file, the index was fine, L0 was fine, and the expert answered with prose the fix had deleted while every signal read green. A fix in a file does not reach a process that already read it.
Like the surface, only you can observe this, and for the same reason: a subprocess cannot reach your server, and launching one is the defect. Call the live tool, read the build line out of its answer, attest it:
# 1. mcp__forge-plan__expert_capability (through YOUR tool surface, not a shell)
# 2. read its `server_build: forge-plan=<id> source=<path>` line
scripts/check-mcp-live-build.sh attest forge-plan=<id> --source <path>
# 3. mcp__project-info__project_info (1414-mjdw: the other long-lived server)
# 4. read its JSON "server_build": "project-info=<id> source=<path>" field
scripts/check-mcp-live-build.sh attest project-info=<id> --source <path>
scripts/check-mcp-live-build.sh check # the WORST attested server
scripts/check-mcp-live-build.sh check project-info # one server by name
Each server keeps its own attestation line, so a current forge-plan never
vouches for a stale project-info. A project-info answer with no
server_build field predates 1414-mjdw: attest project-info=unreported.
If the answer carries no server_build: line, that is the finding, not a
reason to skip: the running server predates the emitter. Attest
forge-plan=unreported and it reads stale:live-server-build-unreported:….
check prints ok:live-build-current:<server> /
stale:live-server-build:<server>:<attested>!=<ondisk> /
stale:live-server-build-unreported:<server> / unattested:* /
unavailable:*. Advisory, never a gate. On any stale:, relaunch the forge (or
the session's MCP servers) before trusting an expert answer — and record the
fallback, per mcp_first_read_path.
Do NOT substitute test -x or a registration-file check for the handshake. A
server that exists but wedges on startup is precisely the outage worth catching,
and a file-existence test calls it healthy. (The first draft of the probe read
.command while dropping .args, launched a bare bash, and reported both
healthy experts as down — a false outage is as bad as a missed one.)
Local Expert System Probe (advisory — at cycle start, before work drain)
Run at Start-Of-Cycle, AFTER the MCP Expert Health Probe and BEFORE worker
drain — and ONLY on hosts with TILLANDSIAS_HOST_EXPERTS set, the same gate
the commit hooks use for host-expert work. On every other host skip this
section entirely rather than paying a connection timeout probing an ollama
that was never provisioned:
[ -n "${TILLANDSIAS_HOST_EXPERTS:-}" ] && "$(. scripts/plan-binary-probe.sh && resolve_plan_binary)" script run scripts/lua/check-local-expert-health.lua
It pings the local Ollama inference endpoint and prints exactly one line matching
^(ok:local-experts-healthy|degraded:local-experts[^;]*|down:local-experts[^;]*|skip:not-applicable)$.
On ok:local-experts-healthy you MAY ask the local pipeline for a triage hint:
TILLANDSIAS_EXPERT_DOMAIN=methodology \
./target/release/tillandsias-plan pipeline "what is pending and neglected in the plan?" \
--domain methodology 2>/dev/null | jq -r '.responses[0].answer // empty'
The pipeline emits {tier, domain, rag_freshness, rag_source_commit, responses}, and each responses[].answer is RAW local-model text: no
retrieval, no validation, no grounding (that wiring is packet
wire-local-experts-mode-through-grounded-pipeline). Treat the output as an
ungrounded hunch, never a directive — anything it claims about specific
packets must be re-checked against tillandsias-plan next <role> and the
plan ledger before acting.
With the gate unset, or on down:* / degraded:*, skip the query and
continue the cycle. The local expert system is a convenience, not a gate —
the deterministic expert tiers and MCP experts work without it.
Reduction Engine
The loop is a reduction engine, not just a worker. Its job is the project's core
principle — Monotonic Reduction of Uncertainty Under Verifiable Constraints
(methodology/philosophy.yaml). Every cycle must move the system toward a
verifiable implementation of the spec by reducing open uncertainty, and must
never let an observed problem evaporate.
Capture: nothing gets lost
Any time a worker notices "welp, this isn't great" — an inefficiency, a rough edge, a fragile assumption, an advisory-only guard, a repeated manual step, a log warning, a deprecation notice — it MUST be filed before the cycle exits.
Every capture is a ledger write, so it carries the agent_id from Start Of
Cycle step 0b (scripts/agent-identity.sh id <backend>), never a hand-typed one.
CAPTURE IS MANDATORY. A NEW ROW IS NOT. These are different acts and conflating them is what grew the ready queue to 410 rows against a service rate of ~19/day. Route every capture:
- A new ROW only if INDEPENDENTLY SCHEDULABLE — it names owned files no open
row already owns, OR a different
pickup_rolecan claim it than every open row covering that scope. - Otherwise an EVENT on the row it belongs to, or a
next_actionclause on that row. The finding is recorded in full either way; only its selectability differs.
When a row's events show a confirmed pattern — several independent observations, ideally from different hosts or cycles, converging on one defect or one missing capability — that row has OVERFLOWED and is a promotion candidate. NOTE it; do NOT promote it. Promotion is Tlatoāni-gated (operator ruling, 2026-08-19): what counts as a confirmed pattern is not yet expressible as a threshold, and an agent promoting on its own judgment restores unbounded arrival by a longer path.
Why this is safe, and why it is not a throughput cut: with arrivals
λ = a + b·μ, the queue moves at dL/dt = a + (b−1)·μ. Above b = 1 every extra
host and every shortened cycle fills the queue FASTER; below b = 1 the same
velocity drains it. Measured b is 1.80. So curation at the acceptance boundary
is the precondition for velocity paying off at all — see
methodology/distributed-work.yaml → why_velocity_is_the_other_lever.
Standing FRESHNESS audit class (order 372, methodology component_freshness)
Each cycle, after worker drain, treat the top component the freshness-advisory
CI phase flagged as a claimable audit source. Re-validate ONE component against
the audit question — last properly looked at and confirmed still meaningful,
useful, efficient, sound, and complete? — and end in exactly one disposition:
refreshed (re-validated, update its # freshness: stamp), updated
(fixed/tuned, update stamp), or obsoleted (delete/tombstone with same-commit
removal of dependents). Apply the discard-over-repair bias: discard a stale
component rather than repair it when a fresh implementation would be better.
scripts/freshness-inventory.sh emits the coverage report + top stale
components each ./build.sh --ci run; the report grammar is pinned by
litmus:freshness-inventory-shape. A freshness audit is a valid reduction step
on its own and counts toward burndown.
This is mandatory, not optional (methodology.yaml →
cooperative_work_discipline; Non-Negotiable Exit Contract → "Explicitly log
things that make you slower"). File it as a dated issue in plan/issues/,
classified as one of: research/, exploration/, enhancement/, or
optimization/. An unfiled finding is a lost finding and a contract violation.
Ledger compaction (check each cycle; act only when eligible)
tillandsias-plan fragments # -> compaction: eligible=<bool> fragments=N bytes=N malformed=N reason=<name>
tillandsias-plan compact # only when eligible=true
plan/index.yaml is a compacted BASE; plan/index.d/*.yaml are append-only
fragments that concurrent hosts write freely. Reads fold them transparently, so
an uncompacted ledger is slower, never wrong — compaction is garbage
collection, not a correctness step. Never let it block filing work.
Two rules, both easy to get wrong:
-
Compaction deletes only the fragments it folded, by name. Never
rm plan/index.d/*. A fragment another host wrote while you were compacting has not been folded, and removing it silently destroys their work. -
Compaction is TEXT-LEVEL and never re-serializes the base. It ran on the real ledger for the first time on 2026-08-03 (order 582-4wdi, commit 81e12b65): 28 fragments folded, 120 comment lines preserved exactly, 535 packets preserved, and the
plan/index.yamldiff was 1021 added lines and zero removed lines. Do not read that zero as the invariant — it was a true measurement of a fold that could only APPEND, taken beforeset-fieldfragments existed, and it has been unreachable since. A fragment that reassigns a field must remove the superseded line; a fold carrying nine status transitions removes ninestatus:lines and that is the fold working.THE PROPERTY TO PROTECT, stated so a healthy compaction cannot fail it: no line is removed EXCEPT one whose field a fragment explicitly reassigned. Comment lines, packet count (minus none, plus the new ones), and the four-space item prefix
append-eventlocates by all survive unchanged. Check THOSE, in one command, rather than eyeballing the removal count:git diff --numstat plan/index.yaml # removals are expected git diff plan/index.yaml | grep '^-' | grep -v '^---' | head -40 # READ themRead the removed LINES, not a histogram of their keys. This recipe used to end with
cut -d: -f1 | sort | uniq -cand the question "ONLY field keys?". That works only while every reassigned field has a SCALAR value. A fragment that replaces a STRUCTURED value removes all of its lines, including list items that are not field keys at all — measured on yoga 2026-08-25, a fold whose 14 removals included- container,- cpu,- gpu,engines,lanesandsupported_device_classes, because a stale capability row'sengines: [{name: ollama, ...}]block was superseded byengines: []. The fold was correct; the histogram made it look like structure loss, which is the same stop-and-investigate 865-ng6r removed for the scalar case. The question is not "are these field keys" but "does each removed line belong to a field some fragment reassigned" — and the cheapest way to answer it is to look at the lines.MEASURED TWICE, on two hosts, each of which stopped its cycle to investigate: lenovinha 2026-08-23 (+888/-17 — five
status, one multi-linenext_action) and yoga 2026-08-25 (+556/-12 — ninestatus, twonext_action, onenext_action_ts; 37 comments and all 526 packet prefixes intact). Both folds were correct. The prose is what cost the time, which is why it is corrected here rather than merely noted (865-ng6r).It refused for months before that, correctly: a
serde_yamlround-trip drops comments and re-indents items to column 0, after whichappend-eventsilently stops finding packets. If compaction ever regresses to round-tripping YAML, restore the refusal rather than accepting a lossy fold. The candidate base must still PARSE and pass the integrity gate before it may replace the base.
If malformed=N is non-zero, a fragment did not parse and was SKIPPED — its
contents are absent from every answer. Treat that as a finding, not noise.
De-slop sweep clock (consult each cycle; act only when due)
scripts/check-deslop-due.sh check # exit 0 = NOT due; exit 1 = due; exit 2 = cannot compute
Read the exit polarity before wiring anything to it. The file is named
check-deslop-due.sh and exits 0 when the sweep is NOT due — same
convention as check-daily-maintenance.sh, where the actionable state is the
non-zero one and the healthy steady state stays at 0 for callers under set -e.
The idiom is if ! scripts/check-deslop-due.sh; then run_the_sweep; fi, or
branch on the verdict token (deslop-due / deslop-not-due), which cannot be
inverted by a copy-paste.
It prints exactly one line carrying the numbers it decided on, e.g.
ok:deslop-not-due:order=844 last=834 delta=10 hours=44 reason=under-threshold.
The rule is event-counted only: due when
(current_order - order_at_last_sweep) >= 200, with a 48h floor so a filing
burst cannot fire two sweeps in a day, and no calendar ceiling — a ceiling
would fire low-yield sweeps over a quiet fortnight and
red:two-sweeps-zero-confirmed would then retire the reconciler for doing
nothing wrong. Time-decaying properties already have their own TTLs
(expire-claims 24h, component_freshness). This is not a build gate and
nothing in ./build.sh runs it; it is a scheduler input for this section.
After running a sweep, record it — the marker is committed, not host-local, so the whole fleet shares one clock:
scripts/check-deslop-due.sh record --examined <rows-looked-at> --confirmed <n> \
[--retracted <n>] [--filed <n>] [--net-lines <±n>]
--examined and --confirmed are mandatory. The sweep's kill rule counts only
sweeps that examined new rows, and this is why: the first real sweep
retracted 51 of 410 rows (12.4%) against a modelled ~50%, and that pass drained
accumulated stock. Steady-state yield will be far lower, so two consecutive
near-zero sweeps is a normal outcome for a healthy queue, not evidence the
reconciler is broken. A sweep that confirms the queue is clean has done its job.
Filing a packet: mint its order, never pick one
tillandsias-plan next-order # -> 581-k3f9
Write the packet to a FRAGMENT, not to plan/index.yaml. Create
plan/index.d/<utc>-<suffix>-<host>.yaml with a top-level packets: list. Only
you can have produced that filename, so git never conflicts and no host waits on
another. Reads fold it in automatically — the packet is queryable immediately.
Fragments are IMMUTABLE: to change something, write a new fragment, never edit an
existing one (that is what makes the fold order-independent).
Prose NEVER passes through argv (order 971-7muc)
tillandsias-plan append-event <ref> <type> --summary-file NOTE.md --host <host>
tillandsias-plan append-event <ref> <type> --summary-file - --host <host> <<'EOF'
...prose...
EOF
Note the QUOTED heredoc delimiter — <<'EOF', never <<EOF. An unquoted
delimiter expands backticks and $(...) inside the body, which is one of the two
ways this defect actually happened.
WHY, and it is worth the four lines because every instrument we have says the
write succeeded. Prose passed as a shell argument is rewritten by the shell
before tillandsias-plan starts, and the damage is unrecoverable by then:
"the `accel_mem_budget_gb` field" -> argv receives: "the field"
Both backticks and the word between them are gone. Two hosts lost the SUBJECT of
a sentence to this on one evening (964-r98h, 970-7fqk), and validate-yaml,
tillandsias-plan check, the fragment-keys guard and ./build.sh --check all
passed the result — because valid YAML carrying plausible prose is exactly what
the corruption produces. It bites hardest on the most valuable writes: prose that
quotes an identifier is prose explaining why something is named what it is, and
backticks are how anyone writes that.
--summary-file is read by the process itself, so no shell ever sees the bytes.
The argv path still exists and now REFUSES text carrying expansion residue (an
unbalanced backtick, a literal $() — but understand what that can and cannot
do: a balanced substitution leaves no trace at all, so the refusal is a
backstop, not a guarantee. The file is the guarantee.
Pinned by scripts/test-ledger-prose-roundtrip.sh, which asserts byte-identity
for text containing backticks, $(...) and $VAR.
Multi-line perl -0pi edits are FORBIDDEN on this tree (hard rule, 2026-09-04)
Three source corruptions in one day, all the same shape: a multi-line
perl -0pi -e 's|...|...|' edit landed its replacement inside a file's
HEADER COMMENT (accel_probe.rs twice, a fixture header once) instead of at
the intended site, the line count stayed plausible, and only the compiler or
a read of the diff's DELETIONS caught it. The author had filed the warning for
the fleet at 11:49Z and hit it twice more in the same afternoon.
Edit by exact-match, single-string replacement only: the Edit tool, a
python3 script that asserts the anchor occurs exactly once before writing,
or sed on ONE addressed line. After any scripted edit, read the diff's
removed lines before committing, not the count of them. A tool whose failure
mode is "plausible file, wrong place" is not a tool for a tree whose gates
grep for shapes.
Never compute "the next free order" yourself. That number comes from a ledger snapshot which is stale the moment another host commits, so two hosts filing in the same window pick the SAME number deterministically. It happened twice on 2026-07-31 (560–562, then 568–570), and six collisions sit at HEAD.
The minted token is permanent. Do not renumber it later, and do not ask a
coordinator to "normalize" it — order tokens leak into code comments, @trace order: headers, and commit messages, and a pushed commit message can never be
corrected. Two hosts landing on prefix 575 produce 575-k3f9 and 575-m2p1:
both correct, both permanent, nothing to reconcile. A shared prefix is normal.
Cite packet_id in anything durable — depends_on, specs, methodology, commit
messages. It is the identity and is unique by construction; the order token is a
human convenience. Canonical: methodology/distributed-work.yaml →
order_id_allocation.
Reduce: smaller, simpler, verifiable packets
Filing is only the intake half. Each recurring cycle then reduces open findings:
- Pick the highest-value open finding that fits this host's capability.
- Split it into the smallest packet that closes a slice of it under a verifiable constraint — a litmus test, an executable check returning a pass/fail exit code, or a parser/validator — never prose intent alone. A guard only an attentive agent honors is a suggestion, not a constraint; reduce it to something that fails loud on its own.
- Promote that packet into
plan/index.yamlas areadynode with a named verifiable closure, then drain it when a capable host can produce evidence. - When the verifiable check passes, the slice is retired; re-derive the remaining residual and repeat.
Reduction is monotonic: each step must lower residual uncertainty or preserve it
while increasing verification level (convergence.yaml → drift_control). A
"reduction" that adds ambiguity or removes a validated invariant is drift and
must be rejected. Shaping a finding into a well-formed ready packet is a
valid reduction step when the current host cannot yet implement it.
Raising the bar is Tlatoāni-gated (do not self-escalate)
The scan bar is a fixed, declared depth. Reducing all open findings to zero at
the current bar is a legitimate, clear convergence point — a fixed point of
the refinement operator — not premature convergence. The loop MUST NOT raise the
bar on its own. Autonomous bar-raising would make the convergence point
undefined (the loop could never report "done"), which is exactly the failure
this rule prevents. See methodology/convergence.yaml → bar_raise_governance.
What the loop does as it approaches zero residual at the current bar:
- Keep reducing open findings at the current bar until none remain.
- Then propose bar-raise candidates — file them as
research/orexploration/issues describing the deeper scan that could be enabled (e.g. treat build/test/runtime warnings, non-fatal errors, deprecation notices, flaky-test signals, slow steps, or stale caches as findings). A proposal is a candidate, not an enabled scan. - STOP there. Enabling any bar-raise — actually treating a deeper signal class as findings — is an explicit, one-off decision that The Tlatoāni must approve every time. Record the approval (who/when/scope) before the deeper scan becomes part of the loop's contract.
Rationale: much of the system is "build what works, then improve from there," so each bar-raise is a deliberate scope expansion the operator owns, not an emergent behavior. Automatable approval of some low-risk bar-raises may come later; until The Tlatoāni declares such a policy, every bar-raise is manual. Reaching zero at the current bar and filing bar-raise candidates is a complete, successful cycle — not an excuse to escalate unprompted.
See plan/issues/meta-orch-enhancement-opportunities-2026-06-20.md for a worked
example of capture → reduce → promote.
When the Gate Fails, Read check-logs.jsonl FIRST
.cache/convergence/check-logs.jsonl holds a per-run verdict for every check,
going back weeks. It is the authoritative record of what failed and when.
scripts/convergence-log-path.sh prints its path and honours
TILLANDSIAS_CHECK_LOG_INDEX. It lived at target/convergence/check-logs.jsonl
until order 888-vgs8: Finalization 9c's cargo clean removes target/
wholesale, so the sweep this skill mandates was erasing the history this
section mandates reading, and the sweep's verdict never said so. The first
resolve after the move adopts a legacy target/ index once, by copy.
Do NOT diagnose from terminal scrollback. The next run overwrites the per-check
logs under target/convergence/check-logs/ (those still go with a sweep), and
re-running to "get a better look" is what destroys the evidence — the re-run
may pass, and then the failure is gone.
jq -r 'select(.status != "pass") | "\(.ci_run_id)\t\(.check_id)\t\(.status)"' \
"$(scripts/convergence-log-path.sh)" | tail -20
scripts/local-ci.sh also prints a Failed checks: block naming each failure.
If you pipe the gate through tail -N or a grep filter, you will cut that block
off and lose the names — this host did exactly that twice on 2026-08-09, spent
two cycles calling an intermittent failure "unexplained", and filed a packet on
the false premise that the names were unrecoverable (637-df4z, closed
mis-diagnosed). The answer had been on disk the whole time: rust-tests and
tray-contract, two parallel tests racing on $HOME and a shared fixture
directory (638-ehzi).
An intermittent failure is a defect with a schedule, not noise. Treating it as noise is how it survives.
Reads Go Through MCP First
Before draining anything: do not read whole ledgers. plan/index.yaml is
31,678 lines and plan/loop_status.md is 7,875; pulling either in full to learn
one fact is the largest single consumer of orchestrator context in this loop, and
it is paid again by every agent on every host every cycle.
Ask forge-plan / project-plan (plan_answer, plan_next, plan_query,
plan_status, plan_blocked_by, methodology_ask, spec_answer) and
project-info (search_code, grep_code, find_files, read_file). Answers
are cited — keep the citations. For a conversational/synthesis status question,
use the Local Experts mode (expert-serve) — same citations-or-typed-refusal
contract as spec_answer.
Drop to the filesystem for exactly three reasons, and name the one that applies:
unavailable (MCP down or confidence=unsupported — fall back and keep going,
then record it so a systematically-refusing expert stays visible);
verification (before any irreversible act, read the CITED SPAN, not the
file); not exposed (no tool covers it — and if the loop needs it repeatedly,
that is a missing tool, so file a packet).
Canonical: methodology/distributed-work.yaml → mcp_first_read_path.
Worker Drain
Writing a validation packet: demand the post-condition AFTER the last MUTATING step
Any packet that asks a host to install, launch, switch channels, or otherwise mutate its runtime must require the health check at the end of everything the host actually does — not after the last step that felt like the test.
Earned 2026-08-10. A sibling's smoke reported 4/4 PASS on a HEALTHY --diagnose
taken at 04:32:20, then ran one more mutating step (a downgrade-to-stable /
restore-to-unstable round trip), which wedged the control wire at 04:33:27 and
left the host in a failed provisioning state for ~25 minutes until the operator
found it. Every per-step result was true; the verdict was still incomplete about
the host's end state, and a stable promotion was made on it.
Their own words are the rule worth copying: "a post-condition check belongs after the last MUTATING step, not after the last interesting one."
So a validation packet this loop writes should say, explicitly:
Final step, after everything above: re-run the health check. If any step after your last health check mutated the host, that check is stale and the run is not finished.
Stranded-claim sweep (coordinator, every cycle)
scripts/check-stranded-in-progress.sh
scripts/check-landed-but-open.sh
scripts/archive-plan-packets.sh
check-landed-but-open.sh (1367-emjg) is the other half of the same leak: it
lists every order a CODE landing on trunk cites in its subject (default window
ten days) whose row is still ready, pending, in_progress or implemented. On
2026-09-23 that was 21 of 177 landed orders; on 2026-09-28 it found 920-tqhs a
month after its relay landed. Plan-only commits are not landings; --cite message also reads commit bodies, which cite orders as context and triples the
list. Report its summary:landed-but-open: line beside the stranded one.
A packet in in_progress is invisible in BOTH directions: ready queries skip
it so nobody claims it, and burndown does not count it so nobody notices it is
unfinished. 21 packets were in that state on 2026-08-09 — ~9% of the live
ledger, oldest at order 153 — every one with no progress event ever recorded
(641-e2qa).
Report the summary: line in the handoff. If the count is rising cycle over
cycle, claims are outliving their cycles and that is the thing to fix, not the
individual packets.
Advisory, never a gate: a packet legitimately in flight right now is indistinguishable from one abandoned an hour ago. Do not bulk-close what it reports. Closing a packet requires checking its exit criteria against the tree; guessing marks unfinished work done, which is strictly worse than leaving it stranded.
Cycle batch triage — decide the batch BEFORE draining
scripts/select-work-batch.sh <linux|macos|windows|any>
Run this once, at the top of the drain, and take the batch it prints. It selects
ONE epic (release_target) and at most budget packets from it, so a cycle
drains a coherent slice instead of five unrelated subsystems — the scatter that
made small packets cost more in orientation than in work.
THE BATCH IS A STORY, AND THE STORY IS THE UNIT OF WORK — not the packet.
Take the whole batch, implement it as one coherent change, and pay the cycle's
fixed costs ONCE: one build/verify pass, one loop-status entry, one attestation.
Do not run ./build.sh --check per packet, and do not attest per packet.
Why this is the rule and not a preference — measured on the fleet 2026-08-16..19:
| commits, 3 days | count |
|---|---|
record(mo-full) (attestation only) |
155 |
chore(plan) (ledger bookkeeping) |
88 |
| every category of actual product work | single digits each |
Over 5 days that is 127 product-code commits against 457 plan-only, 149 attestation-only and 219 merge commits. Every cycle pays the same exit cost whether it carried one packet or eight — loop-status, attestation record, attestation self-check at the new HEAD, and the merges each of those provokes. A one-packet cycle spends most of itself on the exit contract. Bundling does not make the overhead smaller; it amortizes it across more delivered work, which is the only lever available without weakening the contract itself.
Budgets, which the selector already implements — do not restate a different
number here, and if this text and scripts/select-work-batch.sh ever disagree,
the SCRIPT is right and this paragraph is stale:
- non-forge hosts: adaptive 6 → 10 (order 682-yiz7, evidence-backed tuning)
- autonomous / pairing forge cycles: adaptive 4, or
TILLANDSIAS_CYCLE_BUDGET - unattended litmus runs: 1 (order 707-3x9d)
This paragraph used to end "Budget is 1 on forge (order 264) and 3 elsewhere" while its own opening sentence said 4 and 6, and the script said 6 → 10. Three numbers for one budget, in one paragraph plus its tool. Agents read the smallest one, which is how a fleet tuned to 10 spent its nights draining one packet at a time.
It is minimax-ranked (largest residual first, per convergence.yaml →
minimax_convergence_strategy), with score-weighted entropy over the top-3.
The seed is printed; record it in the loop-status entry so the cycle can be
replayed. Budgets are stated ONCE, above — never restate them here.
THE SEED DOES NOT SEPARATE CONCURRENT HOSTS. Do not rely on it to. This
paragraph used to end "...so coverage spreads over time and two concurrent hosts
do not collide on one epic". The second half is false and was never measured.
Measured 2026-08-19 on macuahuitl against the live ledger: three distinct seeds
(macuahuitl-, yoga-, esmeraldinha-20260819) produced a byte-identical
batch — same epic=socket-audit-master, same pick=2/3, same score, same head
packet. Two independent reasons, and neither is fixable by reseeding:
- The default seed is
${TILLANDSIAS_HOST_KIND:-host}-<date>(select-work-batch.sh:232).TILLANDSIAS_HOST_KINDis set only inside the forge, so every bare-metal host falls back to the literalhostand they all seed identically. Even when set it is host KIND, so three Linux boxes collide with each other regardless. - The
urgent=<packet>override puts one globally-urgent packet at the head of the batch. Urgency is a property of the PACKET, so it is host-independent by construction and preempts the epic pick on every host at once.
SEPARATION COMES FROM CLAIMING, which is the mechanism that already exists and
is sitting at zero. next/select-work-batch filter to unleased, and
"leased" means status: in_progress. expire-claims (order 672-bz7u) reaps
claims older than 24h so a dead host cannot strand work permanently. As of
2026-08-19 expire-claims reports in_progress=0 — nothing has been claimed at
all, so every host is offered every packet, and on 2026-08-18 two hosts
implemented 798-tk7b six minutes apart (order 814-iyu7, ~4h duplicated).
So, before you implement anything from the batch — and before a coordinator ROUTES a row to another host — ask whether a sibling branch already holds it (order 1513-pppk):
scripts/check-claims-across-branches.sh --batch <order>...
# -> claimed-elsewhere:<order>:<branch> per held row, then ok:cross-branch-claims:<n>
# blocked:* means the fold could not be read: do not treat it as "free"
A trunk-only read cannot see a claim that sits on osx-next or windows-next until the relay, and on 2026-09-29 the coordinator nearly re-assigned a row another host had claimed that morning. Then claim:
tillandsias-plan set-field <order> status in_progress \
--host "$(hostname -s)" --reason "claimed for cycle <UTC ts>"
git add plan/index.d && git commit -m "claim(<order>): <host>" && git push
scripts/check-claim-confirmed.sh <order>
# -> ok:claim-confirmed:<order> | refused:claim-not-on-origin | refused:claim-lost:<order>
# | unknown:claim-origin-unreachable (never a pass)
Push the claim BEFORE the work, not with it — an unpushed claim separates
nobody, and a push that failed quietly (a locked keyring, 2026-09-29) looks
exactly like one that landed until check-claim-confirmed asks origin
(1493-d93i). Then:
- Losing the race is normal and cheap. The ledger is a CRDT; two hosts can
claim in the same window. On your next fetch, if another host's claim event
for that order carries an EARLIER timestamp (ties broken by lexicographically
smaller hostname), you lost: release yours back to
readyand take the next batch item. Do not both continue — that is exactly 814-iyu7. - Release on exit, always. Completed work moves to its terminal status.
Work you did NOT finish goes back to
readyin the same cycle you abandon it. Leaving itin_progresshides it fromreadyAND from burndown until the 24h reaper runs — 21 packets were stranded that way on 2026-08-09 (641-e2qa). Claiming is only safe because releasing is unconditional.
The triage: line reports ungrouped=N — eligible packets with no
release_target. That number is the health of the epic tier itself: when it is
large, selection is degrading toward flat priority order regardless of what this
script does. Surface it in the handoff.
Canonical: methodology/distributed-work.yaml → cycle_batch_triage.
When choosing the builder role, run /advance-work-from-plan repeatedly in a ./plan friendly way in fresh cycles until one of these is true:
- no eligible ready work remains for this host;
- every eligible item is blocked;
- a terminal failure was filed;
- the current cycle has already produced a coherent commit and the next packet would exceed the recurring-loop budget.
Forge-hosted cycles (TILLANDSIAS_HOST_KIND=forge) are the OPPOSITE of
greedy — decided by The Tlatoāni 2026-07-10 (order 264), replacing the earlier
"drain as many as possible" exception:
-
Drain as much of the batch as fits the envelope, as ONE story — bounded by TIME, not by a count. Superseded 2026-08-19 by The Tlatoāni, who set order 264 in the first place: "that's also why I was asking for bundles of related packets in stories and epics, rather than the 'take a single packet' baked into the ./methodology that I've been fighting you to lift."
Order 264's REASONING survives intact and is the reason this is a time bound rather than a licence: a litmus-launched forge cycle lives inside
litmus:opencode-prompt-e2e-shapeSTEP 3's 600s budget, and a cycle that overruns it dies mid-work with nothing pushed. What does NOT survive is the count. "One packet" was a proxy for "fits in 600s", and it is a bad proxy in both directions: three one-line ledger closures fit easily, and one large packet does not fit at all.So the forge asks the envelope question about the STORY, not the packet, and the selector's adaptive budget (4, or
TILLANDSIAS_CYCLE_BUDGET) is the starting size. -
Before implementing, estimate whether implement+verify+commit+push for the whole story fits the launch envelope.
-
If it does not fit, do NOT start implementing: split the packet into smaller ready child packets (
split_intopattern), record the shaping events, commit and push. The shaping commit IS that cycle's completed work — a split that lowers residual ambiguity is a valid reduction step. -
Canonical statement:
methodology/distributed-work.yamlworker_agent_protocol.forge_cycle_budget. Interim reliance on step timeouts is tracked by order 265 (forge heartbeat/liveness signals).
Each worker cycle must obey the non-negotiable exit contract above.
Node-Closure Claim (avoid duplicated ledger-hygiene work)
Before re-deriving and closing or hygiene-editing a plan/index.yaml node,
claim it so a concurrent cycle does not independently produce the identical edit
(the idempotent-but-wasteful collision recorded in
plan/issues/agent-concurrency-collisions-2026-06-20.md). Run the executable
claim instead of eyeballing the ledger:
scripts/claim-ledger-node.sh claim <node-id> # e.g. release-nix-cache-ref-scoping/choose-approach
It emits exactly one line matching
^(claimed|reclaimed|in-flight|released|free):[a-z0-9._/-]+$ and exits 0 when
this cycle owns the node (claimed:/reclaimed:) or non-zero (in-flight:)
when a live lease is held elsewhere — in which case skip that node and pick the
next eligible one. The lease is an advisory, CRDT-friendly reservation, not a
mutex on the file: it respects the stable-ID + idempotent-merge preconditions in
methodology/between-commits-work-discipline.yaml, so a missed or expired lease
never corrupts state (at worst two cycles converge on the same safe edit).
Release with scripts/claim-ledger-node.sh release <node-id> after the closure
is committed; expired leases (default TTL 4h) are auto-reclaimed. Pinned by
litmus:ledger-node-claim-shape.
Release-Targeted and Milestone Packets
Worker selection prefers packets carrying release_target: <milestone-packet-id> before the general backlog; a host with no eligible
targeted work falls back normally (never idle). kind: milestone packets
are criteria holders — never claim one for implementation; claim children
and record burndown as progress events on the milestone. Large ambitious
goals follow methodology/distributed-work.yaml →
ambitious_milestone_reduction (fat-agent research → operator-signed
decision record → smallest demonstrable rungs → verification): the
coordinator mirrors the milestone's burndown in plan/loop_status.md each
cycle.
Long-Running (multi_cycle) Packets
Packets marked multi_cycle: true in plan/index.yaml follow
methodology/distributed-work.yaml → long_running_packets (plan order 251):
- Claims are cycle-scoped; the packet returns to
readyafter each cycle's commit. Areadymulti_cycle packet with prior progress events is normal, not stalled. - Do NOT mark one
done— even with all exit criteria implemented — until itsverification_required.completion_gateis satisfied byverified-byevents from every named agent. Implementation-complete meansphase: verification, notcompleted. - Treat a packet sitting at
phase: verificationas a dispatch item for the named verifier agents and surface it during coordination. - The active set is mirrored in
plan/long-running.md; keep that view in sync in the same commit as any phase/status/verification change.
E2E Gates
Run eligible e2e gates after worker drain:
| Host kind | Local-build e2e | Curl-install e2e |
|---|---|---|
| linux_immutable | no | yes |
| linux_mutable | yes | yes |
| macos | yes | yes |
| windows | yes | yes |
Before running any local-build e2e gate, consult the structured host-eligibility
verdict instead of re-deriving the skip reason in prose: run
scripts/e2e-preflight.sh eligibility, which prints exactly one line matching
^(eligible|skip:[a-z0-9-]+)$. Proceed with the local-build gate only on
eligible; on any skip:<reason> (e.g. skip:no-podman-user-session in the
Cowork sandbox, which has no /run/user/<uid>, or skip:smoke-lock-held when a
parent/local sibling smoke already owns the host lock) record the verdict once
and skip the local-build gate without re-litigating it. Pinned by
litmus:e2e-eligibility-probe-shape.
Rules:
- Local-build e2e uses
/build-install-and-smoke-test-e2e. - On macOS the local build that gate exercises is
scripts/build-macos-tray.sh(the/build-macos-trayskill wraps it and files findings toplan/issues/macos-build-findings-<DATE>.md);./build.sh --installis refused on Darwin by design (723-whrx), so there is no build.sh install path to look for. - Published-release e2e uses
/smoke-curl-install-and-test-e2e. - Destructive substrate reset is expected setup on Tillandsias smoke hosts.
podman system reset --forceis a precondition for Linux idempotence tests, not a blocker. Do not pause, ask for confirmation, or skip the e2e gate for fear of wiping Podman whenTILLANDSIAS_DESTRUCTIVE_RESET_OKis unset or1. Only treat reset as blocked when the environment explicitly setsTILLANDSIAS_DESTRUCTIVE_RESET_OK=0. - If the plan records a latest tested release older than the current GitHub release, prioritize curl-install e2e.
- File every finding as a plan packet; write a PASS report for clean runs.
- Host-qualify any report filename another host could produce the same
day (1004-fue3). The smoke runbook once templated its report path from
release and UTC date with no host field; two platforms smoking one release
on one day collided add/add at land time, and the naive resolution — "take
one side" — silently deleted a platform's result. Name each report or
per-host finding with the host baked in (
<host_kind>-<host_id>, or the hostname), never a bare subject-and-date. The smoke template carries the field now; a hand-written findings path (…-findings-<DATE>.md, the shape named above for/build-macos-tray) still does not, so add it yourself (drill: plan/issues/fleet-restart-2026-09-12.md, Two lanes, one report filename).
Mutable Linux Coordinator Duties
Only linux_mutable performs global coordination:
- Run
/multihost-orchestrationafter worker drain or when sibling branches advanced. - Merge eligible
origin/windows-nextandorigin/osx-nextwork intolinux-next, with explicit conflict mediation if needed. - Run more frequent local-build e2e gates than other hosts.
- Run
/merge-to-main-and-releaseonly whenlinux-nextis green, plan evidence is current, and no release is already in flight. Read thecloud.usageMCP tool first (order 1966-47vt): Cloudflare at ≥ 2,700 of 3,000 build minutes this month means no merge to main unless the fix is urgent. - After a release succeeds, ensure the plan records the new latest release so immutable Linux hosts know to run curl-install e2e.
- Move the project's openspec CLI forward deliberately (order 1441-myz3).
openspec/cli-versionis the project's ONE openspec version. Forges install exactly it (ensure_openspec_pinned, lib-common.sh) and never refresh it to @latest, so a fresh forge's /opsx sets match the committed ones and launch leaves the checkout clean. Keeping the pin current is the coordinator's job, once per cycle and cheap when nothing is due. Run it on a CLEAN tree based onorigin/linux-next(awork/<order>ref or the coordinator's own checkout), with npm on PATH (host ortillandsias-builder):scripts/openspec-pin.sh check # ok:openspec-pin-current:<v> | due:openspec-bump:<pin>-><latest> | unknown:… scripts/openspec-pin.sh bump # only on due: (or `bump --to <v>` to re-level at the current pin) scripts/openspec-pin.sh drift # must print ok:openspec-generated-matches-pin before committingbumpinstalls the new version into a version-keyed cache, runsopenspec update --forcewith an ISOLATED openspec config (the profile is derived from the committed workflows, never from the machine running it), writes the pin, and never commits. Onbumped:openspec:<old>-><new>:<n>-paths, commit everything it changed as ONE change, subjectchore(openspec): bump CLI <old> -> <new>, and land it like any work ref. Onreview:openspec-bump:…, stop and read stderr. It names either generated files that still disagree with the pin, or a superseded copy the CLI left behind instead of overwriting (1.13.2: "Left 11 files in .codex/ that differ from the copy in .agents/"). Remove only the copy the CLI names as superseded, re-rundriftto ok, and commit that removal in the SAME change. Paths outside the generated surface mean something unexpected happened: do not commit, and file a packet.unknown:(registry unreachable) waits for the next cycle. Changing the workflow profile is a separate decision; it is never a side effect of a bump. This is the only sanctioned way the generated /opsx sets move. Launch-generated dirt is refused (step 4 of Start Of Cycle), never committed.
Cycle Metrics (report before the handoff)
Run scripts/cycle-metrics.sh --cycle-start <Start-Of-Cycle UTC> [<since-ref>]
and include its output verbatim in the final handoff. It emits one key=value
line per block — branch on those, never on prose. --cycle-start is the UTC
you recorded at Start-Of-Cycle (e.g. 2026-09-04T06:00:00Z); without it the
repeat: line measures the last three hours, not this cycle.
Every handoff MUST carry these lines; the two questions they answer are distinct — measure before you optimize:
-
mcp:— "are the servers used?" Per-server call volume (servers=,plan-expert-calls=). Today only the plan expert is instrumented; the other servers are nameduninstrumented-see-682-m8ek, not faked as zero. Also carrieshealth=(order 737-zcj5) —ok/down:<csv>/absent/unprobed— sourced from the health probe, NOT from usage. Call volume cannot see an outage: a server that is down writes no usage rows, and neither does one nobody called, so both render as absence in every count on this line.health=unprobedis deliberately nothealth=ok; reporting an unmeasured expert as healthy is the order-531 shape. -
mcp_outage:— emitted ONLY when a probe recorded a non-up state (records=,health=,log=). Its absence on a healthy cycle is the negative control, not an omission. When present, it is the ledger trace of the outage: the handoff pastes these metrics verbatim and the loop-status entry is built from the handoff, so the outage reachesplan/loop_status.d/without any agent choosing to write it down. -
expert_accuracy:— "are the experts RIGHT?" The groundtruth pass-rate (pass=/total=/rate=), graded against the committed rung-1 query set. Distinct fromanswer_rate: an expert can answer every question yet cite spans that do not support the answer. Accuracy is pass/total of graded cases, never call volume. It readsdeferred source=litmus:expert-groundtruth-harnesswhen no binary can grade in-cycle (then the quick-tier gate carries it). -
flow:— "does work-done-per-cycle outrun the fixed per-cycle overhead as batches grow?" The ROLLING packets-per-cycle view (cycles=,avg_completed_per_cycle=,avg_commits_per_cycle=,overhead_ratio=) over a per-host append log.overhead_ratiois total commits per total completed packet — the fixed per-cycle cost amortized across the work it produced — and is the number the greedier-batching decision (682-yiz7) consumes: if larger batches amortize overhead, this ratio FALLS as batch size rises. It readssource=absentuntil this host has appended at least one flow record.Each cycle MUST append its flow record so the rolling view stays live. Emit it here, right after running
scripts/cycle-metrics.shfor the handoff and before Finalization commits — best-effort, the emit never fails the cycle:scripts/cycle-metrics.sh --emit-flow \ host=<this-host> \ batch_epic=<from select-work-batch `batch: epic=`> \ batch_seed=<from `batch: seed=`> \ batch_size=<from `batch: size=`> \ budget=<from `batch: budget=`> \ claimed=<packets this cycle claimed> \ completed=<packets this cycle completed> \ filed=<new packets/issues this cycle filed> \ commits=<from cycle-metrics `repo: commits_this_cycle=`> \ plan_open=<open packet count> plan_total=<total packet count> -
timing:— "where does the cycle's wall-clock go?" The ROLLING view over a per-host duration log (steps=,build_check_ms_avg=,litmus_ms_avg=,slowest=<step>:<ms>). "Time spent building, testing" is the most likely bottleneck and was invisible until the build/test/litmus entry points began appending one duration record per heavy step (packet 682-emvg).build.sh --check(the local gate), eachscripts/local-ci.shlitmus phase, and eachscripts/run-litmus-test.shsuite now self-instrument — best-effort, the timing write never changes the wrapped step's exit code or output. It readssource=absentuntil this host has run one instrumented step.slowestnames the single step to attack first. -
repeat:— "which steps were paid REPEATEDLY inside THIS cycle?" The within-cycle count view over the same timing log (window=,steps=,top3=<step>=<count>,...), order 1001-q3zf. Count, not duration: a gate re-run after every rebase costs its duration TIMES the rounds, and only the product is actionable. Pass--cycle-start <your Start-Of-Cycle UTC>(or exportTILLANDSIAS_CYCLE_START_TS) so the window is this cycle and not the default last three hours:scripts/cycle-metrics.sh --cycle-start 2026-09-04T06:00:00Z. Readssource=absentwhen the timing log is missing. -
recur:— "which steps are paid on EVERY cycle, and what do they cost cumulatively?" The cross-cycle view over the last 7 days (window=,runs=,steps=,top3=<step>:runs=<n>:total_ms=<t>:avg_ms=<a>:fail_pct=<p>,...), ranked by total_ms — a cheap step paid three hundred times outranks an expensive one paid twice. Step names contain colons (step:...,check:litmus-pre-build), so parse an entry FROM THE RIGHT: the trailing:key=valuefields never contain a colon; the step is everything before the first:runs=. -
skippable:— "which expensive, repeated, outcome-invariant steps could be memoised or skipped?" This is the operator's instrument for "repeated work that wastes every cycle / expensive work that may be cached or skipped" (2026-09-03). A candidate ran at leastmin_runs=times, averaged at leastfloor_ms=, and NEVER changed outcome in the window, so every run after the first bought no information. Entries are<step>:runs=<n>:avg_ms=<a>:fail_pct=<p>:saved_ms_upper=<s>, parsed from the right likerecur:.saved_ms_upperis a BOUND, not a saving — total_ms minus one run; the log carries no input identity, so nothing here can say a run WOULD have hit a cache. Paste it as a bound. Do not file a skip-or-memoise packet from one host's reading: the coordinator's cross-host recurrence audit (coordinate-multihost-work) acts only when the same step topsskippable:on two or more hosts.
The two lines worth reading first:
answer_rate— the experts' USEFULNESS. Not call count. An expert called two hundred times that refuses two hundred times is heavily used and completely useless, and a call counter reports that as healthy adoption. That is not hypothetical: order 531 had everyplan_answerreturningconfidence=unsupported(the forge was seeded from a pre-expert branch) while launch state truthfully reportedexperts: ready.verdict— the single fact to look at first.attention:is not a failure; it is the cycle naming its own weakest point.attention:expert-answered-nothing-check-base-branchis the order-531 signature and means the ARTIFACT is wrong, not that the questions were hard.
Two rules about these numbers:
- Never report a metric the tooling did not produce.
experts_substitutionreadsunknownbecause it needs the agent harness's tool log, which is not in this repo. Leave it unknown. An estimated number makes an unmeasured thing look measured, which is worse than reporting nothing. - Never propose making expert metrics reward activity.
answeredis reachable only via citations the compiled expert emits when it resolved a real packet or path, so calling tools more cannot raise the rate. A change that letsansweredbe reached without citations converts a quality signal into a volume signal and must be refused on that ground.
Finalization
Before exit:
-
Reduction-engine capture check: confirm every "this isn't great" observation from this cycle is filed in
plan/issues/(classifiedresearch/,exploration/,enhancement/, oroptimization/) and, where reduced, promoted to aplan/index.yamlpacket. An unfiled finding blocks exit. A dirty-start preflight refusal performs no reduction cycle; it uses the final handoff exception above and exits without touching the checkout. -
Refresh
plan/index.yamlif this cycle changed active work, blockers, tested release, or host assignments. Record THIS cycle's status as a NEW## Cyclefragment inplan/loop_status.d/viatillandsias-plan loop-status-append --host <host> --ts <UTC-ISO> --file fragment.md(< fragment.mdand a bare path also work; order 1004-8vkv) — never edit the sharedplan/loop_status.mddirectly, or a concurrent host's status write conflicts for the same reason the old monolithic ledger did (packet 582-nqw5). The folded view (tillandsias-plan loop-status) is the status every host sees;loop-status-compactfolds fragments into the base when drift makes it eligible.A long cycle's
--tslegitimately predates its write, and--backfillis the sanctioned path for it (order 801-w4pn).--tsmust agree with the host clock within 900s or the write is refused; a cycle that read the clock at Start Of Cycle and appends at Finalization two hours later trips that by construction. Do not widen the window and do not invent a fresh timestamp — the limit is what catches a fabricated hour, and re-reading the clock would silently relabel when the work happened. Pass--backfillwith the real timestamp, or omit--tsentirely when "now" is genuinely the right stamp:tillandsias-plan loop-status-append --host <host> --ts <cycle-start-UTC> --backfill --file fragment.mdTHE FRAGMENT MUST BE SUPPLIED EXPLICITLY.
--file <path>,< pathand a bare path argument all work. Before order 1004-8vkv a bare path was silently dropped and the command fell through to stdin, which on an inherited socket (a forge agent's stdin, by construction) blocked for 26 minutes at 0.0%% CPU. It is now refused in 5s naming the explicit forms, but supplying the input explicitly is still the invocation to type.--backfillonly reaches BACKWARD (a future--tsis still refused), so it waives nothing the limit exists to protect, and the refusal already names it. 2b. Ask which litmus specs cover the files you touched (order 748-tkjx), BEFORE the gate rather than after the next host finds the red:scripts/litmus-covering-specs.sh --run --changed # the specs to re-run scripts/litmus-covering-specs.sh <path>... # one file, with detail./build.sh --checkruns NO litmus (deliberate — the suite is minutes, and a gate that slow gets bypassed with--no-verify), so a green gate plus the spec you happened to be working in can both pass over another spec's broken pin. That is measured, twice: on 2026-08-15 an edit toimages/default/lib-common.shleftlitmus:startup-context-addendum-shapered through a green--checkand a passing 7/7 run of the agent's own spec; on 2026-08-28 three tests were found red back toaf745f3fdonly because a full suite happened to run (921-vtf4).lib-common.shalone is covered by twelve specs across four lanes.ADVISORY and cheap (~90ms over the whole corpus, no build). It answers from committed data — each test's declared
workspace containsartifacts plus its commands — so there is no second list to drift.match:declaredis an authored coverage claim,match:commandis coverage the preconditions never declared;binding:unboundmeans no spec binds that test, so it executes in no suite (885-92iu) and is excluded from the count and from--run. A path no litmus references answersok:litmus-coverage:0-spec(s)and nothing else — an editor of an uncovered file is not nagged.
2c. Report split parents still competing with their own children (order 750-zrt4):
tillandsias-plan split-parents # parent<TAB>status<TAB>child:status,...
A packet whose work has been split keeps ready and gains a split_into
list, and only ready is claimable — so a fully-delegated parent stays in
the pool forever. Measured 2026-08-14/15: select-work-batch.sh linux
reported urgent=mirror-identity-lane-sidecar-and-sshd on four consecutive
cycles and no host claimed it (eleven design rungs behind a 3-hour
estimate), while one level up 606-bvnp ranked #1 claimable with all four
slices already spoken for. Both were fixed by hand, which depended on an
agent noticing a four-cycle stall.
REPORTS ONLY — it names the children and their statuses so you can see where
the work went, and changes nothing. Resolve one by setting the parent
blocked with an event naming the children (accurate: it cannot progress
without them), or, if it genuinely retains work of its own, declare
retains_direct_work: true. Never obsoleted — that status satisfies
dependents and would falsely unblock work that is still waiting on the
rungs.
-
Validate touched YAML with a parser, using the one that EXISTS where you are:
tillandsias-policy validate-yaml <files>where built, elseyq . <file> >/dev/null, elseruby -ryaml -e "YAML.load_file('<file>')".rubyis NOT in the forge image;yqis — a skill that names only ruby sends a forge agent to a tool that does not exist, and the tool sitting next to it ispython3, which is FORBIDDEN for committed automation (seeplan/issues/meta-orch-enhancement-opportunities-2026-06-20.mdorder 63). Its presence on PATH is not permission. The forge startup context lists what is actually available. 3b. Re-verify the credential BEFORE the gate (order 892-aw9p):scripts/check-credential-channel.sh reverifyThe Start-Of-Cycle guard runs once and cannot see a credential that dies afterwards. MEASURED on calmecacpilli 2026-08-25: it returned
ok:gh-keyring-push-verified, two pushes succeeded on that credential, and ~50 minutes later the third failed withremote: Invalid username or token. Nothing the guard measured was wrong — the verdict was true when issued. Its RESULT simply outlived the thing it checked, which is the same shape as a stale gate stamp (887-bz88).Run it HERE, immediately before
./build.sh --check, because that is the last point where the remaining cost is still worth saving: a dead credential then costs the gate's wall-clock (40s here, 276s on the slowest host) instead of being discovered after it, with the work done and the exit contract forbidding an unpushed exit. Not per push — the healthy path must not pay a round trip for every git operation, and a guard slow enough to notice is a guard that gets bypassed.On
blocked:credential-expired-mid-cyclethe credential WORKED and then stopped; that is distinct from never having had one, and the printed remedy is the OPERATOR re-seeding the token (tillandsias --github-login), not seeding a store. An agent never runsgh auth refreshorgh auth loginitself (1025-a896: a re-auth on one host evicts the token on every other; 1497-ahmd corrected the guard, which used to print it). Do NOT discard the cycle's work to get unstuck — salvage first (872-c9nd) and report blocked with the salvage ref.
3c. EVERY LEDGER WRITE HAPPENS BEFORE THE GATE, NOT AFTER IT (yolanda,
2026-09-02). The gate stamp hashes the CONTENT of every tracked and
untracked file in the worktree, so a ledger write after a green gate
creates a new plan/index.d/ or plan/loop_status.d/ file, moves the
digest, and the pre-push hook then refuses a tree whose CODE the gate
validated perfectly. Order your Finalization so that append-event,
set-field, loop-status-append and any issue file all land FIRST,
then gate once, then commit and push.
MEASURED: three extra full gates in one cycle on windows, about 25 minutes, plus a fourth on this host the same day. Both agents rediscovered the ordering by trial; it was written down nowhere, which is why it is here now rather than in a message that scrolls away.
The one exception is the attestation ledger, which CANNOT precede the
gate — record attests the head that finalization produces. That is
why step 9 re-verifies and re-derives; it is not a second chance at
the gate.
DO NOT diagnose this from the refusal message: it lists paths whose
MTIME is newer than the stamp, while the decision is made on CONTENT.
A merge or a regeneration that rewrites bytes identically appears in
that list without being the cause, and the file that actually moved
the digest may not appear at all. scripts/gate-stamp.sh verify is the
authority.
USE THE ONE COMMAND THAT DOES 4-6 IN THE RIGHT ORDER (order 859-4jny, discoverability gap found 2026-09-03):
scripts/land-on-platform-branch.sh [branch] [attempts] # -> ok:land:<sha>:attempt-N
It fetches, rebases, GATES, pushes, and then PROVES the commit landed by
asking the remote (git merge-base --is-ancestor HEAD origin/<branch>) —
because neither a zero exit status nor a <branch> -> <branch> line in the
output is sufficient evidence, and both have reported LANDED for a refused
push. It refuses non-retryable failures at once with their remedy (auth) and
retries only a lost race.
Never read a land or gate verdict through a pipe (1137-da83). Three
false claims in one hour on two hosts: scripts/land-on-platform-branch.sh … | tail -25 read as exit 0 when the land had exited 3 and named its gate
log — the tool was nearly filed against for the defect its header exists
to prevent — and tasklist … | head -2; echo rc=$? reported head's
status, published to a peer as a two-host difference that did not exist.
Capture the command's own status (out="$(cmd 2>&1)"; rc=$?) or set -o pipefail. If you use PIPESTATUS, snapshot the array as the very next
command (ps=("${PIPESTATUS[@]}")); any intervening command resets it,
and the one everyone writes — a=$?; b="${PIPESTATUS[0]}" — yields 0 and
0. On Windows, $? through wsl.exe -d <distro> -- bash -lc from Git
Bash is always 0 (1155-jurn): canary false; echo "$?" before trusting
any status through it (drill: plan/issues/fleet-restart-2026-09-12.md, What the pipe-verdict fixture found — extends the coordinator's rule entry below, which named this fixture as its follow-up).
WHY THIS IS NAMED HERE AND NOT LEFT TO BE DISCOVERED. Steps 4, 5 and 6 are
three manual acts whose ORDER is the whole contract, and a host that
hand-rolls them can invert them. That is not hypothetical: on 2026-09-03 a
forge inverted gate-and-push TWICE in one session — once under real urgency
(an ephemeral container, work not yet durable) and once with no urgency at
all — while spending that same session filing defects about instruments that
report success without measuring anything. Its improvised loop
(fetch && rebase && push) had NO GATE IN IT BY CONSTRUCTION.
The tool that cannot make that mistake had existed since 859-4jny and this
skill named it ZERO times, so every host was taught the hand-rolled ritual
and none were taught the safe one. Documentation was not the gap and neither
was discipline: the host had run salvage-dirty-worktree.sh earlier in the
same cycle, so it was not unaware of its tooling. Push is what "I am done
with this" feels like, and the action that feels like finishing must be the
action that is correct — so the finishing command is named here, first,
before the three steps it replaces.
Fall through to 4-6 by hand only when this refuses for a reason it names.
4. Run the local gate: ./build.sh --check and fix what it reports.
An unparseable or unformatted push poisons every downstream clone. Push CI
no longer exists on any working branch — only the manually-dispatched
release workflow remains (litmus:github-actions-budget) — so this gate is
the ONLY trunk protection. Do not push past a red gate (evidence case:
plan/issues/local-gate-evidence-query-packets-clippy-2026-08-09.md).
Terminology (851-gpb5): this skill called this step "the pre-push gate"
for months while methodology used the SAME name for a different rule —
the step-6 merge below — and the collision kept that rule invisible.
Here "local gate" always means ./build.sh --check; "pre-push gate" is
reserved for methodology's pull_merge_cadence.pre_push_gate.
5. Commit targeted files only.
6. Satisfy methodology's pre-push gate (pull_merge_cadence.pre_push_gate,
methodology/multi-host-development.yaml), then push the relevant branch.
On a non-linux-next branch (osx-next, windows-next, any shared
non-trunk branch; agent/* and salvage/* are exempt) that gate
requires, before EVERY push — not just at cycle start:
git fetch origin && git merge origin/linux-next # resolve conflicts locally
The v5 pre-push hook enforces the merge half on the two platform branches
(scripts/hooks/pre-push-linux-next-merged.sh, installed by
scripts/install-hooks.sh); the fetch half stays yours — the hook can
only compare against the origin/linux-next your last fetch recorded. If
the hook refuses, merge and push again; never --no-verify past it.
7. If a startup boundary was recorded, run the guard's verify mode. A guard
failure is a blocker: do not attempt destructive Git cleanup. Finalization
never deletes, restores, or resets a worktree path.
Do NOT remove $boundary_dir here (order 725-bu54). Retirement belongs
to the NEXT cycle's snapshot, which retires the previous boundary before
taking its own. Removing it here treats this Finalization as THE end of the
cycle — but an operator-driven loop has no single exit, and a cycle that
continues after a completed Finalization then finds its own boundary gone
and cannot attest. That happened twice in one night and cost a valid marker.
Re-running verify after further commits is correct and expected: it still
compares against the boundary taken BEFORE any work, and re-stamps the head
it observed so the marker matches what was actually pushed.
8. Confirm there are no uncommitted changes created by this cycle and the
branch is not ahead of upstream. Pre-existing dirty paths may remain only
when the boundary guard verifies they are byte-identical to startup.
Then prove the cycle's FINDINGS survive it (order 741-3y48):
scripts/check-forge-findings-persisted.sh
A GATE, not advisory — non-zero means work already done is about to be thrown
away. It covers plan/index.d/, plan/loop_status.d/, plan/issues/ and
plan/mo-full-attestations.d/, and it catches what git status cannot: a
COMMITTED fragment that was never pushed presents as a clean tree and is one
teardown from gone. That is not hypothetical — on 2026-08-15 an in-forge
review filed three valid fragments, tillandsias-plan check accepted them,
and the container took all three with it; they survived only because a human
happened to be reading stdout. A cycle that files nothing prints
ok:no-findings and is silent, so a clean run stays quiet.
This matters most inside a forge, where the checkout is a clone that dies with
the container — but it is correct everywhere, and an unattended host that
commits without pushing loses the same work to the next git reset.
9. Record the verified marker durably, then emit the terminal marker (orders
614-2gqx + 651-2x5s) as your FINAL output line. record derives-and-verifies
the marker exactly as self does and appends the verified line to the
per-host ledger (plan/mo-full-attestations.d/<host>.md) so automation can
consume a real, reachable hash without parsing a transcript. The ledger
line attests the WORK head; committing it advances the head, so re-run the
guard verify (permitted: further commits are expected, step 7) and
re-derive ONCE more — the terminal marker must name the head that CONTAINS
the ledger record:
# RUN `record` ONLY AFTER THE WORK PUSH HAS ACTUALLY SUCCEEDED, and check
# that it wrote. MEASURED on lenovinha 2026-09-03: `record` ran while HEAD
# was still unpushed and CORRECTLY REFUSED; a push-retry loop then landed
# the work WITHOUT re-running `record`; and the terminal `self` printed
# COMPLETE, because by then local and remote agreed. So the emitted marker
# was VALID and the durable ledger line — the thing automation actually
# consumes — was never written. A green marker is not evidence the ledger
# was appended.
#
# THE TRAP IS THE RETRY LOOP, not the ordering as written here: step 6
# already pushed, so this recipe is correct on the happy path. Insert a
# fetch/rebase/retry between the push and `record` — which every host does
# on a busy trunk — and `record` is silently skipped on any iteration that
# refused. Re-run `record` after the LAST successful push, not the first
# attempt.
#
# THIS IS NOW ENFORCED, NOT ONLY ADVISED (order 974-uk95). `self` refuses a
# COMPLETE marker unless THIS cycle wrote a durable record: `record` stamps
# the head it recorded, the boundary guard's `snapshot` clears that stamp at
# Start Of Cycle, and a marker whose HEAD does not descend from a recorded
# head is refused. So the skipped-record case now fails LOUDLY — no marker,
# which every outer launcher already treats as failure — instead of printing
# a valid marker beside an empty ledger. BLOCKED is exempt: a cycle saying it
# did not finish must still be able to say so.
#
# The ordering is still yours to get right. The guard catches a skipped
# record; it cannot make a retry loop call `record` for you.
#
# AUDIT IT THE WAY THEY CAUGHT IT: grep the ledger for this cycle's head
# rather than trusting the marker.
# grep -c "$(git rev-parse HEAD)" plan/mo-full-attestations.d/<host>.md
# Expect the WORK head to appear, not the record commit's own head — the
# ledger line attests the work and committing it moves HEAD, which is why
# the terminal marker is re-derived one commit later.
scripts/mo-full-attest.sh record # verify + append + print the marker
git add plan/mo-full-attestations.d/ && git commit -m "record(mo-full): attest <branch> cycle"
git push
scripts/meta-orchestration-worktree-guard.sh verify "$boundary_dir" # re-stamp the new head
scripts/mo-full-attest.sh self # terminal marker at the head containing the record
Emit the FINAL self line verbatim as your final line — do not retype or
edit it. If any step prints MO-FULL: FAIL … and exits non-zero, do NOT
emit a marker — treat it as a blocker (a missing marker is itself the loud
failure). The ledger line and the emitted line differ by exactly the
bookkeeping commit; both are real commits the local gate
(scripts/check-mo-full-attestations.sh) verifies exist and are reachable.
This gives EVERY full-mode lane the same convergence verification the
litmus launcher's check applies to its one lane, and leaves a durable
record that outlives the transcript. The marker is the machine attestation
that finalization steps 1-8 all passed; the outer launcher rejects exit
zero without it. See "Full-Mode Terminal Attestation" above for the grammar
and invariants.
9b. Release the checkout lock (order 873-zcim) — AFTER the terminal marker is derived, as the cycle's last mutation:
TILLANDSIAS_CYCLE_HOLDER_PID=$PPID scripts/cycle-checkout-lock.sh release
The stale-reclaim bound (dead holder, or 3h) covers a cycle that dies before reaching this line, so a crashed cycle cannot silence the cadence — but do not lean on it: an explicitly released lock frees the checkout NOW, a reclaimed one frees it up to three hours later, and on a 30-minute cadence that is six skipped fires.
9c. Reclaim the build cache when it is genuinely due (order 709-in2f,
methodology build_cache_hygiene) — LAST, after the lock is released, so a
full rebuild's cost is paid in the idle gap rather than inside the cycle:
scripts/check-build-cache-sweep.sh # exit 0 = NOT due; skip the body
Read the exit polarity before wiring anything to it. It exits 0 when
the sweep is NOT due, the same convention as check-deslop-due.sh and
check-daily-maintenance.sh: the actionable state is the non-zero one, so a
healthy steady state stays quiet under set -e. Branch on the verdict token
(build-cache-sweep-not-due / build-cache-sweep), which a copy-paste
cannot invert.
Due when target/ exceeds 40 GiB, OR the marker is older than 14 days, OR
the marker is absent/unreadable. On due:, run cargo clean (plus
scripts/nix-toolbox.sh gc where nix is present — never a bare
nix store gc, which would delete the whole persistent cache rather than
prune it), then stamp what actually ran:
scripts/check-build-cache-sweep.sh stamp --host <host> \
--action 'cargo-clean:<result>,nix-gc:<result>'
--action is REQUIRED, for the same reason check-daily-maintenance.sh
requires --steps: a stamp recording that "something happened" without
recording what moves the unfalsifiability up a level instead of removing it.
Record skipped-absent / deferred-<reason> honestly.
The convergence check log — the file "When the Gate Fails" above orders
you to read FIRST — is NOT in target/ since order 888-vgs8: it lives at
.cache/convergence/check-logs.jsonl, resolved by
scripts/convergence-log-path.sh, so this sweep no longer erases the
failure history. The per-check logs under target/convergence/check-logs/
do go with it, as they always did; they are overwritten every run anyway.
BEST-EFFORT, NEVER A GATE: a failed sweep must not fail the cycle, and the
sweep must not run before the work. Ephemeral forges read skip:forge-exempt
and never sweep — their target/ dies with the container.
04 同仓库更多 Skills
start-a-new-project
Use when the user wants to start a new project, asks how to begin, or is in an empty or nearly empty repository ("how do I start?", "I want to build an app", "help me set up a new project"). Guides them, in plain language, to describe the app as specs first (spec-driven development with OpenSpec), recommends Flutter (and the Flame engine for games and graphics), and sets up specs, a plan folder for longer work, and a README. Works for any project, not only Tillandsias.
smoke-curl-install-and-test-e2e
Clean-room end-to-end smoke test of a PUBLISHED release. Curl-installs the latest release binary from GitHub, does a full `podman system reset --force`, runs `tillandsias --debug --init` from a pristine state, and (if init is clean) launches `tillandsias --cloud 8007342/tillandsias --opencode` with ONE short fixed smoke prompt (run the credential guard, reply with a fixed token; never the /meta-orchestration loop, 1941-smkp). Every issue observed — by this skill's agent during install/reset/init, or by the agent's one turn inside the forge — is filed as a plan/issues work packet for later pickup by /advance-work-from-plan.
project-discipline
How THIS project wants its branches and pushes shaped — read its branch-discipline level (0 push to main freely; 1 integration branch + pull requests; 2 work refs into the integration branch), the drift between what it declares and what its history shows, which hooks it has installed, how to raise it, and the work format each hook refusal asks for. Every refusal that says "use /project-discipline for instructions" resolves here. Works for any project in a Tillandsias forge, not only Tillandsias.
probe-macos-tray-on-windows
Daily cross-platform health probe — compiles the macos-tray crate from the local Windows host. Expected to succeed as a stub (the crate is fully cfg-gated for non-macOS); a build failure here means a shared module or crate broke for everyone. Files structured findings to plan/issues/probe-macos-tray-on-windows-findings-<DATE>.md.
multihost-orchestration
Compatibility alias for the canonical coordinate-multihost-work skill.
merge-to-main-and-release
Refresh linux-next, open/update the PR to main, merge once green, bump VERSION, tag, and trigger the workflow_dispatch release. Runs on a daily cadence so the latest linux-next work reaches a downloadable Linux release operators can smoke-test on a Fedora Silverblue host.