Skip to content

factory: decide first, with checkpoints a person can change direction from - #223

Open
LittleChimera wants to merge 5 commits into
stagingfrom
factory/decide-first-server
Open

LittleChimera wants to merge 5 commits into
stagingfrom
factory/decide-first-server

Conversation

@LittleChimera

@LittleChimera LittleChimera commented Nov 4, 2026 •

Copy link
Copy Markdown

Related issue

Closes # (decide first and checkpoints: notes/factory-v3/decide-first.md §1–§4 and §6, server and agent side; the web side is a separate PR)

Summary

Luka, 2026-11-04: "factory should always see if it can decide itself, and record decisions somewhere. i as a user, should be able to see decisions that it made and change direction if needed. each decision point is like a checkpoint" and "right now, there's no reason it shouldn't continue on its own to build the assistant".

ELI5: before, every agent question and most project proposals waited for a person. Now each one first goes to a policy. If the factory may decide it (reversible, within budget, not Tier 0, has a recommended option, not reserved by an owner or the autonomy level), it decides at once, writes a checkpoint and the agent keeps going. A person sees "Decided since you looked · n" on every node, opens the list, and can change direction from any checkpoint: the factory undoes what it can and the agent re-plans.

agent asks / decides / proposes
        │
        ▼
 decide_policy.route ── Tier 0? reserved? no pick? ──► person: D-n waits (as before)
        │ factory
        ▼
 answer as "factory" + checkpoint K-n ──► tool result is the answer (agent goes on)
        │                                 Activity line, decided_since, Slack digest
        ▼
 person: "change direction" from K-n / D-n / C-n
   preview: what is undone, who re-plans, cost
   apply:   person's K-m (overrides K-n), Undo / cancel / charter back / revert task,
            [Factory decision · K-m changed by …] to the deciding agent; Undo 10 min

Spec → what this PR does

§1 Decide first + §2 the policy (server/factory/decide_policy.py, decide_first.py)

  • Every decision point is routed before a person sees it: sys_factory_ask (POST /agent/decisions), the new sys_factory_decide (POST /agent/checkpoints), and every projects proposal that would wait for an operator (proposals.propose).
  • The factory decides when all hold: reversible, within budgets (factory spend today, the project's cap), no Tier-0 class (merge, credentials, spend, prod_flag, infra, policy, delete_data, pii), a recommended option (or the agent's justified choice), not addressed to a person (for=), and no class it falls in is reserved.
  • Classes come from the asker's class and from the words (generic English; the strictest class wins, so a loose declared class cannot dodge a reserved one). A proposal's classes come from its ops only.
  • Defaults by level: careful nothing (today's behaviour), standard scope / tech / UX / ordering, tryout also follow-ups. New work, goal or outcome changes, structural changes and anything the policy cannot classify wait at every level.
  • Owners widen or narrow per class and per subtree: autonomy.decide: [{kind, who, ref?}] (class B like all of autonomy.*: owners only, HUMAN_ORIGIN, agents get 403 path_not_allowed; who: factory is refused for Tier 0 and structural). Stored under its own top-level key (autonomy_decide) so a server from before drops only it and decides nothing.
  • A decided ask is answered as answered_by: factory, which is never a person (answer_authority.NOT_A_PERSON): a proposal carrying it out keeps its own approval rules, except that one create citing a follow-up the server decided on an ask (reply_to="D-n" or its "K-n"), beside the asker's project, is itself a follow-up.
  • FYI without a person: the decision.answered card (the chat's Activity), a muted Activity line for an agent's own decision, "Decided since you looked", the digest. Proposals keep their own FYI and Undo.

§3 Every decision is a checkpoint (factory_checkpoints, K-n; checkpoints.py, store halves, migration fza0e1f2a3b4)

  • One row per factory decision and per change of direction: who decided (agent / server / person), question, options, choice, why, confidence, class, the policy's verdict, node, project, effects, facts (definition version, level), status decided / asked / answered / changed, links (decision_id, proposal_id, overrides, changed_by).
  • Adapter, no history migration: open D-n → asked, person-answered D-n → answered (server-closed alarms left out), pending C-n → asked, applied/undone C-n → decided (autonomy, grace, minor update) or answered (a person applied, asked for or answered it), rejected → answered.

§4 Changing direction (change_direction.py, POST …/checkpoints/{ref}/preview|apply, Undo through POST …/undo/{token})

  • Pick another option or write a direction. Preview: what is undone, what is reverted, who re-plans, cost; preview_id over the facts (409 stale).
  • Apply: the person's checkpoint (overrides the old one, which reads changed); takes back through existing cores: a proposal in its Undo window is undone, past it the projects it created are cancelled (a person's proposal, applied at once) and charters put back; merged code gets a revert task (git revert of the merge, never a force-push); pending proposals that followed it are rejected. The same choice with only more direction re-plans without undoing.
  • Tells the deciding agent (else the project's lead, else the factory's lead): [Factory decision · K-m changed by <person> · "<option>"] with the direction quoted and what was taken back. Undo puts the old checkpoint back, re-files what was undone, cancels revert tasks, and tells the agent.

§6 Existing open asks (decide_first.sweep, in the proposals sweep every minute)

  • An ask that waits keeps what it was routed with (a hidden asked checkpoint). Once the settings change, an ask that only the level or an owner held is routed again with those inputs and decided with its recommended option when allowed; the asker is mailed [Factory decision · D-n answered by the factory · "<option>"]. Asks from before decide-first are routed once from their words. Pending projects proposals the policy now allows are applied. Merges, Tier 0, reversible=false, for= and asks without a recommendation never are. A careful factory with no widening setting skips the sweep entirely.

§5 (server half)

  • Node page decided_since (left out at 0; since the viewer's last look, else the last 7 days).
  • GET …/n/{ref}/decisions?cursor=&asked=1: the subtree's checkpoints newest first, one line each (asks stay in Your move unless asked=1).
  • GET …/checkpoints/{K-n|D-n|C-n}: the details (the peek). Shapes in checkpoint_views.py, validated on every response.
  • Slack digest: "Decided since you looked · n" with a link to /factory/<fid>/n/product?lens=decisions.

Agent side

  • sys_factory_decide for leads and PLs (runner + schema + registration); sys_factory_ask takes class. Safe both ways: on an older server sys_factory_decide answers not_served (ask with your choice recommended); on an older host the protocol says to ask that way; the server decides it at once either way.
  • Bundle text only (no new policy path): the protocol's "Decide first" section (both copies identical), the lead's dispatch and HUMAN lines, the PL's BLOCKED line (tightened to stay under the 15,000-byte launch budget), and both playbooks' rows for K-n changed by.

Deviations

  • Server-opened decisions (alarms, loops, budget raises, access requests) are not routed: they are the server's own alarms and mostly Tier 0 (money, access). Agent asks and projects proposals are.
  • Tier-0 and class detection from words is a conservative safety net on top of the declared class: a false positive only sends a question to a person.
  • Undo of a change of direction re-files undone proposals and cancels revert tasks; a cancelled project stays cancelled and a put-back charter stays (the Undo line says so).
  • No new chat card type (the web contract stays as it is): a decided ask shows as its decision card in Activity, an agent's own decision as a muted system line.
  • No web changes here; the decide-first-web PR reads these routes.

Review fixes (second round)

Blockers:

  • The sweep keeps an ask's routing inputs. An ask that waits for a person stores a hidden asked checkpoint with its declared class, reversible, for=, subject, the words it was routed on, the verdict with holds (tier0, irreversible, to_person, budget, no_option, never, owner, level) and the definition version. The sweep re-routes only rows whose holds are all level/owner, only once per definition version, with the saved inputs. Tier 0 (declared or worded), reversible=false and for= are never decided later. Asks from before decide-first are routed once from their words, and the route is then kept; an ask younger than 2 minutes with no kept route is left to its own request.
  • A deduped twin is routed as it was asked. It uses its own kept inputs, never the new request's body. A twin under the same explicit key with another question is returned open and is never decided. decide_ask refuses an option that is not one of the row's options. A repeated answer is served only for the same key and question.
  • Tier 0 is no longer default-allow. other is in no level's defaults, so an unclassified point goes to a person. Tier-0 words are broad and generic. prod_flag: flag … on, enable … flag/for, n% of traffic, everyone, for customers, to the public, go live, launch, release, publish, rollout. merge: land a PR/change, main/master/default/base/release branch, ship/push … to main, into main, plus the bases of active projects that are not auto-merge targets. credentials: token (not design tokens), key (not primary/cache/sort … keys), signing, certificates, .env, env file, vault. spend: buy, pay, paid, pricing, billing, invoice, cards, plans/tiers, upgrades of a plan, dollars. infra: deploy, environment, CI workflows, buckets, domain names. delete_data: remove/reset/wipe/overwrite … records/data/database/users. pii: addresses, user or full names, user data. Every phrasing from the review is a parametrized test, and the three live shapes still go to the factory at tryout.

Majors:

  • A repeated question or sys_factory_decide is answered only from decided rows. When a person changed direction, it gets the person's checkpoint (their choice or direction), never the stale choice. A repeated decision whose earlier choice differs is told the recorded choice. list_checkpoints(status=…) was added to both stores.
  • Follow-ups: only a checkpoint the server decided on an agent's ask counts, never an agent's own K-n. It counts once: another pending or applied proposal citing it makes the next one wait. The created project must sit beside the asker's project (depends on it, or is on its repo).
  • Owners' subtree settings read the subject or node as well as the caller's project (project:, task:, pr:, D-n, I-n/O-n/G-n), the strictest first, in asks, sys_factory_decide and the sweep.
  • Change direction follows answer authority. A D-n, or a K-n decided on one (or a change of direction from either), assigned to the owners or to one person can be changed only by them. The preview shows can_apply: false with who to ask, apply returns 403 not_assigned, and the peek's can_change matches.
  • Apply loads the checkpoints under the verb lock and marks the old checkpoint changed (compare-and-set) before it writes anything. An adapted item is checked for an existing override. A double apply now gets one 200 and one 409 already_changed, one person checkpoint and one re-plan mail.
  • Restore puts back the before of the proposal being taken back, carried in the step.
  • Code a finished follow-up merged gets its revert task in the checkpoint's own active project; otherwise the re-planner plans it, and the preview lists it. PRs of the checkpoint's project merged after it are listed for the re-planner to check. The C-n an agent recorded as a checkpoint's effects count as followers.
  • At careful (nothing widened), sys_factory_decide returns not_recorded ("decide as before") and opens no ask unless the choice is Tier 0 or irreversible. Elsewhere a held decision opens a non-blocking ask. The bundle text and tool description narrow the tool to choices you would otherwise ask a person about, not routine implementation choices.

Minors:

  • Cost: settings, spend and bases are read once per sweep; an ask is re-routed only after a definition change. Pending proposals are re-validated only when the proposal or the definition version changed, and splits, merges and cancels are skipped before validation.
  • An owner's who: person also holds a proposal the level would apply (a owner_reserved: warning; requested, carried and minor-update ones are untouched).
  • Digest: "Decided since you looked" keeps the digest from being skipped only for a decision in the last 24 h. It leaves out checkpoints touching a hidden project (by project, node, effects or ops).
  • A person's change of direction K-n carries their approval like an answered D-n (reply_to="K-n", same guards: in force, a person with EDIT, given to that agent, routine fields, one proposal).
  • The sweep skips opportunity proposals.
  • None skipped.

Test Plan

  • Second round: tests/server/factory/test_decide_first_guards.py (36) covers each review finding. The sweep never decides a declared Tier-0 or irreversible point; an owner-reserved declared class waits until the owner lifts it; a repeated question is routed as first asked (the merge twin, for=); no option the ask lacks is chosen; every review phrasing is Tier 0, and unclassified points wait at standard; a human-merged base is a merge; a person's change of direction answers a repeated ask or decision; a follow-up is used once and an agent cannot mint one; owner subtree settings read a lead's subject or node; change direction keeps to who may answer; a double apply changes once (stored and adapted); restore uses the right before; a finished follow-up's merged PR gets a revert task; an owner reservation holds a level-applied proposal; a re-plan citing the person's K-n applies; the digest's visibility and 24 h rule. The existing decide-first tests were updated for the hidden asked rows and not_recorded.
  • Second round runs: tests/server/factory (SQLite, -n 8) green; tests/examples, tests/runner/test_factory*, checkpoint store, migration and enum tests: 624 passed. PostgreSQL 16 (throwaway, port 55551): alembic upgrade head, downgrade -1, upgrade head with a single head fza0e1f2a3b4 and no model diff; store and decide-first suites with OMNIGENT_TEST_DB_URI: 91 passed. ruff clean; pyrefly 28 (pre-existing, none in touched files).
  • tests/server/factory/test_decide_first.py (46): the live regression (follow-up, tab placement and edge-path asks with recommended options are decided at tryout, answered as the factory, recorded with class, policy and facts, no answer mail, nothing in Needs you); a merge into main still waits; Tier 0 never auto-decides (declared class or words; owners cannot give it to the factory); careful unchanged; for= and no recommendation wait; a repeat returns the same checkpoint; an open twin is decided when asked again; owners reserve and widen by class and subtree; agents and operators cannot widen the policy; the sweep decides old open asks and mails the asker, leaves merges and open-ended asks; pending upkeep applies once the level allows; sys_factory_decide records or asks; a decided follow-up applies its project; the adapter shows old D-n and C-n; node pages count decided-since and the lens lists them; change direction undoes an applied proposal, records the overriding checkpoint and re-plans; change direction from a decided follow-up cancels its project, quotes the direction, and Undo puts the checkpoint back.
  • tests/server/factory/test_decide_first_digest.py, tests/stores/test_factory_store_checkpoints.py (SQL + memory), tests/db/test_migration_factory_checkpoints.py, tests/runner/test_factory_decide_tool.py, tests/examples/test_factory_decide_first_text.py; updated registration, definition golden and autonomy expectations.
  • PostgreSQL 16 (throwaway, port 55551): alembic upgrade head → downgrade -1 → upgrade head on fza0e1f2a3b4; models match the migrated schema (compare_metadata empty); store, decide-first and digest tests with OMNIGENT_TEST_DB_URI: 53 passed.
  • Factory-scoped suites (SQLite, -n 8): tests/server/factory, the factory runner tests, tests/examples, the factory store and migration tests: 5453 passed, 1 failed (test_decision_routing::test_non_blocking_decisions_and_custom_windows: an ask now reads the definition first and pins the seeded v1, so the test writes its definition before the asks; fixed and re-run with the decide-first, node, routing and bundle suites: 274 passed).
  • ruff check/format clean; pyrefly 28 errors (all pre-existing, none in touched files); FORBIDDEN_SAMPLE_TOKENS green.

How to try it on a factory: set Steer › autonomy level to tryout, have an agent ask a question with a recommended option (or sys_factory_decide); the tool result says status: "answered" with decided, the node shows "Decided since you looked · 1", GET /v1/factory/factories/<fid>/n/product/decisions lists it, and POST …/checkpoints/K-1/preview {"option_id": "2"} then …/apply changes direction and mails the agent.

Demo

N/A (server and agent side; the cockpit's Decisions lens is the web PR).

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

Migration verified on PostgreSQL 16 (upgrade, downgrade -1, upgrade) and the store tests ran there too.

Changelog

The factory decides what its autonomy allows, records every decision as a checkpoint, and lets a person change direction from any of them.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the size/XL Pull request size: XL label Nov 4, 2026


def _error(status: int, code: str, message: str) -> JSONResponse:
return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}})
LittleChimera and others added 5 commits November 4, 2026 17:01
… from

Every decision point goes to a server policy before a person sees it: an
agent's question (sys_factory_ask), a decision it takes itself (the new
sys_factory_decide) and a projects proposal that would wait for an
operator. The factory decides what is reversible, within budget, not Tier 0,
has a recommended option and is not reserved by the owners' per-class,
per-subtree settings (autonomy.decide) or the autonomy level's defaults;
the rest waits for a person as before.

Every decision is a checkpoint (factory_checkpoints, K-n); a person's
answers (D-n) and applied proposals (C-n) read as checkpoints through an
adapter. Node pages count what the factory decided since the viewer's last
look, GET .../n/{ref}/decisions lists them, and a person changes direction
from any checkpoint with preview, apply and Undo: what can be taken back is
(Undo, cancels, charters, revert tasks for merged code), and the deciding
agent is told to re-plan. Open asks from before are decided by the sweep at
the levels that allow it; merges into main and asks without a
recommendation still wait. The Slack digest says "Decided since you looked".
The lead and PL bundles teach decide first (text only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irst line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An ask that waits keeps what it was routed with (a hidden `asked`
  checkpoint: declared class, reversible, for=, subject, words, holds,
  definition version); the sweep re-routes only level- or owner-held asks,
  with those inputs, once per definition version. Tier 0, irreversible and
  for= never auto-decide later.
- A deduped twin is routed with its own inputs, never the new request's;
  another question under the same key is never decided; decide_ask refuses
  an option the ask lacks.
- Tier 0 is broad and generic (prod flags, merges, credentials, spend,
  infra, data deletion, personal data, plus the bases people merge into);
  "other" is in no level's defaults, so an unclassified point asks a person.
- A repeated question gets the factory's decided choice or the person's
  change of direction in force, never a stale choice.
- A decided follow-up counts only when the server decided it on an ask,
  once, for a project beside the asker's.
- Owners' subtree settings read the subject or node a point is about.
- Change direction follows answer authority, applies once under the verb
  lock (409 already_changed), restores the right charter, plans reverts
  for merged code of finished projects and lists PRs merged since.
- sys_factory_decide records nothing while nothing is the factory's and
  opens non-blocking asks otherwise; bundle text narrowed.
- Owner reservations hold level-applied proposals; a person's K-n carries
  approval like a D-n; the digest's decided line is quiet after 24 h and
  respects visibility; the sweep reads settings once and skips
  opportunities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants