Gitlab portfolio
Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi. MIT community plugin.
npx -y skills add Kanevry/session-orchestrator --skill gitlab-portfolioAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Use when you need a single-pane cross-repo health view across all vault-registered GitLab and GitHub projects. Discovers repos from `_overview.md` frontmatter in `<vault>/01-projects/*/`, aggregates open issues, MRs, critical labels, and stale signals via parallel `glab`/`gh` calls, then writes an idempotent `_PORTFOLIO.md` dashboard. Runs automatically at session-start Phase 2 when `gitlab-portfolio.enabled=true`. Triggers: "show portfolio status", "refresh the portfolio dashboard", "which repos have critical issues", "run /portfolio". <example>Context: session-start, gitlab-portfolio.enabled=true, vault has 5 registered repos. user: "/session deep" assistant: "Portfolio: 3 critical issues across 2 repos — run /portfolio for details. Dashboard written to vault/01-projects/_PORTFOLIO.md."</example>
SKILL.md
9.8 KB, as published. Nobody here has run it
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.