Skip to content

refactor(agents): behaviour from agent and template rows - #385

Merged
sudoTomas merged 3 commits into
mainfrom
refactor/behaviour-from-rows
Nov 6, 2026
Merged

sudoTomas merged 3 commits into
mainfrom
refactor/behaviour-from-rows

Conversation

@sudoTomas

@sudoTomas sudoTomas commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

What embry0 did with a pipeline template or an agent by the built-in ones' names is now a column of its row: a template's fixed_route, offered_to_triage, job_kind, delivers and leader_agent, and an agent's station and quest_template. A job records who coded it and who led its template when it is planned, and is credited from that. An operator's own templates and agents take part the way the built-in ones do. Fixes #167 (part of #159); agent roles follow in #395.

Motivation — Intent + Problem

Operators write templates and agents as rows, but the behaviour around them was keyed by built-in names, so a template or agent of a deployment's own could not take part without a fork:

  • an operator's own specialist template was still triaged and offered to triage (SPECIALIST_TEMPLATE_NAMES);
  • its jobs' briefs said "the result of the work" (_TEMPLATE_DELIVERS);
  • an agent of its own could never be given a task from the guild (QUEST_TEMPLATE_BY_AGENT, can_quest), and stood at a desk (STATION_OF);
  • a party was drawn as its template's leader by name (LEADER_BY_TEMPLATE), and progression credited a pull request, a cleanup and an audit by template and agent names in SQL (_CODER, AUDIT_TEMPLATE and the event literals; the world's cleanup count likewise), so a copy of the context-audit template earned no audit;
  • the coding agents were listed twice (route_plan and salvage).

How it works

  • One module answers "what does this row say": embry0/workflows/row_behaviour.py. A row without a column (a fake, an older summary) reads as the column's default, which is how a template or agent of the deployment's own behaved before.
  • Routing. Triage looks up the template the job names (plan_route.pinned_template, the single lookup that already fetched its triage config); fixed_route skips the classifier. Triage's template list leaves out a row with offered_to_triage false, and plan_route drops triage's own pick of one (before the slop override, so hiding slop-cleanup from triage never turns off slop routing).
  • The kind a job is recorded as is the template's default_for_kind, else its job_kind override, else what its route makes it as the route is now: every step but the output a specialist is analysis (the old specialist_job_kind rule, read from the row's graph). A new specialist-only template is analysis; a specialist route given a coding step is not.
  • Credit is recorded on the job. A job that names a template takes its row's leader when it is created (jobs.leader_agent, so a pending job is known for what it is), and plan_route records it again with jobs.coding_agent (the agent its route's coding step runs as) and the job's kind. Progression and the world credit from those columns, never from today's template rows: a pull request is its coder's (the developer when it recorded none), and a slop cleanup is a merged job the Slopmaster coded (a copy of slop-cleanup keeps its Slopmaster step, so its merges are cleanups too). So an edit or a deletion of a template row later changes no past credit.
  • Which jobs are context audits is one predicate, embry0/storage/repositories/audit_jobs.py: a job the context auditor led (the agent whose report an audit's claims are read from), and not a finding's fix job. Progression (audited XP, lore claims, the clean_audit quest, audit_merged) and the findings feed both read it, so a copy of context-audit earns audit credit and offers its findings, and a context-audit job planned after an operator gave the template another leader does neither. The scheduler counts every such audit for its cadence and live check, plus every job of its own template and every job it dispatched whatever led them: those work on the files an audit opens a pull request on, and after a leader change the scheduler must not start an audit again on every tick. The job it starts is always its own template's. The QA pipeline, onboard and pr-review are never planned; WORKFLOW_PIPELINES holds their leader and deliverable once, and no template may take their names (422, on create, rename and duplicate).
  • Briefs. The candidates query joins the job's template row and reads delivers as template_delivers; it is not part of the brief's input hash, so no existing brief is rewritten by the upgrade.
  • The guild. GET /world reads the agent and template rows (kept with the board for 60 s; an agent or template write forgets them, so a change shows on the next read). A character's station and can_quest/quest_template come from its row: quest_template NULL is no task, '' a task naming no template (triage picks; only a coding agent's), a name that template; a task naming a template that no longer exists is no task. A party is drawn as the leader its job recorded, or before it is planned its template row's.
  • Write paths. API: POST/PUT /pipelines/templates and POST/PUT /agents, each value checked with a 422 naming the field, plus leader_agent: no agent named ... runs in jobs, quest_template: no pipeline template named ... and quest_template: '' for an agent that does not code. Only a value the write changes is checked: the forms send every field, and a stored task or leader that went stale does not stop a save. A template rename moves every agent's quest_template that named it, in the same transaction, reading the old name under the row's lock so two renames at once carry the task to the final name. On PUT, null clears job_kind, leader_agent and quest_template and is a 422 for the NOT NULL columns. The agent reads carry a read-only coding_agent. MCP agent_create/agent_update take station and quest_template; clear_quest_template=true sends the one null (there is no MCP template write tool). Dashboard: Guild on the agent form (the no-template task offered only to a coding agent); Behaviour… in a template's menu on the Pipelines page, with badges for "Skips triage" and "Runs only when named". A fixed route with a qa step is saved with a warning (only triage asks for QA). duplicate copies the columns.
  • When the rows cannot be read (the database fails), nothing is assumed: no route is fixed, so triage runs; triage is offered no template and its pick of one is dropped for the kind default; the guild offers no task, and the failed read is not cached. Only with no database at all do the built-in rows as seeded answer (builtin_template, builtin_agent).
  • Stations are part of the identity contract (docs/contracts/identity.json stations, mirrored in agentIdentity.ts), each an object of the world art.

