I started this project with three agent sessions open and the fairly optimistic assumption that more workers would get the lab revision finished sooner. It only took a few runs before two agents had covered some of the same ground, another had followed generic Markdown advice instead of the lab rules, and I had created more review work for myself.
The project was a private repository of hands-on technical labs. Codex, Claude Code and GitHub Copilot CLI could all create valid Markdown, plausible commands and tidy-looking changes. The problems appeared when I reviewed the changes together: the writing did not always sound like the existing labs, transitions between challenges were repeated or lost, and repository-specific rules were occasionally replaced by generic advice.
None of those failures looked dramatic in an individual terminal. Most of the output was close enough that I had to read the whole learner journey to spot what had gone wrong.
Adding more instructions to one universal prompt made it harder to maintain, and another wall of terminal panes only gave me more places to look. What I was missing was a coding-agent scheduler: a small control plane that decides what work is ready, who owns it, which context they need, how the result is proven and who is allowed to integrate it.
This post covers the scheduler I built around Codex, Claude Code and GitHub Copilot CLI. The implementation is private, so the examples below are sanitised, but the design can be reproduced with normal Markdown, Git worktrees, a few scripts and your existing pull request process.
What do I mean by a coding-agent scheduler?
The name needs a quick explanation. Here, scheduling means coordinating work as it becomes ready; it has nothing to do with cron or calendar time.
The scheduler sits between a request and the repository. It turns an idea into an approved goal, creates an ownership graph, routes bounded assignments to suitable agents, checks that each worker can load the right project instructions, and sends completed work through one integration queue.
In text, the flow is:
rough request
-> approved goal and measurable exit conditions
-> dependency and file-ownership graph
-> controller schedules bounded workers
-> review findings receive explicit decisions
-> acceptance evidence is reconciled
-> one pull request queue approves or returns the work
A terminal manager can show me that several agents are active. The scheduler also records whether two agents own the same file, whether a reviewer used the correct project skill, and whether someone has mistaken a passing parser for proof that a hosted lab works.
The first design failed because project knowledge got lost
My first attempt installed reusable agent roles, lifecycle hooks and a general lab-writing skill. On paper, it was sensible. In practice, the global skill started competing with the repository’s own guidance.
The generic skill knew how a learning exercise should normally be structured. The target repository knew how these labs should sound, how one challenge should hand state to the next, which checks mattered and how the platform behaved. When those two sources disagreed, the generic workflow could win whilst the result still passed a Markdown check.
After that first attempt, I drew a firm boundary. The orchestration layer coordinates the work, whilst the repository remains the source of truth for the domain.
This boundary also works for an application repository. A shared scheduler may decide when to run a security reviewer, but the repository should define its supported build, threat model, architecture and release rules. The scheduler should not arrive with a second, slightly different version of the project.
Stage one: shape the goal before launching workers
A worker can make a surprisingly large change from a vague brief before anyone notices that it has interpreted the goal differently. I therefore have the controller turn each request into:
- the observable outcome;
- what is in and out of scope;
- constraints and actions that still require approval;
- measurable exit conditions; and
- the evidence required for each important claim.
For substantial work I use an acceptance-to-evidence matrix:
| ID | Outcome | Owner | Evidence lane | Planned evidence |
|---|---|---|---|---|
| A1 | Changed files parse | writer | static | parser and unit tests |
| A2 | A fresh learner can complete the flow | track lead | hosted runtime | clean environment transcript |
| A3 | The result matches the existing voice | editorial reviewer | manual review | rubric with cited comparisons |
The lane prevents me from using evidence from one environment to make a claim about another. A static test proves that YAML parses, whilst a hosted run is needed to show that the application reaches the required state. In the same way, one screenshot shows that a page rendered on that occasion; repeatable lifecycle behaviour needs evidence from a clean run. I only move an item to a stronger lane when the corresponding evidence has actually been collected.
Stage two: split work around ownership
I first split the work by directory because that was convenient for Git. Once I reviewed a complete learning track, it became clear that the directory boundaries did not match the way a learner experienced the material.
For a lab project, I assign one lead to each complete learning track. A lead can delegate coherent groups of challenges to writers and use separate read-only reviewers for pedagogy and platform lifecycle. The lead still owns the end-to-end learner journey.
overall controller
└── learning-track lead (repeat for each independent track)
├── challenge-group writer
├── read-only pedagogy reviewer
└── read-only lifecycle reviewer
The track lead reviews all the transitions after the writers return their challenge groups. That gives the writers room to work in parallel, with one person still responsible for checking that the sections form a complete learner journey.
The same approach applies to code. Split a feature around stable interfaces and clear ownership, not simply because src/ contains four directories. If two assignments may edit the same file or decide the same interface, schedule them in sequence.
I chose three concurrent workers as a starting point. There was no data behind a larger number, and the early runs already showed how quickly repeated context, merge conflicts and review work could grow. I will raise that ceiling if the results show that another worker improves accepted output, rather than because I have space for another terminal pane.
Stage three: give every worker a small contract
A worker starts with a fresh context, so the assignment has to carry the information needed to begin. I also keep it short because a handoff stops being useful when the task is buried halfway down it.
Here is a sanitised example:
ASSIGN LAB-REVIEW-02
objective: align one challenge group with the approved tone rubric
scope: challenges/02-* through challenges/04-*
skills: lab-author, editorial-voice
base: review-branch@<commit>
dependencies: RUBRIC-01
acceptance: commands remain accurate; transitions preserve learner state
verify: parser, link check, rubric review
constraints: do not edit shared config; do not publish
budget: 35 minutes; one retry; stop on missing environment evidence
return: outcome, commit, checks, risks and evidence; no raw logs
The scheduler records the same contract in a machine-readable sidecar so deterministic checks can inspect it before launch. Each handoff contains the exact base commit, context files, writable output, required tools and skills, delegation policy, budget, verification commands and halt response.
A blocked worker should stop and return useful state:
BLOCKED: <one missing decision or dependency>
tried: <brief evidence>
options: <A and B when a choice exists>
recommend: <preferred option and why>
state: <branch, last completed check and remaining risk>
When an agent waits for a decision, it consumes allowance and leaves the task looking active. Returning the state makes the pause obvious and lets a human answer in the worker’s visible session. This also saves the controller from becoming a rather expensive messenger service.
Where I put instructions, skills, agents, hooks and tools
All three providers expose customisation features with overlapping names. I initially found it tempting to repeat important rules across every available surface. That added context and made it harder to tell which copy of a rule the worker had followed.
I use this split:
| Surface | What belongs there | Avoid putting here |
|---|---|---|
| Project instructions | Stable repository facts, build commands and non-negotiable rules | A long procedure needed for one occasional task |
| Skill | Reusable procedure or specialist project knowledge | Always-on session telemetry |
| Custom agent | A role, tool boundary, model choice and return contract | The whole project handbook |
| Hook | A deterministic lifecycle action or short reminder | Open-ended semantic routing |
| CLI or script | Repeatable validation, state checks and transforms | Editorial or architectural judgement |
I implemented the four scheduler stages as reusable Markdown skills before adding more automation:
.agents/skills/
├── shape-goal/SKILL.md
├── to-tickets/SKILL.md
├── orchestrate-agents/SKILL.md
├── gatekeep-prs/SKILL.md
└── project-domain/SKILL.md
The first four belong to the orchestration layer. project-domain belongs to the repository and can carry the lab, application or documentation rules that should not become a global default.
My project initialisation script only adds a missing entrypoint or local-artifact ignore block. Existing instructions, agents, skills and hooks are left alone, which avoids replacing working repository configuration during setup.
Codex skills use progressive disclosure: the initial context contains skill metadata, and the complete SKILL.md loads when the skill is selected. Codex can discover repository skills under .agents/skills from the working directory towards the repository root. Its subagent workflow provides isolated workers for parallel or noisy tasks, whilst the controller retains the main decisions.
Claude Code skills package reusable instructions and resources, whilst Claude Code subagents run with their own context, tools and permissions. A fresh subagent starts without all the useful details from the parent conversation, so those details need to be present in the handoff.
GitHub Copilot CLI skills can live in .github/skills, .agents/skills or .claude/skills. Its custom agents execute as subagents with a separate context window.
The shared Agent Skills format means the project can retain one canonical domain skill:
.agents/skills/lab-author/SKILL.md # canonical project skill
.claude/skills/lab-author/SKILL.md # thin adapter when required
.github/skills/lab-author/SKILL.md # thin adapter when required
The provider adapters only describe discovery and invocation differences. The actual project rules stay in the canonical skill.
Verify the skill entrypoint before launching the worker
One of the most useful scheduler checks came from a very ordinary failure: the handoff said use lab-author, but an external worker started in a worktree from which that skill was not discoverable.
The model did what models often do when context is missing. It carried on with something plausible.
Before launching an external worker, my preflight now:
- resolves the provider and confirms its CLI is available;
- starts from the worker’s real directory;
- follows that provider’s project and user skill search order;
- checks that each
SKILL.mdis readable and declares the requested name; - records the selected entrypoint and a SHA-256 digest;
- verifies the exact base, context, tools, output boundary, budget and acceptance matrix; and
- clears stale resolution state and blocks launch if any check fails.
I use the digest to record exactly which copy of the skill was selected at launch. If that file changes before the worker starts, the worker stops and asks for a new preflight. The digest says nothing about how well the model followed the instructions, which is why the later review and acceptance checks are still required.
The current implementation has thirteen automated tests for this path, along with live no-launch preflights for Codex, Claude Code and GitHub Copilot CLI using two independent project skills. These checks tell me that discovery works and the worker is ready to launch. The quality of its eventual changes is checked later.
When should the scheduler use native subagents or external sessions?
My routing starts with a couple of questions:
Is the task small or tightly coupled to the controller's decisions?
yes -> keep it in the controller
no -> is it bounded and unlikely to need intervention?
yes -> native subagent
no -> visible external session with a preflighted handoff
Native subagents are the default for bounded exploration, tests, log analysis and read-only reviews. They keep noisy intermediate work out of the controller’s context and return a compact result. OpenAI also notes that Codex subagents use additional tokens, so I account for that extra usage before assigning work.
For a longer job, cross-provider work, a separate worktree or likely human intervention, I use an external CLI session. A terminal cockpit makes it easier to see which session needs attention. I still mark the work complete from its commit, checks, evidence and pull request state rather than from what the pane happens to show.
Git worktrees also need a cleanup owner
Concurrent writers need separate checkouts. Git worktrees allow several working trees to share one repository whilst using different branches.
git fetch origin
git worktree add ../task-a -b agent/task-a origin/main
git -C ../task-a rev-parse --abbrev-ref HEAD
git -C ../task-a rev-parse HEAD
The scheduler records that branch and commit in the handoff. Two writable workers never own the same file at the same time.
The separate checkout fixed the isolation problem, although it introduced some lifecycle work that I had not accounted for. During development, I nearly removed a completed feature worktree as if it were a duplicate repository. Some skill adapters still pointed at it, so that tidy-up would have broken later agent discovery.
My cleanup sequence is now:
- confirm which worktree and branch produced the completed change;
- verify the accepted commit exists in the durable checkout;
- inspect adapters and symlinks for references to the temporary path;
- repoint them to the canonical source;
- rerun provider discovery from the main checkout;
- remove the worktree with
git worktree remove; and - prune stale metadata and delete the feature branch only when it is safe.
I now put cleanup ownership in the handoff as well. Without it, a temporary worktree can quietly become part of the setup because other files have started referring to its path.
Stage four: review evidence, then use one PR queue
Reviewers stay read-only. They return APPROVE or structured findings containing severity, class, exact evidence, reproduction, expected and actual behaviour, a proposed correction, the authoritative evidence lane and confidence.
The controller gives every finding one disposition:
- accepted;
- rejected as factually incorrect;
- rejected as a preference;
- duplicate; or
- deferred.
Accepted medium- and high-severity findings are reproduced before being sent back to the original writer. Reusing that writer keeps ownership and context stable. One correction round is normally enough; repeated failure means the assignment, model or missing decision needs to change.
Writers open pull requests but do not merge them. A single gatekeeper sorts eligible work by priority and then creation time, inspects one pull request, and checks reviews, CI, conflicts and acceptance evidence.
The queue has four states:
PENDING -> an external check or review is incomplete; revisit on an event
BLOCKED -> return one deduplicated diagnostic to the owning writer
APPROVE -> every gate passes, but merge is not authorised
MERGED -> identity was rechecked and merge was explicitly authorised
If a contributor’s branch needs a repair, the finding goes back to that contributor. This keeps the integration agent out of the writer role and leaves ownership of the fix clear.
Immediately before an authorised merge, the gatekeeper fetches again and compares the recorded head and base identities. If either changed, the gate restarts. It is one extra fetch that avoids discovering later that the reviewed commit and the merged commit were different.
Keep hooks limited to deterministic jobs
I briefly considered using hooks to classify the user’s prompt and inject the correct domain skill. That moved a fuzzy language decision into a lifecycle mechanism and recreated the same global-versus-project problem.
The hooks now record a small set of session start and stop metadata, normalise the working directory, hash the session identifier and inject one short reminder about bounded handoffs and project-owned skills.
Prompt text, responses, credentials and source content stay out of those records. Skill selection, issue creation and the decision that a goal interview is complete remain with the controller.
That matches the deterministic role hooks are good at. Codex hooks, Claude Code hooks and Copilot CLI hooks use different envelopes and configuration, but they can call the same small executable for events handled in the same way.
How I keep provider usage under control
Provider plans, model catalogues and allowance displays change too quickly for one static conversion table. A ChatGPT allowance window, Claude subscription bar, API token bill and GitHub AI Credits are different accounting systems. I treat each authenticated provider view as the ledger and use a small local journal only to explain task-level choices.
The journal records the task class, provider, model, effort, elapsed time, retries, input size, checks, findings, accepted findings and result. When a provider does not expose a number, the journal records null. I compare the original provider figures instead of inventing a token or currency conversion between unlike plans.
To make those figures useful, I compare similar jobs and record how much of each result I accepted. For example, I would compare two repository inventories with each other, rather than comparing one of them with an editorial review that needs much more judgement.
My practical controls are:
- keep the capable controller focused on decisions and final synthesis;
- use cheaper workers for deterministic scans and mechanical changes;
- start workers with the smallest useful file set;
- pass paths and recorded decisions rather than conversation transcripts;
- load only the skills required by the role;
- return filtered errors instead of complete logs;
- set time, retry and stop conditions for every assignment; and
- use a provider-enforced cap where one exists.
For example, GitHub documents a public-preview soft AI credit limit for Copilot CLI sessions:
copilot -p "<bounded assignment>" --max-ai-credits NUMBER
The response already in progress may finish above that soft threshold, so the handoff still needs a time, retry and halt policy.
What is verified, and what am I still measuring?
The four workflow stages now run across the three provider tools. Project initialisation leaves existing files alone, domain skills remain in the project, each external handoff is checked before launch, and reviews use the same set of states.
The skill resolver and handoff path are covered by thirteen automated tests. Live no-launch preflights have passed for the three provider CLIs. I have also tested cleaning up a completed worktree and then rerunning skill discovery, because that was the path I nearly broke during development.
I am still measuring how the scheduler performs on real repository work, so I cannot yet say whether it has improved tone, continuity, technical accuracy, speed or cost. For each run, I record findings by class, accepted and rejected findings, retries, elapsed time, provider-reported usage and the validation lanes actually completed.
If the result is worse, the evaluation should show which worker had the task, what context and skill were selected, which evidence was missing and who made the final decision. That gives me something specific to change before the next run.
Build the smallest useful scheduler first
I would begin with six pieces before writing a custom CLI:
- Put stable repository rules in the provider instruction files.
- Create one canonical project skill for the specialist procedure agents repeatedly need.
- Define one controller contract with measurable exit conditions and a small worker ceiling.
- Give each worker exact ownership, a budget, verification and a halt response.
- Keep reviewers read-only and record a disposition for every finding.
- Require pull requests and give one gatekeeper integration authority.
A starter file structure
If the four workflow skills are already installed in your user profile, the repository only needs its own project-domain skill and the entrypoint required by each provider. I prefer the self-contained layout below when the workflow needs to travel with the repository. Claude Code needs the .claude/skills entrypoints shown here; Copilot and Codex can discover the canonical copies under .agents/skills.
project/
├── AGENTS.md
├── CLAUDE.md # Claude Code entrypoint
├── .agents/
│ └── skills/
│ ├── project-domain/
│ │ ├── SKILL.md # canonical project knowledge
│ │ └── references/ # optional larger guidance
│ ├── shape-goal/SKILL.md
│ ├── to-tickets/SKILL.md # optional when using GitHub issues
│ ├── orchestrate-agents/SKILL.md
│ └── gatekeep-prs/SKILL.md
├── .claude/
│ └── skills/ # required for Claude Code
│ ├── project-domain -> ../../.agents/skills/project-domain
│ ├── shape-goal -> ../../.agents/skills/shape-goal
│ ├── to-tickets -> ../../.agents/skills/to-tickets
│ ├── orchestrate-agents -> ../../.agents/skills/orchestrate-agents
│ └── gatekeep-prs -> ../../.agents/skills/gatekeep-prs
├── .github/
│ ├── copilot-instructions.md # Copilot repository entrypoint
│ └── agents/
│ ├── scheduler-controller.agent.md # optional repeated role
│ └── scheduler-reviewer.agent.md # optional repeated role
└── .agentflow/
├── config.json # optional tracked policy
├── handoffs/ # generated locally and ignored by Git
├── logs/
└── tmp/
Copy the complete skill folders if they contain a references directory; several workflow skills load those files as part of their procedure. The example uses directory symlinks for Claude Code so that every provider reaches the same files. If your repository cannot use symlinks, create a small SKILL.md under each .claude/skills/<name>/ directory that points Claude Code to the canonical .agents/skills/<name>/SKILL.md instead.
Once the canonical skill folders exist, these commands create the Claude Code entrypoints. Leave out any optional skill you did not copy:
mkdir -p .claude/skills
ln -s ../../.agents/skills/project-domain .claude/skills/project-domain
ln -s ../../.agents/skills/shape-goal .claude/skills/shape-goal
ln -s ../../.agents/skills/to-tickets .claude/skills/to-tickets
ln -s ../../.agents/skills/orchestrate-agents .claude/skills/orchestrate-agents
ln -s ../../.agents/skills/gatekeep-prs .claude/skills/gatekeep-prs
| Path | What I put in it |
|---|---|
AGENTS.md |
Stable repository facts: real build and test commands, sensitive-data rules, protected files and where the domain skill lives. |
CLAUDE.md |
A short Claude Code entrypoint that points back to the repository rules and any Claude-specific commands. |
.github/copilot-instructions.md |
The equivalent Copilot entrypoint. I keep project policy in the canonical files instead of copying it here. |
.agents/skills/project-domain/SKILL.md |
The specialist procedure for this repository, including the checks that count as evidence. This is the canonical copy. |
.agents/skills/shape-goal/SKILL.md |
Turns a rough request into an approved outcome, scope and measurable exit conditions. |
.agents/skills/to-tickets/SKILL.md |
Converts the approved goal into dependency-aware issues. Leave it out if issues are not part of the process. |
.agents/skills/orchestrate-agents/SKILL.md |
Splits approved work, assigns ownership, selects workers and collects their evidence. |
.agents/skills/gatekeep-prs/SKILL.md |
Defines the pull request gates and who may approve or merge. It is only needed for a pull request workflow. |
.github/agents/*.agent.md |
Optional Copilot role profiles once a role repeats. The controller can delegate; a reviewer stays read-only and returns findings. |
.claude/skills/ |
Required Claude Code entrypoints for the canonical skills. The directory symlinks above avoid maintaining a second copy. |
.github/skills/ |
Optional Copilot adapters if you choose not to use its .agents/skills discovery path. |
.agentflow/config.json |
Optional tracked policy such as the worker ceiling, delegation depth and default execution lane. A launcher must read this file if you expect it to enforce those values. |
.agentflow/handoffs/ |
One readable Markdown assignment and one JSON sidecar per external worker. The sidecar holds the base SHA, paths, budgets and preflight result. |
.agentflow/logs/ and .agentflow/tmp/ |
Optional local run data and temporary files. Neither belongs in the repository history. |
AGENTS.md does not need to become another handbook. A useful first version can be this short:
# Repository rules
- Build: `<real build command>`
- Test: `<real test command>`
- Load `.agents/skills/project-domain/SKILL.md` for domain work.
- Keep credentials and generated handoffs out of Git.
- Writers change only the files named in their handoff.
- Reviewers do not edit files.
- Merging always requires explicit authorisation.
The provider entrypoints can remain very small. My CLAUDE.md contains one line:
@AGENTS.md
The Copilot entrypoint does the same job in plain language:
Follow the repository's `AGENTS.md`. Keep summaries terse and use provider-neutral
handoff files under `.agentflow/handoffs/` when transferring work.
The first domain skill can also be small. Its description tells the agent when to load it, and the body records the procedure that is specific to the repository:
---
name: project-domain
description: Review and modify this repository's domain artefacts. Use for
authoring, structural review and domain-specific QA.
---
# Project domain workflow
1. Read the nearest repository instructions.
2. Load only the references required for the assigned role.
3. Preserve the documented user journey and lifecycle.
4. Cite file paths and commands for factual findings.
5. Run the narrowest authoritative checks.
6. Stop and return `BLOCKED` when required evidence is unavailable.
Finally, keep the generated worker state out of Git:
# BEGIN agentflow local artifacts
.agentflow/handoffs/
.agentflow/tmp/
.agentflow/logs/
# END agentflow local artifacts
With those files in place, open the repository root in your chosen coding-agent tool and give the controller an observable goal. Name orchestrate-agents and project-domain explicitly for the first few runs so you can confirm that both skills are discovered from the expected paths.
I would also leave hooks and custom role profiles out of the first run. Add a controller or reviewer profile when you find yourself repeating the same permissions and return format, and add a hook when there is a small deterministic lifecycle action worth automating.
I would leave the setup there for the first few runs. If skills resolve differently between providers, that is the point to write a preflight. Add a usage journal once you have comparable jobs to measure, and add a visible terminal cockpit if you regularly need to step into a running session.
I only add more parallel workers when they help useful work move without losing the intent of the original request.
Before adding another layer, I ask four questions: who owns the decision, what context does the worker load, how is the result proven, and who may integrate it? If the answers are vague, I leave that layer out. More agent activity on screen does not make the resulting pull request any easier to review.
Provider features and documentation links were checked on 22 July 2026. Skills, hooks, limits and CLI behaviour can change, so recheck the linked first-party documentation before adopting the examples.