diff --git a/AGENTS.md b/AGENTS.md
index 2416d0a..a7baf7c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Disinto — Agent Instructions
## What this repo is
diff --git a/action/AGENTS.md b/action/AGENTS.md
index 55dadae..16e2a80 100644
--- a/action/AGENTS.md
+++ b/action/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Action Agent
**Role**: Execute operational tasks described by action formulas — run scripts,
diff --git a/bin/disinto b/bin/disinto
index 5fa230d..be51d27 100755
--- a/bin/disinto
+++ b/bin/disinto
@@ -260,25 +260,44 @@ services:
networks:
- disinto-net
- # Staging deployment slot — activated by Woodpecker staging pipeline (#755).
- # Profile-gated: only starts when explicitly targeted by deploy commands.
- # Customize image/ports/volumes for your project after init.
- staging:
- image: alpine:3
- profiles: ["staging"]
- security_opt:
- - apparmor=unconfined
- environment:
- DEPLOY_ENV: staging
+ # Edge proxy — reverse proxies Forgejo, Woodpecker, and staging.
+ # IP-only at bootstrap; domain + Let's Encrypt added later via vault.
+ edge:
+ image: caddy:alpine
+ restart: unless-stopped
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./docker/Caddyfile:/etc/caddy/Caddyfile:ro
+ - caddy_data:/data
+ depends_on:
+ - forgejo
+ - woodpecker
+ - staging
+ networks:
+ - disinto-net
+
+ # Staging container — static file server for project staging artifacts.
+ # CI pipelines write to the staging-site volume to update content.
+ # Seeds default page on first boot; CI overwrites volume contents later.
+ staging:
+ image: caddy:alpine
+ restart: unless-stopped
+ volumes:
+ - staging-site:/srv/site
+ - ./docker/staging-seed:/srv/seed:ro
+ command: ["sh", "-c", "cp -n /srv/seed/* /srv/site/ 2>/dev/null; caddy file-server --root /srv/site --listen :80"]
networks:
- disinto-net
- command: ["echo", "staging slot — replace with project image"]
volumes:
forgejo-data:
woodpecker-data:
agent-data:
project-repos:
+ caddy_data:
+ staging-site:
networks:
disinto-net:
@@ -308,6 +327,77 @@ COMPOSEEOF
echo "Created: ${compose_file}"
}
+# Generate docker/Caddyfile for the edge proxy.
+generate_caddyfile() {
+ local caddyfile="${FACTORY_ROOT}/docker/Caddyfile"
+
+ if [ -f "$caddyfile" ]; then
+ echo "Caddyfile: ${caddyfile} (already exists, skipping)"
+ return
+ fi
+
+ cat > "$caddyfile" <<'CADDYEOF'
+# Caddyfile — generated by disinto init
+# IP-only at bootstrap; domain + Let's Encrypt added later via vault.
+
+:80 {
+ handle /forgejo/* {
+ uri strip_prefix /forgejo
+ reverse_proxy forgejo:3000
+ }
+
+ handle /ci/* {
+ uri strip_prefix /ci
+ reverse_proxy woodpecker:8000
+ }
+
+ handle {
+ reverse_proxy staging:80
+ }
+}
+CADDYEOF
+
+ echo "Created: ${caddyfile}"
+}
+
+# Generate default staging page in the staging-site volume seed directory.
+generate_staging_page() {
+ local staging_dir="${FACTORY_ROOT}/docker/staging-seed"
+ local index_file="${staging_dir}/index.html"
+
+ if [ -f "$index_file" ]; then
+ echo "Staging: ${index_file} (already exists, skipping)"
+ return
+ fi
+
+ mkdir -p "$staging_dir"
+
+ cat > "$index_file" <<'HTMLEOF'
+
+
+
+
+
+ Staging
+
+
+
+
+
Nothing shipped yet
+
This staging site will update automatically when CI pushes new artifacts.
+
+
+
+HTMLEOF
+
+ echo "Created: ${index_file}"
+}
+
# Generate docker/agents/ files if they don't already exist.
generate_agent_docker() {
local docker_dir="${FACTORY_ROOT}/docker/agents"
@@ -1394,6 +1484,8 @@ p.write_text(text)
forge_port="${forge_port:-3000}"
generate_compose "$forge_port"
generate_agent_docker
+ generate_caddyfile
+ generate_staging_page
# Create empty .env so docker compose can parse the agents service
# env_file reference before setup_forge generates the real tokens (#769)
touch "${FACTORY_ROOT}/.env"
diff --git a/dev/AGENTS.md b/dev/AGENTS.md
index ccfe0c7..c2148e8 100644
--- a/dev/AGENTS.md
+++ b/dev/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Dev Agent
**Role**: Implement issues autonomously — write code, push branches, address
@@ -14,7 +14,7 @@ in-progress issues are also picked up. The direct-merge scan runs before the loc
check so approved PRs get merged even while a dev-agent session is active.
**Key files**:
-- `dev/dev-poll.sh` — Cron scheduler: finds next ready issue, handles merge/rebase of approved PRs, tracks CI fix attempts. Formula guard skips issues labeled `formula`, `action`, `prediction/dismissed`, or `prediction/unreviewed` (replaced `prediction/backlog` — that label no longer exists)
+- `dev/dev-poll.sh` — Cron scheduler: finds next ready issue, handles merge/rebase of approved PRs, tracks CI fix attempts. Formula guard skips issues labeled `formula`, `action`, `prediction/dismissed`, or `prediction/unreviewed`. Also injects CI failures and review feedback into active tmux sessions via `_inject_into_session()` (uses `tmux load-buffer` + `paste-buffer` to handle multi-line text safely).
- `dev/dev-agent.sh` — Orchestrator: claims issue, creates worktree + tmux session with interactive `claude`, monitors phase file, injects CI results and review feedback, merges on approval
- `dev/phase-handler.sh` — Phase callback functions: `post_refusal_comment()`, `_on_phase_change()`, `build_phase_protocol_prompt()`. `do_merge()` detects already-merged PRs on HTTP 405 (race with dev-poll's pre-lock scan) and returns success instead of escalating. Sources `lib/mirrors.sh` and calls `mirror_push()` after every successful merge.
- `dev/phase-test.sh` — Integration test for the phase protocol
@@ -33,6 +33,8 @@ check so approved PRs get merged even while a dev-agent session is active.
**Crash recovery**: on `PHASE:crashed` or non-zero exit, the worktree is **preserved** (not destroyed) for debugging. Location logged. Supervisor housekeeping removes stale crashed worktrees older than 24h.
+**Rebase-before-push**: the phase protocol instructs Claude to `git fetch && git rebase` on `$PRIMARY_BRANCH` before every push (initial, CI fix, and review address). This avoids merge conflicts when main has advanced since branch creation. Uses `--force-with-lease` on CI/review fix pushes.
+
**Lifecycle**: dev-poll.sh (`check_active dev`) → dev-agent.sh → tmux `dev-{project}-{issue}` → phase file
drives CI/review loop → merge + `mirror_push()` → close issue. On respawn after
`PHASE:escalate`, the stale phase file is cleared first so the session starts
diff --git a/gardener/AGENTS.md b/gardener/AGENTS.md
index c9ba3b1..2ab894a 100644
--- a/gardener/AGENTS.md
+++ b/gardener/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Gardener Agent
**Role**: Backlog grooming — detect duplicate issues, missing acceptance
diff --git a/gardener/pending-actions.json b/gardener/pending-actions.json
index 747973c..d54a5ad 100644
--- a/gardener/pending-actions.json
+++ b/gardener/pending-actions.json
@@ -1,32 +1,12 @@
[
{
- "action": "edit_body",
- "issue": 765,
- "body": "Depends on: none\n\n## Goal\n\nThe disinto website becomes a versioned artifact: built by CI, published to Codeberg's generic package registry, deployed to staging automatically. Version visible in footer.\n\n## Files to add/change\n\n### `site/VERSION`\n```\n0.1.0\n```\n\n### `site/build.sh`\n```bash\n#!/bin/bash\nVERSION=$(cat VERSION)\nmkdir -p dist\ncp *.html *.jpg *.webp *.png *.ico *.xml robots.txt dist/\nsed -i \"s|Built from scrap, powered by a single battery.|v${VERSION} · Built from scrap, powered by a single battery.|\" dist/index.html\necho \"$VERSION\" > dist/VERSION\n```\n\n### `site/index.html`\nNo template placeholder needed — `build.sh` does the sed replacement on the existing footer text.\n\n### `.woodpecker/site.yml`\n```yaml\nwhen:\n path: \"site/**\"\n event: push\n branch: main\n\nsteps:\n - name: build\n image: alpine\n commands:\n - cd site && sh build.sh\n - VERSION=$(cat site/VERSION)\n - tar czf site-${VERSION}.tar.gz -C site/dist .\n\n - name: publish\n image: alpine\n commands:\n - apk add curl\n - VERSION=$(cat site/VERSION)\n - >-\n curl -sf --user \"johba:$$FORGE_TOKEN\"\n --upload-file site-${VERSION}.tar.gz\n \"https://codeberg.org/api/packages/johba/generic/disinto-site/${VERSION}/site-${VERSION}.tar.gz\"\n environment:\n FORGE_TOKEN:\n from_secret: forge_token\n\n - name: deploy-staging\n image: alpine\n commands:\n - apk add curl\n - VERSION=$(cat site/VERSION)\n - >-\n curl -sf --user \"johba:$$FORGE_TOKEN\"\n \"https://codeberg.org/api/packages/johba/generic/disinto-site/${VERSION}/site-${VERSION}.tar.gz\"\n -o site.tar.gz\n - rm -rf /srv/staging/*\n - tar xzf site.tar.gz -C /srv/staging/\n environment:\n FORGE_TOKEN:\n from_secret: forge_token\n volumes:\n - /home/debian/staging-site:/srv/staging\n```\n\n## Infra setup (manual, before first run)\n- `mkdir -p /home/debian/staging-site`\n- Add to Caddyfile: `staging.disinto.ai { root * /home/debian/staging-site; file_server }`\n- DNS: `staging.disinto.ai` A record → same IP as `disinto.ai`\n- Reload Caddy: `sudo systemctl reload caddy`\n- Add `forge_token` as Woodpecker repo secret for johba/disinto (if not already set)\n- Add `/home/debian/staging-site` to `WOODPECKER_BACKEND_DOCKER_VOLUMES`\n\n## Verification\n- [ ] Merge PR that touches `site/` → CI runs site pipeline\n- [ ] Package appears at `codeberg.org/johba/-/packages/generic/disinto-site/0.1.0`\n- [ ] `staging.disinto.ai` serves the site with `v0.1.0` in footer\n- [ ] `disinto.ai` (production) unchanged\n\n## Related\n- #764 — docker stack edge proxy + staging (future: this moves inside the stack)\n- #755 — vault-gated production promotion (production deploy comes later)\n\n## Affected files\n- `site/VERSION` — new, holds current version string\n- `site/build.sh` — new, builds dist/ with version injected into footer\n- `.woodpecker/site.yml` — new, CI pipeline for build/publish/deploy-staging"
+ "action": "comment",
+ "issue": 3,
+ "body": "Closing: this issue was fully implemented by PR #2 (commit `723167d`). All acceptance criteria are met:\n- `edge` (Caddy) service added to docker-compose\n- `staging` Caddy static file server configured\n- `docker/Caddyfile` template generated by `disinto init`\n- Default \"Nothing shipped yet\" staging page in `docker/staging-seed/`\n\nThe dev-agent sessions recorded here crashed after the implementation was already merged into `main`. Closing as implemented."
},
{
- "action": "edit_body",
- "issue": 764,
- "body": "Depends on: none (builds on existing docker-compose generation in `bin/disinto`)\n\n## Design\n\n`disinto init` + `disinto up` starts two additional containers as base factory infrastructure:\n\n### Edge proxy (Caddy)\n- Reverse proxies to Forgejo and Woodpecker\n- Serves staging site\n- Runs on ports 80/443\n- At bootstrap: IP-only, self-signed TLS or HTTP\n- Domain + Let's Encrypt added later via vault resource request\n\n### Staging container (Caddy)\n- Static file server for the project's staging artifacts\n- Starts with a default \"Nothing shipped yet\" page\n- CI pipelines write to a shared volume to update staging content\n- No vault approval needed — staging is the factory's sandbox\n\n### docker-compose addition\n```yaml\nservices:\n edge:\n image: caddy:alpine\n ports:\n - \"80:80\"\n - \"443:443\"\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile\n - caddy_data:/data\n depends_on:\n - forgejo\n - woodpecker-server\n - staging\n\n staging:\n image: caddy:alpine\n volumes:\n - staging-site:/srv/site\n # Not exposed directly — edge proxies to it\n\nvolumes:\n caddy_data:\n staging-site:\n```\n\n### Caddyfile (generated by `disinto init`)\n```\n# IP-only at bootstrap, domain added later\n:80 {\n handle /forgejo/* {\n reverse_proxy forgejo:3000\n }\n handle /ci/* {\n reverse_proxy woodpecker-server:8000\n }\n handle {\n reverse_proxy staging:80\n }\n}\n```\n\n### Staging update flow\n1. CI builds artifact (site tarball, etc.)\n2. CI step writes to `staging-site` volume\n3. Staging container serves updated content immediately\n4. No restart needed — Caddy serves files directly\n\n### Domain lifecycle\n- Bootstrap: no domain, edge serves on IP\n- Later: factory files vault resource request for domain\n- Human buys domain, sets DNS\n- Caddyfile updated with domain, Let's Encrypt auto-provisions TLS\n\n## Affected files\n- `bin/disinto` — `generate_compose()` adds edge + staging services\n- New: default staging page (\"Nothing shipped yet\")\n- New: Caddyfile template in `docker/`\n\n## Related\n- #755 — vault-gated deployment promotion (production comes later)\n- #757 — ops repo (domain is a resource requested through vault)\n\n## Acceptance criteria\n- [ ] `disinto init` generates a `docker-compose.yml` that includes `edge` (Caddy) and `staging` containers\n- [ ] Edge proxy routes `/forgejo/*` → Forgejo, `/ci/*` → Woodpecker, default → staging container\n- [ ] Staging container serves a default \"Nothing shipped yet\" page on first boot\n- [ ] `docker/` directory contains a Caddyfile template generated by `disinto init`\n- [ ] `disinto up` starts all containers including edge and staging without manual steps"
- },
- {
- "action": "edit_body",
- "issue": 761,
- "body": "Depends on: #747\n\n## Design\n\nEach agent account on the bundled Forgejo gets a `.profile` repo. This repo holds the agent's formula (copied from disinto at creation time) and its journal.\n\n### Structure\n```\n{agent-bot}/.profile/\n├── formula.toml # snapshot of the formula at agent creation time\n├── journal/ # daily logs of what the agent did\n│ ├── 2026-03-26.md\n│ └── ...\n└── knowledge/ # learned patterns, best-practices (optional, agent can evolve)\n```\n\n### Lifecycle\n1. **Create agent** — `disinto init` or `disinto spawn-agent` creates Forgejo account + `.profile` repo\n2. **Copy formula** — current `formulas/{role}.toml` from disinto repo is copied to `.profile/formula.toml`\n3. **Agent reads its own formula** — at session start, agent reads from its `.profile`, not from the disinto repo\n4. **Agent writes journal** — daily entries pushed to `.profile/journal/`\n5. **Agent can evolve knowledge** — best-practices, heuristics, patterns written to `.profile/knowledge/`\n\n### What this enables\n\n**A/B testing formulas:** Create two agents from different formula versions, run both against the same backlog, compare results (cycle time, CI pass rate, review rejection rate).\n\n**Rollback:** New formula worse? Kill agent, spawn from older formula version.\n\n**Audit:** What formula was this agent running when it produced that PR? Check its `.profile` at that git commit.\n\n**Drift tracking:** Diff what an agent learned (`.profile/knowledge/`) vs what it started with. Measure formula evolution over time.\n\n**Portability:** Move agent to different box — `git clone` its `.profile`.\n\n### Disinto repo becomes the template\n\n```\ndisinto repo:\n formulas/dev-agent.toml ← canonical template, evolves\n formulas/review-agent.toml\n formulas/planner.toml\n ...\n\nRunning agents:\n dev-bot-v2/.profile/formula.toml ← snapshot from formulas/dev-agent.toml@v2\n dev-bot-v3/.profile/formula.toml ← snapshot from formulas/dev-agent.toml@v3\n review-bot/.profile/formula.toml ← snapshot from formulas/review-agent.toml\n```\n\nThe formula in the disinto repo is the template. The `.profile` copy is the instance. They can diverge — that's a feature, not a bug.\n\n## Affected files\n- `bin/disinto` — agent creation copies formula to .profile\n- Agent session scripts — read formula from .profile instead of local formulas/ dir\n- Planner/supervisor — can read other agents' journals from their .profile repos\n\n## Related\n- #747 — per-agent Forgejo accounts (prerequisite)\n- #757 — ops repo (shared concerns stay there: vault, portfolio, resources)\n\n## Acceptance criteria\n- [ ] `disinto spawn-agent` (or `disinto init`) creates a Forgejo account + `.profile` repo for each agent bot\n- [ ] Current `formulas/{role}.toml` is copied to `.profile/formula.toml` at agent creation time\n- [ ] Agent session script reads its formula from `.profile/formula.toml`, not from the repo's `formulas/` directory\n- [ ] Agent writes daily journal entries to `.profile/journal/YYYY-MM-DD.md`"
- },
- {
- "action": "edit_body",
- "issue": 742,
- "body": "## Problem\n\n`gardener/recipes/*.toml` (4 files: cascade-rebase, chicken-egg-ci, flaky-test, shellcheck-violations) are an older pattern predating `formulas/*.toml`. Two systems for the same thing.\n\n## Fix\n\nMigrate any unique content from recipes to the gardener formula or to new formulas. Delete the recipes directory.\n\n## Affected files\n- `gardener/recipes/*.toml` — delete after migration\n- `formulas/run-gardener.toml` — absorb relevant content\n- Gardener scripts that reference recipes/\n\n## Acceptance criteria\n- [ ] Contents of `gardener/recipes/*.toml` are diff'd against `formulas/run-gardener.toml` — any unique content is migrated\n- [ ] `gardener/recipes/` directory is deleted\n- [ ] No scripts in `gardener/` reference the `recipes/` path after migration\n- [ ] ShellCheck passes on all modified scripts"
- },
- {
- "action": "add_label",
- "issue": 742,
- "label": "backlog"
- },
- {
- "action": "add_label",
- "issue": 741,
- "label": "backlog"
+ "action": "close",
+ "issue": 3,
+ "reason": "already implemented in PR #2 (commit 723167d)"
}
]
diff --git a/lib/AGENTS.md b/lib/AGENTS.md
index 09847fa..f163615 100644
--- a/lib/AGENTS.md
+++ b/lib/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Shared Helpers (`lib/`)
All agents source `lib/env.sh` as their first action. Additional helpers are
diff --git a/planner/AGENTS.md b/planner/AGENTS.md
index 9749afd..79e63fe 100644
--- a/planner/AGENTS.md
+++ b/planner/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Planner Agent
**Role**: Strategic planning using a Prerequisite Tree (Theory of Constraints),
diff --git a/predictor/AGENTS.md b/predictor/AGENTS.md
index 327a842..87d3533 100644
--- a/predictor/AGENTS.md
+++ b/predictor/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Predictor Agent
**Role**: Abstract adversary (the "goblin"). Runs a 2-step formula
diff --git a/review/AGENTS.md b/review/AGENTS.md
index e010ff5..de41872 100644
--- a/review/AGENTS.md
+++ b/review/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Review Agent
**Role**: AI-powered PR review — post structured findings and formal
@@ -9,7 +9,7 @@ whose CI has passed and that lack a review for the current HEAD SHA, then
spawns `review-pr.sh `.
**Key files**:
-- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI. Sources `lib/guard.sh` and calls `check_active reviewer` — skips if `$FACTORY_ROOT/state/.reviewer-active` is absent.
+- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI. Sources `lib/guard.sh` and calls `check_active reviewer` — skips if `$FACTORY_ROOT/state/.reviewer-active` is absent. When injecting review into a dev session, first looks for a bot comment containing ``, then falls back to formal Forgejo PR reviews (state `APPROVED` or `REQUEST_CHANGES`) — ensures the dev-agent receives the verdict even when bot comments are absent.
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal forge review, auto-creates follow-up issues for pre-existing tech debt. Before starting the session, runs `lib/build-graph.py --changed-files ` and appends the JSON structural analysis (affected objectives, orphaned prerequisites, thin evidence) to the review prompt. Graph failures are non-fatal — review proceeds without it.
**Environment variables consumed**:
diff --git a/supervisor/AGENTS.md b/supervisor/AGENTS.md
index 322ab4b..e7842e4 100644
--- a/supervisor/AGENTS.md
+++ b/supervisor/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Supervisor Agent
**Role**: Health monitoring and auto-remediation, executed as a formula-driven
diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh
index b0a6cf0..365be65 100644
--- a/tests/smoke-init.sh
+++ b/tests/smoke-init.sh
@@ -26,7 +26,9 @@ pass() { printf 'PASS: %s\n' "$*"; }
cleanup() {
rm -rf "$MOCK_BIN" "$MOCK_STATE" /tmp/smoke-test-repo \
"${FACTORY_ROOT}/projects/smoke-repo.toml" \
- "${FACTORY_ROOT}/docker-compose.yml"
+ "${FACTORY_ROOT}/docker-compose.yml" \
+ "${FACTORY_ROOT}/docker/Caddyfile" \
+ "${FACTORY_ROOT}/docker/staging-seed"
# Restore .env only if we created the backup
if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then
mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env"
diff --git a/vault/AGENTS.md b/vault/AGENTS.md
index 5b010ec..f44e063 100644
--- a/vault/AGENTS.md
+++ b/vault/AGENTS.md
@@ -1,4 +1,4 @@
-
+
# Vault Agent
**Role**: Three-pipeline gate — action safety classification, resource procurement, and human-action drafting.