Repository navigation
Factory: any-source intake, router and AI filter (draft, e2e first) - #2478
Merged
Merged
Conversation
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
…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
marked this pull request as ready for review
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
The design and acceptance criteria are in
docs/design/factory-general/DESIGN.md.Lanes merged into this branch
inbox.Addis the single door for outside work. It is reached fromcodeaf factory add,POST /api/intakeandfactory.intake_add. It also providesFeed/Pollfor connectors andNotifyto send write-backs.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_testpreviews what the rules would match.factory/gi-judge):when.askasks 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.factory/gi-jira): a Jira Cloud feed selected by JQL, a replier that comments the PR link back on the ticket, andjirastubfor tests.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/v3by merging, not rebasing, because the lanes are built on top of it.🤖 Generated with Claude Code