Add the reroute gate and the reroute landing with the REROUTE block #873

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

Problem

ADR-0259 decision 9, as amended by ADR-0260: map-router runs os-sdlc-gate DISPATCH_ID in-turn, before it stops. The chosen map: and node: exist only in its stop handoff, so the reroute gate has nothing to check. The fix: map-router records a draft reroute with a new CLI, os-sdlc-implementation-map-update. The reroute gate confirms a valid draft exists. The landing turns the draft into an applied reroute and positions the run at the chosen entry node. The reply carries the REROUTE block (ADR-0258 decision 4). Today Gate::Name::ALL (model/gate/name.rb:12) has no reroute, Gate::Commands#to_h (query/gate/commands.rb:13) has no command for it, Landing#land! (command/implementation/landing.rb:13) always follows decision, and Implementation#current_map (model/implementation.rb:70-72) reads reroutes_dataset.first, so a draft row would switch the map.

Reproduce

n/a: design change, nothing to reproduce.

Approach

  1. Gate name and command: add REROUTE = "reroute" to Gate::Name::ALL and Name::REROUTE => "os-sdlc-reroute-check #{dispatch id}" to Gate::Commands#to_h. Add bin/os-sdlc-reroute-check DISPATCH_ID (Boot command reroute-check, same shape as bin/os-sdlc-required-work-check). The gate signature does not change.
  2. Map-update CLI: add bin/os-sdlc-implementation-map-update DISPATCH_ID --map NAME --node NODE --situation S --misfit M (Boot command implementation-map-update). It requires the dispatch to be pending (Dispatch.pending!) and owned by agent type map-router, with the same checks as the other dispatch CLIs. Reroute::Draft validates. It loads the map with Map::Catalog.default.load(map); loading runs the validator, so the entry-gates rule (#870) holds. It fails, with an error text that names the cause, exits nonzero, and writes nothing, when: situation or misfit is missing; the map does not load; the map has no entries:; node is not in entries (the message names the node and lists the allowed entries); the implementation already has 3 applied reroutes (cap, ADR-0258 decision 4).
  3. On green it writes one Reroute row (#871): map, node, situation, misfit, draft: true, and dispatch_id = the requesting dispatch, found as the map-router dispatch's received_handoff.creating_dispatch (nil when the orchestrator wrote the handoff). A second call in the same map-router dispatch replaces the draft, so one row exists per dispatch.
  4. Reroute::Check (the gate) only confirms that a valid draft exists for this dispatch. It fails with a message that names the cause when none exists.
  5. Draft versus applied: only applied reroutes (draft: false) change Implementation#current_map and count toward the 3-reroute cap. current_map reads the newest applied reroute. A draft never switches the map.
  6. Reroute landing: in Landing#land!, when the routed dispatch's node has the reroute gate and the verdict is pass, the landing ignores decision, turns the draft into an applied reroute (draft: false), calls advance!(position: <that reroute node>) (that node is in the new current map), and stamps the map-router created handoff target with that node's agent. Implementation::Transition stays the only caller (ADR-0168).
  7. REROUTE block, rendered by Reroute::Block from the newest applied reroute row:
REROUTE  by orchestrator
  situation: <situation>
  map:       <map> (selected)
  why:       <misfit> <the chosen map's fits: line>
  resume at: <node>

by <agent_type> (dispatch N) replaces by orchestrator when dispatch_id is set. The landing reply carries it as reroute:. The orchestrator does not read gate replies, so the NextStep dispatch payload that follows the reroute landing also carries reroute:.
8. The orchestrator path does not change: os-sdlc-reroute-request ID --situation --misfit (no dispatch id) at needs-orchestrator.
9. Agent prompt: agents/map-router.md already names the map-update step (ADR-0260).

Required work:

  1. Add the reroute gate name — check: present REROUTE = "reroute" in plugins/os-sdlc/lib/os_sdlc/model/gate/name.rb
  2. Map the gate to its check command — check: present Name::REROUTE in plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb
  3. Add the gate check CLI — check: present reroute-check in plugins/os-sdlc/bin/os-sdlc-reroute-check
  4. Add the map-update CLI — check: present implementation-map-update in plugins/os-sdlc/bin/os-sdlc-implementation-map-update
  5. Add the draft validator — check: present entries in plugins/os-sdlc/lib/os_sdlc/reroute/draft.rb
  6. Make current_map read applied reroutes only — check: present draft in plugins/os-sdlc/lib/os_sdlc/model/implementation.rb
  7. Add the reroute landing — check: present reroute in plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.rb
  8. Render the REROUTE block — check: present REROUTE by in plugins/os-sdlc/lib/os_sdlc/presenter/**/*.rb

Acceptance criteria

  1. Given a map-router dispatch that recorded a valid draft (an approved map, a node in its entries), when the gate passes and NextStep lands the dispatch, then one applied reroutes row exists (draft: false, dispatch_id nil after an orchestrator request), and the reply holds the REROUTE block with by orchestrator, resume at: <node>, and the map's fits: text in why.
  2. Given that landing, when NextStep routes the map-router dispatch, then the run is positioned at the chosen node, current_map is the chosen map, and the dispatch payload names that node's agent and carries the REROUTE block.
  3. Given map-update ran twice in one map-router dispatch, when the reroutes rows are counted, then there is one row.
  4. Given map-update gets a node not in the map's entries, when it runs, then it exits nonzero, names the node and the allowed entries, and writes no row.
  5. Given map-update gets a map with no entries, when it runs, then it exits nonzero and writes no row.
  6. Given map-update gets no misfit, when it runs, then it exits nonzero, names misfit, and writes no row.
  7. Given an implementation with 3 applied reroutes rows, when map-update runs, then it exits nonzero with the reroute cap and writes no row.
  8. Given a draft reroute row exists and no applied row, when Implementation#current_map is read, then it returns the implementation's own map.
  9. Given no draft exists for the dispatch, when the reroute gate runs, then it fails and names the missing draft.

Owner sketch

class OsSdlc::Reroute::Draft
  def initialize(root:, dispatch:, map:, node:, situation:, misfit:)
  def write
end

class OsSdlc::Reroute::Check
  def initialize(root:, dispatch:)
  def report
end

class OsSdlc::Implementation::Landing
  def reroute!
end

class OsSdlc::Reroute::Block
  def initialize(reroute, map)
  def to_s
end

Origin

  • Trigger: user ruling A3 on the map reroute design, 2026-10-01 (ADR-0258 decision 4, ADR-0259 decision 9, amended by ADR-0260)
  • Improvised this session: none
  • Chain: a misfit run escalates and is hand-finished ← no gate can move a run to another map ← DESIGN (docs/adr/0258, docs/adr/0259)
  • Root candidate: #859
  • Where: plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.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: #871, #885, #886

## Problem ADR-0259 decision 9, as amended by ADR-0260: map-router runs `os-sdlc-gate DISPATCH_ID` in-turn, before it stops. The chosen `map:` and `node:` exist only in its stop handoff, so the `reroute` gate has nothing to check. The fix: map-router records a draft reroute with a new CLI, `os-sdlc-implementation-map-update`. The `reroute` gate confirms a valid draft exists. The landing turns the draft into an applied reroute and positions the run at the chosen entry node. The reply carries the REROUTE block (ADR-0258 decision 4). Today `Gate::Name::ALL` (model/gate/name.rb:12) has no `reroute`, `Gate::Commands#to_h` (query/gate/commands.rb:13) has no command for it, `Landing#land!` (command/implementation/landing.rb:13) always follows `decision`, and `Implementation#current_map` (model/implementation.rb:70-72) reads `reroutes_dataset.first`, so a draft row would switch the map. ## Reproduce n/a: design change, nothing to reproduce. ## Approach 1. Gate name and command: add `REROUTE = "reroute"` to `Gate::Name::ALL` and `Name::REROUTE => "os-sdlc-reroute-check #{dispatch id}"` to `Gate::Commands#to_h`. Add `bin/os-sdlc-reroute-check DISPATCH_ID` (Boot command `reroute-check`, same shape as `bin/os-sdlc-required-work-check`). The gate signature does not change. 2. Map-update CLI: add `bin/os-sdlc-implementation-map-update DISPATCH_ID --map NAME --node NODE --situation S --misfit M` (Boot command `implementation-map-update`). It requires the dispatch to be pending (`Dispatch.pending!`) and owned by agent type map-router, with the same checks as the other dispatch CLIs. `Reroute::Draft` validates. It loads the map with `Map::Catalog.default.load(map)`; loading runs the validator, so the entry-gates rule (#870) holds. It fails, with an error text that names the cause, exits nonzero, and writes nothing, when: `situation` or `misfit` is missing; the map does not load; the map has no `entries:`; `node` is not in `entries` (the message names the node and lists the allowed entries); the implementation already has 3 applied reroutes (cap, ADR-0258 decision 4). 3. On green it writes one Reroute row (#871): `map`, `node`, `situation`, `misfit`, `draft: true`, and `dispatch_id` = the requesting dispatch, found as the map-router dispatch's `received_handoff.creating_dispatch` (nil when the orchestrator wrote the handoff). A second call in the same map-router dispatch replaces the draft, so one row exists per dispatch. 4. `Reroute::Check` (the gate) only confirms that a valid draft exists for this dispatch. It fails with a message that names the cause when none exists. 5. Draft versus applied: only applied reroutes (`draft: false`) change `Implementation#current_map` and count toward the 3-reroute cap. `current_map` reads the newest applied reroute. A draft never switches the map. 6. Reroute landing: in `Landing#land!`, when the routed dispatch's node has the `reroute` gate and the verdict is `pass`, the landing ignores `decision`, turns the draft into an applied reroute (`draft: false`), calls `advance!(position: <that reroute node>)` (that node is in the new current map), and stamps the map-router created handoff target with that node's agent. `Implementation::Transition` stays the only caller (ADR-0168). 7. REROUTE block, rendered by `Reroute::Block` from the newest applied reroute row: ``` REROUTE by orchestrator situation: <situation> map: <map> (selected) why: <misfit> <the chosen map's fits: line> resume at: <node> ``` `by <agent_type> (dispatch N)` replaces `by orchestrator` when `dispatch_id` is set. The landing reply carries it as `reroute:`. The orchestrator does not read gate replies, so the NextStep dispatch payload that follows the reroute landing also carries `reroute:`. 8. The orchestrator path does not change: `os-sdlc-reroute-request ID --situation --misfit` (no dispatch id) at `needs-orchestrator`. 9. Agent prompt: `agents/map-router.md` already names the map-update step (ADR-0260). Required work: 1. Add the reroute gate name — check: present `REROUTE = "reroute"` in `plugins/os-sdlc/lib/os_sdlc/model/gate/name.rb` 2. Map the gate to its check command — check: present `Name::REROUTE` in `plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb` 3. Add the gate check CLI — check: present `reroute-check` in `plugins/os-sdlc/bin/os-sdlc-reroute-check` 4. Add the map-update CLI — check: present `implementation-map-update` in `plugins/os-sdlc/bin/os-sdlc-implementation-map-update` 5. Add the draft validator — check: present `entries` in `plugins/os-sdlc/lib/os_sdlc/reroute/draft.rb` 6. Make current_map read applied reroutes only — check: present `draft` in `plugins/os-sdlc/lib/os_sdlc/model/implementation.rb` 7. Add the reroute landing — check: present `reroute` in `plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.rb` 8. Render the REROUTE block — check: present `REROUTE by` in `plugins/os-sdlc/lib/os_sdlc/presenter/**/*.rb` ## Acceptance criteria 1. Given a map-router dispatch that recorded a valid draft (an approved map, a node in its `entries`), when the gate passes and NextStep lands the dispatch, then one applied reroutes row exists (`draft: false`, `dispatch_id` nil after an orchestrator request), and the reply holds the REROUTE block with `by orchestrator`, `resume at: <node>`, and the map's `fits:` text in `why`. 2. Given that landing, when NextStep routes the map-router dispatch, then the run is positioned at the chosen node, `current_map` is the chosen map, and the dispatch payload names that node's agent and carries the REROUTE block. 3. Given map-update ran twice in one map-router dispatch, when the reroutes rows are counted, then there is one row. 4. Given map-update gets a node not in the map's `entries`, when it runs, then it exits nonzero, names the node and the allowed entries, and writes no row. 5. Given map-update gets a map with no `entries`, when it runs, then it exits nonzero and writes no row. 6. Given map-update gets no `misfit`, when it runs, then it exits nonzero, names `misfit`, and writes no row. 7. Given an implementation with 3 applied reroutes rows, when map-update runs, then it exits nonzero with the reroute cap and writes no row. 8. Given a draft reroute row exists and no applied row, when `Implementation#current_map` is read, then it returns the implementation's own map. 9. Given no draft exists for the dispatch, when the `reroute` gate runs, then it fails and names the missing draft. ## Owner sketch ```ruby class OsSdlc::Reroute::Draft def initialize(root:, dispatch:, map:, node:, situation:, misfit:) def write end class OsSdlc::Reroute::Check def initialize(root:, dispatch:) def report end class OsSdlc::Implementation::Landing def reroute! end class OsSdlc::Reroute::Block def initialize(reroute, map) def to_s end ``` ## Origin - Trigger: user ruling A3 on the map reroute design, 2026-10-01 (ADR-0258 decision 4, ADR-0259 decision 9, amended by ADR-0260) - Improvised this session: none - Chain: a misfit run escalates and is hand-finished ← no gate can move a run to another map ← DESIGN (docs/adr/0258, docs/adr/0259) - Root candidate: #859 - Where: plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.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: #871, #885, #886
jared changed title from Add os-sdlc-reroute-request, out-of-map map-router dispatch, and reroute gate to Add the reroute gate and the reroute landing with the REROUTE block 2026-10-01 15:29:00 +00:00
Author
Owner

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

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

Waiting for a ruling: where does the reroute gate get the router's map and node at gate time?

Implementation 4 stopped at the change-auditor (dispatch 28, verdict fail). The run waits at needs-orchestrator (escalation 1).

The conflict. ADR-0259 decision 9 says the reroute gate validates the router handoff. agents/map-router.md tells map-router to run os-sdlc-gate DISPATCH_ID until green, then stop with a YAML handoff that holds map: and node:. But Dispatch#complete! records the handoff only at stop (model/dispatch.rb:92-104), and bin/os-sdlc-gate takes only a dispatch id. At gate time map and node exist nowhere; received_handoff holds only the requester's situation and misfit. AC 1, 4, 5 and 6 cannot work on the real pipeline as written.

Options

A. The agent passes them to the gate:

os-sdlc-gate DISPATCH_ID --map poodr-guard-implementation --node test-writer

The gate CLI and Boot::Inputs change; the stop handoff still carries map: and node:.

B. The agent stops first, the gate runs on the stored handoff, and a red gate returns the run to map-router. This changes the in-turn gate model.

C. The agent writes a draft handoff to a store before the gate; the gate reads the draft.

Recommendation: A, about 70% confidence. Deciding reason: it is the smallest change, keeps the in-turn gate model, and needs no new store. Every option amends ADR-0259 decision 9 and agents/map-router.md.

After the ruling: amend ADR-0259 with /os-adr:create, update map-router.md and this ticket (Approach 2-3; Required work also needs the Boot/Inputs wiring, the HandoffContent readers, the dedup rule, the cap owner, and a home for Reroute::Check and Reroute::Block), then open a new implementation. #874 stays blocked until then.

Default if no answer: nothing moves; #873 and #874 stay open.

**Waiting for a ruling: where does the `reroute` gate get the router's `map` and `node` at gate time?** Implementation 4 stopped at the change-auditor (dispatch 28, verdict fail). The run waits at `needs-orchestrator` (escalation 1). **The conflict.** ADR-0259 decision 9 says the `reroute` gate validates the router handoff. `agents/map-router.md` tells map-router to run `os-sdlc-gate DISPATCH_ID` until green, then stop with a YAML handoff that holds `map:` and `node:`. But `Dispatch#complete!` records the handoff only at stop (`model/dispatch.rb:92-104`), and `bin/os-sdlc-gate` takes only a dispatch id. At gate time `map` and `node` exist nowhere; `received_handoff` holds only the requester's `situation` and `misfit`. AC 1, 4, 5 and 6 cannot work on the real pipeline as written. **Options** A. The agent passes them to the gate: ``` os-sdlc-gate DISPATCH_ID --map poodr-guard-implementation --node test-writer ``` The gate CLI and `Boot::Inputs` change; the stop handoff still carries `map:` and `node:`. B. The agent stops first, the gate runs on the stored handoff, and a red gate returns the run to map-router. This changes the in-turn gate model. C. The agent writes a draft handoff to a store before the gate; the gate reads the draft. **Recommendation:** A, about 70% confidence. Deciding reason: it is the smallest change, keeps the in-turn gate model, and needs no new store. Every option amends ADR-0259 decision 9 and `agents/map-router.md`. **After the ruling:** amend ADR-0259 with `/os-adr:create`, update `map-router.md` and this ticket (Approach 2-3; Required work also needs the Boot/Inputs wiring, the HandoffContent readers, the dedup rule, the cap owner, and a home for `Reroute::Check` and `Reroute::Block`), then open a new implementation. #874 stays blocked until then. **Default if no answer:** nothing moves; #873 and #874 stay open.
Author
Owner

User ruling 2026-10-01 (recorded in ADR-0260, amends ADR-0259 decision 9). The reroute gate had nothing to check: map and node exist only in the map-router stop handoff. Resolution: new CLI os-sdlc-implementation-map-update DISPATCH_ID --map --node --situation --misfit validates (map in catalog, node in entries, situation and misfit present, fewer than 3 applied reroutes) and writes a draft Reroute, one per dispatch. The reroute gate only confirms a valid draft exists. The landing applies the draft and positions the run. Only applied reroutes change current_map and count toward the cap. Orchestrator path (os-sdlc-reroute-request) is unchanged. Rejected: args on os-sdlc-gate, gate after stop, a new store. Body rewritten to match. #874 needs no change.

User ruling 2026-10-01 (recorded in ADR-0260, amends ADR-0259 decision 9). The reroute gate had nothing to check: map and node exist only in the map-router stop handoff. Resolution: new CLI os-sdlc-implementation-map-update DISPATCH_ID --map --node --situation --misfit validates (map in catalog, node in entries, situation and misfit present, fewer than 3 applied reroutes) and writes a draft Reroute, one per dispatch. The reroute gate only confirms a valid draft exists. The landing applies the draft and positions the run. Only applied reroutes change current_map and count toward the cap. Orchestrator path (os-sdlc-reroute-request) is unchanged. Rejected: args on os-sdlc-gate, gate after stop, a new store. Body rewritten to match. #874 needs no change.
Author
Owner

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

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

Resolution

Done: os-sdlc-implementation-map-update writes a draft reroute per map-router dispatch; reroute gate (os-sdlc-reroute-check) confirms it; Landing applies it; only applied reroutes change current_map and count to cap 3 (ADR-0260)

Evidence: commit d3f4888f; bin/test: '9 suites passed' (PASS os-sdlc (minitest)); implementation 6 complete

Follow-ups: #890, #891

## Resolution **Done:** os-sdlc-implementation-map-update writes a draft reroute per map-router dispatch; reroute gate (os-sdlc-reroute-check) confirms it; Landing applies it; only applied reroutes change current_map and count to cap 3 (ADR-0260) **Evidence:** commit d3f4888f; bin/test: '9 suites passed' (PASS os-sdlc (minitest)); implementation 6 complete **Follow-ups:** #890, #891
jared closed this issue 2026-10-01 20:19:13 +00:00
jared referenced this issue from a commit 2026-10-02 17:43:17 +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#873
No description provided.