Skip to content

SDLC control plane routes on asserted facts, not read facts (consolidates 51 self-filed pipeline issues) #3065

Description

@valorengels

Plan: https://github.com/tomcounsell/ai/blob/main/docs/plans/sdlc-control-plane-asserted-facts.md

Context for the stranger: this repo's SDLC pipeline takes a GitHub issue from idea to merged code through staged skills (ISSUE → PLAN → CRITIQUE → BUILD → TEST → REVIEW → DOCS → MERGE), dispatched to child agents and tracked in a Redis-backed ledger. This issue consolidates 51 separate issues the pipeline filed about its own control plane between 2026-06-11 and 2026-08-28. It replaces them.

The signal

The pipeline routes on facts it asserts, not facts it reads.

At every decision point — which lane am I, what stage am I in, did that gate pass, what did that stage do — the pipeline consults a value it minted earlier or a report a child handed it, when the ground truth (git, the branch, the ledger, the filesystem) is one command away. Every one of the 51 issues is an instance of that substitution, and it fails open in every direction: an unverified thing gets recorded as verified, and downstream stages route on it.

That is why the class regenerates. Each issue patches one asserted fact. The next lane asserts a different one.

The four shapes, each with a citable incident

1. Gates that are structurally incapable of firing.
On 2026-08-07, three independent gates in unrelated subsystems were each found reporting success while unable to detect the failure they existed to catch (#2658): a Verification-table sed row whose BRE address matched nothing on macOS yet printed its expected output and passed; an AST recurrence guard whose matcher matched zero call sites; a skill self-check that could not fail. Three same-day instances is a population property, not three bugs. Related: critique_cycle_count never increments, so the plan/critique loop has no cap (#2885); a registry probe counts an entry with a missing callable: key as resolved (#3038); the verification runner compares an expectation to an exit code rather than stdout, false-FAILing every prints N row (#2791).

No gate in this repo is required to demonstrate red before it is trusted green.

2. Stage reports that contradict the branch.
On 2026-08-10 (#2701), a BUILD child halted for lack of the Task tool, spawned a nested build agent carrying the same directive, and that nested agent wrote six commits to session/ask-me-telegram-polls (tip 80dcae8e9) outside its supervisor's loop. The supervisor's final report stated the branch had zero production commits (#3030, #2715). The pipeline then made routing decisions on that report. Nothing reconciles a stage report against the branch it claims to describe — git log on the named branch settles it in one command. Same shape: a forked /do-plan-critique silently degrades to a single reasoner while reporting a full roster, because a context: fork skill invoked from inside a subagent does not get the Agent tool (#3024, #2886, roster ran serially in 6/6 rounds).

3. Lane identity minted once, wrong, with no repair path.
sdlc-tool session-ensure mints sdlc-N at lane start, before /do-plan names the lane. On issue #2708 the recorded slug became sdlc-2708 while the real branch was session/job-expectations-obligation-primitive. resolve_lane_slug treats the recorded value as authoritative and never re-derives; adopt_lane_slug is conditional-on-empty. A recorded-but-wrong slug is permanently authoritative (#2816). This recurred one day after PR #2792 closed the same class via a different cause (#2718/#2735/#2793) — a patched instance, not a fixed root. Same shape: SDLC_ISSUE_NUMBER goes stale within a long-lived dev session and silently points downstream stages at the wrong issue (#2849); the issue lock is not repo-scoped, so two repos sharing an issue number collide (#2813); current_stage can never return PATCH because SDLC_STAGES omits it, so slugged PATCH sessions resolve to main with no worktree (#3017).

4. The pipeline's own definition is not single-source.
agent/pipeline_graph.py presents itself as canonical. A repo-wide sweep found 7 hardcoded code duplicates, ~30 prose restatements, 4 disagreeing stage-model tables, and only 2 clean importers. The duplicates have drifted and two carry live defects (#2491). validate_build.py carries two more private plan-document grammars (#2870).

Corroborating detail found during consolidation: 12 of these 51 issues link a docs/plans/*.md that does not exist on disk. #2870 links docs/plans/foo.md. The pipeline records plan references without checking the referent — the same asserted-fact failure, applied to its own paperwork.

Desired outcome

Not 51 patches. One property: a fact the pipeline routes on must be readable from ground truth at the moment it is used, or it must not be routed on.

Concretely, that means at minimum:

  • A gate must demonstrate red before it counts as green. An undemonstrated gate is an unverified gate.
  • A stage report is a claim until reconciled against the branch/ledger it describes.
  • Lane identity is derived from the branch/worktree that exists, not from a value minted before one did.
  • The pipeline graph has one definition and every consumer imports it.

Ownership of the closed issues

The 51 issues listed below are closed as consolidated into this one. Their bodies remain readable on GitHub; the incident detail above is drawn from them and nothing load-bearing was dropped. Plan documents already written for members of this set stay on disk and remain valid input.

Recon Summary

Performed 2026-09-03 against main @ 00a3d93ca, scoping the residual after the
2026-09-02/03 seven-lane batch (supervisor report, comment 5521519215). Every claim below
was re-read in the source at the cited file:line; four were executed against main to
observe behavior rather than infer it.

Confirmed:

  • Three defects from this consolidation are already fixed on main and are out of residual
    scope: critique cycle cap (bfa4a6f7d), crashed-PLAN NO_RULE (d9cf29dd6), crashed-PATCH
    row 8g (3c689f211). All three verified present in git log with their tests.
  • Wrong recorded lane slug is unrepairable, and the G8 probe converts it into a forced
    /do-patch.
    resolve_lane_slug (tools/lane_identity.py:488-548) returns the recorded
    slug unconditionally at rung 1 (:535-538) and allow_heal defaults to False;
    adopt_lane_slug (:436-480) is conditional-on-empty via _record_slug_if_empty. No path
    re-derives from the real branch; the module docstring concedes this at :37-39. In
    tools/sdlc_next_skill.py, _check_branch_pushed (:181-198) cannot distinguish a
    wrong-but-present slug from a genuinely missing branch, so :355-361 emits
    stage_artifacts_verified: False and guard_g8_artifact_verification
    (agent/sdlc_router.py:550-600, listed at GUARDS:1071 ahead of G7/G5/G6) dispatches
    /do-patch on an OPEN approved PR.
  • G3 never sees the row the table would return. guard_g3_pr_lock
    (agent/sdlc_router.py:469-488) reads only context["proposed_skill"] and
    meta["last_dispatched_skill"]; guards run to completion at :2246-2248 before the table
    at :2250-2255, and there is no post-table re-validation. agent/session_runner/runner.py:1408
    calls decide_next_dispatch(stage_states, meta) with no context at all, so proposed is
    always "" on that path.
  • G3's redirect ladder has no /do-docs branch. :501-509 has exactly three arms
    (/do-merge, /do-patch, else /do-pr-review). REVIEW completed + APPROVED + DOCS pending
    falls to the else arm. Arm 1 also keys on the REVIEW marker, not on a recorded APPROVED
    verdict, making it weaker than rows 9/10 (:1956, :1970).
  • Row 2b preempts DOCS unconditionally on a shipped lane. _rule_critique_verdict_stale
    (:1568-1592) → _critique_verdict_is_stale (:1431) reduces to a timestamp compare
    (:1531-1563) and reads neither pr_number, nor REVIEW/DOCS, nor any review verdict
    (grep-confirmed over :1431-1592). Row 2b sits at :2050-2055; row 9 (/do-docs) is at
    :2181-2186, fifteen rows later. Every guard steps aside in that state.
  • Four distinct verification runner/parser defects, all reproduced by execution on main:
    (a) run_checks(timeout=120) (agent/verification_parser.py:387-392) renders a timeout as
    [FAIL] with no TIMEOUT status (:423-432, :499), while scripts/validate_build.py:238
    hardcodes timeout=30 and emits SKIP (:260-263) — the two runners disagree on the same
    event; (b) evaluate_expectation (:304-384) supports exactly six forms and returns False
    at :384 for everything else, so prints `0`, >= 1, == 0, and empty output all
    silently false-FAIL and are indistinguishable from real failures; (c) command = cells[1].strip("") (:286-288) strips backticks only, so trailing prose after an em-dash is executed as part of the shell command; (d) BRE/ERE escape guidance exists in agent/verification_parser.py:60-68anddocs/features/machine-readable-dod.md:118-139butgrep -rn "grep -E|BRE|ERE|alternation" .claude/skills-global/` returns zero verification
    hits, so it is absent from the surface that generates rows.
  • Plan prose cannot gate merge. The merge decision reads exactly four inputs
    (_rule_ready_to_merge, agent/sdlc_router.py:1962-1978) plus G6's six
    (:910-957); tools/merge_predicate.py has zero occurrences of plan or docs/plans.
    A plan document can declare exactly three machine-read keys today — tracking:
    (tools/lane_identity.py:165-168), revision_applied: and revision_applied_at:
    (tools/sdlc_stage_query.py:438-500) — each via a hand-rolled single-key regex. There is no
    generic frontmatter parser. status:, type:, appetite:, owner:, slug:, covers: are
    read by no code.
  • next-skill persisting nothing is intentional and documented
    (tools/sdlc_next_skill.py:63-69, :88-89, :739-748;
    .claude/skills-global/do-sdlc/SKILL.md:299-303). The residual exposure is that compliance is
    prose-enforced: nothing degrades if the caller skips sdlc-tool dispatch record, and the
    consequence surfaces much later as a G4 oscillation block attributed to the wrong cause.
  • session-ensure destroys live leases, both halves confirmed (comment 5521461151).
    tools/sdlc_session_ensure.py:613-620 discards the handle it just saved (:589-595) and
    re-queries by session_id, which is a plain Field() and not the primary key
    (models/agent_session.py:156-157); Popoto resolves that filter by SMEMBERS
    (popoto/models/query.py:1326,1345), so fresh_rows[0] is unordered and session_id is
    explicitly non-unique (models/agent_session.py:132). Separately, candidate (:538-540)
    carries no provenance flag distinguishing mint from adopt — reuse_run_id cannot serve as one
    because it is overwritten at :526 — so the compare-and-delete in release_issue_lock
    (models/session_lifecycle.py:1484-1520) matches by construction on an adopted id and deletes
    a lease this call never created.

Revised:

Pre-requisites:

  • None external. All work is inside this repo, needs no new secret, service, or dependency, and
    touches no Popoto schema.
  • Test coverage gaps to close alongside the fixes: no test exists for an em-dash/trailing-prose
    command cell, for run_checks timeout handling, or for the Verification-table parser executes non-check tables and reads the wrong column as the command #3022 header shape.
    tests/fixtures/verification/runner_agreement.md asserts per-check parity between the two
    runners but contains no timeout row, which is why the FAIL-vs-SKIP divergence survives it.
    tests/unit/test_verification_parser.py:210 currently asserts the unknown-expectation
    fall-through as intended behavior and must be revised, not merely extended.

Dropped:

  • Re-planning the three already-hotfixed defects (bfa4a6f7d, d9cf29dd6, 3c689f211).
  • A "crashed BUILD" routing row, per the Revised bucket above: row 5 already owns that state.
  • Re-litigating next-skill's non-persistence as a defect. It is a deliberate, documented
    contract; only the silent-non-compliance exposure is residual.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    planTracked plan document in docs/plans/skillsRelated to skills, tools, and SDLC pipelineupvotePre-approved for autonomous SDLC pickup

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions