From 44cbbbde62f6c025cde17404a6c5b23064368cba Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 12:20:15 +0000 Subject: [PATCH 1/3] feat: integrate tea CLI for forge issue/label/comment operations (#666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lib/tea-helpers.sh with tea_file_issue, tea_relabel, tea_comment, tea_close — thin wrappers preserving secret scanning on write ops - Add tea 0.9.2 binary to docker/agents/Dockerfile - Configure tea login in docker/agents/entrypoint.sh from FORGE_TOKEN/FORGE_URL - Derive TEA_LOGIN in lib/env.sh (codeberg vs local forgejo) - Source tea-helpers.sh conditionally when tea binary is available - Migrate predictor formula from inline curl to tea CLI commands - Register tea-helpers.sh in smoke test function resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/agent-smoke.sh | 6 ++- docker/agents/Dockerfile | 4 ++ docker/agents/entrypoint.sh | 18 +++++++++ formulas/run-predictor.toml | 34 ++++++++-------- lib/AGENTS.md | 1 + lib/env.sh | 15 +++++++ lib/tea-helpers.sh | 78 +++++++++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 lib/tea-helpers.sh diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index 89cb74e..5b7dd54 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -98,6 +98,7 @@ echo "=== 2/2 Function resolution ===" # lib/load-project.sh — sourced by env.sh when PROJECT_TOML is set # lib/file-action-issue.sh — sourced by gardener-run.sh (file_action_issue) # lib/secret-scan.sh — sourced by file-action-issue.sh, phase-handler.sh (scan_for_secrets, redact_secrets) +# lib/tea-helpers.sh — sourced by env.sh when tea is available (tea_file_issue, tea_relabel, etc.) # lib/formula-session.sh — sourced by formula-driven agents (acquire_cron_lock, run_formula_and_monitor, etc.) # lib/mirrors.sh — sourced by merge sites (mirror_push) # lib/guard.sh — sourced by all cron entry points (check_active) @@ -111,7 +112,7 @@ echo "=== 2/2 Function resolution ===" # If a new lib file is added and sourced by agents, add it to LIB_FUNS below # and add a check_script call for it in the lib files section further down. LIB_FUNS=$( - for f in lib/agent-session.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh lib/secret-scan.sh lib/file-action-issue.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh; do + for f in lib/agent-session.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh lib/secret-scan.sh lib/file-action-issue.sh lib/tea-helpers.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh; do if [ -f "$f" ]; then get_fns "$f"; fi done | sort -u ) @@ -124,7 +125,7 @@ KNOWN_CMDS=( false 'fi' find flock for getopts git grep gzip gunzip head hash 'if' jq kill local ln ls mapfile mkdir mktemp mv nc pgrep printf python3 python read readarray return rm sed set sh shift sleep - sort source stat tail tar test 'then' tmux touch tr trap true type + sort source stat tail tar tea test 'then' tmux touch tr trap true type unset until wait wc while which xargs ) @@ -179,6 +180,7 @@ check_script lib/agent-session.sh check_script lib/ci-helpers.sh check_script lib/secret-scan.sh check_script lib/file-action-issue.sh lib/secret-scan.sh +check_script lib/tea-helpers.sh lib/secret-scan.sh check_script lib/formula-session.sh lib/agent-session.sh check_script lib/load-project.sh check_script lib/mirrors.sh diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile index 3ad7ef3..236b4ad 100644 --- a/docker/agents/Dockerfile +++ b/docker/agents/Dockerfile @@ -4,6 +4,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash curl git jq tmux cron python3 openssh-client ca-certificates \ && rm -rf /var/lib/apt/lists/* +# tea CLI — official Gitea/Forgejo CLI for issue/label/comment operations +RUN curl -sL https://dl.gitea.com/tea/0.9.2/tea-0.9.2-linux-amd64 -o /usr/local/bin/tea \ + && chmod +x /usr/local/bin/tea + # Claude CLI is mounted from the host via docker-compose volume. # No internet access to cli.anthropic.com required at build time. diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 993f721..9b83d32 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -57,6 +57,24 @@ log "Claude CLI: $(claude --version 2>&1 || true)" install_project_crons +# Configure tea CLI login for forge operations (runs as agent user). +# tea stores config in ~/.config/tea/ — persistent across container restarts +# only if that directory is on a mounted volume. +if command -v tea &>/dev/null && [ -n "${FORGE_TOKEN:-}" ] && [ -n "${FORGE_URL:-}" ]; then + local_tea_login="forgejo" + case "$FORGE_URL" in + *codeberg.org*) local_tea_login="codeberg" ;; + esac + su -s /bin/bash agent -c "tea login add \ + --name '${local_tea_login}' \ + --url '${FORGE_URL}' \ + --token '${FORGE_TOKEN}' \ + --no-version-check 2>/dev/null || true" + log "tea login configured: ${local_tea_login} → ${FORGE_URL}" +else + log "tea login: skipped (tea not found or FORGE_TOKEN/FORGE_URL not set)" +fi + # Start matrix listener in background (if configured) if [ -n "${MATRIX_TOKEN:-}" ] && [ -n "${MATRIX_ROOM_ID:-}" ]; then log "Starting matrix listener in background" diff --git a/formulas/run-predictor.toml b/formulas/run-predictor.toml index dd00403..fcbe849 100644 --- a/formulas/run-predictor.toml +++ b/formulas/run-predictor.toml @@ -148,27 +148,27 @@ For each weakness you identify, choose one: Valid outcome. Not every run needs to produce a prediction. But if you skip, write a brief note to your scratch file about why. -## Filing +## Filing (use tea CLI — labels by name, no ID lookup needed) -1. Look up label IDs: - curl -sf -H "Authorization: token $FORGE_TOKEN" \ - "$FORGE_API/labels" | jq '[.[] | select(.name | startswith("prediction")) | {name, id}]' - curl -sf -H "Authorization: token $FORGE_TOKEN" \ - "$FORGE_API/labels" | jq '.[] | select(.name == "action") | .id' +tea is pre-configured with login "$TEA_LOGIN" and repo "$FORGE_REPO". -2. File predictions: - curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \ - -H "Content-Type: application/json" \ - "$FORGE_API/issues" \ - -d '{"title":"","body":"<body>","labels":[<prediction_unreviewed_id>]}' +1. File predictions (labels by name, no ID lookup): + tea issues create --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --title "<title>" --body "<body>" --labels "prediction/unreviewed" -3. File action dispatches (if exploiting): - curl -sf -X POST -H "Authorization: token $FORGE_TOKEN" \ - -H "Content-Type: application/json" \ - "$FORGE_API/issues" \ - -d '{"title":"action: test prediction #NNN — <formula> <focus>","body":"<body>","labels":[<action_label_id>]}' +2. File action dispatches (if exploiting): + tea issues create --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --title "action: test prediction #NNN — <formula> <focus>" \ + --body "<body>" --labels "action" -4. Do NOT duplicate existing open predictions. If your theory matches +3. Close superseded predictions: + tea issues close <number> --login "$TEA_LOGIN" --repo "$FORGE_REPO" + +4. Add a comment when closing (optional): + tea comment create <number> --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --body "Superseded by #NNN" + +5. Do NOT duplicate existing open predictions. If your theory matches an open prediction/unreviewed or prediction/backlog issue, skip it. ## Rules diff --git a/lib/AGENTS.md b/lib/AGENTS.md index 5d6ad86..be56a4c 100644 --- a/lib/AGENTS.md +++ b/lib/AGENTS.md @@ -18,4 +18,5 @@ sourced as needed. | `lib/build-graph.py` | Python tool: parses VISION.md, prerequisite-tree.md, AGENTS.md, formulas/*.toml, evidence/, and forge issues/labels into a NetworkX DiGraph. Runs structural analyses (orphaned objectives, stale prerequisites, thin evidence, circular deps) and outputs a JSON report. Used by `review-pr.sh` (per-PR changed-file analysis) and `predictor-run.sh` (full-project analysis) to provide structural context to Claude. | review-pr.sh, predictor-run.sh | | `lib/secret-scan.sh` | `scan_for_secrets()` — detects potential secrets (API keys, bearer tokens, private keys, URLs with embedded credentials) in text; returns 1 if secrets found. `redact_secrets()` — replaces detected secret patterns with `[REDACTED]`. | file-action-issue.sh, phase-handler.sh | | `lib/file-action-issue.sh` | `file_action_issue()` — dedup check, secret scan, label lookup, and issue creation for formula-driven cron wrappers. Sets `FILED_ISSUE_NUM` on success. Returns 4 if secrets detected in body. | (available for future use) | +| `lib/tea-helpers.sh` | `tea_file_issue(title, body, labels...)` — create issue via tea CLI with secret scanning; sets `FILED_ISSUE_NUM`. `tea_relabel(issue_num, labels...)` — replace labels. `tea_comment(issue_num, body)` — add comment with secret scanning. `tea_close(issue_num)` — close issue. All use `TEA_LOGIN` and `FORGE_REPO` from env.sh. Labels by name (no ID lookup). Sourced by env.sh when `tea` binary is available. | env.sh (conditional) | | `lib/agent-session.sh` | Shared tmux + Claude session helpers: `create_agent_session()`, `inject_formula()`, `agent_wait_for_claude_ready()`, `agent_inject_into_session()`, `agent_kill_session()`, `monitor_phase_loop()`, `read_phase()`, `write_compact_context()`. `create_agent_session(session, workdir, [phase_file])` optionally installs a PostToolUse hook (matcher `Bash\|Write`) that detects phase file writes in real-time — when Claude writes to the phase file, the hook writes a marker so `monitor_phase_loop` reacts on the next poll instead of waiting for mtime changes. Also installs a StopFailure hook (matcher `rate_limit\|server_error\|authentication_failed\|billing_error`) that writes `PHASE:failed` with an `api_error` reason to the phase file and touches the phase-changed marker, so the orchestrator discovers API errors within one poll cycle instead of waiting for idle timeout. Also installs a SessionStart hook (matcher `compact`) that re-injects phase protocol instructions after context compaction — callers write the context file via `write_compact_context(phase_file, content)`, and the hook (`on-compact-reinject.sh`) outputs the file content to stdout so Claude retains critical instructions. When `MATRIX_THREAD_ID` is exported, also installs a Stop hook (`on-stop-matrix.sh`) that streams each Claude response to the Matrix thread. When `phase_file` is set, passes it to the idle stop hook (`on-idle-stop.sh`) so the hook can **nudge Claude** (up to 2 times) if Claude returns to the prompt without writing to the phase file — the hook injects a tmux reminder asking Claude to signal PHASE:done or PHASE:awaiting_ci. The PreToolUse guard hook (`on-pretooluse-guard.sh`) receives the session name as a third argument — formula agents (`gardener-*`, `planner-*`, `predictor-*`, `supervisor-*`) are identified this way and allowed to access `FACTORY_ROOT` from worktrees (they need env.sh, AGENTS.md, formulas/, lib/). `monitor_phase_loop` sets `_MONITOR_LOOP_EXIT` to one of: `done`, `idle_timeout`, `idle_prompt` (Claude returned to `>` for 3 consecutive polls without writing any phase — callback invoked with `PHASE:failed`, session already dead), `crashed`, or `PHASE:escalate` / other `PHASE:*` string. **Unified escalation**: `PHASE:escalate` is the signal that a session needs human input (renamed from `PHASE:needs_human`). **Callers must handle `idle_prompt`** in both their callback and their post-loop exit handler — see [`docs/PHASE-PROTOCOL.md` idle_prompt](docs/PHASE-PROTOCOL.md#idle_prompt-exit-reason) for the full contract. | dev-agent.sh, action-agent.sh | diff --git a/lib/env.sh b/lib/env.sh index d94c146..221438e 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -65,6 +65,15 @@ export FORGE_API="${FORGE_API:-${FORGE_URL}/api/v1/repos/${FORGE_REPO}}" export FORGE_WEB="${FORGE_WEB:-${FORGE_URL}/${FORGE_REPO}}" export CODEBERG_API="${FORGE_API}" # backwards compat export CODEBERG_WEB="${FORGE_WEB}" # backwards compat +# tea CLI login name: derived from FORGE_URL (codeberg vs local forgejo) +if [ -z "${TEA_LOGIN:-}" ]; then + case "${FORGE_URL}" in + *codeberg.org*) TEA_LOGIN="codeberg" ;; + *) TEA_LOGIN="forgejo" ;; + esac +fi +export TEA_LOGIN + export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}" export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}" export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}" @@ -218,3 +227,9 @@ matrix_send_ctx() { printf '%s' "$event_id" fi } + +# Source tea helpers (available when tea binary is installed) +if command -v tea &>/dev/null; then + # shellcheck source=tea-helpers.sh + source "$(dirname "${BASH_SOURCE[0]}")/tea-helpers.sh" +fi diff --git a/lib/tea-helpers.sh b/lib/tea-helpers.sh new file mode 100644 index 0000000..33b4dfc --- /dev/null +++ b/lib/tea-helpers.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# tea-helpers.sh — Thin wrappers around tea CLI for forge issue operations +# +# Usage: source this file (after env.sh), then call tea_* functions. +# Requires: tea binary in PATH, TEA_LOGIN and FORGE_REPO from env.sh, +# scan_for_secrets from lib/secret-scan.sh +# +# tea_file_issue <title> <body> <labels...> +# Sets FILED_ISSUE_NUM on success. +# Returns: 0=created, 3=API/tea error, 4=secrets detected +# +# tea_relabel <issue_number> <labels...> +# tea_comment <issue_number> <body> +# tea_close <issue_number> + +# Load secret scanner +# shellcheck source=secret-scan.sh +source "$(dirname "${BASH_SOURCE[0]}")/secret-scan.sh" + +tea_file_issue() { + local title="$1" body="$2" + shift 2 + FILED_ISSUE_NUM="" + + # Secret scan: reject issue bodies containing embedded secrets + if ! scan_for_secrets "$body"; then + echo "tea-helpers: BLOCKED — issue body contains potential secrets. Use env var references instead." >&2 + return 4 + fi + + # Join remaining args as comma-separated label names + local IFS=',' + local labels="$*" + + local result + result=$(tea issues create --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --title "$title" --body "$body" --labels "$labels" \ + --output simple 2>&1) || { + echo "tea-helpers: tea issues create failed: ${result}" >&2 + return 3 + } + + # Parse issue number from tea output (e.g. "#42 Title") + FILED_ISSUE_NUM=$(printf '%s' "$result" | grep -oE '#[0-9]+' | head -1 | tr -d '#') + if [ -z "$FILED_ISSUE_NUM" ]; then + # Fallback: extract any number + FILED_ISSUE_NUM=$(printf '%s' "$result" | grep -oE '[0-9]+' | head -1) + fi +} + +tea_relabel() { + local issue_num="$1" + shift + + local IFS=',' + local labels="$*" + + tea issues labels "$issue_num" --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --labels "$labels" +} + +tea_comment() { + local issue_num="$1" body="$2" + + # Secret scan: reject comment bodies containing embedded secrets + if ! scan_for_secrets "$body"; then + echo "tea-helpers: BLOCKED — comment body contains potential secrets. Use env var references instead." >&2 + return 4 + fi + + tea comment create "$issue_num" --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + --body "$body" +} + +tea_close() { + local issue_num="$1" + tea issues close "$issue_num" --login "$TEA_LOGIN" --repo "$FORGE_REPO" +} From 687bf0ad5bdbaa19c2cfc5c83ec94ffa05a935f7 Mon Sep 17 00:00:00 2001 From: openhands <openhands@all-hands.dev> Date: Wed, 25 Mar 2026 13:22:56 +0000 Subject: [PATCH 2/3] fix: move tea-helpers.sh out of LIB_FUNS loop in smoke test (#666) On Alpine/busybox, adding tea-helpers.sh to the LIB_FUNS for-loop caused forge_api to go missing from the extracted function set. Since no other script currently calls tea_* functions, tea-helpers.sh is checked standalone via check_script instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .woodpecker/agent-smoke.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index 5b7dd54..c280006 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -98,12 +98,12 @@ echo "=== 2/2 Function resolution ===" # lib/load-project.sh — sourced by env.sh when PROJECT_TOML is set # lib/file-action-issue.sh — sourced by gardener-run.sh (file_action_issue) # lib/secret-scan.sh — sourced by file-action-issue.sh, phase-handler.sh (scan_for_secrets, redact_secrets) -# lib/tea-helpers.sh — sourced by env.sh when tea is available (tea_file_issue, tea_relabel, etc.) # lib/formula-session.sh — sourced by formula-driven agents (acquire_cron_lock, run_formula_and_monitor, etc.) # lib/mirrors.sh — sourced by merge sites (mirror_push) # lib/guard.sh — sourced by all cron entry points (check_active) # # Excluded — not sourced inline by agents: +# lib/tea-helpers.sh — sourced conditionally by env.sh (tea_file_issue, etc.); checked standalone below # lib/ci-debug.sh — standalone CLI tool, run directly (not sourced) # lib/matrix_listener.sh — standalone systemd daemon (not sourced) # lib/parse-deps.sh — executed via `bash lib/parse-deps.sh` (not sourced) @@ -112,7 +112,7 @@ echo "=== 2/2 Function resolution ===" # If a new lib file is added and sourced by agents, add it to LIB_FUNS below # and add a check_script call for it in the lib files section further down. LIB_FUNS=$( - for f in lib/agent-session.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh lib/secret-scan.sh lib/file-action-issue.sh lib/tea-helpers.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh; do + for f in lib/agent-session.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh lib/secret-scan.sh lib/file-action-issue.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh; do if [ -f "$f" ]; then get_fns "$f"; fi done | sort -u ) From 50b5cea2cb311849208a637fbdbbee3afcef3212 Mon Sep 17 00:00:00 2001 From: openhands <openhands@all-hands.dev> Date: Wed, 25 Mar 2026 13:34:58 +0000 Subject: [PATCH 3/3] fix: tea_relabel uses edit subcommand, add sha256 checksum for tea binary (#666) - tea_relabel: use `tea issues edit` instead of `tea issues labels` (the latter is the list subcommand and ignores --labels) - Dockerfile: verify tea binary sha256 after download Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- docker/agents/Dockerfile | 2 ++ lib/tea-helpers.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile index 236b4ad..b7641c1 100644 --- a/docker/agents/Dockerfile +++ b/docker/agents/Dockerfile @@ -5,7 +5,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # tea CLI — official Gitea/Forgejo CLI for issue/label/comment operations +# Checksum from https://dl.gitea.com/tea/0.9.2/tea-0.9.2-linux-amd64.sha256 RUN curl -sL https://dl.gitea.com/tea/0.9.2/tea-0.9.2-linux-amd64 -o /usr/local/bin/tea \ + && echo "be10cdf9a619e3c0f121df874960ed19b53e62d1c7036cf60313a28b5227d54d /usr/local/bin/tea" | sha256sum -c - \ && chmod +x /usr/local/bin/tea # Claude CLI is mounted from the host via docker-compose volume. diff --git a/lib/tea-helpers.sh b/lib/tea-helpers.sh index 33b4dfc..0292742 100644 --- a/lib/tea-helpers.sh +++ b/lib/tea-helpers.sh @@ -55,7 +55,7 @@ tea_relabel() { local IFS=',' local labels="$*" - tea issues labels "$issue_num" --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ + tea issues edit "$issue_num" --login "$TEA_LOGIN" --repo "$FORGE_REPO" \ --labels "$labels" }