Răsfoiți Sursa

goals: hide goal-agent runs from every task listing

The internal goal_run rows never surface where tasks are browsed —
the queue, the project pages (detail list + tallies), the dashboard
(counters, ready count, active tasks), the activity timeline (their
own lifecycle, mirrored blocks, comments, telemetry and goal-event
task links) — and a saddle hosting a goal agent reads as supervising
its GOAL, not as running the internal task. Goals are browsed from
the goal page only.
clanker 1 lună în urmă
părinte
comite
7004c39d4e

+ 25 - 6
src/Services/TimelineService.vala

@@ -56,8 +56,15 @@ namespace ClankerSaddle.Services {
          */
         public async Vector<TimelineEvent> recent_window() throws Error {
             var titles = new Dictionary<string, string>();
+            // Goal-agent runs are internal tasks: their own lifecycle,
+            // mirrored blocks and telemetry never surface as task
+            // events (the goal-keyed events below carry the story).
+            var run_tasks = new Invercargill.DataStructures.HashSet<string>();
             foreach (var task in yield db.query<TaskEntity>().materialise_async()) {
                 titles.set(task.id.to_string(), task.title);
+                if (task.kind == TASK_KIND_GOAL_RUN) {
+                    run_tasks.add(task.id.to_string());
+                }
             }
             var saddle_names = new Dictionary<string, string>();
             foreach (var saddle in yield db.query<Saddle>().materialise_async()) {
@@ -73,6 +80,9 @@ namespace ClankerSaddle.Services {
 
             foreach (var task in yield db.query<TaskEntity>()
                     .order_by_desc(expr("id")).limit(WINDOW).materialise_async()) {
+                if (run_tasks.has(task.id.to_string())) {
+                    continue;
+                }
                 string title = titles.get_or_default(task.id.to_string()) ?? task.title;
                 if (task.created_at != null) {
                     events.add(new TimelineEvent() {
@@ -116,6 +126,9 @@ namespace ClankerSaddle.Services {
 
             foreach (var block in yield db.query<TaskBlock>()
                     .order_by_desc(expr("id")).limit(WINDOW).materialise_async()) {
+                if (run_tasks.has(block.task_id.to_string())) {
+                    continue;
+                }
                 if (block.created_at != null) {
                     events.add(new TimelineEvent() {
                         kind = "block-raised", at_epoch = ((!)block.created_at).to_unix(),
@@ -138,7 +151,7 @@ namespace ClankerSaddle.Services {
 
             foreach (var comment in yield db.query<TaskComment>()
                     .order_by_desc(expr("id")).limit(WINDOW).materialise_async()) {
-                if (comment.created_at == null) {
+                if (comment.created_at == null || run_tasks.has(comment.task_id.to_string())) {
                     continue;
                 }
                 events.add(new TimelineEvent() {
@@ -152,7 +165,7 @@ namespace ClankerSaddle.Services {
 
             foreach (var progress in yield db.query<TaskProgress>()
                     .order_by_desc(expr("id")).limit(WINDOW).materialise_async()) {
-                if (progress.created_at == null) {
+                if (progress.created_at == null || run_tasks.has(progress.task_id.to_string())) {
                     continue;
                 }
                 events.add(new TimelineEvent() {
@@ -165,7 +178,7 @@ namespace ClankerSaddle.Services {
 
             foreach (var session in yield db.query<Session>()
                     .order_by_desc(expr("id")).limit(WINDOW).materialise_async()) {
-                if (session.started_at == null) {
+                if (session.started_at == null || run_tasks.has(session.task_id.to_string())) {
                     continue;
                 }
                 string saddle = saddle_names.get_or_default(session.saddle_id.to_string())
@@ -194,7 +207,7 @@ namespace ClankerSaddle.Services {
                     continue;
                 }
                 string? task_key = session_tasks.get_or_default(entry.session_id.to_string());
-                if (task_key == null) {
+                if (task_key == null || run_tasks.has((!)task_key)) {
                     continue;
                 }
                 string? lead = first_line(entry.output_text ?? entry.command_text);
@@ -217,11 +230,17 @@ namespace ClankerSaddle.Services {
                 if (goal_event.created_at == null) {
                     continue;
                 }
+                // A goal event may reference the goal agent's own run
+                // task — the row stays goal-keyed, but the internal
+                // task never becomes a clickable task reference.
+                int64 event_task_id = run_tasks.has(goal_event.task_id.to_string())
+                    ? 0 : goal_event.task_id;
                 events.add(new TimelineEvent() {
                     kind = @"goal-$(goal_event.kind)",
                     at_epoch = ((!)goal_event.created_at).to_unix(),
-                    task_id = goal_event.task_id,
-                    task_title = titles.get_or_default(goal_event.task_id.to_string()) ?? "",
+                    task_id = event_task_id,
+                    task_title = event_task_id > 0
+                        ? titles.get_or_default(event_task_id.to_string()) ?? "" : "",
                     goal_id = goal_event.goal_id,
                     goal_title = goal_titles.get_or_default(goal_event.goal_id.to_string()) ?? "",
                     text = snippet(goal_event.text), sequence = sequence++

+ 36 - 1
src/entrypoints/HomeEntrypoint.vala

@@ -17,6 +17,13 @@ namespace ClankerSaddle {
         public string host { get; set; default = ""; }
         public int64 current_task_id { get; set; }
         public string current_task_title { get; set; default = ""; }
+        /**
+         * Set when the saddle's current task is a goal agent's run:
+         * the card then links the GOAL (runs are internal — never a
+         * task destination at a glance).
+         */
+        public int64 current_goal_id { get; set; }
+        public string current_goal_title { get; set; default = ""; }
     }
 
     /** One entry in the dashboard's cross-task activity timeline. */
@@ -116,6 +123,12 @@ namespace ClankerSaddle {
             var all_tasks = yield tasks.list_tasks(null, null);
             foreach (var task in all_tasks) {
                 titles.set(task.id.to_string(), task.title);
+                // Goal-agent runs are internal tasks — they never count
+                // against the human-facing queue summary (the goal page
+                // owns their visibility).
+                if (task.kind == TASK_KIND_GOAL_RUN) {
+                    continue;
+                }
                 switch (task.status) {
                     case STATUS_DRAFT:
                         pub.draft_count++;
@@ -141,7 +154,13 @@ namespace ClankerSaddle {
                         break;
                 }
             }
-            pub.ready_count = (yield tasks.get_ready_tasks()).length;
+            int64 ready_count = 0;
+            foreach (var task in yield tasks.get_ready_tasks()) {
+                if (task.kind != TASK_KIND_GOAL_RUN) {
+                    ready_count++;
+                }
+            }
+            pub.ready_count = ready_count;
 
 
             var saddle_rows = new Vector<DashboardSaddleView>();
@@ -161,6 +180,17 @@ namespace ClankerSaddle {
                         .where(expr("id == $0", elem<int64?>(saddle.current_task_id)))
                         .first_async();
                     view.current_task_title = task != null ? ((!)task).title : "";
+                    // A goal agent's run reads as its GOAL here, not as
+                    // the internal run task.
+                    if (task != null && ((!)task).kind == TASK_KIND_GOAL_RUN) {
+                        var goal = yield db.query<Goal>()
+                            .where(expr("id == $0", elem<int64?>(((!)task).goal_id)))
+                            .first_async();
+                        if (goal != null) {
+                            view.current_goal_id = ((!)goal).id;
+                            view.current_goal_title = ((!)goal).title;
+                        }
+                    }
                 }
                 saddle_rows.add(view);
             }
@@ -168,6 +198,11 @@ namespace ClankerSaddle {
             var active = new Vector<DashboardActiveView>();
             var pinned = new Vector<DashboardBlockView>();
             foreach (var task in yield tasks.list_tasks(null, null)) {
+                // Goal-agent runs are internal — never an "active task"
+                // row (the goal page shows the agent's activity).
+                if (task.kind == TASK_KIND_GOAL_RUN) {
+                    continue;
+                }
                 if (task.status == STATUS_ASSIGNED || task.status == STATUS_WORKING
                         || task.status == STATUS_BLOCKED || task.status == STATUS_REVIEW_REQUESTED) {
                     var row = new DashboardActiveView() {

+ 10 - 0
src/entrypoints/ProjectsEntrypoint.vala

@@ -54,6 +54,11 @@ namespace ClankerSaddle {
                     if (task.project_id != project.id) {
                         continue;
                     }
+                    // Goal-agent runs are internal tasks — they never
+                    // count against the project's task tallies.
+                    if (task.kind == TASK_KIND_GOAL_RUN) {
+                        continue;
+                    }
                     if (task.status == STATUS_DONE || task.status == STATUS_FAILED) {
                         view.done_tasks++;
                     } else {
@@ -220,6 +225,11 @@ namespace ClankerSaddle {
             var task_display = yield task_service.display_map_for(project_tasks);
             var task_rows = new Vector<ProjectTaskRowView>();
             foreach (var task in project_tasks) {
+                // Goal-agent runs are internal — the goal page owns
+                // their visibility.
+                if (task.kind == TASK_KIND_GOAL_RUN) {
+                    continue;
+                }
                 var display = task_display.get_or_default(task.id.to_string());
                 task_rows.add(new ProjectTaskRowView() {
                     id = task.id,

+ 14 - 0
src/entrypoints/SaddlesEntrypoint.vala

@@ -44,6 +44,9 @@ namespace ClankerSaddle {
         public Statum.Model.ActionDto? update_identity { get; set; }
         public int64 current_task_id { get; set; }
         public string current_task_title { get; set; default = ""; }
+        /** Set when the current task is a goal agent's run: link the goal instead. */
+        public int64 current_goal_id { get; set; }
+        public string current_goal_title { get; set; default = ""; }
         public string allocated_display { get; set; default = ""; }
         public string token { get; set; default = ""; }
         public Statum.Model.ActionDto? unregister { get; set; }
@@ -180,6 +183,17 @@ namespace ClankerSaddle {
                         .where(expr("id == $0", elem<int64?>(saddle.current_task_id)))
                         .first_async();
                     view.current_task_title = task != null ? ((!)task).title : "";
+                    // A goal agent's run reads as its GOAL, not as the
+                    // internal run task.
+                    if (task != null && ((!)task).kind == TASK_KIND_GOAL_RUN) {
+                        var goal = yield db.query<Goal>()
+                            .where(expr("id == $0", elem<int64?>(((!)task).goal_id)))
+                            .first_async();
+                        if (goal != null) {
+                            view.current_goal_id = ((!)goal).id;
+                            view.current_goal_title = ((!)goal).title;
+                        }
+                    }
                 }
                 rows.add(view);
             }

+ 6 - 0
src/entrypoints/TasksEntrypoint.vala

@@ -152,6 +152,12 @@ namespace ClankerSaddle {
             }
             var listed = new Vector<ClankerSaddle.Data.TaskEntity>();
             foreach (var task in yield task_service.list_tasks(null, null)) {
+                // Goal-agent runs are internal: they never list here —
+                // goals are browsed from the goal page, which alone
+                // exposes the run task (its terminals, its progress).
+                if (task.kind == TASK_KIND_GOAL_RUN) {
+                    continue;
+                }
                 if (goal_filter_id > 0 && task.goal_id != goal_filter_id) {
                     continue;
                 }

+ 5 - 1
src/pages/home.html

@@ -204,7 +204,11 @@
                     <h3 stm-text="saddle.name">name</h3>
                     <p><span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
                         <span class="muted" stm-text="saddle.host">host</span></p>
-                    <p stm-if="saddle.current_task_id > 0">Working on
+                    <p stm-if="saddle.current_goal_id > 0">Supervising goal
+                        <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
+                           stm-text="saddle.current_goal_title">goal</a>
+                        <span class="badge badge-kind-goal">goal</span></p>
+                    <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Working on
                         <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a></p>
                     <p stm-if="saddle.current_task_id == 0" class="muted">No current task</p>
                 </section>

+ 6 - 1
src/pages/saddles.html

@@ -26,7 +26,12 @@
                     <h3 stm-text="saddle.name">name</h3>
                     <p><span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
                         <span class="muted" stm-text="saddle.host">host</span></p>
-                    <p stm-if="saddle.current_task_id > 0">Current task:
+                    <p stm-if="saddle.current_goal_id > 0">Supervising goal:
+                        <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
+                           stm-text="saddle.current_goal_title">goal</a>
+                        <span class="badge badge-kind-goal">goal</span>
+                        <span class="muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
+                    <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Current task:
                         <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a>
                         <span class="muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
                     <p stm-if="saddle.current_task_id == 0" class="muted">No current task</p>

+ 36 - 1
test/web_stack_test.py

@@ -5246,8 +5246,43 @@ def bundle_goals(ctx):
     check(any(e["kind"] == "started" for e in goal_state(goal1)["events"]),
           "the narrative records the start")
 
-    print("== plan 12: the goal agent plans, plan review, revisions ==")
+    print("== feedback: goal-agent runs are hidden from every task listing ==")
+    # The internal run task never lists anywhere tasks are browsed —
+    # only the goal page exposes it (worker tasks stay listed). The
+    # plan script is armed BEFORE the first dispatch so this section's
+    # waits never consume the round the plan-review flow below needs.
     fake.goal_script = [("plan", "# Plan v1\n\n1. Add tests\n2. Ship")]
+    check(wait_for(lambda: task_state(auth, run_task)["status"]
+                   in ("assigned", "working") or False,
+                   timeout=60, what="run task dispatches") is not None,
+          "the run task reaches its saddle")
+    queue_rows = slot_public(auth, "/tasks", "tasks")["tasks"]
+    check(all(int(t["id"]) != run_task for t in queue_rows),
+          "the tasks queue never lists the goal-agent run task")
+    check(all(t["kind"] != "goal_run" for t in queue_rows),
+          "no goal_run rows leak into the queue at all")
+    project_state = slot_public(auth, "/projects/1", "project")
+    check(all(int(t["id"]) != run_task for t in project_state["tasks"]),
+          "the project page's task list excludes the run task")
+    dashboard = slot_public(auth, "/", "dashboard")
+    check(all(int(a["id"]) != run_task for a in dashboard["active_tasks"]),
+          "the dashboard's active tasks exclude the run task")
+    check(all(int(i["task_id"]) != run_task for i in dashboard["activity"]),
+          "the activity timeline never references the run task")
+    check(wait_for(lambda: int(next(
+              (s for s in slot_public(auth, "/", "dashboard")["saddles"]
+               if s["name"] == "fake-saddle"), {})
+          .get("current_goal_id") or 0) == goal1 or False,
+          timeout=30, what="saddle card links the goal") is not None,
+          "a saddle hosting the goal agent reads as supervising the goal")
+    fake_dash_card = next(s for s in slot_public(auth, "/", "dashboard")["saddles"]
+                          if s["name"] == "fake-saddle")
+    check(fake_dash_card["current_goal_title"] == "Ship the flange",
+          "the supervision link carries the goal title")
+
+    print("== plan 12: the goal agent plans, plan review, revisions ==")
+    # (The plan script was armed before the run's first dispatch — see
+    # the listing-hiding section above — so the first round submits it.)
     check(wait_for(lambda: goal_state(goal1)["status"] == "plan_review" or False,
                    timeout=60, what="goal agent submits its plan") is not None,
           "the goal agent's plan push enters plan review")