Files
central_ai_llm_master/scripts/paperclip-roundtrip.sh
Sam Rolfe 441d859afe Phase 1: file one Paperclip job from a script
Add scripts/paperclip-roundtrip.sh. It files one Paperclip issue and prints
the returned id and identifier. The key comes from the environment, then from
environment.d. The script never prints the key.

Fix the runbook: the create route is POST /api/companies/{companyId}/issues.
The old POST /api/issues route returns 404.

Record the first filed job: AUD-55 (d58891ce-9934-4710-9bba-d37974251ed9).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-10 17:21:03 +11:00

183 lines
6.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# paperclip-roundtrip.sh - file one Paperclip job (issue) from the command line.
#
# This is the outbound half of the Central AI Hub Phase 1 round trip.
# It proves that a script can file a job to Paperclip and get an identifier back.
# See hub_brief.md (build order step 1) and docs/outline/04-agent-runbook.md.
#
# The script never prints or commits the API key. It reads the key from the
# environment, then from the environment.d secrets file.
#
# Usage:
# scripts/paperclip-roundtrip.sh [options]
#
# Options:
# -t, --title TITLE Job title.
# -d, --description TEXT Job description text.
# -f, --description-file F Read the description from file F ("-" is stdin).
# -a, --assignee AGENT_ID Assign the job to this agent id.
# -p, --priority LEVEL backlog|low|medium|high|critical (default: medium).
# -P, --project PROJECT_ID Put the job in this project.
# --parent ISSUE_ID Make the job a child of this issue id.
# --idempotency-key KEY Idempotency key. A retry with the same key is safe.
# --json Print the raw API response after the summary.
# -h, --help Show this help.
#
# Environment:
# PAPERCLIP_API_KEY Required. Read from the environment first, then from
# ${PAPERCLIP_SECRETS_FILE:-~/.config/environment.d/10-secrets.conf}.
# PAPERCLIP_BASE_URL Paperclip base URL. Default: http://192.168.20.13:3100
# PAPERCLIP_COMPANY_ID Company id. Default: the Hub company.
# PAPERCLIP_STATE_DIR Where the filed-issue record is written.
# Default: <repo>/.state
#
# Exit codes:
# 0 job filed
# 1 usage or configuration error
# 2 Paperclip API error
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
readonly DEFAULT_BASE_URL="http://192.168.20.13:3100"
readonly DEFAULT_COMPANY_ID="d4921013-e5da-4d88-8016-57d5746e2d03"
readonly DEFAULT_TITLE="Hub round-trip job: confirm the return path"
readonly DEFAULT_DESCRIPTION="Prove the outbound Paperclip path: a script files a job and reads back the identifier."
BASE_URL="${PAPERCLIP_BASE_URL:-${PAPERCLIP_BOARD_URL:-$DEFAULT_BASE_URL}}"
COMPANY_ID="${PAPERCLIP_COMPANY_ID:-$DEFAULT_COMPANY_ID}"
SECRETS_FILE="${PAPERCLIP_SECRETS_FILE:-$HOME/.config/environment.d/10-secrets.conf}"
STATE_DIR="${PAPERCLIP_STATE_DIR:-$PROJECT_DIR/.state}"
TITLE="$DEFAULT_TITLE"
DESCRIPTION="$DEFAULT_DESCRIPTION"
DESCRIPTION_FILE=""
ASSIGNEE=""
PRIORITY="medium"
PROJECT_ID=""
PARENT=""
IDEMPOTENCY_KEY=""
PRINT_JSON=0
usage() {
awk 'NR>2 && /^#/ { sub(/^# ?/, ""); print } NR>2 && !/^#/ { exit }' "${BASH_SOURCE[0]}"
}
die() {
echo "error: $*" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title) TITLE="${2:?missing value for $1}"; shift 2 ;;
-d|--description) DESCRIPTION="${2:?missing value for $1}"; shift 2 ;;
-f|--description-file) DESCRIPTION_FILE="${2:?missing value for $1}"; shift 2 ;;
-a|--assignee) ASSIGNEE="${2:?missing value for $1}"; shift 2 ;;
-p|--priority) PRIORITY="${2:?missing value for $1}"; shift 2 ;;
-P|--project) PROJECT_ID="${2:?missing value for $1}"; shift 2 ;;
--parent) PARENT="${2:?missing value for $1}"; shift 2 ;;
--idempotency-key) IDEMPOTENCY_KEY="${2:?missing value for $1}"; shift 2 ;;
--json) PRINT_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1 (use --help)" ;;
esac
done
case "$PRIORITY" in
backlog|low|medium|high|critical) ;;
*) die "invalid priority: $PRIORITY" ;;
esac
# --- Read the description from a file when asked. -----------------------------
if [[ -n "$DESCRIPTION_FILE" ]]; then
if [[ "$DESCRIPTION_FILE" == "-" ]]; then
DESCRIPTION="$(cat)"
else
[[ -r "$DESCRIPTION_FILE" ]] || die "cannot read description file: $DESCRIPTION_FILE"
DESCRIPTION="$(cat -- "$DESCRIPTION_FILE")"
fi
fi
# --- Resolve the API key. Never print it. -------------------------------------
if [[ -z "${PAPERCLIP_API_KEY:-}" ]]; then
if [[ -r "$SECRETS_FILE" ]]; then
PAPERCLIP_API_KEY="$(awk -F= '/^PAPERCLIP_API_KEY=/{sub(/^PAPERCLIP_API_KEY=/,""); print; exit}' "$SECRETS_FILE")"
fi
fi
[[ -n "${PAPERCLIP_API_KEY:-}" ]] || die "PAPERCLIP_API_KEY is not set and not in $SECRETS_FILE"
command -v curl >/dev/null 2>&1 || die "curl is required"
command -v jq >/dev/null 2>&1 || die "jq is required"
BASE_URL="${BASE_URL%/}"
# --- Check the board is reachable. --------------------------------------------
if ! curl -sS -m 10 -o /dev/null "$BASE_URL/health"; then
echo "error: Paperclip is not reachable at $BASE_URL/health" >&2
exit 2
fi
# --- Build the request body. --------------------------------------------------
BODY="$(jq -n \
--arg title "$TITLE" \
--arg description "$DESCRIPTION" \
--arg priority "$PRIORITY" \
--arg projectId "$PROJECT_ID" \
--arg assigneeAgentId "$ASSIGNEE" \
--arg parentId "$PARENT" \
--arg idempotencyKey "$IDEMPOTENCY_KEY" \
'{title: $title, description: $description, priority: $priority}
+ (if $projectId != "" then {projectId: $projectId} else {} end)
+ (if $assigneeAgentId != "" then {assigneeAgentId: $assigneeAgentId} else {} end)
+ (if $parentId != "" then {parentId: $parentId} else {} end)
+ (if $idempotencyKey != "" then {idempotencyKey: $idempotencyKey} else {} end)')"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
HEADERS=(-H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json")
if [[ -n "${PAPERCLIP_RUN_ID:-}" ]]; then
HEADERS+=(-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID")
fi
HTTP_CODE="$(curl -sS -o "$TMP" -w '%{http_code}' -X POST \
"${HEADERS[@]}" \
--data "$BODY" \
"$BASE_URL/api/companies/$COMPANY_ID/issues")" || die "curl failed to reach $BASE_URL"
if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then
echo "error: Paperclip returned HTTP $HTTP_CODE" >&2
cat "$TMP" >&2
echo >&2
exit 2
fi
ISSUE_ID="$(jq -r '(.issue // .).id // empty' "$TMP")"
IDENTIFIER="$(jq -r '(.issue // .).identifier // empty' "$TMP")"
[[ -n "$ISSUE_ID" ]] || die "Paperclip response has no issue id"
# --- Record the result. -------------------------------------------------------
mkdir -p "$STATE_DIR"
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$ISSUE_ID" "$IDENTIFIER" \
>> "$STATE_DIR/paperclip-roundtrip-issues.log"
PREFIX="${IDENTIFIER%%-*}"
echo "Filed Paperclip job"
echo " title: $TITLE"
echo " priority: $PRIORITY"
echo " issue id: $ISSUE_ID"
echo " identifier: $IDENTIFIER"
if [[ "$PREFIX" != "$IDENTIFIER" && -n "$PREFIX" ]]; then
echo " board url: $BASE_URL/$PREFIX/issues/$IDENTIFIER"
fi
echo " record: $STATE_DIR/paperclip-roundtrip-issues.log"
if [[ "$PRINT_JSON" == "1" ]]; then
echo
jq . "$TMP"
fi