Skip to content

Immutable Shipped Stories

A core principle of MOMENTUM: once a story ships to production, it becomes immutable. It's a historical record of intent and implementation. If behavior needs to change later, you create a new story.

This guide explains why, how to enforce it, and how to handle behavior changes elegantly.


Why Immutable?

1. Auditability

When a story ships, it documents:

  • What we built
  • Why we built it (acceptance criteria)
  • When it shipped
  • What behavior it realized

Rewriting that story later obscures what actually happened. Git's history should reflect reality.

2. Traceability

If a bug is discovered in shipped code, you want to know:

  • What story introduced it?
  • What were the acceptance criteria?
  • What assumptions did we make?

Rewriting the story after the fact makes this impossible.

3. Learning

By keeping old stories intact, you can:

  • Look back and see what assumptions were wrong
  • Understand how a feature evolved
  • Train new team members on "here's what we shipped and why"

4. Coherence

Stories are part of a larger narrative (epics → strategy). Changing a shipped story breaks the narrative. Better to create a new story that explicitly supersedes it.


Shipped Story Lifecycle

Draft → Ready → In Development

Draft
  ├─ Writing acceptance criteria
  ├─ Designer working on UX
  └─ Ready for engineering

In Development
  ├─ Engineering in progress
  ├─ Code review
  └─ Merged to main

Ready for Deployment / In Staging

Deployed to Production

Status: Done (immutable)

├─ Link in related docs
├─ Update metrics
└─ Refer to in new stories if behavior changes

Once a story is marked status: done, do not edit its acceptance criteria, description, or scope.


Metadata for Shipped Stories

Use front-matter to mark shipped status clearly:

---
id: ST-EP003-001
type: story
title: "Redesign checkout form layout"
status: done  # immutable
epic: EP-003
shipped_date: 2025-01-20
owner: bob@example.com
merged_pr: #456
deployed_to_prod: 2025-01-21
related_stories:
  - ST-EP003-003  # Continuation or related work
metrics:
  - form_completion_time
  - form_field_clarity
---

Key fields:

  • status: done – Signals immutability
  • shipped_date – When it went to staging/prod
  • deployed_to_prod – When users experienced it
  • merged_pr – Link to the change that shipped it
  • related_stories – If new stories supersede or extend this one

The Rule: Never Rewrite Shipped Stories

❌ Don't Do This

# Story EP003-001: Redesign checkout form layout

[...original story content...]

## 1. User Story
As a customer, I want an intuitive checkout form, so I can complete purchase quickly.

## 2. Acceptance Criteria
- [ ] Form has fields in order: email, address, card
- [x] SHIPPED ✓ 2025-01-21

🔴 LATER UPDATE (Feb 8):
Actually, we realized we should also validate ZIP codes in real-time...
Let me add that acceptance criterion...

Don't "add" or "update" shipped acceptance criteria.

✅ Do This Instead

Original story (immutable):

---
id: ST-EP003-001
status: done
shipped_date: 2025-01-20
---

# Story EP003-001: Redesign checkout form layout

## 1. User Story
As a customer, I want an intuitive checkout form, so I can complete purchase quickly.

## 2. Acceptance Criteria
- [ ] Form has fields in order: email, address, card
- [ ] Real-time field validation: email, card number
- [ ] Graceful error states for invalid input

New story (tracks the change):

---
id: ST-EP003-007
type: story
title: "Add address ZIP validation to checkout"
status: draft
epic: EP-003
depends_on:
  - ST-EP003-001  # This story's work is based on EP003-001
related_stories:
  - ST-EP003-001
sources:
  - discovery/insights.md#INS-018  # Insight that drove this
---

# Story EP003-007: Add address ZIP validation to checkout

## 1. User Story
As a customer, I want ZIP code validation in checkout, so I don't enter invalid addresses.

## 2. Acceptance Criteria
- [ ] ZIP code field validates on blur (supports US + international formats)
- [ ] Show error if ZIP doesn't match address state
- [ ] Allow retry or use alternate address

## 3. Context
ST-EP003-001 shipped basic address validation but didn't validate ZIP codes. After launch, we learned that ~8% of users enter mismatched ZIP/address combinations, leading to delivery issues (INS-018).

Create a new story that explicitly references the original.


How to Handle Behavior Changes

Scenario 1: Bug Fix (Not a Scope Change)

A shipped story has a bug. Do you create a new story or fix it?

Rule: If the bug violates an acceptance criterion, fix it in the code. Don't change the story – the acceptance criteria remain the truth of what was shipped.

Example:

  • Story EP003-001 says: "ZIP field validates on blur"
  • Bug: ZIP validation broken for international addresses
  • Action: Fix the code (bug fix)
  • Story: Leave unchanged – the AC was the intent; the implementation was buggy

