| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080 |
- 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 <a
- * review | a question | its tasks' terminal transitions>"
- * 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<StackOptionView> stack_options { get; set; default = new Vector<StackOptionView>(); }
- /** 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<GoalLinkView> links { get; set; default = new Vector<GoalLinkView>(); }
- public Vector<GoalBlockView> blocks { get; set; default = new Vector<GoalBlockView>(); }
- public Vector<GoalBlockView> blocks_open { get; set; default = new Vector<GoalBlockView>(); }
- public Vector<GoalBlockView> blocks_answered { get; set; default = new Vector<GoalBlockView>(); }
- public Vector<GoalTaskView> goal_tasks { get; set; default = new Vector<GoalTaskView>(); }
- public Vector<GoalEventView> events { get; set; default = new Vector<GoalEventView>(); }
- public Vector<GoalCommentView> comments { get; set; default = new Vector<GoalCommentView>(); }
- public Vector<GoalRunView> runs { get; set; default = new Vector<GoalRunView>(); }
- public Vector<GoalPushBranchView> push_branches { get; set; default = new Vector<GoalPushBranchView>(); }
- public Vector<GoalProgressView> progress { get; set; default = new Vector<GoalProgressView>(); }
- }
- /**
- * 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<GoalBlockView>();
- var project_options = new Vector<GoalProjectOptionView>();
- var effective_rules = new Vector<GoalRuleView>();
- 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<RefreshGoalDataAction, GoalActionPrivate>(
- new GoalActionPrivate() { goal_id = goal_id }, "GET");
- }
- var pub_root = (PropertyDictionary) GObjectMapping.to_properties(view.pub);
- pub_root["links"] = StateJson.list<GoalLinkView>(view.links.to_immutable_buffer());
- pub_root["blocks"] = StateJson.list_rich<GoalBlockView>(view.blocks.to_immutable_buffer(),
- (item, dict) => {
- dict["options"] = StateJson.strings(
- ((GoalBlockView) item).options ?? new string[0]);
- });
- pub_root["open_blocks"] = StateJson.list_rich<GoalBlockView>(
- 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<GoalBlockView>(
- 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<GoalTaskView>(
- view.goal_tasks.to_immutable_buffer(),
- (item, dict) => {
- dict["capabilities"] = StateJson.strings(
- ((GoalTaskView) item).capabilities ?? new string[0]);
- });
- pub_root["events"] = StateJson.list<GoalEventView>(view.events.to_immutable_buffer());
- pub_root["comments"] = StateJson.list<GoalCommentView>(view.comments.to_immutable_buffer());
- pub_root["runs"] = StateJson.list<GoalRunView>(view.runs.to_immutable_buffer());
- pub_root["progress"] = StateJson.list<GoalProgressView>(
- view.progress.to_immutable_buffer());
- pub_root["push_branches"] = StateJson.list<GoalPushBranchView>(
- view.push_branches.to_immutable_buffer());
- pub_root["plan_revisions"] = StateJson.list<GoalBlockView>(
- plan_revisions.to_immutable_buffer());
- pub_root["rules"] = StateJson.list<GoalRuleView>(
- effective_rules.to_immutable_buffer());
- pub_root["project_options"] = StateJson.list<GoalProjectOptionView>(
- project_options.to_immutable_buffer());
- pub_root["stack_options"] = StateJson.list<StackOptionView>(
- 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<bool>(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<bool>(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<Project>()
- .where(expr("id == $0", elem<int64?>(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<GoalCommentAction, GoalActionPrivate>(sealed);
- pub.add_link = ((!)ctx.actions).author_private<AddGoalLinkAction, GoalActionPrivate>(sealed);
- if (!pub.terminal) {
- pub.apply_stack = ((!)ctx.actions).author_private<ApplyStackToGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_DRAFT) {
- pub.start = ((!)ctx.actions).author_private<StartGoalAction, GoalActionPrivate>(sealed);
- pub.update = ((!)ctx.actions).author_private<UpdateDraftGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_ACTIVE) {
- pub.pause = ((!)ctx.actions).author_private<PauseGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_PAUSED) {
- pub.resume = ((!)ctx.actions).author_private<ResumeGoalAction, GoalActionPrivate>(sealed);
- }
- if (!pub.terminal) {
- pub.cancel = ((!)ctx.actions).author_private<CancelGoalAction, GoalActionPrivate>(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<Project>()
- .where(expr("id == $0", elem<int64?>(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<GoalCommentAction, GoalActionPrivate>(sealed);
- pub.add_link = ((!)ctx.actions).author_private<AddGoalLinkAction, GoalActionPrivate>(sealed);
- if (!pub.terminal) {
- pub.apply_stack = ((!)ctx.actions).author_private<ApplyStackToGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_DRAFT) {
- pub.start = ((!)ctx.actions).author_private<StartGoalAction, GoalActionPrivate>(sealed);
- pub.update = ((!)ctx.actions).author_private<UpdateDraftGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_ACTIVE) {
- pub.pause = ((!)ctx.actions).author_private<PauseGoalAction, GoalActionPrivate>(sealed);
- }
- if (goal.status == GOAL_PAUSED) {
- pub.resume = ((!)ctx.actions).author_private<ResumeGoalAction, GoalActionPrivate>(sealed);
- }
- if (!pub.terminal) {
- pub.cancel = ((!)ctx.actions).author_private<CancelGoalAction, GoalActionPrivate>(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<RemoveGoalLinkAction, GoalActionPrivate>(
- new GoalActionPrivate() { goal_id = goal.id })
- : null
- };
- var linked = yield ctx.db.query<Project>()
- .where(expr("id == $0", elem<int64?>(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<AnswerGoalBlockAction, GoalBlockAnswerPrivate>(
- 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<TaskEntity>();
- 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<string>();
- foreach (var link in yield ctx.db.query<TaskCapability>()
- .where(expr("task_id == $0", elem<int64?>(task.id)))
- .order_by(expr("id"))
- .materialise_async()) {
- var capability = yield ctx.db.query<Capability>()
- .where(expr("id == $0", elem<int64?>(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<TaskEntity>()
- .where(expr("id == $0", elem<int64?>(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<RetryGoalRunAction, GoalActionPrivate>(
- 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<GoalForceWakeAction, GoalActionPrivate>(
- 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<Saddle>()
- .where(expr("id == $0",
- elem<int64?>(((!)run_session).saddle_id)))
- .first_async();
- if (host != null && ((!)host).status == SADDLE_OFFLINE) {
- pub.requeue_run = ((!)ctx.actions)
- .author_private<RequeueGoalRunAction, GoalActionPrivate>(
- 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<ClankerSaddle.Data.Session>()
- .where(expr("task_id == $0", elem<int64?>(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<TaskEntity>()
- .where(expr("id == $0", elem<int64?>(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<GoalService>();
- private TaskService task_service = Inversion.inject<TaskService>();
- private ProjectService project_service = Inversion.inject<ProjectService>();
- private EventService event_service = Inversion.inject<EventService>();
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private UserService users = Inversion.inject<UserService>();
- 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<bool>(goal.status == GOAL_PLAN_REVIEW));
- rows.add(new JsonElement.from_properties(row));
- }
- var project_options = new Vector<GoalProjectOptionView>();
- 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<CreateGoalAction>()
- });
- pub_root["goals"] = new JsonElement.from_elements(rows);
- pub_root["projects"] = StateJson.list<GoalProjectOptionView>(
- 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<GoalService>();
- private TaskService task_service = Inversion.inject<TaskService>();
- private ProjectService project_service = Inversion.inject<ProjectService>();
- private EventService event_service = Inversion.inject<EventService>();
- private SaddleService saddle_service = Inversion.inject<SaddleService>();
- private SaddlePollState poll_state = Inversion.inject<SaddlePollState>();
- private RuleService rule_service = Inversion.inject<RuleService>();
- private StackService stack_service = Inversion.inject<StackService>();
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private UserService users = Inversion.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 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);
- }
- }
- }
|