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>
This commit is contained in:
2026-10-10 17:21:03 +11:00
parent cd56e69a2f
commit 441d859afe
3 changed files with 196 additions and 6 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.env .env
*.visual-check.* *.visual-check.*
.state/

View File

@@ -19,7 +19,11 @@ result that returns.
`Hub`. `Hub`.
- Note: `POST /api/agents/{id}/keys` is **board-only**. A run JWT gets - Note: `POST /api/agents/{id}/keys` is **board-only**. A run JWT gets
`403 Board access required`. Mint keys with the local board key. `403 Board access required`. Mint keys with the local board key.
2. File one job with `curl`. Use the issue-create endpoint. 2. File one job with the round-trip script. The script uses the issue-create
endpoint: `POST /api/companies/{companyId}/issues`.
- The old `POST /api/issues` route does not exist. It returns `404`.
- Company id: `d4921013-e5da-4d88-8016-57d5746e2d03`.
- Script: `scripts/paperclip-roundtrip.sh`.
3. Confirm the job appears in the Paperclip board. 3. Confirm the job appears in the Paperclip board.
4. Wait for the agent to close the job. 4. Wait for the agent to close the job.
5. Send the result to Jervis `POST /announce` so the house speaks it. 5. Send the result to Jervis `POST /announce` so the house speaks it.
@@ -28,11 +32,14 @@ result that returns.
### Commands ### Commands
```bash ```bash
export PAPERCLIP_API_KEY=<from secrets> # File one job with the script. It reads PAPERCLIP_API_KEY from the
curl -s -X POST http://192.168.20.13:3100/api/issues \ # environment, then from ~/.config/environment.d/10-secrets.conf.
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \ scripts/paperclip-roundtrip.sh \
-H "Content-Type: application/json" \ --title "Hub round-trip job: confirm the return path" \
-d '{"title":"round-trip test","description":"prove the return path"}' --description "Prove the outbound Paperclip path." \
--assignee 403de1f7-645c-4d35-bc7e-19b01d77bacc \
--priority high \
--project 7ae791bb-8d9a-462f-99bf-013421d20623
# Speak the result. # Speak the result.
curl -s -X POST http://192.168.20.13:8501/announce \ curl -s -X POST http://192.168.20.13:8501/announce \

182
scripts/paperclip-roundtrip.sh Executable file
View File

@@ -0,0 +1,182 @@
#!/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