feat: integrate tea CLI for forge issue/label/comment operations (#666)
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
5e66ba7d12
commit
44cbbbde62
7 changed files with 137 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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":"<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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
15
lib/env.sh
15
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
|
||||
|
|
|
|||
78
lib/tea-helpers.sh
Normal file
78
lib/tea-helpers.sh
Normal file
|
|
@ -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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue