Repository navigation
cockpit v3: node refs and the levels block (S1, S3) - #207
Merged
Merged
Conversation
S1. node_refs.py: one grammar for every node of the level tree (product,
G-n, O-n, ask:, need:, I-n, R-n, feature:, <slug>, T-n, adhoc:, PR-n,
commit:, run:, fx, fx:<area>, C-n, D-n, member:, env:), normalize() that
reads any spelling old or new (the read-time shim), resolve(facts, ref)
and the chat thread mapping. Decisions' subjects, issues' targets, events'
subjects and chat thread names are normalized on write to the node's key;
every key the server writes today is stored as it is.
S3. The structure block in the definition: per level name, plural, show,
debug and authority; home, responders, verbs, kind names, order columns,
tiles_max and fold_single_child, with the spec's defaults. Owner-only,
never a trial, and no agent proposes it. GET /factories/{fid}/levels
returns both trees with what is hidden in this factory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- node_refs: a PR spelling that names a repo (pr:<repo>#n, its URL, pr:<id>) keeps the repo; canonical_key rewrites it to pr:<id> only on an exact repo+number match and resolve() returns None otherwise. RefFacts indexes PRs by (repo, number), case-insensitively. - The agent decision route reads only the rows its subject names (RefFacts.for_subject: list_prs(number=n), get_pr, get_project) instead of every project and PR of the factory; the store's list_prs takes number=. - Definition preview: a change only owners make says "No agent proposes it."; level changes get their own data sentence. - structure.kinds names only the kinds not named after their level; /levels fills feature, project, task, factory... from the level names, so one rename renames both. structure.kind_authority holds a study's authority (Scout, within the need's research budget) and /levels returns it. - docs: decisions store a known PR as pr:<id>; events, issues and unknown PRs keep pr:<n>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply_now's compare-and-set wrote result=None before the cores ran and only wrote result.authority back at the end, so a second proposal carrying the same answer that checked answer_authority.claimed_by in between found no claim and applied too (test_two_proposals_at_once_carry_an_answer_out_once failed under load). The claim now stays on the row while it applies; a deterministic test checks it from inside the apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LittleChimera
force-pushed
the
factory/v3-server-foundations
branch
from
November 3, 2026 20:03
2a6ac50 to
807edc2
Compare
3 of 13 tasks
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
Cockpit v3, plan items S1 (node refs) and S3 (the levels block). Server only. Rebased on staging with #204 and #205 in:
I-nrefs resolve to the store's initiatives.Summary
The v3 cockpit addresses every node by one ref and reads its levels from the definition. This PR adds the two foundations every later v3 server PR (S2, S4 onward) builds on. v1 and v2 do not change: every subject, target and thread key the server writes today is stored exactly as before.
ELI5: every box in the level tree gets a permanent name tag (
T-16,PR-9876,assistant-core,fx:health). Old notes that called it "task:T-16", "!9876" or "#assistant-core" can still be read back to the same tag. What each level is called ("Feature", "Outcome", …), who answers on it and who may change it are settings the owner edits, not code.Plan bullets → what this PR does
S1. Node refs (P0)
node_refs.py: the grammar forG-n,O-n,ask:…,I-n,R-n, slug,T-n,adhoc:…,PR-n,commit:…,run:…,fx:…,C-n,member:…,env:…omnigent/server/factory/node_refs.py:parse()for every kind in api.md §2, plusproduct,fx,need:<implicit key>,feature:<implicit key>andD-n. Each kind maps to(tree, level). Ids are bare hex, shas lowercase, and implicit keys can never look like a numbered ref. A project whose slug is a reserved word (product,fx,factory) isproject:<slug>.resolve(facts, ref)resolve(RefFacts, ref) -> Node | Nonereturnsref, kind, tree, level, title, implicit, row. A spelling that names a repo (pr:<repo>#n, the PR URL,pr:<id>) only resolves to that repo's PR; another repo's#nisNone.RefFacts.load(deps, fid)reads the store once. S4 fillsimplicit,asksandcommitsfrom its projection. Integration and self projects resolve into the factory tree (item). An adhoc ref resolves only for a PR that no task owns.decisions.subject,issues.target,events.subject, chat thread keyscanonical_key()is applied on four paths:• the agent decision route, which reads only the rows its subject names (
RefFacts.for_subject: the PRs with that number, the PR with that id, or the slug's project):T-4→task:T-4,PR-9630/!9630/pr:9630/pr:<repo>#9630/the PR URL →pr:<id>(a spelling that names a repo only on an exact repo and number match; another repo's#9630is kept as written), a project's bare slug →project:<slug>•
issues.raise_issue,reportanddedupe_key•
events.emitandstored_subject•
chat.resolve_thread:product→factory,fx→improvements, an existing project's slug →project:<slug>,PR-n→pr:<id>normalize(text, facts=None)reads any spelling, old or new, as the ref. Without the store it reads refs, keys, the chat's!n/#slug/p:slug,pr:<repo>#<n>,session:<id>andfactory. With the store it also readspr:<id>,task:<slug>/<key>,task:<id>andloop:<id>, and a session that a task ran becomes a run. No migration and no history rewrite.task:T-4,member:<id>,pr:<id>normalize; unknown refs →Nonetests/server/factory/test_node_refs.py(187 cases) andtests/server/factory/test_node_refs_writes.py(6 cases on the real routes and store). Details under Test Plan.S3. Levels block in the definition (P1)
structurein the definition (definition_blocks.py): levels (key, name, plural, show, debug), responders, verbs per level, authority per level, order column names,tiles_max,fold_single_childStructureDefhas these fields:•
levels.<key>={name, plural, show, debug, authority: {create, move, approval}}for both trees: product › goal › need › feature › project › task › change › run, and fx › area › item•
home,responders(per level, plus the study kind),verbs(per level, plus study),kinds(names of the kinds not named after their level: opportunity, ask, question, proposal, study, adhoc, pr, commit, rule, agent, env, alarm, decision, definition),kind_authority(a study: Scout, within the need's research budget),order {cols, later},tiles_max(bounds 1–60) andfold_single_childLevel keys and their order are code (
TREES). Names must be unique per tree.when_any, changeinline, rundebug. Responders follow api.md §3.1. Verbs, kind names and column titles follow the prototype'sDATA. Authority follows critique §13: goals by owners, needs by the discovery ledger, features by owners, projects by autonomy, tasks by the lead at once.not_trialable: "how work is shown, not how it is done").GET /factories/{fid}/levelsomnigent/server/factory/api/levels.pyandlevels.py. Anyone who can read the factory can call it. It returnstrees.{product,factory}={root, name, home, levels[]}, plusresponders,verbs,kinds(a kind named after its level, such as feature, project, task or factory, reads that level's name, so one rename changes both),kind_authority,order,tiles_max,fold_single_childandversion. Each level carries itsauthority.show,debugandhiddenare left out when they hold their defaults.PUT /definitionshows in/levels; hidden-when-empty computed per factorytests/server/factory/test_levels.py(32 cases). Details under Test Plan.structure.is inOWNER_ONLY_PREFIXESandCLASS_MAP(B), and in staging'sAGENT_NEVER, so every seat gets 403path_not_allowedwith "the levels … are set by owners in the cockpit". It is in no seat'sAGENT_PATHS, so it is also agent-gated.Deviations (and why)
Stored subjects and targets keep the key spelling, not the bare ref. api.md §2 says these fields are "normalized to this grammar" and that a migration rewrites today's text into bare refs. I did not do that. v1/v2 web code and about 15 server readers parse the typed prefixes:
pr:in LiveFeed, runbookModel and activityModel;project:andmember:in the control menus;proposal:in decisionsPageModelpr_from_subject, task_pulse, waits, slack DM rulesStoring
T-4orPR-9630would break v2. Instead:canonical_key. A test enumerates every subject builder in the code.task:T-4,project:<slug>,decision:D-7,pr:<id>).normalize()reads both forms as the bare ref.The key form for a node is the thread-key form api.md §3.9 already uses (
project:<slug>,task:<ref>,feature:<ref>,need:<ref>,study:<ref>).showvalues arealways | when_any | when_many | inline, plus a separatedebug: true. api.md's sample has"show": "debug"for runs. The brief listsshowanddebugas two fields, and the prototype usesdebug:true, so I followed the brief.The tree root is not in
levels[]. As in api.md, each tree hasroot, and its display name comes from the root level (name). The root'sshow/authoritystay in the definition for later.The
structureblock also holdshome,kindsandkind_authority. The owner's rule is "level names and kinds from data". Kind display names (for example "Task (no ticket)") come from the prototype'sDATA.kinds, minus the kinds named after their level: those read the level's name, so nothing is said twice.Level counts for
hiddencome from stored rows plus implicit parents. If a level below has a node, the level above has at least one (its implicit parent). The factory tree'sitemcount is unknown here, so it is never hidden. S4's projection will replace these counts with exact per-nodelevels_below.Thread spellings.
chat.resolve_threadnow also accepts node refs for the threads that exist today (product,fx, a project's slug,PR-n).need:/feature:/study:/task:threads are left to S11.thread_key()andthread_ref()already map them, andT-4is still refused with 400invalid_thread.One existing test changed.
test_telemetry_span_idsasserted that a decision's span keeps the agent's raw PR spelling (pr:9724,PR: <repo>#9724). The decision subject is now that PR's key (pr:<id>). The span still lands on the same PR.Review follow-ups
node_refs: a PR spelling that names a repo was matched by number only, sopr:sample-org/other#9630became the sample-app PR'spr:<id>_prkeeps the repo fromowner/repo#nand the PR URL (Ref.repo, not part of the ref text).RefFactsindexes PRs by(repo.lower(), number).canonical_keyrewrites topr:<id>only on an exact repo and number match, else returns the text unchanged.resolvereturnsNone. With facts,normalizereturnsNonewhen the number exists only in other repos. As a side effect,pr:<repo>#77andpr:<id>now resolve to the right row when two repos share#77;PR-77alone still resolves toNone. Tests: the reviewer's three cases plus thePR: <repo>#nand@shaforms, case-insensitive repo names, and the agent route storingpr:sample-org/other#9630as written.structure(andautonomy)_whosays "No agent proposes it." when every changed path is inAGENT_NEVER. Level changes get their own data sentence ("Level names and settings change how the cockpit shows work, not how it is done; nobody is mailed."). Test:test_the_preview_of_a_rename_says_no_agent_proposes_it.kindsrepeated the level namesKindsDefkeeps only the kinds not named after a level.KIND_IS_LEVELmaps product, goal, feature, project, task, run, factory (→fx), area and item to their level, and/levelsfills them from the level names. The rename test no longer changesstructure.kinds.feature;structure.kinds.featureis now refused (422).structure.kind_authority.study={create: [scout], move: [], approval: budget}, returned by/levels._subject_keyloaded every project and PR on each agent askRefFacts.for_subjectreads only the rows the subject names:list_prs(fid, number=n)(newnumberfilter on the store, both backends, with a store test),get_pr(id)orget_project(fid, slug). Free text,T-4,member:and the like read nothing. A parametrized test checks the reads and that the key equals the one from fully loaded facts.PR-9630aspr:<id>pr:<id>when the store knows the PR; events, issues and an unknown PR storepr:9630.None skipped.
Also fixed (found while getting the suite green):
test_two_proposals_at_once_carry_an_answer_out_once(from #204) failed twice under load.proposals.apply_nowwroteresult=Nonein its compare-and-set and only wroteresult.authorityback after the cores ran. A second proposal for the same answer that checkedanswer_authority.claimed_byin that window found no claim and applied too. The claim now stays on the row while it applies (one line).test_an_answer_stays_claimed_while_its_proposal_applieschecks the claim from inside the apply; it failed on staging's code and passes now.Test Plan
uv run --frozen pytest -q -n 8 tests/server/factory tests/stores tests/examples/test_factory_sample_tokens.py tests/examples/test_factory_bundles.py --ignore=tests/stores/test_databricks_volumes_artifact_store.py -p no:cacheprovider: 5199 passed, 1 skipped after the rebase onto staging with factory: no double approvals, an owner-set autonomy level, stale proposals close themselves #204 and factory: outcome-first initiatives — walking skeleton, acceptance runs on the base env, one progress view #205. The databricks store test needs the optionaldatabrickspackage, which this env does not install.test_discovery_smoke_e2e::test_the_smoke_loopfailed once under heavy machine load (the project card had noplyet) and passed in every run after; this PR does not touch it.test_node_refs.py(213 cases):parse,normalize,keyandcanonical_keytask:T-4,Task:t-4,member:<id>,pr:<repo>#n,pr:<id>with the store,!n,#n,#slug,p:slug,decision:,proposal:,initiative:,need:O-n,session:<id>as a member or a run,task:<slug>/<key>,loop:<id>None#9630, resolve toNoneRefFacts.for_subjectreads only the rows a subject names and gives the same key as fully loaded factstest_node_refs_writes.py(6 cases), against the real routes and store:task:T-4orpr:<id>; another repo'spr:sample-org/other#9630is kept as writtenT-4is stored astask:T-4, is found asTask:t-4and cleared asT-4#slugis stored asproject:<slug>product,fx, a slug andPR-nresolve to their threads;nowhereandT-4stay 400RefFacts.load, and a stored initiative resolves asI-non the feature leveltest_levels.py(35 cases):GET /levelsreturns both trees, and goal is hidden with no goalsPUT /definition(feature → Outcome, project → Workstream) shows in/levelswithversion + 1, and the kinds named after those levels follow with no change of their ownpath_not_allowedtiles_maxout of bounds, unknown key or level, a kind named after its level, an unknown approval)when_anyon need/task andwhen_manyon run follow the stored rows and implicit parentstest_definition_blocks.py(the not-trialable list gainsstructure),golden/definition_doc_defaults.json(the new block),tests/stores/test_factory_store.py(list_prs(number=)on both backends) andtest_answer_authority.py(the claim during apply).pre-commit run --files <changed>: ruff format, ruff check and the repo hooks pass. pyrefly reports the same 28 errors as staging, all in untouched files (runtime/telemetry.py,background/reconcile.py,onboarding/sandboxes/gensee.py).FORBIDDEN_SAMPLE_TOKENSratchet: green. Fixtures usesample-org/sample-app,api-versionand similar.How to check by hand on a dev server:
curl -H "Authorization: …" $SERVER/v1/factory/factories/<fid>/levels. You get both trees;goalhas"hidden": truewhile the definition has no goals.PUT /v1/factory/factories/<fid>/definitionwith{"changes":[{"op":"replace","path":"structure.levels.feature.name","value":"Outcome"},{"op":"replace","path":"structure.levels.feature.plural","value":"Outcomes"}]}. ThenGET …/levelsshows Outcome and the version goes up by 1. Pinning the previous version is the Undo.sys_factory_proposewith astructure.*change returns 403path_not_allowed.sys_factory_askwithsubject: "T-4". The decision'ssubjectreadstask:T-4. Withsubject: "pr:<another org>/<another repo>#<n of a tracked PR>"it stays as written.Demo
N/A: server only, no UI in this PR. Compared with the prototype's
DATA:GET /levelswith the defaults returns the same ladder (trees.product.levelsgoal…run withwhen_any/inline/debug,trees.factory.levelsarea/item), the same responders (product: atlas … run: null … item: sentinel), the same verbs per level and the same column titles (First / Then / After that / Last,Later (held)). It also matches thestructure:YAML the prototype prints undern=fx-levels(tiles_max: 12,fold_single_child: implicit_only).Type of change
Test coverage
Coverage notes
The write-path normalization is covered at two levels:
The full factory suite runs on top of both.
🤖 Generated with Claude Code