Seen on issue #83: qa's approval pasted whitespace-nit detail into the issue
thread. The issue is for the creator/orchestration; review internals belong on
the PR. On APPROVE the full reply (+run report) now posts to the PR and the
issue gets exactly '✅ Reviewed PR #N — looks good.' (falls back to the issue
only if no PR could be resolved).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New learning step: ask @pm for a retrospective on any issue and the system
turns what happened into prompt-visible rules for future runs.
- run-agent.sh: @pm gains a RETRO marker (emit only when the maintainer asks);
LEARNINGS.md (caller repo root, capped at 4KB) is injected into EVERY agent's
prompt as "TEAM LEARNINGS" — the feedback loop that makes delegation more
robust over time.
- publish.sh: on @pm's RETRO marker, open a "retro: issue #N" issue pointing at
the issue + its PR (state=all resolve, works after merge) and trigger @senior
on it (has gitea-api to read both threads). The retro produces a LEARNINGS.md
PR through the NORMAL choreography (senior → pm → qa), so retros are reviewed
like any change. Strip the RETRO marker from visible replies.
- publish.sh: bounce counter now counts only @qa-authored comments matching the
exact trigger template — on PR #84 it jumped 1/3 → 3/3 because a qa review
QUOTED our own "(fix attempt …)" template from the diff, halving the fix
budget. Template + regex pinned together with a sync note.
- README: document the retro loop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fixed trigger templates referred to their own author in the third person —
e.g. @qa posting 'please address @qa's review above' (seen on PR #80) or a dev
posting 'the dev has pushed an update'. Reworded to first person ('my review',
'I have pushed', 'per my plan'), which also makes routing order-independent:
each trigger now mentions ONLY its target agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes from a full repo review:
- fetch-thread.sh: attribute every comment to its REAL author (@pm/@qa/… —
agents post as their own users now); the old "🤖 @name line at the top" hint
pointed at headers we removed, leaving every teammate comment anonymous. Also
strip the hidden loop-prevention marker from bodies (prompt noise).
- agent.yml: per-issue concurrency group (queued, no cancel) — two quick
comments on one issue no longer race the same ai/issue-N branch.
- rescue-pr.sh: after opening a rescue PR, hand it back into the flow with an
unmarked @pm trigger (the pm→qa choreography previously stalled silently on
any rescued run); drop the old "🤖 **@name**" header style; add ops to the
token case; mark the status note with the hidden marker.
- README: token table said "@qa merges / TOKEN_QA needs write:repository" —
inverted since the PM-orchestrated flow (@pm merges, autopilot only; @qa is
read-only). Updated the agent table (descs, node1-ssh moved to homelab) and
added a "How a task flows" section.
- agents.json: pm/qa descs now describe the orchestrator/reviewer roles (these
feed the roster prompt agents route by).
- NEW ci.yml: bash -n + shellcheck(-S error) on every script, YAML-parse on
every workflow, agents.json schema check — the ${x:-{}} brace bug would have
been caught here before it shipped.
- install-opencode.sh: pin opencode (default 1.17.13, override via
OPENCODE_VERSION) — a breaking release no longer takes down every agent.
- build-activity-log.sh: ollama/ollama-cloud models are subscription-billed
(no $/token exists) — label the footer "subscription" instead of a
misleading $0.0000; metered (anthropic) models keep the real dollar cost.
- route.sh: document that mention-priority is list-order and load-bearing for
the flow's trigger comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each run now appends a report to the agent's reply: the tool calls it made plus
input/output token totals and the dollar cost. opencode's --format json emits
per-step `tokens` and `cost` (USD, priced from the model) on step_finish events;
build-activity-log.sh sums them across the run.
- build-activity-log.sh: compute for EVERY agent (not just devs — @pm/@qa also
call tools and cost money); output a collapsed <details> report (summary line
shows "N tool calls · in X · out Y · $Z"; body lists the tools + a token/cost
breakdown). Zero-tool runs get a one-line "$Z · in X · out Y" footer.
- publish.sh: build $activity once (near the top) and append it to every agent's
reply — @pm plan/finalize, @qa verdict/recommendations, and dev PR comments.
- agent.yml: rename the step accordingly.
Models without pricing (self-hosted ollama) report cost $0.0000.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the delegation/trigger choreography so @pm orchestrates end-to-end from
the issue thread while @qa reviews on the PR and never merges.
Flow: issue → @pm plans (asks creator; autopilot skips) → dev builds PR → dev
pings @pm on the issue → @pm hands to @qa → @qa reviews ON THE PR (recommends /
BOUNCE:@dev, direct qa↔dev loop, 3 max) → @qa APPROVEs → verdict on the ISSUE →
@pm → normal: tells creator "ready to merge" (human merges); autopilot: @pm
MERGE_PR. @pm never reads or comments on the PR (small context); @qa never merges.
- run-agent.sh: @pm prompt = 3 phases (plan / hand PR to @qa / finalize),
autopilot only drops the approval gate + adds @pm MERGE_PR. @qa prompt =
reviewer, ends with APPROVE / BOUNCE:@dev / HALT; identical in both flows.
- publish.sh: shared post_to()/trig()/resolve_pr() helpers + ISSN; @qa routes
reply to PR (bounce) vs issue (approve) and fires @pm/@dev; @pm handles
DELEGATE:@dev, DELEGATE:@qa (resolves the PR), and autopilot MERGE_PR; dev
NEW=true fires @pm on the issue, NEW=false fires @qa on the PR. Strip
APPROVE/HALT markers.
Tokens re-minted to match: TOKEN_PM gains write:repository (it merges now),
TOKEN_QA drops to read:repository (never merges).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`${VAR:-{}}` appends a stray '}' when VAR is set (bash brace-matching), so the
JSON handed to a caller skill's setup.sh was corrupted and its jq failed with
"Unmatched '}'" — install-caller-skills.sh then caught the non-zero exit and
skipped the skill. Default SECRETS_JSON in two safe steps and pass it as a plain
var. This is why node1-ssh never installed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
toJSON(secrets) leaks multiline secret values (their newlines are escaped, so the
runner's log mask misses them). Note in install-caller-skills.sh that caller-skill
secrets exposed via SECRETS_JSON must be single-line (base64-encode keys/multiline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gitea already attributes every comment/PR to its author, so the "🤖 **@name**" /
"🔨 **@name**" header at the top of agent comments was redundant noise. Remove it
everywhere and, in the process, close a self-trigger loop.
- publish.sh: post()/prpost() now append a hidden `<!-- 🤖 … -->` marker instead
of each message carrying a visible "🤖 **@name**" header. The gate keys on the
'🤖' char to skip agent replies, so the marker preserves loop-prevention while
being invisible. All reply/status/autopilot strings drop the name header.
- route.sh: the "building on branch" notice is posted with the agent's PAT and
previously had NO 🤖 marker + an "@name" mention, so it re-triggered the agent
(the observed loop on issue #139). Reword without the self-name and add the
hidden marker so it can't fire a new run.
- publish.sh: broaden the leading self-header stripper to drop any leading line
referencing the agent's own @handle (e.g. "## 🔨 @senior — …"), not just bare
"**@name**" lines. Legit headings like "## Summary" are preserved.
- run-agent.sh: tell the model not to begin its reply with its own name/handle.
- Trigger comments (delegation / autopilot / bounce) stay marker-free by design,
so they still fire the next agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A consuming repo can now add repo-specific skills under .gitea/agent-skills/<name>/
(SKILL.md + skill.json + optional setup.sh); the framework installs the ones
allowed for the running agent. This keeps deploy-target / infra specifics in the
repo they belong to instead of hardcoded in the shared workflow.
- install-caller-skills.sh: scans the caller workspace, installs each skill whose
skill.json `agents` list includes the running agent, runs its optional setup.sh
with all inherited secrets available as $SECRETS_JSON (toJSON(secrets) — so a
caller's setup can read repo-specific secret names the framework can't know),
and merges the allowed skills into the permission.skill allow-list.
- agent.yml: replace the hardcoded node1-ssh step with the generic caller-skills
step (passes NAME, WORKSPACE, SECRETS_JSON).
- Remove the built-in node1-ssh skill: delete skill-node1-ssh.sh, drop "node1-ssh"
from agents.json (senior/lead). The homelab repo now owns that skill.
- run-agent.sh: note caller-provided skills aren't in the roster (route them via
the caller's AGENTS.md).
toJSON(secrets) verified supported on this Gitea (1.27) via an isolated probe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All three consuming repos (gitea/agents, gitea/ops, ffaerber/homelab) now
carry the per-agent TOKEN_* secrets (org-level for gitea/*, user-level for
ffaerber/*), so the shared AGENT_TOKEN fallback is no longer needed.
- agent.yml: SELF_TOKEN ternary ends in '' instead of secrets.AGENT_TOKEN;
removed AGENT_TOKEN from the publish + rescue step env.
- publish.sh: TTOK is just the agent's own token; if an agent somehow has none
(TOK fell back to the built-in GT), TTOK is left empty so the trigger/merge
is skipped rather than silently no-op'ing under the built-in token.
- README: drop the AGENT_TOKEN row; state that every consuming repo must carry
the per-agent TOKEN_* (no shared fallback).
The AGENT_TOKEN Actions secrets (org + user) and its gitea/secrets/.env entry
are removed separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the shared AGENT_TOKEN as the primary credential; every agent now acts
as its own Gitea user (TOKEN_PM for @pm, TOKEN_OPS for @ops, …) for API
calls, delegation/autopilot trigger comments, and PR merges.
- agent.yml: Run-agent step injects SELF_TOKEN — a ternary selecting the
running agent's own token by name, falling back to AGENT_TOKEN for repos
not yet migrated to per-agent tokens (e.g. homelab). Only that one token
enters the agent process, so no agent can act as another. The gitea-api /
gitea-admin skill-setup steps no longer carry a token (they only write docs).
- Gate: trust the agent roster (pm/junior/senior/lead/qa/ops) as comment
authors so an agent's own delegation/autopilot trigger comment (posted with
its PAT, no 🤖 prefix) fires the next run. @ops added to the mention set.
- publish.sh: TOK = agent identity (comments/replies); new TTOK = trigger/merge
token (agent PAT, else AGENT_TOKEN fallback) for delegation, autopilot @qa
triggers, and PR merges that must fire downstream workflows.
- skill-gitea-api.sh / skill-gitea-admin.sh / run-agent.sh: AGENT_TOKEN/
TOKEN_OPS → SELF_TOKEN in the emitted skill docs and env contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gitea-admin skill now authenticates with TOKEN_OPS (the ops user's admin token), and TOKEN_OPS
is injected into the agent process ONLY when name==ops. No other agent holds an admin credential in
its env. This unhooks @ops from AGENT_TOKEN, so AGENT_TOKEN can be narrowed to a non-admin token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every token value must be written to gitea/secrets/.env (the master, mirrored to org Actions secrets)
AND the Actions secret, kept in sync, removed from both on delete. Drop the tokens.md instruction —
the @ops test dutifully re-created tokens.md because the skill still referenced it.
PR #55 added @ops to the registry but route.sh only looped pm/junior/senior/lead/qa, so @ops could
never be triggered, and there was no token case for it. Add 'ops' to the routing loop and the
TOKEN_OPS case in route.sh + publish.sh, and pass TOKEN_OPS in the workflow env next to TOKEN_QA
(route/publish/rescue steps) so route.sh (set -u) doesn't error on an @ops run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ops now treats gitea/secrets/tokens.md as the source-of-truth inventory (readable by @ffaerber and
@ops only) and records every token it mints/rotates there, alongside storing the live value in the
matching Actions secret.
A comment-mode operator agent (opus) for administering Gitea itself from issues in gitea/ops:
create orgs/users/repos, manage labels & Actions secrets, and mint least-privilege per-user tokens.
- agents.json: new @ops role (comment-mode, skill gitea-admin, confirms before destructive ops).
- skill-gitea-admin.sh: SKILL.md documenting org/user/repo/label/secret ops + the create-user →
mint-scoped-token → store-as-secret flow (never printing tokens). Gated on NAME=ops so the admin
how-to is written ONLY for @ops; permission.skill also denies it to other agents.
- agent.yml: wire the skill step (uses AGENT_TOKEN — an admin PAT during bootstrap).
Bootstrap note: AGENT_TOKEN is admin for now, so every agent's process technically holds an admin
credential (skill-scoping hides the doc, not the env var). Once @ops is minting scoped per-user
tokens, narrow AGENT_TOKEN and inject a dedicated admin token only for @ops.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qa's autopilot merge used the built-in Actions token (TOKEN_QA unset -> fell back to GITEA_TOKEN).
Gitea does not fire new workflow runs for pushes made by the built-in Actions token, so the merge
landed on main but deploy.yml never ran — the change was merged-but-not-deployed. Merge with
AGENT_TOKEN (a PAT, already used for delegation and proven to trigger runs) so the push to main
triggers the deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per maintainer: @qa must never change code — it only accepts a PR or sends it back to the dev with
precise instructions. Replaces the halt-on-any-problem behavior with a bounce loop:
- QA prompt: emit MERGE_PR (approve), or `BOUNCE: @<dev>` after spelling out exactly what to change.
HALT_AUTOPILOT is now only for problems a dev can't fix (needs a human decision).
- publish.sh: on BOUNCE, re-trigger the named dev (via AGENT_TOKEN) with "autopilot fix attempt N/3".
Count prior attempts from the PR thread; at 3 it stops (removes the label) and hands to @ffaerber.
- publish.sh: after a dev pushes a fix (resume), auto-re-trigger @qa to re-verify — closing the loop
(dev fix -> qa re-check -> merge or bounce again).
- Updated the initial @qa trigger wording (bounce, not halt).
Verified: BOUNCE target parsing and the 3-strike counter (attempts 1/3,2/3,3/3 then HALT).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The activity <details> was labelled 'tool calls & reasoning' but included the agent's final prose
summary ('Added the whoami service... Exposed via Traefik... Pushed to branch...'), which is just a
restatement of the PR description and not a tool call. Emit only tool_use events; relabel to
'N tool calls'. The what-changed narrative lives in the PR description; this section is the record of
actions taken.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batches the remaining comment-noise cleanups on top of the activity-log fix:
- Resume comment no longer re-posts the full PR description (## Summary/## Changes) — the PR body and
diff already carry it; the comment just links the PR (the reasoning trail shows what the run did).
- Strip a leading self-header the model sometimes emits ("🤖 **@pm**" on its own line, sometimes
twice) before we prepend our own, so headers aren't doubled.
- Also drop the BEGIN_PR_DESCRIPTION block from the plain reply text, and simplify the activity
block's summary (it repeated "🤖 **@name** — activity log").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'tool calls & reasoning' activity log dumped each text part verbatim, including the agent's
BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION block — which is already published as the PR description, so
it appeared twice (e.g. homelab PR #117). Strip that block from text parts, and drop a text entry
that becomes empty after stripping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an agent was re-triggered on an issue that already had a branch/open PR, route.sh created a
fresh branch from main (git checkout -b), so the push back was rejected non-fast-forward and the new
commits were silently dropped (|| true) -- the run reported success and re-announced the stale PR,
but the work was gone (observed on issue #17 run #88). Resume the existing branch when it exists, so
new commits fast-forward onto it and update its PR; only branch from main when none exists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The homelab health-check test mis-routed a node1 inspection to @junior (skills: []), which cannot
reach node1 — only @senior/@lead hold node1-ssh. Root cause: the roster shown to agents listed
desc + vision but not skills, so @pm had no way to route by capability (despite a comment claiming
it could). Add each teammate's skill names to the roster line. Names only; the scoped how-to detail
stays hidden via permission.skill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge the activity-log comment into the main reply comment and merge the
sub-issues-created comment into the pm reply, so each agent run posts
exactly one comment (reply + tool calls & reasoning inline).
Stage the shared scripts into $RUNNER_TEMP and point $SCRIPTS there for every
step, so an agent that commits/deletes the in-tree .agents-workflow checkout no
longer destroys the scripts the post-agent steps run (issue #33). Scrub any
in-tree .agents-workflow artifact before publishing, and add a failure-safe
rescue step that opens a PR for pushed work when a run fails.
Extract the agent registry to a shared agents.json and have both
route.sh (agent → model) and install-opencode.sh (ollama-cloud provider
models map) derive from it, so the two lists can no longer drift.
route.sh now copies agents.json to /tmp/agents.json instead of an inline
heredoc. install-opencode.sh builds the ollama-cloud map by
collecting every registry model with the prefix and
stripping it — non-ollama-cloud models (e.g. @lead's anthropic/claude-opus,
the local ornith:35b) are excluded by construction.
Resolves#31.
The shared agent.yml is a reusable (workflow_call) workflow, so on Gitea
github.event_name evaluates to 'workflow_call' — not the original 'issues'/
'issue_comment'. route.sh branched on EVENT == "issues", so issue-opened runs
scanned the empty comment body and skipped the @pm fallback, exiting 1 with
"no known agent mentioned" (runs #393/#394). Comment runs were unaffected
because the @mention lives in $BODY.
Discriminate on the comment id (CID) instead, which IS reliably forwarded in
the event payload: empty => issue-opened (scan issue body, default @pm), set
=> comment (scan comment body). Also add .gitignore for .env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebased onto the per-agent skill-scoping change so PR #25 carries both:
- route.sh keeps the registry 'skills' allow-list and emits skills as a step output
- install-opencode.sh writes the permission.skill block (deny-all + allow listed)
Pure refactor otherwise: each step's shell moves to its own file, called via
bash "$SCRIPTS/<name>.sh". The two extracted SKILL.md bodies are byte-identical to
main; routing/config/publish behavior is unchanged. Because this is a reusable
workflow (workflow_call) the runtime checkout is the caller's repo, so agent.yml now
checks THIS repo out into .agents-workflow/ (pinned @main) and points $SCRIPTS there.