Status: draft design spec
Audience: Brier operators, OpenClaw reducer implementers, Symphony workflow authors
This document defines the ticket state model that sits between the human tracker and the machine evidence graph.
The key idea is:
- humans should see a small, legible FSM
- automation should operate on a richer evidence vector
The Brier case study showed that the human tracker state alone is not enough for honest automation. A ticket could be:
In Progressin Linear while the PR was already ready to mergeDonein repo truth while still stale in Linear- merged in GitHub while runtime proof was still missing
- technically claimed by Symphony while no meaningful work had happened yet
The recommended human-facing workflow is:
BacklogTodoIn ProgressReviewMergingReworkDone
Reviewis the canonical handoff name.Human Reviewshould be treated as a historical alias only during migration.Blockedis best treated as an orthogonal machine or label concept, not necessarily a primary tracker state.Donemeans tracker closure, not automatically runtime proof.
The human FSM is intentionally coarse. That is good for operators. It is bad as the only automation substrate.
The machine needs to know facts such as:
- was the ticket claimed?
- is there a workspace?
- has a meaningful diff appeared?
- does a PR exist?
- are required checks green?
- was the PR merged?
- was Linear reconciled?
- does runtime proof exist?
- is the lane stalled?
Those are not human-facing states. They are evidence fields.
The reducer should track at least the following dimensions per ticket.
| Dimension | Example fields | Why it exists |
|---|---|---|
| Identity | issue_id, issue_identifier, repo, project_slug |
stable correlation |
| Planning | state_human, dependencies_satisfied, spec_node_ids |
what should be happening |
| Claim/execution | claimed_at, worker_id, workspace_path, session_id |
whether Symphony really picked it up |
| Workspace progress | first_meaningful_diff_at, changed_files, workpad_seen |
bootstrap vs real work |
| Git publication | branch, commit_count, latest_commit_sha, branch_pushed_at |
code artifact evidence |
| PR status | pr_number, pr_state, pr_url, review_decision |
review surface truth |
| Check status | required_checks, checks_green, failing_checks, last_check_update_at |
merge gate truth |
| Merge status | merged_at, merge_commit_sha |
what actually landed |
| Tracker reconciliation | tracker_reconciled_at, tracker_state_matches_repo |
prevent Linear drift |
| Runtime proof | runtime_required, runtime_proved_at, runtime_proof_ids |
code truth vs live truth |
| Blockers | blocker_kind, blocker_summary, needs_human, retry_count |
honest escalation |
| Freshness | last_event_at, stale_after_s, stalled |
dead-lane detection |
A reducer-owned ticket record can look like this:
{
"issue": {
"id": "linear:ERICJUT-23",
"identifier": "ERICJUT-23",
"repo": "ericjuta/brier",
"state_human": "In Progress",
"spec_node_ids": [
"spec:mlb-struct/request-efficiency"
]
},
"execution": {
"worker": "symphony",
"claimed_at": "2026-04-04T13:12:00Z",
"workspace_path": "/home/ericjuta/code/brier-symphony-workspaces/ERICJUT-23",
"session_id": "019d..."
},
"git": {
"branch": "ericjut-23-request-efficiency",
"latest_commit_sha": "abc123",
"branch_pushed_at": "2026-04-04T13:28:00Z"
},
"pr": {
"number": 20,
"state": "OPEN",
"checks_green": true,
"merged_at": null
},
"runtime": {
"required": true,
"proved_at": null,
"proof_ids": []
},
"freshness": {
"last_event_at": "2026-04-04T13:31:22Z",
"stale_after_s": 2700,
"stalled": false
},
"blocker": null
}The reducer should derive machine labels from the evidence vector instead of writing them as primary truth.
Useful derived labels include:
claimedbootstrap_onlyactive_with_diffpr_openreview_readyreview_blockedmergeablemerged_awaiting_tracker_reconcileruntime_proof_pendingstalledblocked_needs_humancomplete
These are for supervision and frontier selection. They should not replace the human FSM.
Expected evidence:
- dependency-safe
- not yet claimed
- no current branch/PR required
Bad smell:
Todowith an attached live workspace and no recent events can mean the worker never moved the ticket toIn Progress
Expected evidence:
- claim or workspace exists
- at least one of: workpad, meaningful diff, commit, PR
Bad smells:
- long-running
In Progresswith no meaningful diff - repeated retries with the same blocker
- token burn without branch or commit evidence
Expected evidence:
- PR exists
- validation is green or the remaining failures are explicitly understood
- review comments have been swept
- workpad or equivalent progress record is current
Bad smells:
Reviewwith no PRReviewwhile required checks are still red for unresolved reasonsReviewlong after merge already happened
Expected evidence:
- branch is approved and actively being landed
- a merge operation is actually in flight
Bad smell:
- long-lived
Merging
This state should normally be transient.
Expected evidence:
- actionable review feedback or explicit reopen
- a real diff loop is happening again
Bad smell:
Reworkwithout any active reviewer feedback or blocker note
Expected evidence:
- merge commit exists, or the relevant repo landing proof exists for the ticket type
- Linear is reconciled
- if runtime proof is part of acceptance, that proof also exists
Bad smell:
Donewith no merge proofDonewhile runtime reconcile is still clearly pending for a runtime-affecting slice
The reducer should treat these as drift conditions.
| Combination | Why invalid | Reducer response |
|---|---|---|
Done but no merged artifact |
tracker got ahead of repo truth | emit drift event; reopen or flag |
Review with no PR |
handoff state lacks code artifact | downgrade to active drift or flag |
In Progress with no claim/diff/commit for too long |
likely ghost lane | mark stalled; surface blocker |
merged PR but tracker still In Progress |
tracker drifted behind repo truth | reconcile or raise exact drift |
| merged and tracker done but runtime proof missing for required ticket | code closure != acceptance closure | keep parent spec node open |
| queue dry but spec nodes still incomplete | current tranche complete, mission not complete | frontier selector must mint next work or surface blocker |
Staleness should be computed by stage, not globally.
Suggested defaults:
| Stage | Example stale threshold |
|---|---|
| claimed, no workpad or diff | 15-30 minutes |
| diff started, no commit | 45-90 minutes |
| PR open, checks not started | 15-30 minutes |
| checks running | CI-provider-dependent |
| merged, tracker not reconciled | 5-15 minutes |
| runtime-proof pending | ticket-specific |
These are policy values, not hard laws. They should stay configurable.
A blocker should not require inventing a separate top-level tracker state. The machine model should capture:
blocker_kind- auth
- missing secret
- flaky CI
- runtime drift
- dependency not ready
- review feedback
- unknown
needs_humanretry_countlast_blocked_atblocker_summaryunblock_action
That lets the reducer stay honest without exploding the human FSM.
Some tickets can be closed at merge. Others cannot.
The machine model therefore needs a boolean such as:
runtime.required
Examples where runtime proof is likely required:
- alert delivery
- live ingestion behavior
- service wiring
- scheduler behavior
- endpoint exposure
- deployment topology changes
Examples where runtime proof may be optional:
- docs-only changes
- pure refactors
- fixtures/tests only
- internal scoring docs that do not claim live behavior changed
During migration, the reducer should normalize aliases:
Human Review->ReviewIn Review->Reviewwhen used as the human handoff equivalent
The stored canonical human state should still be Review.
Observed shape:
- PR merged cleanly
- tracker still active
- Symphony could reopen or continue stale work
Machine interpretation:
merge_commit_shapresenttracker_state_matches_repo = false- derived label:
merged_awaiting_tracker_reconcile
Observed shape:
- session existed
- tokens were being burned
- only bootstrap fingerprint was present
Machine interpretation:
- claim exists
first_meaningful_diff_at = null- derived label:
bootstrap_only
Observed shape:
- cadence fix merged on
master - live Phoenix workers still came from old Symphony workspaces
Machine interpretation:
- repo artifact complete
- runtime proof missing
- derived label:
runtime_proof_pending
The ticket model should stay split:
- human FSM for operator legibility
- machine evidence vector for automation honesty
That split is what makes event-driven orchestration realistic instead of performative.