Skip to content

Factory: any-source intake, router and AI filter (draft, e2e first) - #2478

Merged
AbirAbbas merged 60 commits into
app/v3from
factory/general-intake
Nov 9, 2026
Merged

AbirAbbas merged 60 commits into
app/v3from
factory/general-intake

Conversation

@santoshkumarradha

Copy link
Copy Markdown
Member

Draft. Do not merge until the owner has run it end to end.

The factory takes work from any source, not only GitHub issues. A router decides where each item goes. The output stays a pull request for now.

Intake ──► Router ──► Pipeline ──► Output ──► Follow

The design and acceptance criteria are in docs/design/factory-general/DESIGN.md.

Lanes merged into this branch

  • Inbox (factory: the inbox — outside work becomes a job (lane I) #2477): inbox.Add is the single door for outside work. It is reached from codeaf factory add, POST /api/intake and factory.intake_add. It also provides Feed/Poll for connectors and Notify to send write-backs.
  • Router (factory/gi-router): ordered rules, matched against an item's source, repo, labels, authors, text and priority, set its repo, pipeline, priority and start (auto, ask or backlog). The queue runs in priority order. factory.routes_test previews what the rules would match.
  • AI filter (factory/gi-judge): when.ask asks a model a yes/no question about the item and keeps a one-sentence reason. The model may only pick the rule's own outcome, a failed call counts as no, and a stranger's ticket is never started by a model's answer.
  • Jira (factory/gi-jira): a Jira Cloud feed selected by JQL, a replier that comments the PR link back on the ticket, and jirastub for tests.
  • End-to-end on deepseek/deepseek-v4.1-flash: a Jira ticket in the stub is routed to factory-demo, opens a PR, and the PR link is commented back; a p0 trigger runs before older work; a repeated delivery makes one job; GitHub behaviour is unchanged when there are no rules.

This branch is kept in sync with app/v3 by merging, not rebasing, because the lanes are built on top of it.

🤖 Generated with Claude Code

santoshkumarradha and others added 30 commits November 6, 2026 15:20
Shared types the three wave-1 lanes build against: route.Rule/Match/Action,
inbox.Entry/Add, and Item.SourceRef/Route. docs/design/factory-general/DESIGN.md
says what each lane owns and the end-to-end acceptance.
…ontract

A connector translates only; one loop drives every feed and one notifier
answers back by source, so Jira and later sources are one small type each.
…udge

Decide stays pure and answers Pending for an unjudged question; verdicts are
kept on the item by rule and re-asked when its words change. A stranger's
ticket can at most be held to ask first by a model's yes.
* factory: the inbox — outside work becomes a job through one Add

inbox.Add files an Entry as a job keyed by SourceRef <source>:<id>, Num 0,
owner tier, the repository's steps; a repeat updates outside words and keeps
what a person set. Validate is the one check every door shares. Poll drives
any Feed under the store's poller lock and source meta; Notify answers a
registered Replier off the hot path when a job opens its pull request, merges
(worker report in lease.Desk.Apply, or seen on GitHub in follow) or stops
for a person.

Doors: wire factory.intake_add (hello feature factory.intake), CLI
codeaf factory add, HTTP POST /api/intake (access token or operator, 64 KiB).
A job with no repository waits: the launch refuses it with NoRepoRefusal and
intake passes it by.

* changes: the factory inbox entry

* changes: shorten the inbox entry's title
* factory: the AI filter's model-backed judge

internal/factory/route/judge implements route.Judge: a fixed system
instruction, the rule's question, then the item as delimited untrusted
data (body cut at 2000 runes). The answer must be exactly
{"yes":bool,"why":"..."}; anything else is an error, never a yes.
20s per attempt, one retry on a transient failure, every attempt's cost
told to Spent. Model: the rule's (WithRuleModel), else the factory's
default, else the arrival read's. judgetest is a fake for other lanes.

cmd/codeaf/factory_judge.go builds it on the provider and books each
call under "factory filter", counted as the factory's own AI.
A -tags e2e test asks deepseek/deepseek-v4.1-flash 4 questions over 6
items, a prompt-injection item among them.

* changes: the AI filter's judge (#2479)

* changes: the AI filter's judge (#2479)
The taxonomy law keeps one type named Verdict in the module, and the person-facing
vocabulary bans the word. The judge's instruction no longer assumes a software team,
because the factory now takes work from any source.
… (lane R) (#2480)

* factory: the router — routing rules, the AI filter and priority order (lane R)

* factory: every host's intake asks the AI filter

factoryIntakeOwned hands intake.Options.Judge the floor's model-backed judge
(factoryJudge), so the app, a team server and the command line ask a
routing rule's question the same way, booked as `factory filter`. A rule
names no model yet, so the factory's default answers.

The manual's routing section now defers to lane A's page for model, cost
and what the model sees, and keeps what intake adds: the answer kept on a
job, "couldn't judge", the activity line and test these rules.
…poll (lane J) (#2476)

* factory: Jira Cloud connector — feed, replier, jirastub, connect wire and poll

internal/factory/jira translates Jira Cloud REST v3 into the inbox:
Feed runs the person's JQL narrowed by an updated cursor (account zone,
one minute overlap) into inbox.Entry values with a jira-project:<KEY>
label and Jira priority mapped p0-p3; Replier comments the pull request
link on open and merge and the job's words when stuck. jirastub fakes
exactly /myself, /search/jql and /issue/{key}/comment.

factory.jira_connect/_status/_disconnect connect this machine's factory,
checking /myself and one search before keeping anything; the token is
kept like the GitHub one. The Jira poll starts beside the GitHub poll
only while connected and restarts on connect and disconnect.

* changes: entry for the Jira connector (#2476)

* factory/jira: a refused answer carries its reason, decided once at the door

The classifier law forbids reading a status as a decision outside the
fact packages; IsAuth now reads the Reason the client stamped.

* factory/jira: on the real inbox — replier follows the connection, recipes, one health record

The Jira answerer is registered from the host's mount while connected and
unregistered on disconnect, independent of the read loops. The poll passes
inbox.WithRecipe through the floor's RecipeAt, as POST /api/intake does.
Status reads last read and last error from the poll's own source record
(store.SourceMeta) instead of a second file. The poll-wiring test now
asserts ticket → job → PR comment, and silence after a disconnect.

* manual: a repeated ticket reaches the outside-work page, not the Jira one

The asker's word is ticket; the heading that answers it now says so, and
the Jira page points there instead of restating the rule.

* factory/jira: prove a Jira job is routed by source jira and its project label

* manual: priority is a mark on the job, not the resident's board

Lane R's priority section used the resident's word, which the chat
manual gate refuses.
The inbox's no-repository guard read RepoOf, which answers "" for a repository
with no owner — every job typed on a floor without GitHub — so launches failed
with "say which repository the work is on". NamesNoRepo is the one check for a
job that names no repository at all; four cmd/codeaf factory tests were red.
* factory: the facts the desktop needs on the wire (lane W)

job.source/repo_url/route, one step count (steps_done/steps_total), runs_on
from the one model chain (which now reaches the person's worker model before
the launch default), entry.author on transcript entries, no machinery in
suggested replies, and a step scratch folder outside the checkout with
repository copies kept out of the change and the commit.

* manual: the model a factory step runs on, said in fewer words; the tilde page answers "start with ~"

* changes: the factory job facts entry
…e AI declined

A Jira service-desk customer is a stranger and every reporter is at most a
collaborator, so an AI yes can no longer start, and a job can no longer
write for, someone outside the team. job.route_declined says why a job no
rule started.
Encode spelled out max_cost, size and default_budget, and an explicit
max_cost holds every job with no cost history, so start: auto on a factory
copy sat at Approval needed: start.
…over the source's (lane E) (#2482)

* factory: a routing rule's priority wins over the one the source sent; general-factory e2e (lane E)

* e2e: the stranger's entry carries trust: stranger; a declined rule shows on route_declined

* e2e: a held start is approved with factory.job_approve

* e2e: only scenario 1 runs on the factory copy; cost includes the engine's usage ledger

* e2e: cost to six places

* e2e: scenario 1 must start on its own; scenario 6 saves every agent's chat; FLE2E_HOME and FLE2E_KEEP_JOB keep the job for the desk

* e2e: three quick steps done once the job follows its pull request; spend_by_model reads today

* e2e: cleanup closes only the FLE2E work this run opened

* manual: a routing rule's priority replaces the one the source sent

* changes: a rule's priority over the source's; the general-factory e2e

* changes: keep the title to one line
…edit keeps the repo's steps

Found live by audit B: a pipeline PR for a two-line change carried all eleven
shipped steps, and editing a step before Start ran the built-in list instead
of the repository's pipeline.
…pository (#2483)

* factory: routing says what it did, and outside work can be given a repository

The audit of factory triggers (2026-11-06) found eight engine bugs:
- Start on a job with no repository is a conflict in the factory's words, never uncertain.
- factory.job_set_repo gives a job that never ran its repository and routes it again.
- A job with no repository reads "Waiting for a repository".
- A rule naming a missing pipeline writes one activity row and never fails the pass (no poll backoff); routes_test says refused; routes_set refuses it when the file can be read; GitHub poll failures are logged.
- Pick up issues off governs the label rule only; routing always runs.
- A queued job that never ran takes a picked pipeline.
- A Backlog rule writes its activity row; a start hold carries no autonomy_why.
- An edit keeps a routed job's route and steps unless a different rule matches, which is said in activity.

* factory: name pull request 2483 in the routing change entry

* factory: with no routing rules GitHub behaves exactly as before

Pick up issues off with an empty routing table runs no intake pass at all,
as before routing; and a start hold a label made keeps its reason, so only a
routing rule's hold stops reading as Auto stopping.
…ts intake (#2487)

* factory: the factory is an AgentField node; AgentField triggers are its intake

internal/factory/agentfield registers the factory on an AgentField control
plane as codeaf-factory with intake, message and status reasoners, renews its
lease as a fresh instance per start, and serves them under a secret path in
its base_url (the trigger dispatcher sends no credential). One translator per
source (linear, sentry, slack, generic, cron) turns a dispatched event into an
inbox.Entry under agentfield:<source>, trust capped by the ceiling; a github
event only wakes the native GitHub read. intake answers 202 and reports the
job's progress and completion (pr_url) on the execution.

Wire: factory.agentfield_connect/_status/_disconnect/_triggers/_sources/
_trigger_add, hello factory.agentfield. Manual page factory-agentfield.

* factory/agentfield: every lock unlocks from a defer

* e2e: the factory as an AgentField node, against a real control plane

* changes: name the AgentField node's pull request
)

* factory: make named pipelines as edits, and a pipeline library the factory keeps

add_pipeline, remove_pipeline and rename_pipeline are pipeline edits, so the
preview, a pull request and the factory copy all take them; pr, ci and issue
are refused as names. The factory keeps a pipeline library (pipeline: and
steps: only) behind factory.library, factory.library_set (operator only) and
factory.library_preview. A job's pipeline name resolves in one place,
pipeline.File.Resolve: the repository's file (or the factory's copy), then
the library, then the built-in steps; factory.pipeline lists every choice
with where it came from. A shipped step a file changed in part writes only
that part.

* changes: the pipeline library and named-pipeline edits (#2484)
…inks, signed webhooks per source (#2485)

* factory: the Trigger API — a token per source, messages and status from outside, signed webhooks, Go and TypeScript clients

* factory: Trigger API reshaped — one event bus with sinks, webhooks per source, message and status doors for AgentField; no source tokens, no SDKs

* factory: shorten the Trigger API change title

* factory: the Trigger API change entry names its pull request

* factory: the AgentField node is a sink on the event bus, and its message and status reasoners are the Trigger API's doors

The node registers as inbox sink "agentfield" and reports each moment to
the job's open executions; the 20-second tick is gone. An execution quiet
for keepAlive is told again with the lease, so a long job never looks
stale and a dropped job still completes. The message and status reasoners
call trigger.Message and trigger.Status; Node.Send and its job_send wiring
are removed. #2487's change entry now names its pull request.
…a door for the foreman (#2488)

* factory: managers that keep a running job running, see the post-PR decision, and a door for the foreman

- A step edit to a running job withdraws the worker instead of releasing it,
  so the job is queued again at the step it was on rather than stopped to
  Backlog and re-run from plan.
- factory_status carries the job's autonomy, the one step count, the PR facts
  the factory read, and the post-PR decision the job page shows; factory_answer
  presses Merge, Mark ready or publication approval through the page's own door.
- factory.foreman_open opens the factory-wide manager; factory_floor reads
  running, queue and shipped, and starts or prioritises a job when asked.
- A window on a moved CODEAF_HOME names that home on the model's page.

* e2e: the managers live lane (audit A2)

* changes: the managers entry

* changes: shorter title

* factory: woken manager answers keep the id a reading gives them, manager edits are recorded, and the foreman counts a job following its PR as running

* factory: the foreman counts a job whose pull request state is not read yet as following it

* e2e: the managers lane logs each state the edited job passes through
…hrough AgentField (#2489)

* factory: close the open programmatic doors; programmatic work comes through AgentField

Remove POST /api/intake and its messages and status routes, /v1/webhooks,
the signed-webhook sink and its outbox, and the codeaf factory add and
codeaf factory webhook commands. The desk's factory.intake_add now files
through inbox.Add in the app's own process, on the factory it hosts.
trigger.Message and trigger.Status stay for AgentField's reasoners.

* changes: 2489 the factory's open programmatic doors are closed

* changes: 2489 the factory's open programmatic doors are closed
…step; the foreman is the Factory manager (#2490)

* e2e: the managers lane says what a landed job waits on, and keeps the job beside the logs

* factory: the next lease's go-on waits for a withdrawn drive and runs the job on

A step edit withdraws the worker running a job, and the next lease lays it
out paused at its step and asks the runner to go on. When the withdrawn
round was still letting go of a model call, the runner found the old,
cancelled control, read it as not paused and the ask as unchanged, and did
nothing; the old drive then ended and the next worker gave the job back to
Backlog with Write still marked running. A cancelled control asked to go on
on a job the store holds paused is now the same handover as a cut pause:
the ask waits for the old drive, then revives the job.

* changes: the edit-stall entry

* factory: a model's step needs a name of its own; a nameless sentence whose first word is not a step is refused

* factory: the foreman is the Factory manager, and a job following its pull request reads so on the floor

* manual: a named step, the Factory manager, following its pull request

* factory: a job running a Follow PR repair also reads following its pull request, on Follow PR

* e2e: the managers lane takes a manager's question card as a person would, never a merge
* e2e: the host guard removes a read-only Go module cache

A factory step that runs go under the guard's throwaway login fills
$HOME/go/pkg/mod read-only; the guard's RemoveAll left it behind and
t.TempDir's cleanup then failed TestFactoryManagersE2E after every
assertion had passed.

* changes: entry for #2492
…prove_step; removed pipeline drops its own steps (#2493)

* factory: the landing approve step waits for its answer; a Manual hold is its own Needs-you kind; a removed pipeline takes its own steps

An approve step that is a run's last step marked itself done on landing (local
runner) or stayed pending (worker), so publication went past a step nobody had
passed. It now lands waiting, the proof approval passes it, and an approval is
refused while any other approve step is unpassed.

Manual's hold before a step read as approve_plan ("Plan ready"). It is now
approve_step, gate step, approval kind step, and names the next step by label.

remove_pipeline left its own step definitions behind, so a later add_step of
the same name was refused as "That value is out of bounds". It now drops the
steps only it used, and the clash refusal names the step.

* changes: entry for #2493

* changes: tighten #2493's entry

* changes: shorten #2493's title

* changes: shorten #2493's title again

* factory: the manager's merge fact is the job page's merge check; steps in person words

After "mark it ready" the manager said merge was not offered while the job
page offered Merge: the press reply never read the job again, so the model
repeated its draft-era status. The mark-ready reply now re-reads the job and
says what the page offers, and the page's merge block, the manager's status
and its press all read factory.MergeOffered.

factory_status named steps by their stored keys (followpr); it now uses the
step's label, or Follow PR for the follow step.

* factory: gofmt the step-words test

* factory: a settled plan approval never answers a later step hold

The approve_step kind accepted a stored plan approval as the hold's own, so
an earlier gate's settled consent closed a later Manual hold's Approve
(TestAppFactoryApprovalSurvivesLaterGatesAndRestart). Only an open plan
approval carries over; a settled step approval is an earlier hold's, as a
settled plan approval already was.

* factory: pin the desk approve on a Manual plan hold through the undo window

The approval settles and write runs; the next hold, before test, reads as
approve_step with an approval of kind step, never as the plan hold again.
…2494)

* factory: connect the first run's GitHub in the browser with the device flow

factory.setup_github_{start,poll,cancel} run GitHub's OAuth device flow for
the factory on this machine. The granted token takes the pasted token's own
connect and is handed back once as github_token for the app's Keychain.
The OAuth client ID is stamped at build time (CODEAF_GITHUB_CLIENT_ID) or set
at run time; a build without one leaves the verb off the wire.

* changes: the app's first run connects GitHub in the browser

* factory: a live e2e for the browser sign-in up to GitHub's device page

Opt-in (CODEAF_LIVE_GITHUB_SIGNIN=1, a mode-600 token file): the real wire and
device client against a loopback stand-in for github.com/login, then the real
GitHub for the connect. Asserts the account, zero writes, the token said once
on the wire and written to no file.
* executor: fence every command an unattended run executes

Factory steps, codeaf chat --once (including through a host's headless
conversation) and codeaf exec now run their commands on a Fenced seat:
network open, writes only in the checkout, its git folder and the job's
own temp and cache folders, keys and sign-ins unreadable, docker refused.
macOS uses seatbelt; Linux uses Landlock with user namespaces where the
system allows them and Landlock alone where it does not (stock Ubuntu).
A blocked command reads one sentence; jail.allow widens the fence.

* executor: run the fence's process tests on macOS as well as Linux

* executor: resolve a not-yet-written path's folder, and keep a hidden folder listable on macOS

* executor: the file-tool fence test allows for macOS's writable temp folders

* executor: the fence wraps whichever seat a session sits on, and an e2e drives it through chat --once

* executor: npm test runs inside a fenced checkout

* executor: a fence built marks its job in use; the stage posture comment names the fence

* session: the fence's file-tool gate reads arguments through decodeToolArguments

* executor: the fence keeps the person's CARGO_HOME so codeaf's shell still finds cargo
santoshkumarradha and others added 21 commits November 7, 2026 18:05
…s re-attached, not retried; a stopped job offers no Resume (#2506)

* factory: a rejected AgentField run ends Rejected, a live remote run is re-attached, a stopped job offers no Resume

E1: a run the control plane cancelled on the person's Reject ended its job
as a failed step (tool_error) when the runner's reading of the cancel beat
its acknowledgement of carrying the Reject. lease.Apply now ends the job
Rejected with the person's words whenever a run ended on a recorded rejected
decision of one of its own pauses, or on the control plane's
approval_rejected reason.

E2: a runner lost while its agentfield step's run went on at the control
plane stamped an interrupted step and spent an automatic retry. queueJob now
requeues such a job with no failure, as when a runner lets go.

E3: a stopped job offered resume, which refuses it with 'is not paused'.

* changes: entry for #2506
…ers no Give back (#2507)

E1: QuestionForm sends a step's question (QKind step) as kind "step" with
its options, the contract appwire's factory.job_answer documents.
E2: an AgentField harness question answered here stays answered until its
step takes it down, on disk too; one withdrawn while the step goes on comes
off the job, so the run's end lands.
E3: the rejected detail ends the person's sentence once.
E4: controls.give_back is false on an ended job; Item.Manual stays set.
…ite receipts; the idle engine stays idle; a rejected step carries no failure (#2508)

* factory: a restart no longer orphans an AgentField run; Stop reaches an unheld run; fenced checks write receipts; idle checks decode nothing; a rejected step carries no failure

E2/E4: a job whose AgentField run is live with no hold is owed a runner
(running, queued, or waiting on that run's approval), so a restart or an
approval given while unheld no longer leaves it "running" forever. A Stop
that landed with no runner holding the job is carried by the next runner,
which cancels the run and writes its end.

E5: the stage fence makes and allows the step's check-receipt folder, and
an out-of-rounds question counts unconfirmed checks in a sentence instead
of quoting their shell lines.

E3: the idle manager's stop check and the step budget check remember the
file they read; an unchanged job decodes nothing (8.5 ms -> 4.5 us per
check on a six-job floor).

E1: a step whose run ended on the person's Reject ends as a native Reject
leaves its step: no failure, no step_failed event.

* factory: keep the stop-check lock under a defer; a comment says no path

* factory: the answer-door test parks its jobs where this runner does not follow them
…ed, idle stays idle (#2510)

* factory: a re-taken run reports only its own step; a live run's lost hold records no failure; a runner quitting mid-step requeues the interrupted step

* factory: an idle engine decodes nothing: step chats read only changed jobs, idle pulls skip an unchanged floor, runner requests settle once a second; a restarted runner's rows go

* PERF: idle runner pulls, the registry's save rate and worker settles, with live idle CPU before and after

* changes: name #2510 on the restart and idle entry
…ief (#2511)

* factory: a step that failed before it reported is evidence for the brief

A remote step that ends failed before reporting keeps no round and no
result, so the brief writer had no summary for its latest try, and a
brief citing that try was refused as naming nothing the job has. The
fifth live A+B run's job 78 never had its brief updated for its failed
build ("Couldn't update the brief").

The latest try of a failed phase with no result now carries the job's
failure detail as its summary, when the failure names that step, and
the summary evidence check reads the same text.

* changes: entry for #2511
…g its floor (r5 E10, E3) (#2512)

* factory: a run-delivered PR can merge, and an idle engine stops re-reading its floor

E10: a pull request a run on another machine opened records the head the
follow first reads after the run ended as the job's approved commit, so
Merge is offered (and the job leaves Running) instead of refusing forever.

E3: the factory store decodes a job document only when its file changed
(a process-wide, stat-validated, deep-copied cache); an idle manager checks
its budget and Stop once a second instead of ten times; the knowledge vault
backs off while quiet and wakes on Build. Live linux rig, 82 jobs, 6 idle
managers: 5.8% -> 1.6% of a core, 1.7 GB -> 8.3 MB read.

Slow exit: the wrap-up now names any part that takes over a second.

* changes: r5 engine fixes entry
…s its slow conversation (r6) (#2513)

* engine: an idle open chat costs nothing between heartbeats; a quit names its slow conversation (r6)

Idle CPU (r6 finding 2): with 24 idle job and step chats the engine sat at
7-9% of a core. Three per-chat costs are gone:

- presence.json is written when it changes, and an idle claim refreshes once
  a minute under a lease it carries (leaseSeconds, three minutes) that
  readers believe instead of the 15 s busy window (SessionPresence.Window);
  working and waiting claims keep the 7 s refresh and the short window.
- the doorstep (answers, requests) is read when the shared directory watch
  reports a write there, and otherwise once a minute for a quiet session; a
  folder the watch cannot see is knocked on every beat as before.
- the job budget watch sleeps on one timer, 5 s while idle, instead of waking
  every 100 ms; a turn, a command start or a presence nudge wakes it.

Rig, 24 idle job chats: Linux 2.07% -> 0.58% CPU, 1426 -> 201 syscalls/s;
mac-studio 5.46% -> 0.53%; presence rewrites 3.6/s -> 0.3/s.

Quit (r6 finding 3): conversations were already let go in parallel; each
one slower than a second is now named on stderr with its gate wait and its
own close. A waiting engine names the pid of the one still closing its home.
A subprocess test quits with twelve conversations that took real turns and
asserts every journal kept its turn and the exit is 0.

* changes: entry for #2513
…nd start time kept (#2514)

* factory: an AgentField step ends in plain words, its outcome on the job

WorkAF live e2e rounds 6 and 7:

- A step run on AgentField reads as its label, never as its reasoner's id.
  A finished step says "finished and opened <link>". A run that did not
  succeed fails with "<step> did not succeed: <why>". The why comes from
  the shape of the run's own last note or its summary: a leading grade is
  dropped, the reason is cut before any quoted command, and "No pull
  request:" reads "…, so no pull request was opened". This is the
  failure's summary. The raw report, command output included, stays as
  the step's output.
- The control plane's notes are read (GET …/notes) so the run's own
  outcome sentence lands in failure.summary; state_presentation.detail
  prefers the summary.
- Engine sentences a person reads no longer say "verified".
- A failed job's file says State "failed" and reads back as new with its
  failure, so every door behaves as before.
- A step revived after a restart keeps its start time and does not say
  "<step> started" a second time.

* changes: the entry for #2514
…lain (#2515)

* factory: every job's PR carries its own file list; Brief words stay plain

A followed pull request now carries pr.files (path, status, line counts,
previous_path for a rename), pr.files_total and pr.files_head, read from
GitHub once per head and bounded at 300 files, for every job with a PR,
AgentField/SWE-AF runs included. A list is only ever about its own head.

The Brief writer is told not to use auditor, verdict, verified,
verification, unverified or refuted, and every drafted heading and text
goes through a deterministic plain-words filter before it is kept, so
"What the verification found" reads "What the checks found".

* changes: entry for #2515

* manual: name the run elsewhere without the removed worker's word
…losing chats; clock leaves with its window (r7) (#2517)

* engine: a held lease's heartbeat no longer re-reads the floor; one group commit saves every closing chat; the clock leaves with its window (r7)

Idle (r7 finding 2): each held job's runner renews its lease every 2 s, which
rewrote leases.json, woke every open pull and, with a hold present, made all
15 slots list and deep-copy all 86 jobs once a second.

- the root watch compares lease.RenewalShape; a renewal, a runner's folders
  and events.jsonl wake no pull
- Desk.floorQueue: one reading of a change shared by every slot; unchanged
  jobs are reused, only changed files are read, sorted only on change;
  OverLimit reads it too
- a job already held at its budget with a reply pending is no longer saved
  unchanged on every look (a write loop)
- runner requests settle the floor every 5 s, not every 1 s
- CODEAF_DIAGNOSTIC_PROFILE_DELAY opens the profile window later

Quit (finding 7): cellstore's one flush slot synced only its own chat's
records while every other closing chat queued; whoever holds it now syncs
every chat's owed records once.

Clock (finding 9): codeaf clock leaves once the window that started it is
gone and nothing is in hand.

* engine: beats settle every 5 s while reports settle within 1 s; an unchanged manager journal is parsed once (r7)

The 5 s settle window delayed Auto's publish after its proof (worker
TestAutoPipelinePassesPlanGateAndPublishesWithoutUserApproval and
TestRecoveryAutoPipelineRepairsTransientFailureAndUpdatesTheSamePR timed
out). Only lease heartbeats and presence now wait five seconds; pulls and
reports settle within a second as before. thread.Read hands back the last
answer for a journal whose file and folder are unchanged.

* engine: a job chat's budget watch lists the workers' mirror folders once per change of them (r7)

Every open job chat's budget watch globbed workers/*/*/<job>.json every
few seconds; on macOS with 43 chats that was a fifth of the idle engine.

* engine: the mirror folders' memory unlocks from a defer (lock law)

* PERF: r7 idle after numbers on linux and macOS
…ote by meaning, nothing to change (#2518)

* factory: a run that finished before a quit is read, not lost; outcome notes by meaning; nothing-to-change reads so

- An agentfield step whose run ended while the step was still running (quit
  seconds after the run finished) is no longer stamped machine_lost: the
  lease queues it, and the round a restart picks up (Job.Continuing) reads
  the run's end as the step's instead of dispatching a second run.
- The post-end drain is bounded by the reading's context, so a closing
  engine is not held for afStopDrain.
- The outcome note is chosen by what it says (no pull request / empty change
  > failure > progress), not by being newest.
- A run that changed nothing ends done with state_presentation
  "Nothing to change" and its detail sentence, not shipped silently.
- "resumed" is not said when a revived job only waits on an AgentField
  approval; the readiness line shows a 7-char commit.

* changes: entry for #2518

* changes: shorten #2518 entry title
… a clean quit hands leases back (r8) (#2519)

* engine: an idle floor's settle and a held job's flush reuse unchanged job files

* engine: a clean quit hands each held lease back so a restart takes the job at once

* docs: change entry for #2519

* PERF.md: the r8 settle and flush numbers on both hosts

---------

Co-authored-by: santoshkumarradha <eng@agentfield.ai>
…restart, empty PRs, plain step words, kept keys (#2520)

* factory: WorkAF r9 engine correctness — an ended run is read after a restart, empty PRs, plain step words, kept keys

- An agentfield step's run record now says its end is owed to the step
  (AgentFieldRun.EndOwed, set at dispatch, cleared when the step reads the
  end). A restarted round adopts an owed end — its pull request or its own
  outcome — instead of starting a second run, in Manual or Auto, whatever the
  step's start time says (r9 job 306: the start had moved past the run's and
  a second run asked for plan approval again). The lease's owed check reads
  the same field. A stopped or rejected run is never a later round's end.
- An open pull request read on its head as 0 files, with GitHub's own counts
  agreeing, is an empty change: Merge refuses it, the job reads "Nothing to
  change", and one the factory's job opened is closed with a comment and a
  plain note.
- Rejected and stopped jobs' files say "rejected" and "stopped", read back as
  new with that end on them, as #2514 did for failed.
- A resumed step chat's brief goes to the model under a whole-prefix lead the
  transcript display hides, so the person never sees it as their line.
- A reader's job shows an agentfield step's label where its note named the
  reasoner (node.reasoner).
- A failed run's headline names a short, plain failing command
  ("… (go build ./... failed), so no pull request was opened") when it fits.
- The AgentField key and internal token, and a pasted GitHub token, are kept
  in the engine's own profile (0600) as well as handed to the app, so a
  desk-launched engine keeps them across restarts.

* changes: entry for #2520

* changes: entry for #2520

* factory: only a shipped, unstopped job closes its empty pull request; a relaunch reads the kept token

* factory: keys stay off disk in app mode — revert the r9 key persistence

Owner decision 2026-11-08. In app mode the AgentField key and internal
token and a pasted or browser-signed-in GitHub token are held in memory
only again; the app hands them over at every engine start. The config,
setup and connection writes are reverted, the tests that pinned the old
rule are restored, and the manual and change entry say so.
…cuts (r9) (#2521)

* engine: a quit hurries cut streams' receipts, a gone checkout is not a failed save, an orphaned clock leaves at once

* factory serve: a root the kernel watches is re-read every 30 s, not every 2 s

* factory: a followed AgentField run that is not moving does not rewrite its job

* factory serve: a machine busy on two jobs is not announced again on every heartbeat

* factory lease: a renewal rewrites leases.json once its stored renewal has aged 10 s

* session: an idle chat's safety tick does not re-read a journal that has not moved; split Clock.Run under its ceiling

* manual: a quit records a cut reply's cost without waiting it out; a gone folder is not a failed save; the clock leaves with its window

* perf, changes: the ninth live round's quit and idle numbers; entry for #2521

* provider: the hurried ask's timer stop unlocks from a defer
…es an AgentField step its input (r10) (#2522)

A route that asks first records the person's yes to Start as an approval.
The worker's done read it as consent to publish and shipped a run-delivered
job down the native path, which added the Follow PR step, so the follow never
pinned the head the run delivered and Merge refused for good. A job that runs
elsewhere now ends as its run did, and RunDeliveredPR ignores the Follow PR
step so jobs shipped the old way heal at the next read.

The manual now says how the app's editor gives an AgentField step its input,
and a pipeline test proves the round's whole SWE-AF pipeline is made by the
editor's edits.
* factory: a late AgentField key restarts the running node

keys.set for agentfield, agentfield-internal-token or agentfield-node
only held the key; the node had read its secrets at start, so a factory
that started connected but keyless (the desk's keychain answered after
its 5 s launch wait) kept registering keyless until the next launch.
Now such a keys.set refreshes the node: restarted with the kept
secrets when they differ, left alone when they match.

* changes: entry for the late AgentField key

* factory: read the live local host under a deferred unlock
… on a reasoner (#2525)

* factory: list and add AgentField triggers from the app; a routing rule can run a job on a reasoner

factory.agentfield_triggers now carries the source catalog (labels, config
fields) and each trigger's entry source, webhook address and creation time.
factory.agentfield_add_trigger makes a trigger aimed at the node's intake and
says what to set up; a source the catalog lacks or a missing secret variable
is refused in words, and no refusal quotes the config. Feature
factory.agentfield_triggers.

A routing rule's then may name run_on (node.reasoner): a matched job runs on
that reasoner with no repository, as a job filed with run_on does. The
reasoner is checked against the control plane when the rules are saved on
this machine's factory. when.sources agentfield:* matches every AgentField
source. Feature factory.route_run_on.

* changes: entry for #2525

* appwire: the routes test engine takes pointer receivers

* factory: a catalog source carries the event types the control plane lists for it

* factory: refusals about run_on say agent, not reasoner
* factory: run_on jobs stop waiting for a repository, the own node is never an agent, waiting jobs route at once

A rule that routes a job withdraws the inbox's 'waiting for a repository'
line; a rule or an agentfield step on this organization's own node is
refused when saved and left out of factory.agentfield_reasoners; saving the
routing table and turning the runner on each run one routing pass.

* changes: entry for the AgentField run_on fixes
Brings in go-aheads (#2524) and Claude Code as a provider (#2505).

Conflicts, resolved by keeping both sides:
- The poll loops take the branch's failure log and app/v3's after-poll
  pass (factoryAfterPoll: start rules, then go-aheads).
- routeAndPickUp keeps the branch's routing table and runs app/v3's
  go-ahead pass after it, even with Pick up issues off and no rules, so
  startOnItsOwn, which routing had replaced, is gone.
- Wire and codec carry both the AgentField fields (run_on, agentfield,
  agentfield_steps, rejection, failure_summaries, source_ref) and the
  go-ahead fields (waits_on, waiting_on, policy_run, policy_group, blocked).
- The manual keeps the routing sections and adds the go-ahead sections;
  the Pick up issues paragraph says a go-ahead still starts its jobs while
  the switch is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fenced-shell belt fact pushed the widest fixed prefix 14 bytes over
its 52497 budget once app/v3's tool text joined it. "- No brew/docker
here." says the same thing to a fenced run in 15 fewer bytes; the fence's
own refusal sentence says the rest, as the comment above it already notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #2544 (an unpublished sync export is kept rather than sealed
again every pass) and #2545 (idle job chats and an unchanged vault cost
nothing per look).

#2545 and this branch's engine rounds each fixed the person-Stop watch's
per-look cost, in two different ways. The branch's kept implementation
remembers the answer against the job document it came from and never keys
a conversation that manages no job on the floor folder, whose time moves
with every heartbeat file renamed into it; #2545's manager index was
rebuilt on exactly that folder time, so a churning floor rebuilt it every
few seconds. The branch's version passes #2545's own tests
(TestAnIdleManagerWatchReadsNoJobs, TestTheManagerWatchSeesEveryChange)
as well as its own, so factory_manager_index.go, which nothing calls any
more, is not carried. Store.readDocument becomes a method so #2545's read
counter reaches it, PERF.md describes the one mechanism, and the #2545
change entry no longer claims the index. The vault change is kept as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as ready for review November 9, 2026 22:07
@AbirAbbas
AbirAbbas merged commit f508fec into app/v3 Nov 9, 2026
12 of 15 checks passed
@AbirAbbas
AbirAbbas deleted the factory/general-intake branch November 9, 2026 22:07
AbirAbbas added a commit that referenced this pull request Nov 9, 2026
…jsonl

#2478 lets a quiet session skip its doorstep pass for a minute because the
watch wakes it when an answer is WRITTEN. deliverAnswer appends to
answers.jsonl, and once that file exists an append does not change the folder's
entries, so a folder-only kqueue watch missed it. The darwin watcher now also
watches answers.jsonl while it is there: a write reports the answer, an empty
just-created file reports nothing (the line comes after the create), and a
drain's rename gives the handle back. A held chat costs its folder handle plus
one for an answers file only while an answer waits.

TestPresenceDoorC6 follows #2478's contract: a watcher error now knocks every
session once as well as running the safety pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas added a commit that referenced this pull request Nov 9, 2026
… per file (#2551)

On macOS the presence watch handed each held chat's session folder to fsnotify, whose kqueue backend opens a descriptor for every file in a watched folder. A darwin-only watcher now holds one handle per folder, plus one for answers.jsonl only while an answer waits to be drained (since #2478 a quiet session relies on the watch for answer appends). Other platforms keep fsnotify behind the same seam. Holds, viewOpen and the idle sweep are unchanged.

Mac proof (macOS 15.6): engine fds per held chat +6 -> +2; takeover reaction unchanged (1-3 ms); 20 darwin and presence tests pass 3/3. Fixes most of #2471.

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.

2 participants