Compare commits
1 commit
a54e238282
...
657b8aff36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
657b8aff36 |
3 changed files with 16 additions and 12 deletions
|
|
@ -22,4 +22,4 @@ sourced as needed.
|
||||||
| `lib/pr-lifecycle.sh` | Reusable PR lifecycle library: `pr_create()`, `pr_find_by_branch()`, `pr_poll_ci()`, `pr_poll_review()`, `pr_merge()`, `pr_is_merged()`, `pr_walk_to_merge()`, `build_phase_protocol_prompt()`. Requires `lib/ci-helpers.sh`. | dev-agent.sh (future) |
|
| `lib/pr-lifecycle.sh` | Reusable PR lifecycle library: `pr_create()`, `pr_find_by_branch()`, `pr_poll_ci()`, `pr_poll_review()`, `pr_merge()`, `pr_is_merged()`, `pr_walk_to_merge()`, `build_phase_protocol_prompt()`. Requires `lib/ci-helpers.sh`. | dev-agent.sh (future) |
|
||||||
| `lib/issue-lifecycle.sh` | Reusable issue lifecycle library: `issue_claim()` (add in-progress, remove backlog), `issue_release()` (remove in-progress, add backlog), `issue_block()` (post diagnostic comment with secret redaction, add blocked label), `issue_close()`, `issue_check_deps()` (parse deps, check transitive closure; sets `_ISSUE_BLOCKED_BY`, `_ISSUE_SUGGESTION`), `issue_suggest_next()` (find next unblocked backlog issue; sets `_ISSUE_NEXT`), `issue_post_refusal()` (structured refusal comment with dedup). Label IDs cached in globals on first lookup. Sources `lib/secret-scan.sh`. | dev-agent.sh (future) |
|
| `lib/issue-lifecycle.sh` | Reusable issue lifecycle library: `issue_claim()` (add in-progress, remove backlog), `issue_release()` (remove in-progress, add backlog), `issue_block()` (post diagnostic comment with secret redaction, add blocked label), `issue_close()`, `issue_check_deps()` (parse deps, check transitive closure; sets `_ISSUE_BLOCKED_BY`, `_ISSUE_SUGGESTION`), `issue_suggest_next()` (find next unblocked backlog issue; sets `_ISSUE_NEXT`), `issue_post_refusal()` (structured refusal comment with dedup). Label IDs cached in globals on first lookup. Sources `lib/secret-scan.sh`. | dev-agent.sh (future) |
|
||||||
| `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 `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/). **OAuth flock**: when `DISINTO_CONTAINER=1`, Claude CLI is wrapped in `flock -w 300 ~/.claude/session.lock` to queue concurrent token refresh attempts and prevent rotation races across agents sharing the same credentials. `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 |
|
| `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 `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/). **OAuth flock**: when `DISINTO_CONTAINER=1`, Claude CLI is wrapped in `flock -w 300 ~/.claude/session.lock` to queue concurrent token refresh attempts and prevent rotation races across agents sharing the same credentials. `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 |
|
||||||
| `lib/vault.sh` | **Vault PR helper** — create vault action PRs on ops repo via Forgejo API (works from containers without SSH). `vault_request <action_id> <toml_content>` validates TOML (using `validate_vault_action` from `vault/vault-env.sh`), creates branch `vault/<action-id>`, writes `vault/actions/<action-id>.toml`, creates PR targeting `main` with title `vault: <action-id>` and body from context field, returns PR number. Idempotent: if PR exists, returns existing number. Requires `FORGE_TOKEN`, `FORGE_URL`, `FORGE_REPO`, `FORGE_OPS_REPO`. Uses agent's own token (not shared) so approval workflow respects individual identities. | dev-agent (vault actions), future vault dispatcher |
|
| `lib/vault.sh` | **Vault PR helper** — create vault action PRs on ops repo via Forgejo API (works from containers without SSH). `vault_request <action_id> <toml_content>` validates TOML (using `validate_vault_action` from `vault/vault-env.sh`), creates branch `vault/<action-id>`, writes `vault/actions/<action-id>.toml`, creates PR targeting `main` with title `vault: <action-id>` and body from context field, returns PR number. Idempotent: if PR exists, returns existing number. Requires `FORGE_TOKEN`, `FORGE_URL`, `FORGE_REPO`, `FORGE_OPS_REPO`. Uses the calling agent's own token (saves/restores `FORGE_TOKEN` around sourcing `vault-env.sh`), so approval workflow respects individual agent identities. | dev-agent (vault actions), future vault dispatcher |
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,15 @@ _prl_log() {
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# pr_create — Create a PR via forge API.
|
# pr_create — Create a PR via forge API.
|
||||||
# Args: branch title body [base_branch]
|
# Args: branch title body [base_branch] [api_url]
|
||||||
# Stdout: PR number
|
# Stdout: PR number
|
||||||
# Returns: 0=created (or found existing), 1=failed
|
# Returns: 0=created (or found existing), 1=failed
|
||||||
|
# api_url defaults to FORGE_API if not provided
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
pr_create() {
|
pr_create() {
|
||||||
local branch="$1" title="$2" body="$3"
|
local branch="$1" title="$2" body="$3"
|
||||||
local base="${4:-${PRIMARY_BRANCH:-main}}"
|
local base="${4:-${PRIMARY_BRANCH:-main}}"
|
||||||
|
local api_url="${5:-${FORGE_API}}"
|
||||||
local tmpfile resp http_code resp_body pr_num
|
local tmpfile resp http_code resp_body pr_num
|
||||||
|
|
||||||
tmpfile=$(mktemp /tmp/prl-create-XXXXXX.json)
|
tmpfile=$(mktemp /tmp/prl-create-XXXXXX.json)
|
||||||
|
|
@ -77,7 +79,7 @@ pr_create() {
|
||||||
resp=$(curl -s -w "\n%{http_code}" -X POST \
|
resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${FORGE_API}/pulls" \
|
"${api_url}/pulls" \
|
||||||
--data-binary @"$tmpfile") || true
|
--data-binary @"$tmpfile") || true
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
|
|
||||||
|
|
@ -92,7 +94,7 @@ pr_create() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
409)
|
409)
|
||||||
pr_num=$(pr_find_by_branch "$branch") || true
|
pr_num=$(pr_find_by_branch "$branch" "$api_url") || true
|
||||||
if [ -n "$pr_num" ]; then
|
if [ -n "$pr_num" ]; then
|
||||||
_prl_log "PR already exists: #${pr_num}"
|
_prl_log "PR already exists: #${pr_num}"
|
||||||
printf '%s' "$pr_num"
|
printf '%s' "$pr_num"
|
||||||
|
|
|
||||||
18
lib/vault.sh
18
lib/vault.sh
|
|
@ -34,11 +34,9 @@ _vault_log() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get ops repo API URL (encodes hyphens for Forgejo API)
|
# Get ops repo API URL
|
||||||
_vault_ops_api() {
|
_vault_ops_api() {
|
||||||
local ops_repo_encoded
|
printf '%s' "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}"
|
||||||
ops_repo_encoded=$(printf '%s' "$FORGE_OPS_REPO" | sed 's/-/%2D/g')
|
|
||||||
printf '%s' "${FORGE_URL}/api/v1/repos/${ops_repo_encoded}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
@ -84,12 +82,16 @@ vault_request() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Source it to get validate_vault_action
|
# Save caller's FORGE_TOKEN, source vault-env.sh for validate_vault_action,
|
||||||
# Note: vault-env.sh sources env.sh which sets up log() and other helpers
|
# then restore caller's token so PR creation uses agent's identity (not vault-bot)
|
||||||
|
local _saved_forge_token="${FORGE_TOKEN:-}"
|
||||||
if ! source "$vault_env"; then
|
if ! source "$vault_env"; then
|
||||||
|
FORGE_TOKEN="${_saved_forge_token:-}"
|
||||||
echo "ERROR: failed to source vault-env.sh" >&2
|
echo "ERROR: failed to source vault-env.sh" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
# Restore caller's FORGE_TOKEN after validation
|
||||||
|
FORGE_TOKEN="${_saved_forge_token:-}"
|
||||||
|
|
||||||
# Run validation
|
# Run validation
|
||||||
if ! validate_vault_action "$tmp_toml"; then
|
if ! validate_vault_action "$tmp_toml"; then
|
||||||
|
|
@ -119,7 +121,7 @@ before execution. See the TOML file for details."
|
||||||
local branch="vault/${action_id}"
|
local branch="vault/${action_id}"
|
||||||
local branch_exists
|
local branch_exists
|
||||||
|
|
||||||
branch_exists=$(curl -sf -s -o /dev/null -w "%{http_code}" \
|
branch_exists=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${ops_api}/git/branches/${branch}" 2>/dev/null || echo "0")
|
"${ops_api}/git/branches/${branch}" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
|
@ -180,7 +182,7 @@ before execution. See the TOML file for details."
|
||||||
_vault_log "Creating PR for ${branch}"
|
_vault_log "Creating PR for ${branch}"
|
||||||
|
|
||||||
local pr_num
|
local pr_num
|
||||||
pr_num=$(pr_create "$branch" "$pr_title" "$pr_body") || {
|
pr_num=$(pr_create "$branch" "$pr_title" "$pr_body" "$PRIMARY_BRANCH" "$ops_api") || {
|
||||||
echo "ERROR: failed to create PR" >&2
|
echo "ERROR: failed to create PR" >&2
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue