Factory intake: client repo provisioning + unfunded gating #4

Closed
opened 2026-08-25 11:04:26 +02:00 by pbwraith · 1 comment
Owner

Goal

Factory front-end (option 1): provision a client Gitea repo, hold client work in a local queue until funded, then create issues with acceptance criteria. No dispatcher changes — the gate lives entirely in this service.

Depends on: issues #1-2 (billing core + API). Files allowed: gitea_saas/gitea_client.py, gitea_saas/intake_factory.py, tests/test_intake_factory.py. Do NOT modify anything else.

Spec

  • GiteaClient (mockable): thin wrapper over Gitea API v1:
    • create_org_repo(name) if missing (owner per env GITEA_OWNER)
    • create_issue(repo, title, body, labels)
    • repo_exists(name)
    • Real impl: HTTP via urllib + token env (GITEA_TOKEN, GITEA_BASE_URL). Mock: in-memory dict.
  • IntakeFactory(core, gitea, config):
    • provision_client(client_id, repo_name=None) -> repo_name — creates or reuses repo <repo>; records repo->client mapping in client_repos table (add to billing schema: client_repos(client_id, repo, created_at)).
    • submit_issue(client_id, repo, title, acceptance_body, labels=None):
      • if client is funded (balance >= SAAS_DEBIT_FLOOR_MINOR, default 500): create Gitea issue immediately, return issue number
      • else: queue in new pending_issues table (id, client_id, repo, title, body_json, labels_json, created_at, status) — status queued
      • returns {delivered: bool, issue_number?: int, pending_id?: int}
    • unlock(client_id) — flush ALL queued pending issues for the client to Gitea (create + p1 label), mark shipped
    • lock(client_id) — future submits queue instead of shipping (no label games, no dispatcher coupling)
    • client_unfunded(client_id) -> bool — true when balance < debit floor
  • Hook into issue #2's API: POST /api/factory/provision, POST /api/factory/{client_id}/issues, POST /api/factory/{client_id}/unlock, POST /api/factory/{client_id}/lock.

Acceptance

  • provision creates repo once; second call reuses (idempotent)
  • funded submit creates Gitea issue immediately, delivered=true
  • unfunded submit queues (no Gitea issue created), delivered=false
  • unlock flushes queued -> Gitea with p1 label, marks shipped, second unlock is a no-op
  • lock stops shipping: subsequent submits queue again
  • client with balance below floor is client_unfunded()==True
  • all tests against MockGitea + tmp DB; no network; verified NO dispatcher/lane-script file is touched by this repo

Verification: python -m pytest tests/test_intake_factory.py -q passes.

## Goal Factory front-end (option 1): provision a client Gitea repo, hold client work in a local queue until funded, then create issues with acceptance criteria. No dispatcher changes — the gate lives entirely in this service. **Depends on:** issues #1-2 (billing core + API). **Files allowed:** `gitea_saas/gitea_client.py`, `gitea_saas/intake_factory.py`, `tests/test_intake_factory.py`. Do NOT modify anything else. ## Spec - `GiteaClient` (mockable): thin wrapper over Gitea API v1: - `create_org_repo(name)` if missing (owner per env `GITEA_OWNER`) - `create_issue(repo, title, body, labels)` - `repo_exists(name)` - Real impl: HTTP via urllib + token env (`GITEA_TOKEN`, `GITEA_BASE_URL`). Mock: in-memory dict. - `IntakeFactory(core, gitea, config)`: - `provision_client(client_id, repo_name=None) -> repo_name` — creates or reuses repo `<repo>`; records repo->client mapping in `client_repos` table (add to billing schema: `client_repos(client_id, repo, created_at)`). - `submit_issue(client_id, repo, title, acceptance_body, labels=None)`: - if client is funded (`balance >= SAAS_DEBIT_FLOOR_MINOR`, default 500): create Gitea issue immediately, return issue number - else: queue in new `pending_issues` table `(id, client_id, repo, title, body_json, labels_json, created_at, status)` — status `queued` - returns `{delivered: bool, issue_number?: int, pending_id?: int}` - `unlock(client_id)` — flush ALL `queued` pending issues for the client to Gitea (create + `p1` label), mark `shipped` - `lock(client_id)` — future submits queue instead of shipping (no label games, no dispatcher coupling) - `client_unfunded(client_id) -> bool` — true when balance < debit floor - Hook into issue #2's API: `POST /api/factory/provision`, `POST /api/factory/{client_id}/issues`, `POST /api/factory/{client_id}/unlock`, `POST /api/factory/{client_id}/lock`. ## Acceptance - [x] provision creates repo once; second call reuses (idempotent) - [x] funded submit creates Gitea issue immediately, delivered=true - [x] unfunded submit queues (no Gitea issue created), delivered=false - [x] unlock flushes queued -> Gitea with p1 label, marks shipped, second unlock is a no-op - [x] lock stops shipping: subsequent submits queue again - [x] client with balance below floor is `client_unfunded()==True` - [x] all tests against MockGitea + tmp DB; no network; verified NO dispatcher/lane-script file is touched by this repo **Verification:** `python -m pytest tests/test_intake_factory.py -q` passes.
pbwraith added the
p1
label 2026-08-25 11:04:26 +02:00
pbwraith added the
in-review
label 2026-08-25 12:46:24 +02:00
Author
Owner

[extend-request from issue #8 / !20] Canonical client needs list_issue_comments

The !20 bounce (2026-08-25 22:38) ruled gitea_saas/gitea_client.py is owned
here by issue #4 (per !23 closing #21) and must not be forked on any other
branch — including to add a method that issue. So requesting it here instead
of forking:

intake_eval.py (issue #8, poll_completed) calls
self._gitea.list_issue_comments(repo, issue_number) to capture each closed
eval-task issue's final comment as the result summary. The current canonical
client (origin/issue-4) does not expose it — diffing the two blobs shows
exactly this 8-line delta:

+    def list_issue_comments(self, repo, number):
+        return self._request("GET", f"{repo}/issues/{number}/comments")

Not blocking !20 from review (its real-client request test skips until the
method lands here so the gate stays honest), but the eval farm's poll_completed
cannot resolve results at runtime until the canonical client grows this method.
Please add it to this file so it ships once and every consumer (factory + eval
farm) uses the same implementation.

[extend-request from issue #8 / !20] Canonical client needs `list_issue_comments` The !20 bounce (2026-08-25 22:38) ruled `gitea_saas/gitea_client.py` is owned here by issue #4 (per !23 closing #21) and must not be forked on any other branch — including to add a method that issue. So requesting it here instead of forking: `intake_eval.py` (issue #8, `poll_completed`) calls `self._gitea.list_issue_comments(repo, issue_number)` to capture each closed eval-task issue's final comment as the result summary. The current canonical client (`origin/issue-4`) does not expose it — diffing the two blobs shows exactly this 8-line delta: ``` + def list_issue_comments(self, repo, number): + return self._request("GET", f"{repo}/issues/{number}/comments") ``` Not blocking !20 from review (its real-client request test skips until the method lands here so the gate stays honest), but the eval farm's `poll_completed` cannot resolve results at runtime until the canonical client grows this method. Please add it to this file so it ships once and every consumer (factory + eval farm) uses the same implementation.
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: pbwraith/gitea-saas-core#4
No description provided.