Skip to content

Workflow Bridge UI — phases 0–7 - #1040

Open
looksystems wants to merge 41 commits into
leeovery:mainfrom
looksystems:ui-pr
Open

Workflow Bridge UI — phases 0–7#1040
looksystems wants to merge 41 commits into
leeovery:mainfrom
looksystems:ui-pr

Conversation

@looksystems

Copy link
Copy Markdown

Workflow Bridge UI — phases 0–7

Builds the Workflow Bridge UI end-to-end from the committed plan set in ui/plans/: a read-only-first veneer over .workflows/ + git + the engine's in-process lib.cjs, adding zero new workflow state — state renders from the engine, gates are never guessed, and never-auto gates get a typed confirm.

Delivered as a pnpm monorepo under ui/ (bridge/ · app/ · shared/), each phase built → done-means verified → adversarially reviewed (findings verified against the code before folding) → ledgered. The full review history is in ui/plans/REVIEW.md (rounds 6–14).

Phases

Phase Delivers
0 — Rails Two-layer event model, durable spine (pure fn of git log + manifest), watcher, trust boundary, version handshake, fixtures
1 — Mirror Read-only SPA: lobby, channels, spine, engine embeds, ⌘K palette
2 — Gates Session manager, journal-projected gates, needs-you queue (S2), thread (S4), the security stack
3 — Attention Laned findings, escalation, quiet-hours digests, coordinator, durable push ledger
4 — Artifact lenses Read/Structure/History, claim chips, what-moved, roadmap, verdict/brief
5 — Delivery Manifest-sourced loop telemetry, consolidation, plan DAG, agent chips
6 — Multiplayer Auth, gate ownership (routing not authority), capture gesture, presence, comments
7 — MCP Apps The gate card inside Claude via a workflow MCP server — same schema, same round-trip

Follow-ups (this branch, post-phase)

Shared ChatInput + file attachments (materialized to the gitignored cache, referenced by path so the session's Read tool consumes them); a calm working indicator for the answer-in-flight moment; a resume affordance for interrupted sessions; and four bugs surfaced by driving the live SPA in a browser (a SessionThread hooks-order crash, a clipped capture popover, an incomplete palette, and a membership-gate bypass on the upload sink).

Quality

  • 286 tests (shared 34 · app 57 · bridge 195), all packages typecheck (tsc --noEmit), app builds.
  • Each phase carries an adversarial review round; every review finding lands twice (the fix + the regression test).
  • Deviations are recorded honestly in the plan and the ledger — notably: the Phase-6 auth ships the identity + member-check contract but not the OAuth redirect flow (identity is attribution-not-authentication until it lands; the bearer token stays the transport boundary); the tick plan-DAG reader and a delivery-running fixture are deferred; the Phase-7 typed-confirm attestation is an honestly-documented host trust assumption.

Notes

  • ui/demo (a local scratch workflow project used for live-testing) is excluded and gitignored — not part of the deliverable.
  • The product's own code/tests/skills are untouched; proposed product-side changes live in ui/plans/UPSTREAM.md and are not implemented here (they await maintainer decision).

🤖 Generated with Claude Code

looksystems and others added 30 commits August 26, 2026 22:16
…low bridge UI

Eight phase plans with an intent baseline, five pre-code specification
artifacts (gate cards, session lifecycle, events, fixtures, attention
policy), upstream proposal tracking, and the findings ledger from ten
adversarial review passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The design contract for phases 1-6: app frame, six screens, a 23-component
catalog, and the visual language (provenance typography, gold-means-gate,
per-thread density) - every element traced to an intent commitment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-element intent traceability, BatchScreenCard and PassThroughAsk,
duplication and badge discipline, the keyboard model, the mobile sheet
story, thread-model fork closed and spine aligned with the event spec,
Tombstone/Archive split, BridgeBanner closed list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spine, watcher, store, version handshake, observability floor

Implements phase-0-rails.md deliverables 1–5, 7–8 against specs 1–5:
pnpm workspaces (bridge/app/shared); zod event vocabulary + gate-card +
journal + config schemas (provisional until the end-of-Phase-2 freeze);
in-process engine adapter via a thin child host with the width pin;
pure spine(repo) over first-parent commits with occurrence-unique ids,
stream epoch, and net-effect semantics; chokidar watcher with debounced
live layer, durable increments, epoch-change re-baseline; SQLite schema
(humans sentinel, cursors, read refs, sessions, gate ledger, comments,
digests, push ledger, event log with a persistent seq counter);
version handshake + migration posture (read-only banner, never runs
migrations); /health, SSE events, structured logs, cost counters, debug
console. 42 tests across both packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… docs

Replay pauses at user-answer boundaries and always ends paused at the
final one (Phase 2 adopt continues from there); offline mode consumes
scripted answers with exact/key match assertions. Converter ingests both
main-session and subagent transcripts (a subagent file marks every
record sidechain). ui.yml runs typecheck + tests on every ui/** commit,
never touching the product's gates. ui/README.md carries the phase-0 §5
packaging/discovery/config/API-key story.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts the recording flushed out

The fixture is converted from a real terminal-driven workflow session
(spec 4's Phase 0 form): a scratch project with the product installed
was driven through /workflow-start → discovery → feature commit →
discussion to a mid-flight stop — per-host limiter scope decided,
background review returned two ask-lane findings, 'Work through them
now?' gate pending. World snapshot = bundle + cache overlay (the lanes
report rides the overlay); spine golden pins 13 durable events;
regen-goldens.ts is the harness path — never hand-edit.

Recording surfaced four real defects, all fixed with tests: replay's
final boundary is the mid-flight assistant tail (outcome-keyed), not
the last user record; the live baseline mixed git-blob and sha256 hash
spaces, leaking spurious artifact.updated; head:null watcher seeds
re-walked history against the tip snapshot; changes in chokidar's
construct-to-ready gap were silently missed (ready() + sweep).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… gaps closed, specs amended

Three parallel reviewers (spec fidelity, intent baseline, quality +
security), every finding verified against the code before acceptance;
full dispositions in ui/plans/REVIEW.md round 6. Headlines: bridge
state moved out of .workflows/ (the one intent-constraint violation);
shared reachability for both epoch-change paths; live-layer branch-
switch race closed; live-only mode no longer forges a durable layer;
debug-banner XSS fixed; engine calls time out; overlay tar path-safety;
config file wired (width/port flow from it); CLI smoke test; offline
answers derived from the recording by the harness; never-auto surface
list extended with the render.cjs-verified finding-choice surface.
Spec amendments: turn = human input (spec 1), artifact.updated's
content-keyed exception stated (EVENTS.md). 62 tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Read lens)

Bridge: engine-host render/detail methods (startOverview, workUnitStatus,
epicDashboard, discoveryMap — the engine's own projections and
derivations, never re-derived), the durable needs-you tier (reconcile,
stale sources, triage, engine-derived blocks, report-pending), read-only
/api routes with path-safety, static SPA serving. App: Vite/React/
Tailwind per specs/components-layouts.md — the frame (rail · surface ·
context panel), S1 lobby with the knowledge-not-ready and empty states,
S3 channel (spine's admissible set only, commits exiled to the drawer,
threads as one-line previews), S5 Read lens (react-markdown + Shiki +
mermaid, firmness gradient, section anchors, 60ch measure), EngineEmbed
verbatim embeds, BridgeBanner closed list, provenance typography, gold
reserved and unused until a gate exists, cmdk palette (navigation only),
light/dark. 72 tests across the three packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, firmness made visible, gold protected

Headlines: epic channels no longer 500 (buildDiscoveryMap's real shape,
pinned by an epic in the API test world); the percent-encoded wu-segment
traversal and the symlinked-artifact read are closed with vector tests;
lobby roadmap/baseline now come from the engine's own startDetail
derivations; one staleness hop counts once. The firmness gradient is
real chrome; SpineItem carries all four catalog variants incl. the gold
gate ref; warn moved off the gold hue; mermaid securityLevel pinned
strict; duplicate status line suppressed; attachDerived parallelised.
Full dispositions in ui/plans/REVIEW.md round 7. 82 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… corpus

Spike (SDK 0.3.246, folded into session-lifecycle.md): width pin and
BRIDGE_ID propagate; CLAUDE_PID/CLAUDE_CODE_SESSION_ID are harness-
overridden — presence records carry the CLI's own identity, so the
bridge reads them back and sweeps cleanup itself; resume keeps the sdk
id stable (identity stays ours regardless); a disallowed tool denies
inline, no hang.

Parser (shared/gate-parser, pure, hash-injected): section extraction,
the option grammar (glyph/cmd/range/prompt rows, NBSP continuations,
strict deviation→refuse), normalizedBody, gate identity, kind taxonomy,
never-auto with typed-confirm defaulting. Ask eligibility was CORRECTED
by measurement — the engine's real rule is 'every MENU section stops
unless the auto instruction says do-not-stop' (gateway menus and
finding announce render plain; spec 1 amended, superseding wording
recorded). Never-auto list gains the render.cjs-verified spec signoff
surface. Authored adversarial corpus (truncated/reordered/paraphrased/
NBSP/two-menus/auto-mode) + a lane parsing the REAL recorded
transcript's sections with byte-identical gate ids. 32 shared tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ary, queue, surface sweep

Sessions (bridge/src/sessions.ts): one query per turn via resume behind
a driver interface (real SDK + scripted test driver); every event tees
to an append-only journal (synchronous — it is the re-derivation
source); gates are PROJECTIONS re-derived from the journal, never a
table (the ledger is audit); serialized answer path with CAS (races
resolve visibly as resolved-externally, exactly one injection);
answer-while-dead verifies the journal tail before injecting; health
states incl. the spiked permission-denial detection; explicit end()
(every SDK turn carries a result — trailing prose is a pass-through
ask); restart restores gates from journals with identical ids; presence
sweep reads identity back from records per the spike.

Boundary (auth.ts): Host allowlist (DNS rebinding), Origin allowlist,
per-install 0600 bearer token on every mutation and the event stream.
Lease (lease.ts): O_EXCL + engine-style stale-break; the loser runs as
a read-only mirror with a banner. Queue (queue.ts): the two honest
tiers joined under spec 5's sort. Sweep (sweep.ts): the engine's MENU
surfaces enumerated from source — a test pins NEVER_AUTO_SURFACES over
every AUTO_OVERRIDE_LINE site; allowlist GENERATED from skills'
frontmatter (paren-aware). API: queue/sessions/thread reads, token-
gated session-start/answer/end mutations. 123 tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… hosting

GateCard per the catalog: product-terms ask with the gold glyph, option
buttons with mono key chips (recommended marked, NEVER pre-selected —
initial focus is the free-text input), one answer path (key tap, typed
key from a focused row, or free text — all ordinary user turns),
typed-confirm never one-taps, relay-divergence notice, inert resolved
states. S2 queue: spec-5 order, both row anatomies (live ask / durable
flag), card overlay with channel context, pass-through rows open the
thread (no fabricated card), durable rows navigate to their resolving
surface. S4 thread: provenance-typed journal rendering (serif
conversation, mono engine sections, DATA never displayed), resolved
cards collapse to a line, PassThroughAsk reply box, session health
badge + end-session. Lobby hosts the single shaping thread; channels
get drive/open-session. Rail + channel badges derive from the queue's
own rows (gold dot = live gate). 17 app tests; build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, presence strip, checkpoint decisions

The end-to-end quick-fix ran from the bridge: captured by chatting the
capture skill in the lobby thread, promoted from the inbox working set,
shaped (MENU: shape gate), scoped, implemented (task gates; a/auto
exercised so auto-mode menus landed in the journal), reviewed clean —
unit completed, the 250ms retry delay landed in src/client.js with real
commits. Structured, typed-confirm (archived-item permanent delete, via
the never-auto label pattern) and free-text answers all rode the one
answer path. kill -9 with two gates open → restart → identical gate ids
re-derived from journals. Terminal presence on an epic topic coexisted
with the running bridge session; the mirror carries held/live rows and
strikes held topics. Cost counters surfaced the run (.40).

The run measured the permission stack into shape (each step recorded in
the spec/round 8): Bash(cmd) grants expand to Bash(cmd:*); acceptEdits
over default; dot-path writes are sensitive-file-gated above declarative
rules AND bare allowedTools names shadow canUseTool — so the bridge owns
a programmatic canUseTool policy (files within the project, Bash by
generated prefix incl. the BASE_BASH dev set, everything else denied
with the reason surfaced). PresenceStrip + channel presence join +
held-topic strike-through. Checkpoint decisions recorded in the plans:
custom-events-over-SSE (phase-0's own criterion fired), and neither
assistant-ui nor CopilotKit — the journal renderer is the surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dened, 12 defects fixed

Three reviewers (fidelity ×2, intent+security). Full dispositions in
ui/plans/REVIEW.md round 8. Security headline: the Bash allowlist was a
naive startsWith — 'git diff && curl evil' passed. Now bashCommandAllowed
rejects command/process substitution, splits on every shell control
operator, and requires EVERY segment allowlisted (injection-vector
tests). WebFetch blanket-allow removed; Write/Edit realpath containment
(symlink escape closed, matching round 7's read-path fix); timing-safe
token compare; session env redacts non-Anthropic secrets; the allowlist
is Bash-only (file tools governed by canUseTool, the dead entries gone).

Fidelity/intent: BatchScreenCard shows the finding batch (was answerable
unseen); MENU:cancel gate is never-auto (was one-tap destructive);
prompt-fallout flips to errored; the read-only-mirror banner surfaces;
injected turns tagged with the gate id; out-of-scope-bank + consult
stale-source queue rows; gateType mapping completed; truncated menu →
pass-through not a fabricated card (N3); the lease moved out of tree
(N1, was leakable to fixtures). Gaps closed: bridge-side one-session-
per-address, 4h idle sweep, buildOrderPos, NEEDS YOU lobby strip, thread
S4 links, 'bridge record-moment', resuming state, allowlist+policy test
lanes, n=2 restart test. One fidelity fork misread this session's own
in-flight folds as unauthorised edits — recorded in the ledger; no
revert, round 8 is the correct number. 149 tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests, coordinator

Pure attention policy (attention.ts) implementing spec 5's tables:
laned findings (any walk-lane → push once at report-landing, batch-only
→ badge+digest; app-connected downgrades to alert, quiet hours accrue);
laneless gate-type table (typed-confirm/consult/replan/signoff push,
task-loop/bootstrap badge, batch-screen digest, conflict pushes only
when it blocks alone, escalation overrides all). The lane mini-extractor
(lanes.ts) reads apply/decide/route from report markdown — anything else
or a present-unreadable file is walk; ENOENT is not-present. The durable
notifier (notifier.ts): at-most-once per (rowKey, contentHash), a
restart re-pushes nothing, quiet-hours accrual → morning roll-up, T_roll
collapse. EscalationTracker: once per attendance, re-armed by activity,
navigation grace. Digests (digest.ts): landed/waiting/next, lobby strip
waiting-suppressed. The coordinator (attention-coordinator.ts) binds all
to live gate events + a 30s tick (report scan, escalation clock, morning
roll-up, digest rebuild). Cursors/read-refs go live (read-ref recorded
on artifact view, HEAD-at-render for Phase 4). /api/activity, /api/digests,
web-push sink. 17 attention tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…AY strip, activity + watchdog

DigestCard (S6 as a card, waiting-suppressed in the lobby strip);
EscalationChip (idle age, escalation + quiet-hours markers, a state not
a countdown). The lobby TODAY strip (digest composition). App reports
app-connected + focused-thread activity (route-derived) for time-based
thread-scoped suppression, and runs a health-heartbeat watchdog that
raises the bridge-unreachable banner when health goes stale >90s. The
queue shows the escalation chip on live rows. Read-refs recorded on
artifact view. 20 app tests; build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drives the multi-unit event script through the real Notifier + policy:
three units in flight → exactly the signoff + walk-report pushes (≤5,
each actionable), everything else badge/digest and reachable via the
queue floor; the overnight case (2am walk report accrues, one morning
roll-up); restart re-pushes nothing; notifications-off still records
every decision (queue is the floor); an unrecognised lane pushes as walk.
Live-verified against the running bridge on the fixture world: at 2am the
restored walk-lane review report accrued (not pushed) — the overnight
case working in anger — and digests built for all three channels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…led, 9 defects fixed

Both reviewers converged: the pure policy was correct and tested, the
coordinator wiring feeding it live data was broken and untested. The
fold overhauls the coordinator and adds a coordinator-level test suite.

Fixed: queue escalated now joins EscalationTracker (spec 5's leeovery#1 sort key
was dead); digest 'next' renders the engine's own next-phase surface;
the notifier no longer conflates a quiet-hours-deferred push with a
batch digest (quiet-hours accrual moved into the notifier — batch never
pings); escalations are exempt from the T_roll collapse and an intraday
roll-up drains bursts (an escalation could be swallowed till morning);
the morning day-guard uses local date (UTC/local mix double-fired daily
outside UTC); report rowKeys carry the phase and hash the file path
(cross-phase reports suppressed each other); the activity heartbeat is
visibility-gated and heartbeat≠interaction (a backgrounded tab silenced
pushes and re-armed escalation); gateCeremony's engaged→none restored;
blocksWithNothingElse checks all open gates. Hardening: HEAD-sha cache,
report size cap + seenReports eviction. Full dispositions in REVIEW.md
round 9. 181 tests; typecheck clean; app builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structure comes from where it lives (corrected premise): discussion
subtopics + lifecycle from the manifest (never session cache), spec
sources AND consult_references from the manifest, investigation/review
heading-keyed (stable templates), research Read-only (low structure is
honest). Graceful degradation everywhere — no headings/no manifest →
Structure absent, never an error. Claim-chip finder (cmd → result
convention, best-effort, misses cost nothing) with anchors matching the
Read lens's own slug algorithm exactly. History lens: git file timeline
+ what-moved diff from the newest recorded read-ref to HEAD, with the
epoch-break 'lost' state (visible degradation, never a wrong diff).
/api/history, /api/roadmap, and structure+whatMoved folded into the
artifact view (ref read before the view overwrites it). 7 structure tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…what-moved, roadmap, verdict/brief

S5 gains Read / Structure / History. Structure is ABSENT (never an error
tab) when unavailable; the rail is per-type — MapRail from manifest
subtopics for discussions, SourcesPanel (sources AND consult references,
both blocking sign-off) + a contents rail for specs, heading rails for
investigation/review/brief. ClaimChip renders the recorded command → its
result with a COPY button and no re-run (N5; verification badge sourced
from the review report only). WhatMovedRibbon is an overlay chip → the
git diff since the last read-ref, with the epoch-break 'lost' state
(never a wrong diff). History lens shows the file timeline. Roadmap
surface: horizons/items/lifecycle/origin/sessions, read-and-navigate,
with links into pulled work units. VerdictBanner (review verdict-first)
+ BriefBadge (regenerable — not a record). Rail Roadmap link + route.
Anchors match the Read lens slug exactly. 28 app tests; build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…done-means

For an epic the topic differs from the work unit; the artifact path
carries it (phase/{topic}/…). buildStructure now derives the topic from
the path so an epic spec's sources + consult references join correctly.
Verified live: the payments-epic billing-model spec is decidable in the
browser — sources (incorporated/pending) and consult references live
from the manifest, the measured claim rendered as a chip; the what-moved
lens shows the diff of a post-read amendment; the freeform discussion
gets its 4-subtopic rail from the manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fixed, exit decision recorded

Two reviewers. Defects: epic discussion structure (flat-file topic
derivation), the Roadmap reading nonexistent fields (now the engine's
own state/work_unit — a 'renders from engine' fix), the read-ref
consumed by background refetches (now a deliberate POST /api/.../read
only on a focused view), the VerdictBanner keyword-sniff (now the
template's **Verdict** line + buckets), unknown-horizon items vanishing,
the dead BriefCard session-log link + positional panels. Gaps closed:
claim-verification badge (review-report-sourced only), spec claim rail +
review verdict/buckets, baseline layers coloured, the 60ch measure +
rail-collapses-first, pending fail-safe default, gate deep-link
scroll+highlight. Hardening: /api/history realpath containment, artifact
size cap, git leading-dash/non-hex rejection. Full dispositions +
the exit decision (live discussion map: kept CLOSED, no dogfooding
evidence asked for it) in REVIEW.md round 10. 205 tests; typecheck clean;
app builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etry + plan DAG readers

The source inventory (specs/telemetry-source-inventory.md) maps every
displayed datum to its manifest field or git fact BEFORE the surface —
the anti-scrape rule. buildTelemetry reads current task/phase, completed
phases/tasks, status, consolidation (gate mode / bank / staging /
consolidated phases), commits landed (from the durable store), and
agents-active — all manifest+git, no transcript. dep-blocked comes from
the engine's own derivation. The plan DAG reads the lowest-common model
(tasks+deps+status) per adapter: local-markdown parses task-file
frontmatter (fixture-pinned), tick reads its CLI graph (stubbed — not
installed in the prototype), Linear degrades to link-out. Channel view
carries telemetry per implementation topic + planFormat; /api/plan
route. 5 telemetry/DAG tests. (No attempt-counter invented — it has no
durable field; recorded in the inventory as a possible upstream.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ard, PlanDAG; inventory correction

The Delivery section on the channel (only when an implementation topic
runs, collapsed by default, never animates): TelemetrySurface shows
current task/phase, completed phases/tasks, fix-attempts + analysis
cycles, dep-blocked ⚑, the consolidation gate ◆, agents-reading, and
commits — all from the manifest. ConsolidationCard renders the bank +
staged tasks as one decide-shaped read view (answering rides the
session's gate). PlanDAG lays the local-markdown tasks by dependency
depth in plain CSS (no heavy graph dep for a read-only scannable view),
degrades a Linear plan to a link-out. Verified live: the completed
quick-fix's telemetry (phases 1,2 done, consolidation gated) and its
2-task local-markdown DAG both render from the manifest/files.
Inventory corrected: fix_attempts + analysis_cycle_total ARE durable
fields (verified against the real manifest) — shown directly, not
invented. 216 tests; build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…}.tasks.{n}

The manifest's staging is staging.{cycle}.tasks.{n}=<decision> (a
nested object per fields.cjs), not the array CLAUDE.md's shorthand
suggested — buildTelemetry's Array.isArray check made staging always
empty. Now flattens each cycle's task decisions to a list; the
ConsolidationCard renders task + decision. Verified live: c1 shows
task 1 approved, analysis cycles 2. (Caught pre-emptively from the
round-11 prompt.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…coped agent count, dag hardening

- planFormatOf resolves the requested topic's format (was: first planning item)
- countAgents scoped to the implementation phase; countAgentsAllPhases for the header chip
- readLocalMarkdownDag skips symlinks + caps at 1MB (mirrors round-10 hardening)
- channel Delivery detail moved behind each topic's collapsed TelemetrySurface (kills the S3 seam)
- plan DAG degrades honestly on a dependency cycle; natural task-id sort
- staging shape corrected to staging.{cycle}.tasks.{n} across code, inventory, plan
- tests for symlink-skip, cycle banner, natural sort; REVIEW.md round 11 ledger

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sence, comments

Backend (bridge):
- identity.ts: single-user sentinel (zero-config) + github-mode member check
  (real GitHub push-access API, fail-closed), server-side cookie sessions
- ownership.ts: gate ownership as UI-side routing (driver→channel-default
  precedence, claim/reassign, 24h-stuck, mayAnswer, externally-resolved join)
- capture.ts: ephemeral headless capture-skill session, durable-fail lobby row
- presence-humans.ts: humans-viewing heartbeat + inferred working sessions
- comments.ts: threads on gates/artifacts, unread ceremony, per-viewer reads
- db.ts: Phase 6 tables (auth_sessions, session_drivers, gate_owner_activity,
  channel_defaults, human_presence, failed_captures, comment_reads) + guarded
  humans.github_login column
- api.ts: whoami/login/logout, claim, capture, comments routes; ownership +
  unread overlay on queue/channel/thread gates; per-human read-refs
- cli.ts: wire Identity + CaptureRunner, assign owner on gate open

Frontend (app):
- GateCard: owner badge, watcher read-only, claim, unread-comments ceremony
  blocking the confirm, quote-to-draft, "answered outside the UI"
- GateComments, CaptureButton (optimistic ack), FailedCaptures, IdentityBadge
- PresenceStrip: three honest kinds (viewing / working / inferred)
- Queue rows: owner/stuck/unread cues; openStream degrades without EventSource

247 tests green; capture gesture verified live (wrote inbox idea + provenance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ness, capture attribution

Security/correctness (with tests):
- capture retry that fails again no longer duplicates the lobby row (discard-first)
- github-mode membership now gates writes: a non-member is a read-only watcher
- capture concurrency cap (4-slot) + capture-scoped Bash allowlist (least privilege)
- non-member login returns 200 (authenticated watcher), not a cosmetic 403 w/ cookie
- externallyResolvedAt: sign-off resolves only on topic-addressed phase.completed
  (was falsely resolved by the topicless workunit.status-changed)
- "answered outside the UI" now applies on every surface (thread/queue/channel)
- /api/sessions openGate enriched → queue overlay shows watcher/unread ceremony
- inferred-sessions reads state.json mtime (presence files only exist for
  research/discussion) — the kind was always empty; verified live
- capture stamps the authenticated author into provenance + body; message-level
  capture-from-message affordance in the thread; verified live ("by You")
- spec-5 queue default view = mine + unowned + stuck (github mode)

Intent/UX:
- ownership overlay suppressed in single-user (zero-config, identity invisible)
- artifact comment threads on the Artifact screen; claim wired into session thread
- watcher/blocked option rows disabled not dead; gold no longer leaks to comment/
  capture chrome; failed-capture rows show the error; whoami sends the real repo

Deviation recorded in phase-6 §1: identity+member-check contract shipped, not the
OAuth redirect flow (deployment wiring) — identity is attribution not
authentication, bearer token stays the transport boundary. REVIEW.md round 12.

249 tests green; capture author + inferred-presence fixes verified live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…resources, parity

- bridge/src/mcp.ts: a third client of the bridge. JSON-RPC-over-stdio MCP server
  (no new dependency), tools (workflow_state/queue/open_gate/answer), ui:// gate-card
  and queue resources rendering the frozen shared card schema — no second card logic
- bridge/src/mcp-client.ts: HTTP BridgeClient, authenticated as a named human
  (install bearer + per-user Phase-6 cookie) — reuses the one answer round-trip
- cli.ts: `bridge mcp` stdio entry (secrets from env, never argv)
- Typed-confirm attestation enforced at the bridge (single point): a never-auto
  gate accepts an answer only with an explicit ui-gesture attestation (SEP-1865);
  a plain model tool call is rejected with a deep link. SPA answers attest.
- The MCP card renders typed-confirm read-only; open-gate comments render read-only

Parity tests: same fixture gate via SPA and MCP -> byte-identical answers + identical
ledger rows; negative: a model tool call answering a typed-confirm gate is rejected
(and the raw endpoint rejects it too); A-owned gate refuses B's MCP submit. Verified
live: stdio initialize/tools-list/workflow_state through the running bridge.

258 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…attestation honesty

- ui:// resources gain a host-bridge script producer (OpenAI Apps / MCP-UI
  postMessage, feature-detected): tap gates + queue rows are interactive;
  typed-confirm stays read-only (never originated from the widget)
- runStdio caps the stdin buffer at 4MB (was unbounded — newline-less DoS)
- typed-confirm attestation guard looks the gate's confirm up by id (live card
  or ledger), independent of how the holder resolved
- MCP answers recorded as via:'mcp' in the ledger (was 'ui' — audit trail now
  distinguishes MCP from SPA; provenance != parity; test asserts it)
- resources/read serves a gate-card template (list/read now consistent)
- initialize echoes the client protocolVersion; notifications get no response;
  mcp-client encodeURIComponent's the gateId; github-mode-no-cookie warning
- attestation comment made honest: it's a host trust assumption the bridge
  cannot verify; the shipped widget keeps typed-confirm read-only so weakening
  it needs both a mis-implementing host AND a prompt injection

262 tests green. REVIEW.md round 13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
looksystems and others added 11 commits August 27, 2026 07:59
… via browser test

A round-12 edit placed `useState(note)` AFTER the `if (!thread) return` guard,
so the loading render ran one fewer hook than the loaded render — React's
hook-count invariant broke and the thread screen (and, with no error boundary,
the whole app) rendered blank. Moved the hook above the guard.

Caught by driving the live SPA with agent-browser (the /s/:id route was blank
with "Minified React error leeovery#310"); unit tests missed it because they didn't
render SessionThread through the null->loaded transition. Added a test that does.

263 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oadmap

Both found by driving the live SPA with agent-browser.

- CaptureButton opened its fixed-width popover with `right-0` from a left-rail
  button, so it extended ~85px past the left viewport edge (invisible). Added
  align/direction props (default left/down); the rail button opens upward-right
  so it never clips. Toast repositioned to match.
- The ⌘K palette hardcoded only a Lobby item (+ channels), omitting Queue and
  Roadmap. Now lists all three top-level rail destinations plus channels.

Tests: SessionThread-style jsdom shims (ResizeObserver/scrollIntoView) for the
cmdk palette; palette lists+navigates; capture popover alignment classes.

266 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…to cache)

The four bespoke chat inputs (thread reply, gate-card free-text, comments) are
consolidated into one <ChatInput> with a consistent keyboard model (Enter sends,
Shift+Enter newline), disabled/busy states, and optional controlled mode (so the
gate card keeps quote-to-draft). CaptureButton stays a form (kind selector).

File attachments (pick / paste / drop) materialize into the gitignored cache and
are referenced as [attached: <path>] in the turn, so the session's existing Read
tool picks them up — no new session round-trip, no forking the product model.

Backend: POST /api/attachments writes to .workflows/.cache/{wu}/attachments (or
.uploads/{session} for the lobby) — the ONE deliberate, documented exception to
"the bridge writes nothing to .workflows/", justified because it's transient,
gitignored, user-input-in-transit (not bridge state) in the purgeable cache.
Hardened as a file-upload sink: name sanitized to a safe basename, random prefix
(never clobbers), 10MB cap, realpath-confined under .cache/, ids validated.

Attachments enabled on session-answer inputs (thread reply, gate answer);
comments are text-only. Verified live: upload round-trip (traversal stripped,
path-safe, gitignored), oversize→413, the reply box renders the shared input
with the attach affordance.

278 tests green (8 upload-security, 4 ChatInput).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adBody hang, quota

Adversarial review of the new file-upload sink. Traversal/containment core
confirmed sound; folded:
- the attachment route now applies the github-mode membership gate (was returning
  before it — a non-member watcher could write files)
- readBody settles on every terminal event (req.destroy fires close/error, never
  end — the promise hung and pinned the buffered body); unit-tested
- a per-target attachment cap (200 → 429); dead base64 catch removed;
  import node:crypto (was the unimported Web Crypto global)

REVIEW.md round 14. 284 tests green (13 attachment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After sending an answer/reply the bridge awaits the whole session turn (often
many seconds); silent waiting read as frozen. A small, scoped spinner + "the
session is working…" now shows during that window in the thread reply and the
gate card, gone the moment the response lands.

A deliberate, requested exception to the codebase's "no spinner theatre" motion
rule — that rule governs AMBIENT surfaces; this is transient feedback for a
synchronous action the human just triggered, not an attention-grab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A session cut off mid-turn (a bridge restart/crash — its journal tail is a
tool-result, not an ask) restores as 'dead' with no open gate. The thread only
renders an input when there's a gate, so such a session was a dead-end: the
SessionHealthBadge said "resumable" but offered no way to resume it.

- SessionManager.resume(id, text): runs a fresh turn resuming the recorded SDK
  session, no gate CAS (there is no gate); refused while live/ended
- POST /api/session/:id/resume
- SessionThread shows a resume ChatInput (with attach + working spinner) when
  the session is dead/errored with no open gate

Verified live: the interrupted demo session now shows "This session was
interrupted — send a message to resume it." with a resume box. Tests: bridge
resume (dead→turn runs, resumes the sdk session), SessionThread resume box.

286 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ui/demo is a scratch workflow project used for live-testing the bridge — not
part of the UI deliverable. Ignoring it prevents its .workflows/ state (and a
binary knowledge store) from being swept into commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The README was frozen around phase 0/1 (app "empty shell", shared "provisional",
"no session driving"). Refresh it: the SPA is built and is the primary UI (build
+ open http://127.0.0.1:4870/), sessions are driven, and add the phase-6 auth
modes, the phase-7 `bridge mcp` invocation, the chat-attachment cache exception,
and `pnpm build` to the test/run steps. The accurate parts (engine discovery,
width pin, config, out-of-tree state, version handshake) are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three self-contained HTML design/presentation pages under ui/artefacts/ (the
claude.ai publish-time frame runtime stripped so they render standalone):
the-workflow-bridge (overview), workflow-bridge-screens (screen mockups), and
agentic-workflows-field-guide. Linked from the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assistant messages were shown as raw markdown source (**bold**, > quotes, ```
fences all literal — caught in the browser). Render them through the existing
Markdown component (react-markdown + remark-gfm + Shiki + mermaid) with the
read-lens prose styles. Engine sections stay verbatim mono (tool-result →
EngineEmbed) — the two are now visually distinct. Test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant