Make escalation a waiting position at needs-orchestrator #872

Closed
opened 2026-10-01 14:26:17 +00:00 by jared · 3 comments
Owner

Problem

ADR-0259 decisions 1, 3, 4 and 5 make escalation a waiting position, not a closing stamp. Today LifecycleStamps::ALL (model/lifecycle_stamps.rb:5) lists escalated_at: :escalated, so an escalated run is closed and no reroute can continue it. Landing#land! (command/implementation/landing.rb:15) terminates the run when the route reaches a terminal with outcome escalation. Map::Validator::TERMINAL_OUTCOMES (model/map/validator.rb:11) accepts escalation, and NodeCheck#check_escalate_route! requires every agent node to reach a terminal with that outcome. NextStep (pipeline/next_step.rb) emits the escalation as a terminal payload through TerminalPayload.

Reproduce

n/a: design change, nothing to reproduce.

Approach

  1. Stamps: remove escalated_at from LifecycleStamps::ALL and from the implementations table (adapter/db.rb:315); bump SCHEMA_VERSION (ADR-0129/0155; do not bump while a run is live). Remove Lifecycle#escalate! and #escalated?. The closing stamps are completed_at, abandoned_at, failed_at; failed_reason means failure only. Update TERMINAL_STAMPS in tests/adapter/db_migration_test.rb.
  2. Waiting node: Map::NodeSpec reads waiting: true; Map::Node#waiting?. A waiting node has no agent and no verdict. Its edges may be empty (the reroute exit comes in #885).
  3. Validator: TERMINAL_OUTCOMES becomes implementation_complete implementation_failed. NodeGuardChecks exempts a waiting node from the single-default-edge rule. check_dead_pocket! skips waiting nodes. check_escalate_route! requires the escalate edge of every agent node to reach a waiting node (through routing nodes); code :missing_escalate_route.
  4. TransitWalk#dispatchable? stops at a waiting node.
  5. Landing#land!: when the decision node is a waiting node, it adds the Escalation row (#884) and calls advance!(position:). It does not terminate the run.
  6. NextStep: when the run is active and its position is a waiting node, it returns Implementation::EscalationPayload: type: "escalation", node, reason and escalation_id from current_escalation, state_dump, on_failure, plus Identity and summary. This applies after the transition and on every later poll; no dispatch opens. TerminalPayload loses its escalation branch.
  7. Maps: in maps/poodr-ticket-implementation.yaml, needs-orchestrator leaves terminals: and becomes the node needs-orchestrator: { agent: null, waiting: true, edges: {} }. Convert the fixture maps under tests/fixtures/maps/ that use needs-orchestrator: escalation the same way; keep one fixture with outcome escalation for AC 4.

The readers that refuse closed runs stay unchanged (ADR-0259): start_subagent.rb:55, completion_intake/lookup.rb:76, Transition#refuse_unroutable!, Dispatch#derived_state_from_owner. abandon_unfinished! now abandons a waiting run; ADR-0259 accepts this.

Required work:

  1. Remove the escalated stamp — check: absent escalated_at in plugins/os-sdlc/lib/os_sdlc/model/lifecycle_stamps.rb
  2. Drop the escalated column — check: absent escalated_at in plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  3. Remove the escalation terminal outcome — check: absent implementation_failed escalation in plugins/os-sdlc/lib/os_sdlc/model/map/validator.rb
  4. Read the waiting node flag — check: present waiting in plugins/os-sdlc/lib/os_sdlc/model/map/node_spec.rb
  5. Make the ticket map waiting node a node — check: absent needs-orchestrator: escalation in plugins/os-sdlc/maps/*.yaml
  6. Add the escalation payload — check: present class EscalationPayload in plugins/os-sdlc/lib/os_sdlc/presenter/**/*.rb
  7. Remove the terminal escalation branch — check: absent escalation in plugins/os-sdlc/lib/os_sdlc/presenter/implementation/terminal_payload.rb

Acceptance criteria

  1. Given an active run whose dispatch escalates, when NextStep routes it, then the run stays active, its position is needs-orchestrator, one escalations row exists, and the payload has type: escalation, node: needs-orchestrator, the row reason, the row id as escalation_id, and a state_dump.
  2. Given a run waiting at needs-orchestrator, when NextStep runs again, then it returns the escalation payload and opens no dispatch.
  3. Given a run waiting at needs-orchestrator, when implementation-open runs for the same session, then the waiting run is abandoned.
  4. Given a map with a terminal whose outcome is escalation, when it loads, then it fails with unknown_terminal_outcome.
  5. Given a map with an agent node whose escalate edge reaches no waiting node, when it loads, then it fails with missing_escalate_route.
  6. Given a map whose waiting node has no edges, when it loads, then it is valid.
  7. Given the shipped ticket map, when it loads, then it is valid and needs-orchestrator is a waiting node.

Owner sketch

class OsSdlc::Map::Node
  def waiting?
