Skip to content
Select themeSelect language

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.

requirement catalogue what the product must offer
│ a clause cites what it satisfies
domain spec (specs/<dom>/) how it behaves: clauses, parameters, scenarios, decisions
│ G1 decisions answered ──► status design-approved
work 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.

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_url and created_at — no labels, no milestone (server/crates/sc-forge/src/git/forgejo.rs:77-85); sc-forge has 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: ready goes to queued, needs_info to needs_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.

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.

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.

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.

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.

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 spec gate 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.