agentleFS
Greg-KleinGreg-Klein/implementation-harness/AGENTS.md

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

  • Other versions1 project uses a version of this.

    Word for word · 1

    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.

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
  1. Very specific:

    All from console/ (Node >= 22.12):

    line 18
  2. Very specific:

    Before a push: npm run verify once, on the tree that is pushed.

    line 35
  3. Distinctive:

    Run these yourself and report their real output, including when a subagent already said the work passes.

    line 36
  4. Very specific:

    After a change under console/ or bin/, run npm run build in console/, then impl restart.

    line 37
  5. Very specific:

    Every agents/*.md, commands/*.md and skills/*/SKILL.md needs YAML frontmatter with name and description, and a skill's name is its …

    line 96

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md

This file provides guidance to coding agents working with code in this repository.

What this is

Two things in one repo:

  1. A Claude Code plugin (.claude-plugin/, commands/, agents/, hooks/, skills/). /implementation-factory:implement drives a GitLab or GitHub ticket end to end (clarify, plan, implement, test, specialized reviews, MR or PR). The ticket URL decides the forge: glab and the glab-gitlab-api skill for GitLab, gh and the gh-github-api skill for GitHub, where the ticket status is never moved and review captures are attached with gh --attach when the installed gh has it (2.99.0 or later), kept local otherwise. improve and rebase run the self-improvement loop on this repo itself, and judge-improvement is run headless by the console to decide whether a finished improvement branch is merged without the user; review is the review step. schedule is run headless by the console to compare the tickets of a batch (agent ticket-scheduler, files defined in contracts/schedule.md).
  2. A local console (console/): a Next.js UI plus a Node server that spawns the real claude binary 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 command

CI (.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 typecheck and the unit tests the change concerns, the whole unit suite (30 s) when in doubt.
  • Before a push: npm run verify once, on the tree that is pushed. When the diff only touches documentation, prompts (agents/, commands/, skills/, contracts/, principles/) or hooks/, 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/ or bin/, run npm run build in console/, then impl restart. The restart does not build: the server runs server/ through tsx but serves the interface from .next/, so a change under components/, lib/ or app/ 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/tasks path, hookSpecificOutput, and the headless scheduling call. startSchedule spawns claude -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 --allowedTools list on /implementation-factory:schedule <input> <output>, in the main checkout, without the hook variables and with IMPL_SCHEDULE_OUTPUT naming its output file (the list matches a command by its first words, the guard decides what each call may do), killed after timeoutMs; 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 with WebFetch and WebSearch denied (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 import node-pty or read hook_event_name, and nothing does. The boundary is not complete for the rest: the hookSpecificOutput shape is typed in server/types.ts (HookOutput), domain.ts tests the tool name Bash and spells .claude/worktrees, the default of IMPL_WORKTREE_COPY_FILES names .claude/settings.local.json, and several messages write "Claude Code" where engine.label exists. Contract in engine/types.ts, rationale and the list of these leaks in engine/README.md. Keep new agent-specific logic in the engine.
  • Guard: hooks/guard.mjs (guardDecision), called first by emit.mjs, refuses nine kinds of PreToolUse call during a run of the workflow (IMPL_RUN_ID set or workflow-state.json present): a workflow agent under its bare name, a developer given a model override other than sonnet (its declared model) or opus (passed by the pilot for a task the plan sizes L), a developer started while planner-output.json lacks a complete integration or end-to-end decision (e2ePolicyProblem: test_strategy.integration_policy and test_strategy.e2e_policy per contracts/planner.md, whose fields a unit test holds equal to E2E_POLICY_FIELDS; when expected, a source, a command and a task whose verification_steps run 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 (reviewTierFloor in hooks/review-tier.mjs: at least 1 on IMPL_SENSITIVE_PATHS, at least 2 on CI or a migration, from workflow-state.json's reviewTier and baseBranch; tier 2 is not published without review-orchestrator having started (SubagentStart, noted by noteReviewStart outside the checkout, keyed on the run, the session and the task directory) and review-summary.md written after that start with READY or BLOCKED alone under ## Verdict, tier 1 without qa-reviewer or the orchestrator and the verdict of qa-report.md or of that summary; a verdict that is not ready is published only as a draft; deliveryCommand finds gh pr create/new, glab mr create/new, git push -o merge_request.create and a gh api/glab api POST, explicit or implied by a field, to pulls or merge_requests; a diff that cannot be read lets the call through), a git commit or a glab or gh publication carrying a session trailer or link, a publication (those, plus a glab api or gh api call 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 a git push one of whose commits the remote lacks adds one (pushedSecret: git log -p of the refspecs' sources, the branch checked out when none is named, --not --remotes, added lines only, in the directory -C names; 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 from runForgeHosts in domain.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/clone naming another host, typed or behind a remote read with git remote get-url --all and --push, every remote when none is named, -c routing keys, GIT_SSH_COMMAND, GIT_PROXY_COMMAND or GIT_CONFIG_* anywhere on the line, a pack program, git remote add|set-url or git config pointing elsewhere, and a file tool on a git config; forgeCliRefusal: glab/gh with another host in --hostname, -R, an address argument outside a free-text option or a host variable, plus their config set and auth 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 (behind then, 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 when IMPL_JUDGE_OUTPUT is set): Write to its verdict file only, no agent, no command. The headless scheduling session is not a run and has rules of its own (scheduleRefusal, applied when IMPL_SCHEDULE_OUTPUT is set, which startSchedule does): Write to its output file only, no other agent than ticket-scheduler, glab and gh only as issue view or an api call naming one issue or its links with no method, field or input option, rm on its output file only. The self-improvement and rebase sessions have no IMPL_RUN_ID and 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 by emit.mjs after the guard. gateObserve notes, per agent_id, the files each agent passes to Edit, Write, MultiEdit or NotebookEdit and which gated agents are alive, and fingerprints the changed paths of the tree when a gated agent starts alone (git status plus hash-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_DIR in tests). gateStop judges the SubagentStop of developer and senior-reviewer during 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/test or cypress (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 a SubagentHandback call, which would not resume (its lines carry handedBack: 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 (settleHandedBackAgents in server/hooks.ts, engine.agentHandedBack). Every verdict is a line of .claude/tasks/gate-log.jsonl (pass, fail, inconclusive for type errors outside the agent's files while a peer edits, skipped for 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 in ms. It fails open, and IMPL_STOP_GATE=off disables it. Rationale in docs/engineering-workflow.md, "The stop gate".
  • Blocking questions: the PreToolUse hook on AskUserQuestion (timeout 3600s) waits for the UI answer, which returns as updatedInput. 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.ts reads it off the PTY output (matcher on the text with escapes and blanks removed, sample of Claude Code 2.1.288 in tests/unit/fixtures/trust-dialog.json) and the engine emits session.prompt / session.prompt.end through StartOptions.onEvent. server/session-prompt.ts keeps it as RunState.sessionPrompt, a state of its own and not the question bridge: shown as a decision in the conversation, answered with sessionPrompt.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's cwd, so the run worktree, not the checkout. A refusal ends the run stopped with its reason (RunSession.endedBy), not failed with a lost_session incident, and its worktree is settled by settleRunWorktree like 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 a Host naming the console, the WebSocket an Origin it served, /api/hooks the secret carried in IMPL_HOOK_URL, drawn at each start unless IMPL_HOOK_TOKEN sets 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.mjs retries once, then spools the event to data/runs/<id>/hooks-spool.jsonl (IMPL_HOOK_SPOOL), replayed before the next hook and on every monitor tick; hookId dedupes. Questions are never retried nor spooled.
  • Run health: server/run-health.ts (pure evaluateRunHealth, injected clock and policy) reads RunSession.signals (pilot turn end, tool calls paired by toolUseId, subagent vs pilot, background calls, notification causes) and workflow-state.json (server/workflow-state.ts), server/run-incidents.ts keeps incidents (fingerprint dedupe, resolution on the lifting event, action checks, archive normalisation), server/run-monitor.ts is 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 only suspected_stall; a mid-workflow turn.end no longer sets attention by itself, and an exit before a result is failed with a lost_session incident, unless it comes within 5 s of a Ctrl-C the user typed in the run's terminal (registry.terminalInput, exitFollowsInterrupt), which ends it stopped like run.stop. An agent that ends without a file its contract requires, with nobody taking over, raises an incident: planner-output.json for ticket-planner, qa-report.md, qa-evidence.json and qa-plan.md for qa-reviewer, designer-review.md, design-evidence.json and design-inventory.md for designer-reviewer, review-summary.md for review-orchestrator (PRODUCER_CONTRACTS). A subagent whose stop does not follow its SubagentHandback call (handedBack: false on agent.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 through incident.action (revision + requestId, decision persisted before the effect). Runs left with an open incident or a kept worktree are read back by server/run-archive.ts and served on /api/archive/*, never registered.
  • server/domain.ts: pure logic, no FS/agent. Prefer putting logic here or in engine/ so it is unit-testable.
  • Review order: RunSession.artifactArrived records the first time each document is seen in RunState.artifactArrivals (called from server/artifacts.ts). reviewPlanNotes in domain.ts turns these times into RunState.reviewNotes, one note when qa-plan.md did not arrive before qa-report.md, or design-inventory.md before designer-review.md. A note is a remark shown in the evidence tab and changes no verdict. phaseForArtifact ignores both plan files, so they never open the next step.
  • Phase: the step the pilot declares in workflow-state.json is the phase (declaredPhase in domain.ts, applied in readWorkflowState through phaseOfWorkflow, never going back except during a reopening, see Run metrics). What a document, an agent start or a command points at (phaseForArtifact, phaseForAgent) goes through RunSession.inferPhase and moves RunState.phase only while no step is declared: a document says which step wrote it, not which one is running. RunSession.inferredPhase keeps that reading for closeWorkflowIfDone, which still ends a run that stopped declaring. A step the pilot skips or never declares has no entry in phaseArrivals.
  • 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 status paused (pauseTransition in domain.ts, applied in readWorkflowState). 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 than paused puts it back to running, the pilot counted as at work. Its session's exit or a shutdown ends it stopped (completed during a reopening), and an archive read back never shows it paused. The page and impl watch say "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), with activity()/publish(). persist() writes run.json one 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, on ticketIdentity: 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 while runHoldsRepository: in progress or session still open) and IMPL_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 the MergeWatcher. start creates the run worktree before the session; a failed git worktree add aborts the launch, never a fallback on the checkout.
  • Batch intake: the launch form sends batch.submit as 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 in targets, and refuses the whole batch when one URL is invalid. A ticket with no checkout and no target queues nothing: the server answers batch.unresolved and 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 through run.start, several through batch.submit); deliveryProjects in domain.ts decides when the session gets IMPL_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.enqueueBatch takes ResolvedTicket[], drops the tickets already queued, held by a run or behind a watched merge request (admitBatch, same key as runLockKey), and queues the rest under one batchId.
  • Ticket proposals: server/ticket-proposals.ts (TicketProposals, owned by the registry) reads the file an outside watcher keeps up to date, data/ticket-proposals.json or IMPL_TICKET_PROPOSALS_FILE, every IMPL_PROPOSALS_POLL_MS (default 5 s, not in impl config), format in contracts/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, openProposals and handledStillFound in domain.ts are 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 to registry.launchProposals with no click: resolveProposedTickets resolves each one on its own and enqueueBatch queues 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.proposals carries the tickets not queued, components/proposal-list.tsx shows them under the queue with their reason, and proposal.dismiss records the decision, and proposal.launch (issueUrl, repositories) queues a refused ticket on the checkouts the user chose. An entry may carry repositories (project paths, projectPath in domain.ts), resolved through checkoutOfProject. The baseBranch a proposal may carry (branchName in domain.ts drops anything that is not a plain branch name) follows it as QueuedRun.baseBranch and RunState.ticketBaseBranch and reaches the session as IMPL_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) writes input.json under <scheduleRoot>/<id>/, calls engine.startSchedule and judges the session by output.json alone, validated whole by validateSchedule against contracts/schedule.md. One session per repository per batch, serialised per repository (analysisChains), outside IMPL_MAX_CONCURRENT_RUNS, bounded by IMPL_SCHEDULE_TIMEOUT_MINUTES (default 5). Entries are analysing until it answers. No session for a single new ticket in a repository with no known prediction (see Forge links); a lone run.start beside known predictions is analysed too. A missing or refused file, a timeout, or a restart during the analysis (INTERRUPTED_ANALYSIS) records the tickets as analysis: "failed". The next analysis of the repository sends the tickets still queued, running or awaiting merge whose analysis failed as tickets to predict, not as known (failedTickets), and replaces their stored prediction; if it fails too they stay failed, and no analysis is opened for them alone. scheduleRoot (scheduleDirectory in domain.ts) is implementation-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 under schedule/. The directory is removed once the answer is read, kept with session.log on a failure, and scheduleRoot is wiped on boot.
  • Forge links: beside the session, forgeEdges in the registry reads the blocking links of every ticket being analysed (fetchIssueLinks in server/ticket.ts: one glab api .../issues/<iid>/links call, or the two gh api .../dependencies/blocked_by|blocking ones, no Claude session). linkEdges in domain.ts turns a link to another ticket the console has in the same repository (repositoryTickets: queued, held by a run or awaiting merge) into a depends_on edge, matched by host, project and number since GitLab writes one issue as /-/issues/ and /-/work_items/, and overlayEdges puts 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: inside launch, before the run may start, so run.start still answers with the run, and for a batch of one through scheduleAnalysis with predicted false, shown as analysing while 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. conflictBetween keeps two tickets of one repository apart on an edge (overlap, depends_on), and a ticket whose analysis failed or whose confidence is low apart from every other ticket of its repository (analysis_failed, low_confidence); tickets of two repositories never conflict. detail is written for the ticket that waits: it says whether the failed analysis or the vague prediction is its own or the other ticket's. describeQueue gives each entry its reason, first match wins: ticket (a run on the same ticket), slot for a forced entry, analysis, conflict (a conflicting run in progress), merge or merge_unknown (the watched merge request of a finished one), dependency or order (a conflicting entry ahead in the queue), else slot. startableEntries returns the slot entries, up to the free slots; a held entry takes no slot and is stepped over. priorityOrder puts the first ticket of a depends_on edge ahead of the second.
  • Merge watch: when a scheduled run ends with a merge request, runEnded records a MergeWatch and the conflicting entries wait for the merge. server/merge-watch.ts (MergeWatcher) polls only the watches that hold a queued entry (heldWatches), every IMPL_MERGE_POLL_MS (default 60 s, not in impl config), and has no timer otherwise. The check is fetchMergeRequestStatus in server/ticket.ts: one glab api call made by the Node server, or gh api for a pull request (deliveryEndpoint and deliveryStatus in domain.ts: GitHub says a merge with merged, its state being only open or closed). It opens no Claude session and costs no tokens. mergeWatchStep: merged or closed drops the watch and releases what it held, opened keeps holding, and a failed call is unknown, 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.force sets QueuedRun.forced, after which only the ticket lock and the slots hold the entry. Mode base starts from the base branch. Mode stacked needs the branch of the blocking ticket (from its run or its watch) and passes it to the session as IMPL_BASE_BRANCH. queue.move reorders (a dependency still goes first), queue.cancel removes.
  • Run worktree: server/run-worktrees.ts owns the lifecycle, server/worktree.ts the git calls, domain.ts the 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. provisionWorktree copies the ignored dependency directories copy-on-write (IMPL_WORKTREE_DEPENDENCY_DIRS, default node_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 relative core.hooksPath (husky's .husky/_, without which the first commit fails); a provisioning failure is only a warning in the activity feed. The session gets IMPL_RUN_WORKTREE, IMPL_SOURCE_REPOSITORY, IMPL_SOURCE_BRANCH (omitted on a detached HEAD) and IMPL_WORKTREE_DEPENDENCIES (symlink as soon as one directory is linked, else clone, omitted when none was brought over). staleDependencies (in run-worktrees.ts, pure part lockedPackages and dependencyDrift) compares each node_modules brought over with the package-lock.json beside it (top-level packages, optional ones and links aside); a mismatch is an attention activity and IMPL_WORKTREE_DEPENDENCIES_STALE for the session, whose pilot reinstalls before the first agent, and an unreadable lockfile is reported, never guessed. RunState.cwd is the worktree and RunState.repository the checkout (sourceRepository reads older archives), so .claude/tasks is the worktree's own: cleared, watched and snapshotted there, never in the checkout. Once the session is gone, settleRunWorktree removes the worktree only when the run is completed with 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). Otherwise RunState.worktree turns kept with the reasons in detail. A shutdown marks it kept, and reconcileRunWorktrees applies the same rules on boot, before the archive is listed. removeWorktreeOnRequest is refused while the session lives and answers confirm with the risks (uncommitted, unpushed) until the request comes back with force. 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) vs broadcastToViewers() (one run's state and terminal). reportFailure(what, subject) is what a write or a reading nobody awaits ends with: a line in server.log each time, with the stack when there is one, and one notice per kind of failure every ten minutes. The process has its own unhandledRejection and uncaughtException handlers, which report and keep the console up. isMissingFile tells an absent file from an unreadable one: an unreadable queue.json or review-findings.json is renamed *.unreadable-<time> before the console starts from nothing. Every HTTP request goes through route, whose failure is answered 500 (400 for an address that does not decode); a hook the console fails to apply is answered 500, which emit.mjs retries then spools. On boot, non-terminal archived runs are reclassified failed, except one reopened after its final report, closed completed.
  • 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.reject and selfImprovement.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), run and terminal.output (to the pages that opened that run via run.subscribe), notice (factory-level events; those of the improvement loop, sent by notice() in self-improvement.ts, carry dismissAfterMs and close on their own after 5 s; any other info notice closes after 10 s, an attention one stays until closed, noticeDismissDelay in lib/run-state.ts), error, incident.result, worktree.result and batch.result (answered to the page that asked). The client sends worktree.remove (runId, optional force) for a live or archived run and gets worktree.result back (removed, confirm with risks, or refused). It sends batch.submit (issueUrls, optional instruction applied to every ticket) and gets batch.result back (batchId, accepted, duplicates), then queue.force (queuedId, mode base or stacked, optional onto), queue.move (queuedId, before, null for the end) and queue.cancel, which answer through the next factory snapshot. proposal.dismiss (issueUrls) and proposal.launch (issueUrl, repositories) answer the same way, a failure through error. batch.submit may carry targets and is answered batch.unresolved (tickets) when a ticket has neither a checkout nor a target. demo.start takes scenario workflow, incident or batch. run.start and demo.start may carry a requestId, which comes back in run.started (runId, sent before the run's state), in the notice of a queued launch or in error: 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 with error. Any message may carry an ackId: the server answers ack with it once the message was handled, after whatever the message answers on its own, or puts it in the error that 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 sends recipe.forget (repository) and gets recipe.result back (forgotten), and findings.forget and findings.result the same way. It sends findings.accept and findings.dismiss (repository, category) and gets findings.fix back (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 .webm or .mp4 path 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.json is parsed when archived (plannedTasks), the engine reads the plan task id out of a developer delegation (the developer-report-<id>.md suffix in the Agent prompt), delegatedTasks drops the ids whose report already exists (a brief cites earlier reports as reading material), pairDelegation binds it FIFO to the next SubagentStart of that type, and planTaskBoard derives each card's column and assignee (report present = done). Agents get a first name and picture in start order (agentIdentity, pictures in public/avatars/) and a role (agentRole).
  • Acceptance evidence: the pilot writes acceptance-criteria.json (stable AC<n> ids), tasks carry criterion_ids, evidence files follow contracts/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. A fail is 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 with kind: "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. qaVerdictConsistency compares the status of the latest QA report with QA's own executed observations on the current code and returns AcceptanceView.qa, with a warning when PASS or PASS_WITH_WARNINGS is declared over a criterion QA did not observe; a focused pass answers only for the criteria in its root mandate (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 whose status is missing or not one of the four tokens is an error diagnostic, so it is listed among the anomalies of acceptance-summary.md instead of skipping the check in silence; server/evidence-archive.ts keeps every version and its captures under data/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.ts ingests, identifies the code with hooks/code-snapshot.mjs (the same utility the workflow runs) until the workflow's end is archived (deliveredCodeSettled in domain.ts: a rebase or an edit made after delivery, outside a reopening, leaves the delivered coverage as it was), writes acceptance-summary.md back to the task directory and answers the pre-cleanup archive-sync-request.json. The full view is /api/runs/<id>/acceptance; RunState.acceptance and RunSummary.acceptance carry 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 and senior-review.md or qa-report.md is archived. CONFIDENCE_RULES is 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. changeFacts reads the diff with changedFiles, addedLines and isTestFile of auto-merge-policy.ts (size, IMPL_SENSITIVE_PATHS through pathMatches, 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), gateFacts the last verdict of each check in the kept gate log, and a finding of senior-findings.json is open only in the last review round, uncorrected and with no rework after it. server/review-confidence-runtime.ts does the readings, kept on the RunSession and 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 from refreshAcceptance alone, which a GET calls), a failed reading of the diff leaving it unknown unless the worktree is gone (changeAfterReading). settleConfidence in acceptance-runtime.ts applies it inside refreshAcceptance; refreshConfidence is called when a run document arrives, when the gate log is kept, when a hook sees the merge request opened (rememberMergeRequest in hooks.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 through incident.action, the closing of a run) calls settleIncidentConfidence first, which revises the incident_open rule from the reasons the note already carries, with no reading, then refreshConfidence on 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. renderAcceptanceSummary writes 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 English detail). confidenceAtDelivery keeps the note of the first completed and is never rewritten; confidenceCalibration in run-metrics.ts counts per note the delivered runs reopened or given feedback (RunState.feedbackCount), served as calibration by /api/metrics. normalizeArchivedRun keeps the three fields through a restart, each validated by storedConfidence, so an archived run still shows its note and figures written again by backfillRunMetrics still count in the calibration. The note changes no behaviour of the workflow. A unit test holds the table of docs/engineering-workflow.md equal to CONFIDENCE_RULES.
  • Run metrics: server/engine/usage.ts reads 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>.jsonl and each subagent's <session>/subagents/agent-<id>.jsonl, with the agent_id the hooks carry. server/run-metrics.ts is pure: trackTimeline (called from RunSession.publish, the one place every state change passes) records the waits on the user in RunState.userWaits and the first arrival at each phase in phaseArrivals; trackReopening (called when workflow-state.json is read) records in RunState.reopenings each change asked after the final report, from the workflow going back to work after completed to 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 first endedAt kept, and a session that exits before that end, a shutdown of the console or a restart that finds it in progress (interruptRun) leaves it completed with an attention line, never failed nor stopped; buildRunMetrics turns the state, the usage and the diff into RunMetrics; metricsBaseline and metricsFindings compare a run to the delivered runs, of the same review tier once three share it, and say nothing below three. server/run-metrics-runtime.ts does the files and git: recordRunMetrics writes data/runs/<id>/metrics.json when the workflow ends and again when the session exits (before the worktree is settled, since the diff is read in it), refreshUsage keeps RunState.usage and RunSummary.tokens moving during the run (at most one reading every 5 s), backfillRunMetrics measures on boot the archived runs that have no final figures. A run records factory (console version and plugin commit) and baseCommit at 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, then baseCommit. The time the stop gate's checks took is time.gate (gateTimes: total and per step, longest first, from the ms of each line): keepGateLog, called from server/artifacts.ts, adds the new lines of gate-log.jsonl to data/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 as reviewTier in workflow-state.json, kept in RunState.reviewTier. The self-audit entry carries metrics, baseline and findings. GET /api/metrics serves every measured run, the live ones recomputed; components/metrics-panel.tsx is 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: demoSessionUsage derives figures from how far the run got.
  • server/artifacts.ts: watches the run worktree's .claude/ (restricted to tasks/, because the workflow deletes and recreates tasks/) 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 from server/artifacts.ts, copies every version to data/repositories/<checkout>-<digest>/runtime-recipe.md (runtimeRecipeStore in domain.ts), and seedRuntimeRecipe puts 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 sends recipe.forget to drop it (forgetRuntimeRecipe), answered by recipe.result; components/recipe-dialog.tsx is 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.json beside its report (contracts/review-findings.md: one entry per finding, filed under one key of a fixed list, FINDING_CATEGORIES in domain.ts, which a unit test keeps equal to the contract's table). keepReviewFindings, called from server/artifacts.ts, adds them to data/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.md when a category was found on at least two tickets in the last 90 days (recurringFindings: other never 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 sends findings.forget to drop them (forgetReviewFindings), answered by findings.result, and components/recipe-dialog.tsx shows them under the recipe.
  • Review guide: server/review-guide.ts. At launch, the first of REVIEW.md, .claude/REVIEW.md and docs/REVIEW.md in the run worktree (REVIEW_GUIDE_CANDIDATES, held equal to contracts/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 gets IMPL_REVIEW_GUIDE naming 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. keepReviewFindings drops a finding whose file matches an ignored path of that copy (guideIgnoresFile, the same glob as IMPL_SENSITIVE_PATHS) from the repository store only; the run's seniorFindings still holds it. A finding with no file is kept. Nits stay with the reviewer.
  • Source fixes: server/finding-fixes.ts. sourceFixGroups counts distinct runs of one category in the last 90 days (other never qualifies). At IMPL_FINDING_FIX_RUNS (default 3, findingFixRunThreshold, 2 to 20) syncFindingFixes writes a pending proposal to data/repositories/<checkout>-<digest>/finding-fixes.json (findingFixStore) and the run says "Source fix proposed". The block (draftFindingFix, workflow language) is appended to CLAUDE.md, else AGENTS.md, else a new CLAUDE.md (instructionTarget, never the review guide), between implementation-factory:finding-fix markers. 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.accept writes it on the main checkout (acceptFindingFix, refused on a run worktree and on a target that resolves outside the checkout) and findings.dismiss records a no. A dismissal sticks until the findings are forgotten. seedRecurringFindings omits 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 unreadable finding-fixes.json aside; a read reports it. Store helpers live in server/json-store.ts, the path glob in server/path-glob.ts. forgetReviewFindings deletes 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 via git merge-tree --write-tree, mergeNeedsRestart flags a merge that touched console/ or bin/, the improvement session runs on Opus and the rebase one on Sonnet (backgroundArguments in the engine, never the user's default model), both detached with AskUserQuestion denied, 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 in pending/, but a session opens only on proof: auditReasons in domain.ts lists 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 as reasons, and improvementCause adds the user feedback and the earlier audits with reasons still waiting (pendingEntry validates each file). With none, the feed says "Self-improvement not needed" and nothing is launched. Nothing is ever pushed. applySelfImprovementReview is 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/), recorded held with its prompts and an attention notice, skipped by later ticks and shown with the merge and dismiss buttons (autoMerge.state: "held"); settleHeldImprovement, after selfImprovement.approve or reject, records the user's merged or rejected with byUser, which recentAutoMerges and 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 when IMPL_SELF_IMPROVEMENT_AUTORUN=false, which the integration suite sets. Every IMPL_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 (autoMergeBlockers in server/auto-merge-policy.ts: a PROTECTED_PATHS file, a deleted or skipped test, over 15 files or 400 lines), on a check the console reruns in the worktree (typecheck, unit, build, integration when console/ or bin/ changed), or on the verdict of engine.startImprovementJudge (headless Opus on /implementation-factory:judge-improvement, run from its input directory under <scheduleRoot>/../improvement-judge/, contract contracts/improvement-verdict.md, validated by judgeVerdict); merged otherwise, unless HEAD or the branch moved meanwhile. A rejected branch is removed (uncommitted work kept as improvement-uncommitted-<slug>.patch beside the report, a worktree left uncommitted being rejected too), and the processed feedback it built on goes back to pending/ with rejectedAttempts (rejectedEntry), at most MAX_REJECTED_ATTEMPTS (2) times. Decisions are lines of <data dir>/self-improvement-decisions.jsonl (merged, rejected, reverted, held); a merged or rejected one carries judgements, one entry per judge session on the branch (wall-clock durationMs measured by the console, tokens, costUsd, turns and models read by the engine from the end report of claude -p --output-format json, headlessUsage in engine/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. improvementJudgeMetrics counts the merges, the rejections (those of the judge apart) and the reverts, served as improvements by /api/metrics and shown under the runs in components/metrics-panel.tsx. A merge that needs a restart is built and restarted through the launcher once no run takes a slot. selfImprovement.revert reverts 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.ts reads 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), and forgeWords gives what the interface calls things, MR !12 or PR #12. Internal names stay GitLab's on both forges (mergeRequestUrl, delivery: "merge_request", mr-description.md): archives and queue.json are read by them. The stand-ins tests/fake-claude/glab and gh keep every suite away from a real forge.
  • server/repository.ts: finds the checkout for a ticket, GitLab or GitHub, by scanning IMPL_SEARCH_ROOTS two 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 its Origin (client.ts). commands.ts holds one function per command (COMMANDS), select.ts the pure part (resolveRun and resolveQueued: 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.ts the text, built on the labels of lib/run-state.ts so both clients say the same words, live.ts what stays connected (follow for watch and run -f, which asks a decision at the terminal only when one is there, attach for 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.ts holds the queue row labels (queueReason, queueStatus, scheduleMark, queueGroups); components/queue-list.tsx renders the queue by batch then repository, with the two overrides. A row is dragged among the rows of its queueDragScope (its repository within its batch, or the lone launches), queueDropTarget giving the before of the queue.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, impl commands, 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 .js suffix (Jest maps it back).
  • Unit tests: describe(...) + it("should ..."), one file per responsibility.
  • Every agents/*.md, commands/*.md and skills/*/SKILL.md needs YAML frontmatter with name and description, and a skill's name is its directory (enforced by tests/unit/plugin-metadata.test.ts).
  • Interface language: IMPL_LANGUAGE (fr default, en; languageOf in lib/i18n/index.ts) decides the page, the terminal client, the server's messages and what a run writes. server/language.ts holds the current one (currentLanguage, currentTexts); the page changes it with language.set, which runs bin/config.mjs set to write .env and is answered by the next factory snapshot (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 as IMPL_LANGUAGE: its activities, incidents and acceptance-summary.md stay in it (session.texts), while notices, errors and queue reasons follow the current one. Every text is in lib/i18n/<area>.ts (defineTexts, French and English side by side, typed by the English one; texts(language)), read by components through useTexts() (components/language.tsx), by the server and by the terminal client; dates and numbers go through the helpers of lib/i18n/index.ts. The English text is what the integration suite selects on (it runs with IMPL_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 through contracts/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.ts holds every coverage sentence in both languages: the "Evidence" tab gets the view in the current language and workflowSummary in acceptance-runtime.ts the 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.md gives their fixed English headings.
  • principles/engineering.md holds the short common decision rules, principles/test-quality.md the test shapes that cannot fail (read only when a diff touches tests); contracts/ holds schemas and policies, contracts/run-worktree.md among 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 and collect-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 in skills/, loaded conditionally. See docs/engineering-workflow.md for routing, independent review and artifact ownership.
  • Developer self-check and independent review-change must remain separate. collect-evidence supplies execution mechanics, never the reviewer's scenario selection or verdict. Reviewer expectations are established before reading author conclusions: QA writes them to qa-plan.md, the design reviewer to design-inventory.md, and briefs pass author evidence by path only.
  • The design review (designer-reviewer, file agents/designer-reviewer-figma.md, skill figma-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 verdict INCONCLUSIVE never exits as ready. A design verdict INCONCLUSIVE is reported as "design not verified" and does not block.
  • Editing commands/ or agents/ changes the workflow prompts run against real tickets; implement.md and 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 confirms link: 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), empty catch or void promise. A persisted file that cannot be read is not an absent one: only ENOENT means 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 in contracts/ and its validator are held together by a test, as FINDING_CATEGORIES is.
  • 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. A GET writes nothing and calls no forge.
  • Identity goes through its helper: ticketIdentity and runLockKey for a ticket, hostLiteral for 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.