The SDLC chain — stage list, workflow bindings, and label vocabulary
State: open/accepted
Summary
Section titled “Summary”- The chain is a fixed, ordered list of 11 stages —
capture,triage,define,ready,implement,check,judge,review,respond,merge,close-out— declared once in code (lib/config/chain.ts) and overridable per stage through the newchain:section ofsdlc.yaml. - 7 stages bind to a built-in workflow (
define,implement,check,judge,review→pr-review,respond→pr-respond,close-out); 4 are checkpoints with no bound workflow (capture,triage,ready,merge) — points where a human or an external event (filing an issue, merging a PR) moves the item, not an automated step. - Two label families make chain position and autonomy visible on GitHub
issues:
sdlc:stage/<stage>(current position) andsdlc:auto/<stage>(autonomy ceiling — the last stage the issue may advance through unattended).sdlc applymanages both as a 22-label vocabulary through a newlabelsapplier. - An issue entering
definegets a linked task entity via a new optionalissuefield on the task frontmatter (schemav7→v8, stamp-only migration). Once linked, the task’sstatusis authoritative;sdlc issues syncreports — but in this phase does not silently fix — a label that disagrees with the linked task’s status. - The
pr-reviewskill this chain’sreviewstage needs (adversarial review of a PR diff by a configurable engine, posting review comments) ships as part of this work, closing the gap the atlas (“Built-in flows”) andlib/config/workflows.tsboth already flagged. - A new
issuesloop (sdlc orchestrate run --loop issues) walks open, chain-labeled issues one stage at a time up to their ceiling, mirroring the existingworktick’s task-ceiling behavior for GitHub issues.
Context
Section titled “Context”Phase 2 gave sdlc orchestrate run three loops — work (roadmap
tasks), prs (review/respond/update), merges (close-out) — that
already walk a de facto pipeline for tasks: define → implement →
check → judge (the work tick’s dispatch sequence), then
review/respond/update (the prs tick, reacting to pr classify
verdicts), then close-out (the merges tick, on a MERGED verdict).
Phase 5 reserved two empty sdlc.yaml sections for this phase to fill:
chain (“Reserved for the sdlc chain … each stage bound to a
workflow”) and issues (“Reserved for issue-tracker sync and the
issues loop”), both strict-empty objects that reject any key until
Phase 7 defines one.
Two gaps stop this pipeline from covering GitHub issues the same
way it covers roadmap tasks: issues have no task-style status
frontmatter to drive dispatch — only labels are visible on the GitHub
side — and the pr-review workflow already names a skill
(skill: 'pr-review') that has never shipped, so today’s prs tick
has nothing to run for a PR’s first review pass; phase gates do it by
hand with an ad hoc adversarial sub-agent instead.
Decision
Section titled “Decision”The chain is a named, canonical pipeline — the same 11 stages for
every project, declared as an ordered const
(CHAIN_STAGE_NAMES in lib/config/chain.ts), with a built-in
stage → workflow binding a project may override per stage (never
reorder or rename) through chain.stages.<name>.workflow in
sdlc.yaml, following the same steps/prepend/append-free,
value-override shape the workflows: section already uses for its own
built-ins.
| # | Stage | Workflow | Kind |
|---|---|---|---|
| 1 | capture | (none) | checkpoint — an issue is filed |
| 2 | triage | (none) | checkpoint — impact/complexity and the initial sdlc:auto/<stage> ceiling are set |
| 3 | define | define | task-ensure-ready / task-auto-define; also links a task (see below) |
| 4 | ready | (none) | checkpoint — define succeeded, readiness stamped |
| 5 | implement | implement | task-work |
| 6 | check | check | the check verb |
| 7 | judge | judge | the judge verb (verify changes --blocking) |
| 8 | review | pr-review | adversarial review by a configurable engine (new skill, this phase) |
| 9 | respond | pr-respond | addresses review feedback under lease |
| 10 | merge | (none) | checkpoint — the PR is merged (by a human, or a future auto-merge policy) |
| 11 | close-out | close-out | task-close-out |
A checkpoint stage’s workflow: null is not “undecided” — it is the
explicit, validated declaration that nothing runs there today. sdlc run/the chain resolver still names it (satisfies “each stage names a
workflow”: the config key is always present, its value is
null for a checkpoint), and a later phase (or a project override) can
bind a real workflow to any of the four without a schema change. The
pr-update workflow stays outside the 11-stage chain: it is a reactive
correction (prs-tick response to a BEHIND/CONFLICTS verdict), not
a position on the linear path.
Labels. sdlc:stage/<name> and sdlc:auto/<name> exist for all 11
stage names (22 labels), derived from CHAIN_STAGE_NAMES — one source
of truth, so the label vocabulary can never drift from the chain’s
stage list. sdlc apply gets a fourth applier, labels, following the
existing Applier shape (plan/apply, --only labels, --check,
--dry-run): it diffs the live repository’s labels (via a new gh label seam in @sksizer/easy-gh) against the 22-label vocabulary and
creates or updates what’s missing or drifted. It never deletes a label
it doesn’t own. Ceiling default: an issue with a sdlc:stage/<x>
label but no sdlc:auto/<x> label has an implicit ceiling equal to its
own current stage — i.e. it does not auto-advance until triage (or a
human) sets one. This is the conservative default: silence means “wait
for a human,” never “proceed.”
Issue ↔ task linking. The task schema gains an optional issue
field (schema v7 → v8; a stamp-only migration, no body transform,
since the field is optional and additive) holding the linked GitHub
issue reference. An issue moved to define with no existing linked
task gets one created (seeded from the issue’s title/body); an issue
already linked reuses its task. Markdown stays the source of
truth: once linked, the task file’s status drives the mirror,
never the reverse — sdlc issues sync walks every linked pair, maps
the task’s status to the stage(s) it is consistent with (in-progress
covers the whole implement…merge span, since the task contract does
not distinguish those sub-stages the way GitHub labels can), and
reports a conflict when the issue’s actual sdlc:stage/<x> label falls
outside that set. Phase 7 ships reporting only — sync never
rewrites a label or a task file on its own; an operator (or a future
--fix) acts on the report. This keeps the first cut of sync safe
to run against a real repo (though this phase still runs it only
against fixtures — see External-write safety) and keeps the
conflict semantics legible: a mismatch is surfaced, never silently
“resolved” by guessing which side is right.
The issues loop. sdlc orchestrate run --loop issues joins
work/prs/merges. Each tick lists open, chain-labeled issues
(sdlc:stage/<x> present), and for each one whose current stage is
below its ceiling, dispatches the next stage’s bound workflow (via
the existing step-runner — no new dispatch mechanism) and, on success,
relabels the issue to that stage; a checkpoint stage (no bound
workflow) advances the label with no dispatch. One stage per issue per
tick, mirroring how a label-driven board moves one column at a time
rather than skipping ahead sight unseen. An issue already at its
ceiling fires a stage.ceiling_reached notification and is skipped —
“blocked is not stopped”: the loop moves on to the next issue.
The pr-review skill. Ships at
solutions/ontological/plugin/plugins/sdlc/skills/pr-review/pr-review.md,
following the pr-check/pr-respond vault-note convention: reads the
PR diff, runs an adversarial review under the engine configured by
orchestrator.review.engine (already the config key the prs/merges
ticks use to dispatch this workflow — see
lib/config/load.ts), applying the S-0017 rubric as its lens, and
posts review comments on the PR. This closes the one gap the atlas
already documents and lets the review stage — and this phase’s own
adversarial-review gate — run through the chain instead of by hand.
A fixed, code-declared stage list (rather than a free-form
project-defined list) keeps sdlc:stage/* a stable, cross-project label
vocabulary: a dashboard, a notification rule, or a future MCP tool can
assume the 11 names exist everywhere, the same way BUILTIN_WORKFLOW_NAMES
is a fixed 8, not a per-project list. Making workflow overridable
per stage (not the stage list itself) mirrors the existing
workflows: override shape exactly, so a project that wants review
to run a different skill, or wants merge to gain a real auto-merge
workflow, does it the same way it already customizes judge or
pr-update — no second override grammar to learn.
Deriving the label vocabulary from CHAIN_STAGE_NAMES rather than
hand-listing 22 label strings in the applier removes an entire class of
drift bug (a stage renamed in code but not in the label list, or vice
versa) for one array.
Report-only sync (no --fix in this phase) is the narrower, safer
cut: the task-status → stage mapping is lossy in one direction
(in-progress cannot say which of five sub-stages a task is in), so an
automatic fix risks moving a label backward. Reporting the mismatch and
leaving resolution to a human (or a later, narrower auto-fix once the
mapping’s edge cases are better understood) matches this phase’s
“Markdown stays the source of truth” framing without overreaching it
into “and therefore always safely computable.”
Options considered
Section titled “Options considered”Option A: a per-project configurable stage list
Section titled “Option A: a per-project configurable stage list”Let chain.stages be an arbitrary ordered array a project defines from
scratch, the way some CI systems let you define pipeline stages.
Rejected: nothing else in sdlc treats its lifecycle vocabulary as
project-defined (task status, workflow names, hook names are all
fixed enums with override-the-value, not override-the-shape,
customization points), and a free stage list would make the label
vocabulary, the notification catalog, and any future dashboard
per-project-shaped for no real gain — no task in this milestone asks
for a different pipeline shape, only for this one to reach issues.
Option B: bidirectional, auto-resolving issues sync
Section titled “Option B: bidirectional, auto-resolving issues sync”Let sync pick a direction per conflict (e.g. “label is newer, relabel
the task”) and fix automatically. Rejected for this phase: no reliable
“which side changed more recently” signal exists yet (labels and task
files are edited through unrelated tools with no shared clock), and
guessing wrong silently regresses an issue’s visible position. Revisit
once sync has run against real data long enough to know which
conflict shapes actually occur.
Option C: no ceiling default (issues advance until stopped)
Section titled “Option C: no ceiling default (issues advance until stopped)”Let an issue with no sdlc:auto/<x> label advance freely, on the
theory that “no ceiling” reads as “no limit.” Rejected as unsafe by
default: filing an issue is a low-friction action a human does without
necessarily meaning “and let it run to close-out unattended.” The
conservative default (no label ⇒ no auto-advance) matches the phase’s
“if the auto-mode classifier denies something, don’t route around it”
posture applied one level up, at the product’s own default.
Consequences
Section titled “Consequences”lib/config/load.ts’schainandissueskeys stop being strict-empty stubs; both gain real,.meta()-documented shapes perconventions/sdlc-yaml.md.- The task schema moves to
v8; every existing task file needs no body change (stamp-only migration), but new/linked tasks carryissue:. @sksizer/easy-ghgainsgh labelandgh issueverbs it did not have before (packages/ts/easy-gh/{verbs.ts,sync.ts,types.ts}), reusable by anything else that later needs issue/label access.sdlc apply’sApplierNameunion andlib/config/meta.ts’sAPPLIER_NAMESgainlabels.LOOP_NAMESinlib/services/orchestrator/ops/run.tsgainsissues, alongside a newlib/services/orchestrator/ticks/issues.ts.- The
pr-reviewskill ships, so theprs/mergesticks’ existingorchestrator.review.engine/max_roundsconfig (already present, previously inert for lack of a skill) starts doing something.
Migration
Section titled “Migration”- Task schema
v7→v8: addissueas an optional field; a stamp-only migration (lib/model/entities/task/migrations/v7-to-v8.ts, mirroring the existingv5-to-v6.tsstamp-only shape) bumpsschema_versionon every task file with no body rewrite. - No migration needed for
chain/issuesconfig: both were strict-empty stubs that accepted no keys, so no existingsdlc.yamlcan already hold a value there to migrate away from.
Open questions
Section titled “Open questions”- Auto-merge. The
mergecheckpoint has no bound workflow in this phase — merging stays a human action (or whatever already causes aMERGEDverdict today). Whethermergeever gets a real workflow (an auto-merge policy gated by its own autonomy ceiling) is future work, not part of this decision. sync --fix. Deferred per Option B above; revisit once real conflict data exists.- Triage automation.
triageis a checkpoint today (a human sets impact/complexity and the initial ceiling label). An automated triage workflow (e.g. an agent proposing labels) is plausible future work, not scoped here.
- Stage and workflow-binding source of truth:
lib/config/chain.ts. The atlas (docs/planning/sdlc-1.0-atlas.html, branchdocs/sdlc-atlas) documents the existingwork/prs/mergesworkflows this chain formalizes; it is kept current as phases land, including this one. sdlc:auto/<stage>marks a ceiling, not a target — an issue can sit below its ceiling indefinitely (e.g. waiting for CI, waiting on a human reviewer atreview); the loop advances it opportunistically, never forces it forward.