Skip to content

Build a harness-agnostic app for cross-session visibility into the ticket lifecycle #984

Description

@williamthorsen

Problem

The ticket lifecycle (design → plan → implement → review → respond → PR → merge → clean up) runs across 3–6 concurrent agent sessions, each in its own worktree, under two harnesses: Claude at home, Rovo at work. Driving it takes continuous manual oversight — scanning for sessions awaiting input, hand-maintaining status in workspace names, hunting for artifacts, pasting artifact paths between sessions, leaving the terminal to view diffs and edit PR bodies, and noticing by hand when the base branch has advanced.

The gap is visibility, not interaction. The harnesses handle interaction well.

Context

Vendors have converged on the generic half of this problem (fleet view, worktree lifecycle, inline diff, open-in-IDE, PR merge), now shipped first-party by Anthropic and others. Of the substrate-aware half, review findings as first-class clickable items has shipped (Sculptor's suggestions pane, Conductor's code review) — but always single-run and mostly single-harness. Still shipped by no tool surveyed: lifecycle-bound findings with dispositions, artifact revision diffs, prompt history, persisted proposed-actions and selections, and ticket metadata — all downstream of this project's own skills. Every vendor answer is also harness-locked, so none covers work done under Rovo.

The third-party category that wrapped the Claude Code CLI has collapsed (Omnara, vibe-kanban, Crystal, Terragon), with CLI-wrapping maintenance cited as a cause of death. Wrapping a harness is a disqualified approach.

Factory (packages/factory) visualizes orchestrated runs and already carries a reusable read-layer: Express API, file watcher, artifact list and render endpoints, and foldEvents. The priority is an app. A visualization remains a useful adjunct, not the destination.

The event substrate exists but only fires for orchestrated runs: every interactive skill writes artifacts and emits zero events, and interactive work is ~95% of the flow. RunHeader and RunEvent carry no session identity, and .claude/tmp/active-run-dir is a single-slot breadcrumb per repo that collides across concurrent sessions.

Proposed solution

A canonical event bus the app reads, fed by adapters. This project's own canonical event schema — the successor to run-log-schema.ts; see design question 2 — is the contract; every signal source is an adapter producing into it.

Harness-agnostic adapters carry the system:

  • Skills — this project's own code, installed into both harnesses, emitting semantic lifecycle events. The primary instrumentation point. The action-items convention already structures every ask as an identified item, so skills can emit proposed actions and the user's selections as events at the moment they are generated.
  • Git — branch, commits, working-tree state, base-branch advance.
  • Forge — PR state, CI, approvals, and ticket metadata (created, priority, revision count).
  • Artifacts — backfill and pane content, not a live event source.

Harness-specific adapters are optional and degrade gracefully. Idle / awaiting-input is the only signal requiring harness cooperation: Claude exposes it via hooks, Rovo does not. Claude Code's stable OpenTelemetry export (user_prompt, tool_result, skill_activated) is a candidate second Claude adapter, corroborating skill-emitted events over a sanctioned push channel.

Not: core status or lifecycle truth depending on any harness CLI, transcript format, or process state. Read-only readers of harness-internal stores (transcripts, session directories) are admissible as optional enrichment adapters that degrade gracefully — never wrapped, never load-bearing.

Trust model: deterministic adapters (git, forge, artifacts) are ground truth; skill events are enrichment on top. Skill-side emission has proven reliable at artifact granularity over thousands of uses (the known gap is orchestrated runs, ~5% of the flow); the open question is whether emission points are dense enough for live status. The app is transparent about freshness: each session shows when its last event arrived, and polling of the deterministic sources bounds staleness rather than letting derived status silently drift.

