Per the Gitea-as-true-master ADR (infra#34), GitHub mirror is now an
explicit opt-in via mirror_to_github=true. Default (omit / false) provisions
a Gitea repo + staging namespace + experiment-brief issue only — no GitHub
repo, no push-mirror.
Rationale: US cloud providers (Microsoft/GitHub) are subject to CLOUD Act
and NSL. Client code, business logic, and infra-adjacent repos should
never live on US-owned infrastructure. Only open-source projects intended
for public community (hyperguild, gitea-mcp, template-*) should opt in.
Changes
- internal/skills/project/handlers.go
- createArgs gains MirrorToGitHub bool (json:"mirror_to_github,omitempty").
- res.GitHubURL is set only when MirrorToGitHub is true; empty string otherwise.
- Steps 2 (create_github_repo) + 3 (mirror) are wrapped in `if args.MirrorToGitHub`.
- experimentBrief renders "Gitea-only" line by default and the existing
"Push-mirror configured" line only on opt-in.
- internal/skills/project/skill.go
- Tool schema gains mirror_to_github (boolean, default false) with description
spelling out when to opt in. Tool Description updated to reflect new default.
- internal/skills/project/handlers_test.go
- Added mirroredArgs() helper (happyArgs + mirror_to_github:true).
- Tests that exercise the GitHub flow (HappyPath, GitHubExists_Idempotent,
GitHubFails, NoGitHubClient_DegradedMode, Idempotent_RepoExists,
MirrorFails, InfraCommitFails) switched to mirroredArgs.
- Added TestProjectCreate_DefaultSkipsGitHubMirror covering the Gitea-only
path: 3 gitea-mcp calls, zero GitHub calls, empty github_url, reached=
[create_repo, infra_commit, issue], body reflects Gitea-only.
Closes gitea/mathias/hyperguild#17. Moves infra#34 acceptance item
"project_create updated: mirror_to_github defaults to false".
110 lines
4.1 KiB
Go
110 lines
4.1 KiB
Go
// Package project implements the `project_create` MCP tool: a single-call
|
|
// pipeline that creates a Gitea repo from a template, configures push-mirror
|
|
// to GitHub, commits a staging namespace manifest to the infra repo, and
|
|
// opens an experiment-brief issue on the new repo. See hyperguild gitea
|
|
// issue #10 for the design.
|
|
package project
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/mathiasbq/supervisor/internal/githubclient"
|
|
"github.com/mathiasbq/supervisor/internal/mcpclient"
|
|
"github.com/mathiasbq/supervisor/internal/registry"
|
|
)
|
|
|
|
// Config holds the orchestration dependencies for the project skill.
|
|
type Config struct {
|
|
// Client talks to the gitea-mcp server. project_create makes
|
|
// sequential calls (create_project_from_template, repo_mirror_push,
|
|
// file_write_branch, issue_create) through this client.
|
|
Client *mcpclient.Client
|
|
|
|
// GitHub is the client used to create the empty destination repo on
|
|
// GitHub before the push-mirror is configured. Gitea's push-mirror
|
|
// cannot push to a non-existent remote, so this step is mandatory
|
|
// when GitHubPAT is set. Pass nil to skip github repo creation
|
|
// entirely (degraded mode — mirror config will land but the actual
|
|
// sync to github will fail until the repo exists).
|
|
GitHub *githubclient.Client
|
|
|
|
// GiteaOwner is the org/user that owns the new repo and the infra repo
|
|
// the namespace manifest is committed to (typically "mathias").
|
|
GiteaOwner string
|
|
|
|
// GitHubOwner is the GitHub org/user the push-mirror targets
|
|
// (typically "mathiasb").
|
|
GitHubOwner string
|
|
|
|
// GitHubPAT is the personal access token used as the push-mirror
|
|
// password and to create the destination repo on GitHub. Must have
|
|
// `repo` scope. Never logged.
|
|
GitHubPAT string
|
|
|
|
// InfraRepo is the name of the infra repo on Gitea where the
|
|
// k3s/staging/<name>/namespace.yaml manifest gets committed
|
|
// (typically "infra").
|
|
InfraRepo string
|
|
}
|
|
|
|
// Skill exposes project_create as an MCP tool.
|
|
type Skill struct{ cfg Config }
|
|
|
|
// New constructs the project Skill.
|
|
func New(cfg Config) *Skill { return &Skill{cfg: cfg} }
|
|
|
|
// Name returns the skill identifier.
|
|
func (s *Skill) Name() string { return "project" }
|
|
|
|
// Tools returns the MCP tool definitions for this skill.
|
|
func (s *Skill) Tools() []registry.ToolDef {
|
|
schema, _ := json.Marshal(map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{
|
|
"type": "string",
|
|
"pattern": `^[a-z][a-z0-9-]{1,38}[a-z0-9]$`,
|
|
"description": "Lowercase repo name. 3-40 chars, must start with a letter.",
|
|
},
|
|
"description": map[string]any{"type": "string"},
|
|
"hypothesis": map[string]any{"type": "string"},
|
|
"folder": map[string]any{
|
|
"type": "string",
|
|
"description": "Informational only — appears in next_steps. Example: AGENTS, AI, QKX.",
|
|
},
|
|
"stack": map[string]any{
|
|
"type": "string",
|
|
"enum": []string{"go-agent", "go-web"},
|
|
"description": "Selects template-go-agent or template-go-web.",
|
|
},
|
|
"private": map[string]any{"type": "boolean"},
|
|
"mirror_to_github": map[string]any{
|
|
"type": "boolean",
|
|
"description": "Default false. When true, also create an empty GitHub repo " +
|
|
"and configure a push-mirror from Gitea. Opt-in per the Gitea-as-true-master " +
|
|
"ADR — only set true for open-source projects (hyperguild, gitea-mcp, template-*). " +
|
|
"Never set true for client projects, business logic, or personal experiments.",
|
|
},
|
|
},
|
|
"required": []string{"name", "description", "hypothesis", "stack"},
|
|
})
|
|
return []registry.ToolDef{
|
|
{
|
|
Name: "project_create",
|
|
Description: "Bootstrap a new project: Gitea repo from template, staging namespace manifest, " +
|
|
"experiment-brief issue. Optionally mirrors to GitHub when `mirror_to_github: true` " +
|
|
"(default false). Idempotent — re-running with an existing repo returns the existing URLs.",
|
|
InputSchema: schema,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handle dispatches the tool call.
|
|
func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
|
if tool != "project_create" {
|
|
return nil, errUnknownTool(tool)
|
|
}
|
|
return s.handleCreate(ctx, args)
|
|
}
|