Skip to content

Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge) - #2306

Draft
AbirAbbas wants to merge 53 commits into
app/v3from
workstreams/main
Draft

AbirAbbas wants to merge 53 commits into
app/v3from
workstreams/main

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Integration branch for engine changes the desktop app's Workstreams feature needs. Draft: do not merge.

The Workstreams feature itself lives in the desktop app; the engine stays the AI harness. After an architecture
correction on 2026-11-03, every workstream-specific piece that had been built here was removed (#2336). What remains is
generic and useful to any client:

Known reds are the base's own (the same tests fail on app/v3's runs).

🤖 Generated with Claude Code

AbirAbbas and others added 21 commits November 2, 2026 19:29
The Workstreams feature lands on workstreams/main in work-package pull
requests before it goes to app/v3. Listing the line here gives each of
those pull requests the light gate and its touched packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

The Workstreams plan, WP-0: Go types for the 25 workstream, page, record and goahead methods and the workstream.changed event, registered through typed() and delegating to an optional Workstreamer; appBackend answers every one unknown_method "workstreams: not implemented yet" and does not announce the workstreams feature until the store lands (WP-1).

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

* app: workstream pages render from editor JSON to markdown

The Workstreams plan, WP-6c: internal/workstream/pagedoc.Markdown renders a
page's editor JSON by the page document format rules, byte for byte the same
as the desktop app's renderer, so a job that reads a page sees the markdown a
person sees when they export it. Pure: no I/O and no store; invalid JSON is an
error, an absent document is empty, unknown nodes keep their text.

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

* tests: page markdown matches the desktop app's fixtures and the contract example

Every assertion of the desktop app's markdown tests as a Go table row with the
identical expected string, the contract's worked example as testdata, invalid
and absent documents, and edge cases that pin JavaScript's line terminators,
number formatting and coercions.

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

* docs: change entry for the workstream page renderer

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ved) (#2270)

* collections: schema 3 with workstream tables and the factory_job ref kind

The collections database moves to schema 3. The first write rebuilds a
schema 1 or 2 file by copying its tables (SQLite cannot change a CHECK in
place), keeping every collection and membership under the same sequence
numbers and times, and adds the workstream tables: rows keyed by the
collection id, pages and their versions, records, go-aheads, the plan, and
a job claim table whose unique index keeps a Factory job in at most one
workstream. factory_job joins the ref kinds in the SQLite CHECK, the
workspace model and the wire validator, with its factory id on the ref.

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

* workstream: domain package for the workstream store

Stage, numbers, plan, pages with versions, records and go-aheads, written
over the collections store's transactions. Every rule the Workstreams plan
(WP-1) gives the store lives here behind plain Go types, so the wire never
reaches into the tables.

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

* app: serve the workstream methods from the store

Every workstream.*, page.*, record.* and goahead.* method now answers from
the store, behind bounds checks at the wire's one door, and hello announces
workstreams. Create makes the collection, the row, an Overview page and one
bare agent (no first turn), and a repeated client id returns the first.
Every write sends workstream.changed. Stage and numbers come from bounded
Factory queue reads and the agent's recorded spend; anything unknown is left
out rather than sent as zero. Collections mark workstreams, refuse to delete
one, and refuse a job another workstream holds with in_other_workstream.

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

* docs: change entry for the workstream store

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

* app: workstream locks unlock from a defer, factory reads are guarded

The guard laws want every lock released by a defer and every goroutine
guarded; the workstream Factory cache and batch reads now are. Update, mark
done and reopen answer with the same live numbers as get, and workstream text
refuses control characters (prose keeps newline and tab).

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

* workstream: page.get renders markdown through pagedoc

The engine keeps one page renderer: page.get's markdown now comes from
pagedoc.Markdown, which matches the desktop app's renderer byte for byte, and
the store's own renderer is gone.

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

* app: a panicking Factory read still finishes its flight; writes keep the first start

A queue read that panics inside its guard now closes its flight and caches the
failure, so waiters are released and a later read tries again. Update, mark
done and reopen persist the first observed start like get does and announce
one workstream.changed for the whole change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e) (#2288)

* tests: the wire contract's workstream examples as engine testdata

The examples, field tables, enums and error codes the Workstreams wire
contract carries, one file per method and event, with the extractor that
regenerates them when the contract changes.

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

* tests: workstream answers through the wire keep the contract's shapes

Each method's example params go through the real wire handler against a
seeded store; each answer must have its example's keys at every level, the
field tables' JSON kinds and enum words, no nulls and integral integers.
Every engine-sent error code has a call that checks its code and detail keys.
A method, event or error code the contract gains without a case fails.

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

* docs: change entry for the workstream wire conformance test

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

* docs: the conformance change entry names its pull request

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

* tests: conformance replies carry every illustrated array element

A reply array shorter than its example no longer skips the elements the
example illustrates (a plan's draft item, an unattributed page version);
page.delete must return exactly the seeded parent and descendant. Request
writes to the wire are bounded like replies.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the integration branch up to app/v3 (28 commits: Factory spend and
budget rows, one engine per home, memory off by default, the app delta
window, process-group reads on macOS, and their tests). No conflicts; the
three files both sides touched (cmd/codeaf/app.go, internal/appwire/server.go,
docs/design/app-v3/PROTOCOL.md) merged line-disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons read by jobs (#2290)

* workstream: deleted pages are kept and restorable; pages read by version

Schema 3 (unreleased) gains deleted_at and the delete that removed each page.
Delete marks a subtree; restore brings back what that delete removed, placed
last under a live parent or at the root. A page reads any saved version.
Read-by-jobs defaults on for pages titled Spec or Decisions at creation.

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

* app: page.get by version, page.list include_deleted, page.restore of a deleted page

The wire handlers serve the wire contract's v0.1 page additions and apply the
Spec/Decisions read-by-jobs default when page.create leaves it unset.

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

* tests: conformance covers the v0.1 page params and example pairs

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

* docs: change entry for workstream page versions and restorable delete

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ges) (#2289)

* workstream: agent pages stay the agent's, lookup by agent, markdown import

The store now refuses, inside the save transaction, an agent write to a page
the person owns, and an agent create of a page it would not own, so a page made
the person's between a read and a save is still refused. ByAgent finds the
workstream whose agent is a conversation. pagedoc.FromMarkdown turns markdown
into the editor JSON the page renderer reads, live-block markers included, and
the contract's example round-trips byte for byte.

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

* session: a workstream agent's prompt section and six tools behind a door

A conversation handed a WorkstreamDoor carries workstream_read,
propose_decision, propose_question, propose_jobs, propose_goahead and
write_agent_page, and a short prompt section naming its workstream and the
rules: it proposes and the person keeps, it writes only its own pages, it
never invents numbers and it starts nothing. Arguments outside each schema
are refused before the door is touched, propose_jobs only appends drafts and
retries a version conflict once, and a go-ahead proposal is a proposed
decision that grants nothing. Without a door the belt and the page are
unchanged, and task nodes, errands and clarifications never carry it.

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

* app: give a workstream's agent its door, and re-read spend after its turns

The app sets the workstream door on the conversation whose created-by is
workstream and on no other; the door finds its workstream at each call, since
the agent is opened a moment before the workstream row exists. Its writes are
attributed to the agent and send workstream.changed, and a workstream agent's
settled turn sends one too so its spend is read again.

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

* manual: what a workstream's agent can do

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

* docs: change entry for the workstream agent's tools

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

* app: gofmt the workstream agent door test

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

* workstream: agent writes check done in their transaction; markdown import is linear

An agent's record or page write now refuses a done workstream inside the
same write transaction, so marking done between the agent's read and its
write leaves nothing behind. Imported list items always begin with a
paragraph, and unmatched backticks or brackets are consumed in one pass
instead of rescanning the rest of the line for each one.

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

* session: workstream tools follow the argument and retry laws

The prompt's workstream brief is a snapshot that never touches the store, so
a re-render under the session lock cannot wait on it. Arguments decode only
through decodeToolArguments, a repeated key is refused at any depth, every
argument refusal is marked, and the plan's one retry is written without a
counting loop. A go-ahead's proposed cap is dollars, excused from the
whole-number law in its own file.

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

* app: keep each workstream agent's brief current without reading the store

The app remembers each workstream's name and why by its agent when it creates,
reads, updates, marks done or reopens one, and when the agent's door resolves
it, so the prompt reads that snapshot. A done refusal from the store reaches
the agent as the workstream being done.

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

* docs: workstream agent tools refuse repeated keys; done is checked in the write

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

* session: import the workstream types in the validation test

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

* workstream: only an explicitly attributed agent proposal is refused when done

The done check in ProposeRecord read the defaulted actor, so the wire's
unattributed record.propose on a done workstream, which has always landed as
a proposal, was refused. It now reads the attribution the caller gave, and the
agent's door keeps passing its own.

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

* session: the workstream agent keeps its "Where it stands" page current

The app shows the agent's summary on the Overview from the agent page titled
exactly "Where it stands". The prompt and write_agent_page now name that page
and ask the agent to keep it current with what it was updated from.

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

* session: a shorter Where it stands rule, and room for it in the prompt budget

The rule now fits in one short line, and the workstream section's own budget
test allows 1000 bytes for the worst-case name and why.

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

* tests: the conformance seed lends the agent the Overview for its one save

The store now refuses an agent write to a page the person owns, so the seed
that reproduces the contract's agent-saved Overview hands the page to the agent
for that save and back again, keeping the agent's attribution and version.

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

* tests: page version and restore fixtures write as the agent only on its pages

The store now refuses an agent write to a page the person owns, so the
version-read and deleted-tree fixtures that save or create as the agent make
those pages the agent's first; what they assert is unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eaf leg 25m) (#2296)

* tests: the conformance busy case waits 200 ms for the lock, not 10 s

The busy error case held a real writer on a second handle and let the
backend's store wait out the full 10-second SQLite busy timeout. That one
subtest was 10.0 of the workstream tests' 11.2 seconds in cmd/codeaf.

It now sets PRAGMA busy_timeout to 200 ms on the backend's own single
connection, reads it back and fails unless it took, then holds the same
real writer. The refusal still travels SQLITE_BUSY -> ErrBusy -> wire
busy through the real store and dispatch. Production keeps its 10 s.

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

* ci: the codeaf touched leg gets a 25-minute package timeout

cmd/codeaf runs unsharded (sharding it failed in #1734). Its serial suite
measured 1065 s (17.8 min) on the 4-vCPU runner on 2026-11-03, 1053 s of
it in tests that predate the workstream work, so the leg hit the
Makefile's 15-minute timeout and every test after the cut went unchecked.
The base branch's own runs time out the same way.

Only the codeaf leg passes --timeout 25m; tui3, session and rest keep the
Makefile's 15m. A timeout is still red. docs/rules/ci.md records the
exception and that it goes once the suite is back under 15 minutes.

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

* docs: change entry for the codeaf leg's timeout

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ow agent ids, goahead proposals that grant when kept (#2297)

* workstream: draft size/budget/source, and go-ahead proposals that grant when kept

Draft plan items keep an optional size, budget and source. A goahead record
carries its proposed scope, cap and end; keeping it grants exactly one go-ahead
in the same write, dropping a kept one revokes it. Schema 3 gains the record
columns in place.

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

* app: workstreams wire v0.2 (row agent ids, page sources, goahead records) and conformance

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

* session: the workstream agent proposes go-aheads as goahead records and sources its draft jobs

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

* changes: number the workstreams wire v0.2 entry

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… chat resume skips the per-chat database scan (#2303)

* workspace: SQLite refuses deleting a workstream's collection, for any writer

An engine that opened collections.db at schema 2 before another engine
migrated it keeps its cached version and its old Delete, which has no
workstream check: its DELETE FROM collections would cascade away the
workstream, its pages, saved versions, records, go-aheads and plan. A
BEFORE DELETE trigger now refuses that delete in the database itself.
New and migrated files get it with the schema; schema-3 files from earlier
integration builds gain it (IF NOT EXISTS) inside their next write. Plain
collections delete and cascade as before; Store.Delete keeps its message.

The deleted-pages purge test deletes the workstream row before its
collection, since the collection alone can no longer be deleted.

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

* chat: resume asks collections.db about workstream agents only for empty chats

v3EmptyUnassignedSession opened collections.db before its cheap local
checks, so resuming in a project scanned the database once per saved
conversation, including every conversation somebody spoke in. The
ownership question now comes last, for folders the local checks already
found empty, and still keeps a folder whose owner cannot be read.

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

* changes: entry for the workstreams stability fixes

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…sions pages read by jobs (#2310)

* workstreams: size limits on plans, pages and records (wire v0.2.1)

Draft text was bounded only by its own length, plans had no count or
encoded-size bound, and a workstream could accumulate pages and records
without limit, so every list read materialized all of them. The store now
refuses, inside its write transaction and before anything is written:
draft text over 80,000 bytes; a plan over 500 milestones, 500 waves,
5,000 items or 4 MiB of encoded milestones and waves; a 5,001st page
(deleted pages included) or record. The new ErrLimit names the bound and
answers too_large on the wire; the plan_set check refuses the counts and
draft text early. The conformance testdata is regenerated from the
contract (too_large on plan_set, record.propose, record.supersede).

The agent manual states the limits and that new agent pages titled Spec
or Decisions are read by jobs.

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

* workstreams: agent pages titled Spec or Decisions are read by jobs, as page.create makes them

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

* workstreams: control characters stay bad_params past the draft text bound; the manual separates the tool's draft limit

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

* workstreams: tests for the size limits and the agent's Read-by-jobs default

TestStabEm1AgentSpecAndDecisionsPagesAreReadByJobs: agent pages titled
Spec or Decisions (any case, surrounding space) are read by jobs, others
not; updates keep the flag. TestStabEM4*: plan bounds through the real
wire and the store, exact bounds accepted and one past refused with
too_large and no event; page and record counts (deleted pages included)
refuse wire and agent writes; plan_set and record.propose too_large
conformance cases; early wire refusals keep bad_params for control
characters.

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

* docs: change entry for the workstream size limits

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the integration branch up to app/v3 (29 commits: job step labels and
conditions, step cost as the sum of its rounds, spending kept across reruns,
closed issues leave Backlog, one way to build a crew watch, run workers act as
their task, one model per role, live lines per call, and their tests). No
conflicts; the three files both sides touched (cmd/codeaf/app.go,
internal/appwire/server.go, internal/session/session.go) merged line-disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since app/v3's "A job keeps its earlier spending when it runs again", a
job's Cost is only its current run and LifetimeCost holds every run. The
workstream's spend summed Cost, so it fell when a member job ran again.
It now takes the larger of the two, which is LifetimeCost from a current
factory and Cost from an older one that does not send it.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e small changes, wake.* methods) (#2319)

* workstreams: schema 4 adds wake tables by an additive migration

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

* workstreams: wake records, settle under autonomy, apply/undo/not now

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

* app: wire wake.list, wake.apply, wake.undo, wake.not_now and the one-shot wake turn

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

* app: the workstream agent wakes each morning and on member job events; wake costs count toward spend

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

* docs: the watching agent's change entry names its pull request

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

* workstreams: undo refuses drafts and pages changed since; morning wakes only for today's slot; wake calls carry their purpose

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

* docs: the protocol document lists the wake methods

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

* workstreams: wake undo keeps a later rename and tolerates sibling reorder; replay knows the wake's role

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

* tests: wake.list fixture follows the contract's money form

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

* workstreams: the wake call has a ledger owner; the wake queue unlocks from a defer

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ma 5 (#2320)

* workstreams: schema 4 adds workstream_sources by an additive migration

Schema 3 stays readable without writing; the first write creates the table and
moves to 4 in one transaction, keeping every existing row. Schemas 1 and 2
migrate straight to 4.

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

* workstreams: sources store (file, repo, issue, link, kept) with strict path and URL checks

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

* app: workstreams wire v0.3: source.list/add/update/remove, create takes sources

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

* app: the workstream agent reads sources and proposes new ones for the person to keep

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

* docs: change entry for workstream sources names its pull request

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

* docs: the app protocol lists the source methods

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…, suggest_page_edit, schema 6) (#2318)

* workstreams: page text blocks, edit anchoring and keeping by code point

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

* workstreams: suggestions store: list, keep one or all as one version, drop

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

* app: workstreams wire v0.3: suggestion.list, suggestion.keep, suggestion.drop

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

* app: the workstream agent suggests edits on the person's pages (suggest_page_edit)

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

* docs: change entry for workstream suggestions

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

* docs: number the workstream suggestions change entry

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

* workstreams: keeping an edit leaves other text nodes' fields intact; empty marks equal no marks

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

* docs: the protocol lists the suggestion methods

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

* workstreams: schema 5 adds the suggestion tables after the watching agent's schema 4

Files at 1-4 stay readable as they are (suggestion reads answer empty); the
first write applies every missing step in order inside its transaction.

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

* docs: the suggestions change entry names schema 5 and wire v0.4

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

* workstreams: schema 6 adds the suggestion tables after Sources' schema 5

Files at 1-5 stay readable as they are (suggestion reads answer empty); the
first write applies every missing step in order inside its transaction.

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

* app: the Suggestions addition counts 36 methods and eight agent tools; change entry names schema 6

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

* app: the suggestion prompt rule fits the workstream section's 1000-byte budget beside the Sources rule

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the integration branch up to app/v3 (6 commits: the stored
repository list served with a background refresh, step conditions read
the change's files, step and My pipeline AI count as Factory AI, notes and
the completion guard share one command outcome, opening a job doesn't
count as a person's touch, a budget raise isn't refused by spend
progress). No conflicts; the three files both sides touched
(cmd/codeaf/app.go, internal/appwire/server.go,
docs/design/app-v3/PROTOCOL.md) merged line-disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eld conversations are kept (#2334)

* A client can attach tools to a conversation over the app wire

agents.tools_set attaches, replaces or removes one named set of tools on a
conversation; agents.tool_call carries each model call to the client and
agents.tool_result answers it. The hello says agents.client_tools.

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

* Client tools ride at the tail of a conversation's belt

A conversation with a client tool door offers the client's tools after every
built-in, with each set's instructions on its own page and nothing added
when no set is attached. Tools follow their declared category, a built-in
name wins, and child tasks and discussions get neither tools nor
instructions.

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

* A conversation any collection holds is not blank-chat litter

Launch cleanup, the blank-chat sweep and the background places sweep keep a
never-spoken conversation that a collection references, reading the
collections database read-only and keeping the folder when it cannot be
read. Opening such a conversation by id after a restart restores its owned,
model, archive and spend facts from its metadata.

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

* codeaf app sends client tool calls to the connected app

The app backend keeps each conversation's client tool sets for as long as
the connection lasts, emits one agents.tool_call per model call and waits
for the answer, its deadline, the turn's end or the connection closing.
Call ids are stable for a replayed call, and a late or repeated answer is
ignored.

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

* Change entry for client tools and collection-held conversations

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

* The client tools manual page is found by its own name

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

* Client tool registry follows the guard and name laws

The registry's sender runs under guard.Go, every registry lock is released
from a defer, and a test name no longer spells the old product name.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… (schema 7) (#2336)

* collections: schema 7 keeps every collection and Factory job ref, drops development tables

The collections store now writes schema 7: collections and memberships in the
generic shape (factory_job refs with their factory, five-column UNIQUE), and
nothing else. Versions 1-7 are read as they stand; the first write migrates
inside its own transaction. Schemas 1 and 2 are copied into the generic
tables; schemas 3-6, written only by development builds, lose their extra
tables, indexes and triggers while every collection, membership and ref
stays. A failed first write leaves the file as it was; 8+ is refused.

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

* app: the engine keeps only the generic pieces of the Workstreams plan

Workstreams move into the app, so the engine drops everything that existed
only for them: the workstream store and page renderer, the wire methods and
hello features, the agent's built-in tools and prompt section, the morning
wake scheduler and its replay purpose, the conformance fixtures, and the old
launch-cleanup hook. Removing the agent door also removes the copy of it that
Fresh and Open carried.

What stays is generic: collections hold Factory jobs (the resolver moves to
appcollections_factory.go under generic names, with its own tests), client
tools, and collection-held conversations that survive cleanup.

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

* docs: protocol and manual keep Factory refs and client tools; workstream entries go

The protocol and the collections manual lose the workstream methods, events,
features and schemas, and keep the factory_job and client-tools sections. The
unreleased workstream entries described features that will not ship from the
engine; one entry for the generic pieces replaces them and the client-tools
entry. The CI-timeout entry no longer mentions the deleted conformance case.

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

* docs: one change entry for the generic collections, client tools and kept conversations

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

* docs: the generic-pieces entry is written fresh, so the gate sees it as added

The first version reused the client-tools entry's wording, so git paired the
two files as a rename and the change-entry gate found no added entry.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@AbirAbbas AbirAbbas changed the title Workstreams (integration branch; draft, do not merge) Generic collections (factory_job refs) and client tools (integration branch; draft, do not merge) Nov 3, 2026
AbirAbbas and others added 8 commits November 3, 2026 21:30
…hown as live (#2337)

* A blank conversation a collection holds is kept but never resumed

Launch resumed a never-spoken conversation that a collection references, ahead
of the last spoken one, because "kept" and "spoken" shared one predicate. The
emptiness check now reads only content; references keep a blank out of both
the resume and the reuse piles, and every removal path still refuses it.

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

* Collections show a held blank conversation as live after a restart

The resolver built its view from agents.list, which leaves named blanks out,
so collections.open and collections.expand called them deleted. A missing id
is now looked up once on disk before it is called deleted.

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

* Tests: held blanks are neither resumed nor called deleted

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

* Change entry for #2337

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

* A permanently deleted conversation stays deleted in its collection

The on-disk fallback found the metadata a permanent deletion keeps, so the
resolver would have shown a deleted conversation as live and allowed a relink
to it. A row pending deletion, or a folder with the deletion marker, now stays
deleted.

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

* Only a conversation member uses the saved-folder fallback

A blank found on disk shared the listing cache that task and process members
use to find their owner, so a collection read could open a crew member's
conversation to look for a child that does not exist. The fallback now serves
conversation members only, from its own cache.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings the integration branch up to app/v3 (4 commits: pairing QA
fixes for failures, approvals, removed computers and the devices loop; a
person's pause survives a lost heartbeat reply; pairing follow-ups where
app and pair approve settle a lost reply and chat rows say a take is
running; the plugins desktop protocol over the connect layer).

One conflict, in cmd/codeaf/app.go: the appBackend struct keeps the
client-tools field beside app/v3's plugin pause, Ollama and auth fields.
internal/appwire/server.go (both optional hello features),
internal/session/connect.go and docs/design/app-v3/PROTOCOL.md merged
line-disjoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Client tools: a tool call names the message its turn answers (source_message_id)

agents.tool_call gains an optional source_message_id: the durable id of
the message the running turn answers, the same string as Entry.message_id
on that message's chat entry. It is captured when the turn starts and
carried on the turn's context, never sampled from the app's current turn,
so it survives an engine restart and a replayed call. A stopped turn asked
again carries its recorded message; a carry-on after a quit or close carries
the message that opened the interrupted run only when the record names it
exactly. Turns the engine wakes on its own notes carry none. Crew
provenance is unchanged. The hello feature agents.client_tools.source
comes with agents.client_tools; turn stays the process-local counter.

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

* Client tools: tests for the source message on tool calls

A call names its own message (and a later message its own), the same id
after reopening the journal, on a resumed stopped turn, on an exact
carry-on and on a fresh-registry redispatch; note-woken turns carry none;
a steer never moves it; the field is omitted when empty and the feature
rides with client tools. Every wait in the new fixtures is bounded.

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

* Change entry for the client tool source message

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

* Client tools: a carry-on after a quit or close names no message

Naming the interrupted run's opening message needed a run id on the
person's line, which changes what a person's line records. A carry-on
turn is one the engine starts itself, so it now carries no
source_message_id, like every other turn the engine starts.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings app/v3 up to 1f1a733, including agent spend being complete
(#2347): agents.open and agent.changed carry the saved figure, and late
recovered charges save it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sses (#2360)

* Client tools name their answer cutoff a wait, not a leaf's room

The pending client-tool call held the instant the engine stops waiting for
the client's answer in a field named `deadline`, sized from milliseconds.
The leaf-room law in internal/exec reads any such write as a hand-sized
leaf room and failed on it. It is not one: the limit comes from the
client's registered tool set (or the call's own in-range DeadlineMS), not
from the subharness table, and routing it through SubharnessFor would
change it.

Rename the field to `answerBy` and the local limit to `waitMS`. The
effective limit, the DeadlineMS sent to the client, the wait timer and the
expiry checks are unchanged.

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

* docs: name pull request 2360 in its change entry

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings app/v3 up to db138db (130 commits), including Factory budget
work (#2353, #2388), desktop Library access to shared knowledge (#2369)
and test-home helpers.

Two conflicts, both kept on both sides:
- cmd/codeaf/app.go appBackend: clientTools beside app/v3's
  libraryWatch and libraryInboxMu.
- internal/session/sweep.go: app/v3's moving-folder sweep (#2350)
  beside the caller's keep rule; sweepSession still takes keep.
  #2350's new launch test calls SweepHomeContext with the keep
  argument the branch added (nil).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2352's test calls SweepPlacesContext with app/v3's four arguments; the
branch's signature also takes the keep rule, so the session test binary
did not compile after the merge. The test keeps nothing (nil).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* turn.tool carries the conversation's live spend so far

A running turn said nothing about money until it ended: Agent.spent_usd is
the figure saved at turn end, spend.changed is the machine's day, and the
live meter was only answered on request by agents.budget.get. Every turn.tool
event now carries an optional spent_usd, read from the same live meter after
the emitter has released its locks, so an app can show what a run has cost
while it is still running. Zero or unknown readings and history rows omit it.

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

* Change entry for the live spend on turn.tool (#2413)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas and others added 24 commits November 5, 2026 12:24
…less than it charged (#2414)

* A resumed conversation restores its late receipts, so it never saves less than it charged

A late provider receipt (a cut stream priced after the fact) is banked
outside every turn seal and leaves only its call line with an arm mark.
Replay summed usage seals alone, so after a restart the meter forgot every
late charge, and the next turn could stamp meta.json below the total a late
receipt had already saved (.25 + .125 saved .375; resumed, a .01 turn saved
.26). Replay now restores a call line that carries an arm (reconciled or
hedge) as one call and no turn, exactly as Agent.reconciled banked it, and
Digest.Spend counts the same lines so a meta row rebuilt from the
transcript agrees. Ordinary call lines stay evidence and are never billed;
Digest.Usage still lists only usage lines.

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

* Change entry for restoring late receipts on resume (#2414)

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

* A late receipt that arrives after Close keeps its line, so resume still restores it

Close lets pending receipt accounting finish after quit, and the closed
journal already admitted usage seals for that reason, but it refused the
arm call line a late receipt writes. The charge then reached meta.json and
vanished on resume again. The closed-file exception now admits a paid arm
call line too; ordinary call lines stay refused after close.

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

* Manual: say late receipts are restored with the smallest wording change

The broader rewrite moved the manual's question ranking (TestTheChatManual-
AnswersTheQuestionsPeopleAsk); keep the original sentences and add the
late-receipt exception beside them.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#2416)

* factory: keep job outcomes in a bounded record and answer them from a cursor

A job that merged, finished, failed or needs a person is recorded at the store
write, beside the live notifications and under the same lock, in its own
job-outcomes.json: a sequence of its own, the newest 2,000 kept, and a floor
below which a reader resets instead of skipping. The live record, its ids and
/v1/events are unchanged. An older binary writing the same folder drops the
outcomes_through marker, which the next commit turns into a gap.

GET /v1/job-events answers the outcomes after a cursor for the jobs the caller
may see, with an optional long poll of up to 15 seconds.

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

* app: factory.events_since reads job outcomes and factory.event pushes them

factory.events_since {factory, after?, limit?, follow?} answers the Factory's
job outcomes after a cursor, or a reset when the cursor is absent, older than
what the Factory keeps, or crosses a gap. With follow, one follower per
factory long-polls the Factory and pushes each later outcome as factory.event.
A Factory older than the route answers supported false. Hello feature
factory.job_events, capability job_events.

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

* changelog: name the pull request

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

* factory: compare outcomes against a deep pre-update copy; make the detach test deterministic

The store's update snapshot shares the stream, merging and follow-up with the
item the callback edits, so an in-place change could hide a needs-you change
from the outcome comparison. Outcomes now compare against their own deep copy
taken before the callback; live.Changes keeps its snapshot as it was.

The follower test now drives its retry wait and observes the outstanding long
poll instead of racing a real backoff; a follower whose connection is gone ends
at once.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…2417)

* Remote door: client tools cross to the connection that set them

A holder that forwards client tools says so in its welcome. A connection
sends its whole snapshot of sets with ClientTools.Set; the holder's
conversation hub puts them on the belt, sends each call to the owning
connection only and takes the answer back with ClientTool.Answer. A
connection that leaves (detach, torn pipe, close, failed write) loses its
sets and its pending calls settle at once with the disconnected answer; a
call nobody answers ends at its existing wait. A roaming client re-sends
its sets after a redial. The protocol version is unchanged: an older peer
never sees the new calls or frames.

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

* App: client tools reach conversations held by another codeaf or computer

agents.tools_set on a conversation the app joined through another host now
forwards the agent's sets to that holder when it supports it, and the
app pushes them again every time it holds the conversation. Calls come
back as agents.tool_call on the app connection that set them. An older
holder keeps the existing bad_scope refusal. A chat held on another
computer stores its sets and uses them once it runs here again. Hosts
without an app give each conversation the forwarding hub, and the app
hello adds agents.client_tools.remote.

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

* docs: change entry for remote client tools (#2417)

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

* Client tools across the remote door: review fixes

One sender per owner connection drains its pending calls and skips any
call already settled or past its wait, so a stuck write holds one sender
and no queue. Requests go through the client's ordinary call path. Each
client carries a random owner token: a redial that arrives while the old
connection is still attached moves the sets to the new connection instead
of colliding with itself. A snapshot replays only onto the conversation it
was acknowledged in. A forwarded call waits for an in-flight publication
only as long as its own context and wait allow. Every new lock unlocks
from a defer, and the chat manual keeps its one bounded section.

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

* Remote client tools test: gofmt the instructions-limit case

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* factory: a job can wait on other jobs' merges

Jobs carry the jobs they wait on, the ones not yet satisfied, whether their
waits were released, and why a parent blocks them. A parent satisfies its
children when its pull request merges or it ships with nothing to merge; one
that fails or ends unmerged blocks them until it is fixed. The job read shows
waits_on, waiting_on and blocked; Pick up issues never starts a job that waits.

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

* factory serve: set a job's waits and release them when parents merge

PUT /v1/jobs/{id}/waits_on sets or clears a Backlog job's waits, refusing
unknown jobs, the job itself, cycles and parents that ended unmerged. A
scheduler in the service rechecks waiting jobs at start, after folder changes
(at most every two seconds) and every minute, and releases a job once every
parent is satisfied. It never starts a job; a person's Start ends its waits.
/v1/about lists the waits_on feature.

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

* app wire: factory.waits_set and factory.features

factory.waits_set sets the jobs one job waits on, outside the job-action table
because its list names parents, not jobs to act on. factory.features reads what
a connected factory serves from its about answer. The engine's factory
capabilities say waits_on. An older factory answers factory_too_old.

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

* test: waits ordering, blocks, refusals, restart and the wire

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

* changes: factory job waits entry

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

* factory: waits hold against closed issues, intake races, hidden parents and resets

A closed issue ends its job unmerged; only a published no-change receipt
satisfies children without a merge. Intake re-checks pending waits under its
own admission lock. Parents the caller cannot read are refused as unknown,
and a parent in another repository is named only by its job number. A start
through the service retires pending waits at once, so a Drop and Bring back
before the next scan cannot revive them.

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

* test: closed issues, intake race, hidden parents, durable starts and the scan loop

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

* test: waits read test calls FromSnapshot with its caller; the queue check reads the plain queue

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

* test: the waits current read fixture names the job the way the factory does

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…y key; factory.job_context_set (#2415)

* Factory keeps a new job's group, milestone, context and starting budget

POST /v1/jobs and /v1/issues take optional group and milestone labels, context
attachments outside the description limit, and a budget from the standard
steps. They are checked before any issue or job is made, stored on the job,
and the job read carries the labels and each attachment's title and source.

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

* The plan step reads the context attached to its job

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

* factory.job_create takes open_issue, group, milestone, budget, context and key

Each is announced in factory.capabilities. The key travels as the create
request's Idempotency-Key, so a retried create makes one job. An older
factory that refuses a new field answers conflict with reason
factory_too_old and nothing is made.

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

* Change entry for factory.job_create extras

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

* Context attachments are their own write: factory.job_context_set

factory.job_* params carry no lists, so context leaves factory.job_create.
factory.job_context_set {factory, job, expect, context} replaces a job's
attachments while it has no run (POST /v1/jobs/{id}/context, hello feature
factory.job_context, capability job_context). The plan step that will
actually run reads them; a disabled, excluded or skipped plan no longer hides
them from the first conversation step that runs.

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

* Change entry names factory.job_context_set

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

* Count only the Factory writes in the new create and context tests

attachStub's own probe is a GET; the tests now count the one write each call
sends. The change entry's title fits on one line.

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

* A plan skipped after earlier steps hands its context to the next conversation

When the plan step is skipped without opening a conversation, the first
conversation after it reads the attachments, once. The chat manual describes
making a job with an issue, labels, a budget and a retry key.

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

* A job outcome carries the group the job was made with

factory.event and factory.events_since already had a group field; it is now
filled from the job's group.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… GitHub connection (#2419)

* Factory lists a repository's issues through its GitHub connection

GET /v1/repos/{owner}/{repo}/issues?since=&page= reads GitHub's issue list
in every state, oldest update first, as the caller the Factory already uses
for issue search, so private repositories work. Pull requests are left out of
issues but counted in rows and newest, so a caller's cursor moves past them.
Bodies are cut at 8,192 bytes on a rune boundary.

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

* factory.repo_issues reads a repository's issues through the Factory

Hello feature factory.repo_issues and capability repo_issues. A repository
the caller cannot see is not_found; GitHub's spent rate limit is busy with
detail.retry_at; a Factory without the route is conflict with reason
factory_too_old.

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

* Change entry for factory.repo_issues

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

* factory.repo_issues uses the shared factory_too_old reason

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…old runs past it (#2423)

* factory: group policies with allowances and a stop

A job may carry an opaque group. A group's policy, kept in the factory's
folder, lists allowances (jobs, a dollar cap, an end) and a stopped flag; its
spend counts every run of the listed jobs. Pick up issues leaves grouped jobs
to their policy.

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

* factory: start released jobs inside an allowance and hold runs past it

The waits scheduler starts a released grouped job on its own only when an
active allowance lists it and has room for its budget; otherwise it stays
startable with the reason. Beside the job budget stop, the lease desk holds a
run the policy started once its allowance ends, is spent or no longer lists it,
and holds every run of a stopped group. A person's start or Continue makes the
run theirs. Stopping a group stops its running jobs after their current step.

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

* app wire: factory.policy_get, factory.policy_set and a job's group

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

* test: allowances, headroom, ends, stops, person runs, spend and the wire

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

* factory: policy gate reads only listed jobs, never cuts an admitted step, compares money in micro-dollars

The gate checks a stopped group first, then reads only its allowance's listed
jobs through a read-only store read, cached for two seconds, never listing the
store under an item lock. A stopped group arms Stop after this step on a step
the service already admitted and holds only a new admission, decided from the
service's own admission record rather than worker clocks. Every policy money
decision compares whole micro-dollars.

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

* test: no lock under listing, bounded reads, stop interleavings, skewed clocks, money edges, intake

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

* changes: factory group policy entry

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

* test: the person-doors policy test builds its handler with an answer door

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

* test: policy app tests read a job as the factory names it; reads are not owner-guarded

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

* factory: a job's group is the one job creation already keeps

The job-creation change made group a field of the job, the create request and
the job read. This step uses it and drops its own copy, its create check and the
tests that duplicated job creation's own.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…om (#2429)

* A job made from a split records the job it was split from

factory.job_create takes split_from, a job of the same Factory the caller can
see (capability create_split_from). The Factory checks it before any issue or
job is made, stores it on the new job, notes it in the job's activity and
returns it on every job read. The parent job is unchanged.

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

* Change entry for split_from

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

* split_from tests count only the Factory writes and read a job as the API spells it

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

* split_from older-Factory test compares the refusal's message

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings app/v3 9a87ed6 (30 commits) into the integration branch. No
textual conflicts. docs/rules/ci.md keeps both sides' branch lists: PRs
into dev, app/v3, santos/dev2 and workstreams/main, pushes to dev,
santos/dev2 and workstreams/main.

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

app/v3 now fills a job's missing author access when Get or List reads it,
which can wait on a GitHub lookup and write the job. Bookkeeping reads use
the saved snapshot instead, as the lease desk and record already do:
policy allowance reports and group stops, waits graph checks and release
scans, the split_from parent check, and the outcome cursor's per-job
visibility. A transient lookup failure no longer shows up in a waits
check, and reading these never changes a job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every request first settles due jobs with a hydrating listing, before
routing, so a whole-request count of author lookups also counts that.
The policy, outcome-cursor and split_from tests now call their handlers
directly and still fail if those reads go back to Get or List.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Factory: GitHub's spent rate limit on a visibility check answers retry, not "repository not found"

The repository visibility check read only GitHub's status, so a spent rate
limit (403 with no remaining quota or a Retry-After, or 429) became a denial
cached for ten minutes and every repository route answered 404. The check now
keeps the headers, never caches a spent limit, and the repository routes answer
429 with retry_at in the issues read's existing shape. A source poll that fails
on a spent limit says "rate limited".

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

* Factory: a keyed request is never answered from a cached 5xx or 429

The idempotency cache replayed the first answer under a key for 24 hours,
including server errors and rate limits, so a retry after a transient failure
could never succeed. Those answers are no longer kept or replayed, including
rows an older build stored; successes and other refusals still replay.

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

* changes: entry for the Factory's retryable transient failures

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

* changes: the spent-limit condition applies to both 403 and 429

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

* Factory: keyed replay asks api.WorkerResendable, as the worker rule does

The taxonomy law keeps HTTP status comparisons out of ordinary packages; the
Factory's existing helper already names the try-again answers (5xx, 408, 429).

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

* changes: 408 is a try-again answer too

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* session: an abandoned turn's paid steps are sealed, so a restart keeps them

A turn let go of at the second stage wrote only its abandoned line, which is
evidence and never billed, so the conversation steps it had paid for were on
the live meter but in no seal. After a restart the replayed meter missed them
and the next stamp saved a lower total. The displaced turn now writes its own
step tally as a journal-only seal when it leaves its tool batch (no duration,
no turn-ending acts). Errands keep their own lines; the abandoned line stays
evidence.

Two ordinary exits that returned without any seal (a settle turn at its bound,
and a final stage report) now seal through sealTurn like every other ending.

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

* session: tests for abandoned and unsealed turn endings across a restart

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

* changes: entry for abandoned-turn step seals

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

* session: the settle-bound and final-report exits seal in the journal only

A full sealTurn on those exits could, after an abandon, end a replacement
turn's seal watch, move its notices and add duration. They now write the step
seal the abandoned exit writes: the money, no duration, no turn-ending acts.
The digest regression closes its agent before reopening the journal.

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

* changes: the extra exits write journal-only step seals

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…h, count and spend (#2446)

A conversation proposal's worker opens on its own work, with the person's message after it as the conversation's background. Run workers' plan CLI writes refuse duplicate titles, tasks more than two levels below the run's root, and more than 20 counted tasks per run. Every ordinary run has a whole-tree dollar ceiling (the lower of the conversation's remaining limit and the per-task limit), and reaching the per-task limit stops the tree as a budget stop. Countdown-started proposals joining a live run obey the same bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings app/v3 a2a444a (10 commits) into the integration branch.

One conflict, .github/workflows/ci.yml: app/v3 #2443 now shards cmd/codeaf
in the touched gate, which replaces the branch's unsharded 25-minute codeaf
leg (its own comment said to drop it once the suite fits 15 minutes). The
branch keeps its workstreams/main pull-request and push triggers.
docs/rules/ci.md keeps the branch list and drops the 25-minute exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er their own task, run subtasks keep their parent (#2452)

A conversation proposal's run is built from its own title, work, deliverable and done-when only. A run worker's plan CLI adds only under its own task, five tasks per worker, so the depth bound binds; a hand-off joined into a live run reads its own assignment and the wake lists only what the worker dispatched. tasks.list keeps each run subtask's parent and group, and a stopped or set-aside run leaves nothing reading running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings app/v3 3a6aa31 (7 commits) into the integration branch: a removed
computer quits quietly, Factory poll and triage stop between test homes,
and five test fixes.

No conflicts, and no file changed on both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#2460)

* Factory: any GitHub 429 is a rate limit, never "repository not found"

A 429 from GitHub on the visibility check or the repository issues read was a
rate limit only when it carried X-RateLimit-Remaining: 0 or Retry-After. A bare
429 fell through to the denial path: the caller read "repository not found" and
the denial was cached for the visibility TTL. Every 429 is now a rate limit
(busy with retry_at: the reset or Retry-After when known, otherwise one minute
from the Factory clock), it is never cached, and the issues read and the poll's
trouble words agree. A 403 still needs the spent-limit headers.

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

* docs: change entry for the bare 429 rate limit (#2460)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Tasks: each check in a run gets its own checker ceiling

The checker seat's spend ceiling was meant for one task, but a run shares one
crew guard across all of its tasks, so the ceiling was tallied run-wide: once
one check had spent it, every later check was refused on its first call. Each
run task's calls now carry its task id beside its seat, and the guard keeps the
checker's ceiling per task. Every check's spend still counts toward the run's
per-task limit and the day's limits, and saved crew records keep their per-seat
totals. A crew without a task mark keeps its per-seat ceiling.

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

* docs: change entry for per-check checker ceilings (#2461)

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

* Tasks: a check's ceiling estimate reads only that check's own costs

Review round: the guard raises a call's estimate to the model's last actual
cost on the shared day. With per-check ceilings that let one check's overrun
inflate the next check's first-call estimate past its own ceiling, so the next
check was still refused before it called. A marked check's ceiling hold now
raises its estimate only by that check's own last cost on the model, and each
tally (run total, day, ceiling) releases exactly the amount it held. The run
total and day holds keep their existing conservative estimate.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#2462)

* Session: a conversation scoped to its own work gets no other-conversation context

A conversation's meta.json can now store scope "conversation". Such a
conversation gets no <elsewhere> block on its turns, and its tasks tool lists
and addresses only its own tasks (no other conversations' landed rows, no
running-in-other-windows or everywhere sections). The default, project, keeps
today's behaviour. The word is written under the meta lock and read beside the
turn and in the tasks tool, so a live conversation follows it from its next turn.

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

* App wire: agents.set_scope sets a conversation's work scope without opening it

agents.set_scope {id, scope} stores project or conversation in the
conversation's meta.json and answers the stored word. It never opens, warms or
interrupts the conversation. Engines that serve it announce agents.scope.

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

* Change entry: name PR 2462

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

* Scope: own constant names, typed wire result, stale meta writers keep the scope

The new scope words are WorkScopeProject/WorkScopeConversation (ScopeProject
already names an answer scope). agents.set_scope returns *AgentScope as typed
handlers must. saveMetaLocked keeps the stored scope when a whole-Meta writer
loaded before a scope change saves with none, and project is stored as its own
word so a reset survives that rule too.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…end total (#2464)

* Tasks: the older task road refuses duplicate work and holds a tree spend total

With CODEAF_TASK_BELT=node the session-tree road bounded depth (3), children per
node (20) and each node's own bill, but two nodes of one tree could hand out the
same work, and the tree as a whole had no spend total. It now applies the default
road's bounds from #2446:

- A child whose title matches, after plandb's normalization (now exported as
  plandb.NormalizedTitle, one definition for both roads), any non-cancelled task
  in the same tree is refused with the same words, and a division naming the same
  work twice is refused whole. Pending proposals reserve their titles. A typed
  task starts a separate tree, so separate trees may reuse a title.
- Every node, helper and check of one tree shares one spend tally held to the
  lower of crew.task_cap and the conversation's remaining limit (the same
  arithmetic as the default road). Reaching it stops the whole tree and the
  ending names the limit and how to raise it. A checkpoint keeps the tree's spent
  total.

Two existing division tests used repeated titles as filler; their fixtures now
name distinct work and keep every assertion.

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

* docs: change entry for the older road's tree bounds (#2464)

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

* Tasks: the older road's tree total is checked where the node limit is

Review round: the first version held the tree to a new shared guard with its
own stop and cancellation, which read a limit stop as a person's stop, missed
paid tool receipts, could re-grant a conversation-bound allowance after a
restore, and waited on checkpoint writes before cancelling. The tree total now
reuses the older road's existing per-node check instead: before each worker
request, the summed bill of the tree's root and every descendant is compared
with the tree's ceiling, which is fixed when the root is admitted (the default
road's arithmetic) and kept in the checkpoint. A node over it ends on the
cost-limit ending with the configured-limit words, or the conversation-limit
words when that bound it. The shared guard and its hooks are gone.

TestAPartsBriefHoldsItsBoundWhateverLengthTheFieldsArriveAt sent two parts with
the same title, which a division now refuses; its second part's title differs
and every field stays at full length.

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

* test: older-road tree-bound fixtures deliver child reports and give plan specs an id

The spend test's graph dropped every report, so the root never finished folding
its parts; it now delivers child reports as production does. The work-identity
test's plan write lacked an id, so plandb refused it before comparing titles.
No production change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ory jobs (#2468)

* App wire: announce collections.factory_job when collections hold Factory jobs

An engine serving collections now also announces collections.factory_job
in hello.features. This wire already checks factory_job refs (a Factory job
by factory and number) and passes them to every collector, but a client had
no way to tell such an engine from an older one that announces only
collections and refuses the kind with bad_params. A client can now check the
feature before it adds a job, instead of finding out after it made one.

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

* Change entry: name PR 2468

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

* App wire: an engine without collections.factory_job may refuse the kind, not must

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#2465)

* Models: a fallback model is for that run only, never the saved choice

On a local launch whose configured model is not listed (no key for its
provider, say, with only a local Ollama running), the terminal picks the first
listed model so the conversation can run. It skipped the profile's talk row,
but it switched through the ordinary model setter, so the fallback became the
conversation's saved model: the agent reopened on it (also in the app, where the
configured provider worked), and errand routes that fall to the conversation's
model followed it. The chat manual already said automatic replacements do not
overwrite a saved preference.

The conversation now has a run-only model door (session UseModelForNow, carried
over the linked-local wire, older engines keep the old setter). Requests run on
the fallback while the saved model, provider, identity and main/fallback pair
keep naming the configured choice; the app reports the configured model. The
terminal's availability reconcile, the implicit credit-default follow, the move
after disconnecting the running model's service, and the linked-local source
refresh use it, and every later reconcile returns to the configured model as
soon as it is listed again. An explicit choice, including choosing the fallback
that is already running, and a fresh service connect still save as before. A
chat continued from another computer keeps its existing replacement rule.

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

* docs: change entry for run-only fallback models (#2465)

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

* Models: the run-only switch is asked off the update loop; renames, queued words and new chats keep the saved choice

Review round:
- The terminal asks the engine for the run-only model through its ordered
  off-loop command, as every engine door must; sends wait for the acknowledged
  switch, and an explicit pick queued behind it supersedes it.
- Renaming a service relabels the saved choice and a run-only model
  separately, so a rename never saves the fallback.
- An explicit choice clears a run-only model word still queued for the next
  request.
- start_chat with no model gives the new chat its parent's configured model;
  the new chat applies its own availability reconcile.
- The implicit credit-default follow is restored as it was: it is the build's
  default moving, not an availability fallback.

Existing tests gain only the acknowledgment step the asynchronous switch needs;
no assertion changed.

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

* Models: parked sends and deferred renames wait for their own chat's model switch

Review round 2:
- A parked message (text and attachments) stays queued while its chat's
  run-only switch is pending or refused, and is sent once on the acknowledged
  model; pending switches travel with the chat when the window moves to another.
- A deferred service rename is bound to the chat it was made for and is retired
  by a later explicit pick, so it never saves into another chat or over a newer
  choice.
- Two new test fixtures gain the provider base URL their sessions require.

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

* Models: a send waits for its chat's pending model switch instead of the window holding it

Review round 3: holding sends at the surface while a run-only switch was
unacknowledged kept missing send roads (a send deferred behind a replay, a
hidden chat's queued send). The surface no longer holds anything: it shows the
replacement at once, and every engine call that starts a turn (a plain send,
an attachment send, a follow-up, an errand send) waits inside its own command
for that agent's switch still on the ordered door line, so the engine always
sees the switch before the message. A refused switch still lets the message go;
the next reconcile retries. A hidden chat reconciles when it comes back to the
front, as before. Existing tests keep only the acknowledgment steps the
asynchronous switch still needs.

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

* Models: a takeover resume waits for its chat's pending model switch

Review round 4: resuming a turn stopped by a takeover started the turn without
waiting for the chat's run-only switch still on the door line. It now waits
inside its command like every other turn start; a cancelled wait leaves the
resume for later, a refused switch still allows one resume.

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

---------

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant