Skip to content

SubStatus State Machine

GuideMode uses a state machine to manage issue workflow progression. This document specifies the rules for subStatus transitions and timestamp management.

GuideMode tracks issues using a dual state model:

  • state (3 values): open, in_progress, closed - simplified for GitHub compatibility
  • subStatus (9 values): Fine-grained workflow state for detailed analytics

The subStatus is the source of truth for workflow position, and state is derived from it for providers other than GitHub.

SubStatusPositionDescription
backlog0Not started, not prioritized
ready1Ready to start, prioritized
discovery2Research, spike, or design work
delivery3Active development
review4Code review, testing, QA
SubStatusDescription
blockedWaiting on external dependency
parkedIntentionally paused

Side-track states can occur at any point in the workflow and don’t affect workflow timestamps.

SubStatusDescription
doneCompleted successfully
canceledClosed without completion

Terminal states end the workflow and use closedAt for timestamp tracking.

Each workflow subStatus has a corresponding timestamp field:

SubStatusTimestamp FieldDescription
readyreadyAtWhen issue became prioritized
discoverydiscoveryStartedAtWhen research/spike began
deliverydeliveryStartedAtWhen active development began
reviewreviewStartedAtWhen entered review/QA
blockedblockedAtWhen became blocked
parkedparkedAtWhen intentionally paused

Note: backlog, done, and canceled don’t have dedicated timestamps. Backlog is the initial state, and terminal states use closedAt.

When moving forward in the workflow (e.g., readydelivery):

  1. Set the timestamp for the new subStatus
  2. Preserve all existing timestamps

Example: Moving from ready to delivery:

  • Sets deliveryStartedAt to current time
  • Preserves readyAt value

When moving backward in the workflow (e.g., reviewdelivery):

  1. Set the timestamp for the new subStatus (re-entering that phase)
  2. Clear all timestamps for phases AFTER the new subStatus

Example: Moving from review back to delivery:

  • Sets deliveryStartedAt to current time
  • Clears reviewStartedAt to null
  • Preserves readyAt (phase before delivery)

Example: Moving from delivery back to backlog:

  • Clears readyAt, discoveryStartedAt, deliveryStartedAt, reviewStartedAt
  • (backlog has no timestamp field to set)

Entering side-track (→ blocked/parked):

  1. Set blockedAt or parkedAt
  2. Preserve all workflow timestamps

Leaving side-track (blocked/parked →):

  1. Clear blockedAt or parkedAt
  2. Set the new subStatus timestamp if not already set

Example: deliveryblocked:

  • Sets blockedAt
  • Preserves deliveryStartedAt (work was in progress)

Entering terminal (→ done/canceled):

  • No subStatus timestamp changes (uses closedAt separately)
  • All workflow timestamps are preserved

Reopening (done/canceled →):

  • Set the new subStatus timestamp only if not already set
  • This allows resuming work from where it was left off

For non-GitHub providers, state is derived from subStatus:

SubStatusDerived State
discovery, delivery, reviewin_progress
done, canceledclosed
backlog, ready, blocked, parkedopen

Issues are excluded from Work-In-Progress (WIP) counts when their subStatus is:

  • backlog, ready (not yet started)
  • blocked, parked (not actively being worked)
  • done, canceled (completed)

Only discovery, delivery, and review count toward WIP.

The state machine is implemented in:

  • apps/server/src/api/utils/sub-status-state-machine.ts - Pure function state machine
  • apps/server/src/api/utils/status-sub-status-inference.ts - Status mapping and transition computation
FunctionPurpose
computeSubStatusTransition()Main function - computes all timestamp changes for a transition
getTimestampFieldsToClear()Returns fields to clear for backward transitions
isBackwardTransition()Detects backward workflow movement
deriveStateFromSubStatus()Derives 3-state from subStatus
computeTransitionTimestamp()Wrapper used by sync services (backward compatible)
import { computeSubStatusTransition, type SubStatusTimestamps } from './sub-status-state-machine.js'
const existingTimestamps: SubStatusTimestamps = {
readyAt: new Date('2024-01-01'),
deliveryStartedAt: new Date('2024-01-02'),
reviewStartedAt: new Date('2024-01-03'),
// ... other fields null
}
// Simulate moving from review back to delivery (rework)
const result = computeSubStatusTransition({
oldSubStatus: 'review',
newSubStatus: 'delivery',
existingTimestamps,
transitionTime: new Date('2024-01-05'),
})
// result.timestamps = {
// deliveryStartedAt: new Date('2024-01-05'), // Updated
// reviewStartedAt: null, // Cleared
// }
// result.isBackward = true
// result.clearedFields = ['reviewStartedAt']
Issue created → backlog
└─→ Prioritized → ready (sets readyAt)
└─→ Development starts → delivery (sets deliveryStartedAt)
└─→ PR opened → review (sets reviewStartedAt)
└─→ Merged → done (sets closedAt)

All timestamps are preserved at completion.

Issue in review → review (reviewStartedAt: Jan 3)
└─→ Needs more work → delivery (Jan 5)
├─→ Sets deliveryStartedAt: Jan 5
└─→ Clears reviewStartedAt
└─→ Fixed → review (Jan 6)
└─→ Sets reviewStartedAt: Jan 6

The rework is reflected in the new reviewStartedAt timestamp.

Issue in delivery → delivery (deliveryStartedAt: Jan 2)
└─→ Waiting on API → blocked
├─→ Sets blockedAt: Jan 3
└─→ Preserves deliveryStartedAt: Jan 2
Unblocked → delivery
└─→ Clears blockedAt
└─→ Preserves deliveryStartedAt: Jan 2

Workflow timestamps are preserved through the blocked state.