Goal Lifecycle
Contract
Iron law: no lifecycle.status transition without an audit-trail entry — the goal file changes only through bin/flow-goal-record.sh --update-lifecycle, and every transition writes a goal-created (draft → active) or goal-evaluation journal artifact. Invoked by /flow:goal create, /flow:start Phase 1, and /flow:debug for draft → active after goal-contract-capture; by /flow:goal pause | resume | clear; after /flow:goal evaluate confirms a terminal verdict; and by goal-evaluator for non-terminal updates. Inputs: goal id, from-state, to-state, reason, trigger (evaluator | command | hook | user). Returns the new status once the write and the artifact both succeed. Permitted skip: the run event when scope.run_id is unset. Nothing else.
State machine
Non-terminal: draft, active, and the resumable waiting_for_user, waiting_for_ci, blocked. Terminal and immutable: achieved, failed, cancelled — new work needs a new goal id. The allowed-transition table with triggers, the disallowed transitions, and the last_evaluation.result mapping live in references/goal-lifecycle-transitions.md; bin/flow-goal-record.sh enforces the same table and refuses anything outside it (including terminal → any, active → draft, and blocked → achieved without passing through active).
Outputs
- Goal file:
lifecycle.status, lifecycle.last_evaluation, optional current_phase / current_activity, turns_evaluated incremented when leaving active for a non-terminal state.
- Journal artifact:
goal-created for draft → active, goal-evaluation for every other transition.
- One
lifecycle_transition event in .flow/runs/<run-id>/events.jsonl when run_id is set.
Workflow
- Validate — read the goal. If its status differs from the caller's from-state, another process transitioned it: stop and raise the six-field escalation (
references/escalation-format.md). Reject a transition outside the table with a stderr explanation and exit 1.
- Compose the lifecycle fragment:
lifecycle:
status: <to>
current_phase: <preserved or caller-updated>
current_activity: <preserved or caller-updated>
turns_evaluated: <incremented when from=active and to is non-terminal>
last_evaluation:
result: <pass | incomplete | fail | needs_human_review | blocked>
reason: <caller-provided; under 200 chars; comma-safe>
at: <ISO-8601 UTC now>
- Write —
bin/flow-goal-record.sh --update-lifecycle --goal-id <id> --lifecycle-file <fragment> --from-status <from>. The helper takes an O_NOFOLLOW lock, replaces only the lifecycle block, validates against schemas/v1/goal.schema.json when jsonschema is installed, and writes tempfile + rename + fsync. Surface any non-zero exit with its stderr; do not retry blindly — a race means re-reading the state.
- Journal — draft → active:
bin/journal-record.sh --issue {N} --type goal-created --metadata goal_id=<id> --metadata source=<src>. Otherwise: bin/journal-record.sh --issue {N} --type goal-evaluation --metadata goal_id=<id> --metadata result=<to> --metadata reason=<short>. Ad-hoc goals with no issue write to the session journal .decisions/session-{YYYY-MM-DD}.md.
- Run event — when
run_id is set, append {"at","type":"lifecycle_transition","goal_id","from","to"} to .flow/runs/<run-id>/events.jsonl; bin/flow-record-activity.sh appends it as part of the FlowActivity the caller records for the phase boundary.
Rules
- Never edit
.flow/goals/<id>.goal.yaml directly; never bypass the from-state check.
- Never resurrect a terminal goal.
- Never skip the journal artifact because "the lifecycle block records it" — the goal file is local state, the journal is the cross-PR audit trail.
- Keep
last_evaluation.reason short; the reasoning lives in the journal artifact body.
References
plugins/flow/references/goal-lifecycle-transitions.md — transition table, disallowed transitions, result mapping.
plugins/flow/references/decision-journal-schema.md — goal-created and goal-evaluation artifact rows.
1---2name: goal-lifecycle3description: Enforce the FlowGoal state machine: every `lifecycle.status` transition (draft → active → {waiting_for_user, waiting_for_ci, blocked, achieved, failed, cancelled}) writes the new lifecycle block through `bin/flow-goal-record.sh` AND a `goal-created` or `goal-evaluation` artifact to the decision journal. Use when any code path mutates `lifecycle.status`: /flow:goal pause/resume/clear, the draft → active step after goal-contract-capture, the evaluator's verdict, or the Stop hook's stuck detection. A goal in `failed` with no artifact explaining why is worse than no state machine.4---56# Goal Lifecycle78## Contract910Iron law: no `lifecycle.status` transition without an audit-trail entry — the goal file changes only through `bin/flow-goal-record.sh --update-lifecycle`, and every transition writes a `goal-created` (draft → active) or `goal-evaluation` journal artifact. Invoked by `/flow:goal create`, `/flow:start` Phase 1, and `/flow:debug` for draft → active after `goal-contract-capture`; by `/flow:goal pause | resume | clear`; after `/flow:goal evaluate` confirms a terminal verdict; and by `goal-evaluator` for non-terminal updates. Inputs: goal id, from-state, to-state, reason, trigger (`evaluator | command | hook | user`). Returns the new status once the write and the artifact both succeed. Permitted skip: the run event when `scope.run_id` is unset. Nothing else.1112## State machine1314Non-terminal: `draft`, `active`, and the resumable `waiting_for_user`, `waiting_for_ci`, `blocked`. Terminal and immutable: `achieved`, `failed`, `cancelled` — new work needs a new goal id. The allowed-transition table with triggers, the disallowed transitions, and the `last_evaluation.result` mapping live in `references/goal-lifecycle-transitions.md`; `bin/flow-goal-record.sh` enforces the same table and refuses anything outside it (including `terminal → any`, `active → draft`, and `blocked → achieved` without passing through `active`).1516## Outputs17181. Goal file: `lifecycle.status`, `lifecycle.last_evaluation`, optional `current_phase` / `current_activity`, `turns_evaluated` incremented when leaving `active` for a non-terminal state.192. Journal artifact: `goal-created` for draft → active, `goal-evaluation` for every other transition.203. One `lifecycle_transition` event in `.flow/runs/<run-id>/events.jsonl` when `run_id` is set.2122## Workflow23241. **Validate** — read the goal. If its status differs from the caller's from-state, another process transitioned it: stop and raise the six-field escalation (`references/escalation-format.md`). Reject a transition outside the table with a stderr explanation and exit 1.252. **Compose** the lifecycle fragment:26 ```yaml27 lifecycle:28 status: <to>29 current_phase: <preserved or caller-updated>30 current_activity: <preserved or caller-updated>31 turns_evaluated: <incremented when from=active and to is non-terminal>32 last_evaluation:33 result: <pass | incomplete | fail | needs_human_review | blocked>34 reason: <caller-provided; under 200 chars; comma-safe>35 at: <ISO-8601 UTC now>36 ```373. **Write** — `bin/flow-goal-record.sh --update-lifecycle --goal-id <id> --lifecycle-file <fragment> --from-status <from>`. The helper takes an O_NOFOLLOW lock, replaces only the `lifecycle` block, validates against `schemas/v1/goal.schema.json` when `jsonschema` is installed, and writes tempfile + rename + fsync. Surface any non-zero exit with its stderr; do not retry blindly — a race means re-reading the state.384. **Journal** — draft → active: `bin/journal-record.sh --issue {N} --type goal-created --metadata goal_id=<id> --metadata source=<src>`. Otherwise: `bin/journal-record.sh --issue {N} --type goal-evaluation --metadata goal_id=<id> --metadata result=<to> --metadata reason=<short>`. Ad-hoc goals with no issue write to the session journal `.decisions/session-{YYYY-MM-DD}.md`.395. **Run event** — when `run_id` is set, append `{"at","type":"lifecycle_transition","goal_id","from","to"}` to `.flow/runs/<run-id>/events.jsonl`; `bin/flow-record-activity.sh` appends it as part of the FlowActivity the caller records for the phase boundary.4041## Rules4243- Never edit `.flow/goals/<id>.goal.yaml` directly; never bypass the from-state check.44- Never resurrect a terminal goal.45- Never skip the journal artifact because "the lifecycle block records it" — the goal file is local state, the journal is the cross-PR audit trail.46- Keep `last_evaluation.reason` short; the reasoning lives in the journal artifact body.4748## References4950- `plugins/flow/references/goal-lifecycle-transitions.md` — transition table, disallowed transitions, result mapping.51- `plugins/flow/references/decision-journal-schema.md` — `goal-created` and `goal-evaluation` artifact rows.