Эх сурвалжийг харах

r3: legacy wire removal (arch 19, P3-1) — SaddleUpdateEndpoint + SaddlePollEndpoint and their routes are gone (the daemon never called them; verified against saddle/src at review time): the poll-era ingest/verdict pair 404s now, pinned by regressions. The service support that existed only for them dies with them: take_pending_answers (the saddle-keyed variant; the for_task assignment carrier stays), take_pending_comments + PendingComment, validate_token (its only caller was the update/poll bearer pin — resolve_bearer collapses to saddle_for_token for the all-id-less wire), the subtask/plan refusal arms, and the update endpoints' own statics move to SaddleWire (read_json_body/json_ok/json_error shared by every remaining /api/saddle/* route). Knock-ons per the plan: tasks.working_branch was write-dead (only /update done ever supplied it) — column + entity field + TaskDetailPublic member + the task-page line + mark_terminal/complete_task's dead parameter die; the plain-task plan-review UI in task_detail.html was unreachable (plans are goal-only since plan 12; only /update block type=plan could raise one) — BlockView.is_plan_review, the plan-review section, and the task page's ~180-line plan-remarks JS (dead with no [data-plan-review] targets) are removed; AnswerBlockAction's plan-verdict arms stay (the deep-link API + off-contract pushes keep working). Test fixtures rewrite into the push-era shapes: block raises ride POST /results blocked pushes (pages/lifecycle/push bundles), the terminal-task review fixture inserts its row directly (terminal pushes are consumed silently), rejected entries ride a results push, the dashboard review raise rides a review entry, the queued-state guard becomes a results state no-op, the /update subtask/plan refusals become 404 pins, and branch_advanced — the /poll verdict hint — is re-pinned on its surviving surface: the answers bundle's parked-resume allocation now asserts branch_advanced=true after a strictly-newer sibling completion on the shared branch. 856 -> 850 checks.

clanker 1 сар өмнө
parent
commit
2cf9149d29

+ 72 - 318
src/Api/SaddleApi.vala

@@ -9,189 +9,18 @@ using ClankerSaddle.Services;
 namespace ClankerSaddle.Api {
 
     /**
-     * `POST /api/saddle/{id}/update` — the ingest channel for a saddle
-     * daemon, authorised by its `Authorization: Bearer <token>` header
-     * (see {@link SaddleService.validate_token}). The body carries a type
-     * discriminator, the session id and a type-specific `data` object:
-     *
-     * ```json
-     * {"type": "output", "session_id": 12, "data": {
-     *     "command": "ls -la", "output": "…", "exit_code": 0, "type": "command",
-     *     "started_at": "…", "completed_at": "…", "duration_ms": 41}}
-     * ```
-     *
-     * Handled types: `output` (a single entry or an `entries` array; both
-     * `data` shapes store `session_commands` rows), `comment`, `block`
-     * (question/rfc/review with optional `options` and markdown `body`;
-     * returns the new `block_id`), `progress`, `review`, `done`
-     * (completes the task), `failed` (fails the task with the
-     * explanation) and `status` (working|blocked). Conversation-level
-     * types also append a typed entry to the session's terminal log.
-     * The plan-12 deletions: `subtask` and `plan` are GONE (the goal
-     * agent's task fan-out lives on the task callbacks and its plan on
-     * the goal) — a legacy daemon posting them gets a 400 naming the
-     * removed verb.
+     * Static helpers of the `/api/saddle/*` wire (formerly the late
+     * SaddleUpdateEndpoint's internals — the update/poll pair died with
+     * the poll era, plan 16 R3 / arch 19): JSON body reading and the
+     * ok/error response envelopes every saddle endpoint shares.
      */
-    public class SaddleUpdateEndpoint : Object, Endpoint {
-
-        protected TaskService tasks = inject<TaskService>();
-        protected SaddleService saddles = inject<SaddleService>();
-        protected InvercargillSql.Orm.OrmSession db = inject<InvercargillSql.Orm.OrmSession>();
-
-        public virtual async HttpResult handle_request(HttpContext http_context,
-                RouteContext route_context) throws Error {
-            string id_text = "";
-            route_context.mapped_parameters.try_get("id", out id_text);
-            int64 saddle_id = int64.parse(id_text);
-
-            if ((yield resolve_bearer(http_context, saddles, saddle_id)) == null) {
-                return json_error(401, "Missing or invalid saddle token");
-            }
-
-            Json.Object body;
-            try {
-                body = yield read_json_body(http_context);
-            } catch (Error e) {
-                return json_error(400, e.message);
-            }
-
-            string type = body.has_member("type") ? body.get_string_member_with_default("type", "") : "";
-            int64 session_id = body.has_member("session_id") ? body.get_int_member("session_id") : 0;
-            var data = body.has_member("data") && body.get_member("data").get_node_type() == Json.NodeType.OBJECT
-                ? body.get_object_member("data") : new Json.Object();
-
-            try {
-                var response = yield handle_update(saddle_id, session_id, type, data);
-                return json_ok(response);
-            } catch (Error e) {
-                return json_error(400, e.message);
-            }
-        }
-
-        private async Json.Object handle_update(int64 saddle_id, int64 session_id,
-                string type, Json.Object data) throws Error {
-            var task_id = yield task_id_for_session(session_id);
-
-            switch (type) {
-                case "output":
-                    if (data.has_member("entries") && data.get_member("entries").get_node_type() == Json.NodeType.ARRAY) {
-                        foreach (var element in data.get_array_member("entries").get_elements()) {
-                            if (element.get_node_type() == Json.NodeType.OBJECT) {
-                                yield store_output(session_id, element.get_object());
-                            }
-                        }
-                    } else {
-                        yield store_output(session_id, data);
-                    }
-                    break;
-
-                case "comment":
-                    string body_text = EntryJson.get_string_or(data, "body", EntryJson.get_string_or(data, "text", ""));
-                    yield tasks.add_comment(task_id, "agent", body_text);
-                    yield tasks.log_session_entry(task_id, "comment", "saddle comment", body_text);
-                    break;
-
-                case "block":
-                    var block = yield tasks.add_block(task_id,
-                        EntryJson.get_string_or(data, "question", ""),
-                        EntryJson.get_string_or(data, "type", "question"),
-                        EntryJson.string_array_member(data, "options", "block options"),
-                        EntryJson.get_string_or(data, "body", null));
-                    yield tasks.log_session_entry(task_id, "block",
-                        @"block: $(block.question)", block.body);
-                    var block_response = new Json.Object();
-                    block_response.set_int_member("block_id", block.id);
-                    return block_response;
-
-                case "subtask":
-                case "plan":
-                    // Plan 12: the subtask and task-plan machinery is
-                    // deleted — the daemon's goal agent fans tasks out
-                    // through the /api/saddle/task/* callbacks and
-                    // plans ride the goal. Surface the removal so a
-                    // legacy daemon fails loudly instead of silently.
-                    throw new Error(Quark.from_string("clanker-saddle"), 90,
-                        @"The '$(type)' update type is gone (plan 12) — use the task callbacks / the goal's plan flow");
-
-                case "progress":
-                    string summary = EntryJson.get_string_or(data, "summary",
-                        EntryJson.get_string_or(data, "text", ""));
-                    yield tasks.add_progress(task_id, summary);
-                    yield tasks.log_session_entry(task_id, "progress", "saddle progress-update", summary);
-                    break;
-
-                case "review":
-                    string review_summary = EntryJson.get_string_or(data, "summary", "");
-                    var review_block = yield tasks.request_review(task_id, review_summary);
-                    yield tasks.log_session_entry(task_id, "review", "saddle review", review_summary);
-                    break;
-
-                case "done":
-                    yield tasks.complete_task(task_id,
-                        EntryJson.get_string_or(data, "summary", null),
-                        EntryJson.get_string_or(data, "working_branch", null), OUTCOME_DONE);
-                    break;
-
-                case "failed":
-                    string failure = EntryJson.get_string_or(data, "summary",
-                        EntryJson.get_string_or(data, "text", ""));
-                    yield tasks.fail_task(task_id, failure.length > 0 ? failure : null);
-                    yield tasks.log_session_entry(task_id, "failed", "saddle failure", failure);
-                    break;
-
-                case "status":
-                    string state = EntryJson.get_string_or(data, "state", "");
-                    if (state == "working" || state == "blocked") {
-                        yield tasks.set_status(task_id, state);
-                    }
-                    break;
-
-                default:
-                    throw new Error(Quark.from_string("clanker-saddle"), 20,
-                        @"Unknown update type '$(type)'");
-            }
-
-            return new Json.Object();
-        }
-
-        private async void store_output(int64 session_id, Json.Object entry) throws Error {
-            var command = new SessionCommand() {
-                session_id = session_id,
-                command_text = EntryJson.get_string_or(entry, "command", ""),
-                output_text = EntryJson.get_string_or(entry, "output", null),
-                exit_code = entry.has_member("exit_code")
-                    && entry.get_member("exit_code").get_value_type() == typeof(int64)
-                    ? entry.get_int_member("exit_code") : 0,
-                entry_type = EntryJson.get_string_or(entry, "type", "command"),
-                piped_input = EntryJson.get_string_or(entry, "input", null) != null
-                    && EntryJson.get_string_or(entry, "input", "").length > 0
-                    ? EntryJson.get_string_or(entry, "input", null) : null,
-                started_at = EntryJson.parse_iso(entry, "started_at"),
-                completed_at = EntryJson.parse_iso(entry, "completed_at"),
-                duration_ms = entry.has_member("duration_ms")
-                    && entry.get_member("duration_ms").get_value_type() == typeof(int64)
-                    ? entry.get_int_member("duration_ms") : 0
-            };
-            yield db.insert_async<SessionCommand>(command);
-        }
-
-        private async int64 task_id_for_session(int64 session_id) throws Error {
-            var session = yield db.query<ClankerSaddle.Data.Session>()
-                .where(Invercargill.Expressions.expr("id == $0",
-                    Invercargill.Expressions.elem<int64?>(session_id)))
-                .first_async();
-            if (session == null) {
-                throw new Error(Quark.from_string("clanker-saddle"), 21,
-                    @"No session exists with id $(session_id)");
-            }
-            return ((!)session).task_id;
-        }
+    internal class SaddleWire : Object {
 
         /**
          * Reads and parses the request body as a JSON object; an empty body
          * parses as an empty object.
          */
-        internal static async Json.Object read_json_body(HttpContext http_context) throws Error {
+        public static async Json.Object read_json_body(HttpContext http_context) throws Error {
             var bytes = yield http_context.request.request_body.read_all();
             // to_raw_string on an empty body is not safe; guard the count.
             string text = "";
@@ -226,7 +55,7 @@ namespace ClankerSaddle.Api {
             return result;
         }
 
-        internal static HttpResult json_error(uint16 status_code, string message) {
+        public static HttpResult json_error(uint16 status_code, string message) {
             var body = new Json.Object();
             body.set_boolean_member("ok", false);
             body.set_string_member("error", message);
@@ -237,71 +66,6 @@ namespace ClankerSaddle.Api {
             result.set_header("Content-Type", "application/json");
             return result;
         }
-
-    }
-
-    /**
-     * `GET /api/saddle/{id}/poll` — the outbound channel for a saddle
-     * daemon, authorised by its `Authorization: Bearer <token>` header:
-     * answered blocks and undelivered human comments for the saddle's
-     * current task, each handed over exactly once.
-     *
-     * ```json
-     * {"answers": [{"block_id": 4, "answer": "Use the flange tool"}],
-     *  "comments": [{"comment_id": 9, "author": "user:admin",
-     *                "body": "…", "quote": "…"}]}
-     * ```
-     */
-    public class SaddlePollEndpoint : Object, Endpoint {
-
-        private SaddleService saddles = inject<SaddleService>();
-        private TaskService tasks = inject<TaskService>();
-
-        public virtual async HttpResult handle_request(HttpContext http_context,
-                RouteContext route_context) throws Error {
-            string id_text = "";
-            route_context.mapped_parameters.try_get("id", out id_text);
-            int64 saddle_id = int64.parse(id_text);
-
-            if ((yield resolve_bearer(http_context, saddles, saddle_id)) == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
-            }
-
-            var answers = new Json.Array();
-            var comments = new Json.Array();
-            try {
-                bool advanced = false;
-                bool advanced_known = false;
-                foreach (var pending in yield saddles.take_pending_answers(saddle_id)) {
-                    if (!advanced_known) {
-                        advanced_known = true;
-                        var saddle_row = yield saddles.get_saddle(saddle_id);
-                        advanced = saddle_row != null
-                            && yield tasks.branch_advanced_since(((!)saddle_row).current_task_id);
-                    }
-                    var element = new Json.Object();
-                    element.set_int_member("block_id", pending.block_id);
-                    element.set_string_member("answer", pending.answer);
-                    element.set_boolean_member("branch_advanced", advanced);
-                    answers.add_object_element(element);
-                }
-                foreach (var pending in yield saddles.take_pending_comments(saddle_id)) {
-                    var element = new Json.Object();
-                    element.set_int_member("comment_id", pending.comment_id);
-                    element.set_string_member("author", pending.author);
-                    element.set_string_member("body", pending.body);
-                    element.set_string_member("quote", pending.quote);
-                    comments.add_object_element(element);
-                }
-            } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
-            }
-
-            var body = new Json.Object();
-            body.set_array_member("answers", answers);
-            body.set_array_member("comments", comments);
-            return SaddleUpdateEndpoint.json_ok(body);
-        }
     }
 
     /**
@@ -321,7 +85,7 @@ namespace ClankerSaddle.Api {
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
             if ((yield resolve_bearer(http_context, saddles)) == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             string project_text = "";
@@ -357,9 +121,9 @@ namespace ClankerSaddle.Api {
                 }
                 var body = new Json.Object();
                 body.set_array_member("tasks", array);
-                return SaddleUpdateEndpoint.json_ok(body);
+                return SaddleWire.json_ok(body);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -380,7 +144,7 @@ namespace ClankerSaddle.Api {
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
             if ((yield resolve_bearer(http_context, saddles)) == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             string id_text = "";
@@ -390,7 +154,7 @@ namespace ClankerSaddle.Api {
             try {
                 var detail = yield tasks.get_task(task_id);
                 if (detail == null) {
-                    return SaddleUpdateEndpoint.json_error(404,
+                    return SaddleWire.json_error(404,
                         @"No task exists with id $(task_id)");
                 }
                 var task = ((!)detail).task;
@@ -412,9 +176,9 @@ namespace ClankerSaddle.Api {
                 element.set_string_member("goal_title", goal_title);
                 var body = new Json.Object();
                 body.set_object_member("task", element);
-                return SaddleUpdateEndpoint.json_ok(body);
+                return SaddleWire.json_ok(body);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -437,7 +201,7 @@ namespace ClankerSaddle.Api {
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
             if ((yield resolve_bearer(http_context, saddles)) == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             try {
@@ -464,9 +228,9 @@ namespace ClankerSaddle.Api {
                 }
                 var body = new Json.Object();
                 body.set_array_member("tags", tags);
-                return SaddleUpdateEndpoint.json_ok(body);
+                return SaddleWire.json_ok(body);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -493,14 +257,14 @@ namespace ClankerSaddle.Api {
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
             if ((yield resolve_bearer(http_context, saddles)) == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             string project_name = EntryJson.get_string_or(body, "project", "");
@@ -509,19 +273,19 @@ namespace ClankerSaddle.Api {
                 && body.get_member("task_id").get_value_type() == typeof(int64)
                 ? body.get_int_member("task_id") : 0;
             if (note_body.strip().length == 0) {
-                return SaddleUpdateEndpoint.json_error(400, "A note body is required");
+                return SaddleWire.json_error(400, "A note body is required");
             }
 
             try {
                 var project = yield projects.get_project_by_name(project_name);
                 if (project == null) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"No project exists named '$(project_name)'");
                 }
                 yield projects.add_note(((!)project).id, task_id, note_body);
-                return SaddleUpdateEndpoint.json_ok(new Json.Object());
+                return SaddleWire.json_ok(new Json.Object());
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -604,9 +368,9 @@ namespace ClankerSaddle.Api {
         public async HttpResult create(HttpContext http_context, Saddle saddle) throws Error {
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             try {
@@ -617,15 +381,15 @@ namespace ClankerSaddle.Api {
                 string title = EntryJson.get_string_or(body, "title", "");
                 string mandate = EntryJson.get_string_or(body, "mandate", "");
                 if (title.length == 0) {
-                    return SaddleUpdateEndpoint.json_error(400, "A title is required");
+                    return SaddleWire.json_error(400, "A title is required");
                 }
                 if (mandate.length == 0) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         "A mandate is required (heredoc body of `task create`)");
                 }
                 string mode = EntryJson.get_string_or(body, "mode", "implement");
                 if (!is_valid_mode(mode)) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"Unknown mode '$(mode)' — valid modes: "
                         + string.joinv(", ", TASK_MODES));
                 }
@@ -635,7 +399,7 @@ namespace ClankerSaddle.Api {
                 if (body.has_member("capabilities")) {
                     var member = body.get_member("capabilities");
                     if (member.get_node_type() != Json.NodeType.ARRAY) {
-                        return SaddleUpdateEndpoint.json_error(400,
+                        return SaddleWire.json_error(400,
                             "capabilities must be a JSON array of tag names");
                     }
                     foreach (var element in member.get_array().get_elements()) {
@@ -665,7 +429,7 @@ namespace ClankerSaddle.Api {
                         foreach (var capability in yield capabilities.list_capabilities()) {
                             valid.add(capability.name);
                         }
-                        return SaddleUpdateEndpoint.json_error(400,
+                        return SaddleWire.json_error(400,
                             @"Unknown capability tag(s): $(string.joinv(", ", unknown.to_array()))"
                             + @" — valid tags: $(string.joinv(", ", valid.to_array()))");
                     }
@@ -679,9 +443,9 @@ namespace ClankerSaddle.Api {
                 }
                 var response = new Json.Object();
                 response.set_int_member("task_id", task.id);
-                return SaddleUpdateEndpoint.json_ok(response);
+                return SaddleWire.json_ok(response);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
 
@@ -693,9 +457,9 @@ namespace ClankerSaddle.Api {
         public async HttpResult delete(HttpContext http_context, Saddle saddle) throws Error {
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             try {
@@ -708,21 +472,21 @@ namespace ClankerSaddle.Api {
                     ? body.get_int_member("task_id") : 0;
                 var detail = yield tasks.get_task(task_id);
                 if (detail == null) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"No task exists with id $(task_id)");
                 }
                 if (((!)detail).task.goal_id != goal_id) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"Task $(task_id) does not belong to your goal");
                 }
                 if (((!)detail).task.status != STATUS_DRAFT) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         "Only draft tasks can be deleted (cancel handles the rest)");
                 }
                 yield tasks.delete_task(task_id);
-                return SaddleUpdateEndpoint.json_ok(new Json.Object());
+                return SaddleWire.json_ok(new Json.Object());
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
 
@@ -736,9 +500,9 @@ namespace ClankerSaddle.Api {
         public async HttpResult enqueue(HttpContext http_context, Saddle saddle) throws Error {
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             try {
@@ -751,17 +515,17 @@ namespace ClankerSaddle.Api {
                     ? body.get_int_member("task_id") : 0;
                 var detail = yield tasks.get_task(task_id);
                 if (detail == null) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"No task exists with id $(task_id)");
                 }
                 if (((!)detail).task.goal_id != goal_id) {
-                    return SaddleUpdateEndpoint.json_error(400,
+                    return SaddleWire.json_error(400,
                         @"Task $(task_id) does not belong to your goal");
                 }
                 yield tasks.enqueue_task(task_id, CREATOR_GOAL);
-                return SaddleUpdateEndpoint.json_ok(new Json.Object());
+                return SaddleWire.json_ok(new Json.Object());
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -783,7 +547,7 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
             var callbacks = new SaddleTaskCallbacks(saddles, tasks, goals, capabilities, db);
             return yield callbacks.create(http_context, (!)saddle);
@@ -806,7 +570,7 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
             var callbacks = new SaddleTaskCallbacks(saddles, tasks, goals, capabilities, db);
             return yield callbacks.delete(http_context, (!)saddle);
@@ -829,7 +593,7 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
             var callbacks = new SaddleTaskCallbacks(saddles, tasks, goals, capabilities, db);
             return yield callbacks.enqueue(http_context, (!)saddle);
@@ -866,7 +630,7 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             // An ask from an offline saddle is proof of life: the daemon
@@ -893,7 +657,7 @@ namespace ClankerSaddle.Api {
             }
             poll_state.mark_ask_result(((!)saddle).id, assignment != null);
             if (error_text.length > 0) {
-                return SaddleUpdateEndpoint.json_error(500,
+                return SaddleWire.json_error(500,
                     @"Allocation failed: $(error_text)");
             }
             if (assignment == null) {
@@ -935,14 +699,14 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             int64 session_id = body.has_member("session_id")
@@ -950,13 +714,13 @@ namespace ClankerSaddle.Api {
                 ? body.get_int_member("session_id") : 0;
             var session = yield session_owned_by(db, session_id, (!)saddle);
             if (session == null) {
-                return SaddleUpdateEndpoint.json_error(400,
+                return SaddleWire.json_error(400,
                     @"No session $(session_id) exists on saddle $(((!)saddle).id)");
             }
 
             if (!(body.has_member("chunks")
                     && body.get_member("chunks").get_node_type() == Json.NodeType.ARRAY)) {
-                return SaddleUpdateEndpoint.json_error(400,
+                return SaddleWire.json_error(400,
                     "A chunks array is required");
             }
 
@@ -966,9 +730,9 @@ namespace ClankerSaddle.Api {
                     body.get_array_member("chunks"));
                 var response = new Json.Object();
                 response.set_int_member("stored", stored);
-                return SaddleUpdateEndpoint.json_ok(response);
+                return SaddleWire.json_ok(response);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
 
@@ -1024,14 +788,14 @@ namespace ClankerSaddle.Api {
                 RouteContext route_context) throws Error {
             var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
+                return SaddleWire.json_error(401, "Missing or invalid saddle token");
             }
 
             Json.Object body;
             try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
+                body = yield SaddleWire.read_json_body(http_context);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
 
             int64 session_id = body.has_member("session_id")
@@ -1039,13 +803,13 @@ namespace ClankerSaddle.Api {
                 ? body.get_int_member("session_id") : 0;
             var session = yield SaddleChunksEndpoint.session_owned_by(db, session_id, (!)saddle);
             if (session == null) {
-                return SaddleUpdateEndpoint.json_error(400,
+                return SaddleWire.json_error(400,
                     @"No session $(session_id) exists on saddle $(((!)saddle).id)");
             }
 
             string state = EntryJson.get_string_or(body, "state", "");
             if (state.length == 0) {
-                return SaddleUpdateEndpoint.json_error(400, "A state is required");
+                return SaddleWire.json_error(400, "A state is required");
             }
             Json.Object? block = body.has_member("block")
                 && body.get_member("block").get_node_type() == Json.NodeType.OBJECT
@@ -1063,9 +827,9 @@ namespace ClankerSaddle.Api {
                 }
                 yield saddles.apply_result_state((!)saddle, (!)session, state, block,
                     summary, tasks);
-                return SaddleUpdateEndpoint.json_ok(response);
+                return SaddleWire.json_ok(response);
             } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
+                return SaddleWire.json_error(400, e.message);
             }
         }
     }
@@ -1086,24 +850,14 @@ namespace ClankerSaddle.Api {
     /**
      * The ONE bearer resolution for the `/api/saddle/*` wire (plan 16
      * R2, arch item 26): the `Authorization: Bearer` credential
-     * resolved to its saddle — null fails the call. The two auth
-     * styles live here together: the {id}-anchored routes (update,
-     * poll) pin the credential to their route's saddle
-     * ({@link SaddleService.validate_token} — the token must be THAT
-     * saddle's); the id-less routes accept whichever registered
-     * saddle owns the token ({@link SaddleService.saddle_for_token}
-     * — the row token first, then any legacy api_tokens row; removing
-     * the saddle revokes both).
+     * resolved to its saddle — null fails the call. The {id}-anchored
+     * poll-era routes (update, poll) died with arch 19, so every
+     * remaining route is id-less and accepts whichever registered
+     * saddle owns the token ({@link SaddleService.saddle_for_token};
+     * removing the saddle revokes it).
      */
     private static async Saddle? resolve_bearer(HttpContext http_context,
-            SaddleService saddles, int64 anchored_saddle_id = 0) throws Error {
-        string token = bearer_token(http_context);
-        if (anchored_saddle_id > 0) {
-            if (yield saddles.validate_token(anchored_saddle_id, token)) {
-                return yield saddles.get_saddle(anchored_saddle_id);
-            }
-            return null;
-        }
-        return yield saddles.saddle_for_token(token);
+            SaddleService saddles) throws Error {
+        return yield saddles.saddle_for_token(bearer_token(http_context));
     }
 }

+ 0 - 2
src/Data/Entities.vala

@@ -67,7 +67,6 @@ namespace ClankerSaddle.Data {
         public string? description { get; set; default = null; }
         public string status { get; set; default = "draft"; }
         public int64 priority { get; set; default = 5; }
-        public string? working_branch { get; set; default = null; }
         public string? summary { get; set; default = null; }
         public string? task_branch { get; set; default = null; }
         public string? branch_from { get; set; default = null; }
@@ -94,7 +93,6 @@ namespace ClankerSaddle.Data {
                 .column<string>("description", o => o.description, (o, v) => o.description = v)
                 .column<string>("status", o => o.status, (o, v) => o.status = v)
                 .column<int64?>("priority", o => o.priority, (o, v) => o.priority = v)
-                .column<string>("working_branch", o => o.working_branch, (o, v) => o.working_branch = v)
                 .column<string>("summary", o => o.summary, (o, v) => o.summary = v)
                 .column<string>("task_branch", o => o.task_branch, (o, v) => o.task_branch = v)
                 .column<string>("branch_from", o => o.branch_from, (o, v) => o.branch_from = v)

+ 0 - 1
src/Data/Migrations/M0001_Initial.vala

@@ -157,7 +157,6 @@ namespace ClankerSaddle.Data.Migrations {
                     .indexed();
                 t.column<int64?>("priority")
                     .default_value<int64?>(5);
-                t.column<string>("working_branch");
                 t.column<string>("summary");
                 t.column<string>("task_branch");
                 t.column<string>("branch_from");

+ 1 - 1
src/Services/GoalService.vala

@@ -614,7 +614,7 @@ namespace ClankerSaddle.Services {
                 // Approving the completion closes the goal agent's
                 // own task: the run is finished, its saddle frees.
                 yield tasks.complete_task(((!)task).id,
-                    ((!)goal).completion_report, null, OUTCOME_DONE);
+                    ((!)goal).completion_report, OUTCOME_DONE);
                 push.notify_global(PushService.goal_terminal_notification(
                     ((!)goal).title, ((!)goal).id, true));
             }

+ 4 - 82
src/Services/SaddleService.vala

@@ -42,14 +42,6 @@ namespace ClankerSaddle.Services {
         public bool branch_advanced { get; set; }
     }
 
-    /** A human comment awaiting delivery to one saddle's conversation. */
-    public class PendingComment : Object {
-        public int64 comment_id { get; set; }
-        public string author { get; set; default = ""; }
-        public string body { get; set; default = ""; }
-        public string quote { get; set; default = ""; }
-    }
-
     /** A saddle registration plus the API token generated for it. */
     public class SaddleRegistration : Object {
         public Saddle saddle { get; set; }
@@ -219,32 +211,6 @@ namespace ClankerSaddle.Services {
             }
         }
 
-        /**
-         * True when `token` matches the credential of `saddle_id` — its
-         * per-saddle row token (plan 10), or any legacy `api_tokens`
-         * row still held for it (tokens minted before the column
-         * existed keep validating) — the credential check for the
-         * `/api/saddle/*` endpoints.
-         */
-        public async bool validate_token(int64 saddle_id, string token) throws Error {
-            if (token.length == 0) {
-                return false;
-            }
-            var saddle = yield get_saddle(saddle_id);
-            if (saddle != null && ((!)saddle).token == token) {
-                return true;
-            }
-            var rows = yield db.query<ApiToken>()
-                .where(expr("saddle_id == $0", elem<int64?>(saddle_id)))
-                .materialise_async();
-            foreach (var row in rows) {
-                if (row.token == token) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
         /**
          * The saddle a bearer token belongs to, or null — the
          * credential check for the id-less `/api/saddle/*` endpoints
@@ -1141,23 +1107,10 @@ namespace ClankerSaddle.Services {
             return newest_open;
         }
 
-        /**
-         * Answered blocks of the saddle's current task not yet handed over
-         * the poll endpoint; marks them delivered.
-         */
-        public async ImmutableLot<PendingAnswer> take_pending_answers(int64 saddle_id) throws Error {
-            var saddle = yield get_saddle(saddle_id);
-            if (saddle == null || ((!)saddle).current_task_id == 0) {
-                return new Vector<PendingAnswer>().to_immutable_buffer();
-            }
-            return yield take_pending_answers_for_task(((!)saddle).current_task_id);
-        }
-
         /**
          * Answered blocks of `task_id` not yet handed over anywhere;
-         * marks them delivered. Used both by the poll endpoint and by
-         * {@link push_task_to_saddle} (a resumed assignment carries its
-         * pending answers in the payload).
+         * marks them delivered — a resumed assignment carries its
+         * pending answers in the payload ({@link push_task_to_saddle}).
          */
         public async ImmutableLot<PendingAnswer> take_pending_answers_for_task(int64 task_id) throws Error {
             var blocks = yield db.query<TaskBlock>()
@@ -1179,37 +1132,6 @@ namespace ClankerSaddle.Services {
             return rows.to_immutable_buffer();
         }
 
-        /**
-         * Human comments of the saddle's current task not yet delivered to
-         * the daemon; marks them delivered (the poll response hands them
-         * over exactly once).
-         */
-        public async ImmutableLot<PendingComment> take_pending_comments(int64 saddle_id) throws Error {
-            var saddle = yield get_saddle(saddle_id);
-            if (saddle == null || ((!)saddle).current_task_id == 0) {
-                return new Vector<PendingComment>().to_immutable_buffer();
-            }
-            var comments = yield db.query<TaskComment>()
-                .where(expr("task_id == $0", elem<int64?>(((!)saddle).current_task_id)))
-                .order_by(expr("id"))
-                .materialise_async();
-            var rows = new Vector<PendingComment>();
-            foreach (var comment in comments) {
-                if (!comment.author.has_prefix("user:")
-                        || poll_state.is_comment_delivered(comment.id)) {
-                    continue;
-                }
-                poll_state.mark_comment_delivered(comment.id);
-                rows.add(new PendingComment() {
-                    comment_id = comment.id,
-                    author = comment.author,
-                    body = comment.body,
-                    quote = comment.quote ?? ""
-                });
-            }
-            return rows.to_immutable_buffer();
-        }
-
         /**
          * Pushes the task's undelivered human comments to the RUNNING
          * daemon's `POST /task-update {kind: "comment"}` endpoint so
@@ -1385,7 +1307,7 @@ namespace ClankerSaddle.Services {
                             string report = output.length > 0 ? output
                                 : strip_quotes(strip_verb(command, "saddle done"));
                             yield tasks.complete_task(task_id,
-                                report.length > 0 ? report : null, null, OUTCOME_DONE);
+                                report.length > 0 ? report : null, OUTCOME_DONE);
                         }
                         return true;
                     case "failed":
@@ -1671,7 +1593,7 @@ namespace ClankerSaddle.Services {
                         yield park_if_current(saddle.id, task_id);
                         break;
                     }
-                    yield tasks.complete_task(task_id, summary, null, OUTCOME_DONE);
+                    yield tasks.complete_task(task_id, summary, OUTCOME_DONE);
                     break;
                 case "failed":
                     string explanation = summary ?? "";

+ 14 - 18
src/Services/TaskService.vala

@@ -1096,12 +1096,11 @@ namespace ClankerSaddle.Services {
          * the status flip, the `completed_at` stamp and the summary
          * precedence — a non-empty `summary` replaces whatever the
          * row holds, else `empty_summary` applies only when the row
-         * holds nothing. `working_branch` records when non-empty (the
-         * done push's branch report). Callers keep their own
-         * idempotence guard (terminal states are sticky, plan 13 B2).
+         * holds nothing. Callers keep their own idempotence guard
+         * (terminal states are sticky, plan 13 B2).
          */
         private async void mark_terminal(TaskEntity task, string status, string? summary,
-                string? working_branch = null, string? empty_summary = null) throws Error {
+                string? empty_summary = null) throws Error {
             task.status = status;
             task.completed_at = new DateTime.now_local();
             if (summary != null && summary.length > 0) {
@@ -1109,9 +1108,6 @@ namespace ClankerSaddle.Services {
             } else if ((task.summary ?? "").length == 0 && empty_summary != null) {
                 task.summary = empty_summary;
             }
-            if (working_branch != null && working_branch.length > 0) {
-                task.working_branch = working_branch;
-            }
             yield db.update_async<TaskEntity>(task);
         }
 
@@ -1135,16 +1131,16 @@ namespace ClankerSaddle.Services {
         }
 
         /**
-         * Marks a task done: records `summary` and `working_branch`, stamps
-         * `completed_at`, closes any open session (outcome `outcome`) and
-         * frees the saddle that was running it. Terminal transitions
-         * project ONCE (plan 13 B2): a task already done or failed keeps
-         * its first outcome — a redelivered push or a second terminal
-         * entry with a fresh id re-runs no bookkeeping, no comment, no
-         * event (retry_task is the one path that un-terminals).
+         * Marks a task done: records `summary`, stamps `completed_at`,
+         * closes any open session (outcome `outcome`) and frees the
+         * saddle that was running it. Terminal transitions project ONCE
+         * (plan 13 B2): a task already done or failed keeps its first
+         * outcome — a redelivered push or a second terminal entry with
+         * a fresh id re-runs no bookkeeping, no comment, no event
+         * (retry_task is the one path that un-terminals).
          */
         public async void complete_task(int64 task_id, string? summary,
-                string? working_branch, string outcome) throws Error {
+                string outcome) throws Error {
             var task = yield db.query<TaskEntity>()
                 .where(expr("id == $0", elem<int64?>(task_id)))
                 .first_async();
@@ -1156,7 +1152,7 @@ namespace ClankerSaddle.Services {
                 return;
             }
 
-            yield mark_terminal((!)task, STATUS_DONE, summary, working_branch);
+            yield mark_terminal((!)task, STATUS_DONE, summary);
 
             yield release_task(task_id, outcome, false);
             yield pulse.bump(task_id, "task completed");
@@ -1193,7 +1189,7 @@ namespace ClankerSaddle.Services {
                     "Draft tasks are deleted, not cancelled");
             }
 
-            yield mark_terminal((!)task, STATUS_FAILED, null, null, "Cancelled by user");
+            yield mark_terminal((!)task, STATUS_FAILED, null, "Cancelled by user");
 
             yield release_task(task_id, OUTCOME_STOPPED, false);
             yield pulse.bump(task_id, "task cancelled");
@@ -1224,7 +1220,7 @@ namespace ClankerSaddle.Services {
                 return;
             }
 
-            yield mark_terminal((!)task, STATUS_FAILED, summary, null,
+            yield mark_terminal((!)task, STATUS_FAILED, summary,
                 "Saddle reported a failure");
 
             yield release_task(task_id, OUTCOME_FAILED, false);

+ 9 - 19
src/entrypoints/TaskDetailEntrypoint.vala

@@ -41,15 +41,11 @@ namespace ClankerSaddle {
     }
 
     /**
-     * A block (question, RFC, review or plan) with its answer state and
-     * answer action. {@link is_plan_review} marks the plan-acceptance
-     * block (`saddle plan` parks every task with a type=plan block
-     * "Plan submitted for review"), which renders the Accept /
-     * Reject-with-feedback flow instead of the plain reply form. While
-     * that section is showing it already renders the plan markdown
-     * (the block's body), so the block's own body preview — the same
-     * text — stays hidden ({@link show_body}); every other typed
-     * block (e.g. a question with a markdown body) keeps its preview.
+     * A block (question, RFC or review) with its answer state and
+     * answer action. A pending review hides its body preview
+     * ({@link show_body}) because the review banner above it renders
+     * the same markdown; every other typed block (e.g. a question
+     * with a markdown body) keeps its preview.
      */
     public class BlockView : Object {
         public int64 id { get; set; }
@@ -59,7 +55,6 @@ namespace ClankerSaddle {
         public string body { get; set; default = ""; }
         public string[] options { get; set; }
         public bool answered { get; set; }
-        public bool is_plan_review { get; set; }
         public bool show_body { get; set; default = true; }
         public string answered_by { get; set; default = ""; }
         public string created_display { get; set; default = ""; }
@@ -108,7 +103,6 @@ namespace ClankerSaddle {
         public string description { get; set; default = ""; }
         public string status { get; set; default = ""; }
         public int64 priority { get; set; default = 5; }
-        public string working_branch { get; set; default = ""; }
         public string summary { get; set; default = ""; }
         public string task_branch { get; set; default = ""; }
         public string branch_from { get; set; default = ""; }
@@ -212,7 +206,6 @@ namespace ClankerSaddle {
                 description = task.description ?? "",
                 status = task.status,
                 priority = task.priority,
-                working_branch = task.working_branch ?? "",
                 summary = task.summary ?? "",
                 task_branch = task.task_branch ?? "",
                 branch_from = task.branch_from ?? "",
@@ -334,7 +327,6 @@ namespace ClankerSaddle {
                     options = EntryJson.parse_string_array(block.options),
                     answered = block.answer != null,
                     answered_by = block.answered_by ?? "",
-                    is_plan_review = block.block_type == BLOCK_PLAN,
                     created_display = format_stamp(block.created_at),
                     answered_display = format_stamp(block.answered_at),
                     answer_action = actions.author_private<AnswerBlockAction, BlockAnswerPrivate>(block_private)
@@ -344,12 +336,10 @@ namespace ClankerSaddle {
                     pub.review_summary = view.body.length > 0 ? view.body : view.question;
                     pub.review_diff_url = yield tasks.diff_url_for_task(task.id);
                 }
-                // The pending plan-review and review sections each render
-                // the markdown themselves (task.plan / the review summary),
-                // so the block's own body preview — the same text — stays
-                // hidden until the block is answered.
-                view.show_body = (!view.is_plan_review && view.block_type != BLOCK_REVIEW)
-                    || view.answered;
+                // The pending review banner renders the markdown itself
+                // (the review summary), so the block's own body preview —
+                // the same text — stays hidden until the block is answered.
+                view.show_body = view.block_type != BLOCK_REVIEW || view.answered;
                 blocks_ascending.add(view);
             }
             var blocks = new Vector<BlockView>();

+ 0 - 4
src/main.vala

@@ -167,10 +167,6 @@ int main(string[] args) {
         // spry:auth-begin
         // spry:auth-end
 
-        application.add_endpoint<SaddleUpdateEndpoint>(
-            new EndpointRoute("/api/saddle/{id}/update", Method.POST));
-        application.add_endpoint<SaddlePollEndpoint>(
-            new EndpointRoute("/api/saddle/{id}/poll", Method.GET));
         application.add_endpoint<SaddleNextTaskEndpoint>(
             new EndpointRoute("/api/saddle/next-task", Method.POST));
         application.add_endpoint<SaddleChunksEndpoint>(

+ 3 - 202
src/pages/task_detail.html

@@ -8,9 +8,7 @@
     // entrypoint); whenever task or saddle state changes anywhere in
     // the app, the sealed refresh action rehydrates the task slot —
     // no fixed-interval polling. RFC bodies get the shared
-    // text-highlight -> comment popover (see main.html). Design-task
-    // plan reviews collect inline comments on the plan and submit them
-    // as the block answer.
+    // text-highlight -> comment popover (see main.html).
     if (window.statum) {
         (function () {
             'use strict';
@@ -41,8 +39,7 @@
             if (window.initQuoteComments) {
                 window.initQuoteComments({
                     matches: function (node) {
-                        return node.closest && node.closest('.rfc-body') != null
-                            && node.closest('[data-plan-body]') == null;
+                        return node.closest && node.closest('.rfc-body') != null;
                     },
                     action: function () {
                         var task = statum.read('task');
@@ -50,182 +47,6 @@
                     }
                 });
             }
-
-            // Plan-review feedback: collected comments live in these maps
-            // (keyed by block id) so statum re-renders never lose them;
-            // the DOM list is rebuilt after every render cycle.
-            var feedbackByBlock = {};
-            var modeByBlock = {};
-            var planPopover = null;
-
-            statum.afterEvaluate(syncPlanFeedback);
-
-            document.addEventListener('click', function (event) {
-                if (!event.target.closest) return;
-                var toggle = event.target.closest('[data-plan-toggle]');
-                if (toggle) {
-                    hidePlanPopover();
-                    var review = toggle.closest('[data-plan-review]');
-                    var id = review && review.getAttribute('data-block-id');
-                    if (id) modeByBlock[id] = !modeByBlock[id];
-                    syncPlanFeedback();
-                    return;
-                }
-                var submit = event.target.closest('[data-plan-submit]');
-                if (submit) {
-                    submitPlanFeedback(submit.closest('[data-plan-review]'));
-                    return;
-                }
-                var remove = event.target.closest('[data-plan-remove]');
-                if (remove) {
-                    var entry = remove.closest('[data-plan-comment-id]');
-                    var blockId = remove.closest('[data-plan-review]').getAttribute('data-block-id');
-                    var list = feedbackByBlock[blockId] || [];
-                    var index = parseInt(entry.getAttribute('data-plan-comment-id'), 10);
-                    if (!isNaN(index) && list[index]) list.splice(index, 1);
-                    feedbackByBlock[blockId] = list;
-                    syncPlanFeedback();
-                }
-            });
-
-            document.addEventListener('mouseup', function (event) {
-                if (!event.target.closest) return;
-                if (planPopover && planPopover.contains(event.target)) return;
-                var body = event.target.closest('[data-plan-body]');
-                if (!body) {
-                    hidePlanPopover();
-                    return;
-                }
-                var review = body.closest('[data-plan-review]');
-                if (!review || review.getAttribute('data-plan-open') !== '1') return;
-                var selection = window.getSelection();
-                var text = selection ? String(selection.toString()).trim() : '';
-                if (text.length === 0) {
-                    hidePlanPopover();
-                    return;
-                }
-                showPlanPopover(event.pageX, event.pageY, text, review);
-            });
-
-            document.addEventListener('mousedown', function (event) {
-                if (planPopover && event.target.closest && event.target.closest('.plan-popover')) return;
-                hidePlanPopover();
-            });
-
-            function showPlanPopover(x, y, quote, review) {
-                hidePlanPopover();
-                planPopover = document.createElement('div');
-                planPopover.className = 'plan-popover';
-                planPopover.style.left = Math.min(x, window.innerWidth - 140) + 'px';
-                planPopover.style.top = y + 'px';
-
-                var quoted = document.createElement('blockquote');
-                quoted.textContent = quote;
-                planPopover.appendChild(quoted);
-
-                var body = document.createElement('textarea');
-                body.placeholder = 'Your comment on the selected plan text';
-                planPopover.appendChild(body);
-
-                var actions = document.createElement('div');
-                actions.className = 'actions';
-                var add = document.createElement('button');
-                add.type = 'button';
-                add.className = 'primary';
-                add.textContent = 'Add comment';
-                add.addEventListener('click', function () {
-                    if (body.value.trim().length === 0) return;
-                    var id = review.getAttribute('data-block-id');
-                    var list = feedbackByBlock[id] || [];
-                    list.push({ quote: quote, body: body.value.trim() });
-                    feedbackByBlock[id] = list;
-                    hidePlanPopover();
-                    syncPlanFeedback();
-                });
-                var cancel = document.createElement('button');
-                cancel.type = 'button';
-                cancel.textContent = 'Cancel';
-                cancel.addEventListener('click', hidePlanPopover);
-                actions.appendChild(add);
-                actions.appendChild(cancel);
-                planPopover.appendChild(actions);
-
-                document.body.appendChild(planPopover);
-                body.focus();
-            }
-
-            function hidePlanPopover() {
-                if (planPopover) {
-                    planPopover.remove();
-                    planPopover = null;
-                }
-            }
-
-            function submitPlanFeedback(review) {
-                if (!review) return;
-                var id = review.getAttribute('data-block-id');
-                var list = feedbackByBlock[id] || [];
-                if (list.length === 0) {
-                    if (window.showToast) window.showToast('Highlight some plan text and add a comment first', 'error');
-                    return;
-                }
-                var task = statum.read('task');
-                var block = null;
-                (task && task.blocks || []).forEach(function (candidate) {
-                    if (String(candidate.id) === String(id)) block = candidate;
-                });
-                if (!block || !block.answer_action) return;
-                var answer = list.map(function (item) {
-                    return item.quote
-                        ? '> ' + item.quote.replace(/\n/g, '\n> ') + '\n\n' + item.body
-                        : item.body;
-                }).join('\n\n');
-                // Free-text feedback rides the `feedback` field so the
-                // action wraps it in the stable verdict wording (the
-                // answer IS the resume_notice the daemon reads).
-                var extra = review.querySelector('[data-plan-feedback] textarea[name="feedback"]');
-                if (extra && extra.value.trim().length > 0) {
-                    answer += '\n\n' + extra.value.trim();
-                }
-                feedbackByBlock[id] = [];
-                modeByBlock[id] = false;
-                statum.act(block.answer_action, { feedback: answer }).catch(function () { });
-            }
-
-            function syncPlanFeedback() {
-                document.querySelectorAll('[data-plan-review]').forEach(function (review) {
-                    var id = review.getAttribute('data-block-id');
-                    var open = modeByBlock[id] ? '1' : '0';
-                    review.setAttribute('data-plan-open', open);
-                    var feedback = review.querySelector('[data-plan-feedback]');
-                    if (feedback) {
-                        if (modeByBlock[id]) feedback.removeAttribute('hidden');
-                        else feedback.setAttribute('hidden', '');
-                    }
-                    var list = review.querySelector('[data-plan-comments]');
-                    if (!list) return;
-                    while (list.firstChild) list.removeChild(list.firstChild);
-                    (feedbackByBlock[id] || []).forEach(function (item, index) {
-                        var entry = document.createElement('li');
-                        entry.setAttribute('data-plan-comment-id', String(index));
-                        if (item.quote) {
-                            var quoted = document.createElement('blockquote');
-                            quoted.textContent = item.quote;
-                            entry.appendChild(quoted);
-                        }
-                        var body = document.createElement('div');
-                        body.textContent = item.body;
-                        entry.appendChild(body);
-                        var remove = document.createElement('button');
-                        remove.type = 'button';
-                        remove.className = 'inline';
-                        remove.setAttribute('data-plan-remove', '');
-                        remove.textContent = 'Remove';
-                        entry.appendChild(remove);
-                        list.appendChild(entry);
-                    });
-                });
-            }
         })();
     }
 </script>
@@ -303,7 +124,6 @@
                     <p class="muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
                     <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
                         <span class="muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
-                    <p stm-if="task.working_branch.length > 0" class="muted">Working branch: <span stm-text="task.working_branch">branch</span></p>
                     <p stm-if="task.status == 'suspended'" class="muted">Parked (`saddle suspend`) —
                         the goal agent resumes on its goal's task events, with the
                         batched status report riding the resume.</p>
@@ -435,25 +255,6 @@
                     </div>
                 </div>
 
-                <div stm-if="!block.answered && block.is_plan_review" class="plan-review"
-                     data-plan-review stm-attribute.data-block-id="block.id">
-                    <div class="md rfc-body" stm-markdown="block.body" data-plan-body>plan</div>
-                    <div class="actions">
-                        <form stm-action="block.answer_action" class="inline">
-                            <input type="hidden" name="verdict" value="accept">
-                            <button type="submit" class="approve">Accept plan</button>
-                        </form>
-                        <button type="button" data-plan-toggle>Reject with feedback</button>
-                    </div>
-                    <div data-plan-feedback hidden>
-                        <p class="muted">Highlight plan text to attach a comment to it, then submit everything
-                            as feedback — the agent revises the plan and asks again.</p>
-                        <ul class="plan-comments" data-plan-comments></ul>
-                        <textarea name="feedback" rows="2" placeholder="Additional feedback (optional)"></textarea>
-                        <button type="button" class="primary" data-plan-submit>Submit feedback</button>
-                    </div>
-                </div>
-
                 <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
                     <span class="muted" stm-text="block.created_display">when</span></p>
 
@@ -481,7 +282,7 @@
                     <button type="submit" class="primary">Answer</button>
                 </form>
 
-                <form stm-if="!block.answered && block.block_type != 'question' && block.block_type != 'review' && !block.is_plan_review"
+                <form stm-if="!block.answered && block.block_type != 'question' && block.block_type != 'review'"
                       stm-action="block.answer_action" class="inline">
                     <textarea name="answer" rows="3" placeholder="Your reply (delivered to the agent)"></textarea>
                     <button type="submit" class="primary">Reply</button>

+ 147 - 151
test/web_stack_test.py

@@ -1244,21 +1244,47 @@ def start_bundle(name):
 
 # =============================================================================
 # Bundle: pages — block presentation, diff URLs, computed display state,
-# terminal rendering (rejected entries, ANSI SGR), task-page layout and
-# the /poll verdict channel. Fixture-driven: tasks and sessions are
-# inserted as rows; only the rejected-ride scenario dispatches.
+# terminal rendering (rejected entries, ANSI SGR) and task-page layout.
+# Fixture-driven: tasks and sessions are inserted as rows; blocks are
+# raised through the push-era wire (POST /api/saddle/results with a
+# blocked state) or inserted directly for terminal-task fixtures; only
+# the rejected-ride scenario dispatches.
 # =============================================================================
 
 def bundle_pages(ctx):
     auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
     app = ctx.app
 
+    def raise_block(session_id, question, kind="question", options=None, body=None):
+        """The push-era block raise: a blocked round-results push
+        carrying the block payload (the poll-era /update channel is
+        gone — 404 below pins that)."""
+        block = {"question": question, "type": kind}
+        if options is not None:
+            block["options"] = options
+        if body is not None:
+            block["body"] = body
+        return saddle_api("POST", "/api/saddle/results", fake_token,
+                          {"session_id": session_id, "state": "blocked",
+                           "block": block})
+
     check(True, "login yields an auth slot")
-    print("== plan blocks: every task's saddle plan parks for acceptance ==")
+
+    print("== the poll-era wire is gone (arch 19) ==")
+    # The daemon never called update/poll (verified against saddle/src);
+    # the routes 404 for any caller, authenticated or not.
+    status, _ = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
+                           {"type": "status", "session_id": 1,
+                            "data": {"state": "working"}})
+    check(status == 404, "POST /api/saddle/{id}/update is gone (404)")
+    status, _ = saddle_api("GET", "/api/saddle/%d/poll" % fake_id, fake_token)
+    check(status == 404, "GET /api/saddle/{id}/poll is gone (404)")
+
+    print("== blocks: push-era raises + body presentation ==")
     conn = db()
     conn.execute("insert into tasks (project_id, title, status, priority,"
                  " created_at)"
-                 " values (1, 'Plan review task', 'working', 5,"
+                 " values (1, 'Block display task', 'working', 5,"
                  " strftime('%s','now'))")
     plan_task = conn.execute("select max(id) from tasks").fetchone()[0]
     conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at, outcome)"
@@ -1268,41 +1294,9 @@ def bundle_pages(ctx):
     session_id = conn.execute("select max(id) from sessions").fetchone()[0]
     conn.close()
 
-    status, body = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                              {"type": "block", "session_id": session_id, "data": {
-                                  "question": "Plan submitted for review",
-                                  "type": "plan",
-                                  "body": "# The plan\n\n1. Do it"}})
-    check(status == 200 and body.get("block_id"), "plan block raised")
-    status, _ = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                           {"type": "block", "session_id": session_id, "data": {
-                               "question": "Which database?",
-                               "type": "question",
-                               "options": ["SQLite", "Postgres"],
-                               "body": "# Storage choice"}})
-    check(status == 200, "question block raised")
-
-    detail = task_state(auth, plan_task)
-    blocks = {b["block_type"]: b for b in detail["blocks"]}
-    check(blocks["plan"].get("is_plan_review") is True,
-          "plan block flagged as the plan review")
-    check(blocks["plan"].get("show_body") is False,
-          "pending plan review hides the duplicate body preview")
-    check(blocks["question"].get("show_body") is True,
-          "question block keeps its body preview")
-
-    # Accept stores the stable approval wording as the answer (it
-    # becomes the resume notice the daemon reads; task-level plans are
-    # gone — the goal owns the approved markdown). Plan 16 S2/R4: the
-    # form posts the verdict MARKER — the server owns the constant.
-    act(auth, blocks["plan"]["answer_action"], {"verdict": "accept"})
-    detail = task_state(auth, plan_task)
-    plan_block = [b for b in detail["blocks"] if b["block_type"] == "plan"][0]
-    check(plan_block.get("show_body") is True,
-          "answered plan review shows the body again (review section gone)")
-    check(plan_block.get("answered") is True
-          and plan_block.get("answer") == "Plan approved. Proceed with implementation.",
-          "verdict=accept stores the stable approval wording server-side (the resume notice)")
+    status, _ = raise_block(session_id, "Which database?", kind="question",
+                            options=["SQLite", "Postgres"], body="# Storage choice")
+    check(status == 200, "question block raised through the results push")
 
     print("== B1: highlight-to-comment quote dialog ==")
     status, page = http("GET", "/tasks/%d" % plan_task, timeout=15)
@@ -1315,12 +1309,9 @@ def bundle_pages(ctx):
           "terminal page keeps its quote dialog")
 
     print("== B3: review section hides the duplicate body ==")
-    status, body = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                              {"type": "block", "session_id": session_id, "data": {
-                                  "question": "Review the diff",
-                                  "type": "review",
-                                  "body": "# The change\n\nReworks the flange module."}})
-    check(status == 200 and body.get("block_id"), "review block raised")
+    status, _ = raise_block(session_id, "Review the diff", kind="review",
+                            body="# The change\n\nReworks the flange module.")
+    check(status == 200, "review block raised through the results push")
     detail = task_state(auth, plan_task)
     review_block = [b for b in detail["blocks"] if b["block_type"] == "review"][0]
     check(detail.get("review_pending") is True,
@@ -1352,11 +1343,8 @@ def bundle_pages(ctx):
           "the review verdict keeps Request changes with free-text feedback (U1)")
     check('value="__other__"' in task_page_html and 'name="other"' in task_page_html,
           "question blocks render the option/other question form (U1)")
-    status, body = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                              {"type": "block", "session_id": session_id, "data": {
-                                  "question": "Free-text thought?", "type": "question"}})
-    check(status == 200 and body.get("block_id"),
-          "a NULL-options question block raises")
+    status, _ = raise_block(session_id, "Free-text thought?", kind="question")
+    check(status == 200, "a NULL-options question block raises")
     bare_question = [b for b in task_state(auth, plan_task)["blocks"]
                      if b["question"] == "Free-text thought?"][0]
     check(bare_question.get("options") == []
@@ -1404,9 +1392,7 @@ def bundle_pages(ctx):
         conn.commit()
         session = conn.execute("select max(id) from sessions").fetchone()[0]
         conn.close()
-        saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                   {"type": "block", "session_id": session, "data": {
-                       "question": "Review the diff", "type": "review"}})
+        status, _ = raise_block(session, "Review the diff", kind="review")
         return task_state(auth, task_id).get("review_diff_url") or ""
 
     check(diff_url_for_project("ssh://fabrica.unitatem.net:222/clanker/clanker-saddle.git",
@@ -1463,15 +1449,13 @@ def bundle_pages(ctx):
                  " values (1, 'Parent', 'done', 5, strftime('%s','now'))")
     parent6 = conn.execute("select max(id) from tasks").fetchone()[0]
     # The post-mortem case: stored done WITH an unanswered review block.
-    conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at, outcome)"
-                 " values (?, ?, strftime('%s','now'), strftime('%s','now'), 'done')",
-                 (parent6, fake_id))
-    session6 = conn.execute("select max(id) from sessions").fetchone()[0]
+    # (A terminal task consumes result pushes silently — plan 13 B2 —
+    # so the row is inserted directly, the goalui deep-link pattern.)
+    conn.execute("insert into task_blocks (task_id, question, type, created_at)"
+                 " values (?, 'Review the final diff', 'review',"
+                 " strftime('%s','now'))", (parent6,))
     conn.commit()
     conn.close()
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": session6, "data": {
-                   "question": "Review the final diff", "type": "review"}})
 
     detail = task_state(auth, parent6)
     check(detail.get("display") == "review requested" and detail.get("terminal") is True,
@@ -1505,15 +1489,17 @@ def bundle_pages(ctx):
           "failed task with answered review shows failed + terminal flags")
 
     print("== rejected entries (non-fatal) ==")
-    # Ingest API: a rejected entry stores on the session and streams
-    # through SSE; the task is untouched.
-    status, _ = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                           {"type": "output", "session_id": session_id, "data": {
-                               "command": "bash -n script.sh",
-                               "output": "syntax error near line 3 (recoverable)",
-                               "exit_code": 1,
-                               "type": "rejected"}})
-    check(status == 200, "rejected entry accepted by the ingest API")
+    # The push wire: a rejected entry rides a round-results push, stores
+    # on the session and streams through SSE; the task is untouched.
+    status, _ = saddle_api("POST", "/api/saddle/results", fake_token, {
+        "session_id": session_id, "state": "working",
+        "entries": [{
+            "id": 9021, "command": "bash -n script.sh",
+            "output": "syntax error near line 3 (recoverable)",
+            "exit_code": 1, "type": "rejected",
+            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+        }]})
+    check(status == 200, "a rejected entry rides the results push")
     request = urllib.request.Request(WEB + "/terminal-stream/%d" % session_id)
     with urllib.request.urlopen(request, timeout=15) as stream:
         rejected_event = None
@@ -1769,12 +1755,8 @@ def bundle_pages(ctx):
     conn.commit()
     split_session = conn.execute("select max(id) from sessions").fetchone()[0]
     conn.close()
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": split_session, "data": {
-                   "question": "Open question for layout?", "type": "question"}})
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": split_session, "data": {
-                   "question": "Answered question for layout?", "type": "question"}})
+    raise_block(split_session, "Open question for layout?", kind="question")
+    raise_block(split_session, "Answered question for layout?", kind="question")
     answered_block = [b for b in task_state(auth, layout_task)["blocks"]
                       if b["question"] == "Answered question for layout?"][0]
     act(auth, answered_block["answer_action"], {"answer": "Yes"})
