parallax#1 was the real-world evidence: infra#179's branch-writability stall
(>40s observed) blew past the tool's 5s budget, substitution ran zero files,
and the repo shipped genuinely broken (go.mod still read `module
__MODULE_PATH__`) — recoverable only via a manual clone/sed/git-mv/push.
Adds `resume: bool`. When true, skips template lookup and repo generation
entirely — the destination must already exist — and jumps straight to the
tree-walk substitution. This is safe to re-invoke repeatedly because
substituteEntry already compares against the branch's CURRENT state on every
call (GetTree is re-fetched fresh each time): a file already fixed in a prior
partial pass shows up already-correct or already-renamed and is a no-op. So the
actual blocker to resumability was purely the "destination must NOT exist"
guard on the create path — flipped it for resume, no change needed to the
substitution logic itself.
Consequences of resume existing:
- infra179FinalizeMessage (#46) now points at the concrete recovery — "call
this tool again with resume=true" — instead of manual clone/sed guidance.
- "no placeholders substituted" only loud-fails on a FRESH create; on resume,
finding nothing left to do is the expected steady state (success).
- dispatch_allow injection (#43) is now idempotent (read-before-write, update
with sha if present-but-different, skip if already correct) so a resumed
call with dispatch_allow=true doesn't error re-creating a path that already
exists (#51's root cause).
- #51's other ask: dispatch_allow failures now land in their own
dispatch_allow_failure field, never conflated with substitution's
partial_failure — the two can independently succeed/fail.
Tests: resume with no destination (error, names "nothing to resume"), resume
continuing a partial substitution (asserts /generate is never re-called, only
the still-wrong file is rewritten), resume when already fully done (success,
not the fresh-create loud-fail), dispatch_allow idempotent re-injection
(two-phase test capturing the real written content, no test-visible knowledge
of the internal constant), and dispatch_allow_failure as a distinct field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The infra#179 partial_failure message and the tool descriptor told callers to
"Finalize locally with `hyperguild new-project`" — but that command does not
exist (the hyperguild CLI only has tier/brain/mode; it was specced, never built).
It pointed users at a dead end.
Extracted the message into a pure infra179FinalizeMessage() and reworded it to
name the actual remaining work — cloning the repo and substituting the leftover
__PROJECT_NAME__ / __MODULE_PATH__ placeholders, or retrying — with no reference
to any scaffolding CLI. Descriptor updated to match. Unit-tested the wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gitea host renamed (infra ADR-0004); the mcp-chassis dep already migrated
(3329ff3), so the sequencing gate is clear. `go mod edit -module` + bulk import
rewrite across all .go files. gitea-mcp is a server binary (not an imported
library), so no downstream consumers break. build + task check green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional dispatch_allow bool (default false). When true, inject a
.dispatch-allow file at repo root on the resolved default branch after
substitution, marking the new project dispatch-eligible (dispatch#3)
without a manual follow-up commit.
Rides the existing upsertRetry path so injection inherits the infra#179
branch-readiness / partial_failure handling; a stalled injection degrades
exactly like substitution. Reported in files_substituted. false/omitted
is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
BranchExists returns true before the generated branch is writable, so
waitForBranch didn't help and a 2.5s retry budget was too short (branch became
writable ~30s post-generate under load in live testing). Drop waitForBranch;
let upsertRetry be the gate — retry the write on the transient "branch does not
exist" not-found for up to 60s, early-exit on success. Once the first write
lands the branch is writable and the rest succeed immediately. Refs #42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live e2e surfaced a race unit tests couldn't (mocks are instant): gitea's
/generate returns and serves reads before the branch ref is writable, so the
first content writes 404 "branch does not exist" for a beat — aborting the
whole substitution pass. Add waitForBranch (poll BranchExists after generate)
+ upsertRetry (retry writes on the transient not-found). Test fake now serves
the branch readiness probe. Refs #42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_project_from_template returned files_substituted:null and produced a
non-building scaffold. Three root causes, all fixed:
1. Empty branch: /generate omits default_branch, so every SubstituteFile read
hit an empty ref and 404'd → nothing substituted. Resolve the branch
explicitly (re-fetch the repo; fall back to "main"). The old unit test hid
this by mocking default_branch:"main".
2. Incomplete + rename-incapable: substitution ran over a fixed 6-file list
that missed cmd/__PROJECT_NAME__/main.go and could not rename the
cmd/__PROJECT_NAME__/ directory. Replace with a recursive tree walk:
content-substitute every blob, and for any path carrying a placeholder,
rename it (POST-create new path + delete old).
3. Stale module host: __MODULE_PATH__ used gitea.d-ma.be (the pre-rename host,
which breaks `go mod download` downstream). Use git.d-ma.be.
Also: fail loud — if nothing was substituted, populate partial_failure instead
of returning silent success (the null that started this).
Tests rewritten to drive the tree-walk flow and assert: cmd/ rename (new path
POST + old path delete), git.d-ma.be module substitution, empty-generate-branch
fallback, and the loud-on-nothing path.
Closes#42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The template name was hardcoded into the binary at startup via
NewCreateProjectFromTemplate("mathias", "template-go-web"), so
generating from a different template (e.g. template-go-agent)
required a code change and restart. The constructor already
parameterised it correctly — the gap was at the tool's input
schema, which never exposed template_name to the caller.
Adds an optional template_name input field. When set, it overrides
the server-configured default for that call only; when omitted,
behavior is unchanged. Template owner stays server-configured —
only the repo name is per-call.
Server-side validation already verifies the resolved template
exists and is marked as a template repo, so no enum constraint
is added — keeps the door open for future templates (go-ml,
go-service, ...) without redeploys.
Adds TestCreateProjectTemplateNameOverride verifying the override
directs both the template lookup and the /generate POST.
Closes#24
Generates a new repo from mathias/template-go-web via Gitea's generate
API, then substitutes __PROJECT_NAME__ and __MODULE_PATH__ placeholders
in six known files (best-effort, partial failure surfaced in result).
Validates name regex, allowlist, template flag, and destination
non-existence before generating. Adds Template field to gitea.Repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>