- Status: done
- Date: 2026-06-05
- Specs touched:
docs/specs/NEXT.md(notedmanifest checkshipped under # Developer / app-author surface, and pinned the deferredmalmo catalog scaffolditem with its build trigger).docs/dev/authoring-apps-with-an-agent.md(the agent-authoring prompt now callscheckinstead of hand-eyeballingadmission.go). No schema change.
Follow-up to the lint CLI (manifest-lint-cli.md) and the resolve CLI (catalog-image-footprint.md). manifest lint validates the schema but deliberately does NOT run admission, so the agent-authoring guide had the author re-check admission by hand — read all of internal/admission/admission.go and eyeball the compose against its rules. That manual step is the error-prone one (it produces a manifest that lints green then fails at install) and it re-derives in the model, every run, a check that already exists as a callable Go function. This adds malmo manifest check: lint + the compose admission policy in one pass, so a single green check is the author's "would this actually install?" bar.
cmd/malmo/check.go(new) —check(ctx, admit, manifestPath): runslint(schema + sibling-compose validation), then re-reads the verbatim compose bytes and runs them through the admission policy. Either failure aborts with the underlying error, which already names the field/slug (lint) or the offending service + field (admission).cmd/malmo/main.go— newmanifest check <path>dispatch case wiring the realadmission.Check; usage line and the package doc comment now list all three subcommands (lint = schema only, check = schema + admission, resolve = mutates).- The admission seam.
checktakes the admission step as acomposeCheckerfunc param, mirroringresolve'simageSizer. Production passesadmission.Check(syntax viadocker compose config -q+ structure); tests passadmission.CheckStructure(the daemon-free path admission already exposes) so the unit tests stay hermetic. - Agent-authoring guide rewritten to use
check(docs/dev/authoring-apps-with-an-agent.md) — the prompt no longer tells the agent to pre-readadmission.go/manifest.goto learn the rules (drops ~550 lines of source from each run's context); step 10's two-validator split collapses to onecheckcommand, leaving only the semantic checkscheckcan't make (main_port is internal, every${MALMO_SERVICE_*}has aservices:entry, every touched folder has apermissions.foldersgrant) as a manual checklist.
APP_LIFECYCLE.md# admission policy —check's admission half is exactlyadmission.Check, the same policy the brain enforces at install and catalog CI enforces at publish. No second implementation to drift.APP_MANIFEST.md(schema) —check's lint half ismanifest.Parse, unchanged.checkis non-strict on unknown fields for the same reason lint is — the storage/services blocks still need an eyeball against the spec.NEXT.md# Developer / app-author surface —checkjoins lint/resolve as shipped; the heaviermalmo install --localsurface stays deferred, and the newmalmo catalog scaffolditem is pinned with its trigger (revisit after ~10 hand-authored apps; build only against observed rewrite patterns).CLAUDE.md# Go discipline — the admission seam follows the export-on-second-consumer / consumer-side-interface pattern resolve already established; no new abstraction beyond the one func type the test needs.
checkneeds the Docker daemon (its admission half shells todocker compose config -q), same asresolve. The hermetic unit tests useadmission.CheckStructurevia the seam; the daemon-backed path is covered by runningcheckon a real sample, not in the unit suite. Consequently there is no "valid check" success row in the dispatch test — the same reason there's no "valid resolve" row.- No CI wiring here. Like lint/resolve, the binary is the reusable unit; the catalog-repo CI step that runs
checkover every app on PR lands with the catalog-repo CI (APP_STORE.md), not in this repo. lintstays. It remains the schema-only subcommand for the catalog CI schema-lint step;checkis the author-facing superset. Both share the samelint()function, so there is no schema drift between them.
go test ./cmd/malmo/... green. cmd/malmo/check_test.go (all via the daemon-free admission.CheckStructure seam):
- Real samples:
checkof bothcatalog/whoamiandcatalog/files-demoreturns clean. - Runs admission, not just schema: a schema-valid manifest whose compose declares host
ports:/ a named volume /privileged: truefails, and the error names the admission problem. - Runs lint first: a bad slug fails with the schema error (kebab-case) before admission runs.
- Dispatch:
manifest checkwithout a path returns the usage sentinel (the success path needs the daemon, so it's exercised by runningcheckon a sample, not in the dispatch table).
- Catalog-repo CI step that runs
malmo manifest check(admission + schema) over every app directory on PR (APP_STORE.md# CI on the repo). malmo catalog scaffold --compose <path>— the deterministic Phase-2 rewrite, deferred until ~10 apps are hand-authored against the newcheck-based loop so it's built on observed rewrite patterns (NEXT.md# Developer / app-author surface).