using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; using InvercargillJson; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle.Data; using ClankerSaddle.Services; namespace ClankerSaddle { /** A project link row on the goal page. */ public class GoalLinkView : Object { public int64 id { get; set; } public int64 project_id { get; set; } public string name { get; set; default = ""; } public string relationship { get; set; default = ""; } public Statum.Model.ActionDto? remove { get; set; } } /** * One goal block (question/RFC/plan revision/completion) as the * page renders it — plan 15 C1: the view over the goal-run * task's own `task_blocks` row (there is no separate goal-side * row). */ public class GoalBlockView : Object { public int64 id { get; set; } public string question { get; set; default = ""; } public string block_type { get; set; default = BLOCK_QUESTION; } public string[] options { get; set; default = {}; } public string body { get; set; default = ""; } public bool answered { get; set; default = false; } public string answer { get; set; default = ""; } public string answered_at_display { get; set; default = ""; } public string answered_by { get; set; default = ""; } public string created_display { get; set; default = ""; } public Statum.Model.ActionDto? answer_action { get; set; } /** True while this is THE pending plan review (the plan section renders its body). */ public bool is_plan_review { get; set; default = false; } /** True while this is THE pending completion review. */ public bool is_completion_review { get; set; default = false; } /** Whether the block body renders as its own preview (not duplicated by a section). */ public bool show_body { get; set; default = true; } /** Plan revisions: the answer was the stable approval verdict. */ public bool approved { get; set; default = false; } } /** * One comment on the goal thread (plan 15 C2: the run task's own * `task_comments` row). {@link is_system} marks the * harness-authored bookkeeping rows — the distinct muted system * styling, never an agent row. */ public class GoalCommentView : Object { public string author { get; set; default = ""; } public string body { get; set; default = ""; } public string quote { get; set; default = ""; } public string created_display { get; set; default = ""; } public bool from_agent { get; set; default = false; } public bool is_system { get; set; default = false; } } /** One progress line from the goal agent's runs (`saddle progress-update`). */ public class GoalProgressView : Object { public int64 id { get; set; } public string summary { get; set; default = ""; } public string when_display { get; set; default = ""; } } /** * One supervision-narrative row (plan 15 C3: the DERIVED read-time * view over the goal's lifecycle stamps, the run task's blocks * and comments, and the goal's tasks' transitions — no * goal_events rows exist). */ public class GoalEventView : Object { public string kind { get; set; default = ""; } public string text { get; set; default = ""; } public int64 task_id { get; set; } public string at_display { get; set; default = ""; } } /** One task row under the goal (the status strip's list). */ public class GoalTaskView : Object { public int64 id { get; set; } public string title { get; set; default = ""; } public string status { get; set; default = "draft"; } public string display { get; set; default = "draft"; } public string mode { get; set; default = "implement"; } public string creator { get; set; default = "human"; } public string updated_display { get; set; default = ""; } /** The task's required capability tags (plan 14 B5 row badges). */ public string[] capabilities { get; set; default = {}; } } /** * One effective rule of the goal (plan 14 B5): the rules that * govern the goal agent and every worker under it — the run * task's own set, the primary project's, and the co-developed * links' in link order. */ public class GoalRuleView : Object { public int64 id { get; set; } public string name { get; set; default = ""; } public string body { get; set; default = ""; } } /** One goal-agent run (a session of the goal-run task). */ public class GoalRunView : Object { public int64 id { get; set; } public string started_display { get; set; default = ""; } public string ended_display { get; set; default = ""; } public bool open { get; set; default = false; } public string outcome { get; set; default = ""; } public string terminal_url { get; set; default = ""; } /** The saddle that hosted the run ("" when unknown). */ public string saddle_name { get; set; default = ""; } } /** One branch of the daemon-reported goal push state. */ public class GoalPushBranchView : Object { public string branch { get; set; default = ""; } public bool pushed { get; set; default = false; } } /** A project option chip (the goal link form's select). */ public class GoalProjectOptionView : Object { public int64 id { get; set; } public string name { get; set; default = ""; } } /** Public root of the `/goals` slot. */ public class GoalsListPublic : Object { public Statum.Model.ActionDto create { get; set; } } /** * The scalar half of the goal page's public state (the lists ride * alongside as separate properties; see {@link GoalState.build}). */ public class GoalPublic : Object { public bool exists { get; set; } public int64 id { get; set; } public int64 project_id { get; set; } public string project_name { get; set; default = ""; } public string title { get; set; default = ""; } public string brief { get; set; default = ""; } public string branch_name { get; set; default = ""; } public string base_branch { get; set; default = ""; } public string status { get; set; default = "draft"; } public string display { get; set; default = "draft"; } public int64 priority { get; set; default = 3; } public string plan { get; set; default = ""; } public string completion_report { get; set; default = ""; } public string compare_url { get; set; default = ""; } public string created_display { get; set; default = ""; } public string started_display { get; set; default = ""; } public string completed_display { get; set; default = ""; } public bool terminal { get; set; } public int64 draft_tasks { get; set; } public int64 queued_tasks { get; set; } public int64 running_tasks { get; set; } public int64 done_tasks { get; set; } public int64 failed_tasks { get; set; } public int64 run_task_id { get; set; } /** True while a completion report awaits review. */ public bool completion_pending { get; set; } /** The saddle currently hosting the goal agent ("" while unassigned). */ public string agent_saddle { get; set; default = ""; } /** Health line for the agent's saddle ("" when none). */ public string saddle_health { get; set; default = ""; } /** The agent's newest recorded activity line ("" when none). */ public string current_command { get; set; default = ""; } public string current_command_display { get; set; default = ""; } /** The current activity's entry has not completed yet. */ public bool current_command_running { get; set; } /** * Plan 15 C4: the ONE muted activity line folded into the * status head (the Current-activity card is gone) — * "running: `cmd`" for the open round, "parked awaiting " * for the parked ones ("" when there is nothing to say). */ public string current_activity { get; set; default = ""; } public Statum.Model.ActionDto? start { get; set; } /** * The draft brief-card editor's save (title/brief/priority): * authored ONLY while draft — its presence IS the page's * edit-draft gate, so a started goal renders no editor at all. */ public Statum.Model.ActionDto? update { get; set; } public Statum.Model.ActionDto? pause { get; set; } public Statum.Model.ActionDto? resume { get; set; } public Statum.Model.ActionDto? cancel { get; set; } /** * Plan 14 B1 parity: re-offer a failed goal's supervision (the * task page's Retry, worded for the run). */ public Statum.Model.ActionDto? retry_run { get; set; } /** * Plan 14 B1 parity: the run is stale/parked on a dead saddle * — requeue it; the next dispatch restores the workspace. */ public Statum.Model.ActionDto? requeue_run { get; set; } /** * Goal 4 task 1 (Force Wake): the human recovery lever — * authored only while the goal is active and its run task is * parked (suspended); pressing it sets the goal force-wake * flag (GoalService.force_wake_goal holds the guards). */ public Statum.Model.ActionDto? force_wake { get; set; } public Statum.Model.ActionDto? comment { get; set; } public Statum.Model.ActionDto? add_link { get; set; } /** * Goal 8 task 2 B: applies a stack's members as goal links. * Authored while the goal is not terminal (the involved set * is frozen once complete/failed/cancelled). */ public Statum.Model.ActionDto? apply_stack { get; set; } public Vector stack_options { get; set; default = new Vector(); } /** The sealed GET refresh action (live pulse rehydration). */ public Statum.Model.ActionDto? refresh { get; set; } } /** * The dependency bundle every goal-view build threads (plan 16 * R2, arch item 12): the services and registries {@link GoalState}'s * builders need, constructed once per entrypoint or refresh — * one object where the 17-parameter populate signature (and * build's own 10-dependency thread) used to be. */ public class GoalViewContext : Object { public GoalService goals { get; set; } public TaskService tasks { get; set; } public ProjectService projects { get; set; } public EventService events { get; set; } public InvercargillSql.Orm.OrmSession db { get; set; } public ActionRegistry? actions { get; set; default = null; } public SaddleService? saddles { get; set; default = null; } public SaddlePollState? polls { get; set; default = null; } public RuleService? rules { get; set; default = null; } public StackService? stacks { get; set; default = null; } public GoalViewContext(GoalService goals, TaskService tasks, ProjectService projects, EventService events, InvercargillSql.Orm.OrmSession db, ActionRegistry? actions = null, SaddleService? saddles = null, SaddlePollState? polls = null, RuleService? rules = null, StackService? stacks = null) { this.goals = goals; this.tasks = tasks; this.projects = projects; this.events = events; this.db = db; this.actions = actions; this.saddles = saddles; this.polls = polls; this.rules = rules; this.stacks = stacks; } } /** * A built goal view (plan 16 R2, arch item 12): the public scalar * half plus the section lists the goal page serialises — * populate's out-params became fields. */ public class GoalView : Object { public GoalPublic pub { get; set; default = new GoalPublic(); } public Vector links { get; set; default = new Vector(); } public Vector blocks { get; set; default = new Vector(); } public Vector blocks_open { get; set; default = new Vector(); } public Vector blocks_answered { get; set; default = new Vector(); } public Vector goal_tasks { get; set; default = new Vector(); } public Vector events { get; set; default = new Vector(); } public Vector comments { get; set; default = new Vector(); } public Vector runs { get; set; default = new Vector(); } public Vector push_branches { get; set; default = new Vector(); } public Vector progress { get; set; default = new Vector(); } } /** * State builder for the goal surfaces: fills a {@link GoalView} * from a goal row — {@link GoalState.full_view} builds the goal * page's whole aggregate (narrative, runs, sections), {@link * GoalState.summary_view} the goals-list row's scalars (the old * `full` flag became the two methods). */ public class GoalState : Object { /** * Builds the goal page's whole slot from `goal_id` — the * shape the entrypoint sets and the sealed refresh action * re-hydrates. `exists` is false when the goal is missing. */ public static async Statum.Slot build(int64 goal_id, GoalViewContext ctx, StateService state_service) throws GLib.Error { var view = new GoalView(); GoalBlockView? plan_pending = null; GoalBlockView? completion_pending = null; var plan_revisions = new Vector(); var project_options = new Vector(); var effective_rules = new Vector(); var goal = yield ctx.goals.get_goal(goal_id); if (goal != null) { view = yield full_view((!)goal, ctx); foreach (var block in view.blocks) { if (block.is_plan_review && plan_pending == null) { plan_pending = block; } if (block.is_completion_review && completion_pending == null) { completion_pending = block; } if (block.block_type == BLOCK_PLAN) { plan_revisions.add(block); } } foreach (var project in yield ctx.projects.list_projects()) { if (project.id != ((!)goal).project_id) { project_options.add(new GoalProjectOptionView() { id = project.id, name = project.name }); } } // Goal 8 task 2 B: the stacks appliable to this goal. if (ctx.stacks != null) { foreach (var stack in yield ((!)ctx.stacks).list_stacks()) { view.pub.stack_options.add(new StackOptionView() { id = stack.id, name = stack.name, member_count = (yield ((!)ctx.stacks).list_members(stack.id)).length }); } } // Plan 14 B5: the goal's effective rules — the set a // dispatch ships for the run (and every worker): // run-task rules, the primary project's, then the // co-developed goal links' in link order. if (ctx.rules != null) { var scope_task = new TaskEntity() { id = view.pub.run_task_id, project_id = ((!)goal).project_id }; foreach (var rule in yield ((!)ctx.rules).effective_rules_for( scope_task, ctx.projects, ((!)goal).id)) { effective_rules.add(new GoalRuleView() { id = rule.id, name = rule.name, body = rule.body }); } } } if (ctx.actions != null && goal != null) { view.pub.refresh = ((!)ctx.actions) .author_private( new GoalActionPrivate() { goal_id = goal_id }, "GET"); } var pub_root = (PropertyDictionary) GObjectMapping.to_properties(view.pub); pub_root["links"] = StateJson.list(view.links.to_immutable_buffer()); pub_root["blocks"] = StateJson.list_rich(view.blocks.to_immutable_buffer(), (item, dict) => { dict["options"] = StateJson.strings( ((GoalBlockView) item).options ?? new string[0]); }); pub_root["open_blocks"] = StateJson.list_rich( view.blocks_open.to_immutable_buffer(), (item, dict) => { dict["options"] = StateJson.strings( ((GoalBlockView) item).options ?? new string[0]); }); pub_root["answered_blocks"] = StateJson.list_rich( view.blocks_answered.to_immutable_buffer(), (item, dict) => { dict["options"] = StateJson.strings( ((GoalBlockView) item).options ?? new string[0]); }); pub_root["goal_tasks"] = StateJson.list_rich( view.goal_tasks.to_immutable_buffer(), (item, dict) => { dict["capabilities"] = StateJson.strings( ((GoalTaskView) item).capabilities ?? new string[0]); }); pub_root["events"] = StateJson.list(view.events.to_immutable_buffer()); pub_root["comments"] = StateJson.list(view.comments.to_immutable_buffer()); pub_root["runs"] = StateJson.list(view.runs.to_immutable_buffer()); pub_root["progress"] = StateJson.list( view.progress.to_immutable_buffer()); pub_root["push_branches"] = StateJson.list( view.push_branches.to_immutable_buffer()); pub_root["plan_revisions"] = StateJson.list( plan_revisions.to_immutable_buffer()); pub_root["rules"] = StateJson.list( effective_rules.to_immutable_buffer()); pub_root["project_options"] = StateJson.list( project_options.to_immutable_buffer()); pub_root["stack_options"] = StateJson.list( view.pub.stack_options.to_immutable_buffer()); pub_root["plan_pending_block"] = plan_pending != null ? new JsonElement.from_properties( (PropertyDictionary) GObjectMapping.to_properties((!)plan_pending)) : new JsonElement.from_element(new NativeElement(false)); pub_root["completion_pending_block"] = completion_pending != null ? new JsonElement.from_properties( (PropertyDictionary) GObjectMapping.to_properties((!)completion_pending)) : new JsonElement.from_element(new NativeElement(false)); return state_service.new_slot(Scope.PAGE, new Statum.State() { type_name = "goal", public_data = pub_root, private_data = new PropertyDictionary() }); } /** * The goals-list row's view (the old `full=false` half): the * {@link GoalPublic} scalars alone — display fields, the * action DTOs, the completion-pending flag, the run-task id * and the task counts. The section lists stay empty: the row * reads none of them, and the old call threaded thirteen * throwaway vectors (and their per-task display/capability * queries) through the full builder to fill views nobody * serialised. */ public static async GoalView summary_view(Goal goal, GoalViewContext ctx) throws GLib.Error { var view = new GoalView(); var pub = view.pub; pub.exists = true; pub.id = goal.id; pub.project_id = goal.project_id; pub.title = goal.title; pub.brief = goal.brief; pub.branch_name = goal.branch_name; pub.base_branch = goal.base_branch ?? ""; pub.status = goal.status; // Plan 16 S5 (U3): a goal awaiting its completion review // reads "awaiting your review" — the run-task status card // agrees (no bare "blocked" for review parks). pub.display = goal.status == GOAL_COMPLETION_REVIEW ? "awaiting your review" : goal.status.replace("_", " "); pub.priority = goal.priority; pub.plan = goal.plan ?? ""; pub.completion_report = goal.completion_report ?? ""; pub.created_display = format_stamp(goal.created_at); pub.started_display = format_stamp(goal.started_at); pub.completed_display = format_stamp(goal.completed_at); pub.terminal = goal.status == GOAL_COMPLETE || goal.status == GOAL_FAILED || goal.status == GOAL_CANCELLED; var project = yield ctx.db.query() .where(expr("id == $0", elem(goal.project_id))) .first_async(); if (project != null) { pub.project_name = ((!)project).name; string base_name = goal.base_branch != null && ((!)goal.base_branch).length > 0 ? ((!)goal.base_branch) : ((!)project).default_branch; pub.compare_url = @"$(TaskService.https_git_url(((!)project).git_url))/compare/$(base_name)...$(goal.branch_name)"; } if (ctx.actions != null) { var sealed = new GoalActionPrivate() { goal_id = goal.id }; pub.comment = ((!)ctx.actions).author_private(sealed); pub.add_link = ((!)ctx.actions).author_private(sealed); if (!pub.terminal) { pub.apply_stack = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_DRAFT) { pub.start = ((!)ctx.actions).author_private(sealed); pub.update = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_ACTIVE) { pub.pause = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_PAUSED) { pub.resume = ((!)ctx.actions).author_private(sealed); } if (!pub.terminal) { pub.cancel = ((!)ctx.actions).author_private(sealed); } } foreach (var block in yield ctx.goals.list_blocks(goal.id)) { if (block.answer == null && (block.block_type == BLOCK_COMPLETION || block.block_type == BLOCK_REVIEW)) { pub.completion_pending = true; break; } } foreach (var task in yield ctx.tasks.list_tasks_for_goal(goal.id)) { if (task.kind == TASK_KIND_GOAL_RUN) { pub.run_task_id = task.id; continue; } switch (task.status) { case STATUS_DRAFT: pub.draft_tasks++; break; case STATUS_QUEUED: case STATUS_ASSIGNED: pub.queued_tasks++; break; case STATUS_WORKING: case STATUS_BLOCKED: case STATUS_REVIEW_REQUESTED: case STATUS_SUSPENDED: pub.running_tasks++; break; case STATUS_DONE: pub.done_tasks++; break; case STATUS_FAILED: pub.failed_tasks++; break; default: break; } } return view; } /** * The goal page's whole view (the old `full=true` half): the * public scalars plus every section list — links, blocks with * the open/answered split, task rows with their computed * displays, the run-control actions, the narrative, the * comment thread, the runs, the live activity and the push * chips. */ public static async GoalView full_view(Goal goal, GoalViewContext ctx) throws GLib.Error { var view = new GoalView(); var pub = view.pub; pub.exists = true; pub.id = goal.id; pub.project_id = goal.project_id; pub.title = goal.title; pub.brief = goal.brief; pub.branch_name = goal.branch_name; pub.base_branch = goal.base_branch ?? ""; pub.status = goal.status; // Plan 16 S5 (U3): a goal awaiting its completion review // reads "awaiting your review" — the run-task status card // agrees (no bare "blocked" for review parks). pub.display = goal.status == GOAL_COMPLETION_REVIEW ? "awaiting your review" : goal.status.replace("_", " "); pub.priority = goal.priority; pub.plan = goal.plan ?? ""; pub.completion_report = goal.completion_report ?? ""; pub.created_display = format_stamp(goal.created_at); pub.started_display = format_stamp(goal.started_at); pub.completed_display = format_stamp(goal.completed_at); pub.terminal = goal.status == GOAL_COMPLETE || goal.status == GOAL_FAILED || goal.status == GOAL_CANCELLED; var project = yield ctx.db.query() .where(expr("id == $0", elem(goal.project_id))) .first_async(); if (project != null) { pub.project_name = ((!)project).name; string base_name = goal.base_branch != null && ((!)goal.base_branch).length > 0 ? ((!)goal.base_branch) : ((!)project).default_branch; pub.compare_url = @"$(TaskService.https_git_url(((!)project).git_url))/compare/$(base_name)...$(goal.branch_name)"; } if (ctx.actions != null) { var sealed = new GoalActionPrivate() { goal_id = goal.id }; pub.comment = ((!)ctx.actions).author_private(sealed); pub.add_link = ((!)ctx.actions).author_private(sealed); if (!pub.terminal) { pub.apply_stack = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_DRAFT) { pub.start = ((!)ctx.actions).author_private(sealed); pub.update = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_ACTIVE) { pub.pause = ((!)ctx.actions).author_private(sealed); } if (goal.status == GOAL_PAUSED) { pub.resume = ((!)ctx.actions).author_private(sealed); } if (!pub.terminal) { pub.cancel = ((!)ctx.actions).author_private(sealed); } } foreach (var link in yield ctx.goals.list_links(goal.id)) { var link_view = new GoalLinkView() { id = link.id, project_id = link.project_id, relationship = link.relationship, remove = ctx.actions != null ? ((!)ctx.actions).author_private( new GoalActionPrivate() { goal_id = goal.id }) : null }; var linked = yield ctx.db.query() .where(expr("id == $0", elem(link.project_id))) .first_async(); if (linked != null) { link_view.name = ((!)linked).name; } view.links.add(link_view); } bool completion_pending = false; bool plan_pending_open = false; // Plan 15 C1: the goal's blocks ARE the goal-run task's // blocks — one row per question, answered in place. foreach (var block in yield ctx.goals.list_blocks(goal.id)) { bool pending_plan = block.block_type == BLOCK_PLAN && block.answer == null; // The completion review may ride either kind: the web's // completion raise or the daemon's done-park review block // (adopted — one row, whichever landed first). bool pending_completion = block.answer == null && (block.block_type == BLOCK_COMPLETION || block.block_type == BLOCK_REVIEW); var block_view = new GoalBlockView() { id = block.id, question = block.question, block_type = block.block_type, options = EntryJson.parse_string_array(block.options), body = block.body ?? "", answered = block.answer != null, answer = block.answer ?? "", answered_at_display = format_stamp(block.answered_at), answered_by = block.answered_by ?? "", created_display = format_stamp(block.created_at), is_plan_review = pending_plan, is_completion_review = pending_completion, // A pending plan hides its body preview (the plan // section renders it); the pending completion review // SHOWS its body — the block IS the review surface. show_body = !pending_plan, approved = block.answer != null && block.answer == NOTICE_PLAN_APPROVED }; if (block.answer == null && ctx.actions != null) { block_view.answer_action = ((!)ctx.actions) .author_private( new GoalBlockAnswerPrivate() { goal_id = goal.id, block_id = block.id }); } if (pending_completion) { completion_pending = true; } if (pending_plan) { plan_pending_open = true; } // Newest-first: list_blocks leads with the latest row, // so appending preserves that order in the view. view.blocks.add(block_view); } pub.completion_pending = completion_pending; // Plan 15 C4: the current (unanswered) question/RFC blocks // render in full; the historic answered ones fold behind // the answered-blocks expander (the task page's pattern). // The rows already arrive newest-first ({@link // TaskService.list_blocks} since the ordering sweep), so a // straight walk keeps both groups newest-first. foreach (var block_view in view.blocks) { if (block_view.answered) { view.blocks_answered.add(block_view); } else if (block_view.is_plan_review) { // rendered by the plan section } else { // Every pending completion-family row renders (the // raise-side cross-dedupe keeps them to one — // fresh databases cannot hold pre-dedupe twins, so // no render-side guard is needed). view.blocks_open.add(block_view); } } var goal_task_rows = new Vector(); foreach (var task in yield ctx.tasks.list_tasks_for_goal(goal.id)) { if (task.kind == TASK_KIND_GOAL_RUN) { pub.run_task_id = task.id; } else { goal_task_rows.add(task); } } // The rows consume TaskService.display_map_for directly // (the same computed display the queue and the dashboard read). var display_map = yield ctx.tasks.display_map_for(goal_task_rows.to_immutable_buffer()); foreach (var task in goal_task_rows) { var task_view = new GoalTaskView() { id = task.id, title = task.title, status = task.status, mode = task.mode, creator = task.creator, updated_display = format_stamp(task.completed_at ?? task.created_at) }; // Plan 14 B5: the row's required-capability badges — // read straight off the task_capabilities links (no // editing here). var task_caps = new Vector(); foreach (var link in yield ctx.db.query() .where(expr("task_id == $0", elem(task.id))) .order_by(expr("id")) .materialise_async()) { var capability = yield ctx.db.query() .where(expr("id == $0", elem(link.capability_id))) .first_async(); if (capability != null) { task_caps.add(((!)capability).name); } } task_view.capabilities = task_caps.to_array(); var display = display_map.get_or_default(task.id.to_string()); task_view.display = display != null ? ((!)display).status : task.status; view.goal_tasks.add(task_view); switch (task.status) { case STATUS_DRAFT: pub.draft_tasks++; break; case STATUS_QUEUED: case STATUS_ASSIGNED: pub.queued_tasks++; break; case STATUS_WORKING: case STATUS_BLOCKED: case STATUS_REVIEW_REQUESTED: case STATUS_SUSPENDED: pub.running_tasks++; break; case STATUS_DONE: pub.done_tasks++; break; case STATUS_FAILED: pub.failed_tasks++; break; default: break; } } // Plan 14 B1: run-control parity on the status head — // "Retry supervision" for a failed goal, requeue-to-resume // for a run stale/parked on a dead saddle (NO notify-me // toggle: goals notify unconditionally by design). if (ctx.actions != null && pub.run_task_id > 0) { var run_row = yield ctx.db.query() .where(expr("id == $0", elem(pub.run_task_id))) .first_async(); if (run_row != null) { string run_status = ((!)run_row).status; if (goal.status == GOAL_FAILED && run_status == STATUS_FAILED) { pub.retry_run = ((!)ctx.actions) .author_private( new GoalActionPrivate() { goal_id = goal.id }); } bool stale_or_parked = run_status == STATUS_WORKING || run_status == STATUS_BLOCKED || run_status == STATUS_REVIEW_REQUESTED || run_status == STATUS_SUSPENDED; // Goal 4 task 1: Force wake — the run is parked // on a LIVE saddle under an ACTIVE goal (the // paused-goal recovery is Resume, the dead-saddle // one Requeue-to-resume; Force wake covers the // parked-with-no-pending-wake stall, goal 2). if (goal.status == GOAL_ACTIVE && run_status == STATUS_SUSPENDED) { pub.force_wake = ((!)ctx.actions) .author_private( new GoalActionPrivate() { goal_id = goal.id }); } if (!pub.terminal && stale_or_parked) { // Plan 16 D4: a parked round's session is // closed 'parked' — the newest record (open or // parked) still names the workspace's saddle. var run_session = yield ctx.tasks.newest_session_for_task(pub.run_task_id); if (run_session != null && (((!)run_session).ended_at == null || ((!)run_session).outcome == OUTCOME_PARKED)) { var host = yield ctx.db.query() .where(expr("id == $0", elem(((!)run_session).saddle_id))) .first_async(); if (host != null && ((!)host).status == SADDLE_OFFLINE) { pub.requeue_run = ((!)ctx.actions) .author_private( new GoalActionPrivate() { goal_id = goal.id }); } } } } } // Plan 15 C3: the narrative derives at read time (the // service returns newest-first rows). foreach (var row in yield ctx.goals.narrative(goal.id)) { view.events.add(new GoalEventView() { kind = row.kind, text = row.text, task_id = row.task_id, at_display = format_stamp(row.at) }); } // Plan 15 C2: the goal's comment thread IS the run // task's task_comments (injection rides the existing // delivery paths) — the agent's pushes and the human's // composer land on the same rows. Plan 16 S5 (U2): // mechanical rows (restores, park notices) never reach // the user-facing thread — the audit ledger keeps them. foreach (var row in yield ctx.goals.list_comments(goal.id)) { if (row.author == COMMENT_AUTHOR_SYSTEM && comment_is_mechanical(row.body)) { continue; } view.comments.add(new GoalCommentView() { author = row.author, body = row.body, quote = row.quote ?? "", created_display = format_stamp(row.created_at), from_agent = row.author == "agent", is_system = row.author == COMMENT_AUTHOR_SYSTEM }); } int64 open_session_saddle = 0; if (pub.run_task_id > 0) { // The goal agent's live progress feed (green, task-page // pattern) — the run task's own progress lines. foreach (var row in yield ctx.tasks.list_progress(pub.run_task_id)) { view.progress.add(new GoalProgressView() { id = row.id, summary = row.summary, when_display = format_stamp(row.created_at) }); } var session_rows = yield ctx.db.query() .where(expr("task_id == $0", elem(pub.run_task_id))) .order_by_desc(expr("id")) .materialise_async(); foreach (var session in session_rows) { string run_saddle = ""; if (ctx.saddles != null) { var saddle_row = yield ((!)ctx.saddles).get_saddle(session.saddle_id); run_saddle = saddle_row != null ? ((!)saddle_row).name : @"saddle #$(session.saddle_id)"; } view.runs.add(new GoalRunView() { id = session.id, started_display = format_stamp(session.started_at), ended_display = format_stamp(session.ended_at), open = session.ended_at == null, outcome = session.outcome ?? "", saddle_name = run_saddle, // Deep-links this run's terminal view: the // picker's client-side ?session_id= // selection lands on exactly this session. terminal_url = @"/tasks/$(pub.run_task_id)/log" }); } // Current activity (task-page parity): the live round's // hosting saddle, its health and the newest recorded // activity line. Plan 16 D4: a PARKED round keeps its // panel — its session row closed 'parked' at the park, // and the newest record (open or parked) is still the // round the page is describing. var open = yield ctx.tasks.newest_session_for_task(pub.run_task_id); if (open != null && (((!)open).ended_at != null && ((!)open).outcome != OUTCOME_PARKED)) { open = null; } if (open != null) { open_session_saddle = ((!)open).saddle_id; if (ctx.saddles != null) { var saddle_row = yield ((!)ctx.saddles).get_saddle(((!)open).saddle_id); if (saddle_row != null) { pub.agent_saddle = ((!)saddle_row).name; if (ctx.polls != null) { string daemon_state = ""; int64 uptime = 0; int64 activity = 0; int64 contact_ms = 0; ((!)ctx.polls).health_for(((!)saddle_row).id, out daemon_state, out uptime, out activity, out contact_ms); if (daemon_state.length > 0) { pub.saddle_health = @"saddle '$(((!)saddle_row).name)' — daemon $(daemon_state)"; } } } } var newest = yield ctx.events.newest_activity(pub.run_task_id); if (newest != null) { string line = activity_line_of((!)newest); if (line.length > 0) { pub.current_command = clip_line(line, 200); pub.current_command_display = format_stamp( ((!)newest).created_at); pub.current_command_running = true; } } } } // The one muted activity line (plan 15 C4): what the run is // doing right now — running the open round, or parked // awaiting a review / a question / its tasks. var activity_run = pub.run_task_id > 0 ? yield ctx.db.query() .where(expr("id == $0", elem(pub.run_task_id))) .first_async() : null; if (activity_run != null) { if (((!)activity_run).status == STATUS_WORKING || ((!)activity_run).status == STATUS_ASSIGNED) { if (pub.current_command.length > 0) { pub.current_activity = pub.current_command_running ? @"running: `$(pub.current_command)`" : @"last: `$(pub.current_command)`"; } } else if (((!)activity_run).status == STATUS_BLOCKED || ((!)activity_run).status == STATUS_REVIEW_REQUESTED) { if (completion_pending) { pub.current_activity = "parked awaiting the completion review"; } else if (plan_pending_open) { pub.current_activity = "parked awaiting the plan review"; } else if (view.blocks_open.length > 0) { pub.current_activity = "parked awaiting a question"; } else { pub.current_activity = "parked"; } } else if (((!)activity_run).status == STATUS_SUSPENDED) { pub.current_activity = "parked awaiting its tasks' terminal transitions"; } } // Plan 15 C4: the push chips derive from the LIVE // daemon-reported per-repo flags of the run's hosting // saddle (the last /status probe) — never a stored // snapshot. A restart with no probe yet renders none. if (open_session_saddle > 0 && ctx.polls != null) { string flags = ((!)ctx.polls).goal_branches_for(open_session_saddle); if (flags.length > 0) { // The SAME parser the completion-accept gate reads // (GoalService.parse_goal_branches) feeds the chips — // gate and display cannot diverge on one payload. foreach (var row in GoalService.parse_goal_branches(flags)) { view.push_branches.add(new GoalPushBranchView() { branch = row.label(), pushed = row.pushed }); } } } return view; } /** A flattened, clipped activity line (the current-activity display). */ /** * One ledger activity event as the goal head's current-activity * line: turn text and progress carry their text, a tool call * names its tool (the run_command command when present), a * tool result summarises as "tool result". */ private static string activity_line_of(EventRecord record) { var payload = EventService.payload_of(record); switch (record.kind) { case EVENT_TURN_TEXT: case EVENT_PROGRESS: case EVENT_LIFECYCLE: return EntryJson.string_member(payload, "text"); case EVENT_COMMENT: return EntryJson.string_member(payload, "body"); case EVENT_TOOL_CALL: string tool = EntryJson.string_member(payload, "tool"); var args = payload.has_member("args") && payload.get_member("args").get_node_type() == Json.NodeType.OBJECT ? payload.get_object_member("args") : null; string command = args != null ? EntryJson.string_member((!)args, "command") : ""; return command.length > 0 ? @"$(tool): $(command)" : tool; case EVENT_TOOL_RESULT: return "tool result"; default: return ""; } } private static string clip_line(string text, int limit) { string flat = text.replace("\n", " ⏎ ").strip(); return flat.length > limit ? flat.substring(0, limit) + "…" : flat; } } /** * Entrypoint for /goals: the goals dashboard — every goal (newest * first) with lifecycle badges, branch, priority and task counts, * plus the create form. The page subscribes to the global pulse * and re-renders whenever goal or task state changes. */ public class GoalsEntrypoint : StatumEntrypoint { private GoalService goal_service = Inversion.inject(); private TaskService task_service = Inversion.inject(); private ProjectService project_service = Inversion.inject(); private EventService event_service = Inversion.inject(); private InvercargillSql.Orm.OrmSession db = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } var ctx = new GoalViewContext(goal_service, task_service, project_service, event_service, db, action_registry); var rows = new JsonArray(); foreach (var goal in yield goal_service.list_goals()) { var view = yield GoalState.summary_view(goal, ctx); var row = (PropertyDictionary) GObjectMapping.to_properties(view.pub); row["plan_pending"] = new JsonElement.from_element( new NativeElement(goal.status == GOAL_PLAN_REVIEW)); rows.add(new JsonElement.from_properties(row)); } var project_options = new Vector(); foreach (var project in yield project_service.list_projects()) { project_options.add(new GoalProjectOptionView() { id = project.id, name = project.name }); } var pub_root = (PropertyDictionary) GObjectMapping.to_properties( new GoalsListPublic() { create = action_registry.author() }); pub_root["goals"] = new JsonElement.from_elements(rows); pub_root["projects"] = StateJson.list( project_options.to_immutable_buffer()); return directives().set(state_service.new_slot(Scope.PAGE, new Statum.State() { type_name = "goals", public_data = pub_root, private_data = new PropertyDictionary() })).subscribe_global(StatePulse.SLOT_TYPE); } } /** * Entrypoint for /goals/{goal_id}: the goal page — status head * with the lifecycle actions, the locked brief, the approved plan * (rejected revisions under the historical-blocks expander), the * task strip + list, the supervision narrative, the comment * thread, the completion review (report + push state + compare * link) and the goal agent's runs with their terminal views. * Subscribes to the global pulse for live re-renders. */ public class GoalDetailEntrypoint : StatumEntrypoint { private GoalService goal_service = Inversion.inject(); private TaskService task_service = Inversion.inject(); private ProjectService project_service = Inversion.inject(); private EventService event_service = Inversion.inject(); private SaddleService saddle_service = Inversion.inject(); private SaddlePollState poll_state = Inversion.inject(); private RuleService rule_service = Inversion.inject(); private StackService stack_service = Inversion.inject(); private InvercargillSql.Orm.OrmSession db = Inversion.inject(); private UserService users = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } int64 goal_id = int64.parse(request.route("goal_id") ?? "0"); var ctx = new GoalViewContext(goal_service, task_service, project_service, event_service, db, action_registry, saddle_service, poll_state, rule_service, stack_service); var state = yield GoalState.build(goal_id, ctx, state_service); return directives().set(state).subscribe_global(StatePulse.SLOT_TYPE); } } }