Repository navigation
Pipeline Roles Shared
Draft, 2026-10-16. Proposed for split-pipeline-roles (decisions ops-in-role-lifecycles, pipeline-commit and wiki-provisioned-by-role), from Axlog's asks:
- move the ops into a deterministic lifecycle;
- limit each agent's git use to a custom commit tool with
git commit's contract that also pushes; - provision the wiki as part of the lifecycle for roles that need it;
- have
pipeline_commitcommit and push the wiki too, transparently.
Each role's lifecycle is its own, in its spec's Lifecycle section, and the roles' lifecycles differ. Nothing makes them share phases. This page holds only what several roles use: the commit tool, the hooks, and how a role's declared wiki access is provisioned. Index: Pipeline roles.
Today the ops (src/ops.ts) are already scripts with no model. What isn't fixed is when they run:
-
Ops come as requests: each stage's workflow code asks the sweep's driver for a batch of launches, mixing ops requests and model sessions. The protocol (
BEGIN_OPS_REQUESTon stdin, theissue-pipeline-opsexternal-cli agent) was built for pi-subagents' workflow sandbox, which has no shell. -
Pushing is spread over several places: a writer session only commits. The commits reach GitHub later through
push,open-prorfinish-sync, or through the job's exit push. Wiki commits the push missed are parked in the wiki mirror for the next job. - The wiki is cloned for every job whenever the repo has one, whether the role reads it, writes it or neither.
-
Writer sessions hold a token: they get the writer's
GH_TOKENand bash, so only the pre-push hook and the prompts keep them from pushing (credential-isolation-limit).
These are the only rules the roles share. Each role orders its steps its own way.
- Its steps are scripts. Only the steps its spec marks as sessions run a model.
- A session never runs an op, and an op never starts a session. The job runs the ops directly, in the order the role's lifecycle gives, not as launches the workflow requests.
- Loops are bounded by counts kept in the state, never by the model's choice.
- The ops keep their reply shapes, stop codes and GitHub effects, so today's ops tests stay valid.
- Every role can be interrupted at any point and started again, carrying on from what is done (
every-role-resumes). See Restart from what is done below. - The pi-subagents sweep (
workflow.js, lanes,/pipeline-start) can't run lifecycles, because it has no shell. Proposed: retire lanes withorchestrator-cutover.
The spec writer, the implementor and the reviewer each run as an explicit state machine. Triage, the finalizer (today's merger, finalizer-role) and the orchestrator stay linear scripts. Each machine is the role's own, with its own states and state type. They share only the runner and these rules:
-
Shape: a machine is a table and one pure function.
- The table,
TABLE, is data: each state's kind (script,sessionorend), the results it accepts, and the next state for each. -
transition(state, result) → { state, effect }is synchronous, does no I/O, and returns only edges the table lists. A result the table doesn't list for that state is a bug, and the function throws.
- The table,
-
State:
{ at, data }, whereatis a state name anddatais the role's own closed type. Counts, start SHAs and the last reply live indata. There is nocarry: unknownand no flag shared across roles. -
Effects: a state's effect is one of:
- an op, with its payload;
- a script check, such as
spec-tests-checkor a no-work check; - a model session, with its agent, schema and task. Only
sessionstates may return one, and the runner refuses it from any other state; - an end:
saved(stage),stopped(code),releasedorheld(stage again next tick). Built (#304): astoppedorreleasedend may name anafterop, such asaskorstop, which the runner runs after the end's save, with the state just saved as itsstate; its failure ends the run as a failed step would. It is namedafter, notthen, because of Biome'snoThenProperty. Rejected: a runner-level stop effect, which coversstopbut notask, and hand-off tricks that setstageand leave the op to a later job.
-
Entry: a job enters its role's machine at the state for the issue's stage. A machine covers all its role's stages, so the table shows how they connect. Each stage boundary in a table is one of two ends:
-
saved(stage): saves the issue at the stage and ends the job. The orchestrator schedules the next job; -
next(stage)(implementor-one-job): saves the issue at the stage and carries on at that stage's entry in the same job, with the same checkout and lease. Only the implementor uses it, for fix → sync → checks (proposed: fix → checks, since a sync runs only at an entry's start,sync-at-edges).
Saving at every boundary keeps the issue's stage current while the job runs, so the orchestrator sees it and a restart begins at the right stage.
-
-
Loops: every cycle in the table passes through an edge guarded by a count in
datawith its limit from the config. A test checks this over the table. -
Hand-off: the machine's
datais rebuilt by locate at each start. The issue's state gets only the hand-off fields the index's hand-off table lists and the role's checkpoints. Today's cross-stage flags (rebasedOnly,resynced,held) become hand-off fields or go away. -
Runner: it runs locate, then the current state's effect, passes the result to
transition, and reports each step injob.json. Built (#304): the job'sEntry.context(RoleContextinsrc/roles/walk.ts: the issue, the repo config, the budgets and models, the claim's paths and the run's start time) is passed tolocatewith the saved state, so a role stays pure and loads nothing itself. Rejected: a role that loads its own config, which puts I/O in a pure role. -
Budget: a session's budget bounds its context, not the issue's spend; the runner charges nothing before a session effect (
budgets-bound-context, Axlog, 2026-10-16). Only the orchestrator's watchdog stops a writer whose context passes its budget, withstopped(budget). -
Failures the runner owns: a network error ends
held; any other failed effect the table doesn't list endsstoppedwith the step-failure code. Tables list only the failures a role handles itself. -
Prelude and teardown: each machine starts with
claimthencheckoutand ends with teardown (save, release the lease, push anythingpipeline_commitfailed to push, remove the checkout). The tables show them once. -
Linear roles: triage and the finalizer run their steps in order on the same runner, with a locate step and no table. Triage is built (#304,
src/roles/triage/). Built (#304): a linear step may giverecover(data, error), which turns that step's failed effect into data and lets the run go on, as triage'sstart-pronly logs a failed draft PR. Rejected: a runner-wide "ignore failure" flag, which can't say what the failure changes.
Every role, machine or linear script, and the orchestrator, can be interrupted at any point and simply started again. There is no --resume and no resume mode: every start works out what is already done and carries on after it. Some lost or repeated work is accepted where avoiding it would make the design less simple (Axlog, 2026-10-16).
-
Built today, for stops: a lifted
agent:stopped-*label resumes at the stage that stopped, carrying the saved state with every limit reset, so no finished stage runs again (Axlog, 2026-10-16). A specced issue goes straight to implement. A stage after review stands only on a review of the branch's head, or of changes with the same patch id (review-keeps-on-clean-rebase), and otherwise goes back to review. A savedverifiedShaand the review's findings carry over with it. A stop with no stopped stage saved, or one that answered questions lift, restarts as before. - Interruptions covered: SIGTERM or SIGINT; SIGKILL, a crash or a reboot, with no cleanup; a lost network; the orchestrator restarting; a pi session dying.
-
Only durable state counts: the issue's saved state, GitHub (the issue, its comments and labels, the PR), and the code and wiki remotes. The checkout, the pi session files and
job.jsonare disposable.job.jsonreports what a job did, for people and the orchestrator, and no start reads it to decide anything. -
Every start is the same: claim, check out fresh from the pushed heads, then locate. Locate is a script in each role that reads the durable state and picks the first state or step with work left. For the machines, it is mostly the entry's no-work check (see
no-work-skips-stages): the checks that skip a stage with nothing to do also skip work already done. Each role spec lists its own locate rules. -
Sessions start fresh. An interrupted session isn't resumed. It runs again from its task, on a checkout that already holds every commit it pushed, since
pipeline_commitpushes each one. Its task says that earlier commits on the branch may be its own. Lost: uncommitted edits, and the session's unfinished reasoning. -
Every effect is safe to run twice. Each op that changes something outside the job checks before it acts, on something visible on GitHub or in git:
- each signed comment carries a hidden marker,
<!-- issue-pipeline:<issue>:<kind> -->, and is created or updated in place by that marker. A rerun replaces the comment rather than adding a second one. Built (#304) for queued comments: a comment in an ops request's envelope may give akey, andcommentByKey(src/comments.ts) edits the pipeline account's comment with that key's marker, or posts one when there is none; it never edits another account's comment. Triage'spostuses it. Rejected: a new upsert op, since a key on the envelope lets any op's queued comment be keyed. Not yet keyed: the commentsaskandstoppost themselves; - labels are set as a set, not toggled;
-
start-prfinds the branch's PR before opening one, and, given a body, sets that PR's body rather than opening a second (#332); marking a PR ready is a no-op when it already is; - pushes use
--force-with-lease, and a push whose target the remote already has does nothing; - sub-issues carry the parent's number and their index in a marker, and
splitopens only the missing ones; -
merge,close-prand closing an issue check the PR's or issue's state first; -
publishchecks whether the wiki's base already holds the issue's wiki commits.
- each signed comment carries a hidden marker,
-
Checkpoints: a value a later step needs, which can't be read back from GitHub or git, is saved in the issue's state as soon as it is known. Each role lists its own. Examples: the spec writer's start heads, and the counts that bound loops (
attempts,fixRounds,ciFixRounds,specReturns). The counts are saved when they go up, so a crash restart never resets a loop's limit. A restart after a lifted stop keeps only the start heads and clears every count, as it resets every limit (#392). -
The lease: a stopped job releases its lease. A killed one leaves it to lapse, and the next claim takes a lapsed lease and carries on at the saved stage, as
claimdoes today. - Graceful stop: SIGTERM or SIGINT aborts the running session or op, saves the issue at its current stage with no lease, and exits 128 plus the signal's number, as today. An op cut off half-way is safe to run again.
-
Teardown on every exit, as today: it saves uncommitted changes as patches in the
dropped/folder of the issue's run directory, records them (lostChanges,droppedPatches) and drops them, pushes whatpipeline_commitfailed to push, and removes the checkout. Nothing applies a patch (dropped-changes-saved, #369; see Dropped changes). A killed job skips teardown, and the next start's fresh checkout replaces the old one. -
Crash loops: the orchestrator counts restarts per issue and stage (today's
maxResumes, proposed renamedmaxRestarts), and stops the issue withagent:stopped-jobpast the limit. -
Lost and repeated work, accepted:
- an interrupted session reruns from its task;
- an interrupted rebase restarts
syncfrom the pushed head, and the resolver resolves its conflicts again (proposed: the resumed implementor entry runs its start sync again,sync-at-edges); - review lenses that had replied run again;
- a CI or verify run starts over.
-
The orchestrator: a tick is a pure function of a fresh snapshot, so an interrupted tick just runs again. Its start records (already kept for the gap before a job writes
job.json) stop it starting a job twice.
Every session's tokens and context are logged when it finishes, so the points where context bloats can be found. The log feeds context-bloat-pass, and nothing in the pipeline reads it to decide anything.
Built: every stage job writes a session record per session and a job record at its end. A record the serializer refuses is reported in the job's job.log. Gaps closed and remaining on the Token spend page lists what the records still miss.
-
Where: one usage comment per issue, signed by the pipeline account and edited in place, found by its marker
<!-- issue-pipeline:<issue>:usage -->(Axlog, 2026-10-16: public is fine, with nothing sensitive or personal in it). The records sit inside an HTML comment as JSON lines, under one visible line: "Issue pipeline usage records (hidden; see the wiki)". Editing doesn't notify the issue's subscribers, and the log lives on GitHub, so it works from any machine. -
Size: a comment holds 65,536 characters, roughly 100 to 200 records. When the next record wouldn't fit, the runner starts a new comment with the marker
usage-2, thenusage-3, and so on. -
Writes: the runner sends each record line to the ops
usageop, since only the ops CLI runs gh; the op reads the comment, appends the line and edits it, or posts the next comment when it is full. Only the job holding the issue's lease writes, and the runner writes one record at a time, so writes never race. A failed write is retried once, then logged and dropped: usage is never worth stopping an issue. -
When: the runner appends one
sessionrecord whenever a session ends: a reply, a handoff, a context steer or stop, an error, or an abort on SIGTERM. A killed job writes nothing for its unfinished sessions. Their tokens still count in the issue's budget, and those records are accepted as lost. -
Public, so nothing sensitive or personal:
usageRecordbuilds every record from an allow-list of fields, and:- holds only numbers, booleans, timestamps and names from fixed sets: roles, stages, machine states, session names, agents, model ids, tool names, the end kinds and reply decisions;
- never holds the text of prompts, tasks, replies, summaries, tool calls or tool output, nor error messages, which can quote code, paths or secrets;
- names input files only by their path in the repo or wiki, or by their run-dir file name (
issue.md,spec.md), never by an absolute path, so no user name or machine layout appears; - holds no host name, user name, process id, lane or pi session id. The job id is a random id.
The serializer refuses a string that matches none of these, and refuses
-->, so a record can't break out of the HTML comment. State, session, agent and tool names must match^[A-Za-z0-9][\w.:-]{0,99}$, and a model id may add one/and a second such name, as inanthropic/claude-opus-5-5. A tool name that doesn't match is counted underother.
- A
sessionrecord:
{
kind: "session";
at: string; // when it finished
repo: string; issue: number; job: string; // job: a random id
jobStartedAt?: string; // the job's start, as the Pipeline-Session trailer names it
key?: string; // the session's launch key, as the trailer names it
role: string; // "spec-writer", "implementor", ...
stage: string; // the issue's stage when the session ran
state: string; // the machine state or linear step, e.g. "implementer", "rereview"
name: string; // the session's name, e.g. a lens's
agent: string; model: string;
attempt: number; // retry, continuation or round number, from 1
ended: "reply" | "handoff" | "context-stop" | "error" | "aborted";
reply: string | null; // the reply's decision or verdict, e.g. "done", "blocked", "pass"
seconds: number;
turns: number; // assistant messages
tokens: { input: number; output: number; cacheRead: number; cacheWrite: number;
cacheWrite1h?: number; cost?: number }; // 1h writes are part of cacheWrite; cost in US dollars
context: {
first: number; // the first turn's context: what the session loads before doing anything
peak: number;
atFinish: number; // the last turn's context
soft: number; hard: number; // the limits
window: number | null; // the model's window; null until a module knows it
steered: boolean; // passed `soft`
};
task: { chars: number; files: Record<string, number> }; // the task's size, and each input file's, in bytes, keyed by repo path or run-dir name; `files` is {} until roles name their input files
tools: Record<string, { calls: number; outputChars: number }>; // per tool, from tool_execution_end
largestToolOutput: { tool: string; chars: number } | null;
}-
A
jobrecord, appended to the same comment when a job ends, and tojobs.jsonlin the issue's run folder: the issue, the job id and start, the role, the stages it went through, its outcome, its seconds, the sum of its sessions' tokens and cost, and the issue's token total so far (issueTokens). Records written beforekey,jobStartedAt,cacheWrite1handcostexisted lack them, so those fields are optional. -
Context at a turn is measured as
contextLimitsmeasures it today (context-watch.ts): one assistant message's input, cache-read and cache-write tokens.
Finding bloat: node report.mjs usage --config <path> [--since <date>] [--role <role>] [--issue <n>] lists the repo's issue comments changed since the date (GET /repos/{owner}/{repo}/issues/comments?since=, 100 per page), keeps the pipeline account's comments with a usage marker, parses their records, drops those whose at is before --since, and prints, by script:
- per role, state and model: count, and the median, 90th percentile and maximum of
context.first,context.peakand total tokens, by nearest rank (the value at index ⌈p·n⌉−1 of the sorted values); - sessions flagged, each with its reason:
-
context.firstaboveusageFlags.firstContext(default 40,000): the task, the prompts or the input files are heavy before any work; -
context.peakpastsoft; -
largestToolOutputaboveusageFlags.toolOutputShare(default 0.25) ofcontext.peak: one read or command output dominates; - an issue and state whose
context.firstgrows across attempts, continuations or rounds: earlier rounds are being carried forward;
-
- the five tools with the most output, per role.
The flag thresholds are in the config under usageFlags. They only label the report: they stop nothing.
- Each role declares its wiki access per stage in its own spec. Built (#303): the access sits in one table,
STAGE_WIKI_ACCESSinsrc/job/roles.ts, besideSTAGE_ROLESand the rest of the job boundary, rather than in each role's module; spec, merge and publish arewrite, every other stageread. From 2026-10-17 a bridge kept implement, fix and checkswritetoo, until the spec writer committed the spec's wiki updates; #334 removed it, so only the spec writer and the merger stages write the wiki:- every stage has at least
read(every-role-reads-wiki, Axlog 2026-10-16; built in #338, whereSTAGE_WIKI_ACCESSis typedread | write), the orchestrator's sessions inside stage jobs included.nonesurvives only as what a repo with no wiki, or a wiki mirror that can't be cloned, gets: nodocs/; -
read: a checkout at the wiki's base branch (base) or at the issue's wiki branch (issue's branch), with no commit tool for it; -
write: a checkout at the issue's wiki branch,agent/issue-Nin the wiki, or at the wiki's base when that branch doesn't exist yet.pipeline_commitcommits and pushes it.
- every stage has at least
- The role's lifecycle provisions the checkout before its first session: it fetches the wiki mirror under the wiki lock and clones it into
<job dir>/docs, beside the code checkout<job dir>/repo(separate-checkouts, Axlog 2026-10-16; built in #338). The job dirwork/<slug>/jobs/issue-N-<stage>is not a git repo itself, the wiki is no longer nested in the code checkout, and the code clone'sinfo/excludegets no wiki line. When sandboxing comes, the job dir is what is mounted at/app. Sessions run in the job dir and are told whatrepo/anddocs/are and what they may do withdocs/. Lanes keep their wiki at<worktree>/<wiki dir>. - The layout is decided once, in
paths(src/config.ts):jobRepoandjobDocsname the two folders (fromsrc/job/layout.ts, which the sandboxed workflow bundle can import), and, in a stage job,paths.wikiis the code checkout's siblingdocs, so the claim, the push,exit-pushandwikiAheadfinddocs/with the ops request'scheckoutunchanged. Rejected (#338): awikifield on thatcheckout, which would widen the ops request and let each caller build the layout. - Each stage also has a code access (
codeAccessOfinsrc/job/roles.ts, #338):writefor writer stages,readfor reader and merger stages.--print-boundaryshows both asaccess: { repo, docs }. - A repo whose wiki doesn't exist yet gets no wiki checkout, whatever the role declares.
- A role with
readaccess that edits a wiki file has the edit dropped in teardown and recorded inlostChanges, like any uncommitted change. - The wiki's
agent/issue-Non GitHub replaces the wiki mirror's localagent/issue-Nas where an issue's wiki work lives between jobs.saveWikiis kept only for commitspipeline_commitfailed to push.
- It is a pi tool, provided by a pipeline extension (
-e pipeline-commit.mjs) to the sessions that commit: the spec writer (stubs and tests,spec-writes-failing-tests), the implementor's sessions, and the merger's wiki conflict resolver (proposed: part of the spec writer). It is how a session commits, and every successful commit is pushed. -
Paths a session may commit, set by its role and checked before git runs, except the spec writer's:
- the spec writer: test files (
testPathGlobs), the stub files of itsnewinterfaces, and the wiki (spec-writes-wiki). A stub can be any source file, sopipeline_commitlets it commit any path, andspec-tests-checkenforces the test-file and stub rules after the session (#334); a broken rule retries the session; - the implementor's sessions: any code or test file, but no wiki file (
implementor-reads-wiki); - the merger's wiki conflict resolver: the wiki only. Proposed (
merge-only-when-ready): it moves to the spec writer, whose wiki access already covers it. - A refused path fails the call before any commit, naming the path.
- the spec writer: test files (
-
Parameters:
{ args: string[] }, the argumentsgit commitwould take, such as["-am", "Add the parser"]or["-m", "Fix", "--", "src/a.ts"]. -
Paths relative to the job dir (built in #338): pathspecs start with
repo/(the code checkout) ordocs/(the wiki checkout), which is stripped before git sees them;repoordocsalone means the whole checkout. Any other pathspec is refused, naming it. A relative-F/--filepath is read from the job dir in both checkouts' commits. -
Write access is checked first (
commit-needs-write-access, Axlog 2026-10-16; built in #338): a pathspec into a checkout the stage doesn'twriteis refused before any git command runs, with an error naming the path, the checkout and the access, such asdocs/Page.md: this stage has read access to the wiki checkout (docs/); it may not commit there. Adocs/path in a job with no wiki checkout is refused too. A refused call stages nothing, commits nothing and pushes nothing, rather than leaving the edit for teardown to drop. A call with pathspecs commits only the checkouts they name; with none, it commits every checkout the stage may write. Access is the stage's declared access, since one GitHub account does all the pipeline's work. -
Code repo: it runs
git commit <args>in the job's code checkout,repo/, so staging,-a, pathspecs,-m/-Fand empty-commit refusal behave asgit commitdoes. -
Wiki, transparently, when the session's wiki access is
write:-
docs/pathspecs are routed to the wiki checkout, andrepo/ones to the code repo. - With no wiki pathspec, every change in the wiki checkout is committed: modified, deleted and new pages (
git add -A), with the same message. - With a wiki pathspec, only the named wiki paths are committed.
- The session never runs git in the wiki, and needs no flag for it.
-
-
What counts as a commit: the call succeeds when at least one repo made a commit. A wiki-only commit succeeds even though the code repo had nothing to commit. With nothing to commit in either repo, it fails as
git commitdoes ("nothing to commit"). A git error in either repo fails the call, and neither repo is pushed. -
Refused arguments, which fail before git runs:
-
--amend, because the commit it would replace was already pushed; -
--no-verify/-n; -
--author,--dateand--reset-author, because commits keep the pipeline's identity; -
--dry-runpasses through as git's own dry run and pushes nothing.
-
-
Push: after the commits, the tool asks the job over its control socket to push. The session holds no token and never pushes itself.
- The job pushes the code branch to
origin agent/issue-Nwith--force-with-leaseagainst the head it last pushed. - It pushes the wiki's
agent/issue-Nunder the wiki lock. - Both use the writer's token and
ISSUE_PIPELINE_OPS=1, so the pre-push hook lets them through. - This is the same push the
pushop does, through the same function (pushinsrc/checkouts.ts). - Built (#303): the job serves the request in-process (
pushServerOfinsrc/job/commit-push.ts, one request at a time), throughpushHead, which thepushop shares, with the ops' credentials; a code push is recorded aspipeline-commitinpushed.json, which a claim adopts where it adoptsexit-push.
- The job pushes the code branch to
-
During a rebase (the conflict resolver in
sync, proposed in the start sync of an implementor entry,sync-at-edges; and the wiki conflict resolver, proposed in the spec writer):- the commit records the resolution and pushes nothing, because the branch head is detached;
- the role's lifecycle runs
rebase --continue, gives the resolver the next conflict if there is one, and pushes once infinish-sync(orpublish) after the rebase checks pass; - the tool's reply says the push was deferred.
- Built (#303): the job also defers every push in the
sync,mergeandpublishstages, wherefinish-sync,finish-wikiandpublishpush after their checks. Until a lifecycle runsrebase --continue, the resolver still runsgit addandGIT_EDITOR=true git rebase --continueitself, the one exception to "never runrebase" below; the sequencer's commits skip the pre-commit hook.
-
Reply to the session:
-
{ code: { exitCode, stdout, stderr, sha | null }, wiki: { exitCode, stdout, stderr, sha | null } | null, pushed: "both" | "code" | "wiki" | "none" | "deferred", pushError: string | null }; -
exitCodeand the output are git's own; -
wikiis null for a session with nowritewiki access.
-
-
A failed push keeps the commits, returns
pushErrorand doesn't fail the call. The nextpipeline_commitpushes everything still unpushed. Teardown pushes once more, and only then records the commits as unpushed. -
Each push is recorded in
job.json, under the launch that made it: the sha pushed, the repo and the time. The flows (codeChanged,wikiAhead) are worked out after the sessions from git, not from the tool's replies. -
CI cost: pushing every commit runs CI more often than pushing once per stage, as
start-pr's push does today for the draft. Proposed (spec-writer-opens-pr): the spec writer opens the draft only atready, so CI triggered by pull-request events doesn't run on its earlier pushes. Every push moves the head, so any verdict given on an older head no longer holds (verifiedSha,passedSha).
-
The prompts: writer prompts say to commit only through
pipeline_commit, and that the session must not rungit commit,push,rebase,reset,checkoutormergeitself. Built (#303) for stage jobs' implementer, fixer, CI fixer and conflict resolver, with the resolver'srebase --continueexception; lanes keep plain git. -
A pre-commit hook: it refuses commits except from
pipeline_commit(ISSUE_PIPELINE_COMMIT=1), in the code checkout and the wiki checkout both. The pre-push hook stays. Built (#303) in job checkouts (guardCommitsinsrc/clone-setup.ts, called fromcloneInto); lanes' clones get none. -
No token in sessions: writer sessions no longer get
GH_TOKEN. That changessandbox-ready-jobs: only jobs, never sessions, hold a token. Built (#303). - What is still possible: with bash, a session can still set the variable, run git itself, or push with Axlog's keyring login. Like today, the hooks only stop accidents until the sandbox comes. See the first open question.
- A job's
job.jsonshows the steps it ran, and every op among them is one its role's lifecycle names. - No session's environment holds a gh token.
- Every commit a writer session made is on GitHub when the session ends. Only a push failure leaves one behind, and teardown retries it.
- A wiki change made by a writer is on the wiki's
agent/issue-Nwith no step butpipeline_commit. - A role with
noneorreadwiki access never pushes the wiki. - The ops' replies and GitHub effects are unchanged, and their tests pass unchanged.
| Interface | Kind | Where | Status |
|---|---|---|---|
each stage's wiki access, STAGE_WIKI_ACCESS
|
data |
src/job/roles.ts, beside STAGE_ROLES (moved from the role modules, #303) |
built |
a machine's TABLE and transition(state, result)
|
data and pure function | new, one module per machine role (src/roles/<role>/machine.ts) |
built (shared types in src/roles/machine.ts; each role's table comes with its part) |
the runner: runMachine(machine, entry, effects)
|
function, with effects injectable | new, src/job/runner.ts
|
built |
commitArgsOf(args, scope) → { code, wiki, dryRun } argv, or the refusal; scope is { wikiDir, wikiAccess, allowed }
|
pure function | new, src/job/pipeline-commit.ts
|
built (#303) |
pipeline_commit tool |
pi extension tool | new, src/job/pipeline-commit-tool.ts, built as pipeline-commit.mjs
|
built (#303) |
| the job's push request over the control socket | socket message |
src/job/control.ts, with the messages in src/job/control-protocol.ts and the handler in src/job/commit-push.ts
|
built (#303) |
a session's reply check over the control socket, which structured_output asks before it ends the session |
socket message |
src/job/control.ts and src/job/control-protocol.ts, with the checks in src/job/reply-checks.ts; a role's session effect names the check (replyCheck) |
built for the spec writer (spec-checks-in-session) |
job.json steps and pushes |
file | src/job/status.ts |
built (pushes #303) |
the usage comment, and usageRecord(launch, context) building a record from a finished launch |
GitHub comment and pure function | new, src/job/usage-log.ts, with the comment in src/job/usage-comment.ts and the write in the ops usage op (src/ops/usage.ts) |
built |
report.mjs usage |
CLI over the usage comments | new, src/report.ts and src/report/usage.ts
|
built |
each role's locate(durable) → state
|
pure function over the issue's state, GitHub and the remotes' heads | new, in each role's module | built for triage (src/roles/triage/locate.ts, #304); proposed for the others |
| each op's check before it acts | ops replies | src/ops/* |
built for keyed post comments (commentByKey, #304); proposed for the others |
| ops replies | CLI and functions |
ops.mjs, src/ops/*
|
today, unchanged |
Unit tests on the pure functions, and integration tests that use local bare repos as the code and wiki remotes, with no GitHub.
| Behaviour | Test | Interface |
|---|---|---|
Only the spec writer and the finalizer have write wiki access (spec-writes-wiki), and only the spec writer's sessions commit to the wiki |
table check over each role's wiki access | role modules |
| A session's commit outside its role's paths is refused before git runs, naming the path | unit | commitArgsOf |
--amend, --no-verify, -n, --author, --date and --reset-author are refused before git runs |
unit | commitArgsOf |
docs/ pathspecs go to the wiki and repo/ ones to the code repo, without the prefix; any other path is refused; a relative -F is read from the job dir (#338) |
unit | commitArgsOf |
| A path in a checkout the stage only reads is refused before git runs, naming the checkout and the access, and changes nothing (#338) | unit, and integration with bare remotes |
commitArgsOf, pipeline_commit
|
A job dir holds repo/ and docs/ side by side for a reader and a writer stage, a triage job gets docs/ and its edits there are lost as docs/…, and a docs/-only commit is exit-pushed (#338) |
integration, fake pi |
job.mjs, job.json
|
| A code change and a new wiki page in one call make two commits with one message, and both are pushed | integration, bare remotes | pipeline_commit |
| A wiki-only change succeeds and pushes only the wiki | integration | pipeline_commit |
| Nothing to commit anywhere fails with git's exit code and message | integration | pipeline_commit |
A session with read wiki access commits code only, and its wiki edits are dropped and listed in lostChanges
|
integration |
pipeline_commit, job.json
|
A push the remote refuses keeps the commit, returns pushError, and the next call pushes both commits |
integration, refusing remote | pipeline_commit |
During a rebase the commit records the resolution, pushes nothing and replies deferred; finish-sync pushes |
integration |
pipeline_commit, finish-sync
|
git commit run directly in a writer's checkout is refused by the hook |
integration | hook |
No session's environment holds GH_TOKEN or gh's token variables |
unit |
agentEnvOf in src/job/roles.ts
|
| The ops' existing tests pass unchanged | existing suites | ops |
transition returns only edges in TABLE, and throws on a result the state doesn't accept |
property test over each machine |
TABLE, transition
|
Only session states return a session effect |
table check over each machine | TABLE |
| Every cycle passes through an edge guarded by a count with a limit | graph check over each TABLE
|
TABLE |
| Every state is reachable from an entry, and every non-end state reaches an end | graph check | TABLE |
| Only hand-off fields and the role's listed checkpoints reach the issue's state | unit | runner |
| Kill anywhere: for each role, kill the run before and after every effect in turn, start it again, and compare the end with an uninterrupted run: the same issue state and GitHub effects, with no duplicate comment, PR, sub-issue or label change. Sessions may run more than once | property test, runner with injected effects and the fake gh
|
every role, through locate and the runner |
| Every op that changes GitHub or a remote, run twice, changes nothing the second time | ops CLI, each op twice | ops-test-kit.mjs |
A start ignores job.json, the old checkout and old session files: it checks out fresh and locates from durable state only |
integration, SIGKILL | runner |
| A killed job's lapsed lease is taken by the next claim, which carries on at the saved stage | ops CLI | claim |
| A count that bounds a loop is saved when it goes up, so a killed and restarted job keeps its limit | unit | runner, checkpoints |
| An orchestrator killed mid-tick and restarted starts no job twice | recorded snapshots |
tick, start records |
A network error ends held, and an unlisted failure ends stopped with the step-failure code |
unit with injected effects | runner |
A session that ends in each way (reply, handoff, context stop, error, SIGTERM) appends exactly one session record with its role, state, attempt, tokens and context |
unit with injected effects | runner, usageRecord
|
context.first, peak and atFinish come from the first, highest and last turns' usage |
unit, fake RPC events | usageRecord |
Per-tool output chars and the largest output are counted from tool_execution_end events |
unit, fake RPC events | usageRecord |
Records go into the issue's usage comment, edited in place by its marker; a full comment rolls over to usage-2
|
integration, fake gh
|
runner, usage log |
| A failed usage write is retried once, then dropped without stopping the issue | integration, fake gh
|
runner, usage log |
| No record holds prompt, task, reply, summary, error or tool text, an absolute path, a host or user name, a pid, a lane or a pi session id | unit, property |
usageRecord, serializer |
The serializer refuses a string outside the allowed sets, and -->
|
unit | serializer |
The report finds usage comments across issues with one paged since listing and ignores other accounts' comments |
unit, fake gh
|
report.mjs usage |
| The report's percentiles and each flag, on a fixture log | unit | report.mjs usage |
next(stage) saves the issue's state before entering the next stage, in the same checkout |
unit with injected effects | runner |
-
Bash for writers: should writer sessions keep bash (proposed for now, with hooks as accident guards), or lose it for a
runtool limited to the config'scommands(test, lint, typecheck)? Losing it enforces "onlypipeline_commit", but stops the implementer from running anything else, such as codegen or installing a package. -
Wiki without pathspecs:
-ain git doesn't add untracked files. Should a wiki commit with no wiki pathspec add new pages anyway (proposed), or follow the code repo's staging exactly? -
Message: should the wiki commit's message say which code commit it went with (for example "(with abc1234)"), so the PR body and
finish-wikican pair them? -
Retiring lanes: is retiring lanes and
workflow.jswithorchestrator-cutoveracceptable? It is needed if the lifecycle replaces the ops-as-launches protocol. -
Re-review inside a fix round: should it stay as a session inside the implementor's
fixlifecycle? Or shouldfixend at the fixer's push, with re-review as its own reviewer stage (rereview) in its own job? A separate stage keeps one role per job, at the cost of a job start per round.