Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/agentify-lane-fixtures.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Golden-fixture lane tests for the agentify framework: drive each lane's real
# prompt via `claude -p` over a mocked world and assert on recorded actions.
# The model layer is token-gated — without CLAUDE_CODE_OAUTH_TOKEN the runner
# SKIPs (the deterministic assertion self-test still runs). Add the secret to
# exercise the prompts on every PR.
name: agentify-lane-fixtures

on:
pull_request:
paths:
- "plugins/e2a/agentify/templates/runtime-skill/**"
- "plugins/e2a/agentify/test/fixtures/**"
- ".github/workflows/agentify-lane-fixtures.yml"
workflow_dispatch: {}

permissions:
contents: read

jobs:
fixtures:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Claude Code + harness deps
run: |
npm install -g @anthropic-ai/claude-code
npm install --prefix plugins/e2a/agentify/test/fixtures/harness
- name: Run lane fixtures
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: bash plugins/e2a/agentify/test/fixtures/run-fixtures.sh
27 changes: 27 additions & 0 deletions .github/workflows/agentify-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Tests for the agentify feedback-loop framework (plugins/e2a/agentify).
# Deterministic: script selftests + addon unit tests + static/YAML/config
# validation. No secrets, no network — runs on every PR that touches it.
name: agentify-test

on:
push:
paths: ["plugins/e2a/agentify/**", ".github/workflows/agentify-test.yml"]
pull_request:
paths: ["plugins/e2a/agentify/**", ".github/workflows/agentify-test.yml"]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install PyYAML (for config validation)
run: pip install --quiet pyyaml
- name: Run the agentify test suite
run: bash plugins/e2a/agentify/test/run.sh
717 changes: 717 additions & 0 deletions docs/design/autonomous-repo-framework.md

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions plugins/e2a/agentify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
name: agentify
description: Deploy the autonomous-repo feedback loop into a GitHub repo. Scaffolds the lane workflows, the runtime skill, and one config file as a reviewed PR, then prints the one-time identity/secret setup checklist. Turns a repo into one that triages incoming feedback into issues and prepares human-gated fix PRs. Use when someone wants to make a repo self-managing / "agentify" it / install the feedback loop.
---

# agentify — deploy the autonomous-repo feedback loop

`/agentify` installs the framework into a target repo: feedback in →
triaged GitHub issue → human-gated fix PR → filer notified. It **bundles
the framework as templates** (this skill's `templates/`), so grabbing this
skill is grabbing the framework. The install lands as a **PR the repo owner
reviews and merges** — the install itself goes through the same human gate
the framework runs on.

> **v0 scope.** The full loop ships: **triage/intake** (email → triaged
> issue), **comms** (filer acks + the fix-gate approval email + verified-reply
> routing), **fix + release** (the coding agent's human-reviewed PR + the
> merge→shipped callback), and this **deploy** flow. The mechanical render is
> automated (`agentify-render.sh`, with a `_selftest`); the Q&A and the
> one-time identity/secret setup stay guided. An `update` mode (re-render
> preserving the adopter's config tweaks) is the natural follow-on — the
> render is already idempotent.

## What gets scaffolded into the target repo

| from `templates/` | to the target repo |
|---|---|
| `autonomous-repo.config.yml.tmpl` | `autonomous-repo.config.yml` (the only file the adopter owns) |
| `runtime-skill/**` | `.claude/skills/autonomous-repo/**` |
| `scripts/ticket_card.sh` | `scripts/ticket_card.sh` |
| `workflows/*.yml.tmpl` | `.github/workflows/*.yml` |

## Deploy procedure

1. **Detect.** Read the target repo: its `OWNER/REPO`, primary language,
test command, and CI — used to fill the fix-lane `verify_setup_script`
and sensible defaults.
2. **Configure.** Ask the adopter the config values and export them as the
`ANS_*` env vars `agentify-render.sh` reads: `ANS_PRODUCT_NAME`,
`ANS_OWNER`, `ANS_REPO`, `ANS_MARKER`, `ANS_REVIEWER_LOGIN`,
`ANS_BOT_LOGIN`, `ANS_SUPPORT_ADDRESS`, `ANS_FIX_GATE_MODE` (`hitl`
recommended), `ANS_APPROVER_ADDRESS`, `ANS_VERIFY_SETUP_SCRIPT`. (The bot
login can be filled later from the checklist; secrets are never gathered
here.)
3. **Render.** Run `agentify-render.sh --to <target-repo-root>`. It fills
`autonomous-repo.config.yml` from the `ANS_*` answers (failing loudly on
any unfilled placeholder) and scaffolds the runtime skill, the scripts,
and the four workflows into their real paths
(`.claude/skills/autonomous-repo/`, `scripts/`,
`.github/workflows/*.yml`). **Re-running updates the scaffolded code but
PRESERVES an existing `autonomous-repo.config.yml`** (your tuned
`always_hitl`, the filled `bot_login`) — pass `--force` only to regenerate
the config. Then **tune** the rendered config's `always_hitl` list for the
product's sensitive surfaces, and sanity-check: `scripts/*.sh _selftest`
all green and the config parses. **Optional addons** (`templates/addons/`)
— e.g. `submit-feedback-mcp` (a `submit_feedback` MCP tool that
email-bridges into the support mailbox) — are opted in via
`ANS_ADDONS="<name> ..."`; the render scaffolds each to `tools/<name>/` and
appends its setup to `AGENTIFY-ADDON-SETUP.md`. Addons are additive; the
loop runs without them.
4. **Auto-do the safe parts.** Create the labels from `labels.*` via `gh`
(`feedback`, `agent-fix`, `wontfix`, `feedback-ops`, the `status:*` set).
5. **Hand off the rest** (print, don't do — see `references/setup-checklist.md`):
create the GitHub App (bot identity) and set `github_app_login` in config;
create the e2a `support@` agent + an agent-scoped API key; add the repo
secrets (`CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`, `E2A_API_KEY`,
`AUTOREPO_APP_ID`, `AUTOREPO_APP_PRIVATE_KEY`); enable Actions; set branch
protection so the fix lane's PRs require review. **Hand over the exact
commands/links — never run the auth yourself.**
6. **Open the install as a PR.** Branch, commit the scaffolded files, open a
PR titled "agentify: install the autonomous-repo feedback loop" that
summarizes what each file does and links the setup checklist. Do not
merge.

## After merge + setup

The lanes activate themselves: each no-ops loudly until its secrets exist,
then starts on the next cron tick. Flip the `AUTOREPO_LANES_PAUSED` repo
variable to pause everything. Run the loop interactively any time with the
`autonomous-repo` skill ("drain the triage queue").

## References

- `references/setup-checklist.md` — the one-time identity/secret setup.
- `references/adapters.md` — the TicketStore / CommsChannel / Intake
adapter contracts and which are implemented.
- `references/security-invariants.md` — the defaults an adopter must not
misconfigure away.
142 changes: 142 additions & 0 deletions plugins/e2a/agentify/agentify-render.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# agentify-render.sh — the deterministic scaffolder behind `/agentify`.
#
# Copies the framework templates into a target repo's real paths and renders
# autonomous-repo.config.yml from the adopter's answers. The interactive
# wizard (SKILL.md) gathers the answers, exports them as ANS_*, and runs this;
# keeping the mechanical part here makes it testable and reproducible.
#
# agentify-render.sh --to <target-repo-root>
# agentify-render.sh _selftest # render into a temp dir + assert
#
# Answers (env, gathered by the wizard):
# ANS_PRODUCT_NAME ANS_OWNER ANS_REPO ANS_MARKER ANS_REVIEWER_LOGIN
# ANS_BOT_LOGIN ANS_SUPPORT_ADDRESS ANS_FIX_GATE_MODE ANS_APPROVER_ADDRESS
# ANS_VERIFY_SETUP_SCRIPT
#
# Renders (idempotent — safe to re-run to update):
# autonomous-repo.config.yml.tmpl -> <target>/autonomous-repo.config.yml
# runtime-skill/** -> <target>/.claude/skills/autonomous-repo/**
# scripts/*.sh -> <target>/scripts/
# workflows/*.yml.tmpl -> <target>/.github/workflows/*.yml (.tmpl stripped)
set -euo pipefail

BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATES="$BASE/templates"

# _esc: escape a value for safe use in a sed replacement (handles \ & |).
_esc() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; }

render_config() { # $1 = target root, $2 = force ("1" to overwrite)
local out="$1/autonomous-repo.config.yml"
# Re-runs UPDATE the code (scaffold) but must NOT clobber the adopter's
# tuned config (always_hitl, the filled bot_login, etc.). Preserve an
# existing config unless --force.
if [ -f "$out" ] && [ "${2:-}" != "1" ]; then
echo "agentify: $out exists — preserving your edits (pass --force to regenerate)."
return 0
fi
sed \
-e "s|{{PRODUCT_NAME}}|$(_esc "${ANS_PRODUCT_NAME:-}")|g" \
-e "s|{{OWNER}}|$(_esc "${ANS_OWNER:-}")|g" \
-e "s|{{REPO}}|$(_esc "${ANS_REPO:-}")|g" \
-e "s|{{MARKER}}|$(_esc "${ANS_MARKER:-}")|g" \
-e "s|{{REVIEWER_LOGIN}}|$(_esc "${ANS_REVIEWER_LOGIN:-}")|g" \
-e "s|{{BOT_LOGIN}}|$(_esc "${ANS_BOT_LOGIN:-}")|g" \
-e "s|{{SUPPORT_ADDRESS}}|$(_esc "${ANS_SUPPORT_ADDRESS:-}")|g" \
-e "s|{{FIX_GATE_MODE}}|$(_esc "${ANS_FIX_GATE_MODE:-hitl}")|g" \
-e "s|{{APPROVER_ADDRESS}}|$(_esc "${ANS_APPROVER_ADDRESS:-}")|g" \
-e "s|{{VERIFY_SETUP_SCRIPT}}|$(_esc "${ANS_VERIFY_SETUP_SCRIPT:-}")|g" \
"$TEMPLATES/autonomous-repo.config.yml.tmpl" > "$out"
# Only real placeholders ({{UPPERCASE_IDENT}}) — not the literal "{{...}}"
# in the template's explanatory comment.
if grep -qE '\{\{[A-Z][A-Z_]*\}\}' "$out"; then
echo "agentify-render.sh: unfilled placeholder(s) remain in $out:" >&2
grep -nE '\{\{[A-Z][A-Z_]*\}\}' "$out" >&2; return 1
fi
}

scaffold() { # $1 = target root
local t="$1"
mkdir -p "$t/.claude/skills/autonomous-repo" "$t/scripts" "$t/.github/workflows"
cp -R "$TEMPLATES/runtime-skill/." "$t/.claude/skills/autonomous-repo/"
cp "$TEMPLATES"/scripts/*.sh "$t/scripts/"
chmod +x "$t"/scripts/*.sh
for f in "$TEMPLATES"/workflows/*.yml.tmpl; do
cp "$f" "$t/.github/workflows/$(basename "$f" .tmpl)"
done
}

# apply_addons: scaffold each opted-in addon (ANS_ADDONS, space-separated) to
# tools/<name>/ and append its setup.md. Addons are additive — the core loop
# runs without them.
apply_addons() { # $1 = target root
local t="$1" name src
for name in ${ANS_ADDONS:-}; do
# Reject anything that isn't a plain addon name — `..`/`/` would let the
# cp escape tools/ (ANS_ADDONS is deployer-set, but fail safe anyway).
case "$name" in
""|*[!a-z0-9-]*) echo "agentify: invalid addon name '$name' (skipped)" >&2; continue ;;
esac
src="$TEMPLATES/addons/$name"
if [ ! -d "$src/files" ]; then
echo "agentify: unknown addon '$name' (skipped)" >&2; continue
fi
mkdir -p "$t/tools/$name"
cp -R "$src/files/." "$t/tools/$name/"
if [ -f "$src/setup.md" ]; then
{ printf '\n## Addon: %s\n\n' "$name"; cat "$src/setup.md"; } >> "$t/AGENTIFY-ADDON-SETUP.md"
fi
echo "agentify: addon '$name' -> tools/$name/"
done
}

if [ "${1:-}" = "_selftest" ]; then
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
export ANS_PRODUCT_NAME="acme" ANS_OWNER="acme" ANS_REPO="widget" ANS_MARKER="acme-feedback" \
ANS_REVIEWER_LOGIN="dev" ANS_BOT_LOGIN="acme-bot[bot]" ANS_SUPPORT_ADDRESS="support@acme.test" \
ANS_FIX_GATE_MODE="hitl" ANS_APPROVER_ADDRESS="boss@acme.test" ANS_VERIFY_SETUP_SCRIPT="scripts/verify.sh"
render_config "$T"; scaffold "$T"
fail=0
# re-run must preserve an existing config (update the code, not the config)
echo 'tuned: yes' >> "$T/autonomous-repo.config.yml"
render_config "$T"; grep -q 'tuned: yes' "$T/autonomous-repo.config.yml" || { echo "FAIL: re-run clobbered the config"; fail=1; }
render_config "$T" 1; grep -q 'tuned: yes' "$T/autonomous-repo.config.yml" && { echo "FAIL: --force did not regenerate"; fail=1; }
grep -q 'repo: "acme/widget"' "$T/autonomous-repo.config.yml" || { echo "FAIL: repo not rendered"; fail=1; }
grep -q 'approver: "boss@acme.test"' "$T/autonomous-repo.config.yml" || { echo "FAIL: approver not rendered"; fail=1; }
grep -qE '\{\{[A-Z][A-Z_]*\}\}' "$T/autonomous-repo.config.yml" && { echo "FAIL: placeholder left"; fail=1; }
[ -f "$T/.github/workflows/feedback-triage.yml" ] || { echo "FAIL: triage workflow missing"; fail=1; }
[ -e "$T/.github/workflows/feedback-triage.yml.tmpl" ] && { echo "FAIL: .tmpl not stripped"; fail=1; }
for w in comms fix released; do [ -f "$T/.github/workflows/feedback-$w.yml" ] || { echo "FAIL: $w workflow missing"; fail=1; }; done
[ -f "$T/.claude/skills/autonomous-repo/triage.md" ] || { echo "FAIL: runtime skill missing"; fail=1; }
[ -f "$T/.claude/skills/autonomous-repo/templates/triage-ack.md" ] || { echo "FAIL: email templates missing"; fail=1; }
[ -x "$T/scripts/ticket_card.sh" ] || { echo "FAIL: ticket_card.sh missing/not exec"; fail=1; }
[ -x "$T/scripts/comms_send.sh" ] || { echo "FAIL: comms_send.sh missing/not exec"; fail=1; }
# addons: none by default
[ -e "$T/tools" ] && { echo "FAIL: tools/ created with no ANS_ADDONS"; fail=1; }
# addons: opt in submit-feedback-mcp
ANS_ADDONS="submit-feedback-mcp" apply_addons "$T"
[ -f "$T/tools/submit-feedback-mcp/server.mjs" ] || { echo "FAIL: addon server.mjs not scaffolded"; fail=1; }
[ -f "$T/tools/submit-feedback-mcp/bridge.mjs" ] || { echo "FAIL: addon bridge.mjs not scaffolded"; fail=1; }
grep -q 'Addon: submit-feedback-mcp' "$T/AGENTIFY-ADDON-SETUP.md" || { echo "FAIL: addon setup not appended"; fail=1; }
ANS_ADDONS="nope-addon" apply_addons "$T" 2>/dev/null; [ -e "$T/tools/nope-addon" ] && { echo "FAIL: unknown addon scaffolded"; fail=1; }
# traversal name rejected (dest would be $T/tools/../evil = $T/evil)
ANS_ADDONS="../evil" apply_addons "$T" 2>/dev/null; [ -e "$T/evil" ] && { echo "FAIL: traversal addon escaped tools/"; fail=1; }
if [ "$fail" = 0 ]; then echo "agentify-render.sh selftest: OK"; else echo "agentify-render.sh selftest: FAILED"; exit 1; fi
exit 0
fi

TARGET=""; FORCE=""
while [ $# -gt 0 ]; do
case "$1" in
--to) TARGET="$2"; shift 2 ;;
--force) FORCE="1"; shift ;;
*) echo "agentify-render.sh: unknown arg '$1'" >&2; exit 2 ;;
esac
done
[ -n "$TARGET" ] || { echo "agentify-render.sh: --to <target-repo-root> is required" >&2; exit 2; }
[ -d "$TEMPLATES" ] || { echo "agentify-render.sh: templates not found at $TEMPLATES" >&2; exit 2; }
render_config "$TARGET" "$FORCE"
scaffold "$TARGET"
apply_addons "$TARGET"
echo "agentify: rendered into $TARGET (config + .claude/skills/autonomous-repo + scripts + .github/workflows)"
33 changes: 33 additions & 0 deletions plugins/e2a/agentify/examples/e2a/agentify-fix-verify-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# agentify-fix-verify-setup.sh — e2a's fix-lane verification bootstrap.
#
# Product-specific (it lives in the target repo and is named by the config
# key `verify_setup_script`, so the fix-lane workflow YAML stays neutral).
# Stands up the RUNNING stack the fix agent verifies against, with THROWAWAY
# credentials only — nothing here can reach production.
#
# For e2a: Postgres on :5433 (the dev port, per CLAUDE.md) via docker
# compose, schema applied, test DB URL exported. The fix agent then runs
# `make test-unit` / the package tests against it.
set -euo pipefail

# Local Postgres (+ Mailpit) the way dev runs it.
make docker-up

# Wait for Postgres to accept connections (compose --wait isn't always set).
for i in $(seq 1 30); do
if docker compose exec -T postgres pg_isready -U e2a >/dev/null 2>&1; then break; fi
sleep 1
done

# The e2a binary auto-applies embedded migrations at startup, but the test
# DB needs the schema for direct-SQL package tests:
make migrate || true

# Throwaway test DB URL (matches CLAUDE.md's E2A_TEST_DATABASE_URL); worthless
# outside this run. Exported to the agent's environment.
{
echo "E2A_TEST_DATABASE_URL=postgres://e2a:e2a@localhost:5433/e2a_test?sslmode=disable"
} >> "${GITHUB_ENV:-/dev/stdout}"

echo "verify stack up: Postgres :5433 (throwaway), schema applied."
56 changes: 56 additions & 0 deletions plugins/e2a/agentify/examples/e2a/autonomous-repo.config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# autonomous-repo.config.yml — e2a's rendered instance (the first adopter).
#
# This is what `/agentify` produces when installed on e2a, with e2a's
# chosen values. It lives under examples/ as the reference install; the
# real deploy copies it to the repo root. See
# docs/design/autonomous-repo-framework.md.

product_name: "e2a"
repo: "Mnexa-AI/e2a"
marker: "e2a-feedback" # <!-- e2a-feedback --> footer in bot-authored issues/PRs

reviewer: "jiashuoz" # PR ship-gate reviewer (requested + assigned)
github_app_login: "e2a-support-bot[bot]" # TODO: created at deploy (GitHub App); ticket-card/marker trusted only from this login

labels:
feedback: "feedback"
agent_fix: "agent-fix"
wontfix: "wontfix"
ops: "feedback-ops"
status_triaged: "status:triaged"
status_awaiting_approval: "status:awaiting-approval"
status_in_progress: "status:in-progress"

ticket_store: "github" # zero-backend: e2a's own conversation store holds the PII
intake: "email" # feedback arrives at support_address; triage creates the issue

comms:
channel: "e2a" # e2a runs its own support loop on e2a (dogfood)
support_address: "support@e2a.dev" # TODO(open-q #6): confirm verified domain (e2a.dev / api.e2a.dev / support.)
e2a_mcp_url: "https://api.e2a.dev/mcp" # e2a-adapter-specific (used only when channel: e2a)
e2a_api_url: "https://api.e2a.dev" # REST base for the comms send wrapper (recipients bound here)

fix_gate:
mode: "hitl" # e2a default: maintainer approves PRs by replying to an email
approver: "jszjosh@gmail.com" # TODO(open-q): Josh's verified reply address for approvals
always_hitl:
- "auth / authz / permissions / sessions"
- "secrets, credentials, tokens, keys"
- "SMTP / SPF / DKIM / emailauth, HMAC header signing (internal/headers)"
- "billing / Stripe / usage metering"
- "migrations on messages / usage_events; schema / stored-data shape"
- "/v1 wire contract / OpenAPI spec / SDK codegen / ID format"
- "deletion or other irreversible operations"

budgets:
triage_items_per_run: 20
comms_emails_per_day: 50

models:
triage: "claude-sonnet-4-6"
fix: "claude-opus-4-8"
comms: "claude-sonnet-4-6"

verify_setup_script: "scripts/agentify-fix-verify-setup.sh" # authored in slice 3 (fix lane)

# Pause switch: set repo variable AUTOREPO_LANES_PAUSED="true" (exact match) to halt all lanes.
Loading
Loading