Repository navigation
cockpit v3: threads per node and structural verbs with preview, apply and Undo (S11, S12) - #215
Merged
Merged
Conversation
|
|
||
|
|
||
| def _error(status: int, code: str, message: str) -> JSONResponse: | ||
| return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}}) |
… and Undo (S11, S12)
S11. The Room gets a thread per need, feature, study and task (need:<ref>,
feature:<ref>, study:R-n, task:<ref>), each resolving only to a node of the
factory. Who answers is the definition's structure.responders per level:
Atlas for features, Scout for needs and studies (Atlas while discovery is
off), the project's lead for a task, with the task named; agents' messages
still route only by @mention, so node threads never loop. Every Room
delivery about a node carries its header (path, why, status line, roll-up,
open asks) within the factory's message cap (8000 chars; the header's
detail, then old context, then the longest message give way). A root label
omnigent.factory.node_header=off leaves it out. sys_factory_node(op="read",
ref) returns the node page to leads, PLs, Scout and the planner; an older
server answers not_served. Decision cards about a task, need, feature or
study are also carded in that node's thread, an alarm the server closed is a
closed_alarm card in its task's thread, and the cockpit's own cards (why,
offer, preview_applied) go only to their node's thread. chat/threads names
each thread's node (node_ref) and lists node threads with nodes=1; preflight
says node_threads.
S12. POST …/n/{ref}/preview plans a verb over the facts: effects, the
parent's columns with a ghost, the roll-up before and after, who approves,
and a preview_id over the facts it touches. POST …/apply runs exactly that
plan (409 stale otherwise), POST …/undo/{token} takes it back once within
10 minutes. Verbs: add (a project in a feature, pulling a held item forward;
a task; a feature), start now (depends_on may name a change, PR-9876, met
once merged), pause/resume, move and reorder (new PROJECT_OPS move and
reorder), split/merge/close, name it, team. 422 wrong_level keeps every
parent one level up; 403 authority names who may.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tch their apply, header only where v3 asks - Reorder with `after` lands in the column after that sibling's, and the preview's ghost, args and fingerprint say that column; a column with two candidate siblings asks which one. - Team Undo pins the version before only while the version the verb made is still in force (409 undo_closed otherwise), so a later definition change is never thrown away. - A person's add under a feature joins the wave it builds now (initiative `same_wave`), so its planned later waves stay ahead; a held item pulled forward leaves its wave, and its Undo puts it back. A hold goes to a wave after the current one. - plan_verb serves a verb only where structure.verbs lists it (resume with pause; reorder, split, merge and close with move) and reads structure.levels[].authority for who applies it; the node page offers exactly what is served (no add at the product, no move at a task). - The envelope guard never cuts delivered messages: it gives up the header's detail, the oldest context, then the whole header. - structure.node_header (node_threads by default, all, off) replaces the root label: v2's shared threads keep their envelope unless a factory asks. - by-<who> thread keys resolve only for a real creator; from-I-n too. - not_served also for the SPA's 404 body; an agent's reorder after a sibling needs a reason; the change chip uses the gate's repo rule; no empty roll-up delta; pause names what it cannot stop. - Close, move and name a feature or need go through the initiative edit and discovery create cores, with their events. - Chat card schemas and web types know the node cards, node thread kinds and node_ref. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ulls_forward, approver_note), apply says it in one line
- Move and Name it with nothing picked are the form's first read: a 200 with
the stored parents one level up, can_apply false, nothing to apply.
- Previews add approver_note, moved_from (Start now, Order, a pull forward),
pulls_forward and apply_label; apply and Undo answer with `message`, and
Undo's expires_at is an ISO time (the web parses it as one).
- The web's preview normalizer reads the server's roll-up
({nodes: [{title, items}], text}, or null) as lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LittleChimera
force-pushed
the
factory/v3-m2-verbs
branch
from
November 4, 2026 04:20
59b99ef to
e986913
Compare
|
❌ UI Snapshot doesn't match the committed baseline. If this UI change is intentional, update the baseline — each path renders in the same pinned image, so the result matches this gate:
Diff PNGs ( |
This was referenced Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes # (cockpit v3 milestone 2, plan items S11 and S12;
notes/factory-v3/final/plan.md)Summary
The owner said "i don't know how i'm supposed to steer it". This PR adds the server half of steering at a node.
stalewhen the facts it touches moved). Undo is available for 10 minutes. The verbs run today's cores (projects proposals, controls, task edits, initiative edits, the discovery create, definition versions), so they stay audited. A move never skips a level (422wrong_level). Start now relaxes a wait to the change it really needs (PR-9876 · met).structure.verbsgates each verb, andstructure.levels[].authoritydecides who may apply it. The node page offers exactly the verbs the routes serve.v1 and v2 behave as before. The Room's own thread list, its cards and its badge are unchanged. By default the node header rides only on cockpit v3's per-node threads, so the threads v2 shares keep their envelope. Agents only act through proposals: the verb routes are human-origin only.
ELI5: you are on a task page. You type in its chat, and the task's project lead gets the message with a short card on top: what the task is, why it exists, what holds it and what asks are open. You press "Start now" on a waiting project. First you see the plan: "waits on assistant-core → needs only PR-9876 · merged (met)", the project moving to First, and "You, as owner. Applies now; Undo for 10 min." You confirm, and an Undo stays available for 10 minutes.
Plan bullets and what this PR does
S11. Thread keys
need:,feature:,study:,task:with a responder per level (omnigent/factory.py,chat.py,chat_routing.py)thread_kindrecognises the four node prefixes (THREAD_NODE_PREFIXES).resolve_threadaccepts each key only for a node of this factory. Unknown nodes return 404unknown_node, never a guess:task:T-16, or a PR's implicittask:adhoc:<id>);need:O-3) and asked messages (need:ask:<id>);feature:I-1);need:from-C-12,need:from-I-2);by-<who>parents only when<who>started a project here (a need also when a feature was named under it).study:R-nwaits for S14's table.T-16→task:T-16).chat_routing.routesanswers node threads from the definition'sstructure.responders:nullroutes to nobody, with a note saying to @mention an agent.T-16and its header names "Task T-16 · <title>".S11. Header injection into each agent turn (
node_context.py, new;chat.build_delivery,Room._deliver)sys_factory_node(op="read", ref=…)"when your tools have it".full_tree, the 5 s memo). Any error leaves it out; a delivery never waits on it.structure.node_header(owners only, like the rest ofstructure):node_threads, the default: only the per-node threads only cockpit v3 posts in. So#factory, project and PR deliveries in v2 factories are unchanged.all: also#factory, a project's and a PR's thread.off: none.SPARSE_FIELDS), so an older server still reads the definition.ENVELOPE_MAX_CHARS(8000, the factory's message cap). It gives way in this order: the header's detail (its first line andStatus:stay), the oldest thread context, then the whole header. Messages that alone pass the cap go out whole.S11.
sys_factory_node(op="read", ref)(tools/builtins/factory.py,tools/manager.py,runner/factory_dispatch.py,api/node_agent.py)GET /v1/factory/agent/node. It returns the sameNodePageasGET …/n/{ref}, as the runner's owner may read it, with verbs disabled ("agents propose changes").not_served: FastAPI's bare{"detail": "Not Found"}, or the web UI's{"error": {"code": "not_found"}}when the server mounts the SPA.unknown_nodekeeps its code. No policy path was added to any bundle.S11.
chat_cards.py: card routing by node ref and new card typesalarms.read) about a task is aclosed_alarmcard in the task's thread, in place of the decision's copy there. Its meta is{was, cause, real_ask}.post_node_cardposts the cockpit's own cards only in their node's thread:why: a bare "why" in a node thread posts it and is not routed;offer;preview_applied: after every applied verb, with its Undo token; also when undone.schemas.ChatCardTypeandChatCardView.metaknowwhy,closed_alarm,offerandpreview_applied(ClosedAlarmMeta,OfferMeta,PreviewAppliedMeta). The web types gainpreview_applied, the node thread kinds andThreadView.node_ref.S11.
GET chat/threadsaddsnode_refproduct,fx, a project's ref,PR-nor the node thread's ref.?nodes=1also lists the node threads that have messages.node_threads: true, which switches cockpit v3's chat to per-node threads (W5 already reads it).S12.
POST …/n/{ref}/preview|apply,POST …/undo/{token}(node_verbs.py,api/node_verbs.py, new){preview_id, verb, ref, title, args, effects[], columns_after (ChildPage, ghosts dashed), approver, approver_note, can_apply, who_may, undo, filed, warnings, based_on};rollup_deltaas{nodes[{ref, title, items[{key,label,before,after}]}], text}, ornullwhen nothing in the roll-up changes;docs/FACTORY.md):moved_from(Start now, Order, a pull forward),pulls_forwardandapply_label(Add child),parents(Move, Name it).can_apply: false. Applying it is 400.based_on/ 409stale:preview_iddigests the plan (verb, normalised args) and the facts it touches. Apply re-plans over fresh facts and refuses with 409stale(carrying the new preview) when the id moved. Abased_onolder than the page only adds a warning, because unrelated churn does not make a write stale.structure.verbslists it for the node's level, else 422not_herenaming the entry. Resume is offered with pause; reorder, split, merge and close are offered with move.structure.levels[<level it changes>].authority. Empty seats mean nobody may (422not_here). Anownerapproval, or seats withownerbut notoperator, leave it to owners. Otherwise editors may. The core's own owner rules still apply on top. The node page (Tree._verb_applies) offers exactly what is served: no add at the product, no move at a task.wrong_levelnames the level a parent must be on. 403authoritycarrieswho. 400preview_required. 409nothing_to_do. The cores' own errors pass through.node.verb_applied(subjectundo:<token>) with how to take it back. Undo works once, within 10 minutes, by the person who applied it or an owner. It uses each core's own way back: the proposal's Undo (falling back to cancelling a project whose lead already started), the inverse control, the initiative edit, the stored value, or the definition version before. Apply and Undo answer with a one-linemessage.S12. The verbs
analysis.initiative.same_wave), so planned later waves stay ahead: v2's next wave and v3's Later still agree. "Plan X now" goes to the feature's thread for its responder.col: "later"holds it in a wave after the current one.depends_onmay name a change ref (PR-9876), met once that PR merged. Supported incharter,start(validation and"PR-n: why"items),gating,waits, both dependency checks (unknown PR → 400; never part of a cycle),initiative_view, the v2 graph (draws no project edge) and the node chips ("depends on PR-9876 · met"). The chip uses the gate's own rule (gating.change_met: the dependent's repo first).PROJECT_OPS:move({op, slug, to: "I-n"}): re-homes the project withprojects.move_project_core; Undo puts its feature and plans back.reorder({op, slug, after?, reason?}): stored as theupdateofdepends_onit makes among its feature's projects. An agent's reorder after a sibling needs areason(400dependency_reason_required, as for an update).sys_factory_project; a PL may reorder its own project.move; the reorder verb files the update. Withafter, the node lands in the column after that sibling's, and the preview's ghost, args and fingerprint say that column (a warning ifcolsaid otherwise). A column with two candidate siblings asks which one. A feature moves under another opportunity.opportunity.created), or is filed under one.definition/previewandPUT …/definition(TeamEditor's changes, or{member, vendor, model}). Undo pins the version before only while the version this verb made is still in force. After a later definition change it is 409undo_closed, so nothing newer is thrown away.Deviations
chat/threadsopt-in: node threads are listed only with?nodes=1, so v2's Room list and dock badge never count messages it cannot open. Cockpit v3'sNodeChatstill calls the shareduseRoomThreads(), so its "children with news" lines need a web follow-up to passnodes=1.controltargets. Noscopefield was added toPOST /control. A project whose lead has not started yet still starts once its dependencies are met (the gate does not know about pauses). The preview warns about it, by name.SERVED_STEERis still empty. Attachingpreview,applyandundoto the live source is the web PR after this one. This PR serves the fields cockpit v3: verb previews with Undo, sheets, since last look (W6, W8, W10) #214 documents. Its preview normalizer also reads this server's roll-up shape, so that wiring is one line.offerproducer yet: the server posts nooffercard on its own. The type, routing and meta are there for W6 and S13.auto(S13) andresearch(S14) are not served by these routes (400unknown_verb). Moving a task, and adding a need or goal, answer 422not_here.structure.node_headeris a definition field.Test Plan
tests/server/factory/test_node_verbs.py(52 tests, over a real store with an initiative):_next_waveand v3 Later agree after an add, a pull-forward and a hold);rollup_delta: null;by-<who>thread needs its node;approver_note,moved_from,pulls_forward,apply_label; apply'smessageand an ISOexpires_at.PR-9001 · met; an editor applying an owner's verb gets 403; a reader never applies; agents filemoveandreorder.tests/server/factory/test_node_threads.py(29 tests):all;ChatMessageView;node_ref, preflight and agent page tests.tests/runner/test_factory_node_tool.py: the SPA-style 404 isnot_servedtoo.tests/server/factory/test_change_dependencies.py, golden definition defaults (the new field), the node page web fixture (no product add, no task move).nodeSteer.test.tsxreads the S12 roll-up shape andnull.Commands (worktree, rebased on
staging@ 523a713, after #213 and #214,OMNIGENT_SKIP_WEB_UI=true CI=1):uv run --frozen pytest -q -n 8 tests/server/factory tests/runner/test_factory_* tests/examples tests/policies tests/factory -p no:cacheprovider→ 9046 passed, 1 failed:test_a_cold_build_with_history_answers_under_300mstook 0.99 s on a machine at load 12 (other agents building). It passes alone three times out of three.uv run --frozen pytest tests/e2e_ui/factory_v3 --browser-channel chrome→ 7 passed (SPA built from the rebased tree withnpm run build, then--ui-skip-build).npm run type-check,npm run lint,npm run format:check,npm test -- src/components/factory src/pages/factory→ 128 files, 2060 tests passed.pre-commit run --files <changed>:staging, none in changed code;no-hardcoded-models, prettier, oxlint, tsc and the hygiene hooks pass.FORBIDDEN_SAMPLE_TOKENSis green (tests/examples/test_factory_sample_tokens.py).How to try it (a person, on a server from this branch):
?cockpit=v3. Open a task page. Its chat now posts intask:T-n. The project's lead gets the message with a--- This thread is about Task T-n … ---header, aStatus:line and the reply call. A message in#factoryor a project's thread arrives as before.PUT …/definition{"changes":[{"op":"replace","path":"structure.node_header","value":"all"}]}. Project threads then carry the header too.curl -X POST …/v1/factory/factories/<fid>/n/<slug>/preview -d '{"verb":"start"}'. Read theeffects("needs only PR-n · merged (met)") andapprover_note, then apply it with itspreview_id. The project's chip reads "depends on · PR-n · met".POST …/undo/<token>brings the dependency back.{"verb":"move"}with noargslists theparents.{"verb":"move","args":{"to":"<a need ref>"}}is 422wrong_level.{"verb":"add"}atproductis 422not_here.Demo
N/A (server and API; cockpit v3's previews and Undo toast render in W6, wired in the next web PR).
Type of change
Test coverage
Coverage notes
The verbs run over a real SQL store through the routes. The node threads run through the Room with a recording mailer. The v3 e2e runs against a real server with the SPA built.
Changelog
Cockpit v3: every need, feature, study and task has its own chat thread whose agent sees the node's status, and structural changes (add, start now, pause, move, reorder, name, team) preview before they apply, with Undo for 10 minutes
🤖 Generated with Claude Code