run-state-management
Manage FlowRun state at `.flow/runs/<ISO-timestamp-id>/run.yaml` — create runs at command entry, write activity records via `bin/flow-record-activity.sh` at phase boundaries, transition `state.status`
Files1
View on GitHubSKILL.mdplugins/flow/skills/run-state-management/SKILL.md
GitHubRun State Management
Contract
Iron law: no phase transition without an activity write — state.completed_activities[] is the source of truth for /flow:resume, and a missing activity makes resume skip ahead and lose work. Invoked when FLOW_RUN_STATE=create (gated by flow.runtime.enabled) by /flow:start, /flow:debug, /flow:address, /flow:review, /flow:merge, /flow:release at command entry (create), at every phase boundary (activity), and at completion (terminal transition); /flow:pr appends activities to the active start-issue run; /flow:resume reads its output. Returns .flow/runs/<id>/run.yaml, activities/<NNN>-<name>.yaml, events.jsonl, and a workflow-run journal artifact. Permitted skips: only when flow.runtime.enabled is false, or when step 1 refuses the run directory because .flow, .flow/runs or the run directory is a symlink — then nothing under .flow/ is written.
Inputs
The invoking command MUST pass:
- Workflow id —
start-issue | debug | address-pr | review-pr | merge-pr | release(matchesplugins/flow/workflows/<id>.workflow.yaml). - Run id —
<ISO-8601-compact-timestamp>-<target-slug>, e.g.2026-05-20T143000Z-issue-42. - Context — repo, branch, issue/pr number, linked journal path, linked goal id (or
null). - Phase — initial phase id (
preflightat creation; the workflow's phase order thereafter).
Outputs
.flow/runs/<id>/run.yaml— FlowRun conforming toschemas/v1/run.schema.json..flow/runs/<id>/activities/<NNN>-<name>.yaml— one FlowActivity per phase boundary,schemas/v1/activity.schema.json..flow/runs/<id>/events.jsonl— line-per-event ledger.workflow-runartifact in the linked decision journal (bin/journal-record.sh --type workflow-run), updated with the final status at the terminal transition.
Exact document shapes and the per-workflow phase-order table: references/run-state-templates.md.
Workflow
- Create the FlowRun at command entry. First create its directory with this block,
RUN_IDset to the run id:
# RUN_DIR_CREATE_BLOCK_BEGIN
case "${RUN_ID:-}" in
''|.|..|*/*|*..*) printf '%s\n' "ERROR: RUN_ID must be a run id without '/' or '..', got '${RUN_ID:-}'" >&2; exit 1 ;;
esac
# Never through a symlink: a repository can commit .flow or .flow/runs as a
# link to a directory outside the checkout, where run.yaml would be written.
"$(__fr="${CLAUDE_PLUGIN_ROOT:-}";[ -x "$__fr/bin/cascade-resolve.sh" ]||__fr=$({ printf '%s\n' plugins/flow;ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/synapti-marketplace/flow/*/ 2>/dev/null|sort -Vr;printf '%s\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/marketplaces/synapti-marketplace/plugins/flow"; }|while read -r __p;do [ -x "${__p%/}/bin/cascade-resolve.sh" ]&&{ printf '%s\n' "${__p%/}";break;};done);printf '%s\n' "$__fr")/bin/flow-mkdir.sh" -- ".flow/runs/$RUN_ID" || { [ $? -eq 3 ] && exit 3; exit 1; }
printf '%s\n' "RUN_DIR=.flow/runs/$RUN_ID"
# RUN_DIR_CREATE_BLOCK_END
If it exits non-zero, create no run: say why — exit 1 is a refused symlink, exit 3 a check that could not run (python3 missing), not a symlink — and carry on as when flow.runtime.enabled is false. Otherwise write run.yaml into RUN_DIR (state.status: active, current_phase = initial phase, completed_activities: [], events: [run_started]) by direct file write — race-free because the run is new — and emit the workflow-run journal artifact with status=active.
- Record an activity at every phase boundary (and significant sub-steps): compose the FlowActivity YAML to a temp file, then
bin/flow-record-activity.sh --run-id <id> --activity-file <path>. The helper assigns the sequence number, validates against the schema, writes atomically (O_NOFOLLOW + flock + tempfile+rename), and appends toevents.jsonl. - Update
state.current_*after each activity: advancecurrent_phaseat a boundary, setcurrent_activity, append the recorded id tocompleted_activities[]. Read-merge-write throughbin/_journal_atomic.py(acquire_lock(run.yaml.lock)+ atomic write) — never a bare overwrite. - Terminal transition when the command ends:
state.status→completed(verdict PASS / action succeeded),blockedwithblocked_reason(verdict FAIL or session ended mid-workflow),failed, orcancelled; then re-emit theworkflow-runartifact with the final status. - SessionEnd (
hooks/scripts/session-end-state.sh, not this skill): appends asession_endevent to each active run'sevents.jsonland printsflow: N active FlowRun(s) persisted— it does not mutaterun.yaml; status changes are the user's decision via/flow:resume.
Rules
- Never write
run.yamloutside the helper except the initial create — concurrent updates need flock. - Never advance
state.current_phasewithout writing an activity. - Never mark
state.status: completedwith an emptycompleted_activities[]. - Readers of
events.jsonlMUST skip un-parseable trailing lines (atomic at write, tolerant at read). - Never auto-resume a
blockedrun without surfacingblocked_reasonfirst.
Reuse map
plugins/flow/skills/autonomous-workflow/SKILL.md— phase structure source of truth; this skill materializes its phase boundaries.plugins/flow/bin/flow-record-activity.sh— atomic activity writer.plugins/flow/bin/flow-mkdir.sh— creates the run directory, never through a symlink.plugins/flow/bin/_journal_atomic.py—acquire_lock,_atomic_writefor run.yaml updates.plugins/flow/schemas/v1/run.schema.json,activity.schema.json— document schemas.plugins/flow/references/decision-journal-schema.md—workflow-runandrun-state-transitionartifact rows.plugins/flow/references/flow-runtime-state.md—.flow/layout, gitignore policy, resumability.