Mission Graph
The concrete engine for the mission-graph kernel (Op1.M1 of cyberfleet-batch): a project's work
list — Missions and Operations, the RAW/parent-child/discovered-from links between them, status
changes, and tombstones — written down as a git-tracked, append-only event log (schema v:1),
plus a zero-dependency fold into two read-only views and one write-time guard:
ready — every Mission that is RAW-satisfied (every dependency retired, transitively) and
not WAW-held (its declared touch-set doesn't clash with in-flight work or a lower-id peer).
Deterministic and read-only.
cycles — every RAW strongly-connected component, reported as a repair item. The fold never
throws on a knotted plan; every Mission on a cycle (and anything depending on it) is quarantined
out of ready instead.
checkOperation — is a hand-declared Operation dependency-closed (the capstone's RAW closure
⊆ the declared set), what's its release floor (the closure alone — support members don't gate
it), and its completed/total progress.
It carries a self-contained .mts script (the repo's node-≥23.6 / no-deps convention). Pure
derivations (fold, ready, cycles, checkOperation, proposeEdge) take and return plain data
only — no fs access — kept apart from a thin store-IO seam with two backends: an in-tree JSONL
file and the branch-independent orphan ref refs/sdd/mission-graph, resolved at run time. The swap
never touches a derivation.
Run it
node "<skill>/scripts/mission-graph.mts" ready [--root .] [--format toon|json]
node "<skill>/scripts/mission-graph.mts" cycles [--root .] [--format toon|json]
node "<skill>/scripts/mission-graph.mts" operation --id <operation-id> [--root .] [--format toon|json]
# the write path (append-only; --root defaults to .)
node "<skill>/scripts/mission-graph.mts" append node --id <id> \
[--kind mission|operation] [--status open|claimed|retired] [--touch-set a,b,c] \
[--blast <level>] [--hitl|--afk] [--model-tier <tier>] [--brief-pointer <path>] [--capstone <id>]
node "<skill>/scripts/mission-graph.mts" append edge --kind RAW|parent-child|discovered-from \
--from <id> --to <id> [--override]
node "<skill>/scripts/mission-graph.mts" append tombstone --target node --id <id>
node "<skill>/scripts/mission-graph.mts" append tombstone --target edge --kind <kind> --from <id> --to <id>
# store home + reach (idempotent; --root defaults to .)
node "<skill>/scripts/mission-graph.mts" migrate [--root .]
node "<skill>/scripts/mission-graph.mts" sync [--root .] [--remote origin]
Default --root is the current directory. The store home is resolved at run time — the orphan ref
refs/sdd/mission-graph inside a git work-tree, else the in-tree file
<root>/.agents/mission-graph/events.jsonl; MISSION_GRAPH_STORE=in-tree|orphan-ref overrides.
Default --format is TOON; --format json emits the same records as JSON for non-LLM
consumers.
migrate retires the in-tree seed after copying it into the ref — git rm the store file,
leaving the deletion staged, not committed (the engine's only working-tree write; it reports that
the commit is required for the migration to reach other clones). Why it is not optional: the seed
is a tracked file, so a seed left behind travels to every clone, where resolveBackend prefers it
over the (never-fetched) ref — the clone reads a stale list that looks valid and no sync can
dislodge it, because the seed is precisely what stops the clone consulting the ref. Two guards:
it refuses to retire a seed whose every line is not already in the ref (deleting otherwise would
be the data loss migration exists to avoid), and it does retire a seed an earlier migration left
behind, so a pre-fix project is repaired by re-running migrate.
Reach. refs/sdd/* is outside refs/heads/*, so git's default refspec never fetches or
pushes it: the ref is shared by every worktree of one clone and travels no further on its own.
sync is the one step that widens that reach.
sync names the ref explicitly on its own command line, so it needs no git configuration and
writes none — not a fetch refspec, not a push refspec. Verified: a fresh clone carrying only the
default +refs/heads/*:refs/remotes/origin/* fetches the store ref fine when the refspec is given
as an argument. The engine never touches .git/config.
No refspec is ever written in its forced (+-prefixed) form. A leading + means force: with
+refs/sdd/*:refs/sdd/* in play, a diverged store ref is overwritten at exit 0 with
(forced update), silently destroying the local appends — the exact data loss the compare-and-swap
write path exists to prevent. Non-forced, the same transfer reports
! [rejected] (non-fast-forward) and exits 1, leaving the ref intact.
sync is fast-forward only, in both directions. Its cases are a total partition of
(local ref, remote ref) — each side present or absent, and when both are present the pair is
exactly one of identical / one an ancestor of the other (either way) / neither:
Two checks run before the partition, in this order — (1) backend: under the in-tree backend,
no-op reporting the home, exit 0, no network touched (an in-tree project must not fail merely for
being offline); (2) reachability: a remote unreachable or not configured fails non-zero and is
never reported as "remote has none". Then:
| local |
remote |
action |
| absent |
absent |
report both absent, change nothing |
| present |
absent |
push |
| absent |
present |
collect |
| present |
present, same commit |
report agreement, change nothing |
| present |
present, local is ancestor |
fast-forward local |
| present |
present, remote is ancestor |
push |
| present |
present, neither is ancestor |
refuse loudly, report both tips, change nothing |
There is no --force; divergence means two write-deciders, which the store surfaces rather than
resolves. The reachability row is load-bearing: git ls-remote fails the same way for an
unreachable remote as for a missing ref, so a naive read conflates them and lands on "both absent" —
reporting a benign empty state for a remote it never saw. That is the silent-empty defect this whole
capability exists to remove; establish reachability first and fail non-zero when it cannot be
established.
Making an ordinary git fetch collect the ref too is out of scope here. That means writing a
persistent fetch refspec into a clone's config — an opt-in, per-clone setup act with a real cost (a
diverged store ref then makes unrelated git fetch calls exit 1). It belongs to the onboarding
skill that already asks consent before writing operational config, not to this engine.
Recovering from a refused sync. The refusal names both tips. Read each side's log with
git cat-file -p <tip>:events.jsonl, then — as the single write-decider — rebuild one list holding
both sides' events and append it forward from the tip you keep. The store is append-only, so no
event is dropped by that reconcile; sync does not automate it, because choosing the surviving
order is a decision, not a merge.
Never configure remote.<remote>.push. Setting it replaces the default branch push refspec,
so a plain git push silently stops sending the current branch while reporting
"Everything up-to-date". sync passes its push refspec as an argument and writes no push config.
sync is orphan-ref-only. Under the in-tree backend it is a no-op that reports the active
backend — an in-tree store rides its branch and needs no ref transport.
append edge runs the write-time cycle guard first: a RAW edge that would close a loop is
rejected unless --override is passed (a genuinely-discovered mutual dependency). A
parent-child/discovered-from edge is never guarded — only RAW ("wait for") edges can cycle.
ready's frontier entries carry id, node, operation, blast, hitlOrAfk, modelTier, briefPointer, whyReady — node is the node's kind (always mission: an Operation is a container, never
itself scheduled).
Boundaries
Read-only derivation + an append-only write path over one in-tree store — it does not decide
how to split a request into Missions, compute a Mission's touch-set, tell a real collision from a
false one at finer-than-node grain (v1: any same-node touch-set overlap is a hard collision), rank
or annotate the frontier, run or assign a Mission, or coordinate a fleet of agents (all deferred,
see the spec's Non-goals). Status is read only from the graph, never from a Mission's
.plan.md brief — the graph is the sole scheduling authority.
1---2name: mission-graph3description: Internal skill: the mission-graph kernel — a git-tracked, append-only work-graph store folded into a ready frontier and a cycles repair view; used by the cyberfleet-batch dispatch loop, not triggered by users directly.4---56# Mission Graph78The concrete engine for the **mission-graph kernel** (Op1.M1 of cyberfleet-batch): a project's work9list — Missions and Operations, the RAW/parent-child/discovered-from links between them, status10changes, and tombstones — written down as a **git-tracked, append-only event log** (schema `v:1`),11plus a zero-dependency **fold** into two read-only views and one write-time guard:1213- **`ready`** — every Mission that is RAW-satisfied (every dependency retired, transitively) and14 not WAW-held (its declared touch-set doesn't clash with in-flight work or a lower-id peer).15 Deterministic and read-only.16- **`cycles`** — every RAW strongly-connected component, reported as a repair item. The fold never17 throws on a knotted plan; every Mission on a cycle (and anything depending on it) is quarantined18 out of `ready` instead.19- **`checkOperation`** — is a hand-declared Operation dependency-closed (the capstone's RAW closure20 ⊆ the declared set), what's its release floor (the closure alone — support members don't gate21 it), and its completed/total progress.2223It carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention). Pure24derivations (`fold`, `ready`, `cycles`, `checkOperation`, `proposeEdge`) take and return plain data25only — no fs access — kept apart from a thin store-IO seam with **two backends**: an in-tree JSONL26file and the branch-independent orphan ref `refs/sdd/mission-graph`, resolved at run time. The swap27never touches a derivation.2829## Run it3031```bash32node "<skill>/scripts/mission-graph.mts" ready [--root .] [--format toon|json]33node "<skill>/scripts/mission-graph.mts" cycles [--root .] [--format toon|json]34node "<skill>/scripts/mission-graph.mts" operation --id <operation-id> [--root .] [--format toon|json]3536# the write path (append-only; --root defaults to .)37node "<skill>/scripts/mission-graph.mts" append node --id <id> \38 [--kind mission|operation] [--status open|claimed|retired] [--touch-set a,b,c] \39 [--blast <level>] [--hitl|--afk] [--model-tier <tier>] [--brief-pointer <path>] [--capstone <id>]4041node "<skill>/scripts/mission-graph.mts" append edge --kind RAW|parent-child|discovered-from \42 --from <id> --to <id> [--override]4344node "<skill>/scripts/mission-graph.mts" append tombstone --target node --id <id>45node "<skill>/scripts/mission-graph.mts" append tombstone --target edge --kind <kind> --from <id> --to <id>4647# store home + reach (idempotent; --root defaults to .)48node "<skill>/scripts/mission-graph.mts" migrate [--root .]49node "<skill>/scripts/mission-graph.mts" sync [--root .] [--remote origin]50```5152- Default `--root` is the current directory. The store home is resolved at run time — the orphan ref53 `refs/sdd/mission-graph` inside a git work-tree, else the in-tree file54 `<root>/.agents/mission-graph/events.jsonl`; `MISSION_GRAPH_STORE=in-tree|orphan-ref` overrides.55 Default `--format` is **TOON**; `--format json` emits the same records as JSON for non-LLM56 consumers.57- **`migrate` retires the in-tree seed** after copying it into the ref — `git rm` the store file,58 leaving the deletion **staged, not committed** (the engine's only working-tree write; it reports that59 the commit is required for the migration to reach other clones). **Why it is not optional:** the seed60 is a *tracked* file, so a seed left behind travels to every clone, where `resolveBackend` prefers it61 over the (never-fetched) ref — the clone reads a **stale list that looks valid** and no `sync` can62 dislodge it, because the seed is precisely what stops the clone consulting the ref. Two guards:63 it **refuses** to retire a seed whose every line is not already in the ref (deleting otherwise would64 be the data loss migration exists to avoid), and it **does** retire a seed an earlier migration left65 behind, so a pre-fix project is repaired by re-running `migrate`.66- **Reach.** `refs/sdd/*` is outside `refs/heads/*`, so git's default refspec never fetches or67 pushes it: the ref is shared by every **worktree of one clone** and travels no further on its own.68 `sync` is the one step that widens that reach.69- **`sync` names the ref explicitly on its own command line**, so it needs **no git configuration and70 writes none** — not a fetch refspec, not a push refspec. Verified: a fresh clone carrying only the71 default `+refs/heads/*:refs/remotes/origin/*` fetches the store ref fine when the refspec is given72 as an argument. The engine never touches `.git/config`.73- **No refspec is ever written in its forced (`+`-prefixed) form.** A leading `+` means *force*: with74 `+refs/sdd/*:refs/sdd/*` in play, a diverged store ref is overwritten at **exit 0** with75 `(forced update)`, silently destroying the local appends — the exact data loss the compare-and-swap76 write path exists to prevent. Non-forced, the same transfer reports77 `! [rejected] (non-fast-forward)` and **exits 1**, leaving the ref intact.78- `sync` is **fast-forward only**, in both directions. Its cases are a **total partition** of79 `(local ref, remote ref)` — each side present or absent, and when both are present the pair is80 exactly one of identical / one an ancestor of the other (either way) / neither:8182 Two checks run **before** the partition, in this order — (1) **backend**: under the in-tree backend,83 no-op reporting the home, exit 0, no network touched (an in-tree project must not fail merely for84 being offline); (2) **reachability**: a remote unreachable *or not configured* fails non-zero and is85 **never** reported as "remote has none". Then:8687 | local | remote | action |88 |---|---|---|89 | absent | absent | report **both absent**, change nothing |90 | present | absent | push |91 | absent | present | collect |92 | present | present, same commit | report agreement, change nothing |93 | present | present, local is ancestor | fast-forward local |94 | present | present, remote is ancestor | push |95 | present | present, neither is ancestor | **refuse loudly**, report both tips, change nothing |9697 There is no `--force`; divergence means two write-deciders, which the store surfaces rather than98 resolves. **The reachability row is load-bearing:** `git ls-remote` fails the same way for an99 unreachable remote as for a missing ref, so a naive read conflates them and lands on "both absent" —100 reporting a benign empty state for a remote it never saw. That is the silent-empty defect this whole101 capability exists to remove; establish reachability first and fail non-zero when it cannot be102 established.103- **Making an ordinary `git fetch` collect the ref too is out of scope here.** That means writing a104 persistent fetch refspec into a clone's config — an opt-in, per-clone setup act with a real cost (a105 diverged store ref then makes unrelated `git fetch` calls exit 1). It belongs to the onboarding106 skill that already asks consent before writing operational config, not to this engine.107- **Recovering from a refused sync.** The refusal names both tips. Read each side's log with108 `git cat-file -p <tip>:events.jsonl`, then — as the single write-decider — rebuild one list holding109 both sides' events and append it forward from the tip you keep. The store is append-only, so no110 event is dropped by that reconcile; `sync` does not automate it, because choosing the surviving111 order is a decision, not a merge.112- **Never configure `remote.<remote>.push`.** Setting it *replaces* the default branch push refspec,113 so a plain `git push` silently stops sending the current branch while reporting114 "Everything up-to-date". `sync` passes its push refspec as an argument and writes no push config.115- **`sync` is orphan-ref-only.** Under the in-tree backend it is a **no-op** that reports the active116 backend — an in-tree store rides its branch and needs no ref transport.117- `append edge` runs the **write-time cycle guard** first: a RAW edge that would close a loop is118 **rejected** unless `--override` is passed (a genuinely-discovered mutual dependency). A119 parent-child/discovered-from edge is never guarded — only RAW ("wait for") edges can cycle.120- `ready`'s frontier entries carry `id, node, operation, blast, hitlOrAfk, modelTier, briefPointer,121 whyReady` — `node` is the node's kind (always `mission`: an Operation is a container, never122 itself scheduled).123124## Boundaries125126Read-only derivation + an append-only write path over one in-tree store — it does **not** decide127how to split a request into Missions, compute a Mission's touch-set, tell a real collision from a128false one at finer-than-node grain (v1: any same-node touch-set overlap is a hard collision), rank129or annotate the frontier, run or assign a Mission, or coordinate a fleet of agents (all deferred,130see the spec's Non-goals). Status is read **only** from the graph, never from a Mission's131`.plan.md` brief — the graph is the sole scheduling authority.