How SupaCloud integrates with a spec-driven project
A spec-driven project keeps its behaviour in versioned specs and its work in small contracts
that bind to spec clauses. People decide before the work, gates decide after it, and an
orchestrator runs the loop in between. SupaCloud is meant to be that orchestrator. This page
explains where it plugs in, what each integration point does in the code on main today, and
what is still to be built. The decision behind it is
ADR 0075;
the files are in Delivery spec format, the work-item side and
the labels in Spec readiness contract.
The loop
Section titled “The loop”requirement catalogue what the product must offer │ a clause cites what it satisfiesdomain spec (specs/<dom>/) how it behaves: clauses, parameters, scenarios, decisions │ G1 decisions answered ──► status design-approvedwork items (WI-*.yaml) which slice comes next: spec_refs, decision_refs, verifies │ readiness ──► tracker labels ready / blocked / spec-ready [1 intake]agent run tier-routed, on an approved slice [2 dispatch] │ a gap ──► question ──► answer ──► decision ──► spec patch [3 question, 4 briefs]pull request + evidence CI, spec gate, audit, independent review │ every required check green on the same head SHA ──► merge [5 merge]spec contract validated, traced, viewed [6 contract]A lower layer never silently changes a higher one. A gap found while implementing becomes a question in the spec, not an assumption in code, and the answer comes back as a decision that is merged like any other change.
1. Intake and readiness
Section titled “1. Intake and readiness”What the loop needs. Readiness is a fact computed from the repository: dependencies closed,
referenced decisions approved, referenced specs design-approved or later. SupaCloud computes it,
writes the ready, blocked, spec-ready and human-gate labels as the project’s only tracker
writer, and dispatches exactly the spec-ready items.
Today. SupaCloud neither reads nor writes issue labels.
- The Forgejo issue it deserialises carries
id,number,title,state,body,html_urlandcreated_at— no labels, no milestone (server/crates/sc-forge/src/git/forgejo.rs:77-85);sc-forgehas no label read or write call at all. - Intake is a poll every 300 seconds (
server/crates/sc-svc-orchestration/src/backlog/mod.rs:27) over at most 100 open issues (server/crates/sc-forge/src/git/forgejo_issues.rs:30,56). - Readiness comes from the classifier:
readygoes toqueued,needs_infotoneeds_info(server/crates/sc-svc-orchestration/src/backlog/classify.rs:5-6), with the LLM classifier as the default. - The backlog lane evaluates no gate filter
(
server/crates/sc-svc-orchestration/src/auto_developer_lane_filters.rs:102-112).
Planned (SC-2, with SC-20). Read labels, milestone, assignees and state on every scan and on
issue webhooks; evaluate a label rule set at dispatch time (require_all, forbid_any, …);
hold an item whose labels fail and cancel it when its issue closes; let a label choose the
profile and tier. On top of that, the owner decision of 2026-09-22 makes SupaCloud the only
writer: it derives readiness from the work-item contracts and specs itself — the reference
implementation is bw-fm27 tools/workitems/sync.py (compute_ready, compute_spec_ready,
load_spec_state) — and maintains the labels through the project’s existing forge connection
(resolve_forge_for_project, server/crates/sc-svc-projects/src/projects/credentials.rs:307,
is the credential path the merge already uses). No bot account, no CI secret.
2. Dispatch by capability tier
Section titled “2. Dispatch by capability tier”What the loop needs. Work items and routing name a capability tier, never a model. The calibrator decides which model stands behind each tier from measured evidence.
Today. The tiers exist — flagship, balanced, fast, independent_review
(server/crates/sc-kernel/src/model_tier.rs:27-34), with the spelling independent-review
accepted — and the calibrator writes and auto-applies ordered tier chains (ADR 0068;
server/crates/sc-svc-orchestration/src/calibration/auto_apply.rs). But no work launch can say
“use tier X”: an agent profile’s model policy is manual, fixed or auto
(server/crates/sc-persistence/src/models/agent_profile.rs:28-32), and the tier of a task is
inferred from its pinned model.
Planned (SC-3). model_policy = tier on profiles and a tier per pipeline stage, resolved at
launch to the first reachable entry of the calibrated chain; a label-routed item carries its tier;
an unreachable tier re-queues the item instead of silently using the runner default. The
independent second review from a different vendor (SC-4) builds on the independent_review tier;
today the pipeline has exactly one review slot
(server/crates/sc-svc-orchestration/src/auto_developer_pipeline.rs:100-110). The owner decided on
2026-09-22 to add an automated security reviewer as well: a security_review calibrator lane
with its own evidence and acceptance telemetry, and a security slot in SC-4 that engages on
security-review gate classes (new dependency names, CI workflows, security tooling, secrets, auth
and trust-boundary code). Its approval clears those classes; a request for changes escalates to
the owner. The build brief #1448 carries this as the owner’s standing decision 6, with the
design in blueprints/SC-3.md, blueprints/SC-4.md and blueprints/SC-16.md of its pack.
3. The question gate and write-back into decisions.yaml
Section titled “3. The question gate and write-back into decisions.yaml”What the loop needs. An agent that hits a real gap asks instead of guessing. The item parks
without holding a slot, the owner answers on any surface, the item resumes with the answer, and
the answer becomes a decisions.yaml entry through a pull request.
Today. question.ask works: a question of up to 4,096 bytes with up to 8 choices
(server/crates/sc-svc-exec/src/question_ask.rs:40,44), a wait of up to 600 seconds (:36),
answered in the web inbox, on the Telegram or Discord card, or by the coordinator that delegated
the task. A deferred ask returns at once and records the answer as a durable mailbox message
(server/crates/sc-svc-exec/src/mailbox/question.rs), which is queued and never revives a
finished task (ADR 0074 D6). What is missing for the loop: the backlog has no waiting state —
its seven states are classified, queued, in_progress, in_review, done, blocked and
needs_info (server/crates/sc-persistence/src/models/backlog.rs:93) — so a parked question
keeps its concurrency slot; a question gate has no deadline (timeout_policy: Hold,
server/src/app_d1_tail_ports.rs:174); and nothing writes an answer to the issue or the
repository.
Planned (SC-10, with SC-20 requirement 7). A park mode that moves the item to
awaiting_answer, ends the run after a handoff note and frees the slot; the answer re-queues the
item with the question, the answer and the branch as resume context — through the dispatcher’s
normal gate stack, so the answer itself still launches nothing. A per-project timeout with a
default action. When the project has a spec source, the answer produces a decisions: entry that
references the question, delivered either in the item’s next implement pull request or as a
separate coordinator pull request — never pushed to the default branch.
4. Decision briefs
Section titled “4. Decision briefs”What the loop needs. Design decisions are answered in batches before the work: one brief per
domain, each item with its options, a recommended option, the rationale and drafts (tables,
diagrams, before/after screenshots, prototypes). The recommendation is pre-selected for
convenience, but a pre-selected option counts as an answer only when the owner confirms it —
per item, or with “accept all open recommendations”. Hand-over is refused while a blocking item
is open. Answers are written into decisions.yaml by pull request.
Today. Nothing. question.ask is the only structured human-input channel, one question at a
time. The first spec pilot (bw-fm27, club economy, 26 decisions) was answered on a questionnaire
page outside SupaCloud and transferred into the repository by hand.
Planned (SC-29 v2; its brief is blueprints/SC-29.md in the pack of
issue #1448).
A brief page with progress, per-item state (open, as recommended, changed), eleven item
types from single_choice to visual_signoff, a catalogue of draft kinds, producers (an MCP
publish tool, capture integration, automatic briefs for a spec pull request that adds proposed
decisions), and sinks — repo_file for decisions.yaml, issue, memory, run (release parked
runs) and webhook.
5. The CI-aware merge
Section titled “5. The CI-aware merge”What the loop needs. Nothing merges until every required check of the target repository is
green on the same head SHA — the project’s CI, its spec gate, and the gate-class status that
only an owner approval turns green — plus the audit and the required review slots.
Today. “Green” is the independent auditor’s task completing, together with the visual gate
passing or being skipped: approve_and_complete documents gates_green as that verdict and
never recomputes it
(server/crates/sc-svc-orchestration/src/backlog/complete.rs:202-213). No commit status is ever
read — server/crates/sc-forge/src/git/ci.rs is a placeholder. Under full_auto the merge
decision has no sensitive-path fallback (complete.rs:161), and at autonomy level 100 the merge
ceiling is full_auto (server/crates/sc-svc-tenancy/src/autonomy/policy.rs:180-181), so the
project’s own merge policy decides. The merge itself is a merge commit
(server/crates/sc-forge/src/git/forgejo_pulls.rs:174); a repository that allows only squash
merges refuses it, and a protected branch that requires checks refuses a merge whose checks are
not green — in both cases the item stays in review and nothing retries.
Planned (SC-1, with SC-24 for merge style). Required statuses from branch protection or a
project override are a mandatory gate input for every merge policy and every autonomy level; a
Merge Gate Ledger keyed by item, round, gate, slot and head SHA; a watch tick that merges on green
and routes a red check back to the implementer with the failing context; human contexts such as
gate-class that notify the owner once and wait without timing out — a pending gate status is
waiting for approval, never a CI failure to rework (SC-5); the merge pinned to the audited SHA;
classified retries.
6. The spec contract
Section titled “6. The spec contract”What the loop needs. The spec format is a product contract: served, versioned and drift-gated like SupaCloud’s other contracts, validated server-side as a second check, traced from requirement to evidence, and readable in the web UI.
Today. spec.get serves exactly four platform contracts — openapi_public,
workflow_schema, mcp_catalog and command_catalog
(server/crates/sc-svc-mcp/src/mcp/spec_corpus.rs:24-29). There is no requirements or EARS code.
The crate sc-spec is the workflow spec (the WorkflowSpec DTO tree), so a requirements-spec
crate needs another name.
Planned (SC-20). Adopt the reference implementation’s format verbatim as
Delivery spec format — version 0.2, whose project profile is
what lets one served contract fit projects with different catalogues, ID shapes and units; publish it
through spec.get,
generated and byte-compared in a drift test; a leaf crate sc-reqspec with the typed format and
its structural checks; spec_sources, a spec gate (off, advisory, required) and a
decision_mode per project; a traceability report from requirement to evidence; drift marking of
dependent items when a merged change edits a clause; a read-only spec viewer. An OpenSpec
import/export adapter follows later; no runtime depends on OpenSpec tooling.
What stays in the repository
Section titled “What stays in the repository”Some rules deliberately do not depend on SupaCloud:
- The gate classes are enforced by a required status in the project’s repository, because
the autonomy slider can raise SupaCloud’s merge ceiling to
full_auto. SupaCloud’s merge policy may only narrow what that status allows. In the reference implementation this is built and live (gate-class, WI-SPEC-007, merged 2026-09-22), and the subsection below describes it as it runs. - The spec gate and the work-item gates run in the project’s own CI. SupaCloud’s
specgate is a second check, not a replacement. - The contracts and specs are the source of the readiness rule. SupaCloud reads them; it does not keep a second copy of work state.
The gate-class status, as it runs in the reference implementation
Section titled “The gate-class status, as it runs in the reference implementation”The rules live in one data file (tools/qa/gate_classes.yaml, itself class G2) and one checker
(tools/qa/check_gate_classes.py), documented path by path in the repository’s own gate-classes
page. Each class names its approver: owner for G1 game design, G2 architecture and contracts,
G3 art direction and first sight, G4 sensitive topics and G5 release and legal; security_review
for S1 new dependencies, S2 CI workflows and S3 security tooling, policy and gate configuration.
What it posts. One commit status, context gate-class, on the pull request’s head commit:
success when no class applies or every class that applies is cleared for exactly this head commit
(the description names who cleared each), pending when a class waits, naming each class with its
first path or its new package names and who is awaited, and error when no honest verdict was
possible — which blocks like pending. It is a required status on the reference
implementation’s main, alongside its three core-ci contexts (core-ci / python (pull_request),
core-ci / format (pull_request), core-ci / dotnet (pull_request)), so a pending pull request cannot be merged by an autonomous
loop or through the API by any account without admin rights.
When it runs. pull_request_target (opened, synchronize, reopened, edited), so every new head
and every retarget; pull_request_review (submitted, edited); and issue_comment (created, edited,
deleted) on a pull request, but only when the comment is or was an /approve-gates command. Other
comments start nothing. The class map and the checker always come from the base branch — for a
comment, the default branch — so a pull request cannot relax the rules it is judged by; the head is
fetched only as data.
How a class is cleared.
- An owner class is cleared by an approving review of the head commit by the owner account, or
by the owner’s pull request comment
/approve-gates <sha>naming that head commit (full SHA, or a prefix of at least 12 hex characters; the pending description prints the prefix to use). - An edited comment counts for nothing, whatever it says afterwards — any account with write access can edit anyone’s comment, so an edited body cannot be trusted to be the owner’s. Post a new comment instead. Editing or deleting one re-evaluates the pull request, which withdraws an approval that is no longer there.
- A security-review class is cleared by an approving review of the head commit by a listed reviewer identity — the automated security review lane — or by the owner, who can always approve. A request for changes by that reviewer escalates every security-review class of the pull request to the owner, who alone clears it then.
- An approval counts only for the commit it names. A new push is classified from scratch, dismissed and stale reviews count for nothing, and a later request for changes by the owner holds every class again, including ones a reviewer had approved.
Two honest limits. Forgejo refuses an approval by the author of a pull request, which is exactly
why the owner clears their own pull requests by comment rather than by review — the comment path is
a workaround for a forge rule, not an extra privilege. And an instance admin can still
force-merge past branch protection whatever the rule says; that is the owner’s escape hatch, and a
reason to keep instance-admin credentials away from agents. In the same spirit: any account or
workflow with write access can post a commit status with any context, so the gate reliably stops the
autonomous merge path — it is not a defence against a writer forging gate-class on purpose.