Comprehensive reference for the SDLC plugin's state management system. All project lifecycle state is tracked in a single YAML file that serves as the source of truth for phase progress, gate results, artifact tracking, and transition history.
- State Management Overview
- state.yaml Format
- State Initialization
- Phase Statuses
- Transition Rules
- Transition Flow (advance_phase.py)
- Gate System Integration
- History Tracking
- session-handoff.json (Build Loop Continuity)
- Spec Backlog (Build Loop Tracking)
- State Diagram
- Cross-References
The SDLC plugin uses a file-based state machine to track a project's progress through 9 phases (ids 0, 1, 2, 3, build, 7, 8, 9, close), ordered by the registry order field and advanced non-sequentially. Key principles:
- Single file: All state lives in
.sdlc/state.yamlinside the target project. This is the only mutable state file for phase progression. - Single source of truth: Phase progress, gate results, artifact lists, and transition history are all co-located. No external database or service is involved.
- Atomic writes: Scripts read the entire file, modify in memory, and write back atomically using
yaml.dump(). There are no partial updates -- either the full state is written or the file is unchanged. - Profile separation: The selected company profile is frozen at initialization time into
.sdlc/profile.yamland is never modified after creation. State and profile are read independently by scripts. - Artifact isolation: Phase artifacts are stored in
.sdlc/artifacts/<slug>/directories, separate from state, where<slug>is the registryslugfield (00-discovery,03-foundation,build,close-- never derived by zero-padding an integer id). The state file tracks artifact paths but does not contain artifact content.
.sdlc/
state.yaml # Mutable -- phase progress and history
profile.yaml # Immutable -- frozen copy of selected profile
constitution.md # Project constitution (from template)
artifacts/
00-discovery/ # Phase 0 artifacts
01-requirements/ # Phase 1 artifacts
02-design/ # Phase 2 artifacts
03-foundation/ # Phase 3 artifacts
build/ # Build loop artifacts (includes session-handoff.json, build-summary.md)
07-documentation/ # Phase 7 artifacts
08-deployment/ # Phase 8 artifacts
09-monitoring/ # Phase 9 artifacts
close/ # Phase C (Close & Transfer) artifacts
Below is the complete structure with every field documented. Fields use ISO 8601 timestamps in UTC.
version: "1.0" # Schema version for forward compatibility
profile_id: "microsoft-enterprise" # ID of the profile selected during /sdlc-setup
project_name: "My Project" # Human-readable project name, set during /sdlc-setup
created_at: "2026-03-20T10:00:00Z" # ISO 8601 timestamp when state was initialized
project_type: "service" # Set during Phase 0 discovery; one of: service | app | library | skill | cli
# Starts as null, populated when project type is determined
current_phase: "build" # Active phase id; int for 0,1,2,3,7,8,9 or string 'build'/'close'
phase_name: "build" # Human-readable name of the current phase (mirrors registry)
phases: # Map of phase_id to phase tracking data (key may be int or string)
0:
name: discovery # Phase name from registry
status: completed # One of: pending | active | completed | skipped
entered_at: "2026-03-20T10:00:00Z" # When this phase became active
completed_at: "2026-03-21T14:30:00Z" # When this phase was completed (null while active/pending)
gate_results: # Results from the last gate check run (map or list)
- gate: "G1-integrity" # Gate identifier
artifact: "constitution.md" # Artifact being checked
passed: true # true = passed, false = failed, null = manual review needed
message: "Artifact exists and is non-empty" # Human-readable explanation
severity: "MUST" # MUST = blocking, SHOULD = advisory, MAY = informational
- gate: "G2-completeness"
artifact: "problem-statement.md"
passed: true
message: "Artifact exists and has content"
severity: "MUST"
artifacts: # List of artifact filenames produced in this phase
- "constitution.md"
- "problem-statement.md"
- "stakeholder-map.md"
- "phase1-handoff.md"
1:
name: requirements
status: completed
entered_at: "2026-03-21T15:00:00Z"
completed_at: "2026-03-22T11:00:00Z"
gate_results: [...] # Same structure as above
artifacts:
- "requirements-spec.md"
- "acceptance-criteria.md"
- "phase2-handoff.md"
# ... phases 2 (design), 3 (foundation) follow the same pattern
build:
name: build
status: active # Currently active phase (the Build loop)
entered_at: "2026-03-25T10:00:00Z"
completed_at: null # null because phase is not yet complete
gate_results: {} # Empty until /sdlc-gate is run
artifacts: [] # Populated as artifacts are created
7:
name: documentation
status: pending # Not yet reached
entered_at: null
completed_at: null
gate_results: {}
artifacts: []
# ... phases 8, 9, close follow with status: pending
history: # Append-only transition log (see Section 8)
# from/to hold phase ids, which can be strings ('build', 'close')
- from: 0
to: 1
at: "2026-03-21T14:30:00Z"
gate_results:
passed: 6
failed: 0
manual: 1
justification: "All gates passed, user confirmed"
- from: 1
to: 2
at: "2026-03-22T11:00:00Z"
gate_results:
passed: 8
failed: 0
manual: 0
# ... one entry per transition| Field | Type | Set By | Mutability | Description |
|---|---|---|---|---|
version |
string | init_project.py | Immutable | Schema version, currently "1.0" |
profile_id |
string | init_project.py | Immutable | Profile selected at setup |
project_name |
string | init_project.py | Immutable | Project name from user input or directory name |
created_at |
string | init_project.py | Immutable | ISO 8601 creation timestamp |
project_type |
string/null | Phase 0 work | Set once | Determined during discovery; null until set |
current_phase |
int|string | advance_phase.py | Updated on transition | Active phase id (may be a string: build, close) |
phase_name |
string | advance_phase.py | Updated on transition | Human-readable name of current phase |
phases[N].name |
string | init_project.py | Immutable | Phase name from registry |
phases[N].status |
string | advance_phase.py | Updated on transition | pending, active, completed, or skipped |
phases[N].entered_at |
string/null | advance_phase.py | Set once per entry | Timestamp when phase became active |
phases[N].completed_at |
string/null | advance_phase.py | Set once per completion | Timestamp when phase was completed |
phases[N].gate_results |
list/map | check_gates.py | Overwritten each gate run | Results from most recent gate evaluation |
phases[N].artifacts |
list | Commands/agents | Appended | Filenames of artifacts produced |
history[] |
list | advance_phase.py | Append-only | Transition log entries |
State initialization occurs when a user runs /sdlc-setup, which calls init_project.py.
- Read profile:
init_project.pyloads the selected profile YAML (e.g.,profiles/microsoft-enterprise/profile.yaml). - Read template: The template at
templates/state-init.yamlis read as raw text. - Variable substitution: Three placeholders are replaced:
${PROFILE_ID}-- fromprofile.company.profile_id${PROJECT_NAME}-- from--nameargument or target directory name${CREATED_AT}-- current UTC timestamp in ISO 8601 format
- Write state file: The substituted content is written to
.sdlc/state.yaml. - Freeze profile: The full profile YAML is copied to
.sdlc/profile.yaml(never modified after this point). - Create directories: All 9 artifact directories, one per phase slug (
00-discovery,01-requirements,02-design,03-foundation,build,07-documentation,08-deployment,09-monitoring,close), are created under.sdlc/artifacts/.
After initialization, the state has these properties:
current_phase: 0phase_name: "discovery"project_type: null (set during Phase 0 when the project type is determined)- Phase 0 status:
activewithentered_atset to creation timestamp - All phases after Discovery:
pendingwithentered_atandcompleted_atboth null - All
gate_results: empty map{} - All
artifacts: empty list[] history: empty list[]
Each phase has one of four statuses:
| Status | Meaning | entered_at | completed_at |
|---|---|---|---|
pending |
Phase has not been reached yet | null | null |
active |
Phase is currently in progress | Set | null |
completed |
Phase finished, all gates passed | Set | Set |
skipped |
Phase was explicitly skipped with justification | Set | Set |
Only one phase can be active at any time. Skipping requires explicit justification recorded in the transition history.
Four invariants govern all phase transitions:
Phases advance in registry order (0, 1, 2, 3, build, 7, 8, 9, close), not by id; build and close are non-numeric ids. The gap where ids 4/5/6 sat is intentional -- batched checking is the rejected anti-pattern; checking happens per-change in the Build loop. The system does not support backward transitions through the normal advance flow. Skipping a phase requires explicit justification recorded in history.
Every transition from a phase to the next by registry order requires ALL exit gates with severity: MUST to pass. Gates with severity: SHOULD or severity: MAY produce warnings but do not block advancement.
State updates are all-or-nothing. The advance_phase.py script performs all state mutations in memory and writes the entire file in a single yaml.dump() call. If gate checking fails or any error occurs before the write, the state file remains unchanged.
If issues are discovered in a later phase that require revisiting an earlier phase, re-entry is permitted. This creates a new history entry with reentry: true, distinguishing it from the original forward progression. The re-entered phase's status returns to active.
Every phase is approval: manual now; there is no automatic. Advancement always requires the --confirmed flag / human sign-off, enforced in advance_phase.py.
All phases require manual approval:
- Phase 0 (Discovery): manual
- Phase 1 (Requirements): manual
- Phase 2 (Design): manual
- Phase 3 (Foundation): manual
- Build Loop (
build): manual - Phase 7 (Documentation): manual
- Phase 8 (Deployment): manual
- Phase 9 (Monitoring): manual
- Phase C (
close, Close & Transfer): manual
The advance_phase.py script implements the complete transition logic. It accepts two arguments: --state (path to state.yaml) and optionally --confirmed (human sign-off flag).
1. LOAD STATE
- Read .sdlc/state.yaml
- Read .sdlc/profile.yaml
- Extract current_phase from state
2. BOUNDARY CHECK
- If the current phase is `terminal` (close): print "already complete", exit 0
3. RUN GATE CHECKS
- Call check_phase_gates(current_phase, state, profile, artifacts_base)
- Print formatted gate results
4. EVALUATE MUST GATES
- Collect all results where passed == false AND severity == "MUST"
- If any MUST failures exist:
Print "BLOCKED" with failure count
Exit with code 1 (gate failure)
5. EVALUATE MANUAL GATES
- Collect all results where passed == null (manual review needed)
- If manual gates exist AND --confirmed is NOT set:
Print "REVIEW REQUIRED" with list of manual gates
Print instructions for stakeholder review
Exit with code 1
6. DRY RUN CHECK
- If --confirmed is NOT set (and no blockers):
Print "Dry run complete -- all automated gates passed"
Print instructions to re-run with --confirmed
Exit with code 0
7. ADVANCE STATE (only reached with --confirmed and all MUST gates passing)
a. Set phases[current_phase].status = "completed"
b. Set phases[current_phase].completed_at = current UTC timestamp
c. Store gate result summary in phases[current_phase].gate_results
d. Set phases[next_phase].status = "active"
e. Set phases[next_phase].entered_at = current UTC timestamp
f. Set current_phase to the next phase by registry `order` (look up via phase_model.py -- NOT id+1)
g. Update phase_name to next phase's name
h. Append transition entry to history[] array
8. WRITE STATE
- Write entire state dict to state.yaml via yaml.dump()
9. OUTPUT GUIDANCE
- Print confirmation of transition
- Display next phase: name, description, skills, required artifacts
- Print artifact directory path and phase definition file reference
| Code | Meaning |
|---|---|
| 0 | Success (advanced) or dry run passed |
| 1 | Gate failure (MUST gates failed or manual approval needed) |
| 2 | System error (missing profile, missing phase definition, file not found) |
Gate checks are performed by check_gates.py and are invoked both by /sdlc-gate (standalone check) and by advance_phase.py (as part of transition).
| Gate ID | Name | What It Checks | Severity |
|---|---|---|---|
| G1-integrity | Artifact Integrity | Required artifacts exist on disk | MUST |
| G2-completeness | Artifact Completeness | Required artifacts are non-empty and have expected content | MUST |
| G3-metrics | Quality Metrics | Code coverage, test pass rates -- quantitative thresholds checked per-change in the Build loop | MUST |
| G4-compliance | Compliance | Framework-specific requirements from profile (e.g., SOC2, HIPAA) | Varies |
| G5-consistency | Cross-Phase Consistency | Detects drift in locked metrics across transitions (budget, timeline, scope, stakeholder roster, quality thresholds, compliance reqs) | SHOULD |
| G6-quality | Quality Standards | Profile-defined quality thresholds | SHOULD |
Each gate check produces a result object:
gate: "G1-integrity" # Gate identifier
artifact: "requirements.md" # What was checked (artifact name or check description)
passed: true # true | false | null (null = manual review required)
message: "Artifact exists" # Human-readable explanation
severity: "MUST" # MUST | SHOULD | MAY- MUST: Blocking. Transition is prevented if any MUST gate fails.
- SHOULD: Advisory. Produces a warning but does not block transition.
- MAY: Informational. Logged for completeness, never blocks.
- Build loop: the gate prints an INFO-level spec-backlog summary from
track_specs.py-- counts by status (merged / in-flight / ready / draft) and risk tier, plus the in-flight list. It reads the spec frontmatter directly and does not block. - Build loop: G3-metrics checks profile quality thresholds such as
coverage_minimumper-change. - Compliance phases: G4-compliance loads framework-specific gates from
profiles/<id>/compliance/<framework>-gates.yaml.
The history array in state.yaml is an append-only audit trail of every phase transition. Entries are never modified or deleted after writing.
- from: 3 # Source phase ID
to: 4 # Destination phase ID
at: "2026-03-25T10:00:00Z" # ISO 8601 transition timestamp
gate_results: # Summary counts (not full results)
passed: 12 # Number of gates that passed
failed: 0 # Number of gates that failed
manual: 1 # Number of manual-review gates
justification: "All gates passed" # Human-readable reason for transitionWhen a completed phase is re-entered, the history entry includes reentry: true:
- from: build
to: 3
at: "2026-03-28T09:00:00Z"
reentry: true
justification: "Foundation rails gap discovered during the Build loop, re-entering Foundation"The audit_gates.py script reads the complete history and gate results to produce an effectiveness report. It analyzes:
- Gate pass/fail rates across all completed phases
- Override history (gates that were overridden with justification)
- Patterns suggesting gates that are always passing (may be too lenient) or frequently failing (may need adjustment)
The audit report is used for process improvement and is typically run after several phases are complete or at project retrospective (Phase 9).
Located at: .sdlc/artifacts/build/session-handoff.json
The Build loop often spans multiple Claude Code sessions. The session handoff file provides cross-session continuity so that each new session can pick up where the last one left off.
{
"$schema": "session-handoff-v1",
"phase": "build",
"last_updated": "2026-03-26T15:00:00Z",
"session_number": 3,
"overall_status": "in_progress",
"current_sprint": 2,
"sections": [
{
"id": "SECTION-001",
"name": "Authentication Service",
"status": "complete",
"agent": "deep-implement",
"started_at": "2026-03-25T10:00:00Z",
"completed_at": "2026-03-25T14:30:00Z",
"evaluator_result": "passed",
"notes": "TDD approach, 94% coverage"
},
{
"id": "SECTION-004",
"name": "API Gateway",
"status": "in_progress",
"agent": "deep-implement",
"started_at": "2026-03-26T09:00:00Z",
"completed_at": null,
"evaluator_result": null,
"notes": "Data layer complete, routing in progress"
}
],
"completed_this_session": ["SECTION-003"],
"in_progress": ["SECTION-004"],
"blocked": [],
"next_actions": [
{
"action": "Complete API Gateway routing layer",
"section": "SECTION-004",
"priority": "P0",
"context": "Data layer done, need route definitions and middleware"
},
{
"action": "Write integration tests for SECTION-003",
"section": "SECTION-003",
"priority": "P1",
"context": "Unit tests complete, integration tests deferred"
}
],
"blockers": [
{
"description": "Waiting for API schema from backend team",
"section": "SECTION-005",
"resolved": false
}
],
"decisions_this_session": [
"Chose WebSocket over SSE for real-time updates"
],
"deviations_this_session": [],
"context_for_next_session": "SECTION-004 routing layer is 60% done. Start with route registration in gateway.ts."
}| Field | Type | Description |
|---|---|---|
$schema |
string | Schema identifier, always "session-handoff-v1" |
phase |
string | Always 'build' |
last_updated |
string/null | ISO 8601 timestamp of last update |
session_number |
int | Incremented each session |
overall_status |
string | "in_progress", "blocked", or "complete" |
current_sprint |
int | Active sprint number from the sprint plan |
sections[] |
array | Per-section tracking with status, agent, timestamps |
completed_this_session |
array | Section IDs finished in the current/last session |
in_progress |
array | Section IDs actively being worked |
blocked |
array | Section IDs that cannot proceed |
next_actions[] |
array | Prioritized action items for the next session |
blockers[] |
array | Active blockers with resolution status |
decisions_this_session |
array | Technical decisions made during the session |
deviations_this_session |
array | Deviations from the original plan |
context_for_next_session |
string | Free-text context for session continuity |
The sdlc-session-start.ps1 hook reads this file at the start of every Claude Code session. When the Build loop is active, the hook:
- Parses
session-handoff.jsonfrom.sdlc/artifacts/build/ - Counts completed, in-progress, and blocked sections
- Outputs a summary line:
[SDLC] Session Handoff: 3/8 sections complete, 1 in progress, 0 blocked (session #3) - Displays
context_for_next_sessionif present - Shows the first
next_actionsentry as the recommended starting point - Warns if there are unresolved blockers
This gives the agent immediate context without needing to read the full handoff file.
In the delivery-standard Build loop, the spec is the unit of work (one spec = one branch = one PR) and the durable source of truth. Build-loop progress is therefore derived directly from the spec files' own frontmatter -- not from a separate hand-maintained tracker that can drift from reality. The retired sections-progress.json model has been removed from the Build loop.
Specs live in <repo>/specs/NNNN-name.md -- in version control, alongside the code, not under .sdlc/. Each spec carries YAML frontmatter:
---
spec: "0007"
name: "Authentication Service"
status: in-flight # draft -> ready -> in-flight -> merged
risk: HIGH # HIGH / MEDIUM / LOW
---| Status | Meaning |
|---|---|
draft |
Spec is being written; not yet ready to build |
ready |
Spec is approved and queued in the backlog |
in-flight |
Spec is on a branch, being implemented or awaiting merge |
merged |
Spec's PR has merged; the change is done |
scripts/track_specs.py derives the backlog state by scanning the spec files:
- Scans
<repo>/specs/*.mdand reads each spec's frontmatterstatusandrisk. - Reports totals, a status breakdown (merged / in-flight / ready / draft), a risk breakdown (HIGH / MEDIUM / LOW), and the in-flight list.
- Can flag a WIP-cap breach with
--wip-cap N(exits non-zero) when more specs are in-flight than the cap allows.
It runs standalone (--repo <path>) or in-workflow (--state .sdlc/state.yaml, where the repo root is the directory containing .sdlc/). In the Build loop, check_gates.py prints this summary as INFO -- it reads progress from reality and does not block.
+---------------------------+
| |
v |
+---------+ +--------+ +-----------+ |
| pending | -> | active | -> | completed | |
+---------+ +--------+ +-----------+ |
| |
| (re-entry from |
| later phase) |
| |
+------ reentry: true ------+
A phase starts as pending. When the previous phase completes and transitions forward, the next phase becomes active. When all exit gates pass and the user confirms, the active phase becomes completed. If issues are discovered later, a completed phase may be re-entered (returning to active).
Phase 0 Phase 1 Phase 2 Phase 3
Discovery --> Requirements --> Design ------> Foundation -->
[active] [pending] [pending] [pending]
| | | |
| gates pass | gates pass | gates pass | gates pass
| + confirm | + confirm | + confirm | + confirm
v v v v
Build Loop Phase 7 Phase 8
(Intent -> Delegate -> Discern) --> Documentation -> Deployment -->
[pending] [pending] [pending]
| | |
| human declares feature-complete | gates pass | gates pass
| (no batch exit gate) + confirm | + confirm | + confirm
v v v
Phase 9 Phase C
Monitoring -----> Close & Transfer
[pending] [pending, terminal]
| |
| gates pass | gates pass + confirm
| + confirm v
v [PROJECT COMPLETE]
The Build Loop has no batch exit gate -- checking happens per-change inside the loop; a human declares the backlog feature-complete to leave. Every transition requires gates to pass plus manual --confirmed sign-off.
/sdlc-next invoked
|
+-> Read state.yaml (current_phase)
|
+-> current phase is terminal (close)?
| YES -> "Already complete" (exit 0)
| NO -> Continue
|
+-> Run check_phase_gates()
|
+-> Any MUST gates failed?
| YES -> "BLOCKED" (exit 1)
| NO -> Continue
|
+-> Any manual gates (passed == null)?
| YES -> --confirmed flag set?
| | NO -> "REVIEW REQUIRED" (exit 1)
| | YES -> Continue
| NO -> Continue
|
+-> --confirmed flag set?
| NO -> "Dry run complete" (exit 0)
| YES -> Continue
|
+-> ADVANCE STATE
| - Complete current phase
| - Activate next phase
| - Append to history
| - Write state.yaml
|
+-> Print next phase guidance (exit 0)
| Topic | File | Description |
|---|---|---|
| Advance logic | scripts/advance_phase.py |
Full transition implementation |
| Gate checking | scripts/check_gates.py |
Gate evaluation for all 6 gate types |
| Phase model | scripts/phase_model.py |
Single source of truth for phase ids, order, slugs (reads phase-registry.yaml) |
| State init | scripts/init_project.py |
Creates .sdlc/ directory and state.yaml |
| Gate audit | scripts/audit_gates.py |
Analyzes gate effectiveness across history |
| Status dashboard | scripts/generate_status.py |
Generates status report from state |
| Phase report | scripts/generate_phase_report.py |
Generates HTML gate report for stakeholders |
| Profile validation | scripts/validate_profile.py |
Validates profile YAML against schema |
| State template | templates/state-init.yaml |
Template with placeholders for initialization |
| Phase registry | phases/phase-registry.yaml |
Phase definitions, gates, artifacts, skills |
| /sdlc-next command | commands/sdlc-next.md |
User-facing command that invokes advance_phase.py |
| /sdlc-gate command | commands/sdlc-gate.md |
User-facing command that runs gate checks |
| Session hook | hooks/sdlc-session-start.ps1 |
Reads state + handoff at session start |
| State machine ref | references/state-machine.md |
Concise reference (progressive disclosure) |