Move os-sdlc orchestrator payload rendering into presenters #805

Closed
opened 2026-09-28 19:15:57 +00:00 by jared · 6 comments
Owner

Problem

Three layers build the payloads that the orchestrator receives. Implementation::OpenPayload is in presenter/. NextStep#dispatch_payload and NextStep#base_terminal_payload are in pipeline/next_step.rb:69-86. CompletionIntake#payload and #dispatch_reply_fields are in command/completion_intake.rb:112-121. OpenPayload and NextStep each build the session_id, implementation_id, node and instructions keys with no shared code. A new payload field, such as the failure block, would go into pipeline/ and make the problem worse.

Reproduce

n/a: refactor ticket.

Approach

NextStep decides which payload applies. A presenter renders it.

# pipeline/next_step.rb
Implementation::DispatchPayload.for(@row, node)
Implementation::TerminalPayload.for(@row, type, root: @root)

Put the shared keys in one place that all three payloads use. CompletionIntake keeps its decision logic and gets its reply hash from a presenter. The JSON that each command prints does not change: same keys, same values, same order where tests pin order.

Test boundary: the new presenters themselves. Implementation::DispatchPayload.for(...).to_h, Implementation::TerminalPayload.for(...).to_h, the completion_recorded reply presenter, Implementation::Timestamp#to_s and Implementation::Duration#to_s do not exist yet, so tests against them are red for the intended reason. The existing tests at CompletionIntake.call and NextStep#call already pin the payload JSON and must stay green unchanged. A test only at CompletionIntake.call passes before any production change, and the run escalates with unexpected_pass (implementation 63).

Required work:

  1. Move dispatch payload rendering from NextStep#dispatch_payload to a presenter in lib/os_sdlc/presenter/implementation/.
  2. Move terminal payload rendering from NextStep#terminal_payload and #base_terminal_payload to a presenter in the same directory.
  3. Move the completion_recorded reply rendering from CompletionIntake#payload and #dispatch_reply_fields to a presenter named Implementation::CompletionReply.
  4. Build the shared session_id, implementation_id, node and instructions keys in one place, used by OpenPayload and the new presenters — check: grep -n "implementation_id:" plugins/os-sdlc/lib/os_sdlc/pipeline/next_step.rb plugins/os-sdlc/lib/os_sdlc/command/completion_intake.rb prints nothing. command/stall_watch.rb and command/start_subagent.rb are out of scope.
  5. Move OsSdlc::Timestamp and OsSdlc::Duration from presenter/timestamp.rb and presenter/duration.rb to presenter/implementation/ as Implementation::Timestamp and Implementation::Duration, with their tests. All callers are inside OsSdlc::Implementation (lens A2 rule 3, from the #804 lens challenge) — check: ls plugins/os-sdlc/lib/os_sdlc/presenter/implementation/timestamp.rb plugins/os-sdlc/lib/os_sdlc/presenter/implementation/duration.rb

Rejected: add the failure field to NextStep#dispatch_payload directly — it puts more rendering into pipeline/.

Acceptance criteria

  1. Given an implementation in the middle of a run, when the next step is a dispatch, then the payload is the same as before the refactor.
  2. Given an implementation at a terminal node, when the next step is terminal, then the payload is the same as before, including reason, state_dump on escalation, and commit when present.
  3. Given a dispatch that records its completion, when os-sdlc-subagent-stop replies, then the reply is the same as before, including next_step and final_output.
  4. Given an implementation and its dispatch node, when Implementation::DispatchPayload renders them, then it returns the dispatch payload hash.
  5. Given an implementation at a terminal node, when Implementation::TerminalPayload renders it, then it returns the terminal payload hash, with state_dump on escalation and commit when present.
  6. Given a local time and a number of seconds, when Implementation::Timestamp and Implementation::Duration render them, then the text is the same as OsSdlc::Timestamp and OsSdlc::Duration gave before the move.

Owner sketch

module OsSdlc
  class Implementation
    class DispatchPayload
      def self.for(implementation, node)
      def to_h
    end

    class TerminalPayload
      def self.for(implementation, type, root:)
      def to_h
    end
  end
end

Non-goals

No new payload fields. The failure block is a separate ticket that depends on this one.

Origin

  • Trigger: design review for the failure block in the implementation lifecycle
  • Improvised this session: none
  • Chain: failure block has no presenter home ← dispatch payload is built in pipeline/ ← DESIGN (pipeline/next_step.rb:82-86)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/pipeline/next_step.rb, plugins/os-sdlc/lib/os_sdlc/command/completion_intake.rb
  • Session: b505a812-db25-4f85-8d52-ae7cda49eaab
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/b505a812-db25-4f85-8d52-ae7cda49eaab.jsonl
## Problem Three layers build the payloads that the orchestrator receives. `Implementation::OpenPayload` is in `presenter/`. `NextStep#dispatch_payload` and `NextStep#base_terminal_payload` are in `pipeline/next_step.rb:69-86`. `CompletionIntake#payload` and `#dispatch_reply_fields` are in `command/completion_intake.rb:112-121`. `OpenPayload` and `NextStep` each build the `session_id`, `implementation_id`, `node` and `instructions` keys with no shared code. A new payload field, such as the failure block, would go into `pipeline/` and make the problem worse. ## Reproduce n/a: refactor ticket. ## Approach `NextStep` decides which payload applies. A presenter renders it. ```ruby # pipeline/next_step.rb Implementation::DispatchPayload.for(@row, node) Implementation::TerminalPayload.for(@row, type, root: @root) ``` Put the shared keys in one place that all three payloads use. `CompletionIntake` keeps its decision logic and gets its reply hash from a presenter. The JSON that each command prints does not change: same keys, same values, same order where tests pin order. Test boundary: the new presenters themselves. `Implementation::DispatchPayload.for(...).to_h`, `Implementation::TerminalPayload.for(...).to_h`, the `completion_recorded` reply presenter, `Implementation::Timestamp#to_s` and `Implementation::Duration#to_s` do not exist yet, so tests against them are red for the intended reason. The existing tests at `CompletionIntake.call` and `NextStep#call` already pin the payload JSON and must stay green unchanged. A test only at `CompletionIntake.call` passes before any production change, and the run escalates with `unexpected_pass` (implementation 63). Required work: 1. Move dispatch payload rendering from `NextStep#dispatch_payload` to a presenter in `lib/os_sdlc/presenter/implementation/`. 2. Move terminal payload rendering from `NextStep#terminal_payload` and `#base_terminal_payload` to a presenter in the same directory. 3. Move the `completion_recorded` reply rendering from `CompletionIntake#payload` and `#dispatch_reply_fields` to a presenter named `Implementation::CompletionReply`. 4. Build the shared `session_id`, `implementation_id`, `node` and `instructions` keys in one place, used by `OpenPayload` and the new presenters — check: `grep -n "implementation_id:" plugins/os-sdlc/lib/os_sdlc/pipeline/next_step.rb plugins/os-sdlc/lib/os_sdlc/command/completion_intake.rb` prints nothing. `command/stall_watch.rb` and `command/start_subagent.rb` are out of scope. 5. Move `OsSdlc::Timestamp` and `OsSdlc::Duration` from `presenter/timestamp.rb` and `presenter/duration.rb` to `presenter/implementation/` as `Implementation::Timestamp` and `Implementation::Duration`, with their tests. All callers are inside `OsSdlc::Implementation` (lens A2 rule 3, from the #804 lens challenge) — check: `ls plugins/os-sdlc/lib/os_sdlc/presenter/implementation/timestamp.rb plugins/os-sdlc/lib/os_sdlc/presenter/implementation/duration.rb` Rejected: add the failure field to `NextStep#dispatch_payload` directly — it puts more rendering into `pipeline/`. ## Acceptance criteria 1. Given an implementation in the middle of a run, when the next step is a dispatch, then the payload is the same as before the refactor. 2. Given an implementation at a terminal node, when the next step is terminal, then the payload is the same as before, including `reason`, `state_dump` on escalation, and `commit` when present. 3. Given a dispatch that records its completion, when `os-sdlc-subagent-stop` replies, then the reply is the same as before, including `next_step` and `final_output`. 4. Given an implementation and its dispatch node, when `Implementation::DispatchPayload` renders them, then it returns the `dispatch` payload hash. 5. Given an implementation at a terminal node, when `Implementation::TerminalPayload` renders it, then it returns the terminal payload hash, with `state_dump` on escalation and `commit` when present. 6. Given a local time and a number of seconds, when `Implementation::Timestamp` and `Implementation::Duration` render them, then the text is the same as `OsSdlc::Timestamp` and `OsSdlc::Duration` gave before the move. ## Owner sketch ```ruby module OsSdlc class Implementation class DispatchPayload def self.for(implementation, node) def to_h end class TerminalPayload def self.for(implementation, type, root:) def to_h end end end ``` ## Non-goals No new payload fields. The failure block is a separate ticket that depends on this one. ## Origin - Trigger: design review for the failure block in the implementation lifecycle - Improvised this session: none - Chain: failure block has no presenter home ← dispatch payload is built in `pipeline/` ← DESIGN (`pipeline/next_step.rb:82-86`) - Root candidate: this ticket - Where: `plugins/os-sdlc/lib/os_sdlc/pipeline/next_step.rb`, `plugins/os-sdlc/lib/os_sdlc/command/completion_intake.rb` - Session: b505a812-db25-4f85-8d52-ae7cda49eaab - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/b505a812-db25-4f85-8d52-ae7cda49eaab.jsonl
Author
Owner

Scope addition from #804 lens challenge: A2 rule (3) applies. Move OsSdlc::Timestamp and OsSdlc::Duration (presenter/timestamp.rb, presenter/duration.rb) under presenter/implementation/ as Implementation::Timestamp and Implementation::Duration; callers already sit inside OsSdlc::Implementation, so no caller edits. Promote to top level only when a caller outside Implementation appears (A2 rule 2).

Scope addition from #804 lens challenge: A2 rule (3) applies. Move OsSdlc::Timestamp and OsSdlc::Duration (presenter/timestamp.rb, presenter/duration.rb) under presenter/implementation/ as Implementation::Timestamp and Implementation::Duration; callers already sit inside OsSdlc::Implementation, so no caller edits. Promote to top level only when a caller outside Implementation appears (A2 rule 2).
Author
Owner

Work started on branch ticket-805 via /os-sdlc:implement (maps/coding.yaml, behavior-preserving refactor).

Work started on branch ticket-805 via /os-sdlc:implement (maps/coding.yaml, behavior-preserving refactor).
Author
Owner

Correction: running maps/poodr-ticket-implementation.yaml, not coding.yaml. The new presenter classes and the Timestamp/Duration move need test-writer edits.

Correction: running maps/poodr-ticket-implementation.yaml, not coding.yaml. The new presenter classes and the Timestamp/Duration move need test-writer edits.
Author
Owner

Implementation 63 escalated: unexpected_pass:test-writer/diff-test at dispatch 652. Cause: under-decomposed ticket. The seam was CompletionIntake.call, where a behavior-preserving refactor has no red test. Recovery per escalation procedure: ticket edited to name the new presenters as the test boundary (AC 4-6), R4 check narrowed, reply presenter named Implementation::CompletionReply. Re-opening as a new implementation.

Implementation 63 escalated: unexpected_pass:test-writer/diff-test at dispatch 652. Cause: under-decomposed ticket. The seam was CompletionIntake.call, where a behavior-preserving refactor has no red test. Recovery per escalation procedure: ticket edited to name the new presenters as the test boundary (AC 4-6), R4 check narrowed, reply presenter named Implementation::CompletionReply. Re-opening as a new implementation.
Author
Owner

Implementation 64 failed: verdict:error from test-reviewer dispatch 669. Failed step: test-reviewer after the behavior-verifier (665) sent the run back to test-writer. Gate details: 8 of 10 new tests were green because programmer 662 had already built the presenters; the reviewer treated green tests as a defect. Its own note says the correct verdict was pass. Worktree ticket-805 is kept with the partial work (presenters built, 2 red commit-case tests). Workflow defect captured separately.

Implementation 64 failed: verdict:error from test-reviewer dispatch 669. Failed step: test-reviewer after the behavior-verifier (665) sent the run back to test-writer. Gate details: 8 of 10 new tests were green because programmer 662 had already built the presenters; the reviewer treated green tests as a defect. Its own note says the correct verdict was pass. Worktree ticket-805 is kept with the partial work (presenters built, 2 red commit-case tests). Workflow defect captured separately.
Author
Owner

Resolution

Done: Payload rendering moved into presenters Implementation::DispatchPayload, TerminalPayload, CompletionReply, Identity, CompletionOutcome; Timestamp and Duration moved to presenter/implementation/; JSON unchanged. Merged to main 17d051b7 (hand-finished after #811).

Evidence: Merge 17d051b7 (ff). ruby tests/all.rb in plugins/os-sdlc: 1311 runs, 2870 assertions, 0 failures, 0 errors, 9 skips. rubocop 22 files no offenses. grep -n 'implementation_id:' next_step.rb completion_intake.rb prints nothing.

Follow-ups: #811 (test-reviewer round scope) must ship before the next pipeline run; #806 unblocked

## Resolution **Done:** Payload rendering moved into presenters Implementation::DispatchPayload, TerminalPayload, CompletionReply, Identity, CompletionOutcome; Timestamp and Duration moved to presenter/implementation/; JSON unchanged. Merged to main 17d051b7 (hand-finished after #811). **Evidence:** Merge 17d051b7 (ff). ruby tests/all.rb in plugins/os-sdlc: 1311 runs, 2870 assertions, 0 failures, 0 errors, 9 skips. rubocop 22 files no offenses. grep -n 'implementation_id:' next_step.rb completion_intake.rb prints nothing. **Follow-ups:** #811 (test-reviewer round scope) must ship before the next pipeline run; #806 unblocked
jared closed this issue 2026-09-29 13:02:31 +00:00
jared referenced this issue from a commit 2026-09-29 13:12:09 +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#805
No description provided.