Design questions to settle before implementation:

  1. Session identity — settled: two-level model. Workstream = repo × branch ref, where the ref is the ticket ID plus an optional .N sub-ID for revisits (the only supported reuse case); it is the primary key and the fleet-view lane. The parsed ticket ID is a lane attribute pointing at the forge ticket, so several lanes may serve one ticket and roll up under it. Session = one conversation, a sub-attribution stamped on events (CLAUDE_CODE_SESSION_ID under Claude, get_session_metadata under Rovo). Worktree path is a lane attribute, never identity. Lane state is a fold over the lane's event group — closed by merge/cleanup events or staleness — never a mutation. Consequence for the artifacts adapter: lanes of one ticket share the ticket's artifact directory, so revisions attribute to lanes by timestamp/session, not by directory.
  2. Run-event schema — settled: superseded, not extended. One canonical, app-first event schema; run-log-schema.ts has no live consumer (Factory's scene never worked; its read layer surfaced no event defects but no consumer ever validated them end-to-end) and is superseded. Events are self-identifying: the envelope carries timestamp, type, workstream, and session, ending positional identity. Payloads group into families by concern — session lifecycle first (interactive work is ~95% of the flow), the orchestration vocabulary revised freely as a second family, adapter-sourced families (git, forge) as they come. A run relates to a session by reference: boundary events carry the runId and detail joins on it; whether run detail lives inline or in per-run files is deferred to question 4. Event shapes, collections, and file layout are all free to serve the app; a future visualizer is a downstream consumer of the same fold, and nothing visualizer-specific belongs in the schema.
  3. Lifecycle vs KB events — settled: two stores, aligned common fields. The two kinds are not deeply similar and stay in their own stores: lifecycle telemetry in the app's event store (layout per question 4), KB events in the curated vault. The fields they do share — id, timestamp, workstream/branch ref, session, cwd — are aligned so records join across the boundary; KB metadata chiefly serves debugging (when and where did this occur), which the shared fields answer. Surfacing a lane's related KB events in the app is a supported use — and raises the return on capturing them. A recurring lifecycle pattern can be distilled into a KB record; the asserted/observed/distilled question stays with the unified-substrate design, not this ticket.
  4. Storage, query, and transport — settled: filesystem-first. Append-only JSONL, one file per session under a per-lane directory in a single events root (~/.codeassembly/events/{repo}/{branch-ref}/{session}.jsonl). Writers are dependency-free atomic appends that succeed with the app down; concurrency resolves by layout, not locking. The app server watches via fs-notify, folds incrementally, and pushes updates to the UI over SSE; any index (e.g. SQLite) lives inside the server as a rebuildable cache, never in writers. Run detail is inline: run events are a payload family stamped with runId. Volume stays small (worktrees live hours to days; peak traffic ≈ one intensive multi-hour session), so retention is a non-issue — closed lanes archive wholesale. Cross-machine aggregation is out of scope but kept cheap by construction: lane keys are machine-independent and logs are self-contained and mergeable by timestamp, so a future sync layer merges stores without rekeying. Sessions carry a harness attribute — one lane routinely mixes harnesses (Claude problem-solving, Rovo implementing, same worktree).

Acceptance criteria

Must have

  • In-flight work across all concurrent sessions appears in one view, grouped into workstream lanes with per-session attribution.
  • Every capability except idle-detection behaves identically under Claude and Rovo.
  • Core status and lifecycle truth do not depend on a harness's CLI, transcript files, or process state; harness-internal readers are optional, read-only enrichment.
  • A missing harness-specific adapter degrades the app rather than breaking it.
  • Interactive sessions emit lifecycle events, not only artifacts.
  • Concurrent sessions in one repository do not collide in event attribution.

Should have

  • Artifacts pane: list, preview, open in editor, and diff across revisions.
  • Findings pane: review findings as clickable items with preview.
  • Prompt-history pane.
  • Proposed actions and user selections from action-items blocks appear as first-class timeline items.
  • Ticket metadata alongside the ticket: created, priority, revision count, related KB events.
  • Rebase prompt when the base branch advances.
  • PR body editable in-app.
  • Conversation browse and search across harnesses via per-harness read-only transcript adapters (~/.claude/projects/, ~/.rovodev/sessions/, soon ~/.rovo/sessions/). The architecture reserves the adapter slot now; implementation is deferred.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions