From session-orchestrator
Performs VCS operations on GitLab/GitHub: create/close issues and MRs, apply labels, run glab/gh commands, resolve project IDs. Single source of truth for CLI syntax and label conventions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/session-orchestrator:gitlab-opshaikuThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Detect which VCS platform the current repo uses and select the right CLI:
Detect which VCS platform the current repo uses and select the right CLI:
# Check git remote
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
if echo "$REMOTE_URL" | grep -q "github.com"; then
VCS=github # use `gh`
else
VCS=gitlab # use `glab`
fi
Session Config overrides:
vcs: github|gitlab — force a specific platformgitlab-host: <host> — override auto-detected GitLab host (glab reads host from git remote by default)Directive: Consuming skills MUST NOT duplicate VCS auto-detection logic or CLI command syntax inline. This skill is the single source of truth for all VCS operations.
When a skill needs VCS operations, include this reference block in its instructions:
VCS Reference: Detect the VCS platform per the "VCS Auto-Detection" section of the gitlab-ops skill. Use CLI commands per the "Common CLI Commands" section. For cross-project queries, see "Dynamic Project Resolution."
Canonical commands: All glab and gh command syntax — flags, output formats,
pagination options — is defined in the "Common CLI Commands" section below. Consuming
skills must reference that section rather than redefining commands. If a skill needs a
command variant not listed there, add it to this file first, then reference it.
What consuming skills should include:
glab/gh invocations or detection snippetsNever hardcode project IDs. Resolve them at runtime — and re-resolve live each session; never cache a project ID across sessions (a stale ID silently targets the wrong project on rename/fork/mirror-drift, and is the root cause behind the close-verification incident documented below).
# GitLab — get numeric project ID
glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])"
# GitHub — get owner/name identifier
gh repo view --json nameWithOwner -q '.nameWithOwner'
When a skill needs to reference other projects (e.g., from cross-repos in Session Config):
# GitLab — resolve project ID by name
glab api "projects?search=<project-name>" | python3 -c "import json,sys; [print(p['id'], p['path_with_namespace']) for p in json.load(sys.stdin)]"
# GitHub — resolve repo details
gh api "repos/<owner>/<name>" --jq '.full_name'
Note: Some API calls require numeric project IDs (GitLab) or owner/repo slugs (GitHub). Always resolve dynamically from the project name.
To enumerate ALL projects (or issues) in a group, a single page is never the whole result — paginate and guard against silent truncation:
# GitLab — paginate a group's projects, following x-next-page until empty
page=1
while [ -n "$page" ]; do
resp=$(glab api "groups/<group-id>/projects?include_subgroups=true&per_page=100&page=$page" --include)
# parse the response body ($resp) for project ids/paths here, deduping by id.
# Then advance by reading the `x-next-page` response header — an empty value
# means this was the last page, so the loop exits (the guard above is what breaks).
page=$(printf '%s\n' "$resp" | awk -F': *' 'tolower($1)=="x-next-page"{sub(/\r/,"",$2); print $2}')
done
x-next-page response header — loop until it comes back empty. A single-page read on a known-large group is a signal the loop stopped early, not proof the group is small.membership=true can return a misleadingly small subset (e.g. a host that only sees a handful of a group's dozens of projects). If the count looks suspiciously low relative to the known group size, retry WITHOUT membership (rely on include_subgroups=true alone) before trusting the result. A zero/one-page result on a known-large group is a probable auth/pagination bug — treat it as a bug signal, never as ground truth that "the group is actually empty."Taxonomy convention (decided 2026-07-05, #727): labels use the SINGLE-COLON form exclusively (priority:high, status:ready, area:vcs, type:chore, from:<agent>). The ::-scoped form (priority::high, GitLab scoped-labels) is DEPRECATED baseline-scaffold legacy and MUST NOT be introduced — this repo mirrors to GitHub, which has no scoped-label semantics (no mutual-exclusion enforcement), so :: yields zero benefit on the mirror while a migration would break every existing label reference and issue.
priority:critical — blocking production or userspriority:high — important, schedule this sprintpriority:medium — plan for next sprintpriority:low — backlog, nice-to-havestatus:ready — defined, ready to pick upstatus:in-progress — actively being worked onstatus:review — MR/PR created, awaiting reviewstatus:blocked — waiting on external dependencyarea:frontend | area:backend | area:databasearea:ai | area:security | area:testingarea:ci | area:infrastructure | area:compliancearea:skills | area:vcs | area:harnessbug | feature | enhancement | refactorchore | documentation | epic | discovery | carryover | broken-windowcarryover — auto-created for 2×SPIRAL or FAILED agent tasks; see scripts/lib/spiral-carryover.mjs.broken-window — knowingly-broken shipment, hard due-date, filed by session-end Phase 2.6 (#730/H5); see scripts/lib/spiral-carryover.mjs (createBrokenWindowIssue).from:<agent> — SHOULD be applied to any issue/MR created by an automated agent (e.g. from:discovery, from:reconcile), so operators can filter agent-authored items from human-authored ones. Single-colon form, per the taxonomy convention above.blocks / is_blocked_by)GitLab's native issue-link types blocks and is_blocked_by (glab api -X POST projects/:id/issues/:issue_iid/links -f link_type=blocks|is_blocked_by) are a Premium/Ultimate license feature. On a Free/Core-tier GitLab instance this call returns HTTP 403 — a license-gate signal, not an auth/permission failure. Do not retry with different credentials or escalate as an auth bug.
Fallback (non-Premium instances):
relates_to instead — link_type=relates_to is available on every GitLab tier (no ordering semantics, just an unscoped relation). Same API shape, only the link_type value changes:
glab api -X POST "projects/:id/issues/:issue_iid/links" \
-f target_project_id=:id -f target_issue_iid=:other_iid -f link_type=relates_to
relates_to carries no ordering meaning, add an explicit ordering note to both issues, e.g. ⚠ Ordering: erst #<blocker_iid>, dann dieses Issue — blocks-Link nicht verfügbar (non-Premium).relates_to on the same project pair: if relates_to succeeds where blocks/is_blocked_by 403s, the license gate — not authentication — is the cause.GitHub has no native issue-blocking relation at all — the body-ordering-note fallback in step 2 above is the standing convention there too, regardless of license tier (see "GitHub (gh)" below).
# Issues
glab issue list --per-page 50 # All open issues
glab issue list --label "status:ready" --per-page 10 # Ready to work on
glab issue list --label "priority:high" --per-page 10 # High priority
glab issue list --closed --per-page 10 # Recently closed
glab issue view <IID> # View issue details
glab issue view <IID> --comments # With comments
glab issue create --title "title" --label "priority:high,status:ready"
glab issue update <IID> --label "status:in-progress" # WARNING: --label REPLACES the full set — see caveat below
glab issue close <IID> # then VERIFY: glab issue view <IID> must show state=closed
glab issue note <IID> -m "Comment text" # Add comment
# MRs
glab mr list # Open MRs
glab mr create --fill --draft # Create draft MR
glab mr merge <MR_IID> # Merge MR
# Pipelines
glab pipeline list --per-page 5 # Recent pipelines
glab pipeline status <ID> # Pipeline details
# API (reads host from git remote automatically)
glab api "projects/$(glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/issues?state=opened&per_page=50"
glab api "projects/$(glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/milestones?state=active"
Label update caveat (PUT-replaces, not additive): glab issue update --label (and the underlying GitLab labels API) PUT-REPLACES the entire label set — it does not add to the existing set. To change a single label you must pass the FULL desired label list, or use the dedicated add/remove operations, which are themselves unreliable across glab versions. Preferred safe pattern: use --label (adds) together with --unlabel (removes) on glab issue update when your installed glab version supports both; otherwise read the current labels first, compute the full new set, and PUT once. The same PUT-replace semantics apply to glab mr update --label.
Close verification: after glab issue close <IID>, always verify the close actually landed — re-read the issue (glab issue view <IID>) and confirm state: closed in the output. A stale/wrong project ID or a silent 404 can report local success while closing nothing; a documented incident closed 32 issues into the void this way (project ID pointed at the wrong project — see "Dynamic Project Resolution" above for the re-resolve-each-session rule that prevents it).
Commit-body close-keyword footgun: GitLab (and GitHub) auto-close an issue when a commit pushed to the default branch contains a close keyword — close/closes/closed/fix/fixes/fixed/resolve/resolves/resolved — followed by #N ANYWHERE in the commit body, not just the subject line. This fires even inside a negation ("does NOT close #N") — the platform pattern-matches the keyword + issue reference; it does not parse English negation, so the negation offers no protection. Rule: when a commit body needs to MENTION an issue without closing intent, always use a non-closing reference — refs #N, part of #N, siehe #N — never a close-keyword verb next to the number, negated or not.
-f/--raw-field vs -F/--field on glab api: -f (--raw-field) sends a literal string value with no coercion and no @file expansion. -F (--field) interprets a value starting with @ as a file to read, and coerces bare true/false/null/numeric strings to their typed form. Prefer -f for literal values — it avoids an unintended @-expansion when a value happens to start with @ (e.g. an @mention in a comment body).
# Issues
gh issue list --limit 50 # All open issues
gh issue list --label "status:ready" --limit 10 # Ready to work on
gh issue list --label "priority:high" --limit 10 # High priority
gh issue list --state closed --limit 10 # Recently closed
gh issue view <NUMBER> # View issue details
gh issue view <NUMBER> --comments # With comments
gh issue create --title "title" --label "priority:high,status:ready"
gh issue edit <NUMBER> --add-label "status:in-progress"
gh issue close <NUMBER>
gh issue comment <NUMBER> --body "Comment text" # Add comment
# PRs
gh pr list --state open # Open PRs
gh pr create --fill --draft # Create draft PR
gh pr merge <NUMBER> # Merge PR
# Workflows (CI equivalent)
gh run list --limit 5 # Recent workflow runs
gh run view <RUN_ID> # Run details
# API
gh api "repos/{owner}/{repo}/issues?state=open&per_page=50"
gh api "repos/{owner}/{repo}/milestones?state=open"
## Description
What happens vs. what should happen.
## Steps to Reproduce
1.
2.
## Root Cause (if known)
## Acceptance Criteria
- [ ]
## Goal
What should be achieved and why.
## Tasks
- [ ]
## Acceptance Criteria
- [ ]
## Session Type
[housekeeping|feature|deep]
## [Carryover] Original Task Description
### What was completed
- [completed items]
### What remains
- [ ] [remaining task 1]
- [ ] [remaining task 2]
### Context for next session
[relevant context, file paths, decisions made]
### Open Questions
_(optional — include only when unanswered questions remain in STATE.md `## Open Questions` at close; omit this section entirely otherwise)_
- [ ] [unanswered question 1] (source: W<N>/<agent>, prio: high|medium|low)
- [ ] [unanswered question 2] (source: W<N>/<agent>, prio: high|medium|low)
### Original Issue
Relates to #ORIGINAL_IID
## [Discovery] <finding title>
**Probe:** <probe_name>
**Severity:** <priority:critical|high|medium|low>
**Category:** <code|infra|ui|arch|session|audit|vault|feature>
### Finding
<description of the problem>
### Evidence
- **File:** `<file_path>`
- **Line:** <line_number>
- **Code:**
<matched_text with surrounding context>
### Impact
<why this matters — severity rationale>
### Recommended Fix
<concrete fix suggestion>
### Acceptance Criteria
- [ ] <specific, verifiable condition>
- [ ] Quality gates pass after fix
Labels: type:discovery, priority:<level>, area:<inferred>, status:ready
Pattern 3 of the gsd Pattern Adoption (Issue #519) registers a PreToolUse hook
hooks/pre-bash-templates-first.mjs that blocks gh|glab pr|mr|issue create|new
Bash calls when the current session contains no prior Read on a matching template file.
When this matters: before you or a subagent opens an MR, PR, or issue via CLI, a matching template must have been read in the current session:
.github/PULL_REQUEST_TEMPLATE.md / .github/ISSUE_TEMPLATE*.gitlab/merge_request_templates/Default.md / .gitlab/issue_templates/*Accepted template paths are configured in .orchestrator/policy/templates-policy.json
(versioned, operator-editable). Default behaviour:
enforcement: "block" — hook exits 2 when no prior template Read is foundbypass_patterns — list of command substrings that skip the hook (e.g. CI/bot calls)Bypass options for the current session (when the hook blocks unexpectedly):
create call after a Read on the template
path; the hook re-evaluates and sees the Read..orchestrator/runtime/templates-acknowledged.json
containing { sessionId, acknowledgedAt }; the hook allows all subsequent create
calls in this session.What the hook mechanically enforces (what this skill previously documented as convention only):
If the hook blocks incorrectly, follow this sequence:
create call.hooks/pre-bash-templates-first.mjs
with reproduce steps (command, session ID, template path that should have matched).# 1. Read the relevant template first (satisfies the hook)
# GitLab MR
Read .gitlab/merge_request_templates/Default.md
# GitHub PR
Read .github/PULL_REQUEST_TEMPLATE.md
# 2. Then create — hook now passes
glab mr create --title "..." --description "..."
gh pr create --title "..." --body "..."
hooks/pre-bash-templates-first.mjshooks/_lib/transcript-history.mjs (checks session transcript for prior Reads).orchestrator/policy/templates-policy.json.orchestrator/runtime/templates-acknowledged.jsonhooks/pre-bash-destructive-guard.mjsnpx claudepluginhub kanevry/session-orchestrator --plugin session-orchestratorReference GitLab CLI (glab) commands for authentication, issues, merge requests, pipelines, releases, repo management, labels, variables from terminal.
Manages GitHub issues, PRs, milestones, and Projects v2 using gh CLI and REST API. Useful for automated project management and bulk operations.
Manages full GitHub issue lifecycle: create with conventional commit titles, sub-issues, cross-repo links, edit/view/list, dump trees to markdown/YAML, push from files, comment/label/close.