Co-authored-by: Codex Agent <codex@hermes.local> Co-committed-by: Codex Agent <codex@hermes.local>
215 lines
7.3 KiB
Bash
Executable File
215 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# agent-dispatch.sh — Generate a lane-aware prompt for any agent
|
|
#
|
|
# Usage: agent-dispatch.sh <agent_name> <issue_num> <repo>
|
|
# agent-dispatch.sh groq 42 Timmy_Foundation/the-nexus
|
|
#
|
|
# Outputs a prompt to stdout. Copy-paste into the agent's interface.
|
|
# The prompt includes issue context, repo setup, lane coaching, and
|
|
# a short review checklist so dispatch itself teaches the right habits.
|
|
|
|
set -euo pipefail
|
|
|
|
AGENT_NAME="${1:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
|
|
ISSUE_NUM="${2:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
|
|
REPO="${3:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
LANES_FILE="${SCRIPT_DIR%/bin}/playbooks/agent-lanes.json"
|
|
|
|
resolve_gitea_url() {
|
|
if [ -n "${GITEA_URL:-}" ]; then
|
|
printf '%s\n' "${GITEA_URL%/}"
|
|
return 0
|
|
fi
|
|
if [ -f "$HOME/.hermes/gitea_api" ]; then
|
|
python3 - "$HOME/.hermes/gitea_api" <<'PY'
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
|
|
print(raw[:-7] if raw.endswith("/api/v1") else raw)
|
|
PY
|
|
return 0
|
|
fi
|
|
if [ -f "$HOME/.config/gitea/base-url" ]; then
|
|
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
|
|
return 0
|
|
fi
|
|
echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2
|
|
return 1
|
|
}
|
|
|
|
GITEA_URL="$(resolve_gitea_url)"
|
|
|
|
resolve_token_file() {
|
|
local agent="$1"
|
|
local normalized
|
|
normalized="$(printf '%s' "$agent" | tr '[:upper:]' '[:lower:]')"
|
|
for candidate in \
|
|
"$HOME/.hermes/${agent}_token" \
|
|
"$HOME/.hermes/${normalized}_token" \
|
|
"$HOME/.config/gitea/${agent}-token" \
|
|
"$HOME/.config/gitea/${normalized}-token"; do
|
|
if [ -f "$candidate" ]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
for candidate in \
|
|
"$HOME/.config/gitea/timmy-token" \
|
|
"$HOME/.hermes/gitea_token_vps" \
|
|
"$HOME/.hermes/gitea_token_timmy"; do
|
|
if [ -f "$candidate" ]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
TOKEN_FILE="$(resolve_token_file "$AGENT_NAME" || true)"
|
|
if [ -z "${TOKEN_FILE:-}" ]; then
|
|
echo "ERROR: No token found for '$AGENT_NAME'." >&2
|
|
echo "Expected one of ~/.hermes/<agent>_token or ~/.config/gitea/<agent>-token" >&2
|
|
exit 1
|
|
fi
|
|
|
|
GITEA_TOKEN="$(cat "$TOKEN_FILE")"
|
|
REPO_OWNER="${REPO%%/*}"
|
|
REPO_NAME="${REPO##*/}"
|
|
BRANCH="${AGENT_NAME}/issue-${ISSUE_NUM}"
|
|
|
|
python3 - "$LANES_FILE" "$AGENT_NAME" "$ISSUE_NUM" "$REPO" "$REPO_OWNER" "$REPO_NAME" "$BRANCH" "$GITEA_URL" "$GITEA_TOKEN" "$TOKEN_FILE" <<'PY'
|
|
import json
|
|
import sys
|
|
import textwrap
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
lanes_path, agent, issue_num, repo, repo_owner, repo_name, branch, gitea_url, token, token_file = sys.argv[1:]
|
|
|
|
with open(lanes_path) as f:
|
|
lanes = json.load(f)
|
|
|
|
lane = lanes.get(agent, {
|
|
"lane": "bounded work with explicit verification and a clean PR handoff",
|
|
"skills_to_practice": ["verification", "scope control", "clear handoff writing"],
|
|
"missing_skills": ["escalate instead of guessing when the scope becomes unclear"],
|
|
"anti_lane": ["self-directed backlog growth", "unbounded architectural wandering"],
|
|
"review_checklist": [
|
|
"Did I stay within scope?",
|
|
"Did I verify the result?",
|
|
"Did I leave a clean PR and issue handoff?"
|
|
],
|
|
})
|
|
|
|
headers = {"Authorization": f"token {token}"}
|
|
|
|
def fetch_json(path):
|
|
req = urllib.request.Request(f"{gitea_url}/api/v1{path}", headers=headers)
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
return json.loads(resp.read().decode())
|
|
|
|
try:
|
|
issue = fetch_json(f"/repos/{repo}/issues/{issue_num}")
|
|
comments = fetch_json(f"/repos/{repo}/issues/{issue_num}/comments")
|
|
except urllib.error.HTTPError as exc:
|
|
raise SystemExit(f"Failed to fetch issue context: {exc}") from exc
|
|
|
|
body = (issue.get("body") or "").strip()
|
|
body = body[:4000] + ("\n...[truncated]" if len(body) > 4000 else "")
|
|
recent_comments = comments[-3:]
|
|
comment_block = []
|
|
for c in recent_comments:
|
|
author = c.get("user", {}).get("login", "unknown")
|
|
text = (c.get("body") or "").strip().replace("\r", "")
|
|
text = text[:600] + ("\n...[truncated]" if len(text) > 600 else "")
|
|
comment_block.append(f"- {author}: {text}")
|
|
|
|
comment_text = "\n".join(comment_block) if comment_block else "- (no comments yet)"
|
|
|
|
skills = "\n".join(f"- {item}" for item in lane["skills_to_practice"])
|
|
gaps = "\n".join(f"- {item}" for item in lane["missing_skills"])
|
|
anti_lane = "\n".join(f"- {item}" for item in lane["anti_lane"])
|
|
review = "\n".join(f"- {item}" for item in lane["review_checklist"])
|
|
|
|
prompt = f"""You are {agent}, working on {repo_name} for Timmy Foundation.
|
|
|
|
YOUR ISSUE: #{issue_num} — "{issue.get('title', f'Issue #{issue_num}')}"
|
|
|
|
REPO: {repo}
|
|
GITEA API: {gitea_url}/api/v1
|
|
GITEA TOKEN FILE: {token_file}
|
|
WORK BRANCH: {branch}
|
|
|
|
LANE:
|
|
{lane['lane']}
|
|
|
|
SKILLS TO PRACTICE ON THIS ASSIGNMENT:
|
|
{skills}
|
|
|
|
COMMON FAILURE MODE TO AVOID:
|
|
{gaps}
|
|
|
|
ANTI-LANE:
|
|
{anti_lane}
|
|
|
|
ISSUE BODY:
|
|
{body or "(empty issue body)"}
|
|
|
|
RECENT COMMENTS:
|
|
{comment_text}
|
|
|
|
WORKFLOW:
|
|
1. Read the issue body and recent comments carefully before touching code.
|
|
2. Clone the repo into /tmp/{agent}-work-{issue_num}.
|
|
3. Check whether {branch} already exists on origin; reuse it if it does.
|
|
4. Read the repo docs and follow its own tooling and conventions.
|
|
5. Do only the scoped work from the issue. If the task grows, stop and comment instead of freelancing expansion.
|
|
6. Run the repo's real verification commands.
|
|
7. Open a PR and summarize:
|
|
- what changed
|
|
- how you verified it
|
|
- any remaining risk or follow-up
|
|
8. Comment on the issue with the PR link and the same concise summary.
|
|
|
|
GIT / API SETUP:
|
|
export GITEA_URL="{gitea_url}"
|
|
export GITEA_TOKEN_FILE="{token_file}"
|
|
export GITEA_TOKEN="$(tr -d '[:space:]' < "$GITEA_TOKEN_FILE")"
|
|
git config --global http."$GITEA_URL/".extraHeader "Authorization: token $GITEA_TOKEN"
|
|
git clone "$GITEA_URL/{repo}.git" /tmp/{agent}-work-{issue_num}
|
|
cd /tmp/{agent}-work-{issue_num}
|
|
git ls-remote --exit-code origin {branch} >/dev/null 2>&1 && git fetch origin {branch} && git checkout {branch} || git checkout -b {branch}
|
|
|
|
ISSUE FETCH COMMANDS:
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}"
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}/comments"
|
|
|
|
PR CREATION TEMPLATE:
|
|
curl -s -X POST "{gitea_url}/api/v1/repos/{repo}/pulls" \\
|
|
-H "Authorization: token $GITEA_TOKEN" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{{"title":"[{agent}] <description> (#{issue_num})","body":"Fixes #{issue_num}\\n\\n## Summary\\n- <change>\\n\\n## Verification\\n- <command/output>\\n\\n## Risks\\n- <if any>","head":"{branch}","base":"main"}}'
|
|
|
|
ISSUE COMMENT TEMPLATE:
|
|
curl -s -X POST "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}/comments" \\
|
|
-H "Authorization: token $GITEA_TOKEN" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{{"body":"PR submitted.\\n\\nSummary:\\n- <change>\\n\\nVerification:\\n- <command/output>\\n\\nRisks:\\n- <if any>"}}'
|
|
|
|
REVIEW CHECKLIST BEFORE YOU PUSH:
|
|
{review}
|
|
|
|
RULES:
|
|
- Do not skip hooks with --no-verify.
|
|
- Do not silently widen the scope.
|
|
- If verification fails twice or the issue is underspecified, stop and comment with what blocked you.
|
|
- Always create a PR instead of pushing to main.
|
|
- Clean up /tmp/{agent}-work-{issue_num} when done.
|
|
"""
|
|
|
|
print(textwrap.dedent(prompt).strip())
|
|
PY
|