NAME

App::karr::Foundation::Coordinator - karr-foundation's judgement layer -- the coordination agent, called only on deviation

VERSION

version 0.600

DESCRIPTION

The third layer of the fleet design. Coordination is shared (tickets, dependencies, the chain, the questions -- all in git refs), execution is local (which agent commands exist here, whether they work, how many may run), and judgement is an agent: planning, routing, and reacting to what nobody planned for.

It is an agent like every other one -- an entry in the config's agents: section, invoked through its own command under its own kind contract (App::karr::Foundation::Agents), classified from the result object it leaves behind ("_run_result" in App::karr::Foundation::Runner), and marked failing by the same availability record when it stops working. What separates it from a board agent is when it runs: never in the hot path. karr-foundation works through written plans on its own, and calls this one only where a plan is missing or has broken. The AI is the compiler, the chain is the program, karr-foundation is the VM -- and between two deviations no AI runs at all, which is what makes the whole arrangement affordable.

Which agent is the coordinator

The one whose definition says role: coordinator. That is a marker on a definition the config already carries and not a second place to configure a fleet: a top-level coordinator: name key would be a second name for an agent that is already named, and two names for one thing are two things to keep in step. A config that marks two agents refuses to guess between them.

A fleet that marks none has no judgement layer, and everything below is a no-op: the deviations are still recorded and still printed, exactly as they were before this class existed. That is the fallback everywhere here -- the behaviour karr-foundation had when the planner was a person.

The four deviations

They are the places that already recorded "the planner is wanted" and nothing else, and they are extended rather than replaced:

  • a kind: plan step, which this VM does not execute;

  • a question past its deadline whose policy is escalate_to_ai;

  • a step gone stale -- its precheck no longer holds, so the plan has gone out of date;

  • a repository the "assignment" cannot route, which is this class's own (see below).

The first three arrive through App::karr::Foundation::Executor, which already collects them for its closing report; the fourth is "route".

One call per tick, not one per deviation

A tick that finds five stale steps has learned one thing -- the plan is out of date -- and telling the coordination agent five times would cost five runs to answer it once. So "want" only records, deduplicating identical entries, and "dispatch" makes exactly one call at the end of the tick with everything that was recorded. That is also why a call happens at the end: a planner called half way through would be planning against a board its own tick was still moving.

Nothing re-reads the assignment or the chain afterwards. Whatever the coordination agent wrote is picked up by the next tick, which is the same rule as everywhere else here -- no AI in the hot path, and no tick that runs the plan it has just written without anybody having seen it.

The assignment: routing without an AI in the way

repos:
  /path/to/repo:
    - minimax
    - claude
    - WAIT

Repository path to an ordered list of agents, with an explicit WAIT for "rather wait than use something unsuitable here". karr-foundation looks the repository up and takes the first entry that currently works; that lookup is the whole of the hot path, and no AI is in it.

It is execution state, so it is local and never in refs: an agent command that exists on one machine does not exist on the next, so a routing table naming agents cannot be shared any more than the definitions it names can. It therefore lives beside agents.state and the config that defines the agents, and follows --config the way both of those do.

What it is not

Nothing domain-specific reaches karr through it: what "done" means for a project, how a release is verified, which project depends on which, arrives through a board's on_drained hook and through the operator's prose, and this class only carries that prose to the agent. There is no learning algorithm either -- the recovery records App::karr::Foundation::Agents keeps are read by the agent, not by karr. It lifts no block (a link is a fact, blocked is a decision), and it does not touch the one hard rule that one repository has one agent.

SEE ALSO

App::karr::Foundation, App::karr::Foundation::Agents, App::karr::Foundation::Executor, App::karr::Foundation::ChainStore

foundation

The owning App::karr::Foundation, held weak_ref (the foundation owns this class). Required. The config, the agent definitions, the hub and the runner all come from it.

name

The name of the agent definition marked role: coordinator, or undef when the config marks none -- in which case this whole class is a no-op and karr-foundation behaves exactly as it did before it existed.

Two marked definitions are a user error rather than a choice: "which of these is the judgement layer" has no safe default, and picking the alphabetically first one would be a guess nobody could see.

configured

next unless $coordinator->configured;

True when this fleet has a coordination agent at all. Everything this class does is gated on it, so a fleet that names none records nothing, calls nothing and prints nothing new.

assignment_file

The path to assignment.yml, the machine-local routing table -- a sibling of the foundation's _config_path, beside agents.state, so --config relocates it with everything else local.

assignment

The parsed routing table: { repos => { PATH => [ NAME, ... ] } }, plus broken with a sentence when the file is there and cannot be used. An absent file is not broken -- it is the ordinary state of a fleet before the first plan, and it routes nothing.

Repository keys are normalised through realpath where the path exists, so a board reached through a symlink or a scan: directory matches the table the coordination agent wrote from its own view of the fleet.

route

my $routed = $coordinator->route( $repo );
# undef                     -- nothing routes this board; resolve it as before
# { agent => 'minimax' }    -- the first agent in its chain that works
# { wait  => 'why' }        -- its chain says wait; no agent runs here now

What the assignment says about one repository. This is the hot path: a lookup and an availability check, with no AI anywhere in it.

The first entry that names a defined and currently available agent wins. A WAIT entry ends the search -- "rather wait than use anything further down" is the one thing an ordered list cannot say by itself -- and so does a chain whose agents are all failing, because the coordination agent wrote that chain and going past its end would be karr routing on its own.

A repository the table does not name, or names with nothing usable, is the fourth deviation: it records that the coordination agent is wanted ("want") and returns undef, so resolution carries on exactly as it did before -- default_agent, claude: true, or no agent at all.

wanted

The deviations recorded so far this tick, in the order they were seen, each { reason => ..., step => ..., repo => ... }. "dispatch" empties it.

want

$coordinator->want( step => 4, reason => 'kind: plan is not executed here' );
$coordinator->want( repo => "$repo", reason => 'no assignment names it' );

Records that the coordination agent is wanted, and returns true when it was recorded. A no-op for a fleet that marks no coordinator: the caller has already said what it wanted out loud and written it into its own log, and this class refuses to be a second such record when nobody can act on it.

Identical entries collapse, which is what makes one tick one call.

dispatch

$coordinator->dispatch;

The one call a tick may make. Returns true when the coordination agent ran.

Nothing happens without a deviation, without a coordinator, or without a hub: the agent's outputs -- a chain, a question -- live in refs/karr-foundation/*, so a fleet with no hub has nowhere to put them, and that is said once rather than worked around.

The run happens in the hub and under the hub's own .karr.lock, which is the same rule every other run here obeys: one agent per repository, however many ticks knock. It carries KARR_ROLE=coordinator, so whatever karr writes it makes land in their own activity log instead of counting as a board agent's engagement with a card (#158).

Its result decides the agent's availability exactly as a drain's does: a reported error or a non-zero exit marks it failing and every board on it waits for one probe interval, anything else says it works. The transcript is not scanned. That scan exists for a board run that moved nothing and printed a rate limit (#160); this run moves no board by definition, and a planner that prints a backlog would trip it on the backlog's own words.

prompt

my $text = $coordinator->prompt( \@wanted );

The instruction the coordination agent is given, as $PROMPT: why it was called, where the fleet's files are, which agents exist here with their availability and their prose, the operator's own prose about how to use them (config key routing:), the shapes it may write, and the boundaries it may not cross.

There is deliberately no key to replace this text with another. What an operator has to say about routing is prose and belongs in routing:, where the agent reads it in context; a second prompt key would be a place to overwrite the part that says what karr can actually read back.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)