@@ -1911,52 +1893,9 @@ def bundle_pages(ctx):
           and "badge-yeehaw" in home_page,
           "the dashboard renders the badge on timeline task references")
 
-    print("== R10: branch_advanced on the /poll verdict channel ==")
-    conn = db()
-    conn.execute("insert into tasks (project_id, title, status, priority,"
-                 " task_branch, created_at) values (1, 'Poll answer parent',"
-                 " 'blocked', 5, 'shared/poll-branch', strftime('%s','now','-120 seconds'))")
-    poll_parent = conn.execute("select max(id) from tasks").fetchone()[0]
-    conn.execute("insert into sessions (task_id, saddle_id, started_at)"
-                 " values (?, ?, strftime('%s','now','-120 seconds'))",
-                 (poll_parent, fake_id))
-    conn.execute("insert into task_blocks (task_id, question, type, answer,"
-                 " answered_at, created_at) values (?, 'Which DB?', 'question',"
-                 " 'SQLite', strftime('%s','now','-60 seconds'),"
-                 " strftime('%s','now','-100 seconds'))", (poll_parent,))
-    conn.execute("update saddles set status = 'busy', current_task_id = ?,"
-                 " allocated_at = strftime('%s','now') where name = 'fake-saddle'",
-                 (poll_parent,))
-    conn.commit()
-    conn.close()
-    status, body = saddle_api("GET", "/api/saddle/%d/poll" % fake_id, fake_token)
-    check(status == 200 and len(body.get("answers", [])) == 1
-          and body["answers"][0].get("branch_advanced") is False,
-          "/poll verdicts carry branch_advanced=false with no siblings")
-
-    # A sibling on the same shared working branch completed after the
-    # park marker (plan 12: siblings are branch- or goal-mates).
-    conn = db()
-    conn.execute("insert into tasks (project_id, title, status, priority,"
-                 " task_branch, summary, completed_at, created_at)"
-                 " values (1, 'Poll sibling', 'done', 5, 'shared/poll-branch',"
-                 " 'sibling finished', strftime('%s','now'),"
-                 " strftime('%s','now','-200 seconds'))")
-    conn.execute("insert into task_blocks (task_id, question, type, answer,"
-                 " answered_at, created_at) values (?, 'Second question?',"
-                 " 'question', 'Yes', strftime('%s','now'),"
-                 " strftime('%s','now','-90 seconds'))", (poll_parent,))
-    conn.commit()
-    conn.close()
-    status, body = saddle_api("GET", "/api/saddle/%d/poll" % fake_id, fake_token)
-    check(status == 200 and len(body.get("answers", [])) == 1
-          and body["answers"][0].get("branch_advanced") is True,
-          "sibling completion after park flips branch_advanced to true")
-
-    # Release the staged fixture: cancel it so its busy@task row does
-    # not linger into the sections below.
-    act(auth, task_state(auth, poll_parent)["cancel"], {})
-
+    # (The old R10 scenario — branch_advanced riding /poll verdicts —
+    # died with the poll channel; the answers bundle pins the hint on
+    # its surviving surface, the resumed assignment payload.)
 
     print("== plan 12: the web surfaces — pile, timeline, queue, task page ==")
     # A goal parked in plan review feeds the unified dashboard pile;
@@ -2861,6 +2800,9 @@ def bundle_subtasks(ctx):
                               {"parent_task_id": plain, "title": "ghost"})
     check(status == 404,
           "the /api/saddle/subtask callback route is gone (404)")
+    # The poll-era ingest channel went with it (arch 19): the whole
+    # /update surface 404s — subtask and plan verb arms included — and
+    # the verdict channel (/poll) with it.
     conn = db()
     plain_session = conn.execute(
         "select id from sessions where task_id = ? and ended_at is null",
@@ -2869,14 +2811,13 @@ def bundle_subtasks(ctx):
     status, body = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
                               {"type": "subtask", "session_id": plain_session,
                                "data": {"title": "Ghost child"}})
-    check(status == 400 and "subtask" in (body.get("error") or "")
-          and "gone" in (body.get("error") or ""),
-          "the 'subtask' update type errors verbatim (%s)" % body.get("error"))
+    check(status == 404,
+          "the /update ingest surface is gone (404, the subtask arm with it)")
     status, body = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
                               {"type": "plan", "session_id": plain_session,
                                "data": {"plan": "# nope"}})
-    check(status == 400 and "plan" in (body.get("error") or ""),
-          "the task-level 'plan' update type errors verbatim (%s)" % body.get("error"))
+    check(status == 404,
+          "the task-level 'plan' update arm is gone with the channel (404)")
     # The daemon's task CLI: no parent/plan members; goal context instead.
     status, body = saddle_api("GET", "/api/saddle/task/%d" % plain, fake_token)
     check(status == 200 and "plan" not in body.get("task", {})
@@ -3726,12 +3667,27 @@ def bundle_allocation(ctx):
     dash_session = conn.execute("select max(id) from sessions").fetchone()[0]
     conn.commit()
     conn.close()
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": dash_session, "data": {
-                   "question": "Dash fixture question?", "type": "question"}})
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "review", "session_id": dash_session, "data": {
-                   "summary": "dash fixture review"}})
+    def raise_on_dash(question, kind="question", body=None, summary=None):
+        """Push-era raises on the dashboard fixture task (the poll-era
+        /update channel is gone): a blocked results push carries the
+        block payload; a review verb rides a review entry."""
+        if kind == "review" and summary is not None:
+            return saddle_api("POST", "/api/saddle/results", fake_token, {
+                "session_id": dash_session, "state": "working",
+                "entries": [{"id": 9317,
+                             "command": "saddle review",
+                             "output": summary, "type": "review",
+                             "timestamp": time.strftime(
+                                 "%Y-%m-%dT%H:%M:%SZ", time.gmtime())}]})[0]
+        block = {"question": question, "type": kind}
+        if body is not None:
+            block["body"] = body
+        return saddle_api("POST", "/api/saddle/results", fake_token, {
+            "session_id": dash_session, "state": "blocked",
+            "block": block})[0]
+
+    raise_on_dash("Dash fixture question?", kind="question")
+    raise_on_dash("dash fixture review", kind="review", summary="dash fixture review")
 
     # The activity timeline: last <= 50 events, newest first, fed
     # from the existing tables. Fixture rows a few seconds old
@@ -3755,10 +3711,8 @@ def bundle_allocation(ctx):
     fixtures_epoch = int(time.time())
     wait_for(lambda: int(time.time()) > fixtures_epoch or False,
              timeout=3, what="next clock second before the probe raise")
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": dash_session, "data": {
-                   "question": "Timeline probe: approve the dashboard flow",
-                   "type": "rfc", "body": "# Probe"}})
+    raise_on_dash("Timeline probe: approve the dashboard flow",
+                  kind="rfc", body="# Probe")
     dash = slot_public(auth, "/", "dashboard")
     pinned = dash.get("pinned", [])
     pinned_ids = [int(b["id"]) for b in pinned]
@@ -4014,12 +3968,12 @@ def bundle_lifecycle(ctx):
         (locked_task,)).fetchone()
     conn.close()
     if caplock_session is not None:
-        status, _ = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-                               {"type": "status", "session_id": caplock_session[0],
-                                "data": {"state": "queued"}})
+        status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
+                               {"session_id": caplock_session[0],
+                                "state": "queued"})
         check(status == 200
               and task_state(auth, locked_task)["status"] in ("working", "done"),
-              "the ingest API's status push never queues a task")
+              "a queued state push is a no-op (only the wire's states drive bookkeeping)")
 
     # Delete guards: a clean draft deletes; non-draft refuses; a draft
     # with dependents is refused naming them.
@@ -4693,6 +4647,29 @@ def bundle_answers(ctx):
 
     detail = task_state(auth, parked_task)
     db_block = [b for b in detail["blocks"] if b["question"] == "Which database?"][0]
+    # branch_advanced on the resume assignment (the /poll verdict
+    # channel's hint lives here now): a sibling on the same shared
+    # working branch completed strictly after the block's park marker
+    # — the resumed allocation must report branch_advanced=true so the
+    # daemon knows to pull before continuing.
+    conn = db()
+    parked_block_created = conn.execute(
+        "select created_at from task_blocks where id = ?", (db_block["id"],)
+    ).fetchone()[0]
+    conn.execute("update tasks set task_branch = 'shared/resume-branch'"
+                 " where id = ?", (parked_task,))
+    conn.commit()
+    conn.close()
+    wait_for(lambda: int(time.time()) > parked_block_created or False,
+             timeout=3, what="next clock second past the park marker")
+    conn = db()
+    conn.execute("insert into tasks (project_id, title, status, priority,"
+                 " task_branch, summary, completed_at, created_at)"
+                 " values (1, 'Resume sibling', 'done', 5, 'shared/resume-branch',"
+                 " 'sibling finished', strftime('%s','now'),"
+                 " strftime('%s','now','-300 seconds'))")
+    conn.commit()
+    conn.close()
     act(auth, db_block["answer_action"], {"option": "SQLite"})
     check(wait_for(lambda: len(parked.wake_log) > 0 or False,
                    timeout=30, what="wake after the answer") is not None,
@@ -4701,9 +4678,11 @@ def bundle_answers(ctx):
         int(p.get("task", {}).get("id") or 0) == parked_task
         and p.get("session_id") != parked_session
         and p.get("resume_notice") == "Human selected: SQLite"
+        and p.get("branch_advanced") is True
         for p in parked.start_payloads[1:]) or False,
         timeout=30, what="parked resume allocation") is not None,
-          "the parked daemon's ask allocates the resume — fresh session, formatted notice")
+          "the parked daemon's ask allocates the resume — fresh session, formatted notice,"
+          " branch_advanced=true after the sibling's completion")
     # The hold_state daemon drives its round manually: the resumed
     # round completes on the fresh session row.
     parked.push_results("done", summary="parked round resumed and finished")
@@ -5194,21 +5173,40 @@ def bundle_push(ctx):
                    what="410 endpoint pruned") is not None,
           "a 410 Gone endpoint is pruned from push_subscriptions")
 
-    # Block events go to EVERY push-subscribed user globally.
+    # Block events go to EVERY push-subscribed user globally. The
+    # poll-era /update raise is gone: a live (non-terminal) host task
+    # with an open session on the fake carries the block through the
+    # results push — task1 is done and terminal pushes are consumed
+    # silently, so the host is its own fixture row.
+    conn = db()
+    conn.execute("insert into tasks (project_id, title, status, priority,"
+                 " created_at) values (1, 'Push block host', 'working', 5,"
+                 " strftime('%s','now'))")
+    push_host = conn.execute("select max(id) from tasks").fetchone()[0]
+    conn.execute("insert into sessions (task_id, saddle_id, started_at)"
+                 " values (?, ?, strftime('%s','now'))", (push_host, fake_id))
+    push_host_session = conn.execute(
+        "select max(id) from sessions").fetchone()[0]
+    conn.commit()
+    conn.close()
+
+    def raise_global_block(question):
+        return saddle_api("POST", "/api/saddle/results", fake_token, {
+            "session_id": push_host_session, "state": "blocked",
+            "block": {"question": question, "type": "question"}})
+
     main_before = len(push.hits("/main"))
     other_before = len(push.hits("/other"))
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": task1_session, "data": {
-                   "question": "Global question for everyone?", "type": "question"}})
+    raise_global_block("Global question for everyone?")
     check(wait_for(lambda: len(push.hits("/main")) > main_before
                    and len(push.hits("/other")) > other_before, timeout=15,
                    what="global block push") is not None,
           "a raised block notifies every push-subscribed user globally")
     block_hit = push.hits("/other")[-1]
     _, payload = decrypt_webpush(block_hit, other_browser, other_public, other_auth)
-    check(payload["title"] == "Push lifecycle task — question raised"
+    check(payload["title"] == "Push block host — question raised"
           and payload["body"] == "Global question for everyone?"
-          and payload["url"] == "/tasks/%d" % task1,
+          and payload["url"] == "/tasks/%d" % push_host,
           "the block payload carries task title + kind, question and click URL (%s)"
           % payload)
     ciphertext_len = len(block_hit["body"]) - (21 + 65)
@@ -5269,9 +5267,7 @@ def bundle_push(ctx):
         {"endpoint": endpoint_flaky, **subscribe_fields})
     push.fail_next["/flaky"] = 1
     flaky_before = len(push.hits("/flaky"))
-    saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
-               {"type": "block", "session_id": task1_session, "data": {
-                   "question": "Flaky delivery question?", "type": "question"}})
+    raise_global_block("Flaky delivery question?")
     check(wait_for(lambda: len(push.hits("/flaky")) >= flaky_before + 2,
                    timeout=20, what="flaky endpoint retried") is not None,
           "a failed send retries on the next dispatcher tick and lands")