Files
command-center/agent/managed-skills/gitea-pr-api-cycle/SKILL.md
T

6.3 KiB
Raw Blame History

name, description
name description
gitea-pr-api-cycle "Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence, the form-encoded merge field, the empty-token-returns-404 trap, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha."

Gitea PR/issue cycle — Dankclaw API

Friction-earned 2026-10-18 (PRs #15–#20, issue #18) and re-earned 2026-10-19 on dank/dankvoice PRs #4–#6: the merge payload claim below was wrong, and an empty token masqueraded as a missing repo.

Where

  • API base: http://192.168.0.143:3000/api/v1/repos/dank/<repo>/{pulls,issues} — repo varies (command-center, dankvoice, serverAppConfigs); numbering is per-repo, so a footer citing #36 means nothing without the repo.
  • Token: never inline, never in argv (-H "Authorization: token …" lands in /proc/<pid>/cmdline, world-readable). Extract into a 600-perm curl config and hand it to curl with -K:
    tok=$(sed -nE 's#^https?://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
    [ "${#tok}" -ge 40 ] || { echo "token extraction failed"; exit 1; }
    cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
    curl -sS -K "$cfg" … ; shred -u "$cfg"
    
  • Empty token answers 404 {"message":"not found"}, NOT 401 — Gitea hides the private repo. A 404 on a path you have used before means auth, not a typo'd repo. The trap that produces the empty token: awk -F'/' … '{print $NF}' on the store line (scheme://user:token@host/) — the host is in $3, the match never fires, you get a silent empty variable. Always assert the length before POSTing.
  • Multi-line bodies: build JSON in python (json.dumps({"title":…, "body":open(f).read(),…})) or jq -n --arg b "$(cat b.txt)". NEVER pass --- as a bare jq argument (parsed as a flag). Escaped quotes inside python3 -c '…' break on \" — write the script to a file or use a heredoc.
  • Always capture status: -o r.json -w '%{http_code}'; a bare -o hides failures.

Create

POST /pulls with {title, body, base:"main", head:<branch>} — JSON is accepted here (unlike merge).

  • 409 pull request already exists for these targets [issue_id: N, …] — a PR for that head/base exists already. Common cause: the push message printed /pulls/new/<branch> and someone (Frank, or an xdg-open you ran) clicked Create. GET /pulls?state=all, find it, then PATCH /pulls/N with your title+body instead of re-posting.
  • GET /pulls?state=all on this Gitea omits head/base per entry — read /pulls/N for those, or key off number/user.
  • Issues: same shape, POST /issues {title, body}; PATCH /issues/N fixes a body retroactively (works for PRs too, since a PR is an issue).

Merge (the 422 trap) — corrected

POST /pulls/N/merge needs form-encoded fields; the merge form field name is lowercase do:

curl -sS -X POST -K "$cfg" \
  --data-urlencode 'do=merge' --data-urlencode 'merge_style=no-ff' \
  http://192.168.0.143:3000/api/v1/repos/dank/<repo>/pulls/N/merge

JSON {"Do":"merge"} (what this skill used to claim) and header variants all return 422 {"message":"[Do]: Required"}. merge_style ∈ no-ff|merge|rebase|squash. Success returns 200 with an empty body; verify by landing a checkout rather than re-reading the PR. Merge only with Frank's explicit per-PR OK.

Post-merge landing

  • Command-center: the checkout IS the deployment — git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main.
  • Package repos (~/Projects/<pkg>): same --ff-only, then reinstall (omp plugin upgrade <name>) and cmp the installed dist/index.js against the repo dist/index.js to prove the live artifact is the merged one. Check ls -ld node_modules after landing anything that touched it.
  • omp plugin upgrade takes the npm package name, not the short name. omp plugin upgrade dankvoice fails with Failed to upgrade dankvoice: dankvoice is not installed while the plugin is plainly installed; the package is omp-dankvoice. Find the truth with ls ~/.omp/plugins/node_modules or find ~/.omp -path '*<name>/dist/index.js' -not -path '*/Projects/*' — the installed tree is unscoped (~/.omp/plugins/node_modules/omp-dankvoice/dist/index.js), not @dank/dankvoice.
  • cmp exiting 1 BEFORE the upgrade is the expected state (installed bundle is the pre-merge build, diff reported at some char/line). Do not treat it as a failed deploy and do not "fix" the build; run the upgrade, then cmp again for byte identity, and grep the new bundle for a string only the merged code contains (e.g. grep -c "Heckle instead of summarizing") as a second, semantic confirmation.
  • Prune: git worktree remove --force <path> (force: untracked deps link), git branch -D, git push origin --delete <branch> (allowed: local Gitea).

Every issue/PR body ends with ---, then cwd: <session cwd> · session: <session id>. Read the real values, never invent an id.

  • The session id is the UUID suffix of the LIVE transcript .jsonl, not the session directory name. Transcript dirs are keyed by cwd (e.g. -.omp-wt-wt-20261019-020309-6d1fc1f) and ONE cwd can hold several sessions (resume / continuation forks), so ls -t ~/.omp/agent/sessions/ cannot tell you which is yours and ls -t <dir> is equally blind. Pick the file being appended to right now: ls -l --time-style=+%H:%M:%S <dir>/*.jsonl beside date +%H:%M:%S — the mtime that equals the current second is the live session. The id is the part after _ in <timestamp>_<uuid>.jsonl.
  • A history.db query for session_id filtered by cwd usually agrees, but it is not authoritative for WHICH of several sessions in that cwd is live — use the jsonl mtime test.
  • local/ does NOT disambiguate: a forked transcript dir carries copies of the same local:// artifacts, so matching artifact names proves nothing about which session is live.
  • cwd is that session's own working directory (the path the harness states), not the build worktree you committed in — when they differ, name the build worktree inside the body and keep cwd the session's.
  • Resolve local://<name>.md with read local://<name>.md (its header prints the absolute path) instead of guessing the directory.