Upgrade. Migration 143 adds the columns and writes every row what the code said of its name before, matched by name as the code matched it, and every existing job what the old name rules credited it with (a slop-cleanup job coded by the Slopmaster, each job led by its template's old leader; the backfill does not move jobs.updated_at). job_kind stays NULL: the graph gives what the old rule did. A template named qa, onboard or pr-review is renamed <name>-template (with its id's head when that is taken too); I did not query a running deployment for such rows. Built-in agents stay insert-if-missing. Built-in templates are upserted at boot: the new columns follow default_for_kind and are written on a first insert only, so an operator's change survives a restart (test). row_behaviour and services.slop join the rollup's FOLD_MODULES, so this release rebuilds the progression rollup once.

Proof that a deployment with only built-in rows behaves the same. tests/fixtures/name_lists_before_rows.py keeps the removed lists and rules as they stood; test_row_behaviour.py checks every built-in template, code pipeline and agent against them (fixed, offered, kind, leader, the coder plan_route records, what a brief says for every kind; station and guild task), and test_migration_143.py checks an upgraded database's rows and jobs the same way. The progression golden test (test_progression_golden.py, raw and through the rollup) passes with its recorded file unchanged.

Pattern provenance

N/A — originated in this PR. The column, seed and write-path handling follows agent_definitions.triage_may_add_skills (migration 142) and pipeline_templates.default_for_kind.

Verification

Automated

  • Python unit suite with a real Postgres, -n 4 --fail-on-warnings, DOCKER_HOST=unix:///nonexistent.sock: 18867 passed, 21 skipped in 514.04s (the 15 Docker-backed infra tests among the skips, as on main)
  • ruff check ., ruff format --check . (whole tree), mypy embry0: clean
  • cd frontend && npm run lint: 0 errors (5 warnings, all pre-existing in files this PR does not touch)
  • cd frontend && npx tsc -b and npm run build: succeed
  • cd frontend && npm run test: 328 files, 2676 tests passed
  • Name denylist (scripts/check_denylist.py --public): 0 matches; gitleaks on the changed files: no leaks

Each new test failed first. Against origin/main (the first commit's tests): routing from rows, the guild's world API, the API write paths, migration 143, MCP and the name-list pin each failed with their own assertions; test_row_behaviour.py and test_behaviour_rows_repo.py fail at import there and were also shown failing by mutation. Against the PR's first commit (e11e71dd, the review findings' tree), the tests for the findings fail with their own messages:

  • recorded credit: test_a_job_keeps_the_credit_it_recorded_whatever_its_template_row_says_later, test_an_audit_is_a_job_the_context_auditor_led, test_plan_route_records_who_codes_the_job_and_who_leads_its_template, test_a_planned_party_is_drawn_as_the_leader_it_recorded, test_an_upgrade_credits_each_job_as_its_name_did;
  • kind from the route: "a route of specialists only is analysis" (probe C), "an edited route keeps no stale analysis kind" (probe D, the built-in row as seeded with its graph edited);
  • unchanged values and renames: test_a_save_that_keeps_a_stored_task_or_leader_is_not_refused_for_it, test_a_rename_carries_each_agents_guild_task_with_it;
  • '' for a coding agent only: test_a_task_naming_no_template_is_a_coding_agents_only (API and helper), and the agent form no longer offering it to another agent;
  • a failed read: "a failed read fixed the route from the seed", "a failed read offered tasks", _offered_to_triage;
  • code pipeline names: test_a_template_cannot_take_a_code_pipelines_name (API and helper), test_a_template_named_like_a_code_pipeline_is_renamed;
  • the pin: AUDIT_TEMPLATE gone, and "a past job's credit must not move with today's template rows".

Against the PR's second commit (the last review's tree), the last two findings' tests fail first:

  • one audit predicate: test_progression_the_findings_feed_and_the_scheduler_agree_on_what_an_audit_is ("the findings feed offers what progression counts as audits: ['acme/changed', 'acme/scheduled']"), and test_a_job_that_names_a_template_takes_its_leader_when_it_is_created;
  • concurrent renames: test_two_renames_at_once_carry_the_task_to_the_last_name holds the row from a second session until both renames wait on it ("template now beta, the agent's task gamma: a rename read a stale name"); it fails the same way, three runs of three, with the FOR UPDATE taken out, and passes five runs of five with it.

Existing tests changed, and why

  • Tests that imported a removed name now read the rows (test_specialist_agents.py, test_triage_fixed_route.py, test_world.py, test_developer_salvage.py); the fake templates repository in test_specialist_agents.py carries the seed's behaviour columns, as real rows do, and plan_route's job update now also writes coding_agent and leader_agent.
  • Tests that write jobs or journal rows directly give a job of a built-in template what plan_route records for it (tests/fixtures/builtin_rows.planned_credit): the progression history fixture behind the golden test, and helpers in test_progression_repo.py, test_fix_job_progression.py, test_quest_job_times.py, test_quests.py, test_progression_rollup.py, test_world_api.py and the golden test's quest-scroll case. Their assertions are unchanged.
  • The findings-feed tests (test_proposals_repo.py) give their context-audit jobs the leader such a job records, and the five fake job repositories of the context-audit tests take the scheduler's new leader_agent argument.
  • test_migration_142.py hands the "last entry" assertion to 143; the MCP tool surface and forwarding tests list the new parameters; TemplateActions.test.tsx, pipelines.test.ts and worldHall.test.ts follow the new menu item, mapping and the removed map.

Manual

  • Not walked in a browser: the running stack has not run migration 143, and this PR does not touch it. The forms are covered by component tests.

Deploy notes

  • Migration 143 runs at startup; it backfills from names and needs no operator step. It renames any template named qa, onboard or pr-review.
  • The progression rollup rebuilds once (new fold modules).
  • Rebuild the frontend image; no sandbox, safety or proxy image changes.
  • No new environment variables.
  • Known and accepted: the world's 60 s row cache is per process (one orchestrator today), and the dashboard and API deploy together, so a form never talks to an API without these fields.

Out of scope / follow-ups

  • Agent roles, and whether an operator's agent may run on the developer node: feat(agents): a coder role for an operator's own agents #395.
  • Name-keyed behaviour found outside the issue's list: generic_agent.py (the watcher log tool for analysis, the task for context-auditor, GENERIC_AGENT_TIMEOUT_SECONDS), plan_route's SLOP_TEMPLATE pick, and the achievement fragments and proposals.py that name the templates their features dispatch (map-areas, pr-followup, context-audit).
  • MCP has no template write tool, so template behaviour is set through the API and the dashboard. MCP agent_update cannot clear execution_mode or auth_mode (pre-existing: _drop_none drops every null).

Design + Plan

  • Design: docs/superpowers/specs/2026-11-05-behaviour-from-rows-design.md
  • Plan: N/A

Anti-goals respected

  • No new npm/Python dependency
  • No silent default flip — a deployment with only built-in rows behaves as before (tested against the old lists)
  • No secrets in diff
  • No emoji in code

Checklist

  • PR title follows Conventional Commits — type(scope): short subject
  • Each commit is one concept, individually meaningful
  • Linked design/plan references (if any) are reachable

Operators write templates and agents as rows, but what embry0 did with
one depended on the names of the built-in ones. An operator's own
specialist template was triaged and offered to triage, its jobs' briefs
said "the result of the work", and an agent of their own could never be
given a task from the guild. The progression and world queries credited
a job's pull request, cleanup and audit by template and agent names.

Each of those is now a column of the row, read through one module,
embry0/workflows/row_behaviour.py:

- pipeline_templates.fixed_route (a job that names it skips triage),
  offered_to_triage, job_kind, delivers and leader_agent;
- agent_definitions.station and quest_template (NULL no guild task,
  '' a task naming no template, else that template).

SPECIALIST_TEMPLATE_NAMES, specialist_job_kind, _TEMPLATE_DELIVERS,
LEADER_BY_TEMPLATE, QUEST_TEMPLATE_BY_AGENT, the frontend's STATION_OF
and progression's _CODER are gone; the coding agents are listed once.
A job's pull request is its template leader's when the leader codes
(coder_sql and row_behaviour.coder, pinned to agree), else the
developer's. The pipelines the code defines (qa, onboard, pr-review)
keep their leader and deliverable in WORKFLOW_PIPELINES.

Migration 143 adds the columns and writes every row what its name
said, so an upgraded deployment behaves as before; the boot seed writes
a built-in template's columns on its first insert only, like
default_for_kind. The columns are set through the API (422 naming the
field, including a leader or a task that names nothing), MCP
agent_create and agent_update, the agent form's Guild section and a
template's Behaviour dialog on the Pipelines page.

