Skip to content

The SDLC chain — stage list, workflow bindings, and label vocabulary

State: open/accepted

  • 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 new chain: section of sdlc.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) and sdlc:auto/<stage> (autonomy ceiling — the last stage the issue may advance through unattended). sdlc apply manages both as a 22-label vocabulary through a new labels applier.
  • An issue entering define gets a linked task entity via a new optional issue field on the task frontmatter (schema v7 → v8, stamp-only migration). Once linked, the task’s status is authoritative; sdlc issues sync reports — but in this phase does not silently fix — a label that disagrees with the linked task’s status.
  • The pr-review skill this chain’s review stage 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”) and lib/config/workflows.ts both already flagged.
  • A new issues loop (sdlc orchestrate run --loop issues) walks open, chain-labeled issues one stage at a time up to their ceiling, mirroring the existing work tick’s task-ceiling behavior for GitHub issues.

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.

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.

#StageWorkflowKind
1capture(none)checkpoint — an issue is filed
2triage(none)checkpoint — impact/complexity and the initial sdlc:auto/<stage> ceiling are set
3definedefinetask-ensure-ready / task-auto-define; also links a task (see below)
4ready(none)checkpoint — define succeeded, readiness stamped
5implementimplementtask-work
6checkcheckthe check verb
7judgejudgethe judge verb (verify changes --blocking)
8reviewpr-reviewadversarial review by a configurable engine (new skill, this phase)
9respondpr-respondaddresses review feedback under lease
10merge(none)checkpoint — the PR is merged (by a human, or a future auto-merge policy)
11close-outclose-outtask-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.”

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.

  • lib/config/load.ts’s chain and issues keys stop being strict-empty stubs; both gain real, .meta()-documented shapes per conventions/sdlc-yaml.md.
  • The task schema moves to v8; every existing task file needs no body change (stamp-only migration), but new/linked tasks carry issue:.
  • @sksizer/easy-gh gains gh label and gh issue verbs 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’s ApplierName union and lib/config/meta.ts’s APPLIER_NAMES gain labels.
  • LOOP_NAMES in lib/services/orchestrator/ops/run.ts gains issues, alongside a new lib/services/orchestrator/ticks/issues.ts.
  • The pr-review skill ships, so the prs/merges ticks’ existing orchestrator.review.engine/max_rounds config (already present, previously inert for lack of a skill) starts doing something.
  • Task schema v7 → v8: add issue as an optional field; a stamp-only migration (lib/model/entities/task/migrations/v7-to-v8.ts, mirroring the existing v5-to-v6.ts stamp-only shape) bumps schema_version on every task file with no body rewrite.
  • No migration needed for chain/issues config: both were strict-empty stubs that accepted no keys, so no existing sdlc.yaml can already hold a value there to migrate away from.
  • Auto-merge. The merge checkpoint has no bound workflow in this phase — merging stays a human action (or whatever already causes a MERGED verdict today). Whether merge ever 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. triage is 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, branch docs/sdlc-atlas) documents the existing work/prs/merges workflows 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 at review); the loop advances it opportunistically, never forces it forward.

← Back to Decisions