Files
hyperguild/internal/skills/project/skill.go
Mathias 6f1cb53295
All checks were successful
CI / Lint / Test / Vet (push) Successful in 11s
CI / Mirror to GitHub (push) Has been skipped
feat(project_create): mirror_to_github opt-in, default false (infra#34 ADR)
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".
2026-05-20 08:35:02 +02:00

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)
}