#!/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: /.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