os-sdlc phase 3: build the map-as-data pipeline per ADR-0112/0113 #341

Closed
opened 2026-08-11 18:20:36 +00:00 by jared · 3 comments
Owner

Implement the design recorded in ADR-0112 (YAML maps, load-time termination proofs in TransitionTable#validate!, stepper-run gates, map selection argument > project.yaml > default) and ADR-0113 (push brief assembly, verdict.md first-line parsing, park-and-notify escalation, programmer/reviewer skill preloading + programmer tool contract: +Bash, -Edit). Draft map: plugins/os-sdlc/maps/default.yaml. Supersedes SkeletonTable. TDD; sequencing/gate logic only in lib/os_sdlc/runner/.

Implement the design recorded in ADR-0112 (YAML maps, load-time termination proofs in TransitionTable#validate!, stepper-run gates, map selection argument > project.yaml > default) and ADR-0113 (push brief assembly, verdict.md first-line parsing, park-and-notify escalation, programmer/reviewer skill preloading + programmer tool contract: +Bash, -Edit). Draft map: plugins/os-sdlc/maps/default.yaml. Supersedes SkeletonTable. TDD; sequencing/gate logic only in lib/os_sdlc/runner/.
Author
Owner

This was the acceptance criteria from the code-probe. I am curious if this would have been better handled (closer to POODR) by breaking it down further, or considering each acceptance criterion as its own wave:

Ticket #341 Wave 2 — runner semantics (ADR-0113 + stepper-run gates from ADR-0112). Write FAILING minitest tests only; never write implementation. Project root: /home/jared/dev/cc-os/plugins/os-sdlc. Code path lib/os_sdlc/runner/, tests tests/runner/ (auto-globbed by tests/all.rb). Read the existing runner code first — engine.rb, engine_factory.rb, db.rb, dispatch.rb, ticket.rb, skeleton_table.rb, command_result.rb, and existing tests/runner/engine_test.rb — and follow their construction/test idioms (in-memory/temp Sequel db, etc.).

Wave 1 (already green, committed): TransitionTable.from_yaml(path), extended Row (on_failure {route:, retries:, exhausted:}, kind:, skippable:), termination proofs, MapSelector.new(plugin_maps_dir:, project:).resolve(explicit:), Project#map. The shipped map is maps/default.yaml (states intake→code-probe→test-writer→programmer→lint→review→merge-ready, escalation kind: human, terminal [merge-ready, failed]).

Acceptance criteria to cover:

  1. Map-driven EngineFactory — EngineFactory builds the engine's table via MapSelector + TransitionTable.from_yaml instead of SkeletonTable::TABLE; accepts an optional explicit map name; the resolved map name is stamped on the ticket row at intake and is immutable in flight (a later run of the same ticket uses the stamped map even if project.yaml changes).
  2. Gate template substitution — gate strings may contain {{test_command}} (from Project), {{brief_path}} (current dispatch's brief path), {{red_assert_command}}; the stepper substitutes before shelling out. Unknown {{placeholder}} → error, not silent literal.
  3. Retry counters — a gate failure on a row with on_failure {route:, retries: N} re-routes to route and increments a per-ticket-per-state retry counter; failure number N+1 (cap exhausted) routes to the exhausted target instead. retries: 0 = one failure goes straight to exhausted.
  4. Push brief assembly — after routing an edge, the Runner writes the next brief to .sdlc/tickets/<ticket>/briefs/<seq>-<from>-to-<to>.md (seq = zero-padded ordinal), records the path on the dispatches row, and includes it in the dispatch action it emits. Briefs are retained (never deleted) after ticket completion.
  5. Verdict gate — review's gate reads line 1 of .sdlc/tickets/<ticket>/verdict.md in Ruby (a verdict-reading object, not shell): exactly "APPROVE" → success; exactly "REQUEST-CHANGES" → gate failure counting against review's retry cap, and the verdict body is snapshotted into the numbered review→programmer brief before the next round can overwrite verdict.md; missing file or malformed first line → plain failure counting against the cap.
  6. Park-and-notify escalation — when a ticket routes into a kind: human state: persist state "escalation" plus the exhausted-from state and gate-log path in SQLite; notify via injected ports (a tracker port receiving add-waiting-label + blocker comment, a notifier port receiving a push notification) — test with fakes/spies, never real tea/network; the engine then stops emitting dispatches (parked). Resuming a parked ticket surfaces the parked info and supports: retry (counter reset), retry-with-guidance (a note string prepended to the retry brief, counter reset), skip (only legal when the exhausted row has skippable: true → advances to its on_success), abort (→ terminal "failed").

Design freedom: name new collaborators as the tests need (e.g. GateTemplate, RetryLedger, BriefAssembler, Verdict, Escalation, TrackerPort/NotifierPort fakes); constructor-injected ports. Don't edit existing test files; new files per concern. Sandi Metz style. If an AC can't be expressed without seeing a piece of existing code behavior, read that file rather than guessing.


This was the acceptance criteria from the code-probe. I am curious if this would have been better handled (closer to POODR) by breaking it down further, or considering each acceptance criterion as its own wave: Ticket #341 Wave 2 — runner semantics (ADR-0113 + stepper-run gates from ADR-0112). Write FAILING minitest tests only; never write implementation. Project root: /home/jared/dev/cc-os/plugins/os-sdlc. Code path lib/os_sdlc/runner/, tests tests/runner/ (auto-globbed by tests/all.rb). Read the existing runner code first — engine.rb, engine_factory.rb, db.rb, dispatch.rb, ticket.rb, skeleton_table.rb, command_result.rb, and existing tests/runner/engine_test.rb — and follow their construction/test idioms (in-memory/temp Sequel db, etc.). Wave 1 (already green, committed): `TransitionTable.from_yaml(path)`, extended Row (on_failure {route:, retries:, exhausted:}, kind:, skippable:), termination proofs, `MapSelector.new(plugin_maps_dir:, project:).resolve(explicit:)`, `Project#map`. The shipped map is maps/default.yaml (states intake→code-probe→test-writer→programmer→lint→review→merge-ready, escalation kind: human, terminal [merge-ready, failed]). Acceptance criteria to cover: 1. **Map-driven EngineFactory** — EngineFactory builds the engine's table via MapSelector + TransitionTable.from_yaml instead of SkeletonTable::TABLE; accepts an optional explicit map name; the resolved map name is stamped on the ticket row at intake and is immutable in flight (a later run of the same ticket uses the stamped map even if project.yaml changes). 2. **Gate template substitution** — gate strings may contain {{test_command}} (from Project), {{brief_path}} (current dispatch's brief path), {{red_assert_command}}; the stepper substitutes before shelling out. Unknown {{placeholder}} → error, not silent literal. 3. **Retry counters** — a gate failure on a row with on_failure {route:, retries: N} re-routes to `route` and increments a per-ticket-per-state retry counter; failure number N+1 (cap exhausted) routes to the `exhausted` target instead. retries: 0 = one failure goes straight to exhausted. 4. **Push brief assembly** — after routing an edge, the Runner writes the next brief to `.sdlc/tickets/<ticket>/briefs/<seq>-<from>-to-<to>.md` (seq = zero-padded ordinal), records the path on the dispatches row, and includes it in the dispatch action it emits. Briefs are retained (never deleted) after ticket completion. 5. **Verdict gate** — review's gate reads line 1 of `.sdlc/tickets/<ticket>/verdict.md` in Ruby (a verdict-reading object, not shell): exactly "APPROVE" → success; exactly "REQUEST-CHANGES" → gate failure counting against review's retry cap, and the verdict body is snapshotted into the numbered review→programmer brief before the next round can overwrite verdict.md; missing file or malformed first line → plain failure counting against the cap. 6. **Park-and-notify escalation** — when a ticket routes into a `kind: human` state: persist state "escalation" plus the exhausted-from state and gate-log path in SQLite; notify via injected ports (a tracker port receiving add-waiting-label + blocker comment, a notifier port receiving a push notification) — test with fakes/spies, never real tea/network; the engine then stops emitting dispatches (parked). Resuming a parked ticket surfaces the parked info and supports: retry (counter reset), retry-with-guidance (a note string prepended to the retry brief, counter reset), skip (only legal when the exhausted row has skippable: true → advances to its on_success), abort (→ terminal "failed"). Design freedom: name new collaborators as the tests need (e.g. GateTemplate, RetryLedger, BriefAssembler, Verdict, Escalation, TrackerPort/NotifierPort fakes); constructor-injected ports. Don't edit existing test files; new files per concern. Sandi Metz style. If an AC can't be expressed without seeing a piece of existing code behavior, read that file rather than guessing. ---
Author
Owner

Implemented on main: commits 651695f, 18803bd, 8906a27, 5ff06f3, f692b7a. 797 tests green, rubocop clean. Four review rounds by os-sdlc:reviewer (opus); final verdict APPROVE. Tracker/notifier adapters + SkeletonTable legacy-branch retirement deferred to #342. Awaiting human sign-off.

Implemented on main: commits 651695f, 18803bd, 8906a27, 5ff06f3, f692b7a. 797 tests green, rubocop clean. Four review rounds by os-sdlc:reviewer (opus); final verdict APPROVE. Tracker/notifier adapters + SkeletonTable legacy-branch retirement deferred to #342. Awaiting human sign-off.
Author
Owner

Resolution

Done: Implemented map-as-data pipeline per ADR-0112/0113: YAML map loader, load-time termination proofs in TransitionTable#validate!, stepper-run gates, map selection (args > project.yaml > default), push brief assembly, verdict gate, park-and-notify escalation, programmer/reviewer skill preloading. Supersedes SkeletonTable.

Evidence: 651695f, 18803bd, 8906a27, 5ff06f3, f692b7a

Follow-ups: none

Approved-by: jared

## Resolution **Done:** Implemented map-as-data pipeline per ADR-0112/0113: YAML map loader, load-time termination proofs in TransitionTable#validate!, stepper-run gates, map selection (args > project.yaml > default), push brief assembly, verdict gate, park-and-notify escalation, programmer/reviewer skill preloading. Supersedes SkeletonTable. **Evidence:** 651695f, 18803bd, 8906a27, 5ff06f3, f692b7a **Follow-ups:** none Approved-by: jared
jared closed this issue 2026-08-12 14:24:28 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#341
No description provided.