No description
Find a file
ellen f99b40e05f
Some checks failed
coordinate.yaml / Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main (push) Failing after 0s
dev.yaml / Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main (push) Failing after 0s
plan.yaml / Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main (push) Failing after 0s
reflect.yaml / Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main (push) Failing after 0s
test.yaml / Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main (push) Failing after 0s
Merge pull request 'docs: record why the issue/label agent pipeline was abandoned' (#28) from docs/abandonment-rationale into main
Reviewed-on: #28
2026-10-04 19:39:39 +00:00
.forgejo/workflows chore: remove agent workflow caller 2026-09-29 19:18:11 +00:00
agents docs: mark the issue/label agent pipeline abandoned 2026-10-04 19:19:14 +00:00
learnings docs: mark the issue/label agent pipeline abandoned 2026-10-04 19:19:14 +00:00
lib docs: mark the issue/label agent pipeline abandoned 2026-10-04 19:19:14 +00:00
CHANGELOG.md docs: record why the issue/label agent pipeline was abandoned 2026-10-04 19:38:23 +00:00
cleanup-old-labels.sh docs: split documentation into README, CHANGELOG and DECISIONS 2026-09-02 17:22:12 +00:00
DECISIONS.md docs: record why the issue/label agent pipeline was abandoned 2026-10-04 19:38:23 +00:00
labels.json labels: /-scoped taxonomy, exclusive agent scope, drop legacy labels 2026-07-20 12:13:05 +00:00
README.md docs: mark the issue/label agent pipeline abandoned 2026-10-04 19:19:14 +00:00
seed-org-labels.sh docs: split documentation into README, CHANGELOG and DECISIONS 2026-09-02 17:22:12 +00:00

agent-workflows

Abandoned (2026-09-29). The issue/label-driven agent automation this repo implemented is no longer used to automate development work. No repo calls the dispatcher any more: the last frozen callers were removed on 2026-09-29 (this repo's own in #26). The workflows below are kept for reference only and do nothing unless a repo starts calling them again. Why: DECISIONS.md.

This was the central home for the Claude agent automation across the Forgejo orgs on git.valssi.eu (valssi, valssi-ai, valssi-public). It is a workflow library, not a deployed service. Each participating repo carried a frozen caller that called back into this repo's reusable workflows at @main. The workflows ran as Forgejo Actions on an act_runner labelled devagent.

Current state

Item State
Per-repo callers (.forgejo/workflows/agent.yaml / caller-agent.yaml) Removed from every repo. The 15-minute (later 30-minute) schedule tick no longer fires.
dispatch.yaml, tick.yaml and the route workflows Still in the tree, but workflow_call only, so nothing triggers them.
dind-prune.yaml Still live. It is this repo's own weekly schedule (Mondays 04:00) and prunes the devagent runner's dind. That runner still serves ordinary CI for other repos (see valssi-ai/forgejo-ai-dev-runner).
Org labels (labels.json) Still seeded in the orgs. Nothing reads them or sets them any more.
Org variables (AGENTS_ENABLED, APPROVER, …) and route secrets Still set where they were. Nothing reads them.

Do not onboard a repo. Copying a caller back in would re-activate the whole pipeline for that repo, including the scheduled tick.

How it worked (historical)

The rest of this file describes the pipeline as it ran up to 2026-09-29. It is kept so that labels, state comments and PR history in the estate can still be understood. None of it is current behaviour.

What runs

Path Role
.forgejo/workflows/agent.yaml (per repo) No longer exists anywhere. Callers were removed estate-wide by 2026-09-29. The frozen caller each participating repo carried; forwards every event and the */15 schedule tick to dispatch.yaml and does nothing else.
.forgejo/workflows/dispatch.yaml Central router. Resolves the tier→model matrix and routes labeled/scheduled events to the matching agent workflow; drains the defer queue and posts the epic digest on the schedule tick.
.forgejo/workflows/plan.yaml Planning agent (workflow_call).
.forgejo/workflows/dev.yaml Implementation agent (workflow_call) — serves both the first build (plan/approved) and dev↔test bounces (agent/dev).
.forgejo/workflows/test.yaml Test/review agent (workflow_call), triggered on a PR.
.forgejo/workflows/coordinate.yaml Coordinator agent (workflow_call) — breaks an epic into child issues.
.forgejo/workflows/reflect.yaml Post-merge reflection agent (workflow_call) — distils lessons into docs PRs.
.forgejo/workflows/dind-prune.yaml Weekly docker system prune on the runner (Mondays 04:00).
agents/ Declarative contract per agent role: summon label, identity, token, model default, handoff. See agents/README.md.
lib/ Shared composite actions every route calls: preflight, state, thread. See lib/README.md.
learnings/ Operational log of gotchas hit while building and running the automation. See learnings/README.md.
labels.json Canonical /-scoped label taxonomy.
seed-org-labels.sh Applies labels.json to one or more Forgejo orgs, idempotently.
cleanup-old-labels.sh Deletes the retired :-scheme labels (agent:*, plan:approved, …) from an org or repo.

Features

Central dispatch

project repo  .forgejo/workflows/agent.yaml   ← frozen caller, copied once, never edited
      │  (subscribes to issue/PR events + 15-min cron, secrets: inherit)
      ▼
agent-workflows  .forgejo/workflows/dispatch.yaml   ← the router: ALL routing lives here
      │  routes on the inherited event / schedule
      ├─▶ plan.yaml        (label: agent/plan)
      ├─▶ dev.yaml         (label: plan/approved, by vars.APPROVER — or agent/dev)
      ├─▶ test.yaml        (PR opened/synchronized on an agent/issue-N branch, or agent/test)
      ├─▶ coordinate.yaml  (label: agent/coordinate, by vars.APPROVER)
      ├─▶ reflect.yaml     (a PR merges, or label: agent/reflect)
      └─▶ tick             (schedule: */15 — drains the defer queue, posts the epic digest)

The caller knows nothing about agents, labels or models — it just forwards every event to the dispatcher. dispatch.resolve runs on every event that reaches it (unconditionally, so a skipped job can never leave route jobs' with: block the whole run) and, for the six command labels, looks up the governing issue's tier/* label and turns it into a --model and, for the local rung, a LiteLLM gateway base URL. Route jobs themselves gate on if: — actor, label, event shape — not on inputs.

The dev↔test loop

agent/plan → plan comment + needs/approval → human sets plan/approved (vars.APPROVER only) → dev builds on branch agent/issue-N, opens a PR, labels it agent/test → test builds and reviews the PR: pass → needs/review for a human merge; fail → findings comment + loop/N bump + re-applies agent/dev on the issue. agent/dev can also be applied directly to an issue that never went through planning — the issue text alone frames the work. A dev run that produces no new commit parks straight to needs/human instead of looping.

Tier ladder and escalation

Four model rungs plus one excluded from automation: tier/local (LiteLLM gateway, no subscription quota), tier/haiku, tier/sonnet, tier/opus. A tier/* label on the governing issue overrides each route's role default (plan/coordinate/reflect default opus; dev/test default sonnet). At loop/3 the current tier's three dev↔test bounces are spent: preflight bumps one rung, resets to loop/1 and retries; at the top rung (or a non-ladder tier) it parks to needs/human with an attempt summary instead. tier/fable is recognised but never invoked by a workflow — resolve falls back to sonnet if it is ever seen on a command-label event.

Safety gates (lib/preflight)

First step of every agent job, in order: kill switches (vars.AGENTS_ENABLED != 'true', or the issue carries ai/off) → night fence (run/night + outside vars.NIGHT_WINDOW, Europe/Helsinki) → caps (loop/3 escalation, or max-attempts-24h attempts in the state comment) → actor guard (command label applied by someone other than vars.APPROVER or an authorized bot PAT) → usage gate (session/weekly Claude subscription utilization over threshold, skipped for tier/local). A block always exits the job green with proceed=false — it defers (blocked/usage, blocked/night, resumable) or parks (needs/human, a human must clear it) but never fails the job outright.

Conversation-aware prompts (lib/thread)

Every model-using route renders the human side of the issue/PR comment thread into its prompt, oldest-first, with the instruction that later comments override earlier ones, the approved plan and the test findings. Agent artifacts (plans, findings, results) are elided to one-line markers — the routes that need them fetch the full text directly — and the machine <!-- agent-state --> / <!-- agent-digest --> comments are dropped entirely.

Shared per-issue state (lib/state)

One <!-- agent-state --> JSON comment per issue: tier, loop, origin_label, deferred_reason, resume_after, and an attempts[] array (agent, model, run_url, outcome, ended). Every route appends an attempt on the way out; preflight reads it for the 24h attempt cap and the loop/tier state.

Queue drain and epic digest (tick)

The 15-minute schedule tick (forwarded from every repo's caller) finds this repo's open blocked/usage / blocked/night issues and PRs, keeps the ones whose gate has re-opened, and resumes up to vars.QUEUE_BATCH of them FIFO by deferral time — dropping the blocked/* label and re-applying the origin_label so the route re-fires. In vars.EPIC_REPO (default valssi/ops) it also posts one status comment per open epic per UTC day: children by state/*, queue depth, attempts in the last 24h, and open needs/*.

Coordination and reflection

coordinate (APPROVER-only, on an epic issue) breaks a goal into child issues across valssi repos, mirrors the epic's milestone into each, and may kick off planning on the first ready child. reflect fires when a PR merges (or on agent/reflect): docs-only, it distils durable lessons into a PR against valssi/docs (reference/*.md + a CHANGELOG line) and optionally valssi-public/agent-workflows agent-def updates, then ticks the source issue's line in its parent epic's task list. Both targets are allowlisted in the workflow itself, not left to the model.

Concurrency

Each route declares a concurrency: group: agent-issue-<repo>-<issue-or-PR> (or agent-reflect-… for reflect), so runs against the same issue or PR serialize — a bounce cannot start while a build for that issue is still in flight. Different issues run in parallel, bounded only by the runner's own capacity.

Label taxonomy (labels.json)

All labels are /-scoped; agent/* is exclusive (only one agent summoned per issue at a time).

Scope Labels Meaning
agent/* plan, dev, test, coordinate, reflect, triage Summon a route. One-shot: the agent removes it on pickup.
plan/approved — Human approval gate; honored only when applied by vars.APPROVER.
needs/* approval, review, input, human Agent → human: something needs attention.
state/* planning, building, testing, passed, blocked Machine-managed, mutually exclusive lifecycle state.
tier/* local, haiku, sonnet, opus, fable Model rung. fable is human-applied only.
loop/* 1, 2, 3 Dev↔test bounce counter at the current tier.
run/night — Start only inside NIGHT_WINDOW.
blocked/* usage, night Queued: the gate will re-open; the tick scheduler resumes it.
ai/off — Agents ignore this issue entirely.

seed-org-labels.sh applies labels.json to one or more orgs (create-or-patch, idempotent — safe to re-run whenever the file changes); it needs an admin PAT with write:organization, since the bot PATs deliberately cannot manage the label schema. cleanup-old-labels.sh deletes the superseded :-scheme names (agent:plan, plan:approved, ai:off, …) from a named org or repo.

Operational learnings

learnings/ is a running log of gotchas from building and running this automation — distinct from this README (the architecture) and from valssi/docs (the design rationale). Current entries:

Configuration

Org-level Forgejo variables (Settings → Actions → Variables), read by dispatch.yaml and lib/preflight:

Variable Meaning
AGENTS_ENABLED Master kill switch. Preflight compares it to the literal string true — anything else, including unset, is a silent no-op.
APPROVER The human login allowed to set plan/approved, agent/coordinate, and to act as an authorized actor on guarded routes.
NIGHT_WINDOW HH:MM-HH:MM in Europe/Helsinki for run/night work. Empty disables the night fence.
QUEUE_BATCH Deferred items the schedule tick resumes per run. Defaults to 2.
LITELLM_URL / LITELLM_MODEL Gateway base URL and model alias for the tier/local rung. Either unset falls back to haiku.
EPIC_REPO Where epics live, for the tick digest and reflect's epic-tick step. Defaults to valssi/ops.

Secrets, passed down via secrets: inherit from the caller through dispatch.yaml to each route:

Secret Used for
CLAUDE_CODE_OAUTH_TOKEN Claude subscription auth for the claude CLI; also read by preflight's usage gate.
CLAUDE_PLAN_PAT Planner identity (claude-plan) — issue/PR read-write, no code-write. Used by plan, test, coordinate.
CLAUDE_DEV_PAT Dev identity (claude-dev) — code-write. Used by dev, reflect.
LITELLM_KEY Optional virtual key for the LiteLLM gateway; only consumed on the tier/local rung.

Onboarding a new repo

Historical. Do not follow; see Current state.

  1. Copy caller-agent.yaml to the repo as .forgejo/workflows/agent.yaml.
  2. Provide the secrets the agents need (org- or repo-level): CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_PLAN_PAT, CLAUDE_DEV_PAT, and the APPROVER variable (plus the optional vars above).
  3. Make sure the org's labels are current: ADMIN_PAT=... ./seed-org-labels.sh <org>.
  4. Done — the repo now follows the central dispatcher forever; changes to any route here reach it on the next event, no redeploy needed.

Operating

  • Add or change agent behaviour: edit the route's workflow here (or dispatch.yaml for routing/gating changes) and merge to main — every subscribed repo picks it up on its next event or the next */15 tick.
  • A repo looks like it's ignoring labels: check vars.AGENTS_ENABLED is exactly true on that org — preflight's kill switch fails silently by design, and the only trace is a preflight: AGENTS_ENABLED != true line in the run log.
  • Re-seed labels after editing labels.json: ADMIN_PAT=... ./seed-org-labels.sh <org> [<org> ...] (create-or-patch, idempotent).
  • Retire old labels: ADMIN_PAT=... ./cleanup-old-labels.sh org:<name> / repo:<owner>/<name>.
  • Inspect an issue's agent state: the <!-- agent-state --> comment (machine- managed — don't hand-edit it) shows tier, loop, any deferral, and the attempt history.
  • Something's stuck at needs/human: read the parking comment for the reason (attempt cap, loop cap at the top tier, or a hard job failure), clear the blocker, and re-apply the relevant agent/* label to resume.