Repository navigation
feat: the solution-go service agent — scaffold a solution backend, not a generic Go service - #2
Conversation
Scaffolds, builds and runs a solution backend, so `codefly add solution` writes code and not only the five YAML documents. Builds and vets clean; one test fails — TestThePageSharesEveryHostSingleton, "the shared policy does not seal react to the host's instance" — which is unfinished work, not a regression. Pushed to keep it off a scratchpad that can vanish. Refs #1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four shape tests that were failing each caught a real mismatch between
what the rule says and what the agent or the test did.
Two were the templates' fault:
- the ./Page share policy spelled `react` as a bare key while every sibling
was a quoted specifier, so the entry that seals the host's React instance
read as absent to anything scanning for one. A share key is an exact
module specifier; it is now spelled as one.
- the scaffolded go.mod named obin-ai/core-solutions in a comment, putting
one consumer's private repository into every solution this agent
scaffolds for anyone. The pin floor (v0.0.12, the native projected-token
runtime) is the fact that matters and is all the comment now states.
Two were the tests' own:
- the manifest-declaration rule read a fenced YAML example in
GETTING_STARTED.md as a declaration. Nothing reads Markdown as a
manifest, and that block is deliberately the manifest an author copies
when the service was added outside `add solution`. Fenced blocks now come
out before matching; prose outside a fence, and every line of a template
that is not Markdown, are still held to the rule.
- the module-edge rule allowed only GETTING_STARTED to mention
service-dependencies, which refused consumes_test.go — the template that
READS the manifest in order to refuse an edge, i.e. this very rule
enforced inside the scaffolded solution's own suite. Both are now named,
with what each says, and an exemption that stops mentioning an edge
fails.
The edge check itself was replaced rather than relaxed. It matched every
`- name:` line in the file, so it read the example's `- name: grpc` — an
endpoint of the hop — as a module edge, and would have read a real edge
spelled as a flow mapping as nothing at all. The example is now PARSED and
held to the host's door structurally, the way check_solution_mesh.py reads a
real manifest and for the reason that script gives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every test in this package read a template; none ran what the templates
render. So this adds the test that does — scaffold a solution the way
`codefly add solution` scaffolds one, complete it the way the CLI completes
it, compile it, and run the suite it ships with — and fixes the two defects
it found immediately.
The first stopped the boot. The solution runtime refuses a consumed module
that declares no method ("consumed module %q declares no methods",
passthrough.go), and the scaffolded consumes.go declared one per api.consumes
entry whether or not anyone had composed its methods yet. A solution
scaffolded with `--consumes` therefore could not render its interface
document and could not start: the comment claiming an empty declaration means
"nothing of it is reachable" described an intent the runtime does not share.
consumed() now leaves an uncomposed module out, which says the same thing and
starts.
The second was an invariant the reference solution does not hold. The
scaffolded drift check asserted consumed() equals api.consumes exactly, but
api.consumes names every module a solution reaches ANYWHERE — the page
reaches modules at the authority's edge with their own clients, never through
the backend — and obin-ai/core-solutions' wiki declares six consumed modules
and relays two. The check is split into the two directions that are actually
true: consumed_gen.go must equal api.consumes (that IS the `codefly sync`
drift check, and it is the generated file that must match), while this
backend may relay any subset of it and nothing outside it.
Both states of `codefly add solution` are now covered, with and without
`--consumes`, and both build with the default -mod=readonly and pass their
own gates on a tree no generator has run in. Removing the filter above turns
the new test red, which is how it was checked.
The service manifest the end-to-end test writes is lifted from the scaffolded
GETTING_STARTED.md's own fenced example rather than written in the test, so
what it proves is that the DOCUMENTED manifest is the one that works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agent.codefly.yaml declared `release.workflow: releaser.yml` and .goreleaser.yaml's comment cited a release_test.go. Neither existed: a `v*` tag would have published nothing, so the agent could not be installed at all. The workflow is core's shape, from codefly-dev/service-go-grpc: the suite runs on the tag first and a release waits on it, then goreleaser-cross publishes with Syft mounted for the per-archive SBOMs. CI reuses core's own go-service-ci reusable workflow pinned at the core version go.mod requires, rather than restating its checks here. release_test.go pins the install path against CORE'S OWN registration rather than against a repeated string: resources.AgentKindRegistrationFor gives the executable name every build must produce and GitHubAsset gives the archive core fetches, and the GoReleaser template is resolved for a concrete platform and compared to it. A rename on either side now fails here instead of publishing an agent that downloads and cannot be found. It also pins the self-hosting choice, which is kept. `source.agent: self` is the CLI's exact sentinel (cmd/agents/build.go isSelfHostedSourcePackager) and it imposes four preconditions before publish — a declared source.bootstrap, a semver version, and a publisher and name that are each one path component — all four now checked in `go test` rather than at a publish that has already tagged. bootstrap-agent is held to the three variables the publisher states the target with (CODEFLY_AGENT_OUTPUT, _OS, _ARCH) and to CGO, which core's tree-sitter source inspection cannot ship without. Verified against the CLI at HEAD rather than assumed: self-hosting is supported, and naming `codefly.dev/go:<v>` instead would make every release of this agent wait on a bump of that one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The templates were hand-written. That is the wrong authoring model for a
service agent and it is why the two defects in the previous commit existed at
all: a template cannot be compiled, vetted or tested, so a scaffold that does
not build looks exactly like one that does.
Every service agent holds a REAL service in base/ and generates
templates/factory from it with core's generator.GenerateServiceTemplate
(`codefly agent generate --service .`), which copies base/, adds a .tmpl
suffix and applies the literal replacements in
base/service.generation.codefly.yaml. codefly-dev/service-go-grpc is the
proof. This repository now does the same.
base/ is a real solution backend called `codefly-base`: it compiles with the
default -mod=readonly, it vets, and ITS OWN SUITE IS THE SUITE EVERY
SCAFFOLDED SOLUTION SHIPS WITH. It is staged into the layout a real solution
has — <root>/services/backend beside <root>/solution.codefly.yaml — because
that is where its consumes_test.go really finds the manifest, at ../../, which
from <repo>/base would be outside this repository.
It deliberately consumes one module and composes no methods for it. That is
the state that was broken: the runtime refuses a consumed module declaring no
method, so base/ is permanently in the shape whose regression would be caught.
templates/factory is now generated, and the three replacements reproduced the
hand-written tree byte for byte except where the move was a deliberate
simplification:
- {{ .Module.Name.LowerCase }} became {{ .SolutionID }}, the same value.
- {{ .InterfaceDescription }} became the prose it rendered to, so the
description lives once, in base/openapi.go, instead of in a Go function
the template referred to.
- {{ .Service.Name.LowerCase }} became `backend`. A solution's entry service
is named `backend` by the shape's definition (the CLI's
solutionEntryService and this agent's backendService are that constant),
the scaffolded go.mod already hard-codes the `/backend` module suffix the
compiled binary is named after, and the word appears in prose on nearly
every line, which a whole-file substitution would corrupt.
The five files base/ needs to be a real service but that must never be
scaffolded are listed under `ignores`: the three documents the CLI owns
(service.codefly.yaml, solution.frontend.codefly.yaml) plus the two the agent
renders itself (consumed_gen.go, openapi/api.swagger.json). A template for any
of them would be a second mechanism for one file. Core's own generator has a
hardcoded exclusion for service.codefly.yaml, but it compares a full path and
never fires, so the list is what actually holds.
The drift check calls core's generator directly rather than shelling out to
the CLI, so it needs no second binary and holds the templates to the exact
code `codefly agent generate` runs. It compares whole trees in both
directions, because regeneration overwrites a file but never deletes one — a
template whose source left base/ would otherwise be scaffolded forever. Both
directions were checked: hand-editing a template fails it, and editing base/
then regenerating carries the change through.
`go test -update` is one flag for base/'s derived files and the templates
together, so the two cannot be refreshed one without the other.
AGENTS.md states the model, because the mistake this commit undoes is one a
reader makes by default.
templates/page stays hand-written and says why, in AGENTS.md and at main.go's
pageFS: GenerateServiceTemplate produces templates/factory from base/ and
nothing else, and the page is written to a SIBLING of the service directory,
so there is no base/ subdirectory it could come from.
29 tests pass, 0 skipped. A scaffolded solution still passes core-solutions'
check-boundaries, check-consumed-apis and check-solution-mesh at exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Re-authored on You were right about the cause, not just the convention. The two defects in the previous commit (a solution that consumed anything could not boot) existed because the templates were hand-written. A template cannot be compiled, so a scaffold that does not build looks exactly like one that does. What
|
| source | main.go, consumes.go, operations.go, openapi.go |
| its own suite | consumes_test.go, openapi_test.go |
| module | go.mod, go.sum (tidy as committed), .gitignore |
| docs | GETTING_STARTED.md |
| ignored — the CLI's | service.codefly.yaml, solution.frontend.codefly.yaml |
| ignored — the agent renders them | consumed_gen.go, openapi/api.swagger.json |
| ignored — the config | service.generation.codefly.yaml |
It is a real solution called codefly-base: builds with default -mod=readonly, vets, and its suite is the suite every scaffolded solution ships with. It deliberately consumes one module and composes no methods for it — precisely the state that was broken — so base/ now sits permanently in the shape whose regression gets caught.
Two things worth flagging because they were not obvious from the brief:
base/cannot be tested where it sits.consumes_test.goreads the solution manifest at../../solution.codefly.yaml, which from<repo>/baseis outside the repository. It is staged into<root>/services/backendbeside<root>/solution.codefly.yaml— the layout a real solution has. Not a workaround;../../is the shipped path and a test that reached elsewhere would not be checking it.- Core's hardcoded
service.codefly.yamlexclusion never fires.visitor.Keepcomparesfile == "service.codefly.yaml"against a full path. Theignoreslist is what actually holds, which is why all five excluded files are listed explicitly. Worth knowing for the other agents.
Regenerate-is-clean: passes, and has teeth both ways
TestFactoryTemplatesAreGeneratedFromBase calls generator.GenerateServiceTemplate directly rather than shelling out to the CLI, so the check needs no second binary and holds the templates to the exact code codefly agent generate runs. It compares whole trees in both directions, since regeneration overwrites but never deletes — a template whose source left base/ would otherwise be scaffolded forever. Checked by hand:
- hand-edit a template → fails, naming the file and saying to edit
base/; - edit
base/, regenerate → change reaches the template.
The three replacements reproduced the hand-written tree byte for byte except three deliberate simplifications: {{ .Module.Name.LowerCase }} → {{ .SolutionID }} (same value), {{ .InterfaceDescription }} → the prose it rendered to (so the description lives once), and {{ .Service.Name.LowerCase }} → backend — the entry service is backend by the shape's definition, the scaffolded go.mod already hard-codes the /backend suffix the binary is named after, and the word is in prose on nearly every line where a whole-file substitution would corrupt it.
go test -update is one flag for base/'s derived files and the templates, so they cannot be refreshed one without the other. AGENTS.md states the model, since hand-editing a template is the default mistake.
templates/page/ — kept hand-written, and here is why
Structural first: GenerateServiceTemplate produces templates/factory from base/ and nothing else, and the page is written to a sibling of the service directory (services/fe-remote), not into it. Under base/ it would be scaffolded inside the backend.
Second: the whole benefit of a base/ is that it compiles and is tested, and that needs npm install — the five @codefly-dev kits are on GitHub Packages behind a read:packages token this environment lacks. A base-page/ generated the same way is the right shape once CI holds that token and npm test would prove something; until then it is a second generator for no verification. Say the word if you want it anyway.
What holds that tree instead is TestThePageSharesEveryHostSingleton pinning every share-policy entry. That is weaker than a compile and is listed under what I did not verify.
State
29 tests, 0 skipped. go build / go vet / gofmt -l clean. A scaffolded solution still passes check-boundaries, check-consumed-apis and check-solution-mesh at exit 0 against a clone of obin-ai/core-solutions, re-run after regeneration.
Everything else in the body stands, including the one tradeoff I would still like a second opinion on: a module in api.consumes that nobody composes reaches nothing silently.
Running `codefly add solution --agent solution-go` against a real CLI built
from codefly-dev/cli main found three things no test here could see, because
every test rendered this agent's templates with this agent's own context.
1. THE AGENT README KILLED `add solution`. templates/agent/README.md.tmpl
opened with `{{ .Service.Name.Title }}`, and `GetAgentInformation` is
called BEFORE any service exists — core has run only the agent's Init, so
Information carries the agent and nothing else. The call failed with "nil
pointer evaluating *resources.ServiceWithCase.Name" and the command died
with the module already written. The README describes the AGENT, so it now
takes no values at all.
2. GETTING_STARTED IS RENDERED BY CORE, with the same bare Information
(base_builder.go, creation mode). The base/ restructure had replaced
`codefly-base` with `{{ .SolutionID }}`, a field Information does not
carry: "can't evaluate field SolutionID in type *services.Information",
again after the module was written. The replacement is now
`{{ .Module.Name.LowerCase }}` — the same value, and a field BOTH contexts
carry, since the scaffold context embeds Information.
3. THE END-TO-END TEST MODELLED THE WRONG ORDER. It wrote
solution.codefly.yaml before Create; `add solution` writes it after. With
`--consumes`, a freshly scaffolded solution therefore has an empty
consumed_gen.go and its drift check fails until `codefly sync` runs. That
failure is TRUE — the backend really does relay nothing its manifest
declares — so the test now pins it: exactly one test may fail, it must be
that one, and it must name the command that fixes it. Then it runs the
agent's Sync and requires the tree green.
advertisement_test.go renders both core-rendered templates the way core
renders them. TestGeneratedTemplatesRenderWithTheScaffoldContext now EXECUTES
each replacement against the scaffold context instead of comparing it to a
list of fields that happen to exist today.
Verified end to end: `codefly add solution notes --agent solution-go
--consumes connect:documents/documents/grpc` scaffolds, `codefly doctor
solution --dir` reports "conforms to the solution shape" at exit 0, and all
three core-solutions gates pass at exit 0 on that tree, with the runtime-pin
gate confirmed to still have teeth on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
A lazybox agent is working on this. claim detail |
Closes #1.
The
solution-goservice agent:codefly add solution --agent solution-gonowscaffolds a solution backend — the
solution-runtime-gopin,consumes.gogenerated from the sibling manifest's
api.consumes, the/healthendpoint'shandler, and the
./Page/fe-remotenpm package — instead of a plain Goservice that knows none of it.
It is a
codefly:serviceagent and nothing else.services.Adddrives Create,so there is no core change and no new wire protocol, and no
codefly:solutionexecutor: that option was weighed and declined in cli#1215.The CLI half shipped as cli#1216 and needs no further change.
Classification: fix, not hack
Every change here is a fix. Nothing is worked around, no gate is weakened, and
no check was relaxed to make something pass.
Three further defects were found by running the real CLI (their own section
below) and all three are fixed at the cause: a README that reached for a value
its caller does not have, a replacement spelled in a field core cannot fill, and
a test that modelled an order the product does not run in. The last was fixed by
making the test state the truth, not by making the truth quieter.
The branch arrived with a build and vet that were clean and four failing
shape tests (the handover named one). Each was a real mismatch between a stated
rule and what the code did:
rsbuild.config.ts.tmplreactwas a bare key where every sibling share key was a quoted specifier, so the entry sealing the host's React instance read as absentgo.mod.tmplobin-ai/core-solutionsin a comment, putting one consumer's private repository into every solution scaffolded for anyoneshape_test.goshape_test.goservice-dependencies, which refusedconsumes_test.go— the template that reads the manifest in order to refuse an edge, i.e. this very rule enforced in the scaffolded solution's own suiteThe module-edge check was replaced rather than relaxed, and is now stronger.
It matched every
- name:line in the file, so it read the example's- name: grpc— an endpoint of the hop — as a module edge, and would have reada real edge spelled as a flow mapping as nothing at all. The example is now
parsed and held to the host's door structurally, which is how
check_solution_mesh.pyreads a real manifest and for the reason that script'sown docstring gives.
The two defects nothing was testing
Every test in the package read a template; none ran what the templates render.
So this adds the test that does — scaffold as
codefly add solutionscaffolds,complete as the CLI completes, compile, and run the suite the tree ships with —
and it found two defects immediately. A solution that consumed anything was
born red, and could not boot.
The runtime refuses a consumed module that declares no method
(
passthrough.go:consumed module %q declares no methods), and thescaffolded
consumes.godeclared one perapi.consumesentry whether or notanyone had composed its methods. A solution scaffolded with
--consumescould not render its interface document and could not start. The comment
claiming an empty declaration means "nothing of it is reachable" described an
intent the runtime does not share.
consumed()now leaves an uncomposedmodule out, which says the same thing and starts.
The scaffolded drift check asserted an invariant the reference solution
does not hold. It required
consumed()to equalapi.consumesexactly,but
api.consumesnames every module a solution reaches anywhere — thepage reaches modules at the authority's edge with their own clients, never
through the backend — and
solutions/wikideclares six consumed modules andrelays two. Split into the two directions that are true:
consumed_gen.gomust equal
api.consumes(that is thecodefly syncdrift check, and itis the generated file that must match), while the backend may relay any
subset of it and nothing outside it.
Removing the filter in (1) turns the new test red, which is how it was checked.
What running the real CLI found
The agent was installed into a scratch Codefly home as
solution-go__0.0.1, acodeflywas built fromcodefly-dev/climain (f7bfa66), andwas run against a real workspace. It found three things no test in this
package could see, because every one of them rendered this agent's templates
with this agent's own context. All three are fixed in
1f4b021.1. The agent's README killed
add solutionoutrighttemplates/agent/README.md.tmplopened with{{ .Service.Name.Title }}. ButGetAgentInformationis whatcodefly add servicecalls to print what theagent scaffolds, and at that moment nothing has been created: core has run
only the agent's
Init, sos.Informationcarries the agent andServiceandModuleare nil. The README describes the agent, so it now takes no valuesat all — there is nothing left to be nil. (
service-go-grpcguards with{{if .Service}}; prose is simpler and says the same.)2.
GETTING_STARTED.mdis rendered by CORE, with the same bare contextcore/agents/services/base_builder.gorenderstemplates/factory/GETTING_STARTED.mditself at builder Load in creation mode,with
s.Information. So that one template is constrained to the fieldsInformationcarries — and thebase/restructure had replacedcodefly-basewith
{{ .SolutionID }}, which it does not. This was a regression therestructure introduced: the hand-written template had used
{{ .Module.Name.LowerCase }}, and collapsing every spelling of the solutionid onto one replacement lost the distinction.
The replacement is back to
{{ .Module.Name.LowerCase }}— the same value, anda field both contexts carry, since the scaffold context embeds
*services.Information. One spelling, renderable by either caller, noper-file exception.
Both failures landed after
add modulehad already written the module,leaving the half-built tree that command's design goes out of its way to avoid.
advertisement_test.gonow renders both core-rendered templates exactly the waycore renders them. Reverting either fix reproduces the exact error above in
go test.3. The end-to-end test modelled the wrong order
It wrote
solution.codefly.yamlbeforeCreate.codefly add solutionwrites it after: it scaffolds both services through the agents' own Create
flows and only then writes the solution's documents. So the tree the test
proved was one no user ever has.
Run in the real order, a solution scaffolded with
--consumeshas an emptyconsumed_gen.goand one failing test:That failure is true, and the agent cannot avoid it:
api.consumesdoes notexist when
Createruns (builder.gosays so), and untilcodefly synchasrun the backend really does relay nothing its solution declares. The check
saying so is the entire reason it exists.
So it is pinned rather than tolerated. The test now requires that exactly
one test fails, that it is that one, and that its message carries the command
that fixes it — then runs the agent's own
Syncand requires the tree green.If
add solutionever syncs before it returns, this fails too, which is themoment to delete the branch.
codefly sync service notes/backendcould not be run here for real: it resolvesthe authority interface first, and a scratch workspace composes no
implementation of it. The agent's
Syncentry point is exercised ingo test.Reuse
Nothing solution-specific is reimplemented:
Runtime, Code, Tooling and the Builder's Load/Init/Build/Deploy are
codefly-dev/service-go's, imported wholesale. This agent adds onlyCreateand
Sync. That is why there is noruntime.gohere, unlikeservice-go-grpc— a solution backend is an ordinary Go program to acompiler and a process manager.
The interface document is rendered by
solution-runtime-go's ownrenderer, not a hand-rolled OpenAPI writer, so the bytes Create commits are
the bytes the scaffolded
go generate ./...writes.TestInterfaceArtifactIsCurrentpassing on a tree no generator has run in isthat, verified.
Consumption is read with
core's ownmanifest.LoadConsumption— amanifest core refuses is one this agent refuses.
release_test.gopins the install path againstcore's ownAgentKindRegistrationFor/GitHubAsset/ExecutableNamerather than arepeated string, so a rename on either side fails in
go test.CI reuses
codefly-dev/core'sgo-service-ci.yml, pinned at the coreversion
go.modrequires, rather than restating its checks.The release workflow is
service-go-grpc's shape (suite on the tag first,then goreleaser-cross with Syft mounted for per-archive SBOMs).
The authoring model is
service-go-grpc's:base/is hand-written andtemplates/factory/is generated from it by core'sgenerator.GenerateServiceTemplate. The drift check calls that functiondirectly rather than shelling out to the CLI.
Authoring model:
base/, not hand-written templatesThe templates were hand-written when this branch was handed over. That is the
wrong model for a service agent, and it is why the two defects above existed:
a template cannot be compiled, vetted or tested, so a scaffold that does not
build looks exactly like one that does.
base/is now the hand-written artifact andtemplates/factory/is generatedfrom it —
codefly agent generate --service ., i.e. core'sgenerator.GenerateServiceTemplate.base/is a real solution backend calledcodefly-base— 15 files, ofwhich 10 generate templates:
main.go,consumes.go,operations.go,openapi.goconsumes_test.go,openapi_test.gogo.mod,go.sum(tidy as committed),.gitignoreGETTING_STARTED.mdservice.codefly.yaml,solution.frontend.codefly.yamlconsumed_gen.go,openapi/api.swagger.jsonservice.generation.codefly.yamlIt compiles with the default
-mod=readonly, it vets, and its own suite isthe suite every scaffolded solution ships with, so what this agent scaffolds
is known-good before anyone scaffolds it. It is staged into the layout a real
solution has (
<root>/services/backendbeside<root>/solution.codefly.yaml)because that is where its
consumes_test.goreally finds the manifest, at../../— which from<repo>/baseis outside the repository.It deliberately consumes one module and composes no methods for it: that is
exactly the state that was broken, so
base/now sits permanently in the shapewhose regression would be caught.
The five ignored files are the ones
base/needs to be a real service but thatmust never be scaffolded. A template for any would be a second mechanism for
one file. Note core's generator has a hardcoded exclusion for
service.codefly.yaml, but it compares a full path and so never fires — theignoreslist is what actually holds.The three replacements reproduced the tree byte for byte
Except where the move was a deliberate simplification:
{{ .Module.Name.LowerCase }}kept as is. It was briefly{{ .SolutionID }}— the same value, and the wrong spelling; see What running the real CLI
found below.
{{ .InterfaceDescription }}→ the prose it rendered to, so the descriptionlives once in
base/openapi.gorather than in a Go function a templatereferred to.
{{ .Service.Name.LowerCase }}→backend. A solution's entry service isnamed
backendby the shape's definition (the CLI'ssolutionEntryServiceand this agent's
backendServiceare that constant), the scaffoldedgo.modalready hard-codes the
/backendsuffix the compiled binary is named after,and the word appears in prose on nearly every line — a whole-file
substitution would corrupt it.
The regenerate-is-clean test passes
TestFactoryTemplatesAreGeneratedFromBasecompares whole trees in bothdirections, because regeneration overwrites a file but never deletes one — a
template whose source left
base/would otherwise go on being scaffoldedforever. Both directions were checked by hand:
base/;base/then regenerating → the change reaches the template.TestGeneratedTemplatesCarryNoBaseSolutionNameadditionally fails if any ofthe four spellings of
codefly-basesurvives into a template, which is whatcatches a new capitalisation introduced in
base/that no replacement covers.go test -updateis one flag forbase/'s derived files and the templatestogether, so the two cannot be refreshed one without the other.
AGENTS.mdstates all of this, because hand-editing a template is the mistakea reader makes by default.
templates/page/— the judgement callIt stays hand-written, and says so in
AGENTS.mdand atmain.go'spageFS. Two reasons, the first structural:GenerateServiceTemplateproducestemplates/factoryfrombase/andnothing else, and the page is written to a sibling of the service
directory (
services/fe-remote), not into it — so there is nobase/subdirectory it could come from. Putting it under
base/would scaffold itinside the backend.
base/is that it compiles and is tested, and thatneeds
npm install— the five@codefly-devkits are on GitHub Packagesbehind a
read:packagestoken this environment has none of. Until then abase-page/would be a second generator for no verification.The condition that reverses this, stated so the next reader does not have to
re-derive it: reason 2 goes away the moment CI holds a
read:packagestoken. At that point the page would compile and be tested the way
base/is,and a
base-page/becomes worth building — rendered by this agent into thesibling directory rather than by
GenerateServiceTemplate, because reason 1(the structural one) never goes away. Reason 1 settles the question on its own
today; reason 2 is only what makes the cost not worth paying yet.
What holds the tree instead is
TestThePageSharesEveryHostSingleton, whichpins every entry of the Module Federation share policy — the part whose failure
is invisible until a browser loads the remote inside a real host. That is
weaker than a compile, and it is listed under what I did not verify.
Release path: declared but absent
agent.codefly.yamldeclaredrelease.workflow: releaser.ymland.goreleaser.yaml's comment cited arelease_test.go. Neither existed — av*tag would have published nothing and the agent could not be installed atall. Both added.
Self-hosting is kept, and now verified rather than assumed.
source.agent: selfis the CLI's exact sentinel (cmd/agents/build.goisSelfHostedSourcePackager), and it imposes four preconditions before publish:a declared
source.bootstrap, a semver version, and a publisher and name thatare each a single path component. All four are now checked in
go testratherthan at a publish that has already tagged, and
bootstrap-agentis held to thethree variables the publisher states the target with
(
CODEFLY_AGENT_OUTPUT/_OS/_ARCH) and to CGO, without which core'stree-sitter source inspection cannot ship. Naming
codefly.dev/go:<v>insteadwould make every release of this agent wait on a bump of that one.
Verified
GOWORK=off go build ./...,go vet ./...,gofmt -lclean; 32 tests pass(34 with subtests), 0 skipped.
base/builds, vets and passes its own suite(
TestBaseCompilesAndPassesItsOwnSuite), and is held to the agent's owngenerator (
consumed_gen.go), the runtime's own renderer(
openapi/api.swagger.json), the runtime pinscaffold.godeclares, and thehost's-door-only manifest shape.
A scaffolded solution, in both states of
codefly add solution(with--consumesand without):-mod=readonly— the generatedgo.mod/go.sumaretidy as generated;
one true exception above: with
--consumes, the consumption drift checkfails until
codefly syncruns, and passes once the agent'sSynchas;.gitignore.The solution the real CLI scaffolded was then copied into a clone of
obin-ai/core-solutionsatsolutions/notes/, beside the eight that livethere. All three gates pass at exit 0, and each was confirmed to have teeth
on that exact tree rather than to be passing vacuously:
check-boundaries.shcheck-consumed-apis.shsolutions/notes: 1 delivered binding(s); no direct module address requiredcheck-solution-mesh.shdocumentsedge added → FAILED ondocuments/documents/grpc(SP-SOL-04)The runtime-pin check is live, since the
services/backend/go.modit needs isone this agent creates. The same edge also fails locally in the scaffolded
go test(TestNoModuleEdgeIsDeclared), which is the point — the author isstanding there, not at the consumer repository's gate.
Also checked against the sources rather than assumed: the GETTING_STARTED
example matches what
cmd/add/solution.goactually writes(
solutionEntryDependencies,solutionEntryEndpoint,authorityInterfaceRange— identical interface range, hop-as-
external, and/healthprobe), and theend-to-end test lifts the service manifest from that rendered example, so a
documented manifest that drifts from the CLI's fails the test.
codefly doctor solution— the checker that judges the shapeThe solution the CLI scaffolded above, judged by the verb that owns the shape
(cli#1218, plus #1221's permission rules), on a
codeflybuilt from cli main:Conforms, exit 0. Both warnings are about the CLI's half of the scaffold,
not the backend:
consumed_module_without_bindingismodule.contract.codefly.yaml, whichadd solutionwrites with no bindings — correctly, since whether a call ismade on a person's behalf is the author's to say. It is also, usefully, the
contract half of the gap recorded below.
page_shell_has_no_applooks like a false positive in the checker, not adefect in the tree: it stats
services/web/package.json, and thenextjsagent puts the app one level down, at
services/web/code/package.json(
next.config.ts,package-lock.jsonandsrc/are all there). Everysolution scaffolded by
add solutionwill carry this warning. Reported hererather than filed — no issues opened.
Each of the backend's own rules passed:
module_identity_missing,module_edge_declared,entry_endpoint_missing,entry_endpoint_not_exported,frontend_declaration_missing,owner_aware_missing,module_package_publishes_shelland the rest.What I did NOT verify
Stated plainly, because none of it is covered above:
./Pageremote is never built or run, and is the one tree with nobase/behind it (see the judgement call above).npm installneeds aread:packagestoken for the five@codefly-dev/*packages on GitHubPackages, which this environment has none. Every public pin was confirmed to
resolve to a published version, but
rsbuild build,vitest runandtsc --noEmitwere not executed, and the Module Federation share policy ispinned only by
TestThePageSharesEveryHostSingletonreading the config — awrong value in a correctly-named entry would still pass.
tested, and judged by
doctor solutionand the three gates — but./backendwas never started, nothing listened on
/health, and no page ever called it.Everything about the running process is read from
solution-runtime-go'ssource, not observed: the boot-time refusals (an empty relay slot, a
composition naming no authority), the readiness answer, the relay to the hop.
codefly syncwas not run end to end. It resolves the authorityinterface before it dispatches, and a scratch workspace composes no
implementation of one. The agent's
Syncis exercised ingo test, with thesame call
sync servicemakes.add solutionrun needed a one-line local patch to the CLI to getpast a stale-workspace bug on cli main (below). The patch is in a throwaway
clone, touches nothing in this repository, and is not part of this PR —
but it does mean the command as published does not yet complete.
templates/builderandtemplates/deployment(Dockerfile, kustomize base and overlays) areunexercised: no image was built and no manifest applied. The boot-time
refusals described in comments — an empty relay slot, a composition naming no
authority — were read in
solution-runtime-go, not observed.composes no methods, because a real method name is a constant of a module's
generated Go client and no module client is available here. That the filter in
defect (1) does not hide a genuine composition is reasoned, not run.
endtoend_test.gois not hermetic — it fetches the scaffolded module'sdependencies.
go.sumwas confirmed complete (the-mod=readonlybuildpasses), so CI needs only its normal network access.
Known gap, recorded rather than fixed here
A module in
api.consumesthat nobody composes methods for reaches nothingsilently — the backend starts, the page's call is simply not relayed, and no
test fails. That is what keeps generation green with
--consumes, whichcore-solutions' own recipe requires ("green on every gate at generation"); the
alternative is a freshly scaffolded tree that is red until the author writes Go.
It does not belong in this agent, and no second checker was built for it.
Whether a module is composed is the workspace's fact, and the solution
manifest and the composition are different documents in different places —
neither this agent nor the solution's own directory can see both. It belongs in
codefly doctor solution(cli#1218), which already runs against a workspacewhen invoked without
--dir, and which already warns on the adjacentcontract-side case (
consumed_module_without_binding, above). Noted by theowner; no issue opened.
Two CLI findings, reported not filed
Both were hit while running the verification above, both are in
codefly-dev/climain rather than here, and no issue was opened for either:add solutioncannot complete on cli main —cannot find module <notes> in <notes-ws>, present: [].addSolutioncaptures the workspace beforeaddModulewrites the module and reuses that snapshot at step 3;common.LoadWorkspacereads from disk each call, so the snapshot is stale.Reproduced identically with the stock
goagent, so it is not this agent.To finish the verification I reloaded the workspace in a local scratch
build only — one
common.LoadWorkspacecall, never committed, in athrowaway clone.
doctor solution'spage_shell_has_no_app, described above.No issues were opened.
🤖 Generated with Claude Code