Plan dossier — CVN-N014-EC-S18 : epic-close — a gated closure assistant for Epics and project versions¶
Story : CVN-N014-EC-S18 (GH #1132, wp#256) · Epic : CVN-N014-EC — agentic dev-workflow skills (wp#205) · Sister skills :
story-advance(S11),create-story(S14). Statut : plan révisé post-plan_review(session529e0651— PASSED · OK · 0 blocker · consensus strong) ; 3 recos P1 intégrées (§6.1 waiver, §6.2 Gate 5 qualité, §6.5 2ᵉ Epic), follow-ups v2 en §10. Standard d'authoring : SKILL_AUTHORING_STANDARD (S00).
Glossary¶
A reader outside the project needs only these terms:
- OP — OpenProject, the project-management system of record (Epics, Stories, versions, Meetings).
- Epic — a parent work item grouping multiple Stories, optionally mapped to an OP version (a sprint/release container that is
openorclosed). - Story — a unit of work; moves through an 8-state workflow (ADR-81) from
NewtoClosed. - Skill — an operator-invoked workflow assistant (here, a Claude Code skill) with explicit approval before any side-effecting write.
- Closure note — a durable documentation page summarizing an Epic's outcome and evidence at closure.
- Retrospective (retro) — a sprint/version review meeting + artifact (defined by the project's operations playbook).
- Skill authoring standard (S00) — the in-repo rules every skill follows (frontmatter, observability, approval-before-write).
- ADR-69 §14 — the rule defining how a project version is closed. ADR-82 — committee review sessions are archived as OP Meetings. ADR-0101 — the universal Story/Epic documentation standard. ADR-25 — no silent fallback / no silent pass.
Executive summary¶
epic-close is a gated closure assistant for Epics and project versions. It turns a manual, un-tooled, memory-dependent wrap-up ritual into an auditable workflow: discover all Stories in an Epic, verify closure and documentation gates, confirm the retrospective and the acceptance evidence, surface any loose ends, then request explicit operator approval before writing the closure note and closing the version.
The skill does not replace human judgment. It orchestrates existing read-only checks, stops on the first failed gate and reports the precise unmet precondition, and keeps the final write under operator control. Its purpose is to prevent half-closed Epics: closed versions with open Stories, missing documentation, absent retrospectives, broken navigation, or no durable outcome record.
Partie I — Cadrage (lecteur)¶
0. Problématisation (sans jargon)¶
Quand la dernière Story d'un Epic ferme, l'Epic (et sa version/sprint OP) doit être clôturé proprement (ADR-69 §14) : vérifier que toutes les Stories sont fermées, que la documentation est complète et navigable, qu'une rétrospective a eu lieu, que le critère d'acceptance de l'Epic est atteint, puis écrire une note de clôture et fermer la version. Aujourd'hui ce rituel est manuel, non outillé, et dépendant de la mémoire de l'opérateur (cf. la checklist en prose de CLAUDE.md §14). C'est exactement le genre de discipline-workflow qui dérive : un Epic fermé avec une Story encore ouverte, une page hub absente de la navigation, une rétro jamais faite, une version laissée open. story-advance a industrialisé la transition d'une Story ; il manque le pendant au niveau Epic. S18 livre epic-close : la couche de jugement qui vérifie le wrap-up complet avant de clôturer, refuse au premier gate non rempli, et fait remonter tout reliquat détectable avant fermeture.
0bis. Les constats sur lesquels on construit (vérifiés)¶
story-advance(S11) est le modèle : couche de jugement user-only (invocable seulement par l'opérateur) au-dessus d'un CLI déterministe (op_story_transition.py), gates read-only, refus si non rempli, approbation avant write, events d'observabilité.epic-closeen est le frère au niveau Epic.check_story_docs.py(S17, ADR-0101 Inv 5) vérifie déjà, par Story, la présence + headings requis + non-vide du doc-set (hub/plan/architecture/runbook/test_strategy ; unN/Ajustifié est conforme). Mode--blockingdisponible. → réutilisé tel quel pour le gate docs, en boucle sur les Stories de l'Epic.op_save_committee_as_meeting.py(ADR-82) logge une session comité en OP Meeting. → la vérification « trail comité » se fait en lisant les Meetings liés aux Stories (pas en ré-écrivant).mkdocs build --strictest déjà le gate anti-lien-cassé (utilisé à chaque PR de doc). → réutilisé pour « navigation correcte ».- ADR-69 §14 définit la procédure de clôture de version ; la rétro est définie par le playbook opérations (OPERATIONS §16.4). → ce sont les sources normatives que la skill opérationnalise.
- Il n'existe aucun
op_version_*.py: la fermeture de version OP est un geste manuel UI (Settings → Versions → Edit → Status closed). C'est le seul write du rituel sans CLI — d'où la décision de design §6.4 / §9. - Politique liens OP : tout pointeur OP vers la documentation repo DOIT être une URL
docs.cvntrade.eurendue, jamais un chemin repo ou un blob GitHub. → contraint la forme du « pointeur OP » de la note de clôture.
1. User stories¶
- En tant qu'opérateur fermant un Epic, je veux une invocation unique (writes soumis à approbation explicite) qui vérifie tout le wrap-up et me refuse la clôture en nommant le gate précis qui manque, pour ne jamais fermer un Epic à moitié.
- En tant que lecteur de la documentation, je veux que la note de clôture de l'Epic soit une page navigable (dans la navigation sous l'Epic) avec un pointeur depuis OP, pour retrouver l'outcome et l'evidence d'un Epic fermé sans fouiller.
- En tant que mainteneur du process, je veux que la clôture émette des events d'observabilité (comme les autres skills) pour auditer quand et pourquoi un Epic a été fermé.
2. Hypotheses (testable, EN)¶
- H1 — A judgment-layer skill over read-only gates prevents half-closed Epics. The closure ritual is memory-driven prose today; only a gate-checked command refuses on a missing precondition. Test: against a deliberately-incomplete Epic (one Story still
In progress, or a hub missing from nav),epic-closeSTOPs and names the exact failing gate; against a complete Epic it reaches the closure-write step. - H2 — Reuse beats reinvention. The per-Story docs gate already exists (
check_story_docs.py --blocking), the nav gate exists (mkdocs --strict), the committee-trail exists (OP Meetings).epic-closeorchestrates them at Epic scope, adding only the Epic-level checks (all-Stories-Closed, retro-exists, acceptance-evidence, no-loose-ends). Test: the skill shells out to those tools; it does not re-implement their logic. - H3 — The retrospective is a mandatory input, not an output. An Epic must not close without a retrospective that already exists before any closure write. Test: no retro doc/OP-Meeting for the version → STOP (no scaffold, no bypass) with a pointer to the operations playbook. The closure note may summarize and link the retro, but cannot be its first creation.
- H4 — The closure note is a first-class navigable artifact. It lives as a doc page wired into the Epic's navigation + an OP pointer (rendered
docs.cvntrade.euURL). Test: post-close, the note page returns 200 and is reachable from the Epic nav; the OP Epic/version carries the rendered-URL pointer.
3. State of the art (EN)¶
story-advance(S11) — the per-Story sibling; same skeleton (resolve → gate → approve → write → emit).epic-closemirrors its SKILL.md shape andemit_skill_event.pyobservability.create-story(S14) — the opening bookend (creates a StoryNew);epic-closeis the closing bookend (closes the Epic after the last StoryClosed). Together they frame the Story lifecycle.check_story_docs.py(S17) — ADR-0101 Inv 5 CI guardrail; the docs-in-place gate is a loop over--blockingper Story.- ADR-69 §14 (version closure) + ADR-81 (Story 8-state workflow) + ADR-82 (committee→Meeting) + operations playbook §16.4 (retro) — the normative ritual
epic-closeoperationalizes. - No prior art for OP version-close automation —
op_version_close.pywould be a new privileged write surface (see §6.4 for the v1 decision).
4. Definition of Done¶
- Skill livrée :
.claude/skills/epic-close/SKILL.md(+scripts/emit_skill_event.pyselon le standard S00),disable-model-invocation: true(side-effect, user-only), conforme au SKILL_AUTHORING_STANDARD. - Les 7 gates read-only implémentés (§6.2) avec arrêt au premier gate non rempli + nommage de la précondition manquante (ADR-25) — y compris la règle de divergence des sources de vérité (§6.1) et le format d'acceptance evidence (§6.2 Gate 6).
- Note de clôture = page doc navigable, conforme au template minimal (§6.3), câblée dans la navigation sous l'Epic (
mkdocs --strictvert) + pointeur OP vers l'URLdocs.cvntrade.eurendue. - Version-close v1 = geste UI vérifié post-hoc (§6.4) : la skill instruit, l'opérateur ferme en UI, la skill re-vérifie
status=closed. - Approbation avant write (S00 §7) : aperçu
Gate checks for → close:+ dry-run, write OP seulement sur « go » explicite. Observabilité :skill_invoked/skill_operator_approval/skill_completed. - Une Epic par invocation ; les writes OP gardent l'humain dans la boucle.
- Dogfood cadré (§6.5) : fermeture réelle ou dry-run sur CVN-N014-ED + dry-run obligatoire sur un 2ᵉ Epic indépendant (exigence durcie post-
plan_review).
5. Consolidation¶
epic-close scelle le cycle de vie que create-story ouvre et story-advance fait progresser : ouverture (Story New) → progression (8 états ADR-81) → clôture d'Epic (dernière Story Closed → version fermée proprement). La thèse tient en une phrase : epic-close n'automatise pas aveuglément la fermeture ; il transforme un rituel manuel en workflow gate-checké, auditable, avec jugement humain avant tout write. La skill ne réinvente rien (elle orchestre check_story_docs.py, mkdocs --strict, les OP Meetings) et n'ajoute que les vérifications de niveau Epic. La rétro en input obligatoire et la note en artefact navigable + pointeur OP garantissent qu'un Epic fermé laisse une trace exploitable, pas un statut OP changé en silence.
Partie II — Plan¶
6. Design¶
Layout (miroir story-advance) : .claude/skills/epic-close/SKILL.md + scripts/emit_skill_event.py. User-only (disable-model-invocation: true). CLI sous-jacents réutilisés : check_story_docs.py, op_save_committee_as_meeting.py (lecture/idempotent), op_story_transition.py (lecture statut), mkdocs build --strict, gh.
Input : un cvn_id d'Epic (ex. CVN-N014-ED) ou un id de version OP. Une Epic par invocation.
6.1 Phase A — Découverte + sources de vérité (read-only)¶
Résoudre l'Epic + sa version OP + énumérer ses Stories par croisement de plusieurs sources. Chaque dimension a une source de vérité désignée ; toute divergence bloque la fermeture jusqu'à résolution ou waiver explicite (écrit dans la note de clôture) :
| Dimension | Source(s) de vérité |
|---|---|
| Appartenance d'une Story à l'Epic | OP (children de l'Epic) + table du hub Epic + CVN-…-S* dans la nav/docs |
| Statut d'une Story | OP (Closed) et GH issue (closed) |
| Documentation | repo docs + check_story_docs.py |
| Trail comité | OP Meetings + sessions committee/sessions/ |
| Preuve de clôture | page de clôture + URL docs.cvntrade.eu |
Un écart entre sources (ex. OP Story Closed mais issue GH ouverte, hub liste une Story absente d'OP, nav contient une Story fantôme) est surfacé et bloquant par défaut (STOP), jamais dérivé silencieusement d'une seule liste (ADR-25 ; cf. la garde anti-troncature de create-story).
Waiver de divergence (formalisé). Une divergence ne peut être waivée qu'avec un bloc explicite — justification + owner + expiry (si la divergence est une dette à résorber) — et mention dans la note de clôture :
divergence_waiver:
divergence: # la divergence précise (source A dit X, source B dit Y)
justification: # pourquoi elle est acceptable malgré le STOP
owner: # qui en répond
expiry: # date/condition de résorption si c'est une dette (sinon: n/a)
Sans ce bloc, le STOP tient. Le waiver est tracé dans la note de clôture (auditable), jamais un bypass silencieux.
6.2 Phase B — Les 7 gates (STOP au premier FAIL, en nommant le gate)¶
| # | Gate | Mécanisme |
|---|---|---|
| 1 | Toutes Stories Closed |
statut OP de chaque Story = Closed et issue GH fermée. Liste les retardataires. |
| 2 | Docs en place (ADR-0101) | check_story_docs.py --cvn-id <story> --blocking ∀ Story + hub Epic présent avec table complète. |
| 3 | Navigation correcte + hiérarchique | mkdocs build --strict vert + chaque Story de l'Epic a une entrée nav imbriquée SOUS le nœud Epic (sous-arbre repliable, pas des frères à plat) + l'overview Epic, la note de clôture et la rétro référencées sous ce même nœud + URL 200 spot-check. Une Story navigable mais non-imbriquée sous son Epic = FAIL. |
| 4 | Trail comité (ADR-82) | chaque session plan_review/pr_review des Stories est un OP Meeting lié (sinon : pointer la session non-loggée). |
| 5 | Rétro existe + qualité minimale (input obligatoire) | l'artefact rétro (page doc et/ou OP Meeting) existe déjà pour la version, et passe un check qualité minimal (logique check_story_docs) : headings requis présents, non-vide substantiel (pas juste un titre), et contenu couvrant outcomes / learnings / follow-ups. STOP si absent ou perfunctoire → pointeur OPERATIONS §16.4. Pas de scaffold, pas de bypass. La note de clôture résume et lie la rétro, sans jamais en être la première création. |
| 6 | Acceptance Epic avec preuve structurée | voir bloc ci-dessous. |
| 7 | Pas de reliquat | 0 PR ouverte ciblant l'Epic, 0 branche Story non-mergée, 0 issue [gate-failure] ouverte (filtré par cvn_id/Epic). |
Epic acceptance evidence format (Gate 6). The Epic acceptance gate may be quantitative or qualitative, but it must be explicit. epic-close surfaces the acceptance evidence in a structured block:
- Criterion — the Epic-level success condition being claimed.
- Evidence — metric, merged PR, published document, live system behavior, or committee decision supporting the claim.
- Source — URL or artifact reference.
- Operator confirmation — explicit approval that the evidence satisfies the criterion.
If the Epic has no explicit acceptance criterion, epic-close STOPs and asks the operator to define or record one before closure.
6.3 Phase C — Writes de clôture (approbation requise)¶
- Composer la note de clôture selon le template minimal :
# Epic closure note — <EPIC>
## Outcome
## Scope delivered
## Story-by-story closure table
## Acceptance evidence
## Retrospective summary (résumé + lien vers la rétro pré-existante)
## Decisions and trade-offs
## Residual risks / follow-ups
## Documentation and navigation links
## OpenProject / GitHub references
Ces headings rendent la note homogène et testable (check_story_docs ou un check dédié futur).
2. Écrire la note comme page doc sous l'Epic (documentation/epics/<EPIC>-closure.md — décision §9 Q2) + la câbler (avec la rétro) dans la nav mkdocs, imbriquées SOUS le nœud Epic (mkdocs --strict re-vérifié) + merger les docs de clôture sur main (pour qu'elles soient live).
3. Gate de validation opérateur (obligatoire — AVANT le pointeur OP) : présenter une checklist explicite et obtenir la confirmation opérateur que toutes les docs sont (a) livrées (mergées sur main), (b) indexées (chaque Story + note de clôture + rétro imbriquées sous le nœud Epic dans la nav), (c) accessibles (URL 200 sur chacune). STOP tant que l'opérateur n'a pas validé — c'est ce qui empêche de poser un pointeur vers une page pas encore live.
4. Poser le pointeur OP (commentaire Epic + note de version) vers l'URL docs.cvntrade.eu rendue (seulement après que §3 a confirmé les URLs en 200).
5. Fermer la version OP — voir §6.4.
6.4 Version-close — décision v1 (recommandée)¶
Recommended v1 decision: keep the OpenProject version close as a manual UI action, verified post-hoc by the skill.
Rationale: epic-close is primarily a judgment and gate-orchestration layer. Automating the final OpenProject version write would add a new privileged CLI surface before the closure workflow has been dogfooded. In v1 the skill runs all read-only gates, generates the closure note, requests explicit operator approval, instructs the operator to close the version in the UI, then verifies that the version status is actually closed (no silent fail). A dedicated op_version_close.py can be introduced later (S19/Sxx) if repeated usage justifies the extra automation surface. The dominant risk is closing an incomplete Epic, not saving a click.
6.5 Dogfood (cadré — 2ᵉ Epic obligatoire)¶
Le plan_review (529e0651) a tranché le dissensus dogfood : la self-référence seule est insuffisante. Exigence durcie :
- fermeture réelle OU dry-run sur CVN-N014-ED (self-référence, preuve réelle) ;
- ET dry-run OBLIGATOIRE sur un 2ᵉ Epic indépendant (robustesse, scénarios divers).
Le 2ᵉ Epic en dry-run évite le biais « tu valides l'outil avec lui-même » sans exiger une fermeture réelle supplémentaire.
6.6 Phase D — Observabilité¶
skill_invoked (start) / skill_operator_approval (avant write) / skill_completed (fin), via emit_skill_event.py. La note de clôture = record durable.
7. Fichiers / actions¶
| Fichier | Action |
|---|---|
.claude/skills/epic-close/SKILL.md |
créer — le rituel (Phases A–D, gates, garde-fous, observabilité) |
.claude/skills/epic-close/scripts/emit_skill_event.py |
réutiliser l'émetteur canonique (selon S00) |
documentation/epics/<EPIC>-closure.md |
la note de clôture = page doc (créée par la skill au runtime) |
mkdocs.yml |
la skill y câble la note sous l'Epic (au runtime) |
| doc-set S18 (hub + ce plan + test_strategy) | ADR-0101 (la skill se gate elle-même) |
scripts/op_version_close.py |
hors v1 — seulement si l'usage répété le justifie (§6.4) |
8. Risques¶
- R1 — gate d'acceptance Epic non-formalisé : critère qualitatif (ex. CVN-N014-ED) vs métrique (
f1_buy≥…). Mitigation : le format d'acceptance evidence (§6.2) impose criterion/evidence/source/confirmation ; STOP si aucun critère. - R2 — énumération des Stories incomplète : une source en retard masque une Story. Mitigation : croisement multi-sources + divergence bloquante (§6.1), jamais dérivé d'une seule liste (ADR-25).
- R3 — version-close sans CLI : write manuel UI = drift possible. Mitigation : v1 = UI + re-vérification post-hoc du statut
closed(§6.4) ; CLI seulement si justifié plus tard. - R4 — rétro « cochée » mais vide / perfunctoire / circularité rétro↔note : Mitigation : Gate 5 vérifie headings + non-vide substantiel + couverture outcomes/learnings/follow-ups et exige la rétro avant tout write ; la note la résume sans la créer.
- R5 — faux « no loose ends » : PR/branche d'une autre Epic confondue. Mitigation : filtrer par
cvn_id/Epic. - R6 — scope creep « version = release » : un reviewer externe peut attendre changelog/tag. Mitigation : scope borné (§9, note de scope) — tag de release/changelog hors-scope sauf si l'Epic correspond à une release produit déployable.
9. Questions for the committee — résolues au plan_review (529e0651, PASSED · OK · 0 blocker)¶
- Version-close : reco v1 (UI vérifiée post-hoc, §6.4) → VALIDÉE (« prudent, pragmatic, operationally sound »).
op_version_close.pydifféré. - Emplacement de la note : default
documentation/epics/<EPIC>-closure.md→ retenu (co-localisé au hub Epic). - Acceptance Epic : bloc structuré (§6.2) validé ; le champ OP
acceptance_criteriamachine-lisible reste une amélioration optionnelle. - Dogfood : durci — un 2ᵉ Epic indépendant en dry-run est désormais obligatoire (§6.5, §4.7), résolvant le seul dissensus du comité.
- Blind spots : aucun bloquant ; les pistes (archivage d'artefacts run, communication de clôture) → §10 follow-ups v2.
Note de scope : le tagging de release / changelog / milestone GitHub est hors-scope de epic-close, sauf si l'Epic correspond à une release produit déployable — auquel cas ces étapes relèvent du pipeline de déploiement, pas du rituel de clôture d'Epic.
10. Follow-ups v2 (hors S18, non bloquants — issus du plan_review)¶
Forward-looking recommandations du comité, explicitement hors périmètre S18 (à transformer en Stories filles si/quand justifié) :
- 8ᵉ gate futur — archivage des artefacts run : checkpoints ML, feature sets, experiment logs référencés dans l'acceptance evidence / la note de clôture. Pertinent surtout pour les Epics ML.
- Communication de clôture automatique : notification (Slack/email) déclenchée par l'event
skill_completedpour la visibilité projet. - Monitoring du geste UI version-close +
op_version_close.pyen S19 si un drift opérateur récurrent est observé (cf. §6.4). - Ops hardening : version-pin des deps CLI (
check_story_docs.py,mkdocs) contre une casse upstream silencieuse ; SLOs sur les taux de pass/fail des gates + détections de divergence (seuils d'alerte + playbooks) ; kill-switch runtime (flag de config) pour stopper les invocations en incident ; shadow-mode (dry-run only) sur quelques Epics avant d'activer les writes.