implementation-harness
AGENTS.md1 starsChanged 34 days agoMITView on GitHub
Two things in one repo: There is no desktop app. The console only runs from the checkout through impl, so a merged self-improvement lands in the code that serves it (plugin files on the next run, console/ and bin/ after impl restart, which the merge notice asks for, or which the console runs itself once idle when the automatic merge is on). bin/implementation-factory is the impl launcher (start/demo/restart/stop/status/config/improve). It starts the server in the background (nohup, output in <data dir>/server.log)…
- Reads credentials
- Installs packages
- Commits and pushes
1 other project uses a version of this.
Adopted by
Most recently taken up by
- Greg-Klein/implementation-factoryword for word, first seen Nov 1, 2026
Other versions1 project uses a version of this.
Word for word · 1
- Greg-Klein/implementation-factory★ 1100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository13 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- clarify-specSkill
- collect-evidenceSkill
- document-changeSkill
- figma-reviewSkill
- gh-github-apiSkill
- gitlab-ticketsSkill
- glab-gitlab-apiSkill
- howSkill
- review-changeSkill
- self-checkSkill
- unslopSkill
- whySkill
The file
We split this AGENTS.md into the rules it gives an agent
5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
All from console/ (Node >= 22.12):
- Very specific: line 35
Before a push: npm run verify once, on the tree that is pushed.
- Distinctive: line 36
Run these yourself and report their real output, including when a subagent already said the work passes.
- Very specific: line 37
After a change under console/ or bin/, run npm run build in console/, then impl restart.
- Very specific: line 96
Every agents/*.md, commands/*.md and skills/*/SKILL.md needs YAML frontmatter with name and description, and a skill's name is its …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
This file provides guidance to coding agents working with code in this repository.
What this is
Two things in one repo:
- A Claude Code plugin (
.claude-plugin/,commands/,agents/,hooks/,skills/)./implementation-factory:implementdrives a GitLab or GitHub ticket end to end (clarify, plan, implement, test, specialized reviews, MR or PR). The ticket URL decides the forge:glaband theglab-gitlab-apiskill for GitLab,ghand thegh-github-apiskill for GitHub, where the ticket status is never moved and review captures are attached withgh --attachwhen the installedghhas it (2.99.0 or later), kept local otherwise.improveandrebaserun the self-improvement loop on this repo itself, andjudge-improvementis run headless by the console to decide whether a finished improvement branch is merged without the user;reviewis the review step.scheduleis run headless by the console to compare the tickets of a batch (agentticket-scheduler, files defined incontracts/schedule.md). - A local console (
console/): a Next.js UI plus a Node server that spawns the realclaudebinary in a PTY (claude --plugin-dir <this repo> "/implementation-factory:implement <ticket>"), inside a git worktree of the target repository created for that run, and makes its progress, agents, questions and documents visible. No direct Anthropic API calls, no API key.
There is no desktop app. The console only runs from the checkout through impl, so a merged self-improvement lands in the code that serves it (plugin files on the next run, console/ and bin/ after impl restart, which the merge notice asks for, or which the console runs itself once idle when the automatic merge is on).
bin/implementation-factory is the impl launcher (start/demo/restart/stop/status/config/improve). It starts the server in the background (nohup, output in <data dir>/server.log) and returns once /api/runs answers. Every other command it accepts (client_commands: run, runs, show, watch, attach, answer, ...) is handed to the terminal client, console/cli/index.ts run with tsx from the caller's directory, with IMPL_CONSOLE_URL (the local server unless the shell names another) and IMPL_PROGRAM. Only run starts a server when none listens, without the browser; a reading command never does. bin/config.mjs + env-schema.mjs + env-file.mjs implement impl config over a local .env (the shell environment wins over .env, which wins over the defaults, see .env.example).
Commands
All from console/ (Node >= 22.12):
npm install # node-pty is native, needs build tools
npm run dev # tsx server/index.ts, http://127.0.0.1:3210
npm run typecheck # tsc --noEmit, then hooks/*.mjs, bin/*.mjs and scripts/*.mjs (tsconfig.scripts.json, checkJs)
npm run test:unit # Jest (swc), tests/unit/*.test.ts
npx jest tests/unit/hooks.test.ts -t "should ..." # single unit test
npm run test:integration # Playwright, starts an isolated dev server on :3211 (500ms demo steps, no local .env, temp data dir, tests/fake-claude on PATH)
npx playwright test tests/integration/questions.spec.ts # single spec
npm run build # next build --webpack
npm run verify # typecheck, unit, build, integration: the CI sequence in one commandCI (.github/workflows/ci.yml) runs typecheck, unit, build, integration. npm run verify takes about five and a half minutes, nearly all of it in the integration suite, so it is run once per push and not once per commit:
- While working, and before a commit:
npm run typecheckand the unit tests the change concerns, the whole unit suite (30 s) when in doubt. - Before a push:
npm run verifyonce, on the tree that is pushed. When the diff only touches documentation, prompts (agents/,commands/,skills/,contracts/,principles/) orhooks/, the unit suite is enough: it is what reads them, and the integration suite does not. - Run these yourself and report their real output, including when a subagent already said the work passes.
- After a change under
console/orbin/, runnpm run buildinconsole/, thenimpl restart. The restart does not build: the server runsserver/throughtsxbut serves the interface from.next/, so a change undercomponents/,lib/orapp/stays invisible until it is built.
impl demo (or the demo mode in the UI) replays a simulated run from server/demo.ts / demo-data.ts without touching any repo or GitLab. Integration tests lean on it. /?demo=incident plays a pilot that hands back with nothing next, /?demo=batch a batch of three invented tickets with one conflict (DEMO_BATCH, canned analysis, merge request merged by a timer).
Architecture
Flow: a hook fires in the Claude Code session (PTY). hooks/hooks.json runs hooks/emit.mjs, which POSTs the event to the local server with IMPL_RUN_ID. server/engine translates the raw hook payload into EngineEvents, server/hooks.ts applies them to the RunSession the hook names, and the WebSocket sends the new state to the React UI (components/factory.tsx shell, run-rail.tsx sidebar, run-view.tsx and panels).
Several runs go at once. Everything a run owns lives on its RunSession, not in module state. What stays at module level belongs to the factory: the runs already audited and the audit queue in self-improvement.ts, the fake pending improvement of the demo in demo.ts. Every client message and HTTP route that acts on a run names it by runId.
server/engine/is the only code that knows the driven agent is Claude Code: binary, hook vocabulary, transcript JSONL format, how to submit input,.claude/taskspath,hookSpecificOutput, and the headless scheduling call.startSchedulespawnsclaude -p --setting-sources project,local --model sonnet --permission-mode dontAsk(the user's settings stay out, a personal hook that rewrites a command would make it a refused one) with a fixed--allowedToolslist on/implementation-factory:schedule <input> <output>, in the main checkout, without the hook variables and withIMPL_SCHEDULE_OUTPUTnaming its output file (the list matches a command by its first words, the guard decides what each call may do), killed aftertimeoutMs; it returns{ finished, kill }and reports nothing while it runs. Every session that reads text the factory did not write, the pilot's and the background ones, starts withWebFetchandWebSearchdenied (DENIED_WEB_TOOLS,--disallowedTools): a fetched URL is a way out for a secret, and the prompts saying "never WebFetch" is not a rule. Nothing above the engine may importnode-ptyor readhook_event_name, and nothing does. The boundary is not complete for the rest: thehookSpecificOutputshape is typed inserver/types.ts(HookOutput),domain.tstests the tool nameBashand spells.claude/worktrees, the default ofIMPL_WORKTREE_COPY_FILESnames.claude/settings.local.json, and several messages write "Claude Code" whereengine.labelexists. Contract inengine/types.ts, rationale and the list of these leaks inengine/README.md. Keep new agent-specific logic in the engine.- Guard:
hooks/guard.mjs(guardDecision), called first byemit.mjs, refuses nine kinds ofPreToolUsecall during a run of the workflow (IMPL_RUN_IDset orworkflow-state.jsonpresent): a workflow agent under its bare name, adevelopergiven a model override other thansonnet(its declared model) oropus(passed by the pilot for a task the plan sizesL), adeveloperstarted whileplanner-output.jsonlacks a complete integration or end-to-end decision (e2ePolicyProblem:test_strategy.integration_policyandtest_strategy.e2e_policypercontracts/planner.md, whose fields a unit test holds equal toE2E_POLICY_FIELDS; whenexpected, a source, acommandand a task whoseverification_stepsrun it), a reviewer started while a plan task has no developer report, a reviewer or a merge-request publication below the tier the diff requires (reviewTierFloorinhooks/review-tier.mjs: at least 1 onIMPL_SENSITIVE_PATHS, at least 2 on CI or a migration, fromworkflow-state.json'sreviewTierandbaseBranch; tier 2 is not published withoutreview-orchestratorhaving started (SubagentStart, noted bynoteReviewStartoutside the checkout, keyed on the run, the session and the task directory) andreview-summary.mdwritten after that start withREADYorBLOCKEDalone under## Verdict, tier 1 withoutqa-revieweror the orchestrator and the verdict ofqa-report.mdor of that summary; a verdict that is not ready is published only as a draft;deliveryCommandfindsgh pr create/new,glab mr create/new,git push -o merge_request.createand agh api/glab apiPOST, explicit or implied by a field, topullsormerge_requests; a diff that cannot be read lets the call through), agit commitor aglaborghpublication carrying a session trailer or link, a publication (those, plus aglab apiorgh apicall that writes) whose command or a text file it sends shows a credential (SECRETS: token shapes, JWT, private key, bearer; the refusal names the kind and the line, never the value), or agit pushone of whose commits the remote lacks adds one (pushedSecret:git log -pof the refspecs' sources, the branch checked out when none is named,--not --remotes, added lines only, in the directory-Cnames; the refusal names the kind, the file and the commit), a command that reaches a host outside the run's forge (IMPL_FORGE_HOSTS, set by the registry at launch fromrunForgeHostsindomain.ts: the ticket's host,api.beside a GitHub one, and the hosts of the checkout's.git/config,gitRemoteHosts;gitRoutingRefusal:git fetch/pull/push/ls-remote/clonenaming another host, typed or behind a remote read withgit remote get-url --alland--push, every remote when none is named,-crouting keys,GIT_SSH_COMMAND,GIT_PROXY_COMMANDorGIT_CONFIG_*anywhere on the line, a pack program,git remote add|set-urlorgit configpointing elsewhere, and a file tool on a gitconfig;forgeCliRefusal:glab/ghwith another host in--hostname,-R, an address argument outside a free-text option or a host variable, plus theirconfig setandauth login; without the variable the host checks let everything through), and a git command that destroys work (reset --hard,clean -f, a whole-tree checkout or restore, a bare forced push,worktree prune, the removal of a run worktree, plus a stash and a deleted or overwritten branch in a linked worktree). The git rules read the commands typed on the line (shellCommands), not what a quoted string or a script runs; git is found anywhere among the words of a command (behindthen,sudo,xargs, an absolute path), a redirection glued to an argument is dropped, and a path that is the current directory, one above it or a pattern matching everything counts as the whole tree. The headless judge of an improvement branch has its own too (judgeRefusal, applied whenIMPL_JUDGE_OUTPUTis set):Writeto its verdict file only, no agent, no command. The headless scheduling session is not a run and has rules of its own (scheduleRefusal, applied whenIMPL_SCHEDULE_OUTPUTis set, whichstartScheduledoes):Writeto its output file only, no other agent thanticket-scheduler,glabandghonly asissue viewor anapicall naming one issue or its links with no method, field or input option,rmon its output file only. The self-improvement and rebase sessions have noIMPL_RUN_IDand are not guarded. A refused call is not forwarded to the console, and a guard that cannot read what it checks lets the call through. A rule a tool input or a file listing decides goes there rather than in a prompt (commands/improve.md). - Stop gate:
hooks/gate.mjs, called byemit.mjsafter the guard.gateObservenotes, peragent_id, the files each agent passes toEdit,Write,MultiEditorNotebookEditand which gated agents are alive, and fingerprints the changed paths of the tree when a gated agent starts alone (git statusplushash-object); at its stop, the paths whose content moved since are its edits too (treeChanges), since agents mostly edit through the shell. No fingerprint is kept while a peer works or once one starts, and a commit made in between voids it (state in the system's temporary directory, per session,IMPL_GATE_STATE_DIRin tests).gateStopjudges theSubagentStopofdeveloperandsenior-reviewerduring a run: per package the files belong to, the type-check of the package, then the lint and the related tests of those files (gateSteps, Node packages only; a browser spec, by its path (isEndToEndSpec) or by importing@playwright/testorcypress(drivesBrowser), is linted but never handed to the unit runner); documentation and YAML files open no check. A failure sends the agent back once (decision: block, the stop is then not forwarded to the console); the second stop always goes through, and so does the stop of an agent whose transcript ends on aSubagentHandbackcall, which would not resume (its lines carryhandedBack: true). That transcript is not always written when the stop fires, so at each pilot turn end the console closes an agent still counted running whose transcript ends on that call with no message written to it since, such as the pilot resuming it (settleHandedBackAgentsinserver/hooks.ts,engine.agentHandedBack). Every verdict is a line of.claude/tasks/gate-log.jsonl(pass,fail,inconclusivefor type errors outside the agent's files while a peer edits,skippedfor a check that could not run,none), which the pilot and the review orchestrator read instead of the report; a check that was started carries its duration inms. It fails open, andIMPL_STOP_GATE=offdisables it. Rationale indocs/engineering-workflow.md, "The stop gate". - Blocking questions: the
PreToolUsehook onAskUserQuestion(timeout 3600s) waits for the UI answer, which returns asupdatedInput. Claude Code replays the hook with answers; the engine ignores that second pass. - Folder trust dialog: it is drawn before any hook or transcript, so
engine/trust-prompt.tsreads it off the PTY output (matcher on the text with escapes and blanks removed, sample of Claude Code 2.1.288 intests/unit/fixtures/trust-dialog.json) and the engine emitssession.prompt/session.prompt.endthroughStartOptions.onEvent.server/session-prompt.tskeeps it asRunState.sessionPrompt, a state of its own and not the question bridge: shown as a decision in the conversation, answered withsessionPrompt.answer(the engine types the keys,EngineSession.answerPrompt), dropped on the first hook, when the screen moves on, or at exit. Never answered automatically. The directory it names is the session'scwd, so the run worktree, not the checkout. A refusal ends the runstoppedwith its reason (RunSession.endedBy), notfailedwith alost_sessionincident, and its worktree is settled bysettleRunWorktreelike that of any stopped run (kept with its reasons). The headless scheduling session has no terminal and is not watched for the dialog. An unrecognised wording shows nothing. server/access.ts+server/hook-bridge.ts: every HTTP request needs aHostnaming the console, the WebSocket anOriginit served,/api/hooksthe secret carried inIMPL_HOOK_URL, drawn at each start unlessIMPL_HOOK_TOKENsets it (the integration suite does, being the only caller that posts hooks without a session; set in a.env, it stops changing from one start to the next).emit.mjsretries once, then spools the event todata/runs/<id>/hooks-spool.jsonl(IMPL_HOOK_SPOOL), replayed before the next hook and on every monitor tick;hookIddedupes. Questions are never retried nor spooled.- Run health:
server/run-health.ts(pureevaluateRunHealth, injected clock and policy) readsRunSession.signals(pilot turn end, tool calls paired bytoolUseId, subagent vs pilot, background calls, notification causes) andworkflow-state.json(server/workflow-state.ts),server/run-incidents.tskeeps incidents (fingerprint dedupe, resolution on the lifting event, action checks, archive normalisation),server/run-monitor.tsis the single scheduler the registry owns (tick +session.signal(), serialised per run with incident actions). A wait on the user is dated from its question (PendingQuestion.askedAt,HealthInput.waitingSince), not from the evaluation. Silence alone is onlysuspected_stall; a mid-workflowturn.endno longer setsattentionby itself, and an exit before a result isfailedwith alost_sessionincident, unless it comes within 5 s of a Ctrl-C the user typed in the run's terminal (registry.terminalInput,exitFollowsInterrupt), which ends itstoppedlikerun.stop. An agent that ends without a file its contract requires, with nobody taking over, raises an incident:planner-output.jsonforticket-planner,qa-report.md,qa-evidence.jsonandqa-plan.mdforqa-reviewer,designer-review.md,design-evidence.jsonanddesign-inventory.mdfordesigner-reviewer,review-summary.mdforreview-orchestrator(PRODUCER_CONTRACTS). A subagent whose stop does not follow itsSubagentHandbackcall (handedBack: falseonagent.stop, read by the engine off the end of its transcript) after it launched a background call of its own has not ended: it waits on that work and resumes, so it raises no such incident and keeps the run open (RunSignals.agentsAwaitingBackground) until its next start, a stop that hands back, or a kill. Actions go throughincident.action(revision + requestId, decision persisted before the effect). Runs left with an open incident or a kept worktree are read back byserver/run-archive.tsand served on/api/archive/*, never registered. server/domain.ts: pure logic, no FS/agent. Prefer putting logic here or inengine/so it is unit-testable.- Review order:
RunSession.artifactArrivedrecords the first time each document is seen inRunState.artifactArrivals(called fromserver/artifacts.ts).reviewPlanNotesindomain.tsturns these times intoRunState.reviewNotes, one note whenqa-plan.mddid not arrive beforeqa-report.md, ordesign-inventory.mdbeforedesigner-review.md. A note is a remark shown in the evidence tab and changes no verdict.phaseForArtifactignores both plan files, so they never open the next step. - Phase: the step the pilot declares in
workflow-state.jsonis the phase (declaredPhaseindomain.ts, applied inreadWorkflowStatethroughphaseOfWorkflow, never going back except during a reopening, see Run metrics). What a document, an agent start or a command points at (phaseForArtifact,phaseForAgent) goes throughRunSession.inferPhaseand movesRunState.phaseonly while no step is declared: a document says which step wrote it, not which one is running.RunSession.inferredPhasekeeps that reading forcloseWorkflowIfDone, which still ends a run that stopped declaring. A step the pilot skips or never declares has no entry inphaseArrivals. - Pause: a workflow that declares
paused(a wait the user asked for, such as another ticket's merge,contracts/workflow-state.md) puts a run in progress whose session is open in the run statuspaused(pauseTransitionindomain.ts, applied inreadWorkflowState). A paused run takes no slot (the queue is drained), keeps its ticket, still holds the conflicting tickets of its repository (runHoldsConflicts), is healthy by definition, opens no self-audit and is never closed for a queued launch (sessionsToReleaseForQueue); its hooks are dropped like those of an ended run. The next declared state other thanpausedputs it back torunning, the pilot counted as at work. Its session's exit or a shutdown ends itstopped(completedduring a reopening), and an archive read back never shows it paused. The page andimpl watchsay "Paused". server/run-session.ts: one run and everything it owns (state, archive, engine session, terminal buffer, artifact and transcript watchers, pending-question bridge, demo timers), withactivity()/publish().persist()writesrun.jsonone write at a time: what changed during a write is one more write of the state as it then stands, and the promise holds until nothing is left to write.registry.publishSnapshot()sends the list of runs only when it differs from the last one sent.server/registry.ts: every run the console holds plus the launch queue (data/queue.json, restored on boot). A launch is resolved to the main checkout first (mainCheckout: a path inside a linked worktree names the same repository). Enforces one run per (repository, ticket) (runLockKey, onticketIdentity: one identity for the/-/issues/and/-/work_items/addresses of a ticket, whatever the case of the host and the project, without query or fragment; held whilerunHoldsRepository: in progress or session still open) andIMPL_MAX_CONCURRENT_RUNS, counted on the runs whose workflow still works (runTakesSlot): a finished run frees its slot at once, its open session keeps only the ticket, and is closed when a launch waits on that ticket (sessionsToReleaseForQueue). A launch that hits either is queued with its reason, and the queue is drained (drain) whenever a run lets go, an analysis answers, a merge request is merged or the user acts on the queue. Several tickets of one repository run in parallel, unless the schedule below keeps them apart. The registry also owns the schedule state (tickets,edges,watches), the per-repository analysis chains and theMergeWatcher.startcreates the run worktree before the session; a failedgit worktree addaborts the launch, never a fallback on the checkout.- Batch intake: the launch form sends
batch.submitas soon as its ticket field holds two URLs or more (lib/ticket-urls.ts,parseTicketUrls, shared by the form and the server).server/ticket-source.ts(resolvePastedTickets) is the only code that knows the batch was pasted, and the place a GitLab query source would go: it resolves every URL to its main checkout, or to the checkouts the page chose for it intargets, and refuses the whole batch when one URL is invalid. A ticket with no checkout and no target queues nothing: the server answersbatch.unresolvedand the form asks where its merge requests go (components/target-picker.tsx). Cross-project delivery: a ticket filed in a project without code goes to the checkouts the user chose, one run per repository (resolveTargets), for a single ticket too (one repository throughrun.start, several throughbatch.submit);deliveryProjectsindomain.tsdecides when the session getsIMPL_DELIVERY_PROJECTS(the run's project is not the ticket's, or there are several), which makes the merge request cite the full reference and, with several projects, close nothing.registry.enqueueBatchtakesResolvedTicket[], drops the tickets already queued, held by a run or behind a watched merge request (admitBatch, same key asrunLockKey), and queues the rest under onebatchId. - Ticket proposals:
server/ticket-proposals.ts(TicketProposals, owned by the registry) reads the file an outside watcher keeps up to date,data/ticket-proposals.jsonorIMPL_TICKET_PROPOSALS_FILE, everyIMPL_PROPOSALS_POLL_MS(default 5 s, not inimpl config), format incontracts/ticket-proposals.md. The file is a snapshot of every ticket that matches the watcher's filter. The console never asks GitLab for tickets and the watcher never calls the console; with no file nothing is queued and nothing else changes.readProposalSnapshot,openProposalsandhandledStillFoundindomain.tsare the pure part: a ticket the console already has (compared by address, a proposal has no checkout) or already handled is left alone, and a decision is forgotten once its ticket leaves the file (data/ticket-proposals-handled.json; when that file cannot be read it is renamed*.unreadable-<time>and the tickets listed at the first reading are taken as handled, none is queued). After every reading, the new tickets go toregistry.launchProposalswith no click:resolveProposedTicketsresolves each one on its own andenqueueBatchqueues the resolved ones as one batch, then they are recorded as handled, so a ticket cancelled from the queue is not queued again while it stays in the file. One that cannot be launched keeps its reason in memory (TicketProposal.refusal) and is not tried again until it leaves the file or the console restarts; nothing is launched nor refused while shutting down.FactorySnapshot.proposalscarries the tickets not queued,components/proposal-list.tsxshows them under the queue with their reason, andproposal.dismissrecords the decision, andproposal.launch(issueUrl,repositories) queues a refused ticket on the checkouts the user chose. An entry may carryrepositories(project paths,projectPathindomain.ts), resolved throughcheckoutOfProject. ThebaseBrancha proposal may carry (branchNameindomain.tsdrops anything that is not a plain branch name) follows it asQueuedRun.baseBranchandRunState.ticketBaseBranchand reaches the session asIMPL_TICKET_BASE_BRANCH, which a stacked start overrides; the pilot then skips the base question when that branch exists. - Batch analysis:
server/schedule-analysis.ts(analyseTickets) writesinput.jsonunder<scheduleRoot>/<id>/, callsengine.startScheduleand judges the session byoutput.jsonalone, validated whole byvalidateScheduleagainstcontracts/schedule.md. One session per repository per batch, serialised per repository (analysisChains), outsideIMPL_MAX_CONCURRENT_RUNS, bounded byIMPL_SCHEDULE_TIMEOUT_MINUTES(default 5). Entries areanalysinguntil it answers. No session for a single new ticket in a repository with no known prediction (see Forge links); a lonerun.startbeside known predictions is analysed too. A missing or refused file, a timeout, or a restart during the analysis (INTERRUPTED_ANALYSIS) records the tickets asanalysis: "failed". The next analysis of the repository sends the tickets still queued, running or awaiting merge whose analysis failed as tickets to predict, not asknown(failedTickets), and replaces their stored prediction; if it fails too they stayfailed, and no analysis is opened for them alone.scheduleRoot(scheduleDirectoryindomain.ts) isimplementation-factory-<user>/schedule/in the system's temporary directory, private to the user, when the data directory sits inside the plugin, because Claude Code refuses a session any write inside the directory of the plugin it loaded; a data directory outside the plugin (IMPL_DATA_DIR) keeps them underschedule/. The directory is removed once the answer is read, kept withsession.logon a failure, andscheduleRootis wiped on boot. - Forge links: beside the session,
forgeEdgesin the registry reads the blocking links of every ticket being analysed (fetchIssueLinksinserver/ticket.ts: oneglab api .../issues/<iid>/linkscall, or the twogh api .../dependencies/blocked_by|blockingones, no Claude session).linkEdgesindomain.tsturns a link to another ticket the console has in the same repository (repositoryTickets: queued, held by a run or awaiting merge) into adepends_onedge, matched by host, project and number since GitLab writes one issue as/-/issues/and/-/work_items/, andoverlayEdgesputs it over the agent's edge for that pair. It is recorded when the session failed too. A single new ticket beside tickets with no prediction gets this reading alone: insidelaunch, before the run may start, sorun.startstill answers with the run, and for a batch of one throughscheduleAnalysiswithpredictedfalse, shown asanalysingwhile it lasts. Unreadable links give no edge. A link to a ticket the console does not have, or of another repository, holds nothing. - Scheduler: pure, in
domain.ts.conflictBetweenkeeps two tickets of one repository apart on an edge (overlap,depends_on), and a ticket whose analysis failed or whose confidence islowapart from every other ticket of its repository (analysis_failed,low_confidence); tickets of two repositories never conflict.detailis written for the ticket that waits: it says whether the failed analysis or the vague prediction is its own or the other ticket's.describeQueuegives each entry its reason, first match wins:ticket(a run on the same ticket),slotfor a forced entry,analysis,conflict(a conflicting run in progress),mergeormerge_unknown(the watched merge request of a finished one),dependencyororder(a conflicting entry ahead in the queue), elseslot.startableEntriesreturns theslotentries, up to the free slots; a held entry takes no slot and is stepped over.priorityOrderputs the first ticket of adepends_onedge ahead of the second. - Merge watch: when a scheduled run ends with a merge request,
runEndedrecords aMergeWatchand the conflicting entries wait for the merge.server/merge-watch.ts(MergeWatcher) polls only the watches that hold a queued entry (heldWatches), everyIMPL_MERGE_POLL_MS(default 60 s, not inimpl config), and has no timer otherwise. The check isfetchMergeRequestStatusinserver/ticket.ts: oneglab apicall made by the Node server, orgh apifor a pull request (deliveryEndpointanddeliveryStatusindomain.ts: GitHub says a merge withmerged, itsstatebeing onlyopenorclosed). It opens no Claude session and costs no tokens.mergeWatchStep:mergedorcloseddrops the watch and releases what it held,openedkeeps holding, and a failed call isunknown, which holds too. A run that ends without a merge request releases its dependents at once. A watch nothing waits on is forgotten after a week. - Queue overrides:
queue.forcesetsQueuedRun.forced, after which only the ticket lock and the slots hold the entry. Modebasestarts from the base branch. Modestackedneeds the branch of the blocking ticket (from its run or its watch) and passes it to the session asIMPL_BASE_BRANCH.queue.movereorders (a dependency still goes first),queue.cancelremoves. - Run worktree:
server/run-worktrees.tsowns the lifecycle,server/worktree.tsthe git calls,domain.tsthe pure decisions (runWorktreePath,isRunWorktreePath,worktreeProvisioning,worktreeRemoval). Every ticket run works in<repo>/.claude/worktrees/<run-id>, detached at the checkout's HEAD, the directory excluded through the repository's.git/info/exclude.provisionWorktreecopies the ignored dependency directories copy-on-write (IMPL_WORKTREE_DEPENDENCY_DIRS, defaultnode_modules, matched by name at any depth), symlinks one when the copy fails, copies ignored config files (IMPL_WORKTREE_COPY_FILES, default.env*,.claude/settings.local.json), and copies the ignored entries at or under a relativecore.hooksPath(husky's.husky/_, without which the first commit fails); a provisioning failure is only a warning in the activity feed. The session getsIMPL_RUN_WORKTREE,IMPL_SOURCE_REPOSITORY,IMPL_SOURCE_BRANCH(omitted on a detached HEAD) andIMPL_WORKTREE_DEPENDENCIES(symlinkas soon as one directory is linked, elseclone, omitted when none was brought over).staleDependencies(inrun-worktrees.ts, pure partlockedPackagesanddependencyDrift) compares eachnode_modulesbrought over with thepackage-lock.jsonbeside it (top-level packages, optional ones and links aside); a mismatch is anattentionactivity andIMPL_WORKTREE_DEPENDENCIES_STALEfor the session, whose pilot reinstalls before the first agent, and an unreadable lockfile is reported, never guessed.RunState.cwdis the worktree andRunState.repositorythe checkout (sourceRepositoryreads older archives), so.claude/tasksis the worktree's own: cleared, watched and snapshotted there, never in the checkout. Once the session is gone,settleRunWorktreeremoves the worktree only when the run iscompletedwith a merge request that is not a draft, the workflow not blocked, the archive sync acknowledged (archiveSyncedAt), the tree clean and HEAD on a remote-tracking ref (local refs, no fetch). OtherwiseRunState.worktreeturnskeptwith the reasons indetail. A shutdown marks itkept, andreconcileRunWorktreesapplies the same rules on boot, before the archive is listed.removeWorktreeOnRequestis refused while the session lives and answersconfirmwith therisks(uncommitted, unpushed) until the request comes back withforce. Only a path directly under.claude/worktrees/is ever removed, the branch never. The demo has no worktree. server/context.ts: the open sockets with the run each one is subscribed to,broadcast()(list of runs, notices) vsbroadcastToViewers()(one run's state and terminal).reportFailure(what, subject)is what a write or a reading nobody awaits ends with: a line inserver.logeach time, with the stack when there is one, and one notice per kind of failure every ten minutes. The process has its ownunhandledRejectionanduncaughtExceptionhandlers, which report and keep the console up.isMissingFiletells an absent file from an unreadable one: an unreadablequeue.jsonorreview-findings.jsonis renamed*.unreadable-<time>before the console starts from nothing. Every HTTP request goes throughroute, whose failure is answered 500 (400 for an address that does not decode); a hook the console fails to apply is answered 500, whichemit.mjsretries then spools. On boot, non-terminal archived runs are reclassifiedfailed, except one reopened after its final report, closedcompleted.- WebSocket protocol, from the page:
run.start,run.subscribe,run.stop,run.close,terminal.input,terminal.resize,instruction.send,question.answer,sessionPrompt.answer,feedback.submit,incident.action,selfImprovement.approve,selfImprovement.rejectandselfImprovement.revert(worktreeName),language.set(language), plus the ones detailed below. From the server:factory(summaries + queue + archived runs + watcher tickets not queued + the current language, to every page),runandterminal.output(to the pages that opened that run viarun.subscribe),notice(factory-level events; those of the improvement loop, sent bynotice()inself-improvement.ts, carrydismissAfterMsand close on their own after 5 s; any otherinfonotice closes after 10 s, anattentionone stays until closed,noticeDismissDelayinlib/run-state.ts),error,incident.result,worktree.resultandbatch.result(answered to the page that asked). The client sendsworktree.remove(runId, optionalforce) for a live or archived run and getsworktree.resultback (removed,confirmwithrisks, orrefused). It sendsbatch.submit(issueUrls, optionalinstructionapplied to every ticket) and getsbatch.resultback (batchId,accepted,duplicates), thenqueue.force(queuedId,modebaseorstacked, optionalonto),queue.move(queuedId,before,nullfor the end) andqueue.cancel, which answer through the nextfactorysnapshot.proposal.dismiss(issueUrls) andproposal.launch(issueUrl,repositories) answer the same way, a failure througherror.batch.submitmay carrytargetsand is answeredbatch.unresolved(tickets) when a ticket has neither a checkout nor a target.demo.starttakesscenarioworkflow,incidentorbatch.run.startanddemo.startmay carry arequestId, which comes back inrun.started(runId, sent before the run's state), in the notice of a queued launch or inerror: the page opens the run its own launch created (lib/launch.ts), not the next one it hears about. A message that cannot be read, or of an unknown type, is answered witherror. Any message may carry anackId: the server answersackwith it once the message was handled, after whatever the message answers on its own, or puts it in theerrorthat refuses it. The terminal client sends every action with one, which is how a command with no answer of its own ends on a real outcome. It sendsrecipe.forget(repository) and getsrecipe.resultback (forgotten), andfindings.forgetandfindings.resultthe same way. It sendsfindings.acceptandfindings.dismiss(repository,category) and getsfindings.fixback (outcome,targetFile,applied,message). HTTP:/api/runs,/api/runs/<id>,/api/runs/<id>/acceptance,/api/repositories?issueUrl=,/api/self-improvement/pending,/api/self-improvement/diff?worktree=,/api/self-improvement/report?worktree=,/api/metrics,/api/repositories/recipe?repository=,/api/repositories/findings?repository=,/api/artifacts?runId=&path=(a.webmor.mp4path answers the bytes with their type, for the evidence tab's player, every other document a JSON preview), and read-only/api/archive/runs/<id>,/api/archive/runs/<id>/acceptance,/api/archive/artifacts. - "Tracking" board:
planner-output.jsonis parsed when archived (plannedTasks), the engine reads the plan task id out of a developer delegation (thedeveloper-report-<id>.mdsuffix in theAgentprompt),delegatedTasksdrops the ids whose report already exists (a brief cites earlier reports as reading material),pairDelegationbinds it FIFO to the nextSubagentStartof that type, andplanTaskBoardderives each card's column and assignee (report present = done). Agents get a first name and picture in start order (agentIdentity, pictures inpublic/avatars/) and a role (agentRole). - Acceptance evidence: the pilot writes
acceptance-criteria.json(stableAC<n>ids), tasks carrycriterion_ids, evidence files followcontracts/evidence.md.server/acceptance.ts(pure) derives each criterion's status (failed wins over blocked, then unverified, then verified, and stale, unversioned or unreplaced evidence never counts as verified) and renders the MR summary. Afailis never read as a confirmation, whatever it cites, and a confirmation counts only on a check the evidence it names speaks to. A QA item withkind: "attempt"is a break attempt: a failing one counts against the criterion it cites, any other is listed under it (AcceptanceCriterionView.attempts) and verifies nothing.qaVerdictConsistencycompares thestatusof the latest QA report with QA's own executed observations on the current code and returnsAcceptanceView.qa, with a warning whenPASSorPASS_WITH_WARNINGSis declared over a criterion QA did not observe; a focused pass answers only for the criteria in its rootmandate(an empty list names none, a mandate naming only unknown criteria is read as absent and every criterion is checked). The warning also goes into the MR summary (qaWarning). A live QA file whosestatusis missing or not one of the four tokens is anerrordiagnostic, so it is listed among the anomalies ofacceptance-summary.mdinstead of skipping the check in silence;server/evidence-archive.tskeeps every version and its captures underdata/runs/<id>/evidence/, with the code snapshots in its index (settleSnapshot) so an archive read back after a restart derives the coverage the live run showed;server/acceptance-runtime.tsingests, identifies the code withhooks/code-snapshot.mjs(the same utility the workflow runs) until the workflow's end is archived (deliveredCodeSettledindomain.ts: a rebase or an edit made after delivery, outside a reopening, leaves the delivered coverage as it was), writesacceptance-summary.mdback to the task directory and answers the pre-cleanuparchive-sync-request.json. The full view is/api/runs/<id>/acceptance;RunState.acceptanceandRunSummary.acceptancecarry figures only, including the QA verdict digest (acceptance.qa: status,consistent, number of unobserved criteria). - Review confidence:
server/review-confidence.ts(pure) gives a reviewed run a note from 0 to 5,RunState.confidence(score,reasons), absent until the merge request is open andsenior-review.mdorqa-report.mdis archived.CONFIDENCE_RULESis the table: a rule holds the note at a cap or takes half points off, and the note is the lowest cap or what the deductions leave, rounded down. What an agent declares (QA verdict, finding severity, blocked workflow) only lowers it, and a fact that could not be read counts against it.changeFactsreads the diff withchangedFiles,addedLinesandisTestFileofauto-merge-policy.ts(size,IMPL_SENSITIVE_PATHSthroughpathMatches, a sensitive path holding the note at 3, CI and migration paths included even when the setting omits them, tests removed or disabled, code without a test),gateFactsthe last verdict of each check in the kept gate log, and a finding ofsenior-findings.jsonis open only in the last review round, uncorrected and with no rework after it.server/review-confidence-runtime.tsdoes the readings, kept on theRunSessionand made again only when their source moved (changeReadingKey: code snapshot id, merge request address and its target branch; size of the gate log; one forge call per merge request address and never fromrefreshAcceptancealone, which aGETcalls), a failed reading of the diff leaving it unknown unless the worktree is gone (changeAfterReading).settleConfidenceinacceptance-runtime.tsapplies it insiderefreshAcceptance;refreshConfidenceis called when a run document arrives, when the gate log is kept, when a hook sees the merge request opened (rememberMergeRequestinhooks.ts, so the summary the step 9 comment quotes is measured on the merge request's diff) and before the metrics of an ended run. An incident transition (applyHealth, a dismissal throughincident.action, the closing of a run) callssettleIncidentConfidencefirst, which revises theincident_openrule from the reasons the note already carries, with no reading, thenrefreshConfidenceon a live run; an archived run gets the first only. The summary is written again whenever the note differs from the one it was last written with (RunSession.summaryConfidenceKey), not when the state moved, since that revision moves the state before the refresh runs.renderAcceptanceSummarywrites the note and its reasons in a section of their own, which the pilot puts at the top of the review comment and of a follow-up comment, never in the description, in the workflow language (acceptance-text.ts, by rule id, never from the stored Englishdetail).confidenceAtDeliverykeeps the note of the firstcompletedand is never rewritten;confidenceCalibrationinrun-metrics.tscounts per note the delivered runs reopened or given feedback (RunState.feedbackCount), served ascalibrationby/api/metrics.normalizeArchivedRunkeeps the three fields through a restart, each validated bystoredConfidence, so an archived run still shows its note and figures written again bybackfillRunMetricsstill count in the calibration. The note changes no behaviour of the workflow. A unit test holds the table ofdocs/engineering-workflow.mdequal toCONFIDENCE_RULES. - Run metrics:
server/engine/usage.tsreads the tokens from the transcripts Claude Code keeps (engine.sessionUsage), only the bytes each file gained since the last reading (createUsageReader): one call is written on several lines under one message id and counted once with its last figures, the pilot's file is<project>/<session>.jsonland each subagent's<session>/subagents/agent-<id>.jsonl, with theagent_idthe hooks carry.server/run-metrics.tsis pure:trackTimeline(called fromRunSession.publish, the one place every state change passes) records the waits on the user inRunState.userWaitsand the first arrival at each phase inphaseArrivals;trackReopening(called whenworkflow-state.jsonis read) records inRunState.reopeningseach change asked after the final report, from the workflow going back to work aftercompletedto its next end that holds, and the run's duration covers the first pass plus those reopenings, never the idle session between them (time.reopened). While a reopening is open (reopeningOpen) and the session lives, the run is back in progress at the step the workflow declares, so its hooks and agents are followed again; its next end closes it again with the firstendedAtkept, and a session that exits before that end, a shutdown of the console or a restart that finds it in progress (interruptRun) leaves itcompletedwith an attention line, neverfailednorstopped;buildRunMetricsturns the state, the usage and the diff intoRunMetrics;metricsBaselineandmetricsFindingscompare a run to the delivered runs, of the same review tier once three share it, and say nothing below three.server/run-metrics-runtime.tsdoes the files and git:recordRunMetricswritesdata/runs/<id>/metrics.jsonwhen the workflow ends and again when the session exits (before the worktree is settled, since the diff is read in it),refreshUsagekeepsRunState.usageandRunSummary.tokensmoving during the run (at most one reading every 5 s),backfillRunMetricsmeasures on boot the archived runs that have no final figures. A run recordsfactory(console version and plugin commit) andbaseCommitat launch. The diff is measured from the branch its merge request targets, read from the forge (fetchMergeRequestTarget,diffBases), since the pilot cuts its branch from the base it chose, not from the checkout's HEAD; without a merge request it falls back on the stacked branch, thenbaseCommit. The time the stop gate's checks took istime.gate(gateTimes: total and per step, longest first, from themsof each line):keepGateLog, called fromserver/artifacts.ts, adds the new lines ofgate-log.jsonltodata/runs/<id>/gate-log.jsonl, since the workflow empties its task directory before it ends, and the figures are read from that copy. The commands the agents run themselves are not timed. The pilot declares its review tier asreviewTierinworkflow-state.json, kept inRunState.reviewTier. The self-audit entry carriesmetrics,baselineandfindings.GET /api/metricsserves every measured run, the live ones recomputed;components/metrics-panel.tsxis the table, opened from the rail. The plan sizes and the tier are the factory's own estimate, the diff is the fact: never grade a run on the first two alone. The demo has no transcript:demoSessionUsagederives figures from how far the run got. server/artifacts.ts: watches the run worktree's.claude/(restricted totasks/, because the workflow deletes and recreatestasks/) and copies documents into the run archive. Only files written since run start count.- Runtime recipe:
server/runtime-recipe.ts. The pilot writes.claude/tasks/runtime-recipe.md(contracts/runtime-recipe.md: how the target app is started, reached and driven, no secret).keepRuntimeRecipe, called fromserver/artifacts.ts, copies every version todata/repositories/<checkout>-<digest>/runtime-recipe.md(runtimeRecipeStoreindomain.ts), andseedRuntimeRecipeputs it back in the task directory at the next launch on that repository, with the stored file's date so the watcher does not archive it as a document of the new run. The demo keeps nothing.GET /api/repositories/recipe?repository=<absolute path>reads it (readRuntimeRecipe), and the page sendsrecipe.forgetto drop it (forgetRuntimeRecipe), answered byrecipe.result;components/recipe-dialog.tsxis the window, opened from the "Repository" line of a run and from the launch form. - Recurring review findings:
server/review-findings.ts. The senior reviewer writes.claude/tasks/senior-findings.jsonbeside its report (contracts/review-findings.md: one entry per finding, filed under one key of a fixed list,FINDING_CATEGORIESindomain.ts, which a unit test keeps equal to the contract's table).keepReviewFindings, called fromserver/artifacts.ts, adds them todata/repositories/<checkout>-<digest>/review-findings.json(reviewFindingsStore), by run and id so a later round adds to the earlier ones, writes of one repository serialised.seedRecurringFindings, at the next launch on that repository, writes.claude/tasks/recurring-findings.mdwhen a category was found on at least two tickets in the last 90 days (recurringFindings:othernever counts, five kinds at most, three latest examples each), dated like its latest finding so the watcher does not archive it. The pilot passes its path to the developers, never to a reviewer. Findings older than 180 days are dropped at the next write. The demo keeps nothing.GET /api/repositories/findings?repository=<absolute path>reads the count, the recurring kinds and the source fixes (readReviewFindingsSummary), the page sendsfindings.forgetto drop them (forgetReviewFindings), answered byfindings.result, andcomponents/recipe-dialog.tsxshows them under the recipe. - Review guide:
server/review-guide.ts. At launch, the first ofREVIEW.md,.claude/REVIEW.mdanddocs/REVIEW.mdin the run worktree (REVIEW_GUIDE_CANDIDATES, held equal tocontracts/review-guide.md) is copied to.claude/tasks/review-guide.md(seedReviewGuide), dated before the run so the watcher does not archive it, and the session getsIMPL_REVIEW_GUIDEnaming that source (reviewGuideEnvironment). No file: no copy and no variable. The pilot passes the copy to the senior reviewer, QA, the designer and the review orchestrator, never to a developer.keepReviewFindingsdrops a finding whosefilematches an ignored path of that copy (guideIgnoresFile, the same glob asIMPL_SENSITIVE_PATHS) from the repository store only; the run'sseniorFindingsstill holds it. A finding with no file is kept. Nits stay with the reviewer. - Source fixes:
server/finding-fixes.ts.sourceFixGroupscounts distinct runs of one category in the last 90 days (othernever qualifies). AtIMPL_FINDING_FIX_RUNS(default 3,findingFixRunThreshold, 2 to 20)syncFindingFixeswrites a pending proposal todata/repositories/<checkout>-<digest>/finding-fixes.json(findingFixStore) and the run says "Source fix proposed". The block (draftFindingFix, workflow language) is appended toCLAUDE.md, elseAGENTS.md, else a newCLAUDE.md(instructionTarget, never the review guide), betweenimplementation-factory:finding-fixmarkers. ESLint or Biome rules are named in that block only when the repository already depends on the tool and the plugin that owns them; no linter config is edited.findings.acceptwrites it on the main checkout (acceptFindingFix, refused on a run worktree and on a target that resolves outside the checkout) andfindings.dismissrecords a no. A dismissal sticks until the findings are forgotten.seedRecurringFindingsomits a category only once the fix is in the new run's worktree (settledFindingCategories,findingFixSettled: the marker, plus each rule named in a linter config for a lint fix). Only a queued writer sets an unreadablefinding-fixes.jsonaside; a read reports it. Store helpers live inserver/json-store.ts, the path glob inserver/path-glob.ts.forgetReviewFindingsdeletes the proposals too. server/transcript.ts: tails the session transcript for the conversation panel (lags behind the terminal by design).server/self-improvement.ts+worktree.ts: feedback/self-audit storage,self-improvement-*worktrees of this repo (unrelated to the run worktrees above), auto-rebase on HEAD moves, merge simulation viagit merge-tree --write-tree,mergeNeedsRestartflags a merge that touchedconsole/orbin/, the improvement session runs on Opus and the rebase one on Sonnet (backgroundArgumentsin the engine, never the user's default model), both detached withAskUserQuestiondenied, since no page shows them and a question would block them for good, and the web tools too, one improvement in flight at a time (autonomous audits are queued and launched one after another, so runs finishing together cannot each open a branch). Every run that left a trace writes its self-audit inpending/, but a session opens only on proof:auditReasonsindomain.tslists what the console observed going wrong (failed run, incident, criterion failed or blocked, QA verdict over an unobserved criterion, workflow declared blocked or ended with blockers and no merge request, change asked after the final report, a cost against the baseline, never a rework round alone), written in the entry asreasons, andimprovementCauseadds the user feedback and the earlier audits with reasons still waiting (pendingEntryvalidates each file). With none, the feed says "Self-improvement not needed" and nothing is launched. Nothing is ever pushed.applySelfImprovementReviewis the one promotion path, called by the UI merge button or by the automatic merge.- Automatic merge:
server/auto-merge.ts, always on: a branch is merged or rejected, never left to the user, except one that passes every lock below but changes a runtime prompt (promptFiles,PROMPT_PATHS:commands/,agents/,skills/,contracts/,principles/), recordedheldwith itspromptsand an attention notice, skipped by later ticks and shown with the merge and dismiss buttons (autoMerge.state: "held");settleHeldImprovement, afterselfImprovement.approveorreject, records the user'smergedorrejectedwithbyUser, whichrecentAutoMergesand the judge's rejection count leave out. A run reads text it did not write, and a prompt merged without the user would carry an injection into every later run. A held branch keeps its worktree, so it holds the one improvement slot until the user decides. It stops with the rest of the loop whenIMPL_SELF_IMPROVEMENT_AUTORUN=false, which the integration suite sets. EveryIMPL_AUTO_MERGE_POLL_MS(default 60 s) it takes each improvement worktree whose report is written and whose tree is clean: no commit, it is removed; otherwise it is replayed on HEAD by git alone, then rejected on a mechanical rule (autoMergeBlockersinserver/auto-merge-policy.ts: aPROTECTED_PATHSfile, a deleted or skipped test, over 15 files or 400 lines), on a check the console reruns in the worktree (typecheck, unit, build, integration whenconsole/orbin/changed), or on the verdict ofengine.startImprovementJudge(headless Opus on/implementation-factory:judge-improvement, run from its input directory under<scheduleRoot>/../improvement-judge/, contractcontracts/improvement-verdict.md, validated byjudgeVerdict); merged otherwise, unless HEAD or the branch moved meanwhile. A rejected branch is removed (uncommitted work kept asimprovement-uncommitted-<slug>.patchbeside the report, a worktree left uncommitted being rejected too), and the processed feedback it built on goes back topending/withrejectedAttempts(rejectedEntry), at mostMAX_REJECTED_ATTEMPTS(2) times. Decisions are lines of<data dir>/self-improvement-decisions.jsonl(merged,rejected,reverted,held); a merged or rejected one carriesjudgements, one entry per judge session on the branch (wall-clockdurationMsmeasured by the console,tokens,costUsd,turnsandmodelsread by the engine from the end report ofclaude -p --output-format json,headlessUsageinengine/usage.ts, absent for a session killed at its timeout), kept in memory until the branch is decided (judged, lost on restart) and validated on reading (judgeRun). A rejection raises no notice.improvementJudgeMetricscounts the merges, the rejections (those of the judge apart) and the reverts, served asimprovementsby/api/metricsand shown under the runs incomponents/metrics-panel.tsx. A merge that needs a restart is built and restarted through the launcher once no run takes a slot.selfImprovement.revertreverts a branch merged in the last day. The policy, the judge's prompt, contract and arguments, the guard, the reviewers and this loop are protected paths: a branch merged without the user cannot loosen how the next one is judged. - Forge:
lib/ticket-urls.tsreads the forge off the shape of an address, never off its host (parseTicketUrl,parseDeliveryUrl:/-/is GitLab,/<owner>/<repo>/issues|pull/<n>is GitHub, a GitLab host is never read as GitHub), andforgeWordsgives what the interface calls things,MR !12orPR #12. Internal names stay GitLab's on both forges (mergeRequestUrl,delivery: "merge_request",mr-description.md): archives andqueue.jsonare read by them. The stand-instests/fake-claude/glabandghkeep every suite away from a real forge. server/repository.ts: finds the checkout for a ticket, GitLab or GitHub, by scanningIMPL_SEARCH_ROOTStwo levels deep and reading.git/config.- Terminal client:
console/cli/, a second client of the server beside the page, with no access to its modules: it reads the HTTP routes and opens the same WebSocket, naming the console as itsOrigin(client.ts).commands.tsholds one function per command (COMMANDS),select.tsthe pure part (resolveRunandresolveQueued: a run is named by its id, the end of it, its ticket number or address, and a name that fits several is refused;answerFromInput: a number picks a choice by rank, in the format the question panel sends),format.tsthe text, built on the labels oflib/run-state.tsso both clients say the same words,live.tswhat stays connected (followforwatchandrun -f, which asks a decision at the terminal only when one is there,attachfor the session's terminal). Exit codes: 1 refused or failed, 2 usage, 3 no console. A feature added to the page gets its command here. lib/: client-side helpers (run-state derivation, conversation, notifications, sound).lib/run-state.tsholds the queue row labels (queueReason,queueStatus,scheduleMark,queueGroups);components/queue-list.tsxrenders the queue by batch then repository, with the two overrides. A row is dragged among the rows of itsqueueDragScope(its repository within its batch, or the lone launches),queueDropTargetgiving thebeforeof thequeue.move; the arrows remain for the keyboard.
Runtime data lives in console/data/runs/<run-id>/ (run.json, metrics.json, gate-log.jsonl, terminal.log, artifacts/), and one runtime recipe, one review-findings.json and one finding-fixes.json per repository in console/data/repositories/. data/queue.json is { version: 2, queue, tickets, edges, watches } (storedQueue, written whole then renamed); the bare array written before batches still loads (restoreQueueFile), and nothing simulated is written. The files of an analysis are in <scheduleRoot>/<id>/, outside the plugin (see Batch analysis). Gitignored and confidential (ticket content): never copy it into tracked files, commits or docs.
Conventions
- Code, identifiers, comments, commit messages, READMEs and
docs/in English. User-facing text (page,implcommands, server messages) exists in French and English, never written inline: see Interface language below. The documentation quotes interface labels in English. - UI styling and wording follow
brand/README.md(tokens, type scale, components, tone). A screen that departs from it is fixed, or the brand book is updated in the same commit. - Commits: conventional prefixes (
fix:,feat:,chore:), subject describes the behavior change in plain words.self-improvement: apply improvements from self-improvement-<id>is reserved for the improvement loop. - Server modules are ESM and import siblings with the
.jssuffix (Jest maps it back). - Unit tests:
describe(...)+it("should ..."), one file per responsibility. - Every
agents/*.md,commands/*.mdandskills/*/SKILL.mdneeds YAML frontmatter withnameanddescription, and a skill'snameis its directory (enforced bytests/unit/plugin-metadata.test.ts). - Interface language:
IMPL_LANGUAGE(frdefault,en;languageOfinlib/i18n/index.ts) decides the page, the terminal client, the server's messages and what a run writes.server/language.tsholds the current one (currentLanguage,currentTexts); the page changes it withlanguage.set, which runsbin/config.mjs setto write.envand is answered by the nextfactorysnapshot (FactorySnapshot.language); a value set in the shell takes over again at the next start, which the page is told. A run fixes its own at launch (RunState.language,runLanguage), passed to the session asIMPL_LANGUAGE: its activities, incidents andacceptance-summary.mdstay in it (session.texts), while notices, errors and queue reasons follow the current one. Every text is inlib/i18n/<area>.ts(defineTexts, French and English side by side, typed by the English one;texts(language)), read by components throughuseTexts()(components/language.tsx), by the server and by the terminal client; dates and numbers go through the helpers oflib/i18n/index.ts. The English text is what the integration suite selects on (it runs withIMPL_LANGUAGE=en) and what the unit suites assert (tests/unit/setup-language.ts). What stays English in both:server.log, anything sent to an agent, identifiers. Prompts read the setting throughcontracts/language.md, where a written rule of the target repository on the language of its merge requests, commits or comments wins for what it names.server/acceptance-text.tsholds every coverage sentence in both languages: the "Evidence" tab gets the view in the current language andworkflowSummaryinacceptance-runtime.tsthe run's for the file the merge request quotes. File-parsing diagnostics are English in both. The templates of the contracts and recipes stay written in French;contracts/language.mdgives their fixed English headings. principles/engineering.mdholds the short common decision rules,principles/test-quality.mdthe test shapes that cannot fail (read only when a diff touches tests);contracts/holds schemas and policies,contracts/run-worktree.mdamong them: the rules every agent of a run follows in its worktree (main checkout never written, symlinked dependency directories, ports, worktree and branch left in place), written once and linked from the pilot, the agents andcollect-evidence. A rule is stated by the file that owns it, and the others point to it rather than restate it. Agents explicitly read them through${CLAUDE_PLUGIN_ROOT}paths; this root AGENTS.md is not runtime context for plugin users. Methods live inskills/, loaded conditionally. Seedocs/engineering-workflow.mdfor routing, independent review and artifact ownership.- Developer
self-checkand independentreview-changemust remain separate.collect-evidencesupplies execution mechanics, never the reviewer's scenario selection or verdict. Reviewer expectations are established before reading author conclusions: QA writes them toqa-plan.md, the design reviewer todesign-inventory.md, and briefs pass author evidence by path only. - The design review (
designer-reviewer, fileagents/designer-reviewer-figma.md, skillfigma-review) runs on Sonnet at every tier. With a mockup (Figma frames or one attached to the ticket) it runs on any change visible in the UI, tier 0 included; with no mockup at all, only when the diff modifies a shared UI component or creates a screen or route (the pilot decides). QA gets a disposable worktree only when the diff touches test files. A QA verdictINCONCLUSIVEnever exits as ready. A design verdictINCONCLUSIVEis reported as "design not verified" and does not block. - Editing
commands/oragents/changes the workflow prompts run against real tickets;implement.mdand agents are tightly coupled to Claude Code tool names.
Code habits
Eight habits a code audit found behind several defects each. Rules to apply when writing and reviewing; nothing counts them.
- What an agent writes about itself is a claim. The delivery URL, the source of an evidence file, a QA mandate, a
confirmslink: the console decides from what it observes itself (the file's name, the forge's answer, a date compared with the agent's start) and turns a declaration that differs into a diagnostic. A value the agent declared never completes a run, opens a merge watch or relaxes a check on its own. - A safety rule is decided by a tool input or an allow-list, never by a sentence in a prompt alone. A session that reads ticket text gets no tool that writes outside its output. A rule that reads shell text is tested on the shapes that go around it (a wrapper in front, an absolute path, a control keyword, a glued redirection), and each test asserts which rule refused.
- A failure is handled or shown. No
.catch(() => undefined), emptycatchorvoid promise. A persisted file that cannot be read is not an absent one: onlyENOENTmeans absent, anything else is kept aside and reported before the file is written again. - Data from outside is validated, not cast: a file an agent, an outside watcher or an older version wrote, a forge answer, a WebSocket message. Parse to
unknown, validate every entry, drop the faulty one with a diagnostic. A contract incontracts/and its validator are held together by a test, asFINDING_CATEGORIESis. - One owner per piece of interface state. Which run is open has one source, the rest is derived from it. State that belongs to a run is keyed by its id, so it does not survive a switch to another run.
- The cost of an event does not grow with the state. A hook, a
publish, a monitor tick or a broadcast does not read again or write again everything the run holds. AGETwrites nothing and calls no forge. - Identity goes through its helper:
ticketIdentityandrunLockKeyfor a ticket,hostLiteralfor a host, an id for a question. Two raw strings are not compared, and values are not joined on a separator the data may contain. An environment variable left empty is an absent one. - A test calls the code it names. It does not restate the rule on a local constant, and it does not pass by a check that sits in front of the one it claims to cover. An absence is asserted after the event that proves the step ran, never after a fixed wait.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