Scenario 2: Scope Change (Post-Ship)

A shipped story's scope needs to change based on user feedback or new requirements.

How to Handle Behavior Changes

Example:

  • Original Story EP003-001: "Form has basic validation"
  • New insight: "Users enter ZIP codes incorrectly" (INS-018)
  • Create: New Story EP003-007 (ZIP validation)
  • Link: ST-EP003-007 references ST-EP003-001

Scenario 3: Deprecation

A shipped story's functionality is being replaced.

Rule: Mark the original story as status: deprecated. Link to the new feature. Leave the original untouched.

Example:

---
id: ST-EP003-005
type: story
status: deprecated  # New field
title: "In-app password reset flow"
deprecated_date: 2025-02-08
superseded_by: [ST-XX-YYY]  # Link to replacement
reason: "User interviews show 85% prefer email reset. Deprecated Feb 8 per design review."
---

# Story EP003-005: In-app password reset flow

[Original story content - unchanged]

Scenario 4: Epic Scope Changed, Affecting Stories

If an epic changes scope and some of its stories are no longer relevant:

  1. Mark those stories status: deprecated
  2. Note why (reason: "Out of scope per PR #123")
  3. Create new stories for new scope items
  4. Link: epics point to current stories; deprecated stories link to their replacements

Example:

In the Feb 8 meeting, you decided to deprecate in-app password reset from the checkout epic.

---
id: ST-EP003-005
status: deprecated
deprecated_date: 2025-02-08
reason: "Deprecation decision from design review (meetings/2025-02-08-design-review.md). In-app reset removed from EP-003 scope."
---

Implications for Epic

When stories are tied to an epic, and you deprecate some:

Epic file /delivery/epic-003-checkout/epic-003-checkout.md:

# ... (Epic header, problem, goals, scope)

## 6. Linked Stories
- **Active Stories:**
  - ST-EP003-001 ✓ Shipped
  - ST-EP003-002 ✓ Shipped
  - ST-EP003-003 In Development
  - ST-EP003-004 Ready

- **Deprecated (out of scope):**
  - ~~ST-EP003-005~~ (In-app password reset; deprecated 2025-02-08)

## 7. Related Epics
- [See below: EP-004 for password reset flow]

And create/update a related epic for the new work:

---
id: EP-004
type: epic
title: "Authentication Improvements"
status: draft
related:
  - EP-003  # Checkout epic (password reset moved out)
---

Story Status Enumeration

Use consistent status values across all stories:

Status Meaning
draft Being written; not ready for review
ready Ready for engineering; acceptance criteria finalized
in-progress Engineering is working on it
done Shipped to production (immutable)
deprecated Was shipped but functionality is no longer in use

Important: Don't use status values like "on-hold" or "cancelled." Instead:

Story Status Enumeration

Add a pre-commit hook or CI check:

#!/bin/bash
# Check for modifications to shipped stories

for file in delivery/**/*.md; do
  if grep -q "^status: done" "$file"; then
    # This story is shipped
    if [ $(git diff --stat "$file" | wc -l) -gt 1 ]; then
      echo "ERROR: Story $file is marked 'status: done' but has uncommitted changes."
      echo "Shipped stories are immutable. Create a new story instead."
      exit 1
    fi
  fi
done

Or, in your LLM instructions (.llm/base.md):

## Immutable Shipped Stories

- Never edit the acceptance criteria or scope of a story marked `status: done`.
- If a shipped story's behavior needs to change, create a new story instead.
- Mark old stories that are superseded as `status: deprecated`.
- **Exception:** Bug fixes in code don't require story updates; the original AC was the intent.

Benefits of Immutability

Auditability – You can trace what was shipped and why
Learning – Understand assumptions and decisions over time
Traceability – Link bugs or metrics back to the original story
Coherence – Product narrative remains intact
Team clarity – New hires understand the full product history


Example: Checkout Epic Story Timeline

# EP-003: Checkout Redesign – Story Timeline

## Shipped Wave 1 (Jan 2025)
- ST-EP003-001 ✓ Form Layout (Jan 20)
- ST-EP003-002 ✓ Field Validation (Jan 25)
- ST-EP003-003 ✓ Error States (Jan 28)

## Shipped Wave 2 (Feb 2025)
- ST-EP003-006 ✓ 3-D Secure Flow (Feb 10)

## Deprecated (out of scope)
- ~~ST-EP003-005~~ In-app Password Reset (deprecated Feb 8, design review)

## Follow-up Stories (from user feedback)
- ST-EP003-007 (draft) ZIP validation (based on INS-018)
- ST-EP003-008 (draft) Mobile checkout UX refinement

Each shipped story is a historical record. Future stories build on lessons learned.