fix(create_project): fast raw create + honest partial_failure (#42, infra#179)
gitea's template-generate is slow-async on this instance (repo not writable for >40s; infra#179) — no synchronous MCP tool can wait that long, and the 60s retry made the call hang until the client timed out. Bound the write-readiness retry to 5s (a healthy gitea commits in ~1s and this still catches it), and when the branch isn't writable in time, return a clear partial_failure: repo created, substitution deferred, finalize locally with `hyperguild new-project`. Substitution logic is intact and completes automatically once generate is fast (infra#179). Tool description updated to describe substitution as best-effort. Refs #42, infra#179. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ func NewCreateProjectFromTemplate(c *gitea.Client, a *allowlist.Allowlist, tmplO
|
|||||||
func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor {
|
func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor {
|
||||||
return registry.ToolDescriptor{
|
return registry.ToolDescriptor{
|
||||||
Name: "create_project_from_template",
|
Name: "create_project_from_template",
|
||||||
Description: "Create a new project repo from a template, substituting placeholders (__PROJECT_NAME__, __MODULE_PATH__) in every file's content AND path (e.g. renaming cmd/__PROJECT_NAME__/) so the result builds. Defaults to the server-configured template; pass template_name to override (e.g. template-go-agent).",
|
Description: "Create a new project repo from a template. Best-effort substitution of placeholders (__PROJECT_NAME__, __MODULE_PATH__) in every file's content AND path (e.g. renaming cmd/__PROJECT_NAME__/): it completes only if the generated branch is promptly writable. If gitea's async generate is slow (infra#179) the repo is still created and partial_failure explains how to finalize locally (`hyperguild new-project`). Check files_substituted and partial_failure. Defaults to the server-configured template; pass template_name to override (e.g. template-go-agent).",
|
||||||
InputSchema: json.RawMessage(`{
|
InputSchema: json.RawMessage(`{
|
||||||
"type":"object",
|
"type":"object",
|
||||||
"properties":{
|
"properties":{
|
||||||
@@ -176,6 +176,21 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If substitution stalled because the generated branch wasn't writable in time,
|
||||||
|
// the repo IS created — say so clearly and point to the local finalize step,
|
||||||
|
// rather than leaking the raw "branch does not exist" (infra#179: gitea's
|
||||||
|
// template-generate is slow-async on this instance, so tool-side substitution
|
||||||
|
// is best-effort).
|
||||||
|
if strings.Contains(result.PartialFailure, "branch does not exist") ||
|
||||||
|
strings.Contains(result.PartialFailure, "not found") {
|
||||||
|
result.PartialFailure = fmt.Sprintf(
|
||||||
|
"repo created, but its branch (%s) was not writable within %ds — gitea's "+
|
||||||
|
"template-generate is slow-async on this instance (infra#179), so substitution "+
|
||||||
|
"is incomplete (%d file(s) done). Finalize locally with `hyperguild new-project` "+
|
||||||
|
"(clone + substitute, no API race). Underlying: %s",
|
||||||
|
branch, substitutionBudget, len(result.FilesSubstituted), result.PartialFailure)
|
||||||
|
}
|
||||||
|
|
||||||
// Fail loud: a scaffold that still holds placeholders does not build. Nothing
|
// Fail loud: a scaffold that still holds placeholders does not build. Nothing
|
||||||
// substituted (with no explicit failure) means the walk found no placeholders —
|
// substituted (with no explicit failure) means the walk found no placeholders —
|
||||||
// suspicious for a real template. Surface it instead of returning silent success.
|
// suspicious for a real template. Surface it instead of returning silent success.
|
||||||
@@ -186,16 +201,20 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
|
|||||||
return textOK(result)
|
return textOK(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// upsertRetry is the readiness gate for the async-generate branch race: gitea's
|
// substitutionBudget bounds how long we retry the first write while the freshly
|
||||||
// /generate returns (and serves reads) before the branch ref is committed, so the
|
// generated branch becomes writable. gitea's /generate returns (and serves reads)
|
||||||
// first writes 404 "branch does not exist" until the initial commit lands (observed
|
// before the branch ref is committed, so writes 404 "branch does not exist" for a
|
||||||
// up to ~30s under load). BranchExists is not a usable signal — it reports the
|
// window. We keep the budget SHORT so the MCP call stays responsive: a healthy
|
||||||
// branch present before writes succeed. So the write itself is the probe: retry on
|
// gitea commits in ~1s and this catches it; a slow one (infra#179, observed >40s)
|
||||||
// the transient not-found until it takes. Once the first write lands, the branch is
|
// fails fast and we defer substitution with clear guidance rather than hang.
|
||||||
// writable and the rest succeed on the first try.
|
const substitutionBudget = 5
|
||||||
|
|
||||||
|
// upsertRetry retries UpsertFile on the transient post-generate "branch does not
|
||||||
|
// exist" not-found, up to substitutionBudget. The write itself is the readiness
|
||||||
|
// probe — BranchExists reports the branch present before writes succeed.
|
||||||
func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name, path string, args gitea.UpsertFileArgs) error {
|
func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name, path string, args gitea.UpsertFileArgs) error {
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < 60; i++ {
|
for i := 0; i < substitutionBudget; i++ {
|
||||||
if _, err = t.c.UpsertFile(ctx, owner, name, path, args); err == nil {
|
if _, err = t.c.UpsertFile(ctx, owner, name, path, args); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user