GitLab Setup & Configuration
We use GitLab Self-Managed (Free Tier). This gives us everything we need: issues, boards, milestones, merge requests, CI/CD, wikis, and container registry. Here's how we structure it.
Group & Project Hierarchy
flexso-btp/ # Top-level group
├── templates/ # Subgroup: project templates
│ ├── cap-fullstack-template # CAP + Fiori Elements starter
│ ├── cap-api-template # CAP headless API starter
│ └── ui5-freestyle-template # UI5 freestyle starter
├── products/ # Subgroup: our SaaS products
│ ├── product-name-a # Each product is a project
│ └── product-name-b
├── extensions/ # Subgroup: customer extensions
│ ├── customer-x-sf-extension # SuccessFactors ext
│ ├── customer-y-s4-extension # S/4HANA ext
│ └── customer-z-salesc-extension # Sales Cloud ext
├── shared-libs/ # Subgroup: shared libraries
│ ├── flexso-cds-common # Shared CDS models
│ ├── flexso-ui-components # Shared UI5/Fiori components
│ └── flexso-btp-utils # Auth, logging, etc.
└── docs/ # Subgroup: team knowledge base
├── framework # This document, living version
├── architecture-decisions # ADRs
└── runbooks # Operational runbooksLabel System
Labels are the lifeblood of our board. We use scoped labels (the :: syntax) so only one label per scope can be active at a time. This prevents label soup.
| Scope | Labels | Purpose |
|---|---|---|
| stage:: | opportunity spec build review ship operate | Tracks where the issue is in our 6-stage lifecycle |
| type:: | feature bug enhancement tech-debt spike maintenance epic | Categorizes the kind of work — epic is the parent issue /spexso-kickstart creates for a traject: it carries no stage:: label, so no marathon ever picks it |
| priority:: | critical high medium low | Urgency level |
| size:: | xs s m l xl | T-shirt sizing for effort estimation |
| platform:: | sf s4 salesc btp is | Which SAP system it relates to |
| ai:: | ai-assisted ai-reviewed ai-generated | Tracks AI involvement for learning |
| needs:: | manual-test conflict-resolution attention | Post-merge flags: manual QA needed, auto-merge failed, or a marathon run left the issue for human review |
| auto-pick | auto-pick | Eligible for the online runner — Spexso proposes it, a human confirms; removed when the issue is done or handed off |
| redesign | redesign | Set by /spexso-redesign, or by /spexso-prototype on an issue that does not have it yet: the issue waits for a new design and no picker takes it. /spexso-brainstorm <id> --design docs/design/issue-<id> loads the design and removes it — see Een scherm herontwerpen |
| release notes | skip-release-note | /spexso-finish writes no release-note fragment for this issue (only in a project with releaseNotes in its config) — see Release notes |
| auto-test | auto-test-success, auto-test-fail, auto-test-skipped | Set by the test server after its browser test; the next run skips every issue with one. Remove the label to have the issue tested again |
stage::is single-value — enforced in code. GitLab's scoped labels give mutual exclusion in the UI, but across automated CLI transitions it isn't guaranteed (and legacy/non-canonical stage labels like a straystage::wipcan linger). Spexso therefore never sets a stage with a rawnpx spexso issue update --label; every transition (brainstorm → spec, start → build, finish → ship) goes throughnpx spexso stage <id> <stage>, which strips every existingstage::*before adding the target. This guarantees exactly one stage label per issue, so the marathon picker can't re-select a shipped issue that still carriesstage::spec.
Blocking links on GitLab CE. Typed
blocksissue links are a GitLab Premium/Ultimate feature; Community Edition (likeflexso.gitlab.host) only acceptsrelates_to. Spexso detects the refusal and records the dependency as aBlocked by #nline in the dependent issue's description, plus arelates_tolink so the relation stays visible in the UI. The marathon,/spexso-start, auto-pick andspexso statusread those lines exactly like native links. WritingBlocked by #n(or- Blocked by: #n — title) in a description by hand works too.
Issue Board Configuration
Each project gets three kanban boards, provisioned automatically by npx spexso init (and re-runnable via /spexso-boards-init):
| Board | Columns | Audience |
|---|---|---|
| Workflow | stage::* (Opportunity → Operate) | Everyone — daily "where is each issue?" view |
| Needs attention | needs::manual-test | needs::conflict-resolution | needs::attention | Tech lead — what got dropped by autonomous runs |
| Backlog grooming | priority::* (critical → low) | Product / PM — refinement sessions |
The provisioning logic lives in lib/boards.js (sister module to lib/labels.js). It delegates to the tracker adapter's syncBoards (also exposed as npx spexso board sync), which calls GitLab's REST endpoints, is idempotent (skips existing boards/columns, never deletes), and enforces the dependency order: labels are created first, boards second.
See boards design for the full rationale and setup-boards scenario for the developer flow.
Milestones
Milestones represent delivery cycles, not sprints. We work in 2-week cycles, but a milestone can span multiple cycles for larger initiatives. Every issue is assigned to a milestone. Milestones have due dates. The milestone view in GitLab shows completion percentage — this is our progress report, no slide decks needed.
Marathons: spexso/ milestones
Spexso plans marathons with its own milestones, titled spexso/<name> and named after their goal (spexso/Escalations). They are not delivery cycles and not agile sprints: a marathon is the set of specced issues one autonomous run works through, usually in hours. Create and close them with npx spexso milestone create "<name>" / close "<name>", or from the marathon dashboard.
Spexso only reads milestones with the spexso/ prefix. Your team's own milestones (cycles, releases, sprints) never show up in .spexso/index.json or the dashboard. An issue has one milestone, so moving an issue into a marathon replaces its current milestone — the dashboard asks first.