The issue's agent roles column is left out: its one consumer would put
an operator's agent on the developer node, a capability of its own.

Refs: #167
Operators write templates and agents as rows, but what embry0 did with
one depended on the names of the built-in ones. An operator's own
specialist template was triaged and offered to triage, its jobs' briefs
said "the result of the work", and an agent of their own could never be
given a task from the guild. The progression and world queries credited
a job's pull request, cleanup and audit by template and agent names.

Each of those is now a column of the row, read through one module,
embry0/workflows/row_behaviour.py:

- pipeline_templates.fixed_route (a job that names it skips triage),
  offered_to_triage, job_kind (an override; NULL reads the route, so a
  route of specialists only is analysis), delivers and leader_agent;
- agent_definitions.station and quest_template (NULL no guild task,
  '' a task naming no template, a coding agent's only, else that
  template).

Credit is recorded on the job when it is planned: plan_route writes
jobs.coding_agent and jobs.leader_agent, and progression and the world
credit from them. A slop cleanup is a merged job the Slopmaster coded,
an audit a job the context auditor led, so a copy of either template
counts and a later edit of a template row changes no past credit.

SPECIALIST_TEMPLATE_NAMES, specialist_job_kind, _TEMPLATE_DELIVERS,
LEADER_BY_TEMPLATE, QUEST_TEMPLATE_BY_AGENT, the frontend's STATION_OF,
progression's _CODER and AUDIT_TEMPLATE are gone; the coding agents are
listed once. The pipelines the code defines (qa, onboard, pr-review)
keep their leader and deliverable in WORKFLOW_PIPELINES, and no
template may take their names.

Migration 143 adds the columns and writes every row and job what its
name gave it, and renames a template named like a code pipeline. The
boot seed writes a built-in template's columns on its first insert only.
The columns are set through the API (422 naming the field, checked only
when a write changes the value), MCP agent_create and agent_update, the
agent form's Guild section and a template's Behaviour dialog. A rename
carries each agent's guild task with it. When the rows cannot be read,
no route is fixed and the guild offers no task.

Agent roles are left to #395.

Fixes: #167
Operators write templates and agents as rows, but what embry0 did with
one depended on the names of the built-in ones. An operator's own
specialist template was triaged and offered to triage, its jobs' briefs
said "the result of the work", and an agent of their own could never be
given a task from the guild. The progression and world queries credited
a job's pull request, cleanup and audit by template and agent names.

Each of those is now a column of the row, read through one module,
embry0/workflows/row_behaviour.py:

- pipeline_templates.fixed_route (a job that names it skips triage),
  offered_to_triage, job_kind (an override; NULL reads the route, so a
  route of specialists only is analysis), delivers and leader_agent;
- agent_definitions.station and quest_template (NULL no guild task,
  '' a task naming no template, a coding agent's only, else that
  template).

Credit is recorded on the job: it takes its template's leader when it
is created, plan_route writes jobs.coding_agent and jobs.leader_agent,
and progression and the world credit from them, so a later edit of a
template row changes no past credit. A slop cleanup is a merged job the
Slopmaster coded. An audit is a job the context auditor led and not a
fix job: one predicate (audit_jobs) for progression and the findings
feed, which the scheduler counts too, with its own template's jobs.

SPECIALIST_TEMPLATE_NAMES, specialist_job_kind, _TEMPLATE_DELIVERS,
LEADER_BY_TEMPLATE, QUEST_TEMPLATE_BY_AGENT, the frontend's STATION_OF,
progression's _CODER and AUDIT_TEMPLATE are gone; the coding agents are
listed once. The pipelines the code defines (qa, onboard, pr-review)
keep their leader and deliverable in WORKFLOW_PIPELINES, and no
template may take their names.

Migration 143 adds the columns and writes every row and job what its
name gave it, and renames a template named like a code pipeline. The
boot seed writes a built-in template's columns on its first insert only.
The columns are set through the API (422 naming the field, checked only
when a write changes the value), MCP agent_create and agent_update, the
agent form's Guild section and a template's Behaviour dialog. A rename
carries each agent's guild task with it, under the row's lock. When the
rows cannot be read, no route is fixed and the guild offers no task.

Agent roles are left to #395.

Fixes: #167
@sudoTomas
sudoTomas force-pushed the refactor/behaviour-from-rows branch from b5467a6 to dfeb003 Compare November 6, 2026 16:10
@sudoTomas
sudoTomas merged commit 62e0750 into main Nov 6, 2026
13 checks passed
@sudoTomas
sudoTomas deleted the refactor/behaviour-from-rows branch November 6, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(agents): behaviour from agent and template rows, not names

1 participant