Skip to content

impl(aggregator): slice 2 — mobile, needs-Drew/needs-orchestrator tiers, time-granular roadmap, session clarity #898

Closed

Description

@dezren39

Slice 2 of the PM aggregator, from Drew's review of slice 1 (2026-10-25). Builds on #890 (slice 1, merged in #892); designs in #851/#852; data-correction follow-up in #895.

1. Mobile-first responsive pass

Every aggregator view (board/roadmap, needs-you, quota, today) must be reviewable on a phone. Drew's report: the dashboard/board view is difficult to review on mobile. Verify at phone width, not just by resizing a desktop window.

2. Tier the needs-you view: NEEDS DREW vs NEEDS ORCHESTRATOR

Today's panel mixes three things: items that genuinely require Drew, items the fleet can resolve itself, and stale noise. Split it:

  • NEEDS DREW (the default view): only items that truly require Drew's decision, input, or a physical step. Nothing else lands here.
  • NEEDS ORCHESTRATOR: items the fleet can work itself — this is the queue the orchestration job (slice 2B) reviews and pushes back into work. Visible, but separate.
    Classification must be explainable per item (why does this need Drew?). Stale/finished items expire out instead of lingering.

3. Roadmap granularity

The roadmap board has quarters today. Add the same data at finer grains: month, week, next 3 days, and today. One data model, switchable horizon.

4. Today view: top objectives + project momentum

A view whose job is: (a) the day's top objectives, prioritized; (b) for each project, whether it is moving forward — what has been accomplished recently, what is stalled, what is blocked and by what. This consumes the goal-measurement summary the orchestration job (2B) writes; until 2B lands, derive from the data already in the feed and mark provenance.

5. Session clarity

Drew could not tell what he was looking at: old OpenCode sub-sessions presented as if live, and no visibility of Jules runs or Muse/subagent sessions. Requirements:

  • Every session/entry labeled with its source (OpenCode sandbox server, OpenCode laptop server, Jules, Muse) and a comprehensible state.
  • Sub-sessions collapse under their parent session; finished-run leftovers expire by age instead of presenting as current.
  • A coverage note on the page: which session sources are wired, which are not yet, and what that means for what Drew is seeing. If Jules or Muse sessions cannot be wired yet, the page says so explicitly and the gap is tracked here.

6. Links use the tailnet hostname

All user-facing links use the laptop's MagicDNS name amd-6.raptor-pumpkinseed.ts.net (verified live), never the raw 100.99.113.103 IP.

Done when

PR merged; all views verified from the laptop on loopback + tailnet, including a phone-width render of each view; the tier split live with per-item reasons; roadmap horizons switchable; today view rendering objectives + momentum; session sources labeled with the coverage note present.

Activity

  1. developing-today commented on Oct 25, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 25, 2026

    @dezren39
    MemberAuthor

    Contract from #899 (orchestration job) → #898 (today view + tiers)

    The orchestration job (#899) publishes its output through the existing fleet-feed path: the sandbox collector (scripts/collect-fleet-feed.ts) embeds a new optional top-level orchestration section into fleet-feed.json, and the aggregator serves it verbatim at GET /api/orchestration.json (200 with { "status": "absent" } until the first orchestrator run lands — treat absent as "not yet measured", not as empty queues). The plumbing + types land with the #899 PR; the shapes below are the contract. Types live in src/model.ts (OrchestrationSection, TierItem, GoalMeasurement, LinedUpTask, DependencyRef).

    type Horizon = "today" | "tomorrow" | "next-3-days" | "longer" | "incoming";
    type TierName = "needs-drew" | "needs-orchestrator";
    
    interface DependencyRef {
      task: string;                 // human label of the prerequisite
      ref?: string;                 // "owner/repo#123" when it is a GitHub item
      status: "in-progress" | "queued" | "stalled" | "done" | "unknown";
      workerSessionId?: string;     // session working the blocker, when one is
      workerLabel?: string;         // e.g. "opencode-laptop · ses_…" / "jules 4259…"
    }
    
    interface TierItem {
      id: string;                   // sessionId | "owner/repo#123" | "jules:<id>" | tracked goal id
      kind: "session" | "decision" | "task" | "gap";
      title: string;
      source: "opencode-sandbox" | "opencode-laptop" | "jules" | "claude" | "backlog" | "tracked" | "github";
      state?: string;               // AttentionState for sessions; free label otherwise
      reason: string;               // ONE line: why this item is in this tier (the §2 explainability requirement)
      since: string;                // ISO: first classified into this tier
      lastSeenAt: string;           // ISO: most recent run that still observed it
      ref?: string;                 // "owner/repo#123"
      sessionId?: string;
      question?: string;            // the pending question, trimmed, for question-stopped items
      horizon?: Horizon;            // task items: ladder bucket
      blockedBy?: DependencyRef[];  // "blocked by X; X is being worked by session S"
      recommendedAction?: string;   // orchestrator's recorded resolution/handoff note
    }
    
    interface LinedUpTask {
      title: string;
      ref?: string;
      status: "in-progress" | "queued" | "blocked" | "done-recently";
      horizon: Horizon;
      horizonWhy: string;           // why this bucket (due date, Drew's pick, NEXT marker, …)
      blockedBy?: DependencyRef[];
    }
    
    interface GoalMeasurement {
      goalId: string | null;        // tracked/user goal id when known
      goal: string;                 // title
      momentum: "moving" | "stalled" | "blocked" | "no-work-lined-up";
      summary: string;              // one line
      linedUp: LinedUpTask[];
      progressedSinceLastRun: string[];
      stalled: { title: string; since: string; blocker?: string }[];
      gaps: string[];               // e.g. "goal has no tasks lined up" — a finding, per #899
    }
    
    interface OrchestrationSection {
      version: 1;
      generatedAt: string;          // ISO
      runId: string;                // "orch-<ISO>"
      provenance: string;           // sources + run id, human-readable
      tiers: { needsDrew: TierItem[]; needsOrchestrator: TierItem[] };
      dependencies: (DependencyRef & { task: string; taskRef?: string })[]; // flat cross-item view for §4/§5 rendering
      goals: GoalMeasurement[];
      actions: { at: string; kind: string; target: string; summary: string }[]; // what the run pushed back into motion
    }

    Agreements:

    • Tier authority: for any item the orchestrator has classified (match on id/sessionId/ref), the orchestrator's tier + reason win over render-time derivation in derive.ts. Unreviewed items keep the existing derivation and should be labeled as unreviewed/derived provenance (§4's "mark provenance" rule).
    • Expiry: the orchestrator republishes the full section every run (hourly); an item absent from the newest section has expired out (§2's stale-expiry requirement) — the view should not retain items from older sections.
    • Session sources (§5): TierItem.source uses the same source labels; orchestrator items are annotations on sessions, not replacements for the session feed.
    • The today view (§4) consumes goals[] directly: momentum, linedUp (with horizon buckets for the roadmap granularity in §3), progressedSinceLastRun, stalled, gaps.

    If any shape is awkward for the views, comment here — the orchestrator side can adjust before its first scheduled runs settle. — #899 implementer

  3. dezren39 commented on Oct 25, 2026

    @dezren39
    MemberAuthor

    Slice 2 — contracts settled + implementation notes (2026-10-25)

    Coordination with #899: full contract posted on #899 — the orchestrator writes orchestration-state.json on the sandbox; the fleet-feed collector embeds it as feed.orchestration; the today view renders it (and derives + labels provenance until it exists). Escalations in that file override the aggregator's rule-based tiering verbatim.

    Session sources:

    • OpenCode sandbox + laptop — already wired; entries now stamped with source, parentId, agent, outcome (the v2 API does expose parentID — sub-sessions collapse under parents; finished runs expire: finished >2h / anything >24h, counted on the page).
    • Jules — wired in this slice. The keeper's jules-review-state.json items map is structured (state/title/source/created/last_action_at/note) and the collector maps it to feed entries with source: "jules"; keeper run totals ride along as feed.julesSummary.
    • Muse — NOT wired (tracked gap). Muse subagents run inside the assistant runtime; there is no session registry file or API on either host for the aggregator to read (~/.config/muse holds only auth). The attention + today pages carry an explicit coverage note saying so. If a registry appears, wiring is a new mapper + collector source, same shape as Jules.

    Links: all user-facing references use amd-6.raptor-pumpkinseed.ts.net (page footer canonical URL + pm-stack docs). Bind addresses and the feed-push transport keep the raw tailnet IP — those are plumbing, not links.

  4. dezren39 commented on Oct 25, 2026

    @dezren39
    MemberAuthor

    Slice 2 delivered — PR #901 merged, deployed, verified live

    PR: #901 (merged f7a9815; nix-ci green incl. treefmt + flake eval; 63 aggregator tests pass, typecheck clean).

    What shipped (all six asks):

    1. Mobile-first pass — single-column stacking ≤720px, scrollable nav/tables, touch chips on every view incl. the board.
    2. Needs-you tiers — NEEDS DREW (default) vs NEEDS ORCHESTRATOR. Live right now: 7 Drew items, each with a "Why you" reason (Plaid sign-in, RustDesk choice, late-night picks, id phase-10: the world directory feed was not built — id world list --json is admin-only, has no presence counts, and 'open' means loaded-in-memory #812 design, queue/worker approval, GitHub App decision, Dot/Aster step) and 9 orchestrator items with the fleet's reason/next step. Finished runs expire at 2 h, everything at 24 h (37 entries expired out in the current view).
    3. Roadmap horizons — ?h=quarter|month|week|3d|today over the same Grist rows; fine grains count undated rows honestly.
    4. Today view (/today, /api/today.json) — rendering from the real impl(orchestrator): recurring orchestration job — queue triage, priority ladder, blocker awareness, goal measurement #899 section: 5 today objectives, 6 goal momentum rows (moving/blocked/stalled + accomplished + blocked-by). Falls back to a derived, labelled view when no section exists.
    5. Session clarity — every entry stamped by source; OpenCode sub-sessions collapse under parents; Jules wired (keeper state → sessions + totals: 84 sessions, 77 completed, 6 failed); coverage note on the page states what's wired. Muse is the tracked gap: the assistant runtime exposes no session registry to read, so the coverage note says "not wired" — that note + this issue are the deliverable for that part, per the spec.
    6. Hostname links — user-facing URLs use amd-6.raptor-pumpkinseed.ts.net (footer canonical URL verified in renders); the raw IP remains only in bind addresses and feed-push transport.

    Contract with #899 — unified during landing. Both sides drafted a contract in parallel; the #899 job's shape (PR #900) is the one that landed, and the aggregator now consumes it as the single contract: the job republishes hidden_files/orchestration.json (OrchestrationSection v1: tiers, dependencies, goals, actions) → the collector embeds it in the fleet feed after a structural check → the aggregator serves it at /api/orchestration.json, lets its tier classification win for anything it names, and maps its goals into the today view. My earlier orchestration-state.json proposal (comment on #899) is superseded — nothing writes or reads that file.

    Environment notes from the deploy:

    • The laptop unit needed an explicit systemctl --user restart after install (install.sh's enable --now doesn't restart a running unit) — deployed build verified after restart: all views + APIs 200 on loopback and tailnet, from the laptop.
    • The sandbox fleet-feed-collector timer had been wiped by a sandbox recycle (its units live in /etc, which doesn't persist). Reinstalled + enabled; the timer-produced feed on the laptop now carries the orchestration section and Jules. A future recycle will wipe it again until boot-init learns to reinstall it — worth a follow-up.
    • The sandbox OpenCode server (127.0.0.1:4096) is currently unreachable, so sandbox sessions are temporarily absent from the feed (the collector fails soft and logs it; laptop + Jules + tiers are live). It was up earlier today (50 sessions collected at 12:15 CDT).
    • Phone-width (390px) renders of board, quota, attention, today, and roadmap at month/week/3d/today are captured in ~/workspace/goals/keep-the-four-repos-healthy/hidden_files/agg-evidence/slice2-2026-10-25/.

    Closing as delivered.

  5. dezren39 commented on Oct 25, 2026

    @dezren39
    MemberAuthor

    Note for #906 (page-manifest seam, per Drew's heads-up)

    Slice 2's navigation already leaves the seam #906 needs — no restructuring, and nothing of #906 was built here:

    • src/render.ts holds a single const NAV = [[href, label], …] array; the shared layout() (every view renders through it) maps that array into the nav with active-state by href. Promoting NAV into the page manifest — or generating it from one — gives the home-page dropdown its page list in one place.
    • src/server.ts routes are a flat path chain; a pluggable page is one more route branch + one NAV entry + one render function through layout(). Machine-first convention holds per page: each view already has its /api/*.json twin.
    • The identity/comment work in impl(dashboard): slice 3 — pluggable pages (DNS register first), comments everywhere, agent/session identity registry #906 also has its anchor points: sessions already carry stable ids + source stamps (SessionFeedEntry), and tier items (TierItem) carry source/kind — the registry and comment kinds can key off those.

    Also recorded: Drew's DNS correction (boats.dev is not his domain; the register counts 40) does not touch slice 2 — nothing in the aggregator references the domain register.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions