Skip to content

cockpit v3: steering end to end on the live node API (milestone 2 integration) - #218

Merged
LittleChimera merged 4 commits into
stagingfrom
factory/v3-m2-integrate
Nov 4, 2026
Merged

LittleChimera merged 4 commits into
stagingfrom
factory/v3-m2-integrate

Conversation

@LittleChimera

@LittleChimera LittleChimera commented Nov 4, 2026 •

Copy link
Copy Markdown

Related issue

Closes # (cockpit v3 milestone 2 integration, "Steer at the node"; notes/factory-v3/final/plan.md §1–§3)

Summary

Milestone 2's pieces landed separately (#213 S8–S10, #214 W6/W8/W10, #215 S11/S12, #216 W7/W9, #217 S13), but the web still read none of the steering routes: SERVED_STEER was empty, so on a live factory every verb said "this server cannot preview it yet". This PR switches every route on and fixes what did not match once the web read the real server. Then it drives each verb, Needs you, Find, the factory tree, the lenses and the phone end to end, against a local server seeded like Luka's factory.

ELI5: before, pressing "Start now" on a live factory did nothing. Now it shows the real plan (where the panel moves among its siblings, what its wait becomes, who approves). Apply does it, and Undo takes it back from three places: the toast, the "Undo (n min left)" button beside the node's verbs, and the verb's card in the node's chat. Applying C-16 from a status line can be undone too, and that Undo really puts things back: C-16 waits for a person again under a fresh ask, T-16 is held for it again, and the ask is back in Needs you.

NodePage verb (s/a/t/u/p/m/n, chat words, quick verbs)
   │  liveNodeSource = SERVED_STEER: preview · apply · undo · needsYou · find · seen
   ▼
POST …/n/{ref}/preview ──► nodeWire.normalizePreview
                            ├─ ghosts with a column → dashed in that column (moved node struck where it was)
                            ├─ "Wait: …" sentences → labelled lines; one arrow per before → after
                            └─ "depends_on_done: …" → words; can_apply:false → Apply off
