chore: gardener housekeeping 2026-04-12
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Claude 2026-04-12 04:08:10 +00:00
parent 9d778f6fd6
commit aeda17a601
10 changed files with 11 additions and 32 deletions

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Disinto — Agent Instructions
## What this repo is

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Architect — Agent Instructions
## What this agent is

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Dev Agent
**Role**: Implement issues autonomously — write code, push branches, address

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Gardener Agent
**Role**: Backlog grooming — detect duplicate issues, missing acceptance

View file

@ -1,22 +1 @@
[
{
"action": "edit_body",
"issue": 710,
"body": "## Goal\n\nDecide and implement a conversation history persistence model for `disinto-chat`. MVP target: append-only per-user NDJSON files on a bind-mounted host volume, one file per conversation, with a simple history list endpoint and sidebar in the UI.\n\n## Why\n\n- Without history, every page refresh loses context. Claude is stateless per invocation; the chat UI is what makes it feel like a conversation.\n- A full database with search is overkill for a personal / small-team factory (#623 security posture). Flat files are enough and recoverable by `cat`.\n\n## Scope\n\n### Files to touch\n\n- `lib/generators.sh` chat service:\n - Add a writable bind mount `${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history` (one per-project host path; compose already pins the project root).\n - Must coexist with #706's read-only rootfs (this is a separate mount, not part of rootfs — sanity-check the sandbox verify script still passes).\n- `docker/chat/server.{py,go}`:\n - On each `POST /chat`, append one NDJSON line `{ts, user, role, content}` to `/var/lib/chat/history/<user>/<conversation_id>.ndjson`.\n - `GET /chat/history` → returns the list of conversation ids and first-message previews for the logged-in user.\n - `GET /chat/history/<id>` → returns the full conversation for the logged-in user; 404 if the file belongs to another user.\n - New conversation: `POST /chat/new` → generates a fresh conversation_id (random 12-char hex) and returns it.\n - UI: sidebar with conversation list, \"new chat\" button, load history into the log on click.\n- File naming: `<user>/<conversation_id>.ndjson` — user-scoped directory prevents cross-user leakage even if a bug leaks ids. `conversation_id` must match `^[0-9a-f]{12}$`, no slashes allowed.\n\n### Out of scope\n\n- Full-text search.\n- Database / SQLite.\n- History retention / rotation — unbounded for now.\n\n### In scope explicitly\n\n- Replaying prior turns back into the `claude --print` subprocess for follow-up turns: the backend must feed the prior NDJSON lines back into claude via whatever convention the agent code uses. Cross-check `docker/agents/entrypoint.sh` for how agents pass conversation state.\n\n## Acceptance\n\n- [ ] Sending 3 messages, refreshing the page, and clicking the conversation in the sidebar re-loads all 3 messages.\n- [ ] A new conversation starts with an empty context and does not see prior messages.\n- [ ] `ls state/chat-history/disinto-admin/` on the host shows one NDJSON file per conversation, each line is valid JSON.\n- [ ] A second user logging in via the #708 allowlist sees only their own conversations.\n- [ ] History endpoints are blocked for unauthenticated requests (inherits #708 / #709 auth).\n\n## Depends on\n\n- #705 (chat scaffold).\n\n## Notes\n\n- NDJSON, not JSON-array: append is O(1) and partial writes never corrupt prior lines. Mirrors the factory's CI log format at `lib/ci-log-reader.py`.\n- Per-user directory, not a single shared dir — path traversal via a crafted `conversation_id` is the main risk. The strict regex above is the mitigation.\n\n## Boundaries for dev-agent\n\n- Do not add SQLite, Postgres, or any database. Files.\n- Do not invent a conversation replay system. Whatever `claude --print` / the agents already do for context is the baseline — match it.\n- Do not store history inside the container's tmpfs — it has to survive container restarts.\n- Parent vision: #623.\n\n## Affected files\n- `lib/generators.sh` — chat service bind mount CHAT_HISTORY_DIR:/var/lib/chat/history\n- `docker/chat/server.{py,go}` — NDJSON append on POST /chat; GET /chat/history; GET /chat/history/<id>; POST /chat/new"
},
{
"action": "edit_body",
"issue": 711,
"body": "## Goal\n\nAdd per-user cost and request caps to `disinto-chat` so a compromised session (or a wedged browser tab firing requests in a loop) cannot run up an unbounded Anthropic bill or starve the agents' token budget.\n\n## Why\n\n- #623 \"Open questions\" explicitly calls this out. Chat is the only user-facing surface that spawns Claude on demand; no other factory surface does.\n- Cheap to enforce (counter + bash-style dict), expensive to forget.\n\n## Scope\n\n### Files to touch\n\n- `docker/chat/server.{py,go}`:\n - Per-user sliding-window request counter: `CHAT_MAX_REQUESTS_PER_HOUR` (default `60`), `CHAT_MAX_REQUESTS_PER_DAY` (default `500`).\n - Per-user token-cost counter: after each `claude --print`, parse the final `usage` event from `--output-format stream-json` if present; track cumulative tokens per day; reject if over `CHAT_MAX_TOKENS_PER_DAY` (default `1000000`).\n - Counters stored in-memory; reset on container restart (acceptable for MVP; file-based persistence is a follow-up).\n - Rejection response: 429 with `Retry-After` header and a friendly HTMX fragment explaining which cap was hit.\n- `lib/generators.sh` chat env: expose the three caps as overridable env vars with sane defaults baked in.\n\n### Out of scope\n\n- Billing dashboard.\n- Cross-container token budget coordination with the agents.\n- Cost tracking via Anthropic's billing API (not stable enough to depend on).\n\n## Acceptance\n\n- [ ] Sending 61 requests in an hour trips the hourly cap and returns 429 with `Retry-After: <seconds>`.\n- [ ] A single large completion that pushes daily tokens over the cap blocks the *next* request, not the current one (atomic check-then-consume is OK to skip for MVP).\n- [ ] Resetting the container clears counters (verified manually).\n- [ ] Caps are configurable via `.env` without rebuilding the image.\n\n## Depends on\n\n- #705 (chat scaffold).\n\n## Notes\n\n- Token accounting from `claude --print`: the stream-json mode emits a final `usage` event. If that event is absent or its format changes, fall back to a coarse request count only — do not block the user on parsing failures.\n- `Retry-After` must be an integer seconds value, not an HTTP-date, for HTMX to handle it cleanly client-side.\n\n## Boundaries for dev-agent\n\n- Do not add a rate-limiting library. A dict + timestamp list is sufficient for three counters.\n- Do not persist counters to disk this chunk. In-memory is the contract.\n- Do not block requests on Anthropic's own rate limiter. That is retried by `claude` itself; this layer is about *cost*, not throttling.\n- Parent vision: #623.\n\n## Affected files\n- `docker/chat/server.{py,go}` — per-user sliding-window request counter and token-cost counter; 429 rejection with Retry-After header\n- `lib/generators.sh` — chat env: CHAT_MAX_REQUESTS_PER_HOUR, CHAT_MAX_REQUESTS_PER_DAY, CHAT_MAX_TOKENS_PER_DAY"
},
{
"action": "edit_body",
"issue": 712,
"body": "## Goal\n\nLet `disinto-chat` perform scoped write actions against the factory — specifically: trigger a Woodpecker CI run, create a Forgejo issue, create a Forgejo PR — via explicit backend endpoints. The UI surfaces these as buttons the user clicks from a chat turn that proposes an action. The model never holds API tokens directly.\n\n## Why\n\n- #623 lists these escalations as the difference between \"chat that talks about the project\" and \"chat that moves the project forward\".\n- Routing through explicit backend endpoints (instead of giving the sandboxed claude process API tokens) keeps the trust model tight: the *user* authorises each action, not the model.\n\n## Scope\n\n### Files to touch\n\n- `docker/chat/server.{py,go}` — new authenticated endpoints (reuse #708 / #709 session check):\n - `POST /chat/action/ci-run` — body `{repo, branch}` → calls Woodpecker API with `WOODPECKER_TOKEN` (already in `.env` from existing factory setup) to trigger a pipeline.\n - `POST /chat/action/issue-create` — body `{title, body, labels}` → calls Forgejo API `/repos/<owner>/<repo>/issues` with `FORGE_TOKEN`.\n - `POST /chat/action/pr-create` — body `{head, base, title, body}` → calls `/repos/<owner>/<repo>/pulls`.\n - All actions record to #710's NDJSON history as `{role: \"action\", ...}` lines.\n- `docker/chat/ui/index.html` — small HTMX pattern: when claude's response contains a marker like `<action type=\"issue-create\">{...}</action>`, render a clickable button below the message; clicking POSTs to `/chat/action/<type>` with the payload.\n- `lib/generators.sh` chat env: pass `WOODPECKER_TOKEN`, `FORGE_TOKEN`, `FORGE_URL`, `FORGE_OWNER`, `FORGE_REPO`.\n\n### Out of scope\n\n- Destructive actions (branch delete, force push, secret rotation) — deliberately excluded.\n- Multi-step workflows / approval chains.\n- Arbitrary code execution in the chat container (that is what the agents exist for).\n\n## Acceptance\n\n- [ ] A chat turn that emits an `<action type=\"issue-create\">{...}</action>` block renders a button; clicking it creates an issue on Forgejo, visible via the API.\n- [ ] CI-trigger action creates a Woodpecker pipeline that can be seen in the CI UI.\n- [ ] PR-create action produces a Forgejo PR with the specified head / base.\n- [ ] All three actions are logged into the #710 history file with role `action` and the response from the API call.\n- [ ] Unauthenticated requests to `/chat/action/*` return 401 (inherits #708 gate).\n\n## Depends on\n\n- #708 (OAuth gate — actions are authorised by the logged-in user).\n- #710 (history — actions need to be logged alongside chat turns).\n\n## Notes\n\n- Forgejo API auth: the factory's `FORGE_TOKEN` is a long-lived admin token. For MVP, reuse it; a follow-up issue can scope it down to per-user Forgejo tokens derived from the OAuth flow.\n- Woodpecker API is at `http://woodpecker:8000/api/...`, reachable via the compose network — no need to go through the edge container.\n- The `<action>` marker is deliberately simple markup the model can emit in its response text. Do not implement tool-calling protocol; do not spin up an MCP server.\n\n## Boundaries for dev-agent\n\n- Do not give the claude subprocess direct API tokens. The chat backend holds them; the model only emits action markers the user clicks.\n- Do not add destructive actions (delete, force-push). Additive only.\n- Do not invent a new markup format beyond `<action type=\"...\">{JSON}</action>`.\n- Parent vision: #623.\n\n## Affected files\n- `docker/chat/server.{py,go}` — new endpoints: POST /chat/action/ci-run, POST /chat/action/issue-create, POST /chat/action/pr-create\n- `docker/chat/ui/index.html` — HTMX pattern for action buttons triggered by <action type=\"...\"> markers\n- `lib/generators.sh` — chat env: WOODPECKER_TOKEN, FORGE_TOKEN, FORGE_URL, FORGE_OWNER, FORGE_REPO"
},
{
"action": "edit_body",
"issue": 713,
"body": "## Goal\n\nContingency track: if the subpath routing + Forgejo OAuth combination from #704 and #708 proves unworkable (redirect loops, Forgejo `ROOT_URL` quirks, etc.), provide a documented fallback using per-service subdomains (`forge.<project>.disinto.ai`, `ci.<project>.disinto.ai`, `chat.<project>.disinto.ai`) under the same wildcard cert.\n\n## Why\n\n- #623 Scope Highlights mentions this as the fallback if subpath OAuth fails.\n- Documenting the fallback up front means we can pivot without a days-long investigation when the subpath approach hits a wall.\n- The wildcard cert from #621 already covers `*.disinto.ai` at no extra cost.\n\n## Scope\n\nThis issue is a **plan + small toggle**, not a full implementation. Implementation only happens if #704 or #708 get stuck.\n\n### Files to touch\n\n- `docs/edge-routing-fallback.md` (new) — documents the fallback topology, diffing concretely against #704 / #708:\n - Caddyfile: four separate host blocks (`<project>.disinto.ai`, `forge.<project>...`, `ci.<project>...`, `chat.<project>...`), each a single `reverse_proxy` to the container.\n - Forgejo `ROOT_URL` becomes `https://forge.<project>.disinto.ai/` (root path, not subpath).\n - Woodpecker `WOODPECKER_HOST` becomes `https://ci.<project>.disinto.ai`.\n - OAuth redirect URIs (chat, woodpecker) become sub-subdomain paths.\n - DNS: all handled by the existing wildcard; no new records.\n- `lib/generators.sh` — no code change until pivot; document the env vars that would need to change (`EDGE_TUNNEL_FQDN_FORGE`, etc.) in a comment near `generate_compose`.\n- `tools/edge-control/register.sh` (from #621) — leave a TODO comment noting the fallback shape would need an additional subdomain parameter per project.\n\n### Out of scope (unless pivot)\n\n- Actually implementing the fallback — gated on #704 / #708 failing.\n\n## Acceptance\n\n- [ ] `docs/edge-routing-fallback.md` exists and is concrete enough that a follow-up PR to pivot would take under a day.\n- [ ] The doc names exactly which files / lines each pivot would touch (Caddyfile, `lib/generators.sh`, `lib/ci-setup.sh` redirect URI).\n- [ ] A pivot decision criterion is written into the doc: \"pivot if <specific symptom>, not if <symptom with a known fix>\".\n\n## Depends on\n\n- None — can be written in parallel to #704 / #708.\n\n## Notes\n\n- Keep the doc short. This is a pressure-release valve, not a parallel architecture.\n- Whichever chunk is implementing subpaths first should update this doc if they hit a blocker so the pivot decision is informed.\n\n## Boundaries for dev-agent\n\n- This is a documentation chunk. Do not implement the fallback unless someone explicitly says to pivot.\n- Do not make the main chunks \"fallback-ready\" — that is over-engineering for a contingency.\n- Parent vision: #623.\n\n## Affected files\n- `docs/edge-routing-fallback.md` (new) — fallback topology doc with Caddyfile, Forgejo ROOT_URL, Woodpecker HOST, OAuth redirect URI changes\n- `lib/generators.sh` — comment near generate_compose documenting env vars that change on pivot\n- `tools/edge-control/register.sh` — TODO comment noting fallback shape needs additional subdomain parameter"
}
]
[]

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Shared Helpers (`lib/`)
All agents source `lib/env.sh` as their first action. Additional helpers are
@ -30,6 +30,6 @@ sourced as needed.
| `lib/git-creds.sh` | Shared git credential helper configuration. `configure_git_creds([HOME_DIR] [RUN_AS_CMD])` — writes a static credential helper script and configures git globally to use password-based HTTP auth (Forgejo 11.x rejects API tokens for `git push`, #361). `repair_baked_cred_urls([--as RUN_AS_CMD] DIR ...)` — rewrites any git remote URLs that have credentials baked in to use clean URLs instead; uses `safe.directory` bypass for root-owned repos (#671). Requires `FORGE_PASS`, `FORGE_URL`, `FORGE_TOKEN`. | entrypoints (agents, edge) |
| `lib/ops-setup.sh` | `setup_ops_repo()` — creates ops repo on Forgejo if it doesn't exist, configures bot collaborators, clones/initializes ops repo locally, seeds directory structure (vault, knowledge, evidence, sprints). Evidence subdirectories seeded: engagement/, red-team/, holdout/, evolution/, user-test/. Also seeds sprints/ for architect output. Exports `_ACTUAL_OPS_SLUG`. `migrate_ops_repo(ops_root, [primary_branch])` — idempotent migration helper that seeds missing directories and .gitkeep files on existing ops repos (pre-#407 deployments). | bin/disinto (init) |
| `lib/ci-setup.sh` | `_install_cron_impl()` — installs crontab entries for bare-metal deployments (compose mode uses polling loop instead). `_create_forgejo_oauth_app()` — generic helper to create an OAuth2 app on Forgejo (shared by Woodpecker and chat). `_create_woodpecker_oauth_impl()` — creates Woodpecker OAuth2 app (thin wrapper). `_create_chat_oauth_impl()` — creates disinto-chat OAuth2 app, writes `CHAT_OAUTH_CLIENT_ID`/`CHAT_OAUTH_CLIENT_SECRET` to `.env` (#708). `_generate_woodpecker_token_impl()` — auto-generates WOODPECKER_TOKEN via OAuth2 flow. `_activate_woodpecker_repo_impl()` — activates repo in Woodpecker. All gated by `_load_ci_context()` which validates required env vars. | bin/disinto (init) |
| `lib/generators.sh` | Template generation for `disinto init`: `generate_compose()` — docker-compose.yml (uses `codeberg.org/forgejo/forgejo:11.0` tag; adds `security_opt: [apparmor:unconfined]` to all services for rootless container compatibility; Forgejo includes a healthcheck so dependent services use `condition: service_healthy` — fixes cold-start races, #665; adds `chat` service block with isolated `chat-config` named volume; all `depends_on` now use `condition: service_healthy/started` instead of bare service names), `generate_caddyfile()` — Caddyfile (routes: `/forge/*` → forgejo:3000, `/woodpecker/*` → woodpecker:8000, `/staging/*` → staging:80, `/chat/*` → chat:8080; root `/` redirects to `/forge/`), `generate_staging_index()` — staging index, `generate_deploy_pipelines()` — Woodpecker deployment pipeline configs. Requires `FACTORY_ROOT`, `PROJECT_NAME`, `PRIMARY_BRANCH`. | bin/disinto (init) |
| `lib/generators.sh` | Template generation for `disinto init`: `generate_compose()` — docker-compose.yml (uses `codeberg.org/forgejo/forgejo:11.0` tag; adds `security_opt: [apparmor:unconfined]` to all services for rootless container compatibility; Forgejo includes a healthcheck so dependent services use `condition: service_healthy` — fixes cold-start races, #665; adds `chat` service block with isolated `chat-config` named volume and `CHAT_HISTORY_DIR` bind-mount for per-user NDJSON history persistence (#710); injects `FORWARD_AUTH_SECRET` for Caddy↔chat defense-in-depth auth (#709); cost-cap env vars `CHAT_MAX_REQUESTS_PER_HOUR`, `CHAT_MAX_REQUESTS_PER_DAY`, `CHAT_MAX_TOKENS_PER_DAY` (#711); subdomain fallback comment for `EDGE_TUNNEL_FQDN_*` vars (#713); all `depends_on` now use `condition: service_healthy/started` instead of bare service names), `generate_caddyfile()` — Caddyfile (routes: `/forge/*` → forgejo:3000, `/woodpecker/*` → woodpecker:8000, `/staging/*` → staging:80; `/chat/login` and `/chat/oauth/callback` bypass `forward_auth` so unauthenticated users can reach the OAuth flow; `/chat/*` gated by `forward_auth` on `chat:8080/chat/auth/verify` which stamps `X-Forwarded-User` (#709); root `/` redirects to `/forge/`), `generate_staging_index()` — staging index, `generate_deploy_pipelines()` — Woodpecker deployment pipeline configs. Requires `FACTORY_ROOT`, `PROJECT_NAME`, `PRIMARY_BRANCH`. | bin/disinto (init) |
| `lib/hire-agent.sh` | `disinto_hire_an_agent()` — user creation, `.profile` repo setup, formula copying, branch protection, and state marker creation for hiring a new agent. Requires `FORGE_URL`, `FORGE_TOKEN`, `FACTORY_ROOT`, `PROJECT_NAME`. Extracted from `bin/disinto`. | bin/disinto (hire) |
| `lib/release.sh` | `disinto_release()` — vault TOML creation, branch setup on ops repo, PR creation, and auto-merge request for a versioned release. `_assert_release_globals()` validates required env vars. Requires `FORGE_URL`, `FORGE_TOKEN`, `FORGE_OPS_REPO`, `FACTORY_ROOT`, `PRIMARY_BRANCH`. Extracted from `bin/disinto`. | bin/disinto (release) |

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Planner Agent
**Role**: Strategic planning using a Prerequisite Tree (Theory of Constraints),

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Predictor Agent
**Role**: Abstract adversary (the "goblin"). Runs a 2-step formula

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Review Agent
**Role**: AI-powered PR review — post structured findings and formal

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b -->
# Supervisor Agent
**Role**: Health monitoring and auto-remediation, executed as a formula-driven