Files

14 KiB
Raw Permalink 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 (POST /pulls rejects Basic with 'token is required'), numeric label ids, the form-encoded merge field, the async 405 'try again later' merge trap, the empty-token-returns-404 trap, merged-branch deletion via the branches endpoint, 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), re-earned 2026-10-19 on dank/dankvoice PRs #4–#6 (the merge payload claim was wrong; an empty token masqueraded as a missing repo), and re-earned 2026-10-20 on dank/command-center PRs #55…#57 (the 405 recalculation trap). Re-earned again 2026-10-20 on dank/dankvoice #25 (merge gate loop worked; suite verdict delivered as an issue comment). Re-earned 2026-10-21 on dank/command-center issue #58 / PR #59 (issue labels are numeric ids; the <type>/<issue#>-<slug> branch name has to be right before the first push). Same day on dank/serverAppConfigs #64: the Basic-vs-token split below cost two wasted POSTs. Re-earned 2026-10-21 on dank/franksign #30 (branch-delete endpoint guess cost one 404; docs bullet went stale under the merge).

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.
  • Web UI pages under /dank/<repo>/pulls/N are session-auth private: a plain URL fetch answers 404 even when the API and xdg-open work. Merge state questions go through the API (GET /pulls/N → state, merged, merge_commit_sha) — never conclude from a failed page fetch.
  • Basic vs token split: the username=/password= fields of ~/.config/secrets/gitea as Authorization: Basic … work for reads and issue/PR comment writes, but POST /pulls refuses them with 403 token is required. Pull-request lifecycle writes need the token header. Use the token for everything PR-shaped; never conclude "API is broken" from a Basic 403 on /pulls.
  • 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)
    echo "tok len ${#tok}"   # 40 — assert before use
    cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
    curl -sS -K "$cfg" http://192.168.0.143:3000/api/v1/repos/dank/<repo>/labels
    rm -f "$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. A 201 response piped straight into python3 -c that prints None None means you parsed an error body (missing number) — print message too.

Create

POST /pulls with {title, body, base:"main", head:<branch>} — JSON is accepted here (unlike merge). Plain <branch> works; the owner:branch fork form is unnecessary on same-repo branches.

  • 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) — used to correct a false verification claim in a PR that had already been merged. Body update returns 201, not 200; confirm by reading body back, not by status code.
  • Issue labels are numeric ids, not names. POST /issues with "labels":["ready-for-human"] answers 422 {"message":"[]: json: cannot unmarshal JSON string into Go int64 within \"/labels/0\""}. Resolve first: GET /labels → pick id by name → post "labels":[20]. (PRs take no labels field on create; label afterwards via /issues/N/labels with {"labels":[id]}.)
  • Comments (the channel for late-arriving evidence, e.g. a test-suite verdict posted after PR creation): POST /issues/N/comments with JSON {body} — works for PRs too, returns 201.
  • Open the issue before the first commit, so Refs #N fits the message, and get the branch name right before the first push. git branch -m <new> + git push -u origin <new> + git push origin --delete <old> is a clean rename with no PR yet; do it rather than ship a branch that violates <type>/<issue#>-<slug> (a docs/… branch carrying a fix: commit is the common slip, and git grep on the old name still shows it).
  • A PR whose branch carries already-merged commits plus one new one is fine: open against main, the diff is just the new commit — the cheap way to land a follow-up fix from a worktree whose branch history is the merged stack (no checkout/branch gymnastics needed).

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/command-center/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.

The async 405 when several PRs merge in a row

Merge #55, then #56 and #57 back-to-back, and #56/#57 answered:

405 {"message":"Please try again later","url":"http://dankclaw:3000/api/swagger"}

That is not a failure and not rate limiting: merging one PR moves main, which invalidates every other PR's cached mergeability, and Gitea recalculates it asynchronously. A merge attempt during recalculation is refused. Remedy — bounded poll on the PR, merge when the flag is true:

for attempt in 1 2 3 4 5 6 7 8 9 10; do
  m=$(curl -sS -K "$cfg" "$api/pulls/$n" | python3 -c \
      'import json,sys;d=json.load(sys.stdin);print(d.get("mergeable"),d.get("merged"))')
  case "$m" in *True*) <merge POST>; break ;; *) sleep 4 ;; esac
done

All three landed within ~5 s of retrying. Treat mergeable as a readiness gate, not a report. Match *True* on the pair defensively: False True means already merged — stop, don't merge again.

Verifying a merge without touching the deployment

GET /pulls/N → state: closed, merged: true, merge_commit_sha. Then prove content from the bare repo, not the working tree: git fetch -q origin && git show origin/main:<path> | grep -n <key line> and git log --oneline --graph -4 origin/main (two parents = the --no-ff Frank asks for). A number you deliberately destroyed can legitimately reappear once — quoted in omp-docs/log.md as the wrong value; git grep -c <badvalue> origin/main naming only the log page is the pass condition, not a leftover. Closes #N in the PR body auto-closes the issue; confirm with GET /issues/N → state.

Stacked PRs (no rebase)

When two open branches edit the same section of the same file, merge the sibling branch into the later one (git merge docs/other-branch) instead of rebasing — Frank rejects the history rewrite. Consequences to state in the PR body: merging the lower PR first reduces the upper diff to just its own work; merging the upper first makes the lower an already-contained no-op. Either order is clean; git branch -d on the lower branch refuses until its commits reach main, because -d checks against the current HEAD.

Post-merge landing

  • Delete the merged branch via the branches endpoint: DELETE /repos/dank/<repo>/branches/<urlencoded-branch> (chore%2Fname) → 204. The refs/heads/… path answers 404 — that is the wrong endpoint, not missing auth (auth failures also 404 here, so check which you changed last). If you then run git push origin --delete <branch> it errors failed to push some refs — harmless: the API already deleted it; don't "fix" the error.
  • Command-center: the checkout IS the deployment — git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main. This refuses if any incoming path is dirty in the worktree — see land-merged-pr-with-blocked-tree-ops for the shared-workspace case where checkout and stash are both policy-blocked.
  • serverAppConfigs: deploy.sh pushes the dev checkout's branch first — if the checkout's main is behind a merge you just made (via the API), the deploy dies with error: failed to push some refs. git merge --ff-only origin/main in the deploy checkout BEFORE running deploy.sh after any externally-merged PR.
  • A PR that changes a path you edited live adds that path to the discard list. Tonight: the live dotfile fix was dirty and incoming, so landing needed git -C ~/.omp checkout -- <that file> as well as the usual file. Discard is content-free only when you proved byte identity first (cmp the branch file against the live one before merging) — say so in the PR body, and note that if live-sync.sh commits the dirty file first the merge stays clean because both sides hold identical bytes.
  • 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, branch delete on the remote (branches endpoint above). Before removing a worktree, check no live process has a cwd inside it (ls -l /proc/*/cwd | grep -c "wt/<name>") — same guard live-sync.sh uses — and copy any file you moved into a gitignored .AGENTTRASH/ there to its permanent home first, or the removal destroys the only copy.
  • Status bullets written pre-merge go stale at merge time. A HANDOFF/readme line saying "unmerged (open PR)" is wrong the moment you merge — audit those lines as part of the merge turn and land a one-bullet docs PR (see the Create section's merged-stack branch trick for doing that without a fresh worktree).

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 this 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.