Skill Tracer
Skills › Compound Engineering › ce-sweep › state-schema.md

state-schema.md

157 lines · skills/ce-sweep/references/state-schema.md at 3449230

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Sweep state schema (v1)
2
3This is the canonical, versioned contract for the ce-sweep state file. The deterministic state engine (`scripts/sweep-state.py`) is the **only** program that writes the file. Every peer agent (source connectors, the analyzer, the orchestrator) reads the file and changes it **exclusively** through the engine's subcommands, so the rules below are enforced in one place. Read this before touching state.
4

Sweep state schema (v1)

This is the canonical, versioned contract for the ce-sweep state file. The deterministic state engine (scripts/sweep-state.py) is the only program that writes the file. Every peer agent (source connectors, the analyzer, the orchestrator) reads the file and changes it exclusively through the engine's subcommands, so the rules below are enforced in one place. Read this before touching state.

Raw lines 1 to 4
Runs code scripts/sweep-state.py :3
5## Top-level shape
6
7```yaml
8schema_version: 1
9lease:
10 writer: "sweep-2026-07-02-cron"
11 timestamp: "2026-07-02T12:00:00+00:00"
12 ttl_minutes: 60
13sources:
14 "slack:C42":
15 cursor: "1699999999.000100"
16 sensitive: true # optional; config-derived
17items:
18 "slack:C42:1699999999.000100":
19 source: "slack:C42"
20 status: "acknowledged"
21 # ...arbitrary connector fields...
22last_run:
23 timestamp: "2026-07-02T12:05:00+00:00"
24 outcome: "completed"
25 writer: "sweep-2026-07-02-cron"
26 counts: {"ingested": 5, "closed": 1}
27```
29| key | type | meaning |
30| --- | --- | --- |
31| `schema_version` | int | Contract version. Currently `1`. A file missing this key is treated as corrupt. |
32| `lease` | map | Single-writer mutex (see Lease). Absent when no writer holds it. |
33| `sources` | map keyed by source id | Per-source resume cursor and optional flags. |
34| `items` | map keyed by `<source-id>:<item-id>` | Per-item lifecycle record. The key is source-scoped so a source-native id (a Slack ts, a GitHub issue number) never collides with the same id from another source. Personas pass a source-native `id` plus `--source`; the engine composes the storage key and records both `source` and `id` on the item so it stays self-describing. |
35| `last_run` | map | Bookkeeping for the most recent sweep (see run-record). |

Top-level shape

schema_version: 1
lease:
  writer: "sweep-2026-07-02-cron"
  timestamp: "2026-07-02T12:00:00+00:00"
  ttl_minutes: 60
sources:
  "slack:C42":
    cursor: "1699999999.000100"
    sensitive: true          # optional; config-derived
items:
  "slack:C42:1699999999.000100":
    source: "slack:C42"
    status: "acknowledged"
    # ...arbitrary connector fields...
last_run:
  timestamp: "2026-07-02T12:05:00+00:00"
  outcome: "completed"
  writer: "sweep-2026-07-02-cron"
  counts: {"ingested": 5, "closed": 1}
keytypemeaning
schema_versionintContract version. Currently 1. A file missing this key is treated as corrupt.
leasemapSingle-writer mutex (see Lease). Absent when no writer holds it.
sourcesmap keyed by source idPer-source resume cursor and optional flags.
itemsmap keyed by <source-id>:<item-id>Per-item lifecycle record. The key is source-scoped so a source-native id (a Slack ts, a GitHub issue number) never collides with the same id from another source. Personas pass a source-native id plus --source; the engine composes the storage key and records both source and id on the item so it stays self-describing.
last_runmapBookkeeping for the most recent sweep (see run-record).
Raw lines 5 to 36
37## Compatibility rule (forward/backward)
39The engine is deliberately additive-safe so a newer writer and an older reader can share a file:
41| situation | engine behavior |
42| --- | --- |
43| Unknown top-level key | Preserved on every write-back. Never dropped. |
44| Unknown field on an item or source | Preserved on write-back. Never dropped. |
45| Unknown `status` value | Preserved and passed through. Skip, never drop. The closed enum below is not a whitelist. |
46| File parses but has no `schema_version` | Treated as `CORRUPT`; the engine refuses to write over it. |
47| `schema_version` greater than the engine knows | Still read and written with every field preserved. The engine only *adds* rules per version and never removes fields. |
49Bump `schema_version` only for a change that a v1 reader could misinterpret. Purely additive fields do not require a bump.

Compatibility rule (forward/backward)

The engine is deliberately additive-safe so a newer writer and an older reader can share a file:

situationengine behavior
Unknown top-level keyPreserved on every write-back. Never dropped.
Unknown field on an item or sourcePreserved on write-back. Never dropped.
Unknown status valuePreserved and passed through. Skip, never drop. The closed enum below is not a whitelist.
File parses but has no schema_versionTreated as CORRUPT; the engine refuses to write over it.
schema_version greater than the engine knowsStill read and written with every field preserved. The engine only adds rules per version and never removes fields.

Bump schema_version only for a change that a v1 reader could misinterpret. Purely additive fields do not require a bump.

Raw lines 37 to 50
51## Status enum
53Closed set of known lifecycle states. Unknown values are preserved, never dropped, so a future state can roll out writer-first.
55| status | meaning |
56| --- | --- |
57| `ingested` | Captured from a source; not yet triaged. |
58| `ack_deferred` | Receipt noted, but triage deferred to a later pass. |
59| `acknowledged` | Triaged and accepted into the sweep pipeline. |
60| `needs_download` | Referenced media/attachment must be fetched before analysis. |
61| `needs_analysis` | Content present, awaiting analysis. |
62| `manual_stuck` | Blocked; needs manual intervention to proceed. |
63| `analyzed` | Analysis complete; findings recorded on the item. |
64| `in_plan` | Folded into a plan or tracked work item. |
65| `fix_pending` | A fix is underway or awaiting verification. Also the downgrade target for an under-evidenced `closed`. |
66| `closed` | Resolved and verified. REQUIRES all three evidence fields (see below). |
67| `source_gone` | The originating source or message no longer exists. |

Status enum

Closed set of known lifecycle states. Unknown values are preserved, never dropped, so a future state can roll out writer-first.

statusmeaning
ingestedCaptured from a source; not yet triaged.
ack_deferredReceipt noted, but triage deferred to a later pass.
acknowledgedTriaged and accepted into the sweep pipeline.
needs_downloadReferenced media/attachment must be fetched before analysis.
needs_analysisContent present, awaiting analysis.
manual_stuckBlocked; needs manual intervention to proceed.
analyzedAnalysis complete; findings recorded on the item.
in_planFolded into a plan or tracked work item.
fix_pendingA fix is underway or awaiting verification. Also the downgrade target for an under-evidenced closed.
closedResolved and verified. REQUIRES all three evidence fields (see below).
source_goneThe originating source or message no longer exists.
Raw lines 51 to 68
69## Evidence fields and the `validate` downgrade rule
71A `closed` item is a claim that work shipped and was verified, so the engine holds it to proof. An item may only remain `closed` if it carries all three:
73| field | meaning |
74| --- | --- |
75| `fix_ref` | Reference to the fix (PR/commit/issue link). |
76| `verified_merge_sha` | The merge commit SHA the fix landed on. |
77| `verified_at` | ISO timestamp the fix was verified. |
79`validate` scans every item and downgrades any `closed` item missing (or with a falsy value for) any of these back to `fix_pending`. It then rewrites the file and returns the list of downgraded ids. This self-heals a state left inconsistent by a crashed run. `validate` is lease-agnostic. It is a repair, run at sweep start.

Evidence fields and the validate downgrade rule

A closed item is a claim that work shipped and was verified, so the engine holds it to proof. An item may only remain closed if it carries all three:

fieldmeaning
fix_refReference to the fix (PR/commit/issue link).
verified_merge_shaThe merge commit SHA the fix landed on.
verified_atISO timestamp the fix was verified.

validate scans every item and downgrades any closed item missing (or with a falsy value for) any of these back to fix_pending. It then rewrites the file and returns the list of downgraded ids. This self-heals a state left inconsistent by a crashed run. validate is lease-agnostic. It is a repair, run at sweep start.

Raw lines 69 to 80
81## `sensitive` semantics
83The **primary** sensitivity mechanism is per-item. The orchestrator reads each source's config `sensitive` flag and includes `"sensitive": true` in the item JSON on every `upsert-item` for that source (run.md phase 2d). A `sensitive: true` on a **source entry** in state is a defensive fallback the engine also honors, but nothing seeds it today. The per-item flag is what enforces R28, so sensitivity works even though source entries carry only a `cursor`. On any `upsert-item` where either the item or its source entry is sensitive, the engine **drops `body` and `quote` before writing**, so redacted content never reaches disk. All other fields (title, url, status, ids) are retained. Redaction happens at write time, so flipping a source to sensitive protects only items written after the flag is set. Re-ingest to redact prior items.

sensitive semantics

The primary sensitivity mechanism is per-item. The orchestrator reads each source's config sensitive flag and includes "sensitive": true in the item JSON on every upsert-item for that source (run.md phase 2d). A sensitive: true on a source entry in state is a defensive fallback the engine also honors, but nothing seeds it today. The per-item flag is what enforces R28, so sensitivity works even though source entries carry only a cursor. On any upsert-item where either the item or its source entry is sensitive, the engine **drops body and quote before writing**, so redacted content never reaches disk. All other fields (title, url, status, ids) are retained. Redaction happens at write time, so flipping a source to sensitive protects only items written after the flag is set. Re-ingest to redact prior items.

Raw lines 81 to 84
85## id-keyed merge rule
87Each writer changes only the keys it sends, not the whole file. `upsert-item` performs an **id-keyed merge**. It loads the existing item, replaces only the keys present in the incoming JSON, and preserves every other field already on that item. `source` is always (re)set from `--source`. No subcommand semantically rewrites the whole file. Each one changes only the keys it was given and preserves the rest, even though the physical write re-emits the file atomically. This lets independent connectors and the analyzer touch the same item across passes without clobbering each other's fields.

id-keyed merge rule

Each writer changes only the keys it sends, not the whole file. upsert-item performs an id-keyed merge. It loads the existing item, replaces only the keys present in the incoming JSON, and preserves every other field already on that item. source is always (re)set from --source. No subcommand semantically rewrites the whole file. Each one changes only the keys it was given and preserves the rest, even though the physical write re-emits the file atomically. This lets independent connectors and the analyzer touch the same item across passes without clobbering each other's fields.

Raw lines 85 to 88
89## Lease (single-writer mutex)
91| field | meaning |
92| --- | --- |
93| `writer` | Unique id of the writer holding the lease. |
94| `timestamp` | ISO time the lease was last stamped. It is stamped on acquire and re-stamped on every mutating write by the holder. |
95| `ttl_minutes` | Minutes after which an un-refreshed lease is reclaimable (default 60). |
97Rules the engine enforces:
99- `lease-acquire` succeeds (`OK`) when the lease is free or already held by the same writer (re-entrant, re-stamps). It returns `LOCKED` when a *live* lease is held by another writer, or `STALE-RECLAIMED` (with `previous_writer` / `previous_timestamp`) when it takes over a lease older than its TTL.
100- Every mutating call (`upsert-item`, `cursor-advance`) **re-checks ownership** before writing and returns `LEASE-LOST` (no write) if the caller is not the current holder. On success it **re-stamps** the lease timestamp so a long sweep keeps the lease alive.
101- `lease-release` clears the caller's own lease (`OK`, also `OK` if none is held). Releasing another writer's lease returns `LEASE-LOST` and does not write.
102- Staleness is only asserted when it can be *proven* from parseable timestamps. An unparseable lease timestamp is treated as live and is never stomped.

Lease (single-writer mutex)

fieldmeaning
writerUnique id of the writer holding the lease.
timestampISO time the lease was last stamped. It is stamped on acquire and re-stamped on every mutating write by the holder.
ttl_minutesMinutes after which an un-refreshed lease is reclaimable (default 60).

Rules the engine enforces:

  • lease-acquire succeeds (OK) when the lease is free or already held by the same writer (re-entrant, re-stamps). It returns LOCKED when a live lease is held by another writer, or STALE-RECLAIMED (with previous_writer / previous_timestamp) when it takes over a lease older than its TTL.
  • Every mutating call (upsert-item, cursor-advance) re-checks ownership before writing and returns LEASE-LOST (no write) if the caller is not the current holder. On success it re-stamps the lease timestamp so a long sweep keeps the lease alive.
  • lease-release clears the caller's own lease (OK, also OK if none is held). Releasing another writer's lease returns LEASE-LOST and does not write.
  • Staleness is only asserted when it can be proven from parseable timestamps. An unparseable lease timestamp is treated as live and is never stomped.
Raw lines 89 to 103
104## Topology scope
106The lease's guarantee depends on where the state file lives:
108| topology | lease scope | protocol |
109| --- | --- | --- |
110| local-commit mode (default) | Single writer **per checkout**. | The lease serializes overlapping sweeps in the same working tree (e.g. a cron sweep and a manual one). The file is written in-tree (and may be committed locally). No cross-machine guarantee. |
111| pushed-shared-branch | One writer **per repo**. | The state file lives on a shared branch multiple checkouts push to. `lease-acquire` must be committed, pushed, and confirmed (fetch back and verify our writer won) **before any source-side write**. This makes the lease a repo-wide mutex across machines. |
113TTL-based reclaim (`STALE-RECLAIMED`) is what lets a crashed or killed writer's lease be taken over after `ttl_minutes` without manual cleanup.

Topology scope

The lease's guarantee depends on where the state file lives:

topologylease scopeprotocol
local-commit mode (default)Single writer per checkout.The lease serializes overlapping sweeps in the same working tree (e.g. a cron sweep and a manual one). The file is written in-tree (and may be committed locally). No cross-machine guarantee.
pushed-shared-branchOne writer per repo.The state file lives on a shared branch multiple checkouts push to. lease-acquire must be committed, pushed, and confirmed (fetch back and verify our writer won) before any source-side write. This makes the lease a repo-wide mutex across machines.

TTL-based reclaim (STALE-RECLAIMED) is what lets a crashed or killed writer's lease be taken over after ttl_minutes without manual cleanup.

Raw lines 104 to 114
115## run-record
117Records the outcome of a sweep run under `last_run`.
119| field | source | meaning |
120| --- | --- | --- |
121| `timestamp` | `--timestamp` (required) | Caller-supplied ISO run time. The engine never invents it. |
122| `outcome` | `--outcome` | One of `completed`, `aborted-locked`, `partial`, `failed`. |
123| `writer` | `--writer` | The writer id that recorded the run. |
124| `counts` | `--counts` (JSON object) | Free-form tallies (per status, per source, etc.). |
126`run-record` is intentionally **lease-agnostic**. A run that aborted precisely because the lease was `LOCKED` (`outcome: aborted-locked`) must still be able to record that fact, but that write happens while the lease holder is mid-sweep. To keep it from clobbering the holder's concurrent upserts, every mutating subcommand holds an **OS advisory lock** (`flock` on `<state>.lock`) across its whole load-modify-write, so two concurrent invocations serialize their writes regardless of lease ownership. The lease decides *which writer is running the sweep*. The file lock decides *which process is writing the file right now*. The `.lock` file is ephemeral and never committed. The skill's commit step adds only the state file and the plan, never `-A`.

run-record

Records the outcome of a sweep run under last_run.

fieldsourcemeaning
timestamp--timestamp (required)Caller-supplied ISO run time. The engine never invents it.
outcome--outcomeOne of completed, aborted-locked, partial, failed.
writer--writerThe writer id that recorded the run.
counts--counts (JSON object)Free-form tallies (per status, per source, etc.).

run-record is intentionally lease-agnostic. A run that aborted precisely because the lease was LOCKED (outcome: aborted-locked) must still be able to record that fact, but that write happens while the lease holder is mid-sweep. To keep it from clobbering the holder's concurrent upserts, every mutating subcommand holds an OS advisory lock (flock on <state>.lock) across its whole load-modify-write, so two concurrent invocations serialize their writes regardless of lease ownership. The lease decides which writer is running the sweep. The file lock decides which process is writing the file right now. The .lock file is ephemeral and never committed. The skill's commit step adds only the state file and the plan, never -A.

Raw lines 115 to 127
128## Engine status words
130Every subcommand prints one status word on line 1, then an optional JSON payload on line 2. Operational conditions **exit 0** (never a traceback). Only CLI misuse exits non-zero.
132| word | when | payload |
133| --- | --- | --- |
134| `OK` | success | command-specific JSON (or none) |
135| `NO-STATE` | `read` on a file that does not exist yet | — |
136| `CORRUPT` | file exists but does not parse as this schema | — |
137| `LOCKED` | `lease-acquire`: a live lease is held by another writer | — |
138| `STALE-RECLAIMED` | `lease-acquire`: an expired lease was taken over | `{previous_writer, previous_timestamp}` |
139| `LEASE-LOST` | mutating call by a non-holder, or releasing another writer's lease (no write) | — |
140| `REFUSED` | `cursor-advance`: unknown `past-item`, or a cursor that would regress | — |
141| `ERROR` | unexpected internal error (defensive; still exit 0) | — |

Engine status words

Every subcommand prints one status word on line 1, then an optional JSON payload on line 2. Operational conditions exit 0 (never a traceback). Only CLI misuse exits non-zero.

wordwhenpayload
OKsuccesscommand-specific JSON (or none)
NO-STATEread on a file that does not exist yet—
CORRUPTfile exists but does not parse as this schema—
LOCKEDlease-acquire: a live lease is held by another writer—
STALE-RECLAIMEDlease-acquire: an expired lease was taken over{previous_writer, previous_timestamp}
LEASE-LOSTmutating call by a non-holder, or releasing another writer's lease (no write)—
REFUSEDcursor-advance: unknown past-item, or a cursor that would regress—
ERRORunexpected internal error (defensive; still exit 0)—
Raw lines 128 to 142
143## YAML subset
145The state file is genuine YAML restricted to a small, deterministic subset so a stdlib serializer/parser round-trips it exactly. Any YAML parser can read it; only the engine writes it.
147| construct | rule |
148| --- | --- |
149| Indentation | 2 spaces per level, no tabs. |
150| Keys | Bare when they match `^[A-Za-z_][A-Za-z0-9_.-]*$`; otherwise a JSON double-quoted string (so ids with `:` are quoted). |
151| Scalars | `null` / `true` / `false` / integers / floats are bare. **Strings are always JSON double-quoted on a single line** (fully escaped). Never block scalars or multiline. |
152| Non-empty maps | Emitted as block mappings, recursing to any depth. |
153| Lists and empty maps | Emitted as inline JSON flow on one line (e.g. `["a", "b"]`, `{}`), which is itself valid YAML. |
154| Key order | Deterministic: a preferred order for known keys, then remaining keys sorted, so diffs stay stable. |
155| Comments / blank lines | Ignored on read. The engine does not emit comments. |
157A file that fails to parse under these rules, or that parses without a `schema_version`, is `CORRUPT`. The engine reports it and refuses to overwrite, so a hand-mangled file is never silently clobbered.

YAML subset

The state file is genuine YAML restricted to a small, deterministic subset so a stdlib serializer/parser round-trips it exactly. Any YAML parser can read it; only the engine writes it.

constructrule
Indentation2 spaces per level, no tabs.
KeysBare when they match ^[A-Za-z_][A-Za-z0-9_.-]*$; otherwise a JSON double-quoted string (so ids with : are quoted).
Scalarsnull / true / false / integers / floats are bare. Strings are always JSON double-quoted on a single line (fully escaped). Never block scalars or multiline.
Non-empty mapsEmitted as block mappings, recursing to any depth.
Lists and empty mapsEmitted as inline JSON flow on one line (e.g. ["a", "b"], {}), which is itself valid YAML.
Key orderDeterministic: a preferred order for known keys, then remaining keys sorted, so diffs stay stable.
Comments / blank linesIgnored on read. The engine does not emit comments.

A file that fails to parse under these rules, or that parses without a schema_version, is CORRUPT. The engine reports it and refuses to overwrite, so a hand-mangled file is never silently clobbered.

Raw lines 143 to 157