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 05:19:57 +00:00
parent ec7dff854a
commit 0c4f00a86c
10 changed files with 56 additions and 14 deletions

View file

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

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b --> <!-- last-reviewed: ec7dff854a71d64f427a4e6d6874de8e6dee822c -->
# Architect — Agent Instructions # Architect — Agent Instructions
## What this agent is ## What this agent is
@ -67,11 +67,12 @@ REJECT review → close PR + journal (model processes rejection, bash merges PR)
Vision issues decompose into sprint sub-issues tracked via "Decomposed from #N" in sub-issue bodies. The architect automatically closes vision issues when all sub-issues are closed: Vision issues decompose into sprint sub-issues tracked via "Decomposed from #N" in sub-issue bodies. The architect automatically closes vision issues when all sub-issues are closed:
1. Before picking new vision issues, the architect checks each open vision issue 1. Before picking new vision issues, the architect checks each open vision issue
2. For each, it queries for sub-issues with "Decomposed from #N" in their body (regardless of state) 2. For each, it queries merged sprint PRs — **only PRs whose title or body reference the specific vision issue** (matched via `#N` pattern, filtering out unrelated PRs that happen to close unrelated issues) (#735/#736)
3. If all sub-issues are closed, it posts a summary comment listing completed sub-issues 3. Extracts sub-issue numbers from those PRs, excluding the vision issue itself
4. The vision issue is then closed automatically 4. If all sub-issues are closed, posts a summary comment listing completed sub-issues (with an idempotency guard: skips if a "Vision Issue Completed" comment already exists)
5. The vision issue is then closed automatically
This ensures vision issues transition from `open``closed` once their work is complete, without manual intervention. This ensures vision issues transition from `open``closed` once their work is complete, without manual intervention. The #N-scoped matching prevents false positives where unrelated sub-issues would incorrectly trigger vision issue closure.
### Session management ### Session management

View file

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

View file

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

View file

@ -1 +1,42 @@
[] [
{
"action": "edit_body",
"issue": 737,
"body": "Flagged by AI reviewer in PR #736.\n\n## Problem\n\n`close_vision_issue()` posts a completion comment and then closes the issue in two separate API calls. The close calls (lines 587-597 of `architect/architect-run.sh`) both use `|| true`, so a failed `state=closed` PATCH is silently ignored.\n\nThe idempotency guard added in PR #736 checks for the completion comment before proceeding. If the comment was posted successfully in a prior run but the close PATCH failed, the guard will now return 0 on all subsequent runs without retrying the close — the vision issue stays open indefinitely with a misleading \"Vision Issue Completed\" comment.\n\n## Fix\n\nAfter posting the comment, verify the issue is actually closed (check `.state` in the PATCH response or re-fetch). If the close fails, return 1 so the next polling cycle retries. The idempotency guard should also verify the issue state, not just the comment presence.\n\n---\n*Auto-created from AI review*\n\n## Acceptance criteria\n\n- [ ] After posting the completion comment, `close_vision_issue()` verifies `.state == \"closed\"` in the PATCH response or re-fetches the issue; if close fails, returns 1\n- [ ] The idempotency guard checks both comment presence AND `.state == \"closed\"` before returning 0; if the comment exists but the issue is still open, attempts the close again\n- [ ] Running `close_vision_issue` twice on a stuck-open issue (comment posted but state still open) retries the close and does not post a duplicate comment\n\n## Affected files\n\n- `architect/architect-run.sh` — `close_vision_issue()` function (~lines 527-600)\n"
},
{
"action": "add_label",
"issue": 737,
"label": "backlog"
},
{
"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\n- `docker/chat/server.py` (or `server.go`) — new `/chat/action/ci-run`, `/chat/action/issue-create`, `/chat/action/pr-create` endpoints\n- `docker/chat/ui/index.html` — action button rendering from `<action type=\"...\">{...}</action>` markers\n- `lib/generators.sh` — chat service env block: pass `WOODPECKER_TOKEN`, `FORGE_TOKEN`, `FORGE_URL`, `FORGE_OWNER`, `FORGE_REPO`\n"
},
{
"action": "remove_label",
"issue": 712,
"label": "blocked"
},
{
"action": "add_label",
"issue": 712,
"label": "backlog"
},
{
"action": "edit_body",
"issue": 707,
"body": "## Goal\n\nGive `disinto-chat` its own Claude identity mount so its OAuth refresh races cannot corrupt the factory agents' shared `~/.claude` credentials. Default to a separate `~/.claude-chat/` on the host; support `ANTHROPIC_API_KEY` as a fallback that skips OAuth entirely.\n\n## Why\n\n- #623 root-caused this: Claude Code's internal refresh lock in `~/.claude.lock` operates outside bind-mounted directories, so two containers sharing `~/.claude` can race during token refresh and invalidate each other. The factory has already had OAuth expiry incidents traced to multiple agents sharing credentials.\n- Scoping chat to its own identity dir means chat can be logged in as a different Anthropic account, or pinned to an API key, without touching agent credentials.\n\n## Scope\n\n### Files to touch\n\n- `lib/generators.sh` chat service block (from #705):\n - Replace the throwaway named volume with `${CHAT_CLAUDE_DIR:-${HOME}/.claude-chat}:/home/chat/.claude-chat`.\n - Env: `CLAUDE_CONFIG_DIR=/home/chat/.claude-chat/config`, `CLAUDE_CREDENTIALS_DIR=/home/chat/.claude-chat/config/credentials`.\n - Conditional: if `ANTHROPIC_API_KEY` is set in `.env`, pass it through and **do not** mount `~/.claude-chat` at all (no credentials on disk in that mode).\n- `bin/disinto disinto_init()` — after #620's admin password prompt, add an optional prompt: `Use separate Anthropic identity for chat? (y/N)`. On yes, create `~/.claude-chat/` and invoke `claude login` in a subshell with `CLAUDE_CONFIG_DIR=~/.claude-chat/config`.\n- `lib/claude-config.sh` — factor out the existing `~/.claude` setup logic so a non-default `CLAUDE_CONFIG_DIR` is a first-class parameter. If it is already parameterised, just document it; if not, extract a helper `setup_claude_dir <dir>` and have the existing path call it with the default dir.\n- `docker/chat/Dockerfile` — declare `VOLUME /home/chat/.claude-chat`, set owner to the non-root chat user introduced in #706.\n\n### Out of scope\n\n- Cross-session lock coherence for multiple concurrent chat containers (single-chat-container assumption is fine for MVP).\n- Anthropic team / workspace support — single identity is enough.\n\n## Acceptance\n\n- [ ] Fresh `disinto init` with \"use separate chat identity\" answered yes creates `~/.claude-chat/` and logs in successfully.\n- [ ] With `ANTHROPIC_API_KEY=sk-ant-...` set in `.env`, chat starts without any `~/.claude-chat` mount (verified via `docker inspect disinto-chat`) and successfully completes a test prompt.\n- [ ] Running the factory agents AND chat simultaneously for 24h does not produce any OAuth refresh failures on either side (manual soak test — document result in PR).\n- [ ] `CLAUDE_CONFIG_DIR` and `CLAUDE_CREDENTIALS_DIR` inside the chat container resolve to `/home/chat/.claude-chat/config*`, not the shared factory path.\n\n## Depends on\n\n- #705 (chat scaffold).\n- #620 (admin password prompt — same init flow this adds a step to).\n\n## Notes\n\n- The factory's existing shared mount is `/var/lib/disinto/claude-shared` (see `lib/generators.sh:113,327,381,426`). Chat must NOT use this path.\n- `flock(\"${HOME}/.claude/session.lock\")` logic mentioned in #623 is load-bearing, not redundant — do not \"simplify\" it.\n- Prefer the API-key path for anyone running the factory on shared hardware; call this out in README updates.\n\n## Boundaries for dev-agent\n\n- Do not try to make chat share `~/.claude` with the agents \"just for convenience\". The whole point of this chunk is the opposite.\n- Do not add a third claude config dir. One for agents, one for chat, done.\n- Do not refactor `lib/claude-config.sh` beyond extracting a parameterised helper if needed.\n- Parent vision: #623.\n\n## Affected files\n\n- `lib/generators.sh` — chat service block: replace throwaway named volume with `${CHAT_CLAUDE_DIR:-${HOME}/.claude-chat}` bind mount; add `CLAUDE_CONFIG_DIR`/`CLAUDE_CREDENTIALS_DIR` env vars; skip mount when `ANTHROPIC_API_KEY` is set\n- `bin/disinto` — `disinto_init()`: add optional prompt for separate Anthropic identity for chat\n- `lib/claude-config.sh` — extract parameterized `setup_claude_dir <dir>` helper\n- `docker/chat/Dockerfile` — declare `VOLUME /home/chat/.claude-chat`, set owner to non-root chat user\n"
},
{
"action": "remove_label",
"issue": 707,
"label": "blocked"
},
{
"action": "add_label",
"issue": 707,
"label": "backlog"
}
]

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 9d778f6fd6672f1fd9446b0007b64846e209dc0b --> <!-- last-reviewed: ec7dff854a71d64f427a4e6d6874de8e6dee822c -->
# Shared Helpers (`lib/`) # Shared Helpers (`lib/`)
All agents source `lib/env.sh` as their first action. Additional helpers are All agents source `lib/env.sh` as their first action. Additional helpers are

View file

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

View file

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

View file

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

View file

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