Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .agents/skills/fleet-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -957,10 +957,12 @@ it existed.
So **the repo does not back your queue up.** Say that plainly when someone
assumes otherwise; `.gitignore`'s header owns the reasoning.

`./scripts/check.sh queue` validates your records and re-proves the ordering and
wake claims against a throwaway queue. It runs in the gate, so a change that
quietly makes the queue serialize by default fails there rather than in a run
six weeks later.
`./scripts/check.sh queue` re-proves the ordering and wake claims against a
throwaway queue. It runs in the gate, so a change that quietly makes the queue
serialize by default fails there rather than in a run six weeks later. It does
**not** read your records — the gate reads no operator state, so one commit gets
one verdict in every checkout. `./scripts/fleet-status.sh --records` validates
them, and `./scripts/queue.sh check` lists every problem.

## The loop

Expand Down
6 changes: 6 additions & 0 deletions .agents/skills/update-fleet/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ and this skill changes no tracked state. Print what failed and hand it over.

A green gate is worth one line. A red one is worth the failing check's output.

**The gate says nothing about the operator's own records** — it reads no queue
record and no registry map, so its verdict here is the verdict CI gave. Whether
those are sound after the sync is `./scripts/fleet-status.sh --records`: report
its two lines, and a problem there as one in the records, not in what
arrived.

## 8. Hand over the stale lead — the step that cannot be automated

If an `INSTRUCTION_PATH` moved, the running Mission Control session **froze
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ jobs:
# install.sh and pane-ask.sh, driven end to end; same stubs, same job.
- run: ./scripts/check.sh onboarding install

# Unfiltered, because what it guards is every check's INPUTS: that no check
# or selftest reads a queue record, a registry map, a gitignored setting, the
# caller's HOME or their git config. A change to any script can break that,
# and a runner holds none of those things, so only a job that builds them —
# a poisoned checkout under a hostile host — can see it. Seconds.
isolation:
name: Isolation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- run: ./scripts/check.sh isolation

# Single required status check. Every job feeds this gate; a branch ruleset
# requires only "All Checks", so adding or removing jobs never needs a
# ruleset change — but a new job MUST be added to `needs:` below, or it can
Expand All @@ -231,6 +243,7 @@ jobs:
- skills
- pane
- onboarding
- isolation
runs-on: ubuntu-latest
steps:
- name: Verify all checks passed
Expand Down
8 changes: 5 additions & 3 deletions .no-mistakes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

commands:
# scripts/check.sh IS the repo's gate — shellcheck, rumdl over every tracked
# markdown file, a YAML parse of the whole tree plus the registry's shape,
# the session profiles, the task queue, and the skills-symlink check. Named
# explicitly rather than left to agent
# markdown file, a YAML parse of the whole tree, the session profiles, the
# task queue, the skills-symlink check, and isolation, which proves the gate
# itself reads no operator state. It reads no live queue record and no
# registry map either — those are `./scripts/fleet-status.sh --records`, the
# operator's own health check. Named explicitly rather than left to agent
# detection so the step is deterministic, and pointed at the same script CI
# and the prek hooks run, so a green local run and a green pull request mean
# the same thing. That parity matters more here than in most repos: CI only
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repos:
pass_filenames: false

- id: yaml
name: YAML parses, registry shape holds
name: YAML parses
entry: ./scripts/check.sh yaml
language: system
files: '\.ya?ml$'
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ runs it, the prek hooks run it, and `.no-mistakes.yaml` points its `lint`
command at it, so a green local run and a green pull request mean the same
thing.

**The gate reads no operator state** — not the queue's records, the registry
map, a gitignored `*.conf`, your HOME or your git config — so one commit gets
one verdict in a worker's worktree, on CI and in this checkout alike.
`./scripts/fleet-status.sh --records` is where your live records are validated
now; `check.sh isolation` is what keeps the gate from reading them
again, and every selftest goes through `scripts/lib/selftest-env.sh`.

Changes that open a pull request land by **squash merge** — the only merge
method the remote allows — so the pull request title becomes the commit on
`main`. `CONTRIBUTING.md` owns that process.
Expand Down
115 changes: 77 additions & 38 deletions scripts/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@
# scripts/check.sh --fix markdown # apply the fixes a check can apply
#
# Checks: shell, markdown, yaml, profiles, queue, reconcile, status, skills,
# pane, voice, automerge, onboarding, install, sync. Only `markdown` has a fixer; `--fix` is a no-op for
# the rest, so `scripts/check.sh --fix` is always safe to run.
# pane, voice, automerge, onboarding, install, sync, isolation. Only `markdown`
# has a fixer; `--fix` is a no-op for the rest, so `scripts/check.sh --fix` is
# always safe to run.
#
# IT READS NO OPERATOR STATE. One commit gets one verdict — on CI, in a worker's
# worktree, and in the control-plane checkout — so no check reads what a running
# fleet wrote into a checkout: the queue's records, the registry map, the
# gitignored `orchestration/*.conf`, the reconciler's runtime, or the caller's
# HOME and git config. Checks read tracked files; every selftest runs through
# `scripts/lib/selftest-env.sh`. The live half moved and did not vanish:
# `./scripts/fleet-status.sh --records` validates the operator's queue records
# and registry map, and `./scripts/queue.sh check` still validates the queue on
# its own. `isolation` holds the line, by re-running the
# checks that could leak in a poisoned copy of the tree under a hostile host.
#
# Requires: shellcheck, rumdl, python3 (with PyYAML), lua. A missing tool
# fails the check rather than skipping it — a gate that silently passes when
Expand Down Expand Up @@ -97,7 +109,7 @@ check_yaml() {
fi

if python3 scripts/lib/check_yaml.py "${files[@]}"; then
ok "yaml: ${#files[@]} tracked files parse, registry shape holds"
ok "yaml: ${#files[@]} tracked files parse"
else
fail "yaml"
fi
Expand Down Expand Up @@ -125,26 +137,22 @@ check_profiles() {
fi
}

# The task queue, in two halves. `queue.sh check` validates the local
# records — a blocker naming a task that no longer exists, a state word nobody
# defined — and says so and passes when the queue has never been used, the way
# check_yaml.py treats an unsynced registry.
# The task queue's claims. The queue makes claims that are easy to invert by
# accident: that independent work goes out all at once, that file overlap does
# NOT serialize, that a turn ending is not a task finishing. Each is a test
# against a throwaway queue, so a change that quietly reverses one fails here
# rather than in a run six weeks later.
#
# `queue-selftest.sh` is the other half and the more important one. The queue
# makes claims that are easy to invert by accident: that independent work goes
# out all at once, that file overlap does NOT serialize, that a turn ending is
# not a task finishing. Each is a test against a throwaway queue, so a change
# that quietly reverses one fails here rather than in a run six weeks later.
# NOT this checkout's own records. `queue.sh check` over them used to run here,
# which validated 72 live topics in the control-plane checkout and an empty
# directory everywhere else — so a record nobody touched could fail a commit
# there that CI passed. Those records are the operator's, and
# `fleet-status.sh --records` is where they are validated now.
check_queue() {
need python3 queue || return

local out
if ! out="$(./scripts/queue.sh check)"; then
fail "queue: ./scripts/queue.sh check"
return
fi
if ./scripts/queue-selftest.sh >/dev/null; then
ok "queue: ${out#queue check: }, ordering and wake claims hold"
ok "queue: ordering and wake claims hold"
else
# Re-run visibly: a failing claim is the whole message.
./scripts/queue-selftest.sh
Expand Down Expand Up @@ -547,9 +555,15 @@ check_voice() {
}
printf 'OPERATOR_NAME=GATEOP\nASSISTANT_NAME=GATEAI\n' >"$tmp/voice.conf"

# The glyph and the agent render into the same manifest, and off the
# tracked defaults only: an operator's session-glyphs.conf or agent.conf in
# this checkout decides nothing about this commit.
mkdir -p "$tmp/defaults/orchestration"
cp orchestration/*.example.conf "$tmp/defaults/orchestration/"

local out="$tmp/FLEET.rendered.md" report
if ! report="$(FLEET_VOICE_CONF="$tmp/voice.conf" \
./scripts/install-extension.sh --render-only "$tmp" 2>&1)"; then
if ! report="$(FLEET_VOICE_CONF="$tmp/voice.conf" FLEET_GLYPH_ROOT="$tmp/defaults" \
FLEET_AGENT_ROOT="$tmp/defaults" ./scripts/install-extension.sh --render-only "$tmp" 2>&1)"; then
fail "voice: install-extension.sh --render-only failed: $report"
miss=1
elif [ ! -f "$out" ]; then
Expand Down Expand Up @@ -597,24 +611,30 @@ check_automerge() {
miss=1
fi

# And the set itself, read the way `shepherd` reads it. A checkout with an
# operator's own auto-merge.conf in it answers about that file instead, so
# the gate reads the tracked one directly there rather than passing on a
# result about somebody's private list.
local shipped
if [ -f orchestration/auto-merge.conf ]; then
shipped="skip"
else
shipped="$(FLEET_AUTO_MERGE_REPOS='' python3 -c '
# And the set itself, read the way `shepherd` reads it — from a root that
# holds the tracked example and nothing else, which is exactly what a fresh
# clone holds. Never this checkout's root: an operator's auto-merge.conf
# there would answer instead, and the gate used to answer `skip` rather than
# judge somebody's private list, so the claim went unproven in the one
# checkout that has one.
local shipped fresh
fresh="$(mktemp -d)" || {
fail "automerge: could not make a temp directory for the fresh-clone reading"
return
}
mkdir -p "$fresh/orchestration"
cp "$example" "$fresh/orchestration/"
shipped="$(env -u FLEET_AUTO_MERGE_ROOT FLEET_AUTO_MERGE_REPOS='' python3 - "$fresh" <<'PY' 2>&1
import sys
sys.path.insert(0, "scripts/lib")
import queue as q
print("entries=" + (" ".join(sorted(q.auto_merge_repos(q.checkout_root()))) or "none"))
' 2>&1)"
if [ "$shipped" != "entries=none" ]; then
fail "automerge: a fresh clone would inherit a merge allowlist: $shipped"
miss=1
fi
print("entries=" + (" ".join(sorted(q.auto_merge_repos(sys.argv[1]))) or "none"))
PY
)"
rm -rf "$fresh"
if [ "$shipped" != "entries=none" ]; then
fail "automerge: a fresh clone would inherit a merge allowlist: $shipped"
miss=1
fi

# THE SAME RULE FOR EVERY TRACKED SETTING. An owner, a repository, a
Expand Down Expand Up @@ -665,7 +685,7 @@ print(" ".join(sorted(q.PUBLISH_METHODS)))
{ fail "automerge: the publish methods are '''$shapes''', and must be artifact shapes"; miss=1; }

[ "$miss" -eq 0 ] &&
ok "automerge: no tracked setting names a repository, a tool or an agent"
ok "automerge: no tracked setting names a repository, a tool or an agent, and a fresh clone merges nowhere"
}

# THE SETUP NOBODY RE-RUNS. Onboarding's scripts — preflight, discover-owners,
Expand Down Expand Up @@ -710,6 +730,24 @@ check_install() {
fi
}

# ONE COMMIT, ONE VERDICT. The header's promise that no check reads operator
# state, held by building the worst case: a poisoned copy of the tree under a
# hostile host, with every check that reads settings or records and every
# selftest cheap enough to run twice re-run inside it. Seconds, because the two
# slow selftests are held to the shared helper rather than re-run.
check_isolation() {
need git isolation || return
need jq isolation || return
need python3 isolation || return

if ./scripts/isolation-selftest.sh >/dev/null 2>&1; then
ok "isolation: a poisoned checkout under a hostile host gets the same verdict"
else
./scripts/isolation-selftest.sh
fail "isolation: scripts/isolation-selftest.sh"
fi
}

checks=()
for arg in "$@"; do
case "$arg" in
Expand All @@ -719,7 +757,7 @@ for arg in "$@"; do
done

if [ ${#checks[@]} -eq 0 ]; then
checks=(shell markdown yaml profiles queue reconcile status skills pane voice automerge onboarding install sync)
checks=(shell markdown yaml profiles queue reconcile status skills pane voice automerge onboarding install sync isolation)
fi

for c in "${checks[@]}"; do
Expand All @@ -738,8 +776,9 @@ for c in "${checks[@]}"; do
automerge) check_automerge ;;
onboarding) check_onboarding ;;
install) check_install ;;
isolation) check_isolation ;;
*)
printf 'error: unknown check %q (want: shell markdown yaml profiles queue reconcile status skills pane voice automerge onboarding install sync)\n' "$c" >&2
printf 'error: unknown check %q (want: shell markdown yaml profiles queue reconcile status skills pane voice automerge onboarding install sync isolation)\n' "$c" >&2
exit 2
;;
esac
Expand Down
64 changes: 58 additions & 6 deletions scripts/fleet-status-selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ for tool in python3 git; do
done

tmp="$(mktemp -d)"
# Nothing of this machine's or this checkout's operator reaches this run;
# scripts/lib/selftest-env.sh says what that covers.
# shellcheck source=scripts/lib/selftest-env.sh
. scripts/lib/selftest-env.sh
selftest_isolate "$tmp/env"
export FLEET_QUEUE_DIR="$tmp/queue"
# `topic add` also opens a run log; keep this run's out of the operator's
# orchestration/runs/, the same way FLEET_QUEUE_DIR keeps its queue out.
Expand Down Expand Up @@ -100,7 +105,7 @@ stubbed="$(sandbox "$tmp/bin-stubbed" "${BASE_TOOLS[@]}")"

# --- a queue with something in it -------------------------------------------

"$QUEUE" topic add selftest --title "Selftest topic" --prompt 'the prompt, verbatim' >/dev/null 2>&1
PATH="$bare" "$QUEUE" topic add selftest --title "Selftest topic" --prompt 'the prompt, verbatim' >/dev/null 2>&1
cat >"$tmp/brief.md" <<'MD'
## What to do

Expand All @@ -118,15 +123,15 @@ None.

It is done.
MD
"$QUEUE" add selftest dispatched-task --title "A dispatched task" --repo "$tmp/repo" \
PATH="$bare" "$QUEUE" add selftest dispatched-task --title "A dispatched task" --repo "$tmp/repo" \
--branch t/dispatched --touches FLEET.md --brief-file "$tmp/brief.md" >/dev/null
"$QUEUE" add selftest ready-task --title "A ready task" --repo "$tmp/repo" \
PATH="$bare" "$QUEUE" add selftest ready-task --title "A ready task" --repo "$tmp/repo" \
--branch t/ready --touches FLEET.md --brief-file "$tmp/brief.md" >/dev/null
"$QUEUE" add selftest waiting-task --title "A waiting task" --repo "$tmp/repo" \
PATH="$bare" "$QUEUE" add selftest waiting-task --title "A waiting task" --repo "$tmp/repo" \
--branch t/waiting --brief-file "$tmp/brief.md" >/dev/null
"$QUEUE" block selftest/03-waiting-task --on selftest/01-dispatched-task \
PATH="$bare" "$QUEUE" block selftest/03-waiting-task --on selftest/01-dispatched-task \
--kind semantic-dependency --why 'consumes the flag the first one adds' >/dev/null
"$QUEUE" attach selftest/01-dispatched-task 11111111-1111-1111-1111-111111111111 >/dev/null
PATH="$bare" "$QUEUE" attach selftest/01-dispatched-task 11111111-1111-1111-1111-111111111111 >/dev/null

# --- 1. every probe missing, and it still answers ----------------------------

Expand Down Expand Up @@ -187,6 +192,53 @@ else
fi
expect "the checkout still reports with no queue at all" "CHECKOUT" "$gone"

# --- 3b. the operator's records are validated here, and not in the gate ------
#
# `scripts/check.sh` reads no operator state, so one commit gets one verdict in
# every checkout — which makes `--records` the only place a malformed queue
# record or a registry map of the wrong shape is reported. It is a flag and not
# a section of the screen, because validating opens every record and the
# screen promises never to open an archived one. It is held to this file's
# first rule all the same: a broken record costs lines, never the exit code.

refute "the screen validates nothing, so it opens no record it need not" "RECORDS" "$out"
ok_rec="$(PATH="$bare" "$STATUS" --records 2>&1)"
expect "a sound queue's records read as ok" "queue: ok — 1 topic(s), 3 task(s)" "$ok_rec"
expect "an unsynced registry map is not a problem" "registry: ok — not synced yet" "$ok_rec"

broken="$tmp/broken-queue"
mkdir -p "$broken/bad/01-bad"
printf 'slug: bad\ntitle: A bad topic\n' >"$broken/bad/topic.yaml"
cat >"$broken/bad/01-bad/task.yaml" <<'YAML'
id: 01-bad
topic: bad
title: A bad record
state: half-done
repo: /nowhere
branch: bad/branch
blocked_by:
- {task: bad/42-gone, kind: semantic-dependency, why: made up for this test}
YAML
printf 'owners: not-a-list\n' >"$tmp/bad-registry.yaml"
rec="$(PATH="$bare" FLEET_QUEUE_DIR="$broken" FLEET_REGISTRY_FILE="$tmp/bad-registry.yaml" "$STATUS" --records 2>&1)"
rc=$?
if [ "$rc" -eq 0 ]; then
pass "broken records cost lines, never the command"
else
fail "broken records cost lines, never the command" "exit $rc${nl}$rec"
fi
expect "RECORDS names a state nobody defined" "state 'half-done' is not one of" "$rec"
expect "and a blocker naming a task that does not exist" "bad/42-gone, which does not exist" "$rec"
expect "and a record with no brief" "no BRIEF.md" "$rec"
expect "and says which command lists every one" "queue.sh check" "$rec"
expect "and a registry map of the wrong shape" "owners missing or not a list" "$rec"
recjs="$(PATH="$bare" FLEET_QUEUE_DIR="$broken" "$STATUS" --records --json 2>&1)"
expect "--records --json carries the same problems" "is not one of" "$recjs"

printf 'owners:\n - name: octo\n repos: []\ntotals:\n repos: 0\n' >"$tmp/good-registry.yaml"
rec="$(PATH="$bare" FLEET_REGISTRY_FILE="$tmp/good-registry.yaml" "$STATUS" --records 2>&1)"
expect "a registry map of the right shape reads as ok" "registry: ok — 0 repos across 1 owners" "$rec"

# --- 4. the state vocabulary is not flattened --------------------------------
#
# THE TEST THIS FILE EXISTS FOR. `uncovered` means "wired to report nothing",
Expand Down
Loading