GitLab Portfolio Skill
Aggregates cross-repo health signals from vault-registered projects into a single
_PORTFOLIO.mddashboard — idempotent, opt-in, and_generator-marked.
Soul
gitlab-portfolio gives you a single-pane view of your entire project portfolio. It iterates all vault-registered repositories, dispatches parallel CLI calls to glab / gh, and writes a structured Markdown dashboard at <vault-dir>/01-projects/_PORTFOLIO.md. The result is a living status snapshot — updated on demand or at session-start — that never overwrites hand-edited files.
When to use
- You want a cross-repo issue/MR summary across many projects without opening each GitLab/GitHub UI.
- Session-start should surface a portfolio health banner (critical open issues, stale repos).
- A nightly routine should refresh the dashboard automatically via
/portfolio. - You need
--dry-runpreview before committing a refresh to the vault.
When NOT to use
- Bidirectional sync (writing issues back to GitLab/GitHub) — use
gitlab-opsfor write operations. - Single-repo deep-dives — use
/discoveryor directglabcommands. - Projects not registered in the vault (
_overview.mdor.vault.yaml) — unregistered repos are silently skipped. - Replacing a hand-authored
_PORTFOLIO.md— the_generatorguard prevents overwriting human content.
Phase 1: Discovery
Implementation: scripts/lib/gitlab-portfolio/aggregator.mjs — discoverRepos().
Iterates <vault-dir>/01-projects/*/ subdirectories:
- Primary — Read
_overview.mdYAML frontmatter. Look forgitlab: <namespace/repo>and/orgithub: <owner/repo>. Both keys may be present on a single repo. - Fallback — If
_overview.mdis absent or missing those keys, read.vault.yamlat the project root:spec.links.gitlab/spec.links.github. - Skip — Directories with neither source are silently skipped.
Output is an array of RepoDescriptor objects: { slug, gitlab, github }.
Phase 2: Aggregation
Implementation: scripts/lib/gitlab-portfolio/aggregator.mjs — aggregateAll(repos, config).
All repos are fetched in parallel via Promise.allSettled. Per-repo CLI dispatch:
# GitLab
glab issue list --repo <namespace/repo> --state opened --output json
glab mr list --repo <namespace/repo> --state opened --output json
# GitHub
gh issue list --repo <owner/repo> --state open --json number,title,labels,createdAt,updatedAt,milestone
gh pr list --repo <owner/repo> --state open --json number,title,labels,createdAt,updatedAt
Per-repo summary fields derived from JSON output:
| Field | Derivation |
|---|---|
openIssues / openMRs |
array lengths |
critical |
issues where any label matches critical-labels (case-insensitive) |
stale |
issues where updatedAt older than stale-days days |
nextMilestone |
earliest non-null milestone.title across open issues |
lastActivity |
max updatedAt across all issues + MRs |
topIssues |
first 3 open issues sorted by createdAt ascending (oldest open first) |
On CLI failure per repo: behaviour is controlled by mode (see Error Handling).
Phase 3: Output
Implementation: scripts/lib/gitlab-portfolio/markdown-writer.mjs — renderDashboard(summaries, config).
Output path: <vault-dir>/01-projects/_PORTFOLIO.md. Frontmatter keys: _generator: session-orchestrator-gitlab-portfolio@1, created (ISO8601, set once), updated (ISO8601, refreshed each write), repos (count). Atomic write: content built in memory, committed via a single writeFileSync.
Phase 4: Idempotency
parseFrontmatter and emitAction imported from scripts/lib/vault-mirror/utils.mjs. Rules:
- No file →
created. - File exists,
_generatorabsent or differs →skipped-handwritten(never overwrite human content). - File exists, generator matches,
updated≤ fresh data →updated. - File exists, generator matches,
updated> fresh data →skipped-noop.
Stdout action shape: {"action":"updated","path":"01-projects/_PORTFOLIO.md","repos":16,"critical":3}
Config
Opt-in via the gitlab-portfolio: block in Session Config (CLAUDE.md / AGENTS.md):
gitlab-portfolio:
enabled: true
mode: warn # warn | strict | off
stale-days: 30
critical-labels: ["priority:critical", "priority:high"]
| Field | Default | Meaning |
|---|---|---|
enabled |
false |
Master switch. |
mode |
warn |
warn / strict / off — failure handling; off ≡ disabled. |
stale-days |
30 |
Issues older than N days are flagged stale. |
critical-labels |
["priority:critical","priority:high"] |
Label substrings that classify an issue as critical (case-insensitive). |
Security
--vault-dir validation (SEC, GH #44). The --vault-dir CLI argument and vault-integration.vault-dir Session Config value are validated against the user's home directory via validatePathInsideProject (scripts/lib/path-utils.mjs). Both phases apply:
- Lexical: paths containing
..traversal that resolve outsideos.homedir()are rejected (exit 2). - Symlink: paths whose real-path (after symlink resolution) escapes
os.homedir()are rejected (exit 2).
Configure vault-integration.vault-dir only with user-controlled paths under ~. The same guard applies to the implicit Phase 2.7 portfolio snapshot at session-start. Reference: CWE-22 path traversal · .claude/rules/security.md SEC-014 adjacent guidance.
Note: vault-dir must be a CHILD of ~ (the home directory), not ~ itself. The guard uses strict isPathInside semantics, which require a proper descendant path.
CLI
node scripts/lib/gitlab-portfolio/aggregator.mjs \
--vault-dir ~/Projects/vault \
[--dry-run]
| Flag | Required | Description |
|---|---|---|
--vault-dir |
yes | Absolute path to Meta-Vault root. Must exist. |
--dry-run |
no | Run discovery + aggregation; print diff to stdout; do not write the dashboard. |
Exit codes: 0 success · 1 bad args/config · 2 vault-dir not found or write failed · 3 repo fetch failures in strict mode.
Hook
When gitlab-portfolio.enabled: true and vault-integration.enabled: true, session-start Phase 2 invokes the aggregator in parallel with ecosystem-health. The hook always runs in warn mode — session-start must not block on non-fatal failures. On critical issues found, a banner is emitted:
🔴 Portfolio: 3 critical issues across 2 repos — run /portfolio for details
Output format
---
_generator: session-orchestrator-gitlab-portfolio@1
created: 2026-05-16T08:00:00Z
updated: 2026-05-16T08:00:00Z
repos: 2
---
# Portfolio Dashboard
> Generated: 2026-05-16 08:00 UTC · 2 repos · 1 critical · 0 stale
## Summary
| Repo | Open Issues | Open MRs | Critical | Stale | Last Activity |
|---|---|---|---|---|---|
| session-orchestrator | 9 | 2 | 1 | 0 | 2026-05-16 |
| my-saas-app | 4 | 1 | 0 | 0 | 2026-05-15 |
## session-orchestrator
**gitlab:** `kanevry/session-orchestrator` · Open: 9 issues / 2 MRs · Critical: 1 · Next milestone: v3.7
| # | Title | Labels | Updated |
|---|---|---|---|
| #41 | gitlab-portfolio skill | enhancement | 2026-05-16 |
| #42 | session-end echo-stub | bug | 2026-05-14 |
| #35 | superpowers adoption | enhancement | 2026-05-10 |
## my-saas-app
**github:** `owner/my-saas-app` · Open: 4 issues / 1 PR · Critical: 0
Error handling
Behaviour is governed by mode. warn: emit warning to stderr, continue with partial data; repo rows show (fetch failed). strict: any repo fetch failure or missing vault-dir exits 3 immediately. off: all errors are silently swallowed, partial data written.
--vault-dir not found and write errors always exit 2 regardless of mode. _PORTFOLIO.md with no _generator key always results in skipped-handwritten (all modes). No repos discovered: warn → exit 0 (no file written); strict → exit 3.
Anti-Patterns
- Sequential
glabcalls — always usePromise.allSettled; sequential calls time out on large portfolios. - Writing without idempotency check — always inspect
_generatorbefore overwriting. - Hardcoding label strings — read
critical-labelsfrom config. - Blocking session-start on fetch failures — hook always runs in
warnmode. - Overwriting
createdon refresh —createdis set once; onlyupdatedchanges.
Critical Rules
_generator: session-orchestrator-gitlab-portfolio@1MUST appear in every file written by this skill.- Output path is always
<vault-dir>/01-projects/_PORTFOLIO.md. No other path is valid. Promise.allSettledis mandatory — one failing repo must never abort the entire run.- Atomic write: build full content in memory, then a single
writeFileSync. - Implementation files:
scripts/lib/gitlab-portfolio/aggregator.mjs(discovery + aggregation) ·scripts/lib/gitlab-portfolio/markdown-writer.mjs(rendering + idempotency) · shared helpers fromscripts/lib/vault-mirror/utils.mjs.