Storage & GitLab Sync
Source of Truth: GitLab Issues
All specs, status, and discussion live as GitLab issues. Humans read and edit them in the GitLab web UI. Functional consultants write specs there. The spexso CLI façade (dispatching to the platform adapters, glab under the hood on GitLab) is the bridge between Claude Code sessions and GitLab.
Local Cache: .spexso/index.json
A tiny auto-generated file (~200-300 tokens) that gives Claude awareness of active issues without reading full specs. It's listed in .gitignore — never committed. Regenerated on every /spexso-sync.
.spexso/index.json schema
{
"synced_at": "2026-04-01T14:30:00Z",
"project": "flexso-btp/extensions/customer-x-sf",
"milestones": [
{ "name": "Escalations", "title": "spexso/Escalations", "due": null, "state": "open" }
],
"issues": [
{
"id": 42,
"title": "Leave request approval workflow",
"stage": "build",
"size": "m",
"type": "feature",
"assignee": "@dev-a",
"milestone": "cycle-12",
"worktree": "issue-42" // null if not started
},
{
"id": 43,
"title": "Fix date picker timezone bug",
"stage": "spec",
"size": "xs",
"type": "bug",
"assignee": null,
"milestone": "cycle-12",
"worktree": null
}
]
}milestones lists the open marathons (spexso/ milestones) — also the ones without issues yet. Other milestones are left out. It is empty on Azure DevOps and when the milestone lookup fails; the sync still writes the issues.
Constitution: .spexso/constitution.md
One-page project principles, seeded and validated by npx spexso constitution (code-enforced cap: ≤40 content lines / ≤3500 chars, placeholders rejected). Committed — it's product knowledge, tuned per project. The planner cites articles; the reviewer flags violations. See Spec Templates.
Attempt Counters: .spexso/attempts.json
The code-enforced 3-strike rail (see TDD flow). Written by npx spexso attempt <task-id> <phase>, which the build orchestrator runs before every tester/builder dispatch; the verb exits non-zero once the phase ceiling is hit (RED: 2, GREEN: 3). Gitignored — per-worktree working state, never committed. npx spexso attempt reset <task-id> clears a task's counters after its commit; npx spexso attempt reset --all empties the ledger before /spexso-build's first task, because keys are task numbers only and the members of a bundle share one worktree.
{
"4": { "red": 1, "green": 2 }
}Marathon runtime artifacts: .spexso/plans/ and .spexso/marathon-log/
Both gitignored, both produced by /spexso-marathon. .spexso/plans/issue-{id}.md holds a plan-wave scout's pre-computed plan (parallel.budget > 1); the issue's runner consumes it via /spexso-plan --pre-plan and the orchestrator deletes it after the merge train processes the issue. Each plan file starts with a frontmatter header (issue, base commit SHA, footprint) so a plan left behind by --preview --deep or an interrupted run can be reused by a later marathon — validity is checked mechanically (frontmatter footprint vs git diff since base, through npx spexso fence). .spexso/marathon-log/{id}.jsonl is the runner's per-issue telemetry line, written through npx spexso marathon-log write <id> so started_at/finished_at/duration_seconds come from the clock (the start time is parked in .spexso/marathon-log/{id}.started by marathon-log start <id>) — one file per issue so concurrent wave siblings never share an append target; the orchestrator folds each into the consolidated .spexso/marathon-log.jsonl (also gitignored) after the train, via npx spexso marathon-log fold <id> (mechanical append + delete). .spexso/marathon-state.json (gitignored) holds the circuit breaker's consecutive-failure counter, written by npx spexso breaker <event> before the verb signals (record-then-signal, like attempts.json); npx spexso breaker reset clears it at marathon start.
.spexso/marathon-runs/<id>.json is one run record per marathon (id like 20261026T140312Z: milestone, issues, startedAt, endedAt, outcome, observe log path). npx spexso marathon-run start and finish write it; observe report --run and the dashboard read it. A run still open when the next one starts is closed as abandoned, ending at the time of its last observe note (where it died; its start when there is none), and a leftover .spexso/stop of the dead run is deleted so the new marathon does not quit at its first stop check. The folder carries its own .gitignore (*), so projects initialised earlier need no edit. Folded marathon-log.jsonl lines carry final_status (the merge train's outcome).
.spexso/dashboard.json — the marathon dashboard's port and pid while it runs (gitignored with the rest of .spexso/).
Transient files: tmp-*, demo artifacts, wiki staging
Also gitignored, because skills leave them behind when a step fails: .spexso/tmp-*.md (multiline bodies for --description-file, deleted only on success), .spexso/demo-{id}.pid and .spexso/demo-artifacts/ from /spexso-demo (including storageState.json, which holds live session cookies for remote targets), and .spexso/.wiki-* (wiki page staging). init and update both maintain these entries, so a blanket git add -A cannot commit them.
.spexso/config.json
Local, committed configuration. Two provider slots plus runtime settings.
Schema
{
"tracker": { "provider": "gitlab", "project": "group/repo" },
"code": { "provider": "gitlab", "project": "group/repo" },
"targetBranch": "main",
"models": {},
"falseFailPatterns": ["cds: command not found"],
"testCommand": "npm test",
"testTargetedCommand": "npm test -- {files}",
"budgets": { "perTaskMinutes": 30 },
"modelBySize": { "xs": "sonnet", "s": "sonnet", "m": "opus", "l": "opus", "xl": "opus" },
"parallel": { "red": true, "green": true, "budget": 1, "sizeScores": { "xs": 1, "s": 1, "m": 2, "l": 3, "xl": 6 } }
}| Field | Meaning |
|---|---|
tracker.provider | Where work items live: gitlab | github | azure | calm (CALM is tracker-only) |
tracker.project | Platform-native project reference (see table below) |
code.provider | Where source + PRs live: gitlab | github | azure |
code.project | Platform-native project reference |
targetBranch | Default merge target (e.g. main) |
models | Per-agent model overrides |
falseFailPatterns | Known environmental false-fails (a tool this machine lacks); gates the GREEN-impossible terminal state (see TDD flow). Default shown; set [] to opt out. Since every worktree gets its own npm ci, Cannot find module and cds build are no longer defaults: they hid real failures. Remove them from an older config. |
testCommand | Full-suite test invocation used by the finish gate and full-suite checks (default npm test) |
trainGate | The merge train's local suite: "full" (default) or "targeted" — only the test files the branch changed plus the static checks, with the MR pipeline as the full gate. Applies to the --train sweep and the post-rebase gate; after a conflict repair or without a pipeline the suite runs in full. See Finish |
testTargetedCommand | Targeted test invocation for one task's tests, {files} placeholder (default npm test -- {files}); set per framework by /spexso-setup |
releaseNotes | Opt-in for release-note fragments: { "language": "en" }. Present → /spexso-finish writes release-notes/unreleased/{id}.md per issue; npx spexso release-notes collects them. See Release notes per ticket |
budgets.perTaskMinutes | Per-task wall-clock ceiling (default 30); npx spexso attempt exits non-zero once exceeded — same escalate rail as the 3-strike counter (see TDD flow) |
modelBySize | Size-label → model dial for tester/builder dispatches (defaults shown); see Model Selection |
bundle | Bundles in /spexso-marathon and the factory: with a budget above 0 (default 0 = off), each window is one bundle of ready issues, built in the lead's worktree and branch, reviewed once and merged through one MR, so one pipeline. The window fills in picker order while the sizeScores (default xs:2 s:3 m:4 l:5 xl:10; no label → m) fit the budget; the first issue always enters, so with budget 10 an XL builds alone. Only with parallel.budget 1, never under --autopick. See Factory → Bundles. |
parallel | Wave execution switches: red batch-spawns a wave's testers in one message (default true); green batch-spawns builders too (default true; set false if concurrent builders trip over the worktree's shared build artifacts). See TDD execution waves. budget caps /spexso-marathon's concurrent load by weight: the window fills in picker order with issues whose sizeScores (default xs:1 s:1 m:2 l:3 xl:6; no label → m; calibrate with the suggested-sizeScores block in npx spexso observe report — see Observability) sum to at most the budget (default 1 = sequential — opt-in, recommended 6 ≈ three M issues; an XL fills a default budget alone, so it builds solo; integration always stays serial through the merge train). hotspots are path-suffix patterns for always-conflicting files (default: package.json, package-lock.json, db/schema.cds, mta.yaml, manifest.json) — two issues touching the same hotspot, even at different paths, never share a wave and trip the merge-train fence (npx spexso fence); set [] to opt out. |
handoff | Optional test hand-off after the merge: `{ "testers": [usernames], "fallback": "random" |
blockerResolvedAt | When a blocker stops blocking its dependents: "merged" (default) — once its code is on the target branch (stage::ship / stage::operate), even while a tester still holds the issue open; "closed" — only once the issue is closed (teams that want QA sign-off before dependents are built). Read by /spexso-marathon, /spexso-start, the auto-pick runner and spexso status. |
pricing | Optional token prices for npx spexso observe report: { "<model id or substring>": { "input", "output", "cacheRead", "cacheWrite" } } in € per million tokens. Default null = the report shows tokens only — spexso ships no price table, because a built-in one goes stale silently. Exact model id wins, else the longest key the id contains ("opus" prices every opus id); an entry missing any of the four fields counts as unpriced. See Observability. |
Project reference format per provider
| Provider | Format | Example |
|---|---|---|
gitlab | group/path (may be nested) | flexso-btp/tools/spexso |
github | owner/repo | acme-corp/my-app |
azure | org/project[/repo] | acme/MyProject/my-app |
calm | tenant + scenario-id (config-only) | (settled in CALM sub-spec) |
Auto-detection & backward compatibility
If the tracker or code section is absent, Spexso detects the platform from the git remote on first run:
Detection (detectProvider in lib/config.js) matches the remote's hostname by substring, case-insensitive:
| Hostname contains | Detected provider |
|---|---|
gitlab (e.g. gitlab.com, flexso.gitlab.host, gitlab.acme.io) | gitlab |
github (e.g. github.com, github.acme.com) | github |
dev.azure.com or visualstudio.com | azure |
Both slots default to the detected platform. An existing GitLab project therefore gets tracker=gitlab, code=gitlab automatically — no config file changes are needed. resolveConfig() in lib/config.js fills in the missing fields but never overwrites existing config.
Mixing platforms
The two slots are independently configurable. Example: SAP CALM as the issue tracker, GitLab for code:
{
"tracker": { "provider": "calm", "project": "tenant/scenario-id" },
"code": { "provider": "gitlab", "project": "acme/my-app" }
}When tracker and code providers differ, spexso commit-ref <id> produces a labeled reference ([calm: 3-123456]) instead of a clickable native one. See Providers (Multi-Platform) for details.
Provider support tiers
GitLab is first-class (fully implemented, field-proven). GitHub is Supported (implemented, contract + golden-shape tested; live e2e pending). Azure DevOps is Best-effort (implemented; az syntax reconstructed from docs, live-verification markers in the source). SAP CALM is tracker-only and a later goal. See Providers (Multi-Platform) for the full breakdown.
Sync Flow
npx spexso issue list (Query active issues) → Parse JSON (Extract fields) → Write index (.spexso/index.json) → Detect worktrees (Map to issues). One extra best-effort milestone lookup (listMilestones) fills milestones with the open spexso/ marathons; if it fails, sync still succeeds without it.
Façade Verbs Used
| When | spexso verb | Purpose |
|---|---|---|
/spexso-sync | npx spexso issue list | Refresh local index (open issues, normalized JSON) |
/spexso-sync | npx spexso milestone list (listMilestones) | Best-effort: fill milestones in the index with open marathons |
/spexso-start | npx spexso issue view <id> | Pull full spec content |
/spexso-brainstorm | npx spexso issue create | Create new spec issue |
/spexso-plan | npx spexso issue note <id> | Post plan as comment (also kept as .spexso/plans/issue-<id>.md for the review's UI contract) |
/spexso-review | npx spexso issue note <id> | Post review summary |
/spexso-finish | npx spexso pr create + npx spexso pr merge | Create + merge MR |
| Stage transitions | npx spexso stage <id> <stage> | Move between stages (single-value: strips other stage::*) |