Story workflow — status, ceremonies, validation, documentation¶
Purpose : map every Story going through the project to a small, predictable status machine in OpenProject, with explicit gates between states (so the operator never has to guess what's next), the rituals that produce each gate's verdict, and the documentation artefact every state must leave behind.
Authority : this document is the canonical operational reference for Story state management. CLAUDE.md §0 redirects here for the gory details. ADR-0081 is the invariant authority behind the 8-state contract ; this file is its implementation guide. ADR-0068 (Expert Committee as default review channel) and ADR-0069 (OpenProject as orchestrator) are the upstream ADRs.
Audience : the operator (and Claude when assisting). No team-wide rollout assumed — the project is single-operator today.
1. The 8 happy-path states (per ADR-0081)¶
[ New ]
│ pick + plan dossier file created
▼
[ In specification ]
│ committee plan_review verdict=PASSED
▼
[ Specified ]
│ impl branch + first commit
▼
[ In progress ]
│ PR opened + QA + CI/CR/committee green
▼
[ Developed ]
│ squash merge to main
▼
[ In testing ]
│ Deploy K8s SUCCESS + smoke test passes
▼
[ Tested ]
│ ML/FTF gate verdict / Infra runbook / final closure comment
▼
[ Closed ]
Plus 2 escape hatches : On hold (pause with ETA) and Rejected (committee blocker without resolution path) — accessible from any happy-path state.
These map to 8 of the 14 OP statuses available — always referenced by name, never by numeric id, since the IDs are instance-specific and the tooling (scripts/openproject_import_gh.py) resolves statuses by name :
| OP status name (authoritative) | Workflow role |
|---|---|
New |
filed, not picked |
In specification |
picked, plan dossier + plan_review in flight |
Specified |
plan PASSED, impl-ready buffer |
In progress |
active dev (single-WIP) |
Developed |
PR open + CI/CR green, awaiting merge |
In testing |
merged + deployed in K8s, awaiting smoke + ML gate |
Tested |
system-validated, awaiting final closure verdict |
Closed |
done |
On hold |
escape — pause with ETA |
Rejected |
escape — committee blocker, work item closed without merge |
Note : OpenProject status IDs vary across instances. To resolve the current ID for a name, query
GET /api/v3/statuseson the active instance. Do not hardcode IDs in tooling — always look them up by name.
The other 4 OP statuses (Confirmed, To be scheduled, Scheduled, Test failed) are not used — operator-side discipline replaces them. They're scaffolding for multi-team workflows that don't fit the single-operator model.
1.1 Sprint version assignment — Stories only¶
The OP version field (= sprint) follows a strict convention :
| Work package type | version |
Rationale |
|---|---|---|
| Story | always set (Backlog OR active sprint) | the unit of delivery — single-WIP applies, sprint = engagement window |
| Epic | never set (always NONE) |
aggregates Stories spanning multiple sprints ; assigning a sprint creates a false promise of single-sprint closure |
| Need | never set (always NONE) |
strategic objective on a multi-sprint / multi-month timeline |
When a Story is created, it lands in Backlog by default ; the operator moves it to the active sprint version on pick (per §0 of CLAUDE.md). When a Story closes, the version stays — the closure record is the historical mapping of work to sprints.
Epics and Needs surface in the OP Roadmap implicitly via their child Stories' parent links (via the OP UI Hierarchy filter). They do not need their own version row to be visible.
Tooling rule : the canonical importer
scripts/openproject_import_gh.pydoes not setversionfor any type. Operators (or ad-hoc creation scripts) MUST NOT setversionon Epics or Needs. Ad-hoc Story creation scripts SHOULD default toBacklogif no explicit sprint is provided.
2. Per-state contract¶
Tooling rule — flipping OP status : every "Operator flips OP status to X" step below is the ADR-81 gate ritual (fetch
lockVersion, PATCH the status by name, leave a mandatory verdict + evidence comment). Usescripts/op_story_transition.pyto do it in one call — it resolves the status by name, validates the edge against the legal-edge graph in this section, and enforces the verdict comment :python scripts/op_story_transition.py \ --wp 218 --to "In progress" \ --verdict "Plan PASSED; impl started" \ --evidence "branch fix/...-S29; committee 2b0ef8c9; meeting 201" \ --cvn-id CVN-N001-EE-S29Non-canonical edges (rework back-edges, state skips) require
--forceand--force-reason "<why>".--dry-runpreviews the PATCH + comment with zero writes. (op_story_sync.pyonly covers Stories in the F1 plan §10 tracking table; this CLI covers any work package by id — EE/diagnostic/tooling Stories.)
2.1 New¶
- Trigger to enter : Story created in OP under a
version=opensprint (or in theBacklogversion, with explicit justification when later pulled). - What happens : backlog grooming. Story body should already capture the problem, the user-facing goal, and the type (ML/FTF / Infra/Ops / ADR/Process / Docs).
- Ceremonies : none.
- Documentation produced : Story body in OP + linked GitHub issue (created via
scripts/openproject_import_gh.pyor manually). - Gate to exit (→
In specification) : operator picks the Story → creates the plan dossier file underdocumentation/reviews/YYYY-MM-DD-<slug>-plan.md(or, for Docs / ADR Stories, the ADR draft file itself ; for trivial bug Stories, the GH issue body serves as the plan with explicit operator note in OP comment).
2.2 In specification¶
- Trigger to enter : plan dossier file exists in working tree (committed or uncommitted on the impl branch). OP status flipped to
In specificationby the operator. - What happens : operator drafts the plan dossier ; runs committee
plan_review(or marks INCONCLUSIVE → addresses gap → re-runs). - Ceremonies :
- Committee
plan_review(ADR-68) —python scripts/expert_committee.py --artifact <dossier> --question "..." --session-type plan_review --issue "#NNN". Default-mandatory ; waivable per ADR-68 invariant on operator override with written justification in OP comment + explicit re-review deadline. - Plan dossier iteration if committee returns
INCONCLUSIVEorREJECTEDwith addressable blockers. - Documentation produced :
documentation/reviews/YYYY-MM-DD-<slug>-plan.md— the plan dossiercommittee/sessions/<session_id>_committee.json— the committee session output (auto-saved byscripts/expert_committee.py)- For ML Stories : MLOps readiness draft begins (per ADR-70)
- Gate to exit (→
Specified) : committeeplan_reviewverdictPASSED(orINCONCLUSIVEwith written waiver per the rules above). Operator flips OP status toSpecified.
2.3 Specified¶
- Trigger to enter : plan_review PASSED + OP status flipped to
Specified. - What happens : Story is "impl-ready", waiting in the queue. The operator may have several
SpecifiedStories — single-WIP does NOT apply here. - Ceremonies : none (the gate already happened).
- Documentation produced : (none beyond
In specification's — ML Stories should have their MLOps readiness signed off by this point per ADR-70) - Gate to exit (→
In progress) : implementation branch created (feat/<cvn_id>-...orfix/<cvn_id>-...) with at least one commit pushed. Operator flips OP status toIn progress.
2.4 In progress¶
- Trigger to enter : impl branch created + first commit pushed.
- What happens : code + tests + docs + (if applicable) MLOps readiness completion. Single-WIP rule applies — at most ONE Story
In progressper operator at any time. - Ceremonies : local QA (
make qa+mkdocs build --strict) before the PR opens. No formal external ceremony at this stage. - Documentation produced :
- Code + unit tests + integration tests
- For Infra / Ops Stories :
documentation/design/<cvn_id>-<slug>-design.md(signed off by operator before code in non-trivial cases) - For ML / Infra-touching-ML Stories (per ADR-70) :
documentation/stories/<cvn_id>/mlops_readiness.md - For new alerts : a new runbook under
documentation/runbooks/(drafted, published inTested) - Gate to exit (→
Developed) : pull request opened onmain; local QA green ; CI 14 checks green ; CI workflow guardrails (G1-G4) green ; CodeRabbit cycle complete with no actionable comments in the latest review (4-5 passes typical) ; if applicable, committeepr_review(ADR-68) verdictPASSED.
2.5 Developed¶
- Trigger to enter : PR open + CI/CR/committee all green per the gate above. Operator flips OP status to
Developed. - What happens : Story is "merge-ready", awaiting operator's go-ahead to squash. The operator may have several
DevelopedStories — single-WIP does NOT apply here (onlyIn progressdoes). - Ceremonies : none (the gates already happened in
In progress). Optional final operator skim of the diff before clicking merge. - Documentation produced : (none beyond
In progress's — the PR description is the audit artefact) - Gate to exit (→
In testing) : operator squash-merges tomain; squash commit message ends withCloses #NNN. The auto-syncer detects the merge + flips OP toIn testingwithin 5 min (per ADR-0079 invariant 10 + ADR-0081 invariant 3).
2.6 In testing¶
- Trigger to enter : squash merge to
mainrecorded. - What happens : K8s deploy workflow runs ; operator validates a representative DAG / API endpoint / Console page ; for ML/FTF Stories, the FTF sweep is triggered + per-track gate decision is computed.
- Ceremonies :
- Deploy K8s — workflow
.github/workflows/cvntrade-deploy-k8s.ymlMUST show SUCCESS for the merge SHA. - Smoke test — operator triggers a representative DAG / hits an API endpoint / loads a Console page (depending on Story type). Pass criterion = no regression vs pre-merge baseline.
- FTF sweep + per-track gate (ML/FTF only) — operator triggers
dag_finetune__ptewith the new factor, waits for ≥ 90 % cell completion, applies the 6 official gates ofF1_BUY_BOOST_PLAN.md§6 (with f1_buy-primary derogation for the F1 mission). - Documentation produced :
- PR description with links to plan dossier + committee session ID
- Squash commit message ending with
Closes #NNN(auto-closes the GH issue) — already created at gate entry - For ML/FTF :
documentation/missions/<area>/YYYY-MM-DD-<slug>-results.md— the results dossier draft (gate-by-gate verdict + provisional decision) - Gate to exit (→
Tested) : Deploy K8s SUCCESS + smoke test passes + (for ML/FTF) per-track gate decision is computable from the sweep results.
2.7 Tested¶
- Trigger to enter : system validation passed. Operator flips OP status to
Tested. - What happens : final-closure prep. For ML : the per-track gate verdict (
lock/keep available/abandon) is finalised in the results dossier ; for Infra : the runbook is published if alerting changed ; for ADR :adr/index.md+ CLAUDE.md are updated if invariant introduced. - Ceremonies : final operator review of the closure artefacts.
- Documentation produced :
- For ML/FTF : results dossier complete with per-track gate verdict + sign-off checklist all checked
- For Infra : runbook published under
documentation/runbooks/(if alerting changed) - For ADR :
documentation/adr/index.mdupdated + CLAUDE.md updated if invariant introduced - Gate to exit (→
Closed) : operator posts final OP comment with PR # + commit SHA + verdict + artefact links.
2.8 Closed¶
- Trigger to enter : final operator OP closure comment posted.
- What happens : the Story is done. OP comment captures the verdict and the references.
- Ceremonies : none for the Story itself. If this is the last
openStory of the active version, triggers a separate version retrospective + gate review (per CLAUDE.md §14) before the next version opens. - Documentation produced :
- OP Story comment with
PR #NNN, squash commit SHA, verdict (lock/keep/abandon for ML), runbook references - For ML : F1 plan §10 row updated → auto-syncer mirrors
- For incidents :
OPERATIONS.md §17.Xlog entry with the standard template
2.9 Escape hatches¶
On hold¶
- When : operator decides to pause (external priority, missing dependency, blocker outside the project scope). Accessible from any happy-path state.
- Mechanism : OP comment with reason + ETA reprise + (optional) link to the blocker. Status →
On hold. The Story is not released from single-WIP if it wasIn progress— operator must explicitly move it back toSpecified(give up the slot) or stay inOn hold(decision documented, slot consumed by a void). - Decay :
On holdfor > 30 days without ETA update should beRejectedor returned toSpecified.
Rejected¶
- When : committee
plan_reviewverdictREJECTEDand operator decides not to address the blockers (entered fromIn specification) ; ORpr_reviewverdictREJECTEDblocking the merge with no scope-fitting correction path (entered fromDeveloped). - Mechanism : OP comment with committee session ID + post-mortem dossier under
documentation/reviews/. Status →Rejected. The work item is closed without a merge ; lessons learned land inOPERATIONS.mdif applicable.
3. Ceremonies — quick reference¶
| Ceremony | Verdict shape | Where in workflow | Mandatory ? |
|---|---|---|---|
| Plan dossier draft | deterministic (text) | gate New → In specification |
yes |
Committee plan_review (ADR-68) |
LLM consensus → PASSED / REJECTED / INCONCLUSIVE | gate In specification → Specified |
yes — INCONCLUSIVE = document gap, agree with operator on next step, re-submit only when gap is closed |
Local QA (make qa + mkdocs build --strict) |
green/red | within In progress |
yes — pre-PR |
| CodeRabbit CR cycle | itératif, 4-5 passes | within In progress |
yes — wait full cycle, never auto-merge mid-cycle |
Committee pr_review (ADR-68) |
LLM consensus | within In progress, before flipping to Developed |
mandatory for Infra/Ops + ML/FTF + ADR PRs ; exemption for docs/dashboards/config-only |
| CI build + tests | green/red | gate In progress → Developed |
yes |
CI workflow guardrails (.github/workflows/pr-workflow-guardrails.yml) |
green/red | gate In progress → Developed |
yes — enforces G1 title format / G2 Story ref / G3 plan dossier / G4 MLOps readiness ; bypass via guardrails-waiver label or repo var GUARDRAILS_KILL_SWITCH=true |
Squash merge to main |
merge SHA | gate Developed → In testing |
yes |
| Deploy K8s + smoke | success/fail | within In testing, post-merge |
yes |
| FTF sweep + per-track gate | lock / keep available / abandon | within In testing, ML/FTF only |
yes for ML/FTF Stories |
| Final closure OP comment | text | gate Tested → Closed |
yes |
| Version retrospective | text rapport | when last Story of a version closes | yes — CLAUDE.md §14 |
4. Documentation per Story type — quick map¶
| Story type | New → In specification → Specified |
In progress → Developed |
In testing → Tested → Closed |
|---|---|---|---|
| ML / FTF | plan dossier + MLOps readiness | code + tests | results dossier (per-track gate verdict) |
| Infra / Ops | plan dossier + design dossier + (if ML paths) MLOps readiness | code + tests + new runbook draft | runbook published (if alerting changed) + OPERATIONS update (if incident-related) + production validation evidence |
| ADR / Process | ADR draft (replaces plan dossier) | adr/index.md update + CLAUDE.md update if invariant introduced | none beyond merge |
| Docs | draft on docs/<slug> branch (no separate plan dossier — the docs ARE the deliverable) |
mkdocs strict build green | visual check on docs.cvntrade.eu post-deploy |
The exact phase template for each Story type lives under documentation/templates/TEMPLATE_story_phases_*.md (4 templates landed in PR #766) and is paste-friendly into the OP Story description. Templates may pre-date this ADR's 8-state contract and may need a refresh — track in CVN-N011-EA backlog.
5. Short rules (operator cheat sheet)¶
- Pick = New → In specification : not
In progress.In specificationis the new "I claimed it, plan_review is running" landing state. - PASSED = In specification → Specified : never skip the committee verdict (or its written waiver per ADR-68).
- First commit = Specified → In progress : the impl branch existing without a commit doesn't promote.
- Single-WIP : at most ONE Story
In progressper operator. Buffer ofSpecifiedStories is unbounded ; same forDeveloped,In testing,Tested. - Merge = Developed → In testing : the auto-syncer flips OP within 5 min of the merge to
main. Operator does not need to manually flip. - Smoke + ML gate = In testing → Tested : both required for ML Stories ; only smoke for non-ML.
- Closure comment = Tested → Closed : always with PR # + commit SHA + verdict + artefact links. For ML, wait for the gate decision after the FTF sweep before flipping to
Closed. - No code without Story : pulling from
Backlogrequires written justification in OP comment. - Verdict triage :
INCONCLUSIVEis notPASSED. Document the gap, agree on next step (gather evidence, time-boxed waiver, or escalation), re-submit only when gap is closed. Closes #NNNin the squash commit message → auto-closes the GH issue. Do not close manually.- Last Story of version : closure triggers
gate review+close version+retrospective(per CLAUDE.md §14). - Stale
In specification/Specified: a Story stuck for > 7 days without progressing should be re-evaluated (priority? blocker? on hold? rejected?).
6. Migration of existing Stories¶
Pas de migration agressive — les Stories en cours suivent leur cycle, le nouveau workflow s'applique aux picks futurs (post-ADR-0081 merge). Le tableau ci-dessous est indicatif pour les Stories actives au moment où ce workflow est introduit.
| Story | Status au moment de l'intro ADR-0081 | Status cible avec ce workflow | Action operator |
|---|---|---|---|
CVN-N011-EA-S09 (ECE bug) |
OP In progress (PR #815 ouvert) |
Developed (PR #815 awaiting merge) ou In testing (post-merge) selon état du PR |
flip on next OP visit |
CVN-N011-EA-S10 (gRPC fork) |
OP In progress |
re-évaluer selon état réel du PR | inspect PR + flip |
CVN-N011-EA-S11 (post-mortem) |
OP New |
reste New jusqu'au pick |
no action |
CVN-N011-EA-S12 (CI guardrails) |
OP In progress |
re-évaluer | inspect + flip |
CVN-N011-EA-S13 (this ADR) |
OP In progress (the ADR PR) |
suit le 8-state flow dès l'introduction | natural progression |
CVN-N014-EA-S01 (PTE dedup wp#90) |
OP New |
first Story to follow the new 8-state flow end-to-end | pick → flip to In specification |
CVN-N014-EA-S03 (FTF empty-window wp#104) |
OP New |
reste New jusqu'au pick |
no action |
Les Stories pré-ADR qui n'ont pas suivi le 8-state flow ne sont PAS re-classées rétroactivement. Leur historique reste dans le 5-state ancien — l'audit-trail OP préserve la séquence réelle des transitions.
7. Cross-references¶
- CLAUDE.md §0 — entry point for the operator workflow (pick + dev process). Redirects to this file for the gory details.
- ADR-0081 — the invariant authority behind this workflow (8 states, rituals per gate, escape hatches).
- ADR-0068 — Expert Committee as the default review channel. Defines
plan_reviewandpr_reviewsession types. - ADR-0069 — OpenProject as project orchestrator (SSoT). Defines the link between OP Stories, GH issues, and commits. Amended by ADR-0081 for the state model.
- ADR-0070 — MLOps readiness template mandatory before merge for ML-touching PRs.
- ADR-0077 — MkDocs + Structurizr as documentation SSoT.
- ADR-0079 — FTF sweep → Story closure 8-step workflow (the per-Story closure ritual nests inside the ADR-0081 Story state machine).
documentation/templates/TEMPLATE_story_phases_ml_ftf.md— phase template for ML/FTF Stories (5 phases).documentation/templates/TEMPLATE_story_phases_infra.md— phase template for Infra / Ops Stories (5 phases).documentation/templates/TEMPLATE_story_phases_adr.md— phase template for ADR / process Stories (3 phases).documentation/templates/TEMPLATE_story_phases_docs.md— phase template for documentation Stories (3 phases).documentation/OPERATIONS.md§15-16 — the operator runbook for committee sessions and OpenProject orchestration (deeper detail than this file, focused on "how do I do X" rather than "what is the state machine").