end

class OsSdlc::Implementation::EscalationPayload
  def self.for(implementation, root:)
  def to_h
end

Origin

  • Trigger: user ruling A1 on the map reroute design, 2026-10-01 (ADR-0259 decisions 1, 3, 4, 5)
  • Improvised this session: none
  • Chain: a reroute cannot continue an escalated run ← escalation closes the run ← DESIGN (docs/adr/0133, docs/adr/0165, docs/adr/0259)
  • Root candidate: #859
  • Where: plugins/os-sdlc/lib/os_sdlc/model/lifecycle_stamps.rb
  • Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl

Parent: #859
Blocked by: #883, #884

## Problem ADR-0259 decisions 1, 3, 4 and 5 make escalation a waiting position, not a closing stamp. Today `LifecycleStamps::ALL` (model/lifecycle_stamps.rb:5) lists `escalated_at: :escalated`, so an escalated run is closed and no reroute can continue it. `Landing#land!` (command/implementation/landing.rb:15) terminates the run when the route reaches a terminal with outcome `escalation`. `Map::Validator::TERMINAL_OUTCOMES` (model/map/validator.rb:11) accepts `escalation`, and `NodeCheck#check_escalate_route!` requires every agent node to reach a terminal with that outcome. `NextStep` (pipeline/next_step.rb) emits the escalation as a terminal payload through `TerminalPayload`. ## Reproduce n/a: design change, nothing to reproduce. ## Approach 1. Stamps: remove `escalated_at` from `LifecycleStamps::ALL` and from the `implementations` table (adapter/db.rb:315); bump `SCHEMA_VERSION` (ADR-0129/0155; do not bump while a run is live). Remove `Lifecycle#escalate!` and `#escalated?`. The closing stamps are `completed_at`, `abandoned_at`, `failed_at`; `failed_reason` means failure only. Update `TERMINAL_STAMPS` in `tests/adapter/db_migration_test.rb`. 2. Waiting node: `Map::NodeSpec` reads `waiting: true`; `Map::Node#waiting?`. A waiting node has no agent and no verdict. Its edges may be empty (the reroute exit comes in #885). 3. Validator: `TERMINAL_OUTCOMES` becomes `implementation_complete implementation_failed`. `NodeGuardChecks` exempts a waiting node from the single-default-edge rule. `check_dead_pocket!` skips waiting nodes. `check_escalate_route!` requires the escalate edge of every agent node to reach a waiting node (through routing nodes); code `:missing_escalate_route`. 4. `TransitWalk#dispatchable?` stops at a waiting node. 5. `Landing#land!`: when the decision node is a waiting node, it adds the Escalation row (#884) and calls `advance!(position:)`. It does not terminate the run. 6. `NextStep`: when the run is active and its position is a waiting node, it returns `Implementation::EscalationPayload`: `type: "escalation"`, `node`, `reason` and `escalation_id` from `current_escalation`, `state_dump`, `on_failure`, plus `Identity` and `summary`. This applies after the transition and on every later poll; no dispatch opens. `TerminalPayload` loses its escalation branch. 7. Maps: in `maps/poodr-ticket-implementation.yaml`, `needs-orchestrator` leaves `terminals:` and becomes the node `needs-orchestrator: { agent: null, waiting: true, edges: {} }`. Convert the fixture maps under `tests/fixtures/maps/` that use `needs-orchestrator: escalation` the same way; keep one fixture with outcome `escalation` for AC 4. The readers that refuse closed runs stay unchanged (ADR-0259): `start_subagent.rb:55`, `completion_intake/lookup.rb:76`, `Transition#refuse_unroutable!`, `Dispatch#derived_state_from_owner`. `abandon_unfinished!` now abandons a waiting run; ADR-0259 accepts this. Required work: 1. Remove the escalated stamp — check: absent `escalated_at` in `plugins/os-sdlc/lib/os_sdlc/model/lifecycle_stamps.rb` 2. Drop the escalated column — check: absent `escalated_at` in `plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 3. Remove the escalation terminal outcome — check: absent `implementation_failed escalation` in `plugins/os-sdlc/lib/os_sdlc/model/map/validator.rb` 4. Read the waiting node flag — check: present `waiting` in `plugins/os-sdlc/lib/os_sdlc/model/map/node_spec.rb` 5. Make the ticket map waiting node a node — check: absent `needs-orchestrator: escalation` in `plugins/os-sdlc/maps/*.yaml` 6. Add the escalation payload — check: present `class EscalationPayload` in `plugins/os-sdlc/lib/os_sdlc/presenter/**/*.rb` 7. Remove the terminal escalation branch — check: absent `escalation` in `plugins/os-sdlc/lib/os_sdlc/presenter/implementation/terminal_payload.rb` ## Acceptance criteria 1. Given an active run whose dispatch escalates, when NextStep routes it, then the run stays active, its position is `needs-orchestrator`, one escalations row exists, and the payload has `type: escalation`, `node: needs-orchestrator`, the row reason, the row id as `escalation_id`, and a `state_dump`. 2. Given a run waiting at `needs-orchestrator`, when NextStep runs again, then it returns the escalation payload and opens no dispatch. 3. Given a run waiting at `needs-orchestrator`, when implementation-open runs for the same session, then the waiting run is abandoned. 4. Given a map with a terminal whose outcome is `escalation`, when it loads, then it fails with `unknown_terminal_outcome`. 5. Given a map with an agent node whose escalate edge reaches no waiting node, when it loads, then it fails with `missing_escalate_route`. 6. Given a map whose waiting node has no edges, when it loads, then it is valid. 7. Given the shipped ticket map, when it loads, then it is valid and `needs-orchestrator` is a waiting node. ## Owner sketch ```ruby class OsSdlc::Map::Node def waiting? end class OsSdlc::Implementation::EscalationPayload def self.for(implementation, root:) def to_h end ``` ## Origin - Trigger: user ruling A1 on the map reroute design, 2026-10-01 (ADR-0259 decisions 1, 3, 4, 5) - Improvised this session: none - Chain: a reroute cannot continue an escalated run ← escalation closes the run ← DESIGN (docs/adr/0133, docs/adr/0165, docs/adr/0259) - Root candidate: #859 - Where: plugins/os-sdlc/lib/os_sdlc/model/lifecycle_stamps.rb - Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl Parent: #859 Blocked by: #883, #884
jared changed title from Derive escalated state so a reroute row reactivates the run to Make escalation a waiting position at needs-orchestrator 2026-10-01 15:28:59 +00:00
Author
Owner

Work started on branch ticket-872 (os-sdlc pipeline, map poodr-ticket-implementation).

Work started on branch ticket-872 (os-sdlc pipeline, map poodr-ticket-implementation).
Author
Owner

Implementation 1 stopped at the test-writer: diff-test fails with LoadError because DiffTestPlan requires the changed YAML fixture maps (gate defect, not this ticket). Captured #888; implementing it first. The test-writer work is kept as WIP commit 58f1eb3f on branch ticket-872; #872 re-opens after #888 merges.

Implementation 1 stopped at the test-writer: diff-test fails with LoadError because DiffTestPlan requires the changed YAML fixture maps (gate defect, not this ticket). Captured #888; implementing it first. The test-writer work is kept as WIP commit 58f1eb3f on branch ticket-872; #872 re-opens after #888 merges.
Author
Owner

Resolution

Done: Escalation is a waiting position at needs-orchestrator (Node#waiting?); the landing records the Escalation row and advances; NextStep renders EscalationPayload; escalated_at removed, failed_reason is failure only; fixture maps converted; SCHEMA_VERSION 29. Deviations: implementation 1 stopped at the test-writer because diff-test required YAML fixtures (gate defect) -> captured and implemented #888 first, kept the test work (WIP 58f1eb3f), re-opened; in run 2 the orchestrator hand-fixed tests/support/repeated_next_row_check.rb (asserted the removed failed_reason; now checks the escalations row) after programmer-repair reported it needed a test edit; the Runner commit left 27 test/fixture paths in dirty_paths, committed by hand as 8425ff91.

Evidence: main a3701b71 (Runner commit, implementation 1 implementation_complete, 12 dispatches, 1 retry; required-work and suite gates green), 8425ff91, History row; bin/test on main: 9 suites passed.

Follow-ups: #888 captured and closed. Dropped: programmer note that check_dead_pocket! also accepts a node that reaches a waiting node - consistent with ADR-0259 waiting nodes; the verifying design session can judge.

## Resolution **Done:** Escalation is a waiting position at needs-orchestrator (Node#waiting?); the landing records the Escalation row and advances; NextStep renders EscalationPayload; escalated_at removed, failed_reason is failure only; fixture maps converted; SCHEMA_VERSION 29. Deviations: implementation 1 stopped at the test-writer because diff-test required YAML fixtures (gate defect) -> captured and implemented #888 first, kept the test work (WIP 58f1eb3f), re-opened; in run 2 the orchestrator hand-fixed tests/support/repeated_next_row_check.rb (asserted the removed failed_reason; now checks the escalations row) after programmer-repair reported it needed a test edit; the Runner commit left 27 test/fixture paths in dirty_paths, committed by hand as 8425ff91. **Evidence:** main a3701b71 (Runner commit, implementation 1 implementation_complete, 12 dispatches, 1 retry; required-work and suite gates green), 8425ff91, History row; bin/test on main: 9 suites passed. **Follow-ups:** #888 captured and closed. Dropped: programmer note that check_dead_pocket! also accepts a node that reaches a waiting node - consistent with ADR-0259 waiting nodes; the verifying design session can judge.
jared closed this issue 2026-10-01 17:39:46 +00:00
jared referenced this issue from a commit 2026-10-01 18:24:32 +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.

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