14 KiB
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#36means nothing without the repo. - Web UI pages under
/dank/<repo>/pulls/Nare session-auth private: a plain URL fetch answers 404 even when the API andxdg-openwork. 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/giteaasAuthorization: Basic …work for reads and issue/PR comment writes, butPOST /pullsrefuses them with403 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(),…})) orjq -n --arg b "$(cat b.txt)". NEVER pass---as a bare jq argument (parsed as a flag). Escaped quotes insidepython3 -c '…'break on\"— write the script to a file or use a heredoc. - Always capture status:
-o r.json -w '%{http_code}'; a bare-ohides failures. A201response piped straight intopython3 -cthat printsNone Nonemeans you parsed an error body (missingnumber) — printmessagetoo.
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 anxdg-openyou ran) clicked Create.GET /pulls?state=all, find it, thenPATCH /pulls/Nwith your title+body instead of re-posting. GET /pulls?state=allon this Gitea omitshead/baseper entry — read/pulls/Nfor those, or key offnumber/user.- Issues: same shape,
POST /issues{title, body};PATCH /issues/Nfixes 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 readingbodyback, not by status code. - Issue labels are numeric ids, not names.
POST /issueswith"labels":["ready-for-human"]answers422 {"message":"[]: json: cannot unmarshal JSON string into Go int64 within \"/labels/0\""}. Resolve first:GET /labels→ pickidbyname→ post"labels":[20]. (PRs take nolabelsfield on create; label afterwards via/issues/N/labelswith{"labels":[id]}.) - Comments (the channel for late-arriving evidence, e.g. a test-suite verdict posted after PR creation):
POST /issues/N/commentswith JSON{body}— works for PRs too, returns 201. - Open the issue before the first commit, so
Refs #Nfits 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>(adocs/…branch carrying afix:commit is the common slip, andgit grepon 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. Therefs/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 rungit push origin --delete <branch>it errorsfailed 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 — seeland-merged-pr-with-blocked-tree-opsfor the shared-workspace case wherecheckoutandstashare both policy-blocked. - serverAppConfigs:
deploy.shpushes the dev checkout's branch first — if the checkout'smainis behind a merge you just made (via the API), the deploy dies witherror: failed to push some refs.git merge --ff-only origin/mainin the deploy checkout BEFORE runningdeploy.shafter 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 (cmpthe branch file against the live one before merging) — say so in the PR body, and note that iflive-sync.shcommits 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>) andcmpthe installeddist/index.jsagainst the repodist/index.jsto prove the live artifact is the merged one. Checkls -ld node_modulesafter landing anything that touched it. omp plugin upgradetakes the npm package name, not the short name.omp plugin upgrade dankvoicefails withFailed to upgrade dankvoice: dankvoice is not installedwhile the plugin is plainly installed; the package isomp-dankvoice. Find the truth withls ~/.omp/plugins/node_modulesorfind ~/.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.cmpexiting 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, thencmpagain 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 guardlive-sync.shuses — 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).
Attribution footer (AGENTS.md)
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), sols -t ~/.omp/agent/sessions/cannot tell you which is yours andls -t <dir>is equally blind. Pick the file being appended to right now:ls -l --time-style=+%H:%M:%S <dir>/*.jsonlbesidedate +%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.dbquery forsession_idfiltered bycwdusually 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 samelocal://artifacts, so matching artifact names proves nothing about which session is live.cwdis 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 keepcwdthe session's.- Resolve
local://<name>.mdwithread local://<name>.md(its header prints the absolute path) instead of guessing the directory.