Skip to content

feat(skills): add GitLab as a third forge and issue tracker - #216

Merged
TitusKirch merged 12 commits into
devfrom
ai/185-gitlab-forge-and-tracker
Aug 3, 2026
Merged

feat(skills): add GitLab as a third forge and issue tracker#216
TitusKirch merged 12 commits into
devfrom
ai/185-gitlab-forge-and-tracker

Conversation

@TitusKirch

@TitusKirch TitusKirch commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Docks GitLab as a second forge and a fourth issue tracker, and promotes the forge host to a resolved, per-repo fact.

The forge axis was designed to take a second forge additively — the schema said so in its own description — but nothing had ever docked there. A repo hosted on GitLab could use neither pull-request, nor prune-branches, nor the work loop. A second gap surfaced alongside it and is not GitLab-specific: every skill assumed a single host, so a self-hosted GitLab or a GitHub Enterprise repo was served the public host by a run that never noticed it had guessed.

Forge axis — root forge gains gitlab, driven by glab:

  • pull-request opens a merge request, reads .gitlab/merge_request_templates/, and speaks GitLab's terminology in everything a human reads (!42, source/target branch).
  • prune-branches classifies stale branches against MR state, with GitLab's wildcard-capable protected-branch names matched as globs.

Tracker axisissue.tracker and work.tracker gain gitlab, meeting the contract ADR-0023 set for a third driver. That covers issue and the four work-* skills.

Per-repo host resolution — new root forgeHost key (a bare hostname, optional port), resolved config → the origin remote → whatever the CLI is already authenticated against. The remote sits above the CLI deliberately: the remote is a repo-level fact, the CLI's configured host a machine-level one, and a session that works two repos must reach two instances. Authentication is never duplicated — the ladder resolves a name, the CLI holds the credentials.

The rule is mirrored, not restated per skill: a sixth <skills-forge> block generated from scripts/forge-block.md, carried by the five skills that drive a forge, with the two work queues naming their worker's REFERENCE for it as they already do for the config contract and the lock spec.

Deliberately out of scope, per the issue: release and merge-deps stay GitHub-only. Both already stop on a forge they do not implement rather than degrading silently, so the hole is covered; stating the limitation properly is #186, and extending merge-deps is #187.

Two API differences are traps rather than details, and both are written down where they bite:

  • glab issue list ANDs a comma-separated --label where gh's search qualifier ORs it, so the implement loop's two input labels are two calls unioned locally. Comma-joining them selects issues carrying both and drains an empty queue in silence.
  • glab has no --body-file, so a multi-line body is passed by command substitution from the same temporary file the GitHub path writes.

Type of change

  • Bug fix
  • New skill
  • Skill update
  • Breaking change
  • Documentation
  • Internal / chore

Checklist

  • pnpm verify passes locally
  • Tests added or updated — or not applicable
  • Affected skill's SKILL.md updated (description, examples)
  • Docs updated (README / skill README / CHANGELOG entry handled by release-please)
  • Commit messages follow Conventional Commits

Test discipline

The change is mostly prose, which drives tdd not at all. Where it touched code — the schema's accept/reject surface and the generator's block mirroring — the loop ran red-first, with the issue body's requirements standing in for the human as the agreed seams:

  • test/schema.test.ts — three new suites (the forge axis, the forgeHost shape, the GitLab tracker) written and failing before the schema moved.
  • test/isolation.test.ts — the <skills-forge> roster and the queue-delegation rule written and failing before scripts/forge-block.md and the generator wiring existed.
  • test/gen-skills.test.ts and the registry fixture — the new artifact named in the drift report before it could be produced.

189 tests pass; the full gate (pnpm verify) is green on the final tree.

Review round 1 — addressed

Both findings, plus a numbering collision that surfaced after the first push:

  • work-review could not read the artifact it reviews on GitLab (blocking). Its body named gh pr diff for the diff and gh pr comment / gh pr review --request-changes for the verdict, and no MR-diff recipe existed anywhere in the repo. A ## Fetching the diff section in work-review/REFERENCE.md now carries both spellings (glab mr diff "$iid", with the same iid-not-id warning pull-request already states), the branch→request lookups, and the case where neither request exists. The body points at it forge-neutrally — the shape work-implement's body already has — and the verdict paragraph names both primitives, saying outright that GitLab has no second verb to upgrade to.
  • glab issue view missing from issue (minor). Both driver sections now carry a Read one issue bullet in their own CLI's spelling, and the grant gains Bash(glab issue view:*). Writing the GitHub call out cleared the one unparsed entry in the allowed-tools gate — its reason kinds are pinned in both directions, so the now-unused kind went with it — and the grant's contract sentence names the GitLab reads it had never mentioned.
  • ADR renumbered 00260028. feat(skills): lead every run report with a TL;DR #212 claimed 0026 first and feat(work-implement): state the recommended effort per loop #213 had already moved to 0027; the older claim keeps its number. 0028 was checked live against dev and every open pull request at push time, not against the tree this branch started from.

Prose again, so tdd drove nothing new; the one code change (dropping the two resolved gate entries in test/allowed-tools.test.ts) was the gate itself reporting the entries were stale, red before green. 189 tests pass and pnpm verify is green on the pushed head, re-run in a clean worktree installed from the lockfile.

Related issues

Closes #185

The forge axis was designed to take a second forge additively but had
never been exercised: `forge` carried a single-value enum, and a repo on
GitLab could use none of the forge-bound skills.

Widen `forge` to github|gitlab and both tracker enums to include gitlab,
and add the root `forgeHost` key — a bare hostname, resolved per repo,
because self-hosted GitLab is the normal deployment and GitHub
Enterprise has the same shape.

Refs #185
…skill

Which forge a repo uses was already a config key; which host that forge
lives on never was, and every skill assumed one. A self-hosted GitLab or
a GitHub Enterprise repo was served the public host by a run that never
noticed it had guessed.

Add a sixth mirrored block, <skills-forge>, generated from
scripts/forge-block.md and carried by the five skills that drive a forge.
It states both halves once: which CLI each forge takes, and the host
ladder — config, then the origin remote, then whatever the CLI is
already authenticated against — resolved per repo rather than per
session. The two work queues name their worker REFERENCE for it, as they
already do for the config contract and the lock spec.

The author-authority rule gains its GitLab tier alongside, since the
tracker driver reads issue comments there too.

Refs #185
The skill drove gh directly and called everything a pull request, so a
GitLab repo could not use it at all.

Add the glab driver beside the gh one: MR create and update, the source
and target branch spelling, iid rather than number, .gitlab/merge_request_
templates/ read before the .github/ directory a migrated repo may still
carry, and the plan naming the host whenever it is not the public one.

Everything outside the two forge sections stays forge-neutral — the
umbrella title, the template filling and the stacked-branch check, which
is git ancestry and needs only the list of open requests.

Refs #185
…quests

Two of the four categories and half the protection set are read from the
forge, so a GitLab repo could not run the skill safely at all.

Add the glab reads beside the gh ones — the merged-request list the
squash and rebase detection rests on, the protected-branch endpoint whose
names may be wildcards rather than literal branches, and the open
requests whose source branch is untouchable. Everything after that list
is git and stays as it was; what follows the forge is the word in the
report, a closed PR or a closed MR.

Both CLIs page at 30 by default, so the truncation warning now names
both, and glab caps a page at 100 — follow the pages rather than raising
one number.

Refs #185
A fourth tracker on the same axis as github, linear and local, meeting
the contract ADR-0023 set: create, update, search, catalogs and the
parent-child edge, all through glab against the resolved host.

Three differences are traps rather than details, so each is written down
where it bites: glab has no --body-file, its labels may be group-scoped
and usable without appearing among the project own, and there is no
sub_issue endpoint — the edge is the linked-issue relation with the
blocks link type, never an epic, which is a group-level object a
repo-scoped skill must not create.

GitLab issue templates live under .gitlab/issue_templates/, read
alongside the .github/ directory a migrated repo may still carry.

Refs #185
The lifecycle is label-based on GitLab exactly as on GitHub, so the
driver is the GitHub recipes in GitLab spelling: --label with --unlabel
in one call for the lease, the linked-issue blocks relation as the
dependency edge, related_merge_requests as the reconcile pushed-artifact
query, resource_label_events as the round count, and glab mr note as the
feedback primitive — with no self-review refusal to fall back from,
because there is no separate review call there at all.

One difference is a silent-stall trap and is stated in the selection
query itself: glab ANDs a comma-separated --label where the gh search
qualifier ORs it, so the loop two input labels are two calls unioned
locally. Comma-joining them selects issues carrying both and drains an
empty queue with nothing to notice.

Refs #185
The capability gate now probes glab against the host the remote names,
never gitlab.com by default — a glab signed in to gitlab.com while the
remote is a self-hosted instance is not viable, and saying so is the
whole point of the check.

forgeHost is proposed only where the host is not the forge public one,
so a github.com or gitlab.com remote still writes no key: deriving it
from the remote is already correct there, and a key set to its own
default is the drift this skill exists to prevent.

Refs #185
Why GitLab docks as a driver on the two existing axes rather than as a
second family of skills, why the host ladder puts the origin remote above
the CLI own configuration, and why release and merge-deps stay GitHub-only
and stop rather than degrade.

Refs #185
@TitusKirch

Copy link
Copy Markdown
Owner Author

AI review — changes requested (round 1 of 3, head 213df35)

Gate: re-run independently on the pushed head in a throwaway worktree (pnpm install --frozen-lockfile + pnpm verify) — green, 189/189 tests. The forge's checks are green too; neither was inherited.

Scope: the issue's three asks are otherwise covered well — the forge axis (forge: gitlab, pull-request → MR + .gitlab/merge_request_templates/, prune-branches → MR state and glob-matched protected branches), the tracker axis (issue.tracker / work.tracker gain gitlab), and per-repo host resolution (forgeHostorigin remote → CLI), mirrored once via scripts/forge-block.md with the roster pinned in test/isolation.test.ts. release and merge-deps verified to still stop on an unimplemented forge rather than degrade. ADR-0026 records the decision. No secrets in the diff.

One gap keeps this from done.


1. work-review cannot read the artifact it reviews on GitLab (blocking)

skills/work/work-review/SKILL.md is the only one of the four work-* skills whose body was not made forge-aware — its description now advertises GitLab (glab), but the body still names only gh:

  • Line 49- **PR present** (\worktree`) → the PR's diff (`gh pr diff `).`

    There is no GitLab counterpart for this anywhere in the repo: grep -r 'mr diff' skills/ returns nothing, and work-review/REFERENCE.md has no MR-diff recipe either. So on a gitlab tracker the review loop reaches step 5 with no stated way to fetch the merge request's diff — the single artifact the whole skill exists to judge. The MR spelling is glab mr diff <iid>, and the iid-not-id warning pull-request's REFERENCE already carries applies here too.

  • Lines 75 and 78 — the verdict table and the paragraph under it name gh pr comment as "the primitive that carries every verdict" and gh pr review --request-changes as the optional upgrade. REFERENCE.md#feedback-recipes already states the GitLab answer correctly (glab mr note / glab issue note, and no second verb to upgrade to because GitLab has no separate review call) — the SKILL.md body just never picked it up. It already names Linear inline ("an issue/Linear comment"), so naming GitLab there is the consistent fix, not a new pattern.

Compare work-implement/SKILL.md, which contains zero gh/glab command references and delegates every recipe to its REFERENCE — that is the shape work-review/SKILL.md should match, either by going forge-neutral or by naming both spellings.

2. glab issue view missing from issue's allowed-tools (minor)

skills/work/issue/SKILL.md grants Bash(gh issue view:*) on the GitHub side but only glab issue list / label list / repo view on the GitLab side, and the new Tracker — GitLab (glab) section in issue/REFERENCE.md names no single-issue read command at all (create / update / close / list / catalogs / links, but no view). Update flows re-read one issue's body, so either add glab issue view to both the driver section and the grant, or state explicitly that glab issue list --output json is the intended single-issue read on this driver.


Round 1 of 3 — back to the implement loop. Everything else in this change reads as complete and correct.

The review body still named only `gh`: the diff it judges was fetched with
`gh pr diff`, and the verdict's primitive was `gh pr comment` with
`gh pr review --request-changes` as its upgrade. On a `gitlab` tracker the loop
therefore reached step 5 with no stated way to fetch the one artifact it exists to
judge — no MR-diff recipe existed anywhere in the repo.

The diff recipes now live in REFERENCE, where every other command recipe this skill
drives already is, and the body points at them in the forge-neutral spelling — the
shape `work-implement`'s body already has. The verdict paragraph names both
primitives and says outright that GitLab has no second verb to upgrade to, a fact the
feedback recipes carried and the body had never picked up.

Refs #185
The GitLab driver section listed create, update, close, list, catalogs and links but
no way to read **one** issue — and every update flow starts by re-reading the live
body. The grant matched: `glab issue view` was missing from `allowed-tools` while
`gh issue view` was there.

Both driver sections now carry the read, in their own CLI's spelling, and the grant
gains `Bash(glab issue view:*)`. Writing the GitHub call out also settles the one
`unparsed` entry in the allowed-tools gate: the contract sentence had abbreviated it
to `gh issue list` / `view`, which no reader could join back into a command, and the
gate's reason kinds are pinned in both directions — so the now-unused kind goes with
it. That sentence also names the GitLab reads it had never mentioned.

Refs #185
ADR numbers are claimed on a branch and collide only once two branches are open at
the same time. #212 took 0026 first and #213 had already moved to 0027, so the older
claim keeps its number and this one takes the next free: 0028, checked live against
`dev` and every open pull request rather than against the tree this branch started
from.

Refs #185
@TitusKirch

Copy link
Copy Markdown
Owner Author

Round 1 feedback addressed — head 1cb0167.

  1. work-review on GitLab (blocking) — the missing MR-diff recipe now exists: ## Fetching the diff in work-review/REFERENCE.md, carrying gh pr diff and glab mr diff "$iid" with the iid-not-id warning pull-request's REFERENCE already states, the --head / --source-branch lookups for finding the request from the branch, and the case where neither request exists. SKILL.md line 49 delegates there forge-neutrally instead of naming a command, matching work-implement's shape. The verdict table and the paragraph under it name both primitives (gh pr comment / glab mr note) and state that GitLab has no second verb to upgrade to.
  2. glab issue view (minor) — added to the GitLab driver section and the grant, with the GitHub counterpart written out in its own section for symmetry. That resolved the gate's one unparsed entry for Bash(gh issue view:*); since UNDEMONSTRATED_REASONS is pinned in both directions, the now-unused kind was dropped with it. The grant's contract sentence also names the GitLab reads it had never mentioned.
  3. ADR renumbered 00260028feat(skills): lead every run report with a TL;DR #212 claimed 0026 before this branch and feat(work-implement): state the recommended effort per loop #213 had already moved to 0027, so the older claim keeps its number. Checked live against dev and every open PR at push time; file, heading, docs/99.adr/index.md and the PR body all moved together.

Gate: pnpm install --frozen-lockfile + pnpm verify in a clean worktree — green, 189/189.

@TitusKirch

Copy link
Copy Markdown
Owner Author

AI review — accepted (round 2 of 3, head 1cb0167)

Gate: re-established independently, not inherited — throwaway worktree detached at the pushed head, pnpm install --frozen-lockfile + pnpm verify: green, 189/189. The forge's own checks on this head are green too (Verify, Conformance, CodeQL, both Analyze jobs), and the base actually triggers them.

Round 1 findings — both verified fixed by grep, not by the description

  1. work-review could not read the artifact it reviews on GitLab (was blocking) — resolved. grep -rn 'mr diff' skills/ scripts/ docs/ test/ now returns skills/work/work-review/REFERENCE.md:385: glab mr diff "$iid", in a new ## Fetching the diff section that carries both spellings, the iid-not-id warning pull-request's REFERENCE already states, the branch→request lookups (gh pr list --head, glab mr list --source-branch), and the neither-exists case routed to review-after-land. SKILL.md line 49 no longer names a command — it delegates forge-neutrally, matching work-implement's shape, which is exactly what round 1 asked for. The verdict paragraph now names gh pr comment / glab mr note and states outright that GitLab has no second verb to upgrade to.

    Anchors checked rather than assumed: #fetching-the-diff, #the-forge-and-its-host and #review-after-land-branchname all resolve against the file's real headings.

  2. glab issue view missing from issue (was minor) — resolved. Present in both places it needed to be: a Read one issue bullet in the GitLab driver section of issue/REFERENCE.md (with the --output json rationale), and Bash(glab issue view:*) in the grant. The GitHub counterpart was written out in its own section for symmetry, which is what let the unparsed entry leave test/allowed-tools.test.ts — and since UNDEMONSTRATED_REASONS is pinned in both directions, dropping the now-unused kind with it is required, not incidental. The grant's contract sentence now names the GitLab reads it had never mentioned.

ADR numbering — independently re-checked live

Confirmed collision-free at 0028. origin/dev ends at 0025; across all eight open pull requests the only ADR claims are #2120026, #2130027, and this PR → 0028. #205 (dev→main) carries no new number. File, # ADR-0028 heading and docs/99.adr/index.md all moved together.

Did the rework introduce anything new — no

The round-2 range 213df35..1cb0167 is 7 files, +34/−19: the ADR rename, the two findings, and the one gate entry the fix made stale. Nothing outside the scope of the feedback.

Scope against the issue body

All three asks covered: the forge axis (forge: gitlab, pull-request → MR + .gitlab/merge_request_templates/, prune-branches → MR state with glob-matched protected branches), the tracker axis (issue.tracker / work.tracker gain gitlab), and per-repo host resolution (forgeHostorigin remote → CLI, explicitly never resolved once per session). Mirrored once via scripts/forge-block.md into the five forge-driving skills, with the two queues delegating to their worker's REFERENCE as they already do for the config and lock blocks; roster pinned in test/isolation.test.ts. release and merge-deps stay out per the issue and still stop rather than degrade. CLAUDE.md/AGENTS.md updated nine→ten artifacts and are byte-identical. Schema additions are additive and the forgeHost ["string","null"] shape is correct — minLength/pattern apply only to the string branch. Spot-checked the glab spellings against the CLI's real flags (mr diff, mr list --source-branch, mr note -m, issue view --output json, issue update --unlabel, label list --per-page, auth status) — all correct. No secrets in the diff; no CODEOWNERS path involved.

Accepted.

…d-tracker

# Conflicts:
#	docs/99.adr/index.md
#	test/schema.test.ts
@TitusKirch
TitusKirch merged commit 975d4a0 into dev Aug 3, 2026
5 checks passed
@TitusKirch
TitusKirch deleted the ai/185-gitlab-forge-and-tracker branch August 3, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant