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

goals: flatten — subtask web machinery and task plans deleted

The plan-12 flattening commit. The migration loses tasks.parent_task_id,
the plan column, the three auto-* columns and the task_dependencies
table; the entities follow (TaskDependency gone, TaskEntity is the
goal-shaped row). Deleted web machinery: the SubtaskEventQueue and its
delivery, the /api/saddle/subtask callback and the entry-poll subtask/
plan update types (both now error verbatim naming the removal), the
done/suspend subtask guards' status channel, subtask auto-approve and
plan auto-approve (blocks always wait for a human), waiting-on-drafts
(push + dashboard), task plans (update_plan, the plan editor and the
task-page plan blocks), dependency edges and the delegating/
waiting-on-subtasks display states — a parked plain task now stays
parked until a human acts; only goal events resume goal agents. The
daemon's task CLI endpoints drop the parent/plan members and carry
goal_id/goal_title instead. Tests reworked goal-era: the subtasks
bundle covers the deleted-machinery refusals plus the goal agent's
task CLI in depth; lineage covers goal context on dispatches and the
task-reading API; allocation's affinity phase pins a parked GOAL agent
to its workspace; lifecycle swaps dependency edges for the
auto-enqueue-after predecessor (enqueue allowed while pending,
dispatch held, dangling references cleared on delete); answers drops
plan auto-approve; push drops waiting-on-drafts; pages gains the
web-surfaces phase (pile, timeline, queue filters, terminal picker)
rebalanced out of the goals shard.
clanker 1 сар өмнө
parent
commit
fa78f8a5bf

+ 0 - 1
meson.build

@@ -133,7 +133,6 @@ app_sources = files(
     'src/Services/Dispatcher.vala',
     'src/Services/RuleService.vala',
     'src/Services/CapabilityService.vala',
-    'src/Services/SubtaskEventQueue.vala',
     'src/Services/TimelineService.vala',
     'src/Services/PushService.vala',
     'src/Api/SaddleApi.vala',

+ 31 - 216
src/Api/SaddleApi.vala

@@ -23,11 +23,14 @@ namespace ClankerSaddle.Api {
      * 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`), `subtask`, `plan`, `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.
+     * 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.
      */
     public class SaddleUpdateEndpoint : Object, Endpoint {
 
@@ -96,32 +99,19 @@ namespace ClankerSaddle.Api {
                         get_string_or(data, "body", null));
                     yield tasks.log_session_entry(task_id, "block",
                         @"block: $(block.question)", block.body);
-                    // The instant-approve levers answer covered review
-                    // and plan blocks web-side the moment they arrive.
-                    yield tasks.auto_answer_if_covered(block);
                     var block_response = new Json.Object();
                     block_response.set_int_member("block_id", block.id);
                     return block_response;
 
                 case "subtask":
-                    // Dedup against the synchronous callback: a subtask
-                    // row already created for this parent/title (the
-                    // callback path) is not duplicated — the entry log
-                    // is the fallback, not a second source of truth.
-                    string subtask_title = get_string_or(data, "title", "Untitled subtask");
-                    var existing_subtask = yield tasks.find_subtask_by_title(task_id, subtask_title);
-                    if (existing_subtask == null) {
-                        yield tasks.create_subtask(task_id, subtask_title,
-                            get_string_or(data, "description", null));
-                    }
-                    break;
-
                 case "plan":
-                    yield tasks.update_plan(task_id, get_string_or(data, "plan",
-                        get_string_or(data, "markdown", "")));
-                    yield tasks.log_session_entry(task_id, "plan", "saddle plan",
-                        get_string_or(data, "plan", get_string_or(data, "markdown", "")));
-                    break;
+                    // 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 = get_string_or(data, "summary",
@@ -134,7 +124,6 @@ namespace ClankerSaddle.Api {
                     string review_summary = 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);
-                    yield tasks.auto_answer_if_covered(review_block);
                     break;
 
                 case "done":
@@ -411,8 +400,7 @@ namespace ClankerSaddle.Api {
                 foreach (var task in rows) {
                     if (q.length > 0) {
                         string haystack = (task.title + "\n"
-                            + (task.description ?? "") + "\n"
-                            + (task.plan ?? "")).down();
+                            + (task.description ?? "")).down();
                         if (!(q in haystack)) {
                             continue;
                         }
@@ -421,7 +409,7 @@ namespace ClankerSaddle.Api {
                     element.set_int_member("id", task.id);
                     element.set_string_member("title", task.title);
                     element.set_string_member("status", task.status);
-                    element.set_int_member("parent_task_id", task.parent_task_id);
+                    element.set_int_member("goal_id", task.goal_id);
                     array.add_object_element(element);
                 }
                 var body = new Json.Object();
@@ -436,13 +424,15 @@ namespace ClankerSaddle.Api {
     /**
      * `GET /api/saddle/task/{id}` — the single-task view behind the
      * daemon's `saddle task` CLI (wire contract): per-saddle-token
-     * authenticated (plan 10), joining the parent's title for lineage
-     * context. 404 when the task does not exist.
+     * authenticated (plan 10), joining the goal's title for context
+     * (plan 12 — the parent/plan members are gone). 404 when the
+     * task does not exist.
      */
     public class SaddleTaskDetailEndpoint : Object, Endpoint {
 
         private SaddleService saddles = inject<SaddleService>();
         private TaskService tasks = inject<TaskService>();
+        private InvercargillSql.Orm.OrmSession db = inject<InvercargillSql.Orm.OrmSession>();
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
@@ -461,20 +451,22 @@ namespace ClankerSaddle.Api {
                         @"No task exists with id $(task_id)");
                 }
                 var task = ((!)detail).task;
-                string parent_title = "";
-                if (task.parent_task_id > 0) {
-                    var parent = yield tasks.get_task(task.parent_task_id);
-                    parent_title = parent != null ? ((!)parent).task.title : "";
+                string goal_title = "";
+                if (task.goal_id > 0) {
+                    var goal_row = yield db.query<Goal>()
+                        .where(Invercargill.Expressions.expr("id == $0",
+                            Invercargill.Expressions.elem<int64?>(task.goal_id)))
+                        .first_async();
+                    goal_title = goal_row != null ? ((!)goal_row).title : "";
                 }
                 var element = new Json.Object();
                 element.set_int_member("id", task.id);
                 element.set_string_member("title", task.title);
                 element.set_string_member("description", task.description ?? "");
                 element.set_string_member("status", task.status);
-                element.set_string_member("plan", task.plan ?? "");
                 element.set_string_member("summary", task.summary ?? "");
-                element.set_int_member("parent_task_id", task.parent_task_id);
-                element.set_string_member("parent_title", parent_title);
+                element.set_int_member("goal_id", task.goal_id);
+                element.set_string_member("goal_title", goal_title);
                 var body = new Json.Object();
                 body.set_object_member("task", element);
                 return SaddleUpdateEndpoint.json_ok(body);
@@ -536,183 +528,6 @@ namespace ClankerSaddle.Api {
         }
     }
 
-    /**
-     * `POST /api/saddle/subtask` — the synchronous subtask-creation
-     * callback a daemon's `saddle subtask` CLI posts to (wire contract):
-     * authenticated by the per-saddle bearer token (plan 10), creating
-     * the task row immediately — same project, branch and priority as
-     * the parent — with `task_dependencies` rows for every `depends_on`
-     * id and the timeline entry on the parent's session. Idempotent on
-     * (parent, title): a retried callback or the later entry-poll
-     * fallback returns the existing task's id instead of duplicating.
-     *
-     * `capabilities: [tags]` (plan 10 phase 3, `saddle subtask
-     * --capability`) attaches required tags to the new task — explicit
-     * only, never inherited from the parent — and is validated against
-     * the operator tag set first: an unknown tag rejects the call with
-     * an error naming the valid tags (the agent reads it verbatim in
-     * the CLI output).
-     *
-     * ```json
-     * {"parent_task_id": 1, "title": "…", "description": "…",
-     *  "depends_on": [3, 4], "capabilities": ["gpu"], "session_id": 2}
-     * ```
-     *
-     * → `{"ok": true, "task_id": 7}`.
-     */
-    public class SaddleSubtaskEndpoint : Object, Endpoint {
-
-        private SaddleService saddles = inject<SaddleService>();
-        private TaskService tasks = inject<TaskService>();
-        private CapabilityService capabilities = inject<CapabilityService>();
-
-        public virtual async HttpResult handle_request(HttpContext http_context,
-                RouteContext route_context) throws Error {
-            if (!(yield validate_saddle_bearer(http_context, saddles))) {
-                return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
-            }
-
-            Json.Object body;
-            try {
-                body = yield SaddleUpdateEndpoint.read_json_body(http_context);
-            } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
-            }
-
-            int64 parent_task_id = body.has_member("parent_task_id")
-                && body.get_member("parent_task_id").get_value_type() == typeof(int64)
-                ? body.get_int_member("parent_task_id") : 0;
-            string title = SaddleUpdateEndpoint.get_string_or(body, "title", "");
-            if (parent_task_id <= 0 || title.length == 0) {
-                return SaddleUpdateEndpoint.json_error(400,
-                    "parent_task_id and title are required");
-            }
-            var parent = yield tasks.get_task(parent_task_id);
-            if (parent == null) {
-                return SaddleUpdateEndpoint.json_error(400,
-                    @"No task exists with id $(parent_task_id)");
-            }
-
-            string[] requested_tags = {};
-            try {
-                requested_tags = capability_tags_of(body);
-            } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
-            }
-            var resolved_capabilities = new Vector<Capability>();
-            if (requested_tags.length > 0) {
-                var unknown = new Vector<string>();
-                foreach (string tag in requested_tags) {
-                    string trimmed = tag.strip();
-                    if (trimmed.length == 0) {
-                        continue;
-                    }
-                    var capability = yield capabilities.find_by_name(trimmed);
-                    if (capability == null) {
-                        unknown.add(trimmed);
-                    } else {
-                        resolved_capabilities.add((!)capability);
-                    }
-                }
-                if (unknown.length > 0) {
-                    var valid = new Vector<string>();
-                    foreach (var capability in yield capabilities.list_capabilities()) {
-                        valid.add(capability.name);
-                    }
-                    return SaddleUpdateEndpoint.json_error(400,
-                        @"Unknown capability tag(s): $(string.joinv(", ", unknown.to_array()))"
-                        + @" — valid tags: $(string.joinv(", ", valid.to_array()))");
-                }
-            }
-
-            int64[] depends_on = {};
-            if (body.has_member("depends_on")
-                    && body.get_member("depends_on").get_node_type() == Json.NodeType.ARRAY) {
-                foreach (var element in body.get_array_member("depends_on").get_elements()) {
-                    if (element.get_node_type() == Json.NodeType.VALUE) {
-                        int64 parsed = 0;
-                        if (element.get_value_type() == typeof(int64)) {
-                            parsed = element.get_int();
-                        } else {
-                            parsed = int64.parse(element.get_string());
-                        }
-                        if (parsed > 0) {
-                            depends_on += parsed;
-                        }
-                    }
-                }
-            }
-
-            try {
-                var existing = yield tasks.find_subtask_by_title(parent_task_id, title);
-                TaskEntity task;
-                bool created = false;
-                if (existing != null) {
-                    task = (!)existing;
-                } else {
-                    // enqueue=false keeps the row in draft until the
-                    // explicit capabilities are attached below — the
-                    // dispatcher must never see the subtask with a
-                    // partial requirement set.
-                    task = yield tasks.create_subtask_with_dependencies(parent_task_id,
-                        title, SaddleUpdateEndpoint.get_string_or(body, "description", null),
-                        depends_on, false);
-                    created = true;
-                }
-                if (created) {
-                    yield tasks.log_session_entry(parent_task_id, "subtask",
-                        @"saddle subtask: $(title)", @"created task #$(task.id)");
-                }
-                // Explicit-only capability attachment (never inherited):
-                // idempotent, so a retried callback re-attaches nothing.
-                foreach (var capability in resolved_capabilities) {
-                    yield capabilities.attach_to_task(capability.id, task.id);
-                }
-                // The parent's auto-enqueue option (plan 11): a subtask
-                // created under a parent carrying it enqueues immediately
-                // — after its capabilities landed, so the queue only ever
-                // sees the full requirement set. Otherwise the subtask
-                // stays in draft for the human Enqueue action (and, when
-                // the parent is parked, (re)arms the waiting-on-drafts
-                // push).
-                if (created && ((!)parent).task.auto_enqueue_subtasks) {
-                    yield tasks.enqueue_task(task.id);
-                } else if (created) {
-                    yield tasks.notify_waiting_on_drafts_if_parked(parent_task_id);
-                }
-                var response = new Json.Object();
-                response.set_int_member("task_id", task.id);
-                return SaddleUpdateEndpoint.json_ok(response);
-            } catch (Error e) {
-                return SaddleUpdateEndpoint.json_error(400, e.message);
-            }
-        }
-
-        /**
-         * The `capabilities` member as a tag array: absent → empty; a
-         * native JSON array of strings → its elements; anything else →
-         * an error (the daemon always sends the array form).
-         */
-        private static string[] capability_tags_of(Json.Object body) throws Error {
-            if (!body.has_member("capabilities")) {
-                return new string[0];
-            }
-            var member = body.get_member("capabilities");
-            if (member.get_node_type() != Json.NodeType.ARRAY) {
-                throw new Error(Quark.from_string("clanker-saddle"), 46,
-                    "capabilities must be a JSON array of tag names");
-            }
-            var tags = new Vector<string>();
-            foreach (var element in member.get_array().get_elements()) {
-                if (element.get_node_type() == Json.NodeType.VALUE
-                        && element.get_string() != null) {
-                    tags.add(element.get_string());
-                }
-            }
-            return tags.to_array();
-        }
-    }
-
     /**
      * `POST /api/saddle/note` — the callback endpoint a daemon's
      * `saddle note` CLI posts to (wire contract): authenticated by the
@@ -921,7 +736,7 @@ namespace ClankerSaddle.Api {
                 }
 
                 var task = yield tasks.create_task(((!)goal).project_id, title, mandate,
-                    5, null, null, null, false, false, false, STATUS_DRAFT,
+                    5, null, null, STATUS_DRAFT,
                     TASK_KIND_TASK, mode, CREATOR_GOAL, goal_id, 0);
                 foreach (var capability in resolved) {
                     yield capabilities.attach_to_task(capability.id, task.id);

+ 10 - 32
src/Data/Entities.vala

@@ -48,18 +48,20 @@ namespace ClankerSaddle.Data {
 
     /**
      * A unit of work for a saddle agent: created in {@link
-     * TaskService.STATUS_DRAFT} and queued only by the human Enqueue
-     * action, dispatched to a saddle, worked, possibly blocked on
-     * questions or plans, and eventually done or failed (completion is
-     * bot-only). Stored in the `tasks` table; child tasks reference
-     * their parent via {@link parent_task_id}. Named `TaskEntity`
-     * (not `Task`) to stay clear of `GLib.Task` in files that import
-     * Gio transitively.
+     * TaskService.STATUS_DRAFT}, queued by its creator (human or goal
+     * agent), dispatched to a saddle, worked, possibly blocked on
+     * questions, and eventually done or failed (completion is
+     * bot-only). The plan-12 shape: goals own the task graph — a task
+     * links its {@link goal_id}, runs as a plain worker or the goal
+     * agent itself ({@link kind}), carries a work {@link mode} and its
+     * {@link creator}, and human tasks may carry an auto-enqueue
+     * predecessor ({@link enqueue_after_task_id}). Stored in the
+     * `tasks` table. Named `TaskEntity` (not `Task`) to stay clear of
+     * `GLib.Task` in files that import Gio transitively.
      */
     public class TaskEntity : Object {
 
         public int64 id { get; set; }
-        public int64 parent_task_id { get; set; default = 0; }
         public int64 project_id { get; set; }
         public string title { get; set; default = ""; }
         public string? description { get; set; default = null; }
@@ -67,16 +69,12 @@ namespace ClankerSaddle.Data {
         public int64 priority { get; set; default = 5; }
         public string? working_branch { get; set; default = null; }
         public string? summary { get; set; default = null; }
-        public string? plan { get; set; default = null; }
         public string? task_branch { get; set; default = null; }
         public string? branch_from { get; set; default = null; }
         public DateTime? created_at { get; set; default = null; }
         public DateTime? started_at { get; set; default = null; }
         public DateTime? completed_at { get; set; default = null; }
         public DateTime? suspended_at { get; set; default = null; }
-        public int64 auto_approve { get; set; default = 0; }
-        public bool plan_auto_approve { get; set; default = false; }
-        public bool auto_enqueue_subtasks { get; set; default = false; }
         /** The goal this task belongs to (0 = standalone). */
         public int64 goal_id { get; set; default = 0; }
         /** "task" (a worker) or "goal_run" (the goal agent's own run). */
@@ -91,7 +89,6 @@ namespace ClankerSaddle.Data {
         public static void entity_mapping(EntityMapperBuilder<TaskEntity> cfg) {
             cfg.table("tasks")
                 .column<int64?>("id", o => o.id, (o, v) => o.id = v)
-                .column<int64?>("parent_task_id", o => o.parent_task_id, (o, v) => o.parent_task_id = v ?? 0)
                 .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
                 .column<string>("title", o => o.title, (o, v) => o.title = v)
                 .column<string>("description", o => o.description, (o, v) => o.description = v)
@@ -99,16 +96,12 @@ namespace ClankerSaddle.Data {
                 .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>("plan", o => o.plan, (o, v) => o.plan = 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)
                 .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
                 .column<DateTime?>("started_at", o => o.started_at, (o, v) => o.started_at = v)
                 .column<DateTime?>("completed_at", o => o.completed_at, (o, v) => o.completed_at = v)
                 .column<DateTime?>("suspended_at", o => o.suspended_at, (o, v) => o.suspended_at = v)
-                .column<int64?>("auto_approve", o => o.auto_approve, (o, v) => o.auto_approve = v ?? 0)
-                .column<bool>("plan_auto_approve", o => o.plan_auto_approve, (o, v) => o.plan_auto_approve = v)
-                .column<bool>("auto_enqueue_subtasks", o => o.auto_enqueue_subtasks, (o, v) => o.auto_enqueue_subtasks = v)
                 .column<int64?>("goal_id", o => o.goal_id, (o, v) => o.goal_id = v ?? 0)
                 .column<string>("kind", o => o.kind, (o, v) => o.kind = v)
                 .column<string>("mode", o => o.mode, (o, v) => o.mode = v)
@@ -137,21 +130,6 @@ namespace ClankerSaddle.Data {
         }
     }
 
-    /** An edge saying one task must finish before another may start. */
-    public class TaskDependency : Object {
-
-        public int64 id { get; set; }
-        public int64 task_id { get; set; }
-        public int64 depends_on_task_id { get; set; }
-
-        public static void entity_mapping(EntityMapperBuilder<TaskDependency> cfg) {
-            cfg.table("task_dependencies")
-                .column<int64?>("id", o => o.id, (o, v) => o.id = v)
-                .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
-                .column<int64?>("depends_on_task_id", o => o.depends_on_task_id, (o, v) => o.depends_on_task_id = v);
-        }
-    }
-
     /** A remark on a task, authored either by a human or by the agent ('bot'). */
     public class TaskComment : Object {
 

+ 6 - 27
src/Data/Migrations/M0001_Initial.vala

@@ -8,11 +8,12 @@ namespace ClankerSaddle.Data.Migrations {
      * tear-down-and-recreate replaced incremental evolution). Covers:
      * projects and their links; the plan-12 goals (with links,
      * comments, typed blocks and the supervision-narrative event log)
-     * and the goal-linked task columns (`goal_id`, `kind`, `mode`,
-     * `creator`, `enqueue_after_task_id`); tasks with dependencies,
-     * comments, typed blocks, progress and the draft-lifecycle columns
-     * (`status` defaults to 'draft', `plan_auto_approve`,
-     * `auto_enqueue_subtasks`); saddle registrations with git identity
+     * and the flattened task shape (`goal_id`, `kind`, `mode`,
+     * `creator`, `enqueue_after_task_id` — the subtask machinery's
+     * parent_task_id/plan/auto-* columns and the task_dependencies
+     * table are gone); tasks with comments, typed blocks, progress
+     * and the draft-lifecycle columns (`status` defaults to
+     * 'draft'); saddle registrations with git identity
      * and the per-saddle token; the session + command + raw-log audit
      * trail; rules and their attachments; project notes; capability
      * tags; and the plan-11 notification tables for task E
@@ -116,8 +117,6 @@ namespace ClankerSaddle.Data.Migrations {
                 t.column<int64?>("id")
                     .auto_increment()
                     .primary_key();
-                t.column<int64?>("parent_task_id")
-                    .references("tasks", "id");
                 t.column<int64?>("project_id")
                     .not_null()
                     .references("projects", "id")
@@ -151,31 +150,12 @@ namespace ClankerSaddle.Data.Migrations {
                     .default_value<int64?>(5);
                 t.column<string>("working_branch");
                 t.column<string>("summary");
-                t.column<string>("plan");
                 t.column<string>("task_branch");
                 t.column<string>("branch_from");
                 t.column<DateTime?>("created_at");
                 t.column<DateTime?>("started_at");
                 t.column<DateTime?>("completed_at");
                 t.column<DateTime?>("suspended_at");
-                t.column<int64?>("auto_approve");
-                t.column<bool>("plan_auto_approve")
-                    .default_value<bool>(false);
-                t.column<bool>("auto_enqueue_subtasks")
-                    .default_value<bool>(false);
-            });
-
-            b.create_table("task_dependencies", (t) => {
-                t.column<int64?>("id")
-                    .auto_increment()
-                    .primary_key();
-                t.column<int64?>("task_id")
-                    .not_null()
-                    .references("tasks", "id")
-                    .indexed();
-                t.column<int64?>("depends_on_task_id")
-                    .not_null()
-                    .references("tasks", "id");
             });
 
             b.create_table("task_comments", (t) => {
@@ -531,7 +511,6 @@ namespace ClankerSaddle.Data.Migrations {
             b.drop_table("task_progress");
             b.drop_table("task_blocks");
             b.drop_table("task_comments");
-            b.drop_table("task_dependencies");
             b.drop_table("tasks");
             b.drop_table("goal_links");
             b.drop_table("goals");

+ 19 - 34
src/Services/AllocationService.vala

@@ -12,7 +12,7 @@ namespace ClankerSaddle.Services {
      * One offerable task for the wake computation: a fresh queued task
      * (dependencies met, {@link affine_saddle_id} 0) or a parked task
      * ready to resume — blocked/review-requested with its newest block
-     * answered, or suspended with a terminal subtask event since it
+     * answered, or suspended with a due goal event behind it
      * parked — pinned to the saddle holding its open session.
      */
     public class OfferableTask : Object {
@@ -73,7 +73,6 @@ namespace ClankerSaddle.Services {
         private Connection connection = inject<Connection>();
         private TaskService tasks = inject<TaskService>();
         private CapabilityService capabilities = inject<CapabilityService>();
-        private SubtaskEventQueue subtask_events = inject<SubtaskEventQueue>();
         private GoalService goals = inject<GoalService>();
         private StatePulse pulse = inject<StatePulse>();
         private PushService push = inject<PushService>();
@@ -128,23 +127,14 @@ namespace ClankerSaddle.Services {
                     }
                 }
             }
-            foreach (var parent in yield tasks.list_tasks(STATUS_SUSPENDED, null)) {
-                // Plan 12: a parked goal agent resumes on its own
-                // machinery — task-terminal events under the goal,
-                // coalesced ~10 s (GoalService.resume_due); the
-                // subtask-parent resume below serves the legacy
-                // parent/child rows.
-                if (parent.kind == TASK_KIND_GOAL_RUN) {
-                    if (yield goals.resume_due(parent)) {
-                        var parked = yield parked_offerable(parent, true);
-                        if (parked != null) {
-                            rows.add((!)parked);
-                        }
-                    }
-                    continue;
-                }
-                if (yield tasks.has_terminal_subtask_since(parent)) {
-                    var parked = yield parked_offerable(parent, true);
+            // Plan 12: a parked goal agent resumes on its own
+            // machinery — task-terminal events under its goal,
+            // coalesced ~10 s ({@link GoalService.resume_due}); plain
+            // suspended tasks (without one) stay parked until a human
+            // or the goal acts.
+            foreach (var run in yield tasks.list_tasks(STATUS_SUSPENDED, null)) {
+                if (run.kind == TASK_KIND_GOAL_RUN && (yield goals.resume_due(run))) {
+                    var parked = yield parked_offerable(run, true);
                     if (parked != null) {
                         rows.add((!)parked);
                     }
@@ -318,13 +308,13 @@ namespace ClankerSaddle.Services {
             if (task.status != STATUS_QUEUED) {
                 return false;
             }
-            foreach (var dependency in db.query<TaskDependency>()
-                    .where(expr("task_id == $0", elem<int64?>(task.id)))
-                    .materialise()) {
-                var other = db.query<TaskEntity>()
-                    .where(expr("id == $0", elem<int64?>(dependency.depends_on_task_id)))
+            if (task.enqueue_after_task_id > 0) {
+                // A queued dependent whose predecessor is not done yet
+                // (a predecessor set after a manual enqueue) waits.
+                var predecessor = db.query<TaskEntity>()
+                    .where(expr("id == $0", elem<int64?>(task.enqueue_after_task_id)))
                     .first();
-                if (other == null || ((!)other).status != STATUS_DONE) {
+                if (predecessor == null || ((!)predecessor).status != STATUS_DONE) {
                     return false;
                 }
             }
@@ -335,7 +325,7 @@ namespace ClankerSaddle.Services {
          * Assembles the claimed task's assignment payload (today's
          * /start shape): pending block answers ride the task object,
          * and every parked resume carries a `resume_notice` — a
-         * suspended parent's the coalesced subtask report, an answered
+         * goal agent's the coalesced batched report, an answered
          * block's the verdict text in the stable wording ({@link
          * TaskService.verdict_notice_for}) — plus the
          * `branch_advanced` verdict. The hand-over itself starts the
@@ -356,18 +346,14 @@ namespace ClankerSaddle.Services {
             bool? branch_advanced = null;
             if (claim.parked) {
                 branch_advanced = yield tasks.branch_advanced_since(task.id);
-                if (claim.suspended_resume && task.kind == TASK_KIND_GOAL_RUN) {
+                if (claim.suspended_resume) {
                     // Plan 12: the goal agent's batched report — every
                     // plan task's status plus the completion reports
                     // that landed while it was parked. Consumes the
-                    // pending goal events (the coalescing watermark).
+                    // pending goal events (the coalescing watermark)
+                    // and ends the parking window.
                     resume_notice = yield goals.take_resume_notice(task.goal_id);
                     yield tasks.clear_suspension(task.id);
-                } else if (claim.suspended_resume) {
-                    resume_notice = yield tasks.subtask_status_report(task.id);
-                    // The parking window is over: a later suspension
-                    // starts a fresh event window.
-                    yield tasks.clear_suspension(task.id);
                 } else {
                     // Answering = allocating (plan 11): the verdict rides
                     // the resuming assignment's resume_notice, injected as
@@ -385,7 +371,6 @@ namespace ClankerSaddle.Services {
             payload.set_int_member("session_id", claim.session_id);
 
             yield saddles.mark_task_comments_delivered(task.id);
-            subtask_events.record_dispatch(task);
             yield pulse.bump(task.id, "task dispatched");
             // Web Push (plan 11 task E): the FIRST dispatch of a task
             // notifies its opted-in users — fired from the same place

+ 2 - 3
src/Services/Dispatcher.vala

@@ -96,11 +96,11 @@ namespace ClankerSaddle.Services {
           * One dispatch cycle: the lost-wake guard offlines saddles whose
           * wake went unanswered, then the wake computation offers every
           * offerable task to a capable sleeping or idle-awake ask daemon
-          * (a parked task's answered block or terminal subtask event
+          * (a parked task's answered block or due goal event
           * resumes it through the affinity allocation — answering IS
           * allocating since plan 11; the retired POST /answer delivery
           * pass is gone, the verdict rides the resuming assignment's
-          * resume_notice). Pending subtask events and human comments are
+          * resume_notice). Pending goal events and human comments are
           * delivered to RUNNING daemons via `POST /task-update`, and
           * silent runners get their one exceptional staleness probe.
           * Wake computation pauses while {@link LlmEndpoint} is
@@ -125,7 +125,6 @@ namespace ClankerSaddle.Services {
             }
 
             yield saddles.requeue_tasks_on_offline_saddles(tasks);
-            yield saddles.deliver_pending_subtask_events(tasks);
 
             // Web Push retry tick (plan 11 task E): anything a failed
             // send left queued retries here (best-effort delivery).

+ 2 - 3
src/Services/GoalService.vala

@@ -259,9 +259,8 @@ namespace ClankerSaddle.Services {
 
             var run = yield tasks.create_task(((!)goal).project_id,
                 @"Goal agent — $(((!)goal).title)",
-                ((!)goal).brief, GOAL_RUN_PRIORITY, null,
-                null, ((!)goal).base_branch,
-                false, false, false, STATUS_QUEUED,
+                ((!)goal).brief, GOAL_RUN_PRIORITY,
+                null, ((!)goal).base_branch, STATUS_QUEUED,
                 TASK_KIND_GOAL_RUN, MODE_ORCHESTRATE, CREATOR_GOAL,
                 ((!)goal).id, 0);
             yield record_event(goal_id, "started",

+ 1 - 16
src/Services/PushService.vala

@@ -53,7 +53,7 @@ namespace ClankerSaddle.Services {
      * encrypted per RFC 8291 (ECDH P-256 + HKDF + AES-128-GCM,
      * aes128gcm content coding) and authorised by an ES256 VAPID JWT.
      *
-     * Targeting: BLOCK events and the suspended-waiting-on-drafts
+     * Targeting: BLOCK events
      * condition go to every push-subscribed user globally; a task's
      * first start, failure and completion go only to users holding a
      * `task_notify_prefs` row for it ("Notify me").
@@ -267,21 +267,6 @@ namespace ClankerSaddle.Services {
             };
         }
 
-        /**
-          * The waiting-on-drafts payload (global audience, fired like a
-          * block event): the parent is parked and cannot finish until
-          * the human enqueues its draft subtasks.
-          */
-        public static PushNotification waiting_drafts_notification(string task_title,
-                int64 task_id, int64 draft_count) {
-            return new PushNotification() {
-                title = @"$(task_title) — waiting on subtasks to be enqueued",
-                body = @"Parked with $(draft_count) subtask(s) still in draft —"
-                    + " enqueue them so this task can finish",
-                url = @"/tasks/$(task_id)"
-            };
-        }
-
         /**
          * The goal block-event payload (plan 12, global audience —
          * goal blocks always notify): title = goal title + kind, body

+ 14 - 105
src/Services/SaddleService.vala

@@ -101,7 +101,6 @@ namespace ClankerSaddle.Services {
         private CapabilityService capability_service = inject<CapabilityService>();
         private TaskService task_service = inject<TaskService>();
         private GoalService goal_service = inject<GoalService>();
-        private SubtaskEventQueue subtask_events = inject<SubtaskEventQueue>();
         private Astralis.WebConfig config = inject<Astralis.WebConfig>();
 
         /**
@@ -229,7 +228,7 @@ namespace ClankerSaddle.Services {
         /**
          * The saddle a bearer token belongs to, or null — the
          * credential check for the id-less `/api/saddle/*` endpoints
-         * (`next-task`, `note`, `subtask`, `tasks`): the row token
+         * (`next-task`, `note`, `task`): the row token
          * first, then any legacy `api_tokens` row (mapped to its
          * saddle). Removing the saddle revokes both.
          */
@@ -663,7 +662,6 @@ namespace ClankerSaddle.Services {
                             ? @"Saddle became unreachable ($(reason)); task requeued"
                             : "Saddle became unreachable; task requeued";
                         yield tasks.add_comment(task_id, "bot", unreachable_text);
-                        tasks.note_requeued_subtask(((!)task));
                     }
                 }
 
@@ -918,8 +916,7 @@ namespace ClankerSaddle.Services {
             // phase 3): the allocated saddle's own tags plus every tag
             // name known across the fleet (the operator-managed
             // catalogue) — the daemon renders both into its system
-            // prompt with the subtask tagging rule, and the same set
-            // validates `saddle subtask --capability`.
+            // prompt and validates `task create --capability`.
             payload.set_member_name("capabilities");
             payload.begin_object();
             payload.set_member_name("saddle_tags");
@@ -1213,62 +1210,6 @@ namespace ClankerSaddle.Services {
             }
         }
 
-        /**
-         * Pushes pending subtask status events to the daemon hosting each
-         * parent's live or parked session — `POST /task-update
-         * {kind: "subtask-status"}` (wire contract, no auth like
-         * `/answer` and `/notify`), so the daemon's live subtask set
-         * stays current for its `done` and `suspend` guards. Best-effort
-         * mirror of {@link deliver_pending_comments}: a parent without an
-         * open session drops its events (its next assignment carries the
-         * full `subtasks` array), and transport failures stay queued for
-         * the next dispatcher tick. The queue coalesces to the latest
-         * state per subtask, so retries never duplicate stale events.
-         */
-        public async void deliver_pending_subtask_events(TaskService tasks) throws Error {
-            foreach (var event in subtask_events.snapshot()) {
-                var session = yield tasks.open_session_for_task(event.parent_task_id);
-                if (session == null) {
-                    subtask_events.discard(event.subtask_id);
-                    continue;
-                }
-                var saddle = yield get_saddle(((!)session).saddle_id);
-                if (saddle == null) {
-                    subtask_events.discard(event.subtask_id);
-                    continue;
-                }
-
-                var payload = new Json.Builder();
-                payload.begin_object();
-                payload.set_member_name("kind");
-                payload.add_string_value("subtask-status");
-                payload.set_member_name("parent_task_id");
-                payload.add_int_value(event.parent_task_id);
-                payload.set_member_name("subtask_id");
-                payload.add_int_value(event.subtask_id);
-                payload.set_member_name("title");
-                payload.add_string_value(event.title);
-                payload.set_member_name("status");
-                payload.add_string_value(event.status);
-                payload.end_object();
-
-                Json.Node? root = null;
-                int status_code = yield request_saddle(((!)saddle).host, "POST",
-                    "/task-update", (owned) payload, out root);
-                if (status_code == 200 || status_code == 204) {
-                    subtask_events.discard(event.subtask_id);
-                } else if (status_code == 404 || status_code == 405) {
-                    // Older daemon without the route: its subtask set
-                    // comes only from assignment payloads.
-                    subtask_events.discard(event.subtask_id);
-                    debug(@"[ClankerSaddle] Saddle $(((!)saddle).id) has no /task-update route");
-                } else {
-                    warning(@"[ClankerSaddle] Subtask status push for $(event.subtask_id)"
-                        + @" to saddle $(((!)saddle).id) returned HTTP $(status_code); retrying next tick");
-                }
-            }
-        }
-
         /**
          * Marks the task's human comments as delivered — they were handed
          * over inside an assignment payload, so neither the poll endpoint
@@ -1326,14 +1267,15 @@ namespace ClankerSaddle.Services {
                         }
                         break;
                     case "plan":
-                        string markdown = output.length > 0 ? output
-                            : strip_quotes(strip_verb(command, "saddle plan"));
-                        if (markdown.length > 0 && goal_id > 0) {
-                            // Goal context: a plan submission enters
-                            // plan review (the state push parks the
-                            // run; the mirror block carries the
-                            // verdict back).
-                            yield goal_service.submit_plan(goal_id, markdown);
+                        // Plan 12: plans are goal-context only — a
+                        // goal-run's `saddle plan` enters plan review;
+                        // plain task runs carry no plan member.
+                        if (goal_id > 0) {
+                            string markdown = output.length > 0 ? output
+                                : strip_quotes(strip_verb(command, "saddle plan"));
+                            if (markdown.length > 0) {
+                                yield goal_service.submit_plan(goal_id, markdown);
+                            }
                         }
                         break;
                     case "progress":
@@ -1346,10 +1288,9 @@ namespace ClankerSaddle.Services {
                         }
                         break;
                     case "review":
-                        var review_block = yield tasks.request_review(task_id,
+                        yield tasks.request_review(task_id,
                             output.length > 0 ? output
                             : strip_quotes(strip_verb(command, "saddle review")));
-                        yield tasks.auto_answer_if_covered(review_block);
                         break;
                     case "done":
                         if (goal_id > 0) {
@@ -1427,11 +1368,10 @@ namespace ClankerSaddle.Services {
                     return;
                 }
             }
-            var raised = yield tasks.add_block(task_id, question,
+            yield tasks.add_block(task_id, question,
                 block_string_or(block, "type", "block_type"),
                 string_array_member(block, block.has_member("options") ? "options" : "block_options"),
                 optional_string_member(block, block.has_member("body") ? "body" : "block_body"));
-            yield tasks.auto_answer_if_covered(raised);
         }
 
         /** The first non-empty of two string members (empty when both are). */
@@ -1671,7 +1611,7 @@ namespace ClankerSaddle.Services {
                 case "suspended":
                     // `saddle suspend`: the same parking as a block, but
                     // the web drives resumption — a wake of the affine
-                    // saddle once a subtask event lands, the resume
+                    // saddle once a goal event lands, the resume
                     // assignment carrying the coalesced report.
                     yield tasks.suspend_task(task_id);
                     yield park_if_current(saddle.id, task_id);
@@ -1901,37 +1841,6 @@ namespace ClankerSaddle.Services {
             return answer;
         }
 
-        /** Splits operands on whitespace honouring double quotes. */
-        private static string[] split_operands(string args) {
-            var operands = new Vector<string>();
-            var current = new StringBuilder();
-            bool in_operand = false;
-            bool quoted = false;
-            for (var index = 0; index < args.length; index++) {
-                char character = args[index];
-                if (character == '"') {
-                    quoted = true;
-                    in_operand = true;
-                    continue;
-                }
-                if (!quoted && character.isspace()) {
-                    if (in_operand) {
-                        operands.add(current.str);
-                        current = new StringBuilder();
-                        in_operand = false;
-                        quoted = false;
-                    }
-                    continue;
-                }
-                current.append_c(character);
-                in_operand = true;
-            }
-            if (in_operand) {
-                operands.add(current.str);
-            }
-            return operands.to_array();
-        }
-
         /** Strips one layer of matching double quotes, when present. */
         private static string strip_quotes(string text) {
             if (text.length >= 2 && text.has_prefix("\"") && text.has_suffix("\"")) {

+ 0 - 102
src/Services/SubtaskEventQueue.vala

@@ -1,102 +0,0 @@
-using Invercargill;
-using Invercargill.DataStructures;
-using Inversion;
-using ClankerSaddle.Data;
-
-namespace ClankerSaddle.Services {
-
-    /** One subtask status event awaiting delivery to the parent's daemon. */
-    public class SubtaskStatusEvent : Object {
-        public int64 parent_task_id { get; set; }
-        public int64 subtask_id { get; set; }
-        public string title { get; set; default = ""; }
-        public string status { get; set; default = ""; }
-        public string summary { get; set; default = ""; }
-    }
-
-    /**
-     * Process-wide queue of subtask status events for daemons hosting
-     * the parent's live or parked session (wire contract:
-     * `POST /task-update {kind: "subtask-status"}`). A singleton by
-     * necessity —
-     * {@link TaskService} records events from any request or poll while
-     * {@link SaddleService} drains them on the dispatcher tick, mirroring
-     * the pending-comment bookkeeping in {@link SaddlePollState}.
-     *
-     * Coalescing: only the LATEST event per subtask stays queued — a
-     * burst of transitions collapses to what the parent's daemon needs
-     * (the current state), keeping the queue bounded by subtask count.
-     * Ids are stringified: the Invercargill collections cannot key on
-     * value types.
-     */
-    public class SubtaskEventQueue : Object {
-
-        private Vector<SubtaskStatusEvent> pending = new Vector<SubtaskStatusEvent>();
-        private HashSet<string> queued_ids = new HashSet<string>();
-        private Mutex queue_lock = Mutex();
-
-        /**
-         * Enqueues `event`, replacing any earlier event for the same
-         * subtask (the replacement moves to the back — newest last).
-         */
-        public void enqueue(SubtaskStatusEvent event) {
-            queue_lock.lock();
-            string key = event.subtask_id.to_string();
-            if (queued_ids.has(key)) {
-                pending.remove_first_where(e => e.subtask_id == event.subtask_id);
-            } else {
-                queued_ids.add(key);
-            }
-            pending.add(event);
-            queue_lock.unlock();
-        }
-
-        /** A copy of the pending events, oldest first. */
-        public ImmutableLot<SubtaskStatusEvent> snapshot() {
-            queue_lock.lock();
-            var copy = new Vector<SubtaskStatusEvent>();
-            foreach (var event in pending) {
-                copy.add(event);
-            }
-            queue_lock.unlock();
-            return copy.to_immutable_buffer();
-        }
-
-        /** Removes the (delivered or dropped) event for `subtask_id`. */
-        public void discard(int64 subtask_id) {
-            queue_lock.lock();
-            pending.remove_first_where(e => e.subtask_id == subtask_id);
-            queued_ids.remove(subtask_id.to_string());
-            queue_lock.unlock();
-        }
-
-        /** The number of distinct subtasks awaiting delivery. */
-        public uint length {
-            get {
-                queue_lock.lock();
-                uint count = pending.length;
-                queue_lock.unlock();
-                return count;
-            }
-        }
-
-        /**
-         * Records a dispatched subtask (the allocation claim writes the
-         * assigned transition directly, bypassing
-         * {@link TaskService.set_status} — this is its hook). The wire
-         * value is `working`.
-         */
-        public void record_dispatch(TaskEntity subtask) {
-            if (subtask.parent_task_id <= 0) {
-                return;
-            }
-            enqueue(new SubtaskStatusEvent() {
-                parent_task_id = subtask.parent_task_id,
-                subtask_id = subtask.id,
-                title = subtask.title,
-                status = "working",
-                summary = subtask.summary ?? ""
-            });
-        }
-    }
-}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 78 - 632
src/Services/TaskService.vala


+ 4 - 154
src/actions/TaskActions.vala

@@ -61,12 +61,6 @@ namespace ClankerSaddle {
                 return directives().notify("error", "Project and title are required").navigate("/tasks");
             }
 
-            int64? parent_id = null;
-            int64 parsed_parent = Forms.integer(request, "parent_task_id", 0);
-            if (parsed_parent > 0) {
-                parent_id = parsed_parent;
-            }
-
             // Required capabilities: the create form's multi-select posts
             // several capability_id fields; every id must resolve before
             // the task is created. (A plain array — the Invercargill
@@ -109,17 +103,11 @@ namespace ClankerSaddle {
                 }
                 var task = yield tasks.create_task(project_id, title,
                     Forms.field_raw(request, "description"),
-                    Forms.integer(request, "priority", 5), parent_id,
+                    Forms.integer(request, "priority", 5),
                     Forms.optional(request, "task_branch"),
                     Forms.optional(request, "branch_from"),
-                    Forms.field(request, "plan_auto_approve") == "on",
-                    Forms.field(request, "auto_enqueue_subtasks") == "on",
-                    Forms.field(request, "auto_approve") == "on",
                     STATUS_DRAFT, TASK_KIND_TASK, mode, CREATOR_HUMAN, 0,
                     enqueue_after);
-                if (Forms.field(request, "plan").length > 0) {
-                    yield tasks.update_plan(task.id, Forms.field_raw(request, "plan"));
-                }
                 foreach (int64 capability_id in required) {
                     yield capabilities.attach_to_task(capability_id, task.id);
                 }
@@ -185,9 +173,6 @@ namespace ClankerSaddle {
                         ? Forms.field(request, "task_branch") : null,
                     request.form != null && request.form.get_field("branch_from") != null
                         ? Forms.field(request, "branch_from") : null,
-                    Forms.field(request, "plan_auto_approve") == "on",
-                    Forms.field(request, "auto_enqueue_subtasks") == "on",
-                    Forms.field(request, "auto_approve") == "on",
                     mode.length > 0 ? mode : null,
                     enqueue_after);
                 return directives().notify("success", "Task saved").navigate(@"/tasks/$(task_id)");
@@ -240,36 +225,6 @@ namespace ClankerSaddle {
         }
     }
 
-    /**
-     * Saves the task's markdown plan from the web UI's plan editor (the
-     * human counterpart of the agent's `saddle plan`).
-     */
-    public class UpdatePlanAction : SealedTaskAction {
-
-        private TaskService tasks = inject<TaskService>();
-        private UserService users = inject<UserService>();
-
-        public override async DirectiveBuilder handle() throws GLib.Error {
-            var guard = yield SpryAuth.require_login(directives(), request, users);
-            if (guard != null) {
-                return (!)guard;
-            }
-
-            int64 task_id = sealed_task_id();
-            if (task_id <= 0) {
-                return directives().navigate("/tasks");
-            }
-
-            try {
-                yield tasks.update_plan(task_id, Forms.field_raw(request, "plan"));
-                return directives().notify("success", "Plan saved").navigate(@"/tasks/$(task_id)");
-            } catch (Error e) {
-                return directives().notify("error", @"Could not save the plan: $(e.message)")
-                    .navigate(@"/tasks/$(task_id)");
-            }
-        }
-    }
-
     /**
      * Records the human's answer to a block; the task and block ids come
      * from the sealed {@link BlockAnswerPrivate} blob. Question blocks
@@ -327,19 +282,9 @@ namespace ClankerSaddle {
             }
 
             try {
-                // Accepting a plan applies the submitted markdown to the
-                // task's plan field (the approved text IS the plan).
-                if (answer == NOTICE_PLAN_APPROVED) {
-                    foreach (var candidate in (yield tasks.get_task(task_id)).blocks) {
-                        if (candidate.id == ((!)ctx).block_id
-                                && candidate.block_type == BLOCK_PLAN
-                                && candidate.body != null
-                                && ((!)candidate.body).length > 0) {
-                            yield tasks.update_plan(task_id, (!)candidate.body);
-                            break;
-                        }
-                    }
-                }
+                // Plan 12: the answer rides the parked workspace's
+                // resume verbatim (the stable verdicts); goal-plan
+                // approval happens on the goal page, not here.
                 yield tasks.answer_block(((!)ctx).block_id, answer);
                 return directives().notify("success", "Answer recorded; the saddle will collect it")
                     .navigate(@"/tasks/$(task_id)");
@@ -350,62 +295,6 @@ namespace ClankerSaddle {
         }
     }
 
-    /** Creates a child task under the sealed parent task id. */
-    public class CreateSubtaskAction : SealedTaskAction {
-
-        private TaskService tasks = inject<TaskService>();
-        private UserService users = inject<UserService>();
-
-        public override async DirectiveBuilder handle() throws GLib.Error {
-            var guard = yield SpryAuth.require_login(directives(), request, users);
-            if (guard != null) {
-                return (!)guard;
-            }
-
-            int64 task_id = sealed_task_id();
-            string title = Forms.field(request, "title");
-            if (task_id <= 0 || title.length == 0) {
-                return directives().notify("error", "A subtask title is required").navigate(@"/tasks/$(task_id)");
-            }
-
-            try {
-                yield tasks.create_subtask(task_id, title, Forms.field_raw(request, "description"));
-                return directives().notify("success", "Subtask created").navigate(@"/tasks/$(task_id)");
-            } catch (Error e) {
-                return directives().notify("error", @"Could not create the subtask: $(e.message)")
-                    .navigate(@"/tasks/$(task_id)");
-            }
-        }
-    }
-
-    /** Adds a dependency edge from the sealed task to the form's target. */
-    public class AddDependencyAction : SealedTaskAction {
-
-        private TaskService tasks = inject<TaskService>();
-        private UserService users = inject<UserService>();
-
-        public override async DirectiveBuilder handle() throws GLib.Error {
-            var guard = yield SpryAuth.require_login(directives(), request, users);
-            if (guard != null) {
-                return (!)guard;
-            }
-
-            int64 task_id = sealed_task_id();
-            int64 depends_on = Forms.integer(request, "depends_on_task_id", 0);
-            if (task_id <= 0 || depends_on <= 0) {
-                return directives().notify("error", "Pick a task to depend on").navigate(@"/tasks/$(task_id)");
-            }
-
-            try {
-                yield tasks.add_dependency(task_id, depends_on);
-                return directives().notify("success", "Dependency added").navigate(@"/tasks/$(task_id)");
-            } catch (Error e) {
-                return directives().notify("error", @"Could not add the dependency: $(e.message)")
-                    .navigate(@"/tasks/$(task_id)");
-            }
-        }
-    }
-
     /**
      * The human-only Enqueue action: moves a draft task into the
      * queue. Refused with a shown error while any dependency is still
@@ -608,45 +497,6 @@ namespace ClankerSaddle {
         }
     }
 
-    /**
-     * Flips a task's subtask auto-approve toggle: when on, review blocks
-     * raised by this task's descendants are answered automatically —
-     * the task's own review stays a human gate.
-     */
-    public class ToggleAutoApproveAction : SealedTaskAction {
-
-        private TaskService task_service = inject<TaskService>();
-        private UserService users = inject<UserService>();
-
-        public override async DirectiveBuilder handle() throws GLib.Error {
-            var guard = yield SpryAuth.require_login(directives(), request, users);
-            if (guard != null) {
-                return (!)guard;
-            }
-
-            int64 task_id = sealed_task_id();
-            if (task_id <= 0) {
-                return directives().navigate("/tasks");
-            }
-
-            try {
-                var detail = yield task_service.get_task(task_id);
-                if (detail == null) {
-                    return directives().navigate("/tasks");
-                }
-                bool enabled = ((!)detail).task.auto_approve == 0;
-                yield task_service.save_auto_approve(task_id, enabled);
-                string state = enabled ? "on" : "off";
-                return directives().notify("success",
-                        @"Subtask auto-approve is now $(state)")
-                    .navigate(@"/tasks/$(task_id)");
-            } catch (Error e) {
-                return directives().notify("error", @"Could not toggle auto-approve: $(e.message)")
-                    .navigate(@"/tasks/$(task_id)");
-            }
-        }
-    }
-
     /**
      * The task detail page's polling action: rebuilds the `task` slot from
      * current data so the three-second client poll shows live comments,

+ 0 - 59
src/entrypoints/HomeEntrypoint.vala

@@ -91,24 +91,6 @@ namespace ClankerSaddle {
         public Statum.Model.ActionDto? push_unsubscribe { get; set; }
     }
 
-    /**
-     * A suspended parent with subtasks still in draft (plan 11): the
-     * dashboard's "waiting on subtasks to be enqueued" block — task D+E
-     * renders it with links to the task pages where the enqueue action
-     * lives (the parent's page and each draft subtask's page).
-     */
-    public class DashboardWaitingDraftView : Object {
-        public int64 task_id { get; set; }
-        public string title { get; set; default = ""; }
-        public int64 draft_subtasks { get; set; }
-    }
-
-    /** One draft subtask link inside a {@link DashboardWaitingDraftView}. */
-    public class DashboardDraftRef : Object {
-        public int64 id { get; set; }
-        public string title { get; set; default = ""; }
-    }
-
     /**
      * Builds the `dashboard` slot state shared by the home entrypoint and
      * the {@link RefreshDashboardAction} pulse refresh, so both hydrate
@@ -161,39 +143,6 @@ namespace ClankerSaddle {
             }
             pub.ready_count = (yield tasks.get_ready_tasks()).length;
 
-            // The plan-11 "waiting on subtasks to be enqueued" block:
-            // every suspended parent with subtasks still in draft, the
-            // drafts linked so the human reaches Enqueue in one click.
-            var draft_children = new Dictionary<string, string>();
-            var drafts_by_parent = new Dictionary<string, Vector<DashboardDraftRef>>();
-            foreach (var task in all_tasks) {
-                if (task.parent_task_id > 0 && task.status == STATUS_DRAFT) {
-                    string parent = task.parent_task_id.to_string();
-                    draft_children.set(parent,
-                        (int64.parse(draft_children.get_or_default(parent) ?? "0") + 1).to_string());
-                    var rows = drafts_by_parent.get_or_default(parent);
-                    if (rows == null) {
-                        rows = new Vector<DashboardDraftRef>();
-                        drafts_by_parent.set(parent, rows);
-                    }
-                    ((!)rows).add(new DashboardDraftRef() {
-                        id = task.id,
-                        title = task.title
-                    });
-                }
-            }
-            var waiting_drafts = new Vector<DashboardWaitingDraftView>();
-            foreach (var task in all_tasks) {
-                int64 drafts = int64.parse(
-                    draft_children.get_or_default(task.id.to_string()) ?? "0");
-                if (task.status == STATUS_SUSPENDED && drafts > 0) {
-                    waiting_drafts.add(new DashboardWaitingDraftView() {
-                        task_id = task.id,
-                        title = task.title,
-                        draft_subtasks = drafts
-                    });
-                }
-            }
 
             var saddle_rows = new Vector<DashboardSaddleView>();
             foreach (var saddle in yield saddles.list_saddles()) {
@@ -333,14 +282,6 @@ namespace ClankerSaddle {
             props["activity"] = StateJson.list<DashboardActivityView>(activity.to_immutable_buffer());
             props["active_tasks"] = StateJson.list<DashboardActiveView>(active.to_immutable_buffer());
             props["pinned"] = StateJson.list<DashboardBlockView>(pinned_sorted.to_immutable_buffer());
-            props["waiting_drafts"] = StateJson.list_rich<DashboardWaitingDraftView>(
-                waiting_drafts.to_immutable_buffer(), (item, dict) => {
-                    var rows = drafts_by_parent.get_or_default(
-                        ((DashboardWaitingDraftView) item).task_id.to_string());
-                    dict["drafts"] = StateJson.list<DashboardDraftRef>(
-                        rows != null ? ((!)rows).to_immutable_buffer()
-                            : (new Vector<DashboardDraftRef>()).to_immutable_buffer());
-                });
 
             return state_service.new_slot(Scope.PAGE, new Statum.State() {
                 type_name = "dashboard",

+ 7 - 78
src/entrypoints/TaskDetailEntrypoint.vala

@@ -61,17 +61,8 @@ namespace ClankerSaddle {
         public Statum.Model.ActionDto? answer_action { get; set; }
     }
 
-    /** A subtask row in the task detail tree. */
-    /** A subtask row on the task page, with its computed display state. */
-    public class SubtaskView : Object {
-        public int64 id { get; set; }
-        public string title { get; set; default = ""; }
-        public string status { get; set; default = ""; }
-        public string display { get; set; default = ""; }
-    }
-
-    /** A dependency row (an edge to another task). */
-    public class DependencyView : Object {
+    /** A task option row (the auto-enqueue-after dropdown's entries). */
+    public class TaskOptionView : Object {
         public int64 id { get; set; }
         public string title { get; set; default = ""; }
         public string status { get; set; default = ""; }
@@ -123,8 +114,6 @@ namespace ClankerSaddle {
         public string saddle_health { get; set; default = ""; }
         public bool auto_approve { get; set; }
         public Statum.Model.ActionDto? toggle_auto_approve { get; set; }
-        public bool plan_auto_approve { get; set; }
-        public bool auto_enqueue_subtasks { get; set; }
         public string display { get; set; default = ""; }
         public bool terminal { get; set; }
         public bool failed { get; set; }
@@ -135,9 +124,6 @@ namespace ClankerSaddle {
         public bool editable_priority { get; set; }
         /** Capabilities editable while draft or queued (no worker yet). */
         public bool editable_capabilities { get; set; }
-        public int64 subtask_total { get; set; }
-        public int64 subtask_open { get; set; }
-        public int64 subtask_draft { get; set; }
         public int64 project_id { get; set; }
         public string project_name { get; set; default = ""; }
         public string git_url { get; set; default = ""; }
@@ -165,9 +151,6 @@ namespace ClankerSaddle {
         public string report_title { get; set; default = ""; }
         public Statum.Model.ActionDto? update { get; set; }
         public Statum.Model.ActionDto? comment { get; set; }
-        public Statum.Model.ActionDto? save_plan { get; set; }
-        public Statum.Model.ActionDto? subtask { get; set; }
-        public Statum.Model.ActionDto? dependency { get; set; }
         public Statum.Model.ActionDto? enqueue { get; set; }
         public Statum.Model.ActionDto? delete { get; set; }
         public Statum.Model.ActionDto? cancel { get; set; }
@@ -194,9 +177,9 @@ namespace ClankerSaddle {
     /**
      * Builds the `task` slot state shared by the task detail entrypoint and
      * the {@link RefreshTaskDataAction} pulse refresh, so both hydrate
-     * identical state: the full task with plan, branch fields and pending
+     * identical state: the full task with branch fields and pending
      * review, plus view rows for comments (with quotes), progress,
-     * blocks (typed, with options), subtasks, dependencies and sessions.
+     * blocks (typed, with options) and sessions.
      */
     public class TaskDetailState : Object {
 
@@ -229,13 +212,11 @@ namespace ClankerSaddle {
                 priority = task.priority,
                 working_branch = task.working_branch ?? "",
                 summary = task.summary ?? "",
-                plan = task.plan ?? "",
                 task_branch = task.task_branch ?? "",
                 branch_from = task.branch_from ?? "",
                 project_id = task.project_id,
                 project_name = project != null ? ((!)project).name : "",
                 git_url = project != null ? ((!)project).git_url : "",
-                parent_task_id = task.parent_task_id,
                 goal_id = task.goal_id,
                 mode = task.mode,
                 creator = task.creator,
@@ -251,9 +232,6 @@ namespace ClankerSaddle {
             pub.terminal = display.terminal;
             pub.failed = display.failed;
             pub.draft = task.status == STATUS_DRAFT;
-            pub.subtask_total = display.subtask_total;
-            pub.subtask_open = display.subtask_open;
-            pub.subtask_draft = display.subtask_draft;
             // The edit matrix (plan 11): draft edits everything but
             // status; non-draft non-terminal edits priority; terminal
             // edits nothing; capabilities stay editable until a worker
@@ -273,9 +251,6 @@ namespace ClankerSaddle {
             var sealed = new TaskActionPrivate() { task_id = task.id };
             pub.update = actions.author_private<UpdateTaskAction, TaskActionPrivate>(sealed);
             pub.comment = actions.author_private<AddCommentAction, TaskActionPrivate>(sealed);
-            pub.save_plan = actions.author_private<UpdatePlanAction, TaskActionPrivate>(sealed);
-            pub.subtask = actions.author_private<CreateSubtaskAction, TaskActionPrivate>(sealed);
-            pub.dependency = actions.author_private<AddDependencyAction, TaskActionPrivate>(sealed);
             // Plan 12: enqueue/delete are creator-only — the human's
             // actions exist just for human-created worker tasks; goal
             // tasks (and the goal agent's own run) are sequenced by
@@ -287,10 +262,6 @@ namespace ClankerSaddle {
             pub.cancel = actions.author_private<CancelTaskAction, TaskActionPrivate>(sealed);
             pub.retry = actions.author_private<RetryTaskAction, TaskActionPrivate>(sealed);
             pub.resume = actions.author_private<ResumeTaskAction, TaskActionPrivate>(sealed);
-            pub.auto_approve = task.auto_approve != 0;
-            pub.plan_auto_approve = task.plan_auto_approve;
-            pub.auto_enqueue_subtasks = task.auto_enqueue_subtasks;
-            pub.toggle_auto_approve = actions.author_private<ToggleAutoApproveAction, TaskActionPrivate>(sealed);
             pub.refresh = actions.author_private<RefreshTaskDataAction, TaskActionPrivate>(sealed, "GET");
             pub.attach_rule = actions.author_private<AttachTaskRuleAction, TaskActionPrivate>(sealed);
             pub.attach_capability = actions.author_private<AttachTaskCapabilityAction, TaskActionPrivate>(sealed);
@@ -387,29 +358,11 @@ namespace ClankerSaddle {
                 }
             }
 
-            var subtasks = new Vector<SubtaskView>();
-            var subtask_display = yield tasks.display_map_for(((!)detail).subtasks);
-            foreach (var subtask in ((!)detail).subtasks) {
-                var subtask_state = subtask_display.get_or_default(subtask.id.to_string());
-                subtasks.add(new SubtaskView() {
-                    id = subtask.id,
-                    title = subtask.title,
-                    status = subtask.status,
-                    display = subtask_state != null ? ((!)subtask_state).status : subtask.status
-                });
-            }
-
-            var dependencies = new Vector<DependencyView>();
-            var candidates = new Vector<DependencyView>();
-            foreach (var dep in ((!)detail).dependencies) {
-                var other = yield tasks.get_task(dep.depends_on_task_id);
-                dependencies.add(task_brief(other, dep.depends_on_task_id));
-            }
             // Plan 12: the auto-enqueue-after dropdown lists every
             // OTHER human-created task of the global list (any status
             // eligible as predecessor); goal tasks are excluded — the
             // goal agent sequences its own.
-            var enqueue_options = new Vector<DependencyView>();
+            var enqueue_options = new Vector<TaskOptionView>();
             string enqueue_after_title = "";
             foreach (var row in yield tasks.list_tasks(null, null)) {
                 if (row.id == task.id || row.kind == TASK_KIND_GOAL_RUN) {
@@ -418,7 +371,7 @@ namespace ClankerSaddle {
                 if (row.goal_id > 0) {
                     continue;
                 }
-                enqueue_options.add(new DependencyView() {
+                enqueue_options.add(new TaskOptionView() {
                     id = row.id,
                     title = row.title,
                     status = row.status
@@ -439,16 +392,6 @@ namespace ClankerSaddle {
                     pub.goal_branch = ((!)goal_row).branch_name;
                 }
             }
-            foreach (var row in yield tasks.list_tasks(null, task.project_id)) {
-                if (row.id != task.id) {
-                    candidates.add(new DependencyView() {
-                        id = row.id,
-                        title = row.title,
-                        status = row.status
-                    });
-                }
-            }
-
             var task_rules = new Vector<TaskRuleRowView>();
             foreach (var link in yield rules.list_task_attachments(task.id)) {
                 var rule = yield rules.get_rule(link.rule_id);
@@ -540,10 +483,7 @@ namespace ClankerSaddle {
             props["blocks"] = block_views(blocks);
             props["open_blocks"] = block_views(open_blocks);
             props["answered_blocks"] = block_views(answered_blocks);
-            props["subtasks"] = StateJson.list<SubtaskView>(subtasks.to_immutable_buffer());
-            props["dependencies"] = StateJson.list<DependencyView>(dependencies.to_immutable_buffer());
-            props["candidates"] = StateJson.list<DependencyView>(candidates.to_immutable_buffer());
-            props["enqueue_after_options"] = StateJson.list<DependencyView>(
+            props["enqueue_after_options"] = StateJson.list<TaskOptionView>(
                 enqueue_options.to_immutable_buffer());
             props["rules"] = StateJson.list<TaskRuleRowView>(task_rules.to_immutable_buffer());
             props["rule_options"] = StateJson.list<RuleOptionView>(rule_options.to_immutable_buffer());
@@ -561,17 +501,6 @@ namespace ClankerSaddle {
                 });
         }
 
-        private static DependencyView task_brief(TaskDetail? detail, int64 id) {
-            if (detail == null) {
-                return new DependencyView() { id = id, title = @"#$(id)", status = "missing" };
-            }
-            return new DependencyView() {
-                id = ((!)detail).task.id,
-                title = ((!)detail).task.title,
-                status = ((!)detail).task.status
-            };
-        }
-
         private static Slot slot_for(StateService state_service, TaskDetailPublic pub,
                 PropertyDictionary props) {
             return state_service.new_slot(Scope.PAGE, new State() {

+ 6 - 14
src/entrypoints/TasksEntrypoint.vala

@@ -133,7 +133,6 @@ namespace ClankerSaddle {
             foreach (var task in tasks_all) {
                 statuses.set(task.id.to_string(), task.status);
             }
-            var all_dependencies = yield task_service.list_all_dependencies();
             var ready = yield task_service.get_ready_tasks();
 
             // Computed display state for every listed task (review
@@ -212,7 +211,6 @@ namespace ClankerSaddle {
                     terminal = display != null && ((!)display).terminal,
                     failed = display != null && ((!)display).failed,
                     priority = task.priority,
-                    parent_task_id = task.parent_task_id,
                     project_id = task.project_id,
                     project_name = project_names.get_or_default(task.project_id.to_string()) ?? "",
                     mode = task.mode,
@@ -222,19 +220,13 @@ namespace ClankerSaddle {
                     enqueue_after_task_id = task.enqueue_after_task_id,
                     ready = is_ready
                 };
-                var parts = new StringBuilder();
-                foreach (var dep in all_dependencies) {
-                    if (dep.task_id != task.id) {
-                        continue;
-                    }
-                    if (parts.len > 0) {
-                        parts.append(", ");
-                    }
-                    parts.printf("#%lld (%s)", dep.depends_on_task_id,
-                        statuses.get_or_default(dep.depends_on_task_id.to_string()) ?? "?");
+                // Plan 12: the dependency summary became the
+                // auto-enqueue-after predecessor note.
+                if (task.enqueue_after_task_id > 0) {
+                    view.has_dependencies = true;
+                    view.dependency_summary = @"after #$(task.enqueue_after_task_id) (%s)".printf(
+                        statuses.get_or_default(task.enqueue_after_task_id.to_string()) ?? "?");
                 }
-                view.has_dependencies = parts.len > 0;
-                view.dependency_summary = parts.str;
                 var required = required_by_task.get_or_default(task.id.to_string());
                 view.capabilities = required != null ? ((!)required).to_array() : new string[0];
                 if (view.capabilities.length > 0 && task.status == STATUS_QUEUED) {

+ 0 - 8
src/main.vala

@@ -30,7 +30,6 @@ int main(string[] args) {
         db.add_entity<Project>(Project.entity_mapping);
         db.add_entity<ProjectLink>(ProjectLink.entity_mapping);
         db.add_entity<TaskEntity>(TaskEntity.entity_mapping);
-        db.add_entity<TaskDependency>(TaskDependency.entity_mapping);
         db.add_entity<TaskComment>(TaskComment.entity_mapping);
         db.add_entity<TaskBlock>(TaskBlock.entity_mapping);
         db.add_entity<TaskProgress>(TaskProgress.entity_mapping);
@@ -63,7 +62,6 @@ int main(string[] args) {
         application.add_scoped<CapabilityService>();
         application.add_scoped<AllocationService>();
         application.add_scoped<TimelineService>();
-        application.add_singleton<SubtaskEventQueue>();
         application.add_singleton<SaddlePollState>();
         application.add_singleton<StatePulse>();
         application.add_singleton<PushService>();
@@ -104,16 +102,12 @@ int main(string[] args) {
         statum.action<CreateTaskAction>();
         statum.action<UpdateTaskAction>();
         statum.action<AddCommentAction>();
-        statum.action<UpdatePlanAction>();
         statum.action<AnswerBlockAction>();
-        statum.action<CreateSubtaskAction>();
-        statum.action<AddDependencyAction>();
         statum.action<EnqueueTaskAction>();
         statum.action<DeleteTaskAction>();
         statum.action<CancelTaskAction>();
         statum.action<RetryTaskAction>();
         statum.action<ResumeTaskAction>();
-        statum.action<ToggleAutoApproveAction>();
         statum.action<RefreshTasksDataAction>();
         statum.action<CreateGoalAction>();
         statum.action<StartGoalAction>();
@@ -169,8 +163,6 @@ int main(string[] args) {
             new EndpointRoute("/api/saddle/results", Method.POST));
         application.add_endpoint<SaddleNoteEndpoint>(
             new EndpointRoute("/api/saddle/note", Method.POST));
-        application.add_endpoint<SaddleSubtaskEndpoint>(
-            new EndpointRoute("/api/saddle/subtask", Method.POST));
         application.add_endpoint<SaddleTaskCreateEndpoint>(
             new EndpointRoute("/api/saddle/task/create", Method.POST));
         application.add_endpoint<SaddleTaskDeleteEndpoint>(

+ 1 - 25
src/pages/home.html

@@ -137,7 +137,7 @@
                     <button type="button" data-push-bell
                             stm-if="dashboard.push_subscribed">Turn off notifications</button>
                     <span class="muted" stm-if="!dashboard.push_subscribed">— be told when an agent asks a
-                        question, raises an RFC or review, or waits on draft subtasks</span>
+                        question, raises an RFC or review</span>
                     <span class="muted" stm-if="dashboard.push_subscribed">— this browser receives question,
                         RFC, review and waiting-on-drafts alerts; turn on "Notify me" per task for its
                         start, failure and completion</span>
@@ -183,30 +183,6 @@
             <p stm-if="dashboard.pinned.length == 0" class="muted">Nothing needs a human —
                 no open questions, RFCs or reviews.</p>
 
-            <!-- "Waiting on subtasks to be enqueued" (plan 11 task D):
-                 every suspended parent whose draft subtasks hold up the
-                 work, linked straight to where Enqueue lives. -->
-            <h3 stm-if="dashboard.waiting_drafts.length > 0">Waiting on subtasks to be enqueued
-                (<span stm-text="dashboard.waiting_drafts.length">0</span>)</h3>
-            <p class="muted" stm-if="dashboard.waiting_drafts.length > 0">These tasks are parked until their
-                draft subtasks are enqueued — follow a link and press "Enqueue task".</p>
-            <div class="cards" stm-if="dashboard.waiting_drafts.length > 0">
-                <section class="card" stm-for-waiting-in="dashboard.waiting_drafts" stm-key="waiting.task_id">
-                    <h3><a stm-attribute.href="'/tasks/' + waiting.task_id" stm-text="waiting.title">task</a></h3>
-                    <p><span class="badge badge-suspended">parked</span>
-                        <span stm-text="waiting.draft_subtasks">0</span>
-                        <span stm-text="waiting.draft_subtasks == 1 ? 'subtask' : 'subtasks'">subtasks</span>
-                        still in draft:</p>
-                    <ul class="waiting-drafts">
-                        <li stm-for-draft-in="waiting.drafts" stm-key="draft.id">
-                            <a stm-attribute.href="'/tasks/' + draft.id" stm-text="draft.title">draft</a>
-                            <span class="badge badge-draft">draft</span>
-                        </li>
-                    </ul>
-                    <p class="muted">Enqueue them from their pages so this task can finish.</p>
-                </section>
-            </div>
-
             <h3 stm-if="dashboard.active_tasks.length > 0">Active tasks</h3>
             <table stm-if="dashboard.active_tasks.length > 0">
                 <thead><tr><th>Task</th><th>Status</th><th>Branch</th><th>Latest progress</th></tr></thead>

+ 8 - 105
src/pages/task_detail.html

@@ -239,8 +239,9 @@
         <div stm-if="task != null && task.exists">
             <p><a href="/tasks">All tasks</a> · project
                 <a stm-attribute.href="'/projects/' + task.project_id" stm-text="task.project_name">project</a>
-                <span stm-if="task.parent_task_id > 0"> · subtask of
-                    <a stm-attribute.href="'/tasks/' + task.parent_task_id" stm-text="'#' + task.parent_task_id">#parent</a></span></p>
+                <span stm-if="task.enqueue_after_task_id > 0"> · auto-enqueues after
+                    <a stm-attribute.href="'/tasks/' + task.enqueue_after_task_id"
+                       stm-text="'#' + task.enqueue_after_task_id">after</a></span></p>
 
             <!-- Progress feed ABOVE the status card (plan 11 task D):
                  the agent's newest line leads the page, its history
@@ -302,12 +303,9 @@
                     <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' && task.subtask_draft > 0" class="muted">
-                        Parked — <span stm-text="task.subtask_draft">0</span> subtask(s) still in draft:
-                        enqueue them so this task's work can finish.</p>
-                    <p stm-if="task.status == 'suspended' && task.subtask_draft == 0" class="muted">Parked — waiting on
-                        subtasks (<span stm-text="task.subtask_open">0</span> open). The task
-                        resumes with a status report whenever one finishes.</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>
                     <form class="inline">
                         <input type="hidden" name="task_id" stm-attribute.value="task.id">
                         <button type="button" stm-if="task.draft" class="primary"
@@ -403,23 +401,6 @@
                         <input type="text" name="task_branch" stm-attribute.value="task.task_branch" placeholder="task/<id>-<slug>"></div>
                     <div class="form-row"><label>Branch from</label>
                         <input type="text" name="branch_from" stm-attribute.value="task.branch_from"></div>
-                    <div class="form-row"><label>Task options</label>
-                        <p class="inline">
-                            <label class="option-card">
-                                <input type="checkbox" name="plan_auto_approve" stm-attribute.checked="task.plan_auto_approve">
-                                <span>Auto-approve plan — this task's plan review is answered instantly</span>
-                            </label></p>
-                        <p class="inline">
-                            <label class="option-card">
-                                <input type="checkbox" name="auto_enqueue_subtasks" stm-attribute.checked="task.auto_enqueue_subtasks">
-                                <span>Auto-enqueue agent-created subtasks</span>
-                            </label></p>
-                        <p class="inline">
-                            <label class="option-card">
-                                <input type="checkbox" name="auto_approve" stm-attribute.checked="task.auto_approve">
-                                <span>Auto-approve subtasks — descendants' review and plan blocks are answered instantly</span>
-                            </label></p>
-                    </div>
                     <button type="submit">Save</button>
                 </form>
                 <form stm-action="task.update" stm-if="!task.editable_all && task.editable_priority">
@@ -430,34 +411,7 @@
                 </form>
             </details>
 
-            <!-- Plan detail behind an expander (plan 11 task D): the
-                 pending plan review still renders the markdown in its
-                 block section below — this expander is the reference
-                 copy plus the editor. -->
-            <details class="plan-expander" stm-if="task.plan.length > 0">
-                <summary><strong>Plan</strong></summary>
-                <div class="card md" stm-markdown="task.plan">plan</div>
-                <details>
-                    <summary>Edit plan</summary>
-                    <form stm-action="task.save_plan">
-                        <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                        <div class="form-row"><label for="plan-editor">Plan (markdown)</label>
-                            <textarea id="plan-editor" name="plan" rows="12" stm-attribute.value="task.plan"></textarea></div>
-                        <button type="submit" class="primary">Save plan</button>
-                    </form>
-                </details>
-            </details>
-            <details class="plan-expander" stm-if="task.plan.length == 0">
-                <summary><strong>Plan</strong> — none yet</summary>
-                <form stm-action="task.save_plan">
-                    <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                    <div class="form-row"><label for="plan-editor">Plan (markdown)</label>
-                        <textarea id="plan-editor" name="plan" rows="12" placeholder="The agent fills this in via `saddle plan` — or write it yourself."></textarea></div>
-                    <button type="submit" class="primary">Save plan</button>
-                </form>
-            </details>
-
-            <!-- Current blocks render in full (plan 11 task D);
+<!-- Current blocks render in full (plan 11 task D);
                  answered ones fold behind the history expander. -->
             <h3>Blocks <span class="muted" stm-if="task.open_blocks.length > 0">(<span stm-text="task.open_blocks.length">0</span> open)</span></h3>
             <div stm-for-block-in="task.open_blocks" stm-key="block.id"
@@ -482,7 +436,7 @@
 
                 <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.length > 0 ? block.body : task.plan" data-plan-body>plan</div>
+                    <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="answer" value="Plan approved. Proceed with implementation.">
@@ -582,57 +536,6 @@
                 </form>
             </dialog>
 
-            <h3>Subtasks</h3>
-            <p class="inline">Auto-approve subtask reviews:
-                <strong stm-text="task.auto_approve ? 'on' : 'off'">off</strong>
-                <button type="button" stm-action="task.toggle_auto_approve"
-                        stm-text="task.auto_approve ? 'Turn off' : 'Turn on'">Turn on</button>
-                <span class="muted">— covered descendants' review AND plan blocks are answered
-                    automatically (questions and RFCs still block; this task's own review is
-                    never auto-approved). This task's own plan is governed by the
-                    auto-approve-plan option.</span></p>
-            <p class="inline" stm-if="task.auto_enqueue_subtasks"><span class="badge badge-done">auto-enqueue on</span>
-                <span class="muted">— subtasks created under this task enqueue immediately
-                    (and inherit the setting).</span></p>
-            <table>
-                <thead><tr><th>#</th><th>Title</th><th>Status</th></tr></thead>
-                <tbody>
-                    <tr stm-for-subtask-in="task.subtasks">
-                        <td stm-text="subtask.id">id</td>
-                        <td><a stm-attribute.href="'/tasks/' + subtask.id" stm-text="subtask.title">title</a></td>
-                        <td><span stm-attribute.class="'badge badge-' + subtask.status" stm-text="subtask.display">status</span></td>
-                    </tr>
-                    <tr stm-if="task.subtasks.length == 0"><td colspan="3" class="muted">No subtasks.</td></tr>
-                </tbody>
-            </table>
-            <details>
-                <summary><strong>New subtask</strong></summary>
-                <form stm-action="task.subtask">
-                    <div class="form-row"><label>Title</label>
-                        <input type="text" name="title" required></div>
-                    <div class="form-row"><label>Description</label>
-                        <textarea name="description" rows="3"></textarea></div>
-                    <button type="submit">Create subtask</button>
-                </form>
-            </details>
-
-            <h3>Dependencies</h3>
-            <p stm-if="task.dependencies.length > 0">Depends on:
-                <span stm-for-dep-in="task.dependencies">
-                    <a stm-attribute.href="'/tasks/' + dep.id" stm-text="'#' + dep.id + ' ' + dep.title">dep</a>
-                    <span stm-attribute.class="'badge badge-' + dep.status" stm-text="dep.status">status</span>
-                </span>
-            </p>
-            <p stm-if="task.dependencies.length == 0" class="muted">No dependencies.</p>
-            <form class="inline" stm-action="task.dependency">
-                <label for="dep-target">Depends on</label>
-                <select id="dep-target" name="depends_on_task_id" required>
-                    <option stm-for-candidate-in="task.candidates" stm-attribute.value="candidate.id"
-                             stm-text="'#' + candidate.id + ' ' + candidate.title">candidate</option>
-                </select>
-                <button type="submit">Add dependency</button>
-            </form>
-
             <h3>Rules</h3>
             <p class="muted">Task rules lead the effective set dispatched with this task.</p>
             <div stm-for-rule-in="task.rules" stm-key="rule.link_id" class="card">

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 425 - 526
test/web_stack_test.py


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно