feat(skills): add GitLab as a third forge and issue tracker - #216
Conversation
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
|
AI review — changes requested (round 1 of 3, head Gate: re-run independently on the pushed head in a throwaway worktree ( Scope: the issue's three asks are otherwise covered well — the forge axis ( One gap keeps this from 1.
|
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
|
Round 1 feedback addressed — head
Gate: |
|
AI review — accepted (round 2 of 3, head Gate: re-established independently, not inherited — throwaway worktree detached at the pushed head, Round 1 findings — both verified fixed by grep, not by the description
ADR numbering — independently re-checked liveConfirmed collision-free at Did the rework introduce anything new — noThe round-2 range Scope against the issue bodyAll three asks covered: the forge axis ( Accepted. |
…d-tracker # Conflicts: # docs/99.adr/index.md # test/schema.test.ts
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, norprune-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
forgegainsgitlab, driven byglab:pull-requestopens a merge request, reads.gitlab/merge_request_templates/, and speaks GitLab's terminology in everything a human reads (!42, source/target branch).prune-branchesclassifies stale branches against MR state, with GitLab's wildcard-capable protected-branch names matched as globs.Tracker axis —
issue.trackerandwork.trackergaingitlab, meeting the contract ADR-0023 set for a third driver. That coversissueand the fourwork-*skills.Per-repo host resolution — new root
forgeHostkey (a bare hostname, optional port), resolved config → theoriginremote → 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 fromscripts/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:
releaseandmerge-depsstay 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 extendingmerge-depsis #187.Two API differences are traps rather than details, and both are written down where they bite:
glab issue listANDs a comma-separated--labelwheregh'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.glabhas 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
Checklist
pnpm verifypasses locallySKILL.mdupdated (description, examples)Test discipline
The change is mostly prose, which drives
tddnot 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, theforgeHostshape, 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 beforescripts/forge-block.mdand the generator wiring existed.test/gen-skills.test.tsand 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-reviewcould not read the artifact it reviews on GitLab (blocking). Its body namedgh pr difffor the diff andgh pr comment/gh pr review --request-changesfor the verdict, and no MR-diff recipe existed anywhere in the repo. A## Fetching the diffsection inwork-review/REFERENCE.mdnow carries both spellings (glab mr diff "$iid", with the sameiid-not-idwarningpull-requestalready states), the branch→request lookups, and the case where neither request exists. The body points at it forge-neutrally — the shapework-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 viewmissing fromissue(minor). Both driver sections now carry a Read one issue bullet in their own CLI's spelling, and the grant gainsBash(glab issue view:*). Writing the GitHub call out cleared the oneunparsedentry 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.0026→0028. feat(skills): lead every run report with a TL;DR #212 claimed0026first and feat(work-implement): state the recommended effort per loop #213 had already moved to0027; the older claim keeps its number.0028was checked live againstdevand every open pull request at push time, not against the tree this branch started from.Prose again, so
tdddrove nothing new; the one code change (dropping the two resolved gate entries intest/allowed-tools.test.ts) was the gate itself reporting the entries were stale, red before green. 189 tests pass andpnpm verifyis green on the pushed head, re-run in a clean worktree installed from the lockfile.Related issues
Closes #185