// 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//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"}, }, "required": []string{"name", "description", "hypothesis", "stack"}, }) return []registry.ToolDef{ { Name: "project_create", Description: "Bootstrap a new project: Gitea repo from template, GitHub push-mirror, staging namespace manifest, experiment-brief issue. 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) }