POST …/n/{ref}/apply ──► toast Undo + "Undo (n min left)" + preview_applied card (token survives a reload)
POST …/undo/{token}  ──► server's "undone" card names the token → the card stops offering it
answer D-45 "Apply" ──► GET …/proposals/C-16: Undo only if applied with undo_until (never a rule)
                     └─ Undo = POST /decisions/{id}/undo ──► C-16 undone, then pending under a fresh D-n
                                                             (T-16's hold back, the ask back in Needs you)

Plan bullets → what this PR does

W6. Verb previews and Undo, wired (v3/nodeSource.ts, v3/nodeWire.ts, v3/verbs/, v3/nodeSteer.ts)

  • SERVED_STEER lists preview, apply, undo, needsYou, find and seen, and the live source carries all of them.
  • Mismatches with the S12/S13 server, fixed in the web:
    • The server lists the changed node under columns_after.ghosts with the column it lands in. A moved node keeps its own state there. The mini columns used to ignore ghosts, so neither the ghost nor the struck place showed. previewColumns now draws those ghosts dashed in their column. A proposal's ghost with no column is the page's own and stays apart.
    • effects arrive as sentences ("Wait: …", "Order: …", "Depends on: …"). A one- or two-word lead now becomes the line's label.
    • Warnings arrive with a code prefix (depends_on_done: …). The code is left out.
    • Values that already start with "→" (auto-merge's → sample-main (here)) no longer get a second arrow.
    • can_apply: false keeps Apply off and says who applies it (an owner's verb seen by an editor).
  • Add child sends what it waits on as the projects op takes it: depends_on: [ref] with depends_why: {ref: reason}. The old single string was a 400. A new child defaults to First, which is where the server places a child that waits on nothing. The pull-forward banner names the column the server placed it in, and the server's sentence that repeats the banner is not shown twice.
  • Undo:
    • An answer that applied a projects proposal (Apply on D-45 applies C-16) gets an Undo while the server keeps one: after the answer the web reads the proposal and offers Undo only when it is applied with an undo_until, until that time on the server's clock. Undo calls POST /v1/factory/decisions/{id}/undo (new, below). It is offered in the toast and beside the verbs, from the status line and from Needs you.
    • Each verb's preview_applied card in the node's thread offers "Undo (n min left)" by its token, so the Undo survives a reload, to the person who applied it (meta.by_user) or an owner. The card that Undo posts now names the token (meta.undo_token, server), so the applied card stops offering it.
  • Name it labels its pick "File under". The server's line now says what it offers ("only a stored feature is offered"). It used to say "a need one level up" while listing features.
  • Team: the shared useTeamPreview sent its first preview with the settled-but-empty body, so the first pick got a 400 (send changes). It now waits for the changes. The member rows size by the panel's own width (container query). Beside the 40 % chat column, the vendor and model pickers had collapsed to zero.

W8. Sheets on the live routes (v3/sheets/V3Sheets.tsx, palette)

  • Needs you, Find and the "n need you" pill now read the real routes.
  • A person's proposal arrives as Markdown ("Why: …"). Status lines and Needs you read it as words (askWords).

W10. Since last look: PUT …/n/{ref}/seen is sent after 3 s on the live source.

W5 / S11. The node chat

  • Children with news read the per-node threads (GET chat/threads?nodes=1, the follow-up named in cockpit v3: threads per node and structural verbs with preview, apply and Undo (S11, S12) #215). Read marks update both thread lists.
  • On a phone, a verb tapped in the full-screen chat used to open its preview behind the overlay. The chat now closes first, so the preview opens on the page, in view.
  • A verb filed as a proposal posted the same proposal card twice (filed, then applied). The node chat now shows one live card, with the earlier copy as its line. The Room's own threads are unchanged.
  • Chips end in "…" when they are wider than their row (phone).

S12 / S13 server fixes (node_verbs.py, nodes.py)

  • Start now no longer warns about links it already had to finished work. Its chip already says "met"; a new link to finished work still warns.
  • The undone preview_applied card names the token it undid.
  • Name it's first read says what it offers (see above).
  • Factory › Team says "5 members · all idle" instead of "· 0 running" (nothing renders as 0).

W9. Factory tree, W7. Lenses: covered end to end (below). The e2e seed now has a base env.

W11 (part): steering end to end (tests/e2e_ui/factory_v3/test_v3_steering.py, new)

  • The seed holds "google-ads-integration" in wave 2 (title match for the pull-forward). It also seeds the base env: the head label, the deployed commit holding every merge, and base-env's env reported ready with smoke OK.
  • 11 tests:
    1. Apply on D-45, then Undo: C-16 is pending again under a fresh D-n, the status line ends at needs you: Apply C-16 (D-n), T-16's line is what it was before, and Needs you lists the fresh ask (not D-45).
    2. Start now on the waiting panel: ghost in First, struck in Then, labelled lines, no met-link warnings; Apply changes the status line and the chip to PR-9876 · met; Undo restores the line.
    3. Add child "Google Ads integration": pulled forward from Later, Later 2 → 1, "Plan … now" in feature:I-1 and in the chat column.
    4. Autonomy & merge on I-1: covers the two live projects, leaves main to people; who_may: an owner; Apply; the chip is inherited by the project and its task; build-tier-drafts-3 is untouched.
    5. Team swap: nothing preselected, then a pick shows the cost line $a/h → about $b/h; Apply is a definition version; Undo re-pins.
    6. Needs you order: D-45 (holds T-16 and the panel), the merges into main, D-49; the quiet pair with "Apply all 2"; D-46…D-48 closed; the factory tree's asks.
    7. Find T-16, #9876 and C-16 each open their node.
    8. Factory › Environments shows the base env (sha, smoke OK, sign-in, 7 merged changes); Health lists the closed alarms; the root says "all in base env".
    9. Every offered lens on the feature, a project and the factory root, plus T-16's lineage: no line between nodes, no empty cell, no sideways scroll.
    10. 390: every verb is reachable in its row; the chat opens full screen; Start now from the chat opens its preview in view.
    11. Apply on C-17 (a rule) from the factory tree's Needs you: a definition version, no Undo offered, the ask leaves the list.
  • checks.py holds the sweep both e2e modules use. An icon inside a control (button, link, chip) is a glyph, not a line between nodes; an icon anywhere else on the node page still counts.

Review fixes (round 2)

  • Blocker: Undo after Apply on a decision did not put things back. New POST /v1/factory/decisions/{id}/undo (proposals.undo_answer, HUMAN_ORIGIN, EDIT; MANAGE for an owner-class proposal): the proposal's own Undo runs (same checks; undo was split into _undo_checks and _take_back, behaviour unchanged), then the proposal goes back to pending under a fresh mirrored decision with based_on read again, no grace timer and a 72 h window from now. Holds on it that the sweep let lapse since it applied come back (tasks.hold_again, task.held, [Factory notice · HOLD BACK · T-n] to the PL). The proposer is told [Factory proposal · C-n back to pending …]. The web calls it, and the toast says where the ask is now. The sample engine does the same: a fresh D-52, and what waited on D-45 waits on it. The e2e checks after Undo that the status line ends at needs you: Apply C-16 (D-…) with the fresh id, that C-16 is pending under it, that T-16's line is what it was before, and that Needs you lists the fresh ask and not D-45.
  • Major: Undo offered for answers that cannot be undone. liveNodeSource.answer now reads GET …/proposals/{C-n} after an Apply and returns the Undo only for an applied projects proposal with an undo_until. expiresAt comes from undo_until through the shell's clock offset. New unit tests cover a rules proposal (no Undo), a projects update (Undo until its undo_until), and Reject (no proposal read).
  • Minor: a verb card's Undo for everyone. preview_applied cards carry by_user (node_verbs._card, the chat.py whitelist, PreviewAppliedMeta). The card offers Undo only to that person or an owner (the same rule as undoLeft).
  • Minor: the 15 s tick re-rendered every card. The clock now lives in AppliedOf and ticks only while its own Undo is open. applied and renderCard change only with the messages, the viewer or the undo function.
  • Minor: ?nodes=1 unbounded. At most the newest 50 node threads (NODE_THREADS_MAX), with a server test.
  • Minor: the sweep skipped every icon. BODY_CHECKS now skips a lucide icon only inside a control (button, a, [role="button"], [data-testid="v3-chip"]). In the chat column the Room's cards and the chat's icons stay out, as before. All 18 e2e pass with the tighter sweep.
  • Minor: a contradictory toast when the proposal failed to apply. The 4xx body now carries reason. The toast says D-50 answered; applying C-17 failed: the definition already has these values, refreshes the pages and offers no Undo. The seed's C-17 and C-18 now add real rules. A new e2e applies C-17 from the factory tree's Needs you: it becomes a definition version, no Undo is offered, and the ask leaves the list.
  • Nothing skipped.

Deviations

  • temp-main is sample-main in tests. FORBIDDEN_SAMPLE_TOKENS forbids the real branch name in fixtures. The autonomy test covers the same thing on the seed's base.
  • Shared code touched, v1/v2 behaviour kept:
    • useTeamPreview no longer sends a preview with no changes. v2's team editor loses only a failing request.
    • RoomCard passes preview_applied to the node renderer only when cockpit v3 provides one.
    • useRoomThreads and fetchRoomThreads take an optional nodes.
    • Read marks update every thread list.
    • The proposal-card fold runs only in the node chat.
  • Answer Undo asks again under a fresh decision. D-45 stays answered (history); C-16 comes back as pending with a new D-n, as a reopened proposal does. v2's POST …/proposals/{ref}/undo is unchanged (still ends undone).
  • Owner-only for Autonomy & merge is checked in the e2e through the server's who_may and can_apply, because the e2e server is single-user. The editor's 403 is covered by cockpit v3: autonomy and auto-merge per subtree (S13) #217's server tests.
  • Not changed:
    • A proposal card in the node chat still shows v2's own Undo next to the verb card's Undo.
    • Start now's "Wait" line keeps the server's parenthetical path.
    • Timeline axis labels can crowd "now" when the lens column is narrow.
      These are follow-ups.
  • Config: none. No migration, no definition or data change, no live data touched. Nothing runner-side changed (no file under omnigent/runner, omnigent/tools, omnigent/policies or omnigent/factory.py). The new route and card field are server-only and additive: an older web ignores by_user and reason.

Test Plan

Worktree on staging @ 74d87c8 (unchanged since), OMNIGENT_SKIP_WEB_UI=true CI=1, final head:

  • E2E: npm run build in web/, then uv run --frozen pytest tests/e2e_ui/factory_v3 --ui-skip-build --browser-channel chrome → 18 passed (7 existing + 11 steering), with the tightened icon sweep.
  • Server: uv run --frozen pytest -q -n 8 tests/server/factory tests/examples/test_factory_sample_tokens.py tests/runner/test_factory_node_tool.py tests/server/test_openapi_drift.py → 4539 passed. New in this round:
    • test_undo_answer.py: Undo of an Apply puts the charter back, leaves the proposal pending under a fresh decision (old one stays answered), puts back the hold the sweep let lapse and tells the PL and the proposer; the fresh ask applies again; an open ask, an unknown id and a closed window are refused; a Reject or a second Undo is refused; someone without access cannot.
    • test_node_threads.py: ?nodes=1 caps the node threads; the preview_applied card carries by_user.
    • test_human_origin.py: the new write route carries HUMAN_ORIGIN (10 routes).
    • Earlier: test_node_verbs.py (Start now's met links, the undone card's token, Add's list with reasons, Name it's wording), test_nodes.py (no "0 running").
  • Web (web/): tsc -b, npm run lint, npm run format:check clean. vitest run src/components/factory src/pages/factory src/lib → 237 files, 4193 tests passed. New in this round:
    • v3/answerUndo.live.test.ts: Undo offered for an applied projects change until its undo_until; none for a rules proposal, an expired or failed one, or a Reject; Undo posts /decisions/{row_id}/undo and names the fresh ask; a refusal that still answered reads as "answered; applying failed".
    • v3/steerLive.test.tsx: Undo on the status line brings back needs you: Apply C-16 (D-52), T-16 waits on D-52, Needs you lists it; a rule's answer offers no Undo.
    • NodeChat.test.tsx: a verb card's Undo shows only to the person who applied it (or an owner).
  • Security scan steps run locally on the PR diff (exfil-scan.py, secret-scan.py): pass.
  • CI: the failures here are the ones staging already has (they failed the same way on cockpit v3: autonomy and auto-merge per subtree (S13) #217): Pre-commit checks (ruff-format on staging's own files, the iOS swift-format config, pyrefly), E2E UI Required (maintainer waiver), stores-crdb and stores-mysql (schema), and E2E Tests (pi-native, claude-sdk and k8s harness journeys). The regex.exec lines the exfil scan flagged are now String.match.
  • pre-commit run --files <changed>: ruff, prettier, oxlint, web tsc and the hygiene hooks pass. pyrefly reports the 28 errors already on staging, none in changed code.

How to try it on a live factory: open it with ?cockpit=v3.

  1. On a waiting project, press s and read the preview. Apply, then use the Undo beside the verbs.
  2. On a feature, press a and type the title of a held item: it is pulled forward. Then press u.
  3. On a project, press t and pick another model for one member: the cost line appears.
  4. Apply a decision on a status line: its Undo stays beside the verbs. Press it: the ask comes back (a new D-n) and what it held waits again.
  5. In Factory › Needs you, apply a rule: no Undo is offered (it is a definition version; revert it from Versions).
  6. Use ⌘K with T-16, #9876 or C-16.

Demo

Captures from the seeded local server at 1440 (light and dark) and 390: https://claude.ai/artifact/ESKbtnfjzptJLskEwCuC6z (private; ask Luka for access). Screenshots are not committed (repo rule), and the CLI cannot upload attachments. The captures are in wt/v3-tmp/v3-m2-integrate/shots/.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

Manual: every verb, Needs you, Find, the factory tree, the lenses and the phone chat were driven by hand in Chrome on a server seeded with tests/e2e_ui/factory_v3/seed.py, at 1440 and 390, in light and dark. That run found the mismatches fixed here: the ghosts not drawn, the team's empty first preview, the phone preview opening behind the chat, the duplicate proposal cards and the "0 running" line. The e2e now covers each of them.

Changelog

Cockpit v3 can steer a live factory: verb previews, Apply and Undo, Needs you, Find and the last look now read the server, and an applied verb can be undone from its card for 10 minutes; undoing an applied decision brings the ask back.

🤖 Generated with Claude Code

LittleChimera and others added 2 commits November 4, 2026 08:07
Milestone 2's web seams now read the server: every steering route
(preview, apply, Undo, Needs you, Find, last look) is on the live
source, and what did not match the S9/S10/S12/S13 server is fixed:

- Previews draw the server's own shapes: the moved or added node sits
  dashed in the column it lands in (the server lists it under ghosts),
  "Wait: ..." sentences read as labelled lines, a plan's code prefix
  is left out of warnings, one arrow between before and after, and
  Apply stays off when the server says the viewer may only look.
- Add child sends what it waits on as depends_on: [ref] with
  depends_why, lands in First unless it waits on a sibling, and says
  the column the server placed it in.
- An answer that applied a proposal (Apply C-16) has its Undo (the
  proposal's own) in the toast and beside the node's verbs.
- A verb's card in its node's thread offers its Undo by token, also
  after a reload; the server's undone card names the token it undid.
- The node chat reads the per-node threads (?nodes=1) for its
  children's news; on a phone a verb from the full-screen chat closes
  it and opens the preview in view.
- Team's first pick never sends a preview with no changes (400);
  team rows size by the panel's width, not the window's.
- Asks read as words (a person's Markdown), chips end in an ellipsis.
- Server: Start now no longer warns about met links it already had,
  the factory team says "all idle" instead of "0 running", and Name
  it says it files under a stored node of its own level.

The e2e seeds the base env and a held Google Ads integration, and
tests/e2e_ui/factory_v3/test_v3_steering.py drives each verb, Needs
you, Find, the factory tree, every lens and the phone against a real
server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A verb filed as a proposal and applied at once posted the same proposal
card twice in its node's thread (filed, then applied). The node chat now
shows the newest as the live card and the earlier one as its line; the
Room's own threads are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XL Pull request size: XL label Nov 4, 2026
LittleChimera and others added 2 commits November 4, 2026 09:10
…server keeps one

- POST /v1/factory/decisions/{id}/undo takes back an Apply on a projects
  proposal's decision: the proposal's own Undo runs, then it waits for a
  person again under a fresh decision (as reopen leaves one), so the work held
  for it waits again (a hold the sweep let lapse comes back and its PL is told)
  and the ask is in Needs you again. The answered decision stays answered.
- The web offers an answer's Undo only when the proposal it applied is applied
  with an undo_until (never a rules, tuning or code change), until that time on
  the server's clock; Undo calls the new route and says where the ask is now.
- An answer the server stored but whose proposal failed to apply says so
  ("D-50 answered; applying C-17 failed: ..."), with no Undo; the failure body
  carries the reason.
- A verb card's Undo shows only to the person who applied it (meta.by_user)
  or an owner, and only an open Undo ticks its clock.
- ?nodes=1 lists at most the newest 50 node threads.
- The e2e sweep exempts icons only inside controls; the seed's C-17 and C-18
  add real rules, and a new e2e applies one from the factory tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LittleChimera
LittleChimera merged commit 270e0b7 into staging Nov 4, 2026
91 of 110 checks passed
@LittleChimera
LittleChimera deleted the factory/v3-m2-integrate branch November 4, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant