Linking work to code
- Start work by naming the branch after the item
- Close a work item from a commit message
- Run a quick action from a commit message
- Review the commits behind a work item
- Review the merge requests behind a work item
- Track an epic's work across repositories
- See what a commit named
- See what a merge request will close
- Pin a work item to a region of code
- See what a work item is anchored to
- Show open work beside the code
- Check whether a merge request touches open work
- Find the files that carry the most open work
- Take an anchor off automatic tracking
- Re-resolve an anchor
- Unpin an anchor
- Browse the TODO markers in a repository
- Promote a TODO into a work item
- Stamp the new ids back into your code
- Require every commit to name a work item
Overview
Write Fixes #412 in a commit message and Gitmoap records the link, then closes the work item when that commit reaches the branch your product ships from. Pin a work item to a region of code and Gitmoap follows that region through every rename, reformat and refactor, telling you when the code the ticket is about has been rewritten or deleted. Scan a // TODO: out of the tree and it becomes a row you can promote to a real work item in one click — and deleting the comment closes it again.
Together these three mechanisms answer the questions a tracker normally cannot: what shipped for this ticket, where is the code this ticket is about, and what has this codebase already admitted to itself.
Quick start
Link a commit to an issue
Name the issue in the commit message, with a closing keyword when the commit finishes it:
fix(payments): stop the retry loop double-charging Fixes #412
Push, and merge as usual. The commit appears on the issue within seconds, and the issue closes when the commit reaches the default branch.
Track work where it lives in the code
Leave a marker in a comment that names the issue it belongs to:
// TODO(#412): rate-limit the webhook fan-out
Push it to the default branch. The issue shows the line, and the file is ranked by the open work anchored in it.
Before you start
- A repository you can read.Every listing on this page follows the repository's visibility: a public repository answers anyone, an internal one answers anyone signed in, and a private one answers only its members. A private repository answers
404rather than403to a non-member, so its existence stays private. - Developer access to work with anchors.Pinning, updating, unpinning and re-resolving an anchor all need Developer on the repository. Reading anchors needs only read access.
- Reporter access to promote a TODO.Promoting files a work item, and filing is a write — the visibility shortcut does not apply, so being able to read a public repository is not enough. You need to be a member at Reporter or above.
- Reporter to link and Developer to close, if you drive the tracker from a commit message.Recording the link needs only the level that reads the item, which is Reporter; a closing keyword and every quick action need Developer, the same level a person needs to close or edit the item by hand. Both principals are checked — the commit's author and whoever pushed — and the lower of the two decides.
- A token with the right scope, if you are calling the API or the CLI rather than clicking:
issue:readfor every listing on this page,issue:updatefor anchors,issue:createto promote a TODO. Scopes are chosen when you mint a token and cannot be added afterwards. - A commit reachable from the default branch, to pin an anchor to. Anything already merged qualifies. An anchor pinned to a commit that lives only on a feature branch is refused, because there is no shared history to follow it forward from.
- A group you can read, for the two epic listings. An epic lives in a group, so those two reads follow the group's visibility rather than any repository's.
- The CLI and a Gitmoap remote, for
gitmoap todo sync. It readsorigin, so run it inside a clone of the repository.
How it works
There are three mechanisms here and they are independent. You can use any one of them without the others.
References in commit messages and merge requests
A reference is a sigil plus a target, written in ordinary prose:
| You write | It names | Fully-qualified form |
|---|---|---|
#412 | work item 412 in this repository | acme/api#412 |
&17 | epic 17 in this repository's immediate parent group | acme&17, acme/team&17 |
!87 | merge request 87 in this repository | acme/api!87 |
%v2.1 | a milestone, by title or id | acme/api%v2.1 |
~bug, ~"needs design" | a label | acme/api~bug |
@sam | a person or a service account | — |
A bare & resolves in the repository's immediate parent group, not the root of your namespace, because that is the group most teams mean when they say "our group". An epic further up the tree is addressed by path.
A reference on its own links. A reference behind a closing keyword links and expresses an intent to close:
| Keyword | Forms (case-insensitive) | Effect |
|---|---|---|
| Close | close, closes, closed | link and close |
| Fix | fix, fixes, fixed | link and close |
| Resolve | resolve, resolves, resolved | link and close |
| Relate | related to, refs | link only, explicitly |
| (none) | a bare #412 | link only |
One keyword governs a whole run of references separated by commas, and, or whitespace, so Fixes #412, #413 and #414 expresses three closing intents.
Where a closing keyword takes effect is the part people get wrong. A commit closes when it lands on a closing branch — the repository's default branch, plus any branch pattern the scope adds in its work-item settings. A merge request closes when it is merged into a closing branch. Pushing Fixes #412 to a feature branch links the commit and closes nothing; that is the point, because the branch is not a claim about the state of the product. A stacked merge request that targets another merge request's source branch also closes nothing, for exactly the same reason.
Because # is the most-used character in software, the parser deliberately ignores it almost everywhere:
| Context | Example | A reference? |
|---|---|---|
| Fenced code block | a line between triple-backtick fences | no — the whole fence is skipped |
| Indented code block | ····fixes #412 | no |
| Inline code span | `#412` | no |
| Inside a URL | https://x.dev/a#412 | no |
| A heading line | ### #412 fix | no — the whole line is skipped |
| Escaped | \#412 | no |
| Glued to a word | abc#412, v1#412 | no — the sigil needs a token boundary before it |
| Space after the sigil | Fixes # 412 | no |
| Written in a sentence | Fixes #412. | yes |
Linking runs after the push is accepted, not during it. A push is never delayed, never rejected and never rolled back because of anything on this page — with one exception, the push rule described under Repository rules, which can require every commit to name a work item. Everything else lands a few seconds behind the push and converges: a redelivery changes nothing, and a link that fails is re-derived from the commit next time.
Quick actions
A slash command in a commit message mutates the work item the nearest preceding reference names:
| Action | Example | Effect |
|---|---|---|
/assign | /assign @sam @ana | add assignees |
/unassign | /unassign @sam | remove one, or all if bare |
/label | /label ~bug ~p1 | add labels |
/milestone | /milestone %v2.1 | set the milestone |
/points | /points 3 | set story points |
/status | /status "In Review" | set a named status |
/close | /close | close |
/reopen | /reopen | reopen to the category it was in before |
Several fit on one line, which is what a real commit message looks like:
fix(payments): stop the retry loop double-charging Fixes #412 /assign @sam /points 3A commit message is never an authorization bypass. Setting user.email in your git config costs nothing, so the author line is not an identity Gitmoap has authenticated. Every action is therefore checked twice — against the commit author and against the person who pushed — and runs only if both hold the access level it needs. An action that is dropped is written to the work item's timeline as a denial, so the author can see it was refused rather than lost.
Code anchors
An anchor binds a work item to a region of code and keeps evaluating it. It records the path, the line range and the commit those coordinates are valid at — which is exactly the address a permalink carries — and then, on every push to the default branch, Gitmoap replays the diff and carries the anchor forward through the hunks.
That gives you a second, live set of coordinates beside the frozen ones, and a state:
| State | What happened | What happens next |
|---|---|---|
live | the region moved, or was edited and is still recognisably the same code | kept up to date silently |
drifted | the region was substantially rewritten | the work item is flagged, and the anchor keeps following |
orphaned | the region is gone — deleted, or rewritten past recognition | the work item is flagged; a TODO marker anchor closes it instead |
needs_review | a push was too large to compute line-by-line | heals itself on the next push to that file, or immediately when you re-resolve it |
manual | you took the wheel | never touched again |
Recognition is a similarity score over the region's lines, with whitespace and comment markers normalised away first. At or above 0.75 the anchor stays live; between 0.2 and 0.75 it is drifted; below 0.2 it is orphaned. Re-indenting and re-spacing change nothing the score can see, so running a formatter over an anchored file does not drift its anchors. Adding a line inside an anchored block grows the region rather than breaking it, and a git mv relocates the anchor rather than orphaning it.
Two things follow from anchors being rows rather than strings in a paragraph. The code browser can draw open work in the gutter beside the lines it is about, and a review can be told that a change overlaps an open bug's anchored region — not merely that it touches the same file.
Anchors track the default branch only. A region that exists on a feature branch and nowhere else has no shared history to follow.
Code TODOs
Gitmoap reads the + and - lines of each push to the default branch and recognises six marker keywords — TODO, FIXME, HACK, XXX, BUG, OPTIMIZE — in a comment:
MARKER [ '(' @owner | #412 ')' ] ':' TEXT// TODO: rate-limit the webhook fan-out# FIXME(@sam): this silently drops the last page/* HACK(#388): remove once the cursor bug is fixed */-- OPTIMIZE: N+1 on the members queryUppercase only, and only in a comment: everything on the line before the marker has to be whitespace or comment punctuation, which is what keeps let msg = "TODO: fix this"; out of your backlog without a parser per language.
What happens to an unlinked marker is a setting on the scope, inherited from the group:
| Policy | Behaviour |
|---|---|
ignore | no scan. Markers that already name a work item still track, because those are anchors |
index (default) | the marker becomes a code TODO row with a one-click Promote. It is not a work item: no number, no status, no assignee, no place on a board |
promote | the marker mints a work item automatically and becomes a marker anchor on it |
index is the default deliberately. A repository with four hundred markers that auto-minted four hundred work items has a destroyed backlog, and the only way out is bulk deletion.
A TODO's identity is its content digest, not its path and not its line. The digest is taken over the normalised marker text with the (@owner) or (#412) group and the comment syntax stripped, so it survives a reformat, a rename and a move. A marker is the same TODO as an existing one if either the digest matches or its position carries onto an existing anchor. Reword it in place and the anchor recognises it; move it and the digest recognises it. Reword it and move it in one push and you get a new TODO — correctly, because it is a different statement in a different place.
Vendored and generated files are never scanned: lock files, minified bundles, source maps, anything the repository marks generated in .gitattributes, and by default vendor/**, third_party/** and node_modules/**.
How to
Start work by naming the branch after the item
A branch whose name begins with the number and a hyphen — 412-retry-loop-double-charges — or which carries #412 anywhere in it — fix/#412 — names work item 412 in the repository the branch is created in. The leading form has to lead: fix-412-retry names nothing, because the digits are not the first thing in the name. Creating it records the branch on the item's timeline and, unless the scope has turned that transition off, moves the item into the in progress category. It is the cheapest way to start a ticket, because you were going to create the branch anyway.
Two things about it surprise people. It fires on create only — pushing more commits to the branch does not drag an item a reviewer has already moved to in review back to in progress. And no link row is written: which branches belong to #412 is answered from the branch names themselves, so renaming the branch changes the answer rather than leaving a stale one behind.
Close a work item from a commit message
Write the closing keyword and the reference in the commit message body, then push the branch and merge it as usual:
fix(payments): stop the retry loop double-charging Fixes #412The link appears on the work item within a few seconds of the push. The item closes when that commit reaches a closing branch — through a merge, a squash, a rebase or a direct push, all of which Gitmoap treats identically. Merging a stacked merge request into another merge request's branch links but does not close, because nothing shipped.
To close from the merge request itself rather than from a commit, put the keyword in its title or description. Gitmoap re-reads the body on every edit, so editing Closes #412 down to a bare #412 before the merge means the merge links but does not close, and deleting the reference removes the link. Neither ever reopens something that is already closed — closing intent is read once, at merge.
Run a quick action from a commit message
Put the action after the reference it applies to. An action binds to the nearest reference before it, or to the first reference in the message if none precedes it:
feat(cart): idempotency keys on checkout Refs #388 /assign @sam /label ~payments /status "In Review"Unknown action names are ignored rather than rejected — a commit message is not a command line, and refusing a push after the fact would help nobody. If an action does not take effect, open the work item's timeline: a refusal is recorded there with the action, the commit and the reason.
Review the commits behind a work item
Every commit whose message named the item, each with the repository it lives in — which is not necessarily the item's own repository, since a reference can be written from anywhere you can see. The page is ordered by repository and then by commit id, not by date, so do not read the first row as the most recent one. A commit whose object git can no longer produce still comes back with its id, because the link records what a message said.
Open the work item and choose the Code tab. Commits lists every commit that named this item, with its subject, its author and the repository it is in. Choosing one opens the commit's diff.
gitmoap issue-code-links list-issue-commits --repo-id <id> --issue-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/commits \ -H "Authorization: Bearer $GITMOAP_TOKEN"Review the merge requests behind a work item
Each linked merge request comes back with a flag saying whether it claims to finish the item or merely mentions it. That flag is the whole difference between a link and a promise, and it is what merge time reads. This page is ordered by repository and then by merge-request number, not by date.
On the work item's Code tab, Merge requests lists everything whose title or description names this item. A Closes badge marks the ones that will close it on merge; the rest are mentions. The status pill shows open, merged or closed.
gitmoap issue-code-links list-issue-merge-requests \ --repo-id <id> \ --issue-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/merge-requests \ -H "Authorization: Bearer $GITMOAP_TOKEN"Track an epic's work across repositories
An epic lives in a group and has no repository of its own, so it is addressed under the group. The listings are otherwise identical, and they span every repository you can see.
Open the epic and choose Code. Commits lists everything that named &17 anywhere in the group's repositories, grouped by repository.
gitmoap issue-code-links list-group-issue-commits \ --group-id <id> \ --issue-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/issues/{issue_id}/commits \ -H "Authorization: Bearer $GITMOAP_TOKEN"On the same tab, Merge requests lists every merge request across the group whose body names this epic, with the same Closes badge as a repository work item.
gitmoap issue-code-links list-group-issue-merge-requests \ --group-id <id> \ --issue-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/issues/{issue_id}/merge-requests \ -H "Authorization: Bearer $GITMOAP_TOKEN"Commits and merge requests you cannot see are dropped from both listings rather than shown as placeholders, so a private repository inside a public group never becomes visible through its cross-references.
See what a commit named
The reverse direction: hand it a full 40-character commit SHA and get back the work items that commit's message named — the ones it closed and the ones it merely mentioned alike. An abbreviated SHA is refused; copy the whole thing. A well-formed SHA that names no commit is not an error: the answer is an empty list, because the question is asked of the links rather than of git.
Open the commit from the repository's history. Under the message, Work items lists everything the message named, each with its type, title and status.
gitmoap issue-code-links list-commit-issues --repo-id <id> --commit-sha <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/commits/{commit_sha}/issues \ -H "Authorization: Bearer $GITMOAP_TOKEN"See what a merge request will close
Open the merge request. The Linked work panel in the sidebar lists every item its title or description names, each with its type, number, title and status. It updates as you edit the description. Which of them the merge will close is the closing flag on the item's own Code tab, so check there when the distinction matters.
gitmoap issue-code-links list-mr-issues --repo-id <id> --mr-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/merge-requests/{mr_id}/issues \ -H "Authorization: Bearer $GITMOAP_TOKEN"Pin a work item to a region of code
Give a path, an inclusive line range and the commit those coordinates are valid at. Gitmoap reads the region at that commit — an anchor is never born broken — and immediately resolves it forward to the current tip of the default branch.
Open a file in the code browser, select the lines, and choose Pin to work item from the selection menu. Pick an existing item or file a new one; the pinned commit defaults to the one you are viewing. The confirmation shows the region as it will be anchored, and names the file's code owners.
gitmoap issue-anchors create \ --repo-id <id> \ --issue-id <id> \ --end-line <n> \ --path <path> \ --pinned-at-sha <pinned at sha> \ --start-line <n>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/anchors \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "end_line": "<int32>", "path": "<string>", "pinned_at_sha": "<string>", "start_line": "<int32>" }'The response names the file's CODEOWNERS owners as suggested assignees — which is the reason to pin an anchor rather than paste a permalink. Turning on the scope's assign from code owners setting makes the suggestion an assignment, and only when the item has no assignee at all: an item somebody has already picked up is triaged, and adding a second name is somebody else's queue getting work they did not ask for. A repository with no CODEOWNERS file is not an error; the suggestion is simply empty. A code owner who is not a member of the scope stays a suggestion and is never assigned, because assigning work to somebody who cannot open the item reads as a person ignoring their queue.
Four rules decide whether a pin is accepted:
- The work item has to be repository-scoped. An anchor points into a repository, and a group-scoped epic has none.
- The region is at most 500 lines, starting at line 1 or later, ending at or after it starts.
- The path, the range and the commit have to resolve, and the file has to be text. A range past the end of the file, a path that does not exist at that commit, or a binary file is refused.
- The commit has to be reachable from the default branch. A feature-branch-only commit is refused, because a squash-merge would leave the anchor pinned to a commit that never enters the default branch's history at all.
An anchor's kind says what it is pinned to. A region — the default, and what you get from the code browser — is a line range. A symbol is the same line range with the name of the thing carried alongside it. A file is the whole file, followed only through renames and deletions, with no line mathematics and no drift. A marker is a TODO comment carrying a work item's number; the TODO scan is what mints one, and it is the only kind that may close its work item when the code goes.
on_orphan: close_issue is accepted only on a marker anchor. Everything else defaults to flag, because deleting the code a bug points at is not the same as fixing the bug.
See what a work item is anchored to
The work item's Code tab lists every anchor with its file, its current line range, its state and how far the region has drifted from what it was pinned to. Choosing one opens the file at those lines.
gitmoap issue-anchors list --repo-id <id> --issue-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/anchors \ -H "Authorization: Bearer $GITMOAP_TOKEN"Show open work beside the code
The gutter query. Ask for one file and you get every anchor on it, each carrying enough of its work item — number, type, title, status — to render a marker beside the lines.
Open any file in the code browser. Anchored regions are marked in the gutter; hovering one shows #412 Bug: retry loop double-charges and the lines it covers. Reading a branch other than the default one projects the anchors onto it on the fly, so the markers land on the right lines there too.
gitmoap issue-anchors list-repo --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/anchors \ -H "Authorization: Bearer $GITMOAP_TOKEN"Passing a ref projects the anchors onto it without storing anything. The default branch is where the resolved coordinates are authoritative. Leave the file out and you get every anchor in the repository instead of one file's — the same rows, unfiltered.
An anchor is not removed when its work item closes, so a closed item's regions keep appearing here and in the review overlap below. Every row carries its item's status, which is what tells the two apart; unpin the ones a finished ticket no longer needs.
Check whether a merge request touches open work
The single highest-value review signal here: not this change mentions #412, but this change edits the code #412 is about, whether or not anybody noticed.
Open the merge request. Touches open work appears above the file list when the change overlaps an anchored region, naming each work item and the file and lines it overlaps. An empty panel means the change touches nothing anyone has anchored.
gitmoap issue-anchors list-for-diff --repo-id <id> --mr-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/merge-requests/{mr_id}/anchors \ -H "Authorization: Bearer $GITMOAP_TOKEN"Overlap is computed against the change's own hunks, from the merge base to the source branch's head — so it describes what this merge request does, not what has landed on the target since it opened. A merge request that edits line 400 of a file anchored at lines 88–104 is correctly reported as touching nothing, and one that deletes the file reaches every anchor on it.
Find the files that carry the most open work
The repository's code-debt view, under its insights, lists files heaviest first, with the open bugs, open stories and TODO markers anchored into each, how many of them have drifted, and — where the repository has a CODEOWNERS file — who owns it. The header shows the repository's total anchor count.
gitmoap issue-anchors list-debt --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/anchors/debt \ -H "Authorization: Bearer $GITMOAP_TOKEN"Heaviest means the most open bugs and stories, and only those two types are counted in those two columns — a file whose anchors are all chores or tasks shows zeros there while its total says otherwise. The total counts every anchor on the file whatever its item's type or status, and the marker column counts the ones the TODO scan pinned.
This comes from the tracker's own data. No heuristic, no code-churn proxy, no static analyser.
Take an anchor off automatic tracking
Setting an anchor's state to manual takes it out of tracking: while it is manual, Gitmoap never re-resolves it, never drifts it and never orphans it — a push that touches its file skips it, and so does re-resolving. This is the escape hatch for the case where the automatic result is confidently wrong. Setting any other state hands it back, and the next push picks it up again.
On the work item's Code tab, open an anchor's menu and choose Stop tracking. The anchor keeps its current coordinates and is labelled manual. The same menu changes what an orphaned anchor does to the work item.
gitmoap issue-anchors update --repo-id <id> --issue-id <id> --anchor-id <id>curl -X PATCH https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"You can change an anchor's state and what happens when it is orphaned. You cannot change its coordinates or its pinned commit — an anchor whose region could be edited would be a mutable claim about an immutable commit. Pin a new one instead.
Re-resolve an anchor
Re-derives the anchor against the current tip of the default branch. Use it to heal an anchor left at needs_review by an oversized push, or to see the effect of a push immediately rather than waiting.
Open the anchor's menu on the work item's Code tab and choose Re-resolve. The line range, the drift figure and the state refresh in place.
gitmoap issue-anchors reresolve \ --repo-id <id> \ --issue-id <id> \ --anchor-id <id>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}/reresolve \ -H "Authorization: Bearer $GITMOAP_TOKEN"An anchor that was skipped heals on its own the next time somebody pushes a change to its file, because its base never advanced — the next diff simply crosses the whole gap. Re-resolving just does it now.
Unpin an anchor
Warning
Unpinning is immediate and cannot be undone. The anchor's frozen coordinates — the path, the line range and the commit that together are its permalink — are destroyed, and the work item stops being tracked against that region. What the anchor already wrote to the work item's timeline stays. The code is untouched and the work item itself is unaffected. Deleting the work item deletes all of its anchors too, and so does deleting the repository the anchor points into.
Prefer setting the anchor to manual if the goal is only to stop Gitmoap moving it: that keeps the record and stops the tracking.
On the work item's Code tab, open the anchor's menu and choose Unpin. The confirmation names the file and line range; there is no undo.
gitmoap issue-anchors delete --repo-id <id> --issue-id <id> --anchor-id <id>curl -X DELETE https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Browse the TODO markers in a repository
The repository's Code TODOs view lists every marker the scan has indexed, with its keyword, text, last-known file and line, who first wrote it and when. Filter by keyword or by path, and use the Promote button on any row to turn it into a work item. The state filter switches between the unpromoted markers, the promoted ones, and those still waiting for their id to be stamped back into the source.
gitmoap code-todos list --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/code-todos \ -H "Authorization: Bearer $GITMOAP_TOKEN"Paging uses two differently-named halves: the response carries next_token, and you pass that value back as cursor.
Promote a TODO into a work item
Promotion mints a work item from the marker, pins a marker anchor to the comment, and stamps the row so the CLI can find it. The type defaults to whatever the scope's TODO settings say — Chore unless changed — and the title defaults to the marker text. Assignees are whoever you name; the marker's (@owner) is the suggestion the promote dialog pre-fills, not something the server applies on its own.
Open Code TODOs, find the row and choose Promote. The dialog pre-fills the title from the marker text, the type from the repository's setting and the assignee from the marker's (@owner). Create takes you to the new work item, which already carries its anchor.
gitmoap code-todos promote --repo-id <id> --content-digest <content digest>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/code-todos/{content_digest}/promote \ -H "Authorization: Bearer $GITMOAP_TOKEN"Promotion is idempotent. Promoting a digest that has already been promoted returns the existing work item with 200, not a conflict, because the statement it makes is already true. The one case that does answer 409 is a genuine race — two promotions of the same TODO in flight at once, where one wins and the other finds the marker already claimed. Retry it and you get the 200 and the winner's work item.
Because it mints a work item and an anchor in one go, it can be refused at either ceiling: the scope's open work item cap, or the anchor cap on the repository. Both answer 429, and the message says which. A type you name has to be one a repository can hold, so epic is refused with 400.
The minted item carries a marker anchor with on_orphan: close_issue, which is the tight loop the feature exists for: delete the comment and the work item closes, with a timeline entry naming the commit that removed it and crediting the person who pushed that removal — not whoever wrote the marker. Three guards stop that from being a surprise. The deletion has to land on the default branch. A marker removed in one place and added in another with the same digest is a move, not a deletion. And the marker anchor has to be the item's only anchor — an item somebody has also anchored to a region of code, or given acceptance criteria, is a real work item now, and a real work item is not closed by a comment edit.
Stamp the new ids back into your code
Promotion mints #430 but the server never rewrites your pushed history, so the source still says a bare // TODO:. Close the loop on your machine, inside a clone:
$ gitmoap todo sync app/payments.rs retry loop double-charges on 5xx → #430 app/cart.rs empty cart returns 500, should be 400 → #431 app/api/webhooks.rs rate-limit the webhook fan-out → #432 ✓ rewrote and staged 3 file(s). Commit with: git commit -m "chore: stamp issue ids into TODOs"With nothing to do it says so and exits — Nothing to stamp — no promoted TODOs are missing their id when no promoted TODO is unstamped, No matching TODOs found in the working tree when none of them is in front of you.
The rewrite replaces the marker's header, keeping your comment syntax, your indentation and the text: // TODO: fix the retry becomes // TODO(#430): fix the retry. An owner group is replaced by the id rather than kept beside it, so TODO(@sam): becomes TODO(#430): — if that name should survive, put it on the work item as an assignee before you stamp. It stages the files and stops. Pass --commit to have it commit as well, --dry-run to print the plan and touch nothing. It never pushes, it is safe in a dirty tree, and it is safe to run twice: a marker that already carries its id is not a candidate.
Markers are matched by digest, never by path and line. A TODO whose digest is nowhere in the working tree is skipped with a warning rather than edited blindly — if you have already reworded that comment, the line at app/payments.rs:88 is somebody else's code now.
Require every commit to name a work item
Two push rules turn the convention into a requirement, and unlike everything else here they run during the push and can reject it: one requires every commit to reference a work item, and the stronger one requires a closing reference. Merge commits are exempt, because git wrote their message, and so are tags. The check is lexical — it does not confirm that the item exists, because a database lookup per commit on the push path would make every push slower to catch a typo that the linker reports anyway.
Both live in the repository's push rules; see Repository rules for how to set them and how they inherit from the group.
Reference
Commit and merge-request links:
6 endpoints
GET/repositories/{repo_id}/issues/{issue_id}/commitsissue:read
List the commits linked to a repository work item
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found
Receive
items[]object3 fields
- commitobjectoptional
- commit_shastring
- repository_iduuid
- next_tokenobjectoptional
GET/repositories/{repo_id}/issues/{issue_id}/merge-requestsissue:read
List the merge requests linked to a repository work item
list_issue_merge_requestsGET /repositories/{repo_id}/issues/{issue_id}/merge-requestsOpen in the API referenceToken scope
issue:read
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found
Receive
items[]object24 fields
- closingbooleanWhether the merge request says it finishes the work item (
Closes #412) rather than merely mentioning it. Merging a closing merge request closes the item. merge_requestobject23 fields
- author_can_pushobjectoptionalWhether the author can push to the repository — false for a drive-by contributor's merge request. Absent where it was not computed.
- closed_atobjectoptional
- closed_byobjectoptional
- created_atstring
- created_byuuid
- descriptionobjectoptional
- is_draftboolean
- merge_commit_shaobjectoptional
- merge_request_idint64
- merged_atobjectoptional
- merged_byobjectoptional
- repository_iduuid
- restack_requiredbooleanoptionalPrecise restack signal — set when this MR's downstack was squash-merged
(a rebase is genuinely required to drop the duplicated diff). Distinct from
the advisory
commits_behind. Reset on the MR's next source-advancing write. - source_branchstring
- source_branch_deletedboolean
- source_branch_headstring
- stack_summaryobjectoptional
- statusclosedmergedopen
- target_branchstring
- target_branch_headstring
- titlestring
- updated_atstring
- versionint64optionalOptimistic-concurrency counter. Bumped on every write; writes condition on the read value.
- closingbooleanWhether the merge request says it finishes the work item (
GET/groups/{group_id}/issues/{issue_id}/commitsissue:read
List the commits linked to a group work item
Token scope
issue:read
Send
Path
- group_iduuidGroup ID
- issue_idint64Work-item iid
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found
Receive
The same shape list_issue_commits returns — 2 fields. Go to it
Show it here
items[]object3 fields
- commitobjectoptional
- commit_shastring
- repository_iduuid
- next_tokenobjectoptional
GET/groups/{group_id}/issues/{issue_id}/merge-requestsissue:read
List the merge requests linked to a group work item
list_group_issue_merge_requestsGET /groups/{group_id}/issues/{issue_id}/merge-requestsOpen in the API referenceToken scope
issue:read
Send
Path
- group_iduuidGroup ID
- issue_idint64Work-item iid
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found
Receive
The same shape list_issue_merge_requests returns — 1 field. Go to it
Show it here
items[]object24 fields
- closingbooleanWhether the merge request says it finishes the work item (
Closes #412) rather than merely mentioning it. Merging a closing merge request closes the item. merge_requestobject23 fields
- author_can_pushobjectoptionalWhether the author can push to the repository — false for a drive-by contributor's merge request. Absent where it was not computed.
- closed_atobjectoptional
- closed_byobjectoptional
- created_atstring
- created_byuuid
- descriptionobjectoptional
- is_draftboolean
- merge_commit_shaobjectoptional
- merge_request_idint64
- merged_atobjectoptional
- merged_byobjectoptional
- repository_iduuid
- restack_requiredbooleanoptionalPrecise restack signal — set when this MR's downstack was squash-merged
(a rebase is genuinely required to drop the duplicated diff). Distinct from
the advisory
commits_behind. Reset on the MR's next source-advancing write. - source_branchstring
- source_branch_deletedboolean
- source_branch_headstring
- stack_summaryobjectoptional
- statusclosedmergedopen
- target_branchstring
- target_branch_headstring
- titlestring
- updated_atstring
- versionint64optionalOptimistic-concurrency counter. Bumped on every write; writes condition on the read value.
- closingbooleanWhether the merge request says it finishes the work item (
GET/repositories/{repo_id}/commits/{commit_sha}/issuesissue:read
List the work items a commit's message named
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
- commit_shastringThe commit's full 40-character SHA
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found
Receive
items[]object24 fields
- author_can_pushobjectoptionalWhether the author can push to where this work item lives — its repository, or a group work item's group. False for a drive-by contributor's text. Absent where it was not computed.
- closed_atobjectoptional
- closed_byobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
- iidint64
- milestone_idobjectoptional
- milestone_scope_idobjectoptional
- milestone_scope_typeobjectoptional
- parent_refobjectoptional
- parent_uidobjectoptional
- pointsint32optional
- regressed_atobjectoptional
- regressed_at_shaobjectoptional
- regressed_from_shaobjectoptional
- regressed_run_idobjectoptional
- root_scope_iduuid
- scope_idstring
- scope_typegrouprepository
- severityobjectoptional
GET/repositories/{repo_id}/merge-requests/{mr_id}/issuesissue:read
List the work items a merge request's body names
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
- mr_idint64Merge-request iid
Query
- limitint32optionalPage size (default 20, max 100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or merge request not found
Receive
The same shape list_commit_issues returns — 1 field. Go to it
Show it here
items[]object24 fields
- author_can_pushobjectoptionalWhether the author can push to where this work item lives — its repository, or a group work item's group. False for a drive-by contributor's text. Absent where it was not computed.
- closed_atobjectoptional
- closed_byobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
- iidint64
- milestone_idobjectoptional
- milestone_scope_idobjectoptional
- milestone_scope_typeobjectoptional
- parent_refobjectoptional
- parent_uidobjectoptional
- pointsint32optional
- regressed_atobjectoptional
- regressed_at_shaobjectoptional
- regressed_from_shaobjectoptional
- regressed_run_idobjectoptional
- root_scope_iduuid
- scope_idstring
- scope_typegrouprepository
- severityobjectoptional
Code anchors:
8 endpoints
POST/repositories/{repo_id}/issues/{issue_id}/anchorsissue:update
Pin a work item to a region of code
Token scope
issue:update
Counts against
anchors_per_issue
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
Body
- end_lineint32
- kindobjectoptionalWhat kind of thing the anchor is pinned to. The kind decides what deleting the code means: a
person judges what deleting code pinned to a bug means, but deleting the
// TODO(#412)that is the work item's reason to exist can close it. - on_orphanclose_issueflagoptionalWhat happens to the work item when its anchor is orphaned.
- pathstring
- pinned_at_shastring
- start_lineint32
Refuses
400The region does not exist, is too large, is binary, is pinned off the default branch, or the work item is group-scoped401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent409The request conflicts with the resource's current state; `reason` says how429The work item or the repository has reached its anchor limit
Receive
No body.
GET/repositories/{repo_id}/issues/{issue_id}/anchorsissue:read
List the code anchors of a work item
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found
Receive
No body.
PATCH/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}issue:update
Update a code anchor's state or orphan behaviour
update_issue_anchorPATCH /repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}Open in the API referenceToken scope
issue:update
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
- anchor_iduuidAnchor ID
Body
- on_orphanobjectoptional
- stateobjectoptional
Refuses
400on_orphan=close_issue on a non-marker anchor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Anchor not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
POST/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}/reresolveissue:update
Re-resolve a code anchor against the current default-branch tip
reresolve_issue_anchorPOST /repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}/reresolveOpen in the API referenceToken scope
issue:update
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
- anchor_iduuidAnchor ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Anchor not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
DELETE/repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}issue:update
Unpin a code anchor
delete_issue_anchorDELETE /repositories/{repo_id}/issues/{issue_id}/anchors/{anchor_id}Open in the API referenceToken scope
issue:update
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
- anchor_iduuidAnchor ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Anchor not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
GET/repositories/{repo_id}/anchorsissue:read
List a repository's code anchors, optionally for one file and one ref
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
Query
- pathstringoptionalOnly the anchors on this file — the code browser's gutter query
- refstringoptionalShow where the anchors fall on this branch, tag or commit instead of the default branch. Nothing is saved
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
No body.
GET/repositories/{repo_id}/merge-requests/{mr_id}/anchorsissue:read
List the work items whose anchored regions a merge request touches
list_anchors_for_diffGET /repositories/{repo_id}/merge-requests/{mr_id}/anchorsOpen in the API referenceToken scope
issue:read
Send
Path
- repo_iduuidRepository ID
- mr_idint64Merge-request iid
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Merge request not found
Receive
No body.
GET/repositories/{repo_id}/anchors/debtissue:read
The repository's technical-debt map, aggregated by file
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
- anchor_countint32How many anchors the rows add up to: every anchor in the repository whose work item you can see.
rows[]object6 fields
- driftedint32
- markersint32
- open_bugsint32
- open_storiesint32
- pathstring
- totalint32
Code TODOs:
2 endpoints
GET/repositories/{repo_id}/code-todosissue:read
List the repository's code TODOs
Token scope
issue:read
Send
Path
- repo_iduuidRepository ID
Query
- statestringoptionalindexed (not yet promoted, the default) | promoted | promoted_unstamped (promoted, but the comment does not yet carry the work item's number) | all
- markerstringoptionalFilter by marker keyword (TODO, FIXME, …)
- path_prefixstringoptionalFilter to a path prefix
- limitint32optionalPage size (1-100, default 20)
- cursorstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found or not viewable
Receive
items[]object16 fields
authorobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- content_digeststringThe TODO's identifier: a SHA-256 digest of its keyword and text, without the
(@owner)or(#id)part or the comment syntax. It holds no path or line number, so moving the comment keeps the same TODO. - first_seen_atstring
- first_seen_shastring
- issue_iidobjectoptional
- lineint32
- markerBUGFIXMEHACKOPTIMIZETODOXXXThe marker keywords the scanner recognises. Uppercase only —
todo:andTodo:are everywhere in prose, README files and variable names, and lowering the case sensitivity to be "helpful" is how a scanner becomes a noise generator. - ownerobjectoptionalThe
@usernamean author wrote in the marker (without the@), a suggested assignee on promotion. Absent when the marker named no owner. - pathstring
- promoted_atobjectoptional
- region_digeststringA digest of the TODO's source line as a code region — what the anchor created on promotion
starts with as its
content_digest. Unlikecontent_digest, it includes any(#id). - repository_iduuid
- textstringThe TODO's text, normalised, at most 255 characters.
- next_tokenobjectoptional
POST/repositories/{repo_id}/code-todos/{content_digest}/promoteissue:create
Promote a code TODO into a work item
promote_code_todoPOST /repositories/{repo_id}/code-todos/{content_digest}/promoteOpen in the API referenceToken scope
issue:create
Counts against
open_issues_per_scope
Send
Path
- repo_iduuidRepository ID
- content_digeststringThe TODO's marker digest
Body
- titleobjectoptional
- work_item_typeobjectoptional
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such code TODO409A concurrent promotion won the race for this TODO429The scope has reached its open-work-item limit
Receive
- author_can_pushobjectoptionalWhether the author can push to where this work item lives — its repository, or a group work item's group. False for a drive-by contributor's text. Absent where it was not computed.
- closed_atobjectoptional
- closed_byobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
- iidint64
- milestone_idobjectoptional
- milestone_scope_idobjectoptional
- milestone_scope_typeobjectoptional
- parent_refobjectoptional
- parent_uidobjectoptional
- pointsint32optional
- regressed_atobjectoptional
- regressed_at_shaobjectoptional
- regressed_from_shaobjectoptional
- regressed_run_idobjectoptional
- root_scope_iduuid
- scope_idstring
- scope_typegrouprepository
- severityobjectoptional
Limits
Two of the operations here are capped, and both refuse with 429 rather than partially succeeding: nothing is created, no counter moves, and retrying without freeing space gives the same answer.
Anchors are capped twice, and the two are reported separately — running out on one work item is a nudge to tidy one ticket, while running out on the repository is a plan limit, and being told to delete anchors from an item that has one would be a wrong answer that reads like a plausible one.
| Limit | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Anchors on one work item | 20 | 50 | 100 | 200 |
| Anchors in one repository | 2,000 | 10,000 | 50,000 | 200,000 |
| Open work items in one group or repository | 5,000 | 20,000 | 100,000 | 500,000 |
Promoting a TODO takes two slots, because it mints a work item and pins its marker anchor in one transaction: an open work item slot in the repository, and an anchor slot. At either cap promotion is refused and the code TODO row stays exactly where it was, still visible and still promotable once you have freed some space.
Three more ceilings apply to what the platform maintains for you rather than to what you can create:
| Limit | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Commits linked to one work item | 500 | 2,000 | 5,000 | 10,000 |
| Merge requests linked to one work item | 200 | 500 | 2,000 | 5,000 |
| Code TODOs indexed in one repository | 5,000 | 20,000 | 50,000 | 200,000 |
Nothing refuses a push when one of these is reached — a push is never rejected by anything on this page except the push rules above. At the commit-link cap that one work item stops collecting commits, and it stops there: the message's closing keyword and its quick actions are not applied either, because the link is written first and the rest of the message is abandoned when it cannot be. At the merge-request-link cap the item stops collecting merge requests, so its Code tab under-reports what names it. Nothing is written and no error reaches the pusher in either case, so a work item that has quietly stopped reacting to commits is worth checking against its link count. Indexing stops at the TODO cap while promotion keeps working. A commit that a force-push leaves on no branch has its link retracted and the count comes down with it, so a branch rebased three times does not burn a work item's budget three times over.
Per-push ceilings bound the work each push does, and every one of them degrades visibly rather than silently:
| Per push | Value | Over it |
|---|---|---|
| Commit messages read for references | 1,000 per updated branch | the oldest are skipped and counted |
| Changed files considered for anchors | 500 | every candidate anchor is marked needs_review |
| Anchors moved | 200 | the same — nothing is truncated and presented as complete |
| TODOs promoted automatically | 10 | the rest are indexed instead, each still promotable in one click |
Two more bounds are per artefact rather than per push. One merge request body links at most 50 work items — a description is prose, and prose can hold five hundred numbers — and the tail is dropped and counted. A marker's text is kept to 255 characters; the identity is computed over the shortened form, so it stays stable.
An anchor left at needs_review heals on the next push to its file, or immediately when you re-resolve it.
Any of these limits can be raised for your namespace without changing plan — see Billing and plans for how to request an increase.
Troubleshooting
400 BAD_REQUESTmentioning repo_id, issue_id or commit_sha
- Cause
- A path value is not well formed — usually a path like
my-group/my-repowhere an id belongs, or an abbreviated SHA - What to do
- Use the repository's id, the item's number, and the full 40-character commit SHA
400 BAD_REQUEST— A group-scoped work item has no repository to anchor into
- Cause
- You are pinning an anchor to an epic
- What to do
- Anchor the repository-scoped story, bug or task under it. An epic can hold commit and merge-request links, just not anchors
400 BAD_REQUEST— A region may span at most 500 lines
- Cause
- The line range is too large to read and score on every push
- What to do
- Pin the part of the region the work item is actually about, or pin several smaller anchors
400 BAD_REQUEST— start_line must be at least 1 or end_line must not be before start_line
- Cause
- Line numbers are 1-based and the range is inclusive
- What to do
- Send
start_line≥ 1 andend_line≥start_line
400 BAD_REQUEST— No such region
- Cause
- The path does not exist at that commit, or the range runs past the end of the file
- What to do
- Re-read the file at that commit and copy the range from it. An anchor is never created broken
400 BAD_REQUEST— Cannot anchor a region of a binary file
- Cause
- The path resolves to a binary blob
- What to do
- Anchor a text file. There is no line range in a binary that would survive a diff
400 BAD_REQUEST— pinned_at_sha must be reachable from the default branch
- Cause
- The commit lives only on a feature branch
- What to do
- Merge the branch first, or pin to a commit that is already on the default branch. A squash-merge would otherwise leave the anchor pinned to a commit that never enters the history
400 BAD_REQUEST— on_orphan=close_issue is only valid for a marker anchor
- Cause
- You asked a hand-pinned region to close its work item when the code is deleted
- What to do
- Leave
on_orphanatflag. Only a TODO marker anchor may close a work item, because only there is the comment the item's whole reason to exist
400 BAD_REQUEST— unknown state on the TODO listing
- Cause
- The
statefilter is one of four values - What to do
- Use
indexed,promoted,promoted_unstampedorall
400 BAD_REQUEST— epic is not a repository-scoped work item type
- Cause
- You asked promotion to mint a type only a group can hold
- What to do
- Promote it as a
chore,task,bug,story,spike,incidentorsub_task. A promoted TODO carries a marker anchor, and an anchor needs a repository
400 BAD_REQUESTnaming a marker keyword
- Cause
- The
markerfilter takes one of the six recognised keywords - What to do
- Use
TODO,FIXME,HACK,XXX,BUGorOPTIMIZE
401 UNAUTHORIZED
- Cause
- No credential was sent, or the one sent is expired or malformed
- What to do
- Sign in again, or mint a fresh token. A public repository's listings answer without a credential — but a bad credential is still refused
403 FORBIDDENpinning, updating, unpinning or re-resolving an anchor
- Cause
- Anchor writes need Developer on the repository
- What to do
- Ask an Owner or Maintainer to raise your access level
403 FORBIDDENpromoting a TODO in a repository you can read
- Cause
- Filing is a write, so it needs real membership at Reporter or above — reading a public repository does not grant it
- What to do
- Ask for Reporter on the repository, then promote
403 INSUFFICIENT_SCOPE
- Cause
- The token is valid but was minted without the scope this call needs
- What to do
- Mint a new token carrying
issue:read,issue:updateorissue:create. Scopes are fixed when a token is created
404 NOT_FOUNDon a work item or repository you are sure exists
- Cause
- Either the repository is private and you are not a member, or the number is wrong
- What to do
- A private repository answers
404rather than403so its existence stays private. Check your membership, then check the number
404 NOT_FOUNDon an anchor
- Cause
- The anchor id belongs to a different work item, or to a repository you cannot read
- What to do
- Re-list the item's anchors and use an id from that response. An anchor is addressed through both the repository and the work item in the URL, and both have to match
404 NOT_FOUNDpromoting a TODO
- Cause
- The digest is wrong, or the marker has left the tree and its row was reaped
- What to do
- Re-list the repository's code TODOs and promote from a digest in that response. A digest is the whole identity here — there is no path or line to fall back on
429 TOO_MANY_REQUESTS— This work item has reached its limit of code anchors
- Cause
- The per-item anchor cap
- What to do
- Unpin anchors this item no longer needs, or split the work across items. Raising the limit is a plan change or an increase request
429 TOO_MANY_REQUESTS— This repository has reached its limit of code anchors
- Cause
- The per-repository anchor cap, which is a plan limit
- What to do
- Unpin anchors on closed work items, or request an increase. Do not go looking for an item to tidy — this cap is about the repository, not the item
429 TOO_MANY_REQUESTSpromoting a TODO
- Cause
- The scope is at its open work item limit, or the repository is at its anchor limit — promotion mints both
- What to do
- Read which one the message names. Close some work, or unpin anchors on closed items, or request an increase. The code TODO row is untouched and stays promotable either way
Fixes #412 was pushed and nothing closed
- Cause
- The commit landed on a branch that is not a closing branch
- What to do
- Check where it landed. The link is recorded either way; the close happens when the commit reaches the default branch, or a branch pattern the scope's work-item settings add
A merge request said Closes #412 and merging closed nothing
- Cause
- Either the reference was edited away before the merge, or the merge request targets another branch that does not ship — a stacked merge request into its parent's source branch, for instance
- What to do
- Re-read the description as it was at merge, and check the target branch. Merging the stack's base is what closes the work
A reference in the message was ignored entirely
- Cause
- It sat inside a fenced block, an indented block, an inline code span, a URL or a heading line, or it was glued to a preceding word
- What to do
- Write the reference in a sentence in the message body.
abc#412,# 412and a#412in a heading are all deliberately not references
A quick action did nothing
- Cause
- Either you or the commit's author lacks the access level the action needs, or the author's email matches no Gitmoap account
- What to do
- Open the work item's timeline: a refused action is recorded there with its reason. Ask for the access level, or make the two emails agree — either set your git
user.emailto the address on your account, or change the account's email to the one you commit with. Links and closes still apply — those are facts about the code, not actions performed by a person
/spend 2h in a commit message has no effect
- Cause
- Gitmoap tracks story points, not time spent, so the action is parsed and ignored
- What to do
- Record the effort with
/points, or in a comment on the work item
The link took a while to appear
- Cause
- Linking happens after the push is accepted, not during it
- What to do
- Wait a few seconds and reload. Nothing is lost — a redelivered push re-derives the same links, and a push is never delayed by this
An anchor is stuck at needs_review
- Cause
- The push that touched its file changed more than 500 files or moved more than 200 anchors, so line-by-line work was skipped rather than approximated
- What to do
- Re-resolve it, or push another change to that file — it heals across the whole gap either way
An anchor drifted after a formatting-only change
- Cause
- Re-indenting and re-spacing cannot drift an anchor — whitespace and comment markers are normalised away before scoring — but a reflow that joins or splits lines changes the lines themselves, and that the score does see
- What to do
- Look at what else was in that commit. A rename inside the region scores high and stays live; a rewritten body scores near zero. Re-resolve after a wholesale reflow, or set the anchor to manual if its coordinates are still right
Anchors did not move after work landed through a merge request
- Cause
- They do — a server-side merge is followed exactly like a push
- What to do
- Check that the merge went into the default branch. Only the default branch is tracked, so merging into a release branch moves nothing
No anchors appear on a feature branch's files
- Cause
- Anchors are maintained against the default branch
- What to do
- Read the file with a ref, which projects the anchors onto that branch on the fly, or wait for the work to merge
A TODO came back as a brand-new row after an edit
- Cause
- It was reworded and moved in the same push, so neither its text nor its position recognised it. The old row is reaped and a new one indexed — a different statement in a different place
- What to do
- Reword and move in separate commits and the identity holds. If the old row had already been promoted, its work item is still there and now has no marker feeding it: close it, or stamp the id into the new comment with
gitmoap todo sync
A todo: in the code never appears
- Cause
- Markers are uppercase only, and have to sit in a comment
- What to do
- Write
TODO:. Lowercase markers are everywhere in prose, variable names and string literals, and matching them turns the scan into a noise generator
A dependency's TODOs are missing, and that is not what you wanted
- Cause
- Vendored and generated paths are excluded by default
- What to do
- Adjust the scope's TODO ignore paths.
vendor/**,third_party/**andnode_modules/**are excluded out of the box, as are lock files, minified bundles and anything marked generated in.gitattributes
gitmoap todo sync says no origin remote or origin is not a gitmoap remote
- Cause
- It resolves the repository from the
originremote's URL - What to do
- Run it inside a clone of the Gitmoap repository, or add the Gitmoap URL as
origin
gitmoap todo sync skipped a marker it listed
- Cause
- The marker's text changed in your working tree, so its digest no longer matches the promoted row
- What to do
- Leave it — the next push re-anchors it and the two converge. Never edit the line by hand to match; the line at that position may be somebody else's code now
The TODO listing paged wrong or looped
- Cause
- The response's cursor is
next_tokenand the request's iscursor - What to do
- Pass the
next_tokenyou received as thecursorof the next request
A work item reopened by itself after a force-push
- Cause
- The commit that closed it is now on no branch, so the close was undone and the item was returned to the category the commit found it in
- What to do
- Land the work again, or close the item by hand. Turn this off in the scope's work-item settings if you would rather a rewritten history left closures alone
FAQ
Does linking slow down my pushes?
No. Every link, close and quick action happens after the push has been accepted and your git client has printed its output. The one thing that runs during a push is the optional push rule requiring a work-item reference, and it does no database work at all.
Can I reference a work item in another repository?
Yes — acme/api#412 from anywhere. It is honoured only if both the person who pushed and the commit's author can see the target and hold the level it needs: reading it to make a link, closing it to close. An unauthorized cross-repository reference is dropped exactly as an unresolvable one is, with nothing written and no error surfaced, because recording someone in a repository you cannot see referenced this item is itself a disclosure.
Why is my anchor's permalink different from its current lines?
Because both are true. The pinned path, range and commit never move and render the immutable link you would otherwise paste; the resolved coordinates are where that code lives on the default branch today.
What happens to anchors when a work item moves to another repository?
They stay pointing at the code they were pinned to, in the repository they were pinned in. Reads authorize both ends, so an anchor whose far end you cannot see is dropped from the listing rather than returned without its details.
Does a draft merge request move its linked items into review?
Yes. Any open merge request that links an item triggers the in review transition when the link first appears — drafts included, and whether the reference claims to close the item or merely mentions it. It fires once, when the link first appears, so adding a reference by editing an already-open merge request still counts. Editing a merged or closed one does not: a correction to the record is not a claim that the work has restarted. The transition is a default rule, and a scope that would rather move items by hand can turn it off.
Related
- Work itemsfiling, hierarchy, statuses, and the settings that decide which branches close an item and what a TODO marker becomes
- Proof of Doneacceptance criteria a pipeline proves, and the merge gate that reads them
- Merge requestsopening, reviewing and merging the work these links are attached to
- Repository rulesthe push rules that require a work-item reference on every commit
- Access tokensminting a token with
issue:read,issue:updateorissue:create - Billing and plansthe anchor and work item ceilings per plan, and how to have one raised