/plugin.yaml` whose every field is derived from the plugin's own sources: id and requires from plugin.go, module from go.mod, package from the directory's import path, framework and plugin minimums from go.mod require lines, name and description from the README's H1 and one-sentence summary; plugin.go and party.Plugin are unchanged (D-01, D-02, D-05)"
+ - "The ID reader follows string constants statically: a literal, a package-level const in the same package, a const in another package of the same module, and a const in another module resolved through `go list -m -json`; no plugin code is compiled or run, and any other expression is an error naming ID() or Requires() (D-02 as extended by the user on 2026-10-29)"
+ - "`plugin:add` registers a plugin whose ID() returns a constant, because pluginIDFromGo now calls the same resolver (fix for research F1; literal-ID plugins register exactly as before)"
+ - "`--name` and `--description` persist in an `overrides:` block, survive regeneration, and `--name=\"\"` clears the override; a README whose H1 equals the plugin ID and no name override is an error that names `--name` (D-03, research F7)"
+ - "`summer plugin:manifest --check [dir]` exits non-zero and prints what it expected when the committed plugin.yaml is missing or stale, and `kiosk.CheckT(t, dir)` fails a plugin repo's `go test` the same way (D-04)"
+ - "Version constraints are Go-style minimums: `framework: vX.Y.Z` and `requires: {id: vX.Y.Z}`; a plugin requirement at v0.0.0 or a missing framework require is an error, a pseudo-version minimum is accepted with a warning, and a required ID whose module is not in go.mod has the empty minimum (D-05, research F8)"
+ - "`summer build` bakes `PluginInfo` (ID, module, package, name, description, version, source per plugin) and `FrameworkVersion` into plugins.gen.go, and the generated main publishes them with party.Describe before party.Activate; a plugin without plugin.yaml has only its ID and version, and a metadata failure warns and never fails the build (D-06)"
+ - "Versions are D-07's: a module-cache plugin reports its go.mod/build-info version, a plugin directory that is its own git repository reports `git describe --tags --match 'v[0-9]*'` without the dirty marker, an in-app plugin reports an empty version, and FrameworkVersion is the effective framework version (the newest tag reachable in a locally replaced framework checkout, otherwise the go.mod require) (D-07, research F3)"
+ - "The registry v1 wire types (kiosk.RegistryPlugin, kiosk.RegistryPluginDetail, kiosk.RegistryVersion, envelopes and error) match the shape confirmed at Task 1, so the summercms.io server (34-03), the CLI (34-04) and cabana (34-05) encode and decode the same JSON (D-16)"
+ - "Every new identifier is documented in modules/kiosk/README.md, modules/party/README.md and docs/plugins/marketplace.md, the root README table lists kiosk, and the docs checker passes"
+ artifacts:
+ - path: "modules/kiosk/manifest.go"
+ provides: "plugin.yaml types, Parse, Marshal, Validate"
+ contains: "SchemaVersion"
+ - path: "modules/kiosk/identity.go"
+ provides: "const-resolving ID()/Requires() reader"
+ contains: "func ReadIdentity("
+ - path: "modules/kiosk/check.go"
+ provides: "Write, Check, StaleError, CheckT"
+ contains: "func CheckT("
+ - path: "modules/kiosk/registry.go"
+ provides: "registry v1 wire types"
+ contains: "RegistryAPIPath"
+ - path: "modules/kiosk/version.go"
+ provides: "strict vX.Y.Z rules"
+ contains: "func CompareVersions("
+ - path: "modules/party/info.go"
+ provides: "party.Info, party.Installed, party.Describe"
+ contains: "func Describe("
+ - path: "internal/build/plugindirs.go"
+ provides: "go list plugin dir resolver, git describe, EffectiveFramework"
+ contains: "func EffectiveFramework("
+ - path: "modules/kiosk/manifest_smoke_test.go"
+ provides: "TestManifestSmoke (tracer)"
+ contains: "func TestManifestSmoke("
+ - path: "internal/build/plugininfo_smoke_test.go"
+ provides: "TestPluginInfoSmoke"
+ contains: "func TestPluginInfoSmoke("
+ - path: "docs/plugins/marketplace.md"
+ provides: "plugin.yaml, plugin:manifest, registry API reference"
+ contains: "plugin:manifest"
+ key_links:
+ - from: "modules/kiosk/identity.go"
+ to: "internal/build/scaffold.go"
+ via: "pluginIDFromGo delegates to kiosk.ReadIdentity, so plugin:add and plugin:manifest read IDs the same way"
+ pattern: "kiosk\\.ReadIdentity"
+ - from: "internal/build/build.go"
+ to: "modules/party/info.go"
+ via: "generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before party.Activate"
+ pattern: "party\\.Describe\\("
+ - from: "cmd/summer/main.go"
+ to: "modules/kiosk/check.go"
+ via: "plugin:manifest runs kiosk.Write, kiosk.Check or kiosk.Generate"
+ pattern: "kiosk\\.(Write|Check|Generate)\\("
+---
+
+## Phase Goal
+
+People browse the public Golem15 plugins at summercms.io/marketplace and add one to an application with `summer plugin:install `; the same command installs private plugins from a git URL; the admin SPA gains a read-only Plugins screen. This plan lays the shared foundation every other plan of Phase 34 builds on: the plugin.yaml format and its generator, the registry v1 contract in Go, and the build-time metadata the admin's Installed tab reads.
+
+Spec-less probe fallback skipped: Phase 34 has no requirement IDs and no SPEC.md, so this plan set derives must_haves from ROADMAP success criteria SC-1..SC-5 and CONTEXT decisions D-01..D-19 (a visible choice, not an omission).
+
+
+Create the public framework module `kiosk` (name confirmed at Task 1) and wire it through the CLI and the build:
+- the plugin.yaml format, generated by `summer plugin:manifest [dir] [--check] [--pretend] [--name] [--description]` from the plugin's own sources (D-01..D-05);
+- a static, const-following ID()/Requires() reader shared with `plugin:add` (user decision 1 of 2026-10-29);
+- `kiosk.CheckT`, the helper each plugin repo's `go test` calls (user decision 2);
+- the registry v1 wire types and strict version rules (D-16, confirmed at Task 1);
+- `summer build` baking `PluginInfo` and `FrameworkVersion` into plugins.gen.go and publishing them through `party.Describe` (D-06, D-07).
+
+Purpose: plans 34-02 (plugin repo rollout), 34-03 (registry server), 34-04 (install CLI) and 34-05 (admin screen) all consume these types; defining them once keeps server, CLI and admin on one contract.
+
+Output: commits in this repository only (code and its docs together, per commit group below). Nothing is pushed or tagged.
+
+**Planner decisions recorded here (confirmed or adjusted at Task 1):**
+- **P34-01 plugin.yaml format** (D-04 costly): one file per plugin package directory (research F6). Header comment line `# Generated by summer plugin:manifest. Do not edit; rerun the command.` then, in this order: `schema: 1`, `id`, `module` (go.mod module path, what `go get` takes), `package` (import path summer.yaml lists; differs from module for a nested plugin such as `…/sm-pgstripe-plugin/subscriptions`), `name`, `description`, `framework` (go.mod require of git.golem15.com/golem15/summercms), `requires` (map of required plugin ID to its minimum, sorted by ID, omitted when empty; empty string when the required plugin ships in the same module or is not a go.mod require), `overrides` (`name`/`description` set with `--name`/`--description`, omitted when unused). A newer `schema` than the reader knows is an error that says to update summer.
+- **P34-02 registry API v1** (D-16 one-way): base path `/api/registry/v1`; `GET /plugins?q=&category=` → `{"data":[RegistryPlugin…],"meta":{"total":N}}`; `GET /plugins/{id}` → `{"data":RegistryPluginDetail}`; `GET /plugins/{id}/versions` → `{"data":[RegistryVersion…]}` newest first; errors `{"error":{"code":"…","message":"…"}}` (404 `not_found`, 400 `invalid_query`); `POST /ingest` (authenticated, body `{"repository":""}`) → 202 `{"data":{"repository":"…","queued":true}}`. RegistryPlugin fields: `id`, `name`, `description`, `module`, `package`, `repository`, `category`, `featured`, `icon_url`, `latest_version`, `latest_framework`, `updated_at` (RFC 3339). RegistryPluginDetail adds `readme_markdown`, `readme_html`, `screenshots` (`[{url, caption}]`), `requires` (of the latest version). RegistryVersion: `version`, `framework`, `requires`, `module`, `package`, `commit`, `tagged_at`. Rules: always an envelope; clients ignore unknown fields; only additive changes within v1; GET-only, no credentials, open CORS, `Cache-Control: public, max-age=300` plus ETag.
+- **P34-03 names**: module `kiosk`; `party.Info{ID, Module, Package, Name, Description, Version, Source}` with Source `module` | `submodule` | `local`; `party.Installed{Framework string; Plugins []Info}`; `party.Describe(app, plugins, framework)`; generated `PluginInfo` and `FrameworkVersion`; config key `plugins.registry_url` (default `https://summercms.io`, empty disables); permission `backend.view_plugins`; admin route `/system/plugins`; marketplace URLs `/marketplace` and `/marketplace//`; token scope `registry`.
+- **P34-04 release number**: the framework release that carries this phase is v0.6.0 (next minor after v0.5.3: a new module and three new commands); 34-02 writes it as each plugin's framework minimum (user decision 2), and plugin tags wait for that tag (user-gated).
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
+@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
+@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
+@.planning/notes/2026-10-29-plugin-marketplace.md
+@CLAUDE.md
+
+
+internal/build/scaffold.go: pluginIDFromGo(path string) (string, error) accepts only a string literal returned by ID() (lines ~360-392); AddPlugin(ctx, startDir, pluginDir) (line 144) calls it; editAppModule writes -require=mod@v0.0.0 and a local replace; runGo(ctx, dir, args...) and applyGoWork(cmd, dir) (lines ~621-640); parseGoMod is a line scanner — do not extend it, use `go mod edit -json`.
+internal/build/build.go: App(ctx, appDir, out) → generate(appDir, m) → generateMain(m), generatePluginsGen(m), formatSource, writeIfChanged; generated main calls party.Activate(app, PluginIDs) (line ~125). internal/build/new.go:453 calls generate(target, m); goldens in internal/build/testdata/new*/ (rewrite with `go test ./internal/build -run TestNew -update`).
+internal/build/manifest.go: Manifest{Module, Binary, Plugins []Plugin{ID, Module}, Tags}, LoadManifest, SaveManifest, ValidatePluginID, validateModulePath (rejects a leading "-").
+modules/party/registry.go: Plugin interface {ID; Requires; Register; Boot}; Activated []Plugin published with app.Publish; Activate(app, ids).
+modules/backpack: app.Publish[T](v) error, app.Lookup[T]() (T, bool) (generic methods, Go 1.27).
+modules/bonfire: Command{Name, Description, Long, Examples, Args, Flags, Run}; Input.Flag(name) (string, bool) returns ("", true) for an explicitly empty --name=""; FlagBool(in, name) for Bare switches; Output has Success, Warning, Info, Println.
+goccy/go-yaml (already required) for YAML; github.com/yuin/goldmark (already required) for the README AST; no new dependency.
+
+
+
+## Execution units and resume points
+
+- Each task ends with the commit listed below, staged by explicit path (never `-A` or `.`). Other sessions may hold uncommitted `.planning/STATE.md`, `.planning/state.json`, `.planning/ROADMAP.md`, `.planning/G15OFFICE.md` or cabana edits; leave them alone. Before editing `internal/build` or `cmd/summer`, run `git status --short internal/build cmd/summer modules/party` and stop if another session has uncommitted edits there.
+- Resume check: `git log --format=%s | grep -Fx ''`; present subjects mean the task is done: re-run its `` and continue with the next one.
+
+| Task | Commit subject | Resume point |
+|------|----------------|--------------|
+| 1 | none (answer recorded in the SUMMARY) | answer recorded |
+| 2 | `feat(kiosk): plugin.yaml manifest and summer plugin:manifest` | TestManifestSmoke passes |
+| 3 | `feat(kiosk): resolve constant plugin IDs, overrides and the CheckT helper` | TestManifestSmoke/consts passes |
+| 4 | `feat(kiosk): registry v1 wire types and version rules` | TestManifestSmoke/registry passes |
+| 5 | `feat(build): bake plugin metadata and the framework version into plugins.gen.go` | TestPluginInfoSmoke passes |
+
+
+
+
+ Task 1: Confirm the plugin.yaml format, the /api/registry/v1 shape, the names and the release number before any code depends on them
+ Released summer binaries and cabana call /api/registry/v1 and published plugin tags carry plugin.yaml forever (tags are never moved, research Pitfall 7); a later change needs a v2 path and an ingester that reads both formats.
+ Adopt P34-01 (plugin.yaml format), P34-02 (registry API v1 shape), P34-03 (names) and P34-04 (v0.6.0 as the framework minimum) as written in the objective, or adjust them.
+
+ D-04 rates the plugin.yaml format costly (every public repo, the ingester and git-URL installs read it) and D-16 rates the API one-way. Research marked the field names `schema`, `package`, `overrides` (A2), the JSON shape (A3), the module name `kiosk` (A10), `party.Info`/`party.Describe` (A8) and `backend.view_plugins` with `/system/plugins` (A6) as assumptions. 34-03 (server) and 34-04 (client) run in parallel after this plan and both encode the shape fixed here through kiosk's wire types, so this is the only point before both. The release number decides the `framework:` minimum 34-02 writes into 19 plugin go.mod files.
+
+
+
+
+
+
+ Reply "as-planned", "adjust: " or "no-schema-field"
+
+
+
+ Task 2: End-to-end "a plugin author runs summer plugin:manifest and gets a committed, checkable plugin.yaml" — literal-ID plugin only
+ modules/kiosk/kiosk.go, modules/kiosk/manifest.go, modules/kiosk/derive.go, modules/kiosk/check.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/testdata/literal/go.mod, modules/kiosk/testdata/literal/plugin.go, modules/kiosk/testdata/literal/README.md, modules/kiosk/README.md, cmd/summer/main.go, README.md, docs/plugins/marketplace.md, docs/console/scaffolding.md
+
+ - internal/build/scaffold.go (pluginIDFromGo ~360-392, runGo/applyGoWork ~621-640: exec style to copy)
+ - internal/build/manifest.go (goccy YAML use, ValidatePluginID, validateModulePath)
+ - cmd/summer/main.go (addPluginCommand ~381-415, toolCommands ~31)
+ - cmd/summer/commands_documented_test.go (what TestCommandsDocumented demands of a command)
+ - modules/fetchguard/README.md (module README structure to copy)
+ - modules/breakwater/breakwater.go (goldmark usage)
+ - docs/plugins/testing.md (front matter: title, description, section, order) and docs/console/scaffolding.md
+ - .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Pattern 1, F6, F7)
+
+
+ Implement P34-01 as answered at Task 1, per D-01, D-02, D-04 and D-05, for a plugin whose ID() and Requires() return literals. Create the package `modules/kiosk` (package doc: plugin manifests and the plugin registry contract; it never names a consuming application):
+ - `kiosk.go`: constants `FileName = "plugin.yaml"`, `SchemaVersion = 1`, `FrameworkModule = "git.golem15.com/golem15/summercms"`, `Header` (the generated-file comment line).
+ - `manifest.go`: `Manifest` with yaml tags in P34-01 order, `Overrides{Name, Description}`, `Marshal(Manifest) ([]byte, error)` (Header plus goccy output; requires emitted as a goccy MapSlice sorted by ID so the bytes are stable), `Parse([]byte) (Manifest, error)`.
+ - `derive.go`: `DeriveOptions{Name, Description *string}` (nil keeps the persisted override, a pointer to "" clears it), `Derive(ctx, dir string, opts DeriveOptions) (Manifest, []string, error)` returning warnings. It finds the module root by walking up from dir to the nearest go.mod, reads it with `go mod edit -json` (exec, cmd.Dir = module root, args after the subcommand, never a shell), sets module, package (module path plus the dir's slash path relative to the root), framework (the FrameworkModule require; missing or v0.0.0 is an error), reads `/plugin.go` for ID()/Requires() string literals (non-literal returns are the D-02 error naming the function; Task 3 widens this), reads `/README.md` with goldmark: name is the first H1's text, description the first paragraph after it (plain text, whitespace collapsed); a missing README or H1 is an error. A README H1 equal to the ID with no name override is an error whose message names `--name` (research F7). Overrides from an existing plugin.yaml are kept unless opts changes them.
+ - `check.go`: `Generate(ctx, dir, opts) ([]byte, []string, error)`, `Write(ctx, dir, opts) (path string, warnings []string, err error)`, `Check(ctx, dir) error` returning `*StaleError{Path string; Missing bool; Want, Got []byte}` whose Error() names the path and the first differing line.
+ - Fixture `testdata/literal` (module `example.test/sm-acme-plugin`, requires FrameworkModule at v0.5.3, plugin.go returning "acme.blog" and nil, README `# Blog` plus one sentence).
+ - `manifest_smoke_test.go` with `TestManifestSmoke` subtest `literal`: copy the fixture to t.TempDir(), Write, assert every field and the header, Check returns nil, edit the README sentence and Check returns a *StaleError, delete plugin.yaml and Check reports Missing.
+ - `cmd/summer/main.go`: `pluginManifestCommand()` registered in toolCommands after `addPluginCommand()`: Name `plugin:manifest`, Description, Long (what is derived from where, that plugin.yaml is committed and never hand-edited, --check exit status), Examples (plain, --check, --pretend, --name), optional Arg `dir` (default "."), Flags `check` and `pretend` (Bare, read with bonfire.FlagBool), `name`, `description` (read with in.Flag so an explicit empty value clears). --check rejects --name/--description; --pretend prints Generate's bytes and writes nothing; otherwise Write and print `Wrote ` with out.Success; warnings go to out.Warning.
+ - Docs: `modules/kiosk/README.md` in the standard structure (H1, one-sentence summary, import line, `**Name:**` mnemonic for a beach kiosk where every plugin on the shelf carries its label, Overview, Features, Usage, API reference, CLI commands, Dependencies, Testing); a row in the root `README.md` module table reusing the summary; new page `docs/plugins/marketplace.md` (section plugins, an order after testing) with "The plugin.yaml manifest" and "Generating and checking it" sections; a `plugin:manifest` row in `docs/console/scaffolding.md`. Every identifier must exist (docs checker).
+ Commit as listed in Execution units.
+
+
+ go test ./modules/kiosk -run 'TestManifestSmoke' -count=1 -v 2>&1 | grep -E '^(--- |=== RUN|FAIL|ok|PASS)' | tail -20
+ a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/literal" line
+ go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree' -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5
+ non-zero exit, or a "FAIL" or "unknown identifier" line in the output
+ go vet ./... && go test -short ./...
+ non-zero exit or a "FAIL" line
+
+
+ - `grep -n "SchemaVersion\|FrameworkModule" modules/kiosk/kiosk.go` prints at least two lines
+ - `go doc ./modules/kiosk Manifest` lists Schema, ID, Module, Package, Name, Description, Framework, Requires and Overrides
+ - `grep -n '"plugin:manifest"' cmd/summer/main.go` prints one line
+ - `cp -r modules/kiosk/testdata/literal "$TMP/acme" && go run ./cmd/summer plugin:manifest "$TMP/acme" && go run ./cmd/summer plugin:manifest --check "$TMP/acme"` exits 0 and `$TMP/acme/plugin.yaml` starts with the header line and contains `id: acme.blog` (with TMP set to a scratch directory)
+ - `grep -n "kiosk" README.md` prints the module table row; `grep -n "plugin:manifest" docs/console/scaffolding.md docs/plugins/marketplace.md` prints at least two lines
+
+ A plugin author can generate, preview and check plugin.yaml for a literal-ID plugin from the CLI, and the format is documented.
+
+
+
+ Task 3: Every public plugin layout derives a valid plugin.yaml: constant IDs, nested plugins, overrides, version checks and the CheckT helper
+ modules/kiosk/identity.go, modules/kiosk/derive.go, modules/kiosk/check.go, modules/kiosk/manifest.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/testdata/consts/go.mod, modules/kiosk/testdata/consts/plugin.go, modules/kiosk/testdata/consts/ids/ids.go, modules/kiosk/testdata/consts/nested/plugin.go, modules/kiosk/testdata/consts/nested/README.md, modules/kiosk/testdata/consts/README.md, modules/kiosk/testdata/other/go.mod, modules/kiosk/testdata/other/plugin.go, modules/kiosk/testdata/other/README.md, modules/kiosk/testdata/framework/go.mod, internal/build/scaffold.go, internal/build/build_test.go, modules/kiosk/README.md, docs/plugins/marketplace.md
+
+ - modules/kiosk/derive.go and modules/kiosk/check.go (Task 2 output)
+ - internal/build/scaffold.go (pluginIDFromGo and its callers: AddPlugin, make:* paths)
+ - internal/build/build_test.go (TestMakeAndAddPluginOnHelloCopy: the temp-app pattern)
+ - ../sm-mfa-plugin/plugin.go (same-file const), ../sm-paymentgateway-plugin/plugin.go and ../sm-paymentgateway-plugin/payments/service.go (cross-package const), ../sm-pgstripe-plugin/subscriptions/plugin.go (Requires with a same-module and an other-module const) — read only
+ - ../sm-chatbots-plugin/go.mod (a plugin requirement at v0.0.0, research F8) — read only
+ - .planning/phases/34-plugin-marketplace/34-RESEARCH.md (F1, F6, F8, Pattern 2)
+
+
+ - consts fixture: ID() returning a same-package const resolves; Requires() returning a slice of a cross-package const (same module, `ids.Translate`) and a const from the `other` fixture module (resolved through `go list -m -json` via a local replace) resolves, and the other-module requirement's minimum is its go.mod require version
+ - nested fixture `consts/nested`: package is `/nested`, module is the root module, and a Requires() entry naming the root plugin of the same module gets the empty minimum
+ - ID() returning a function call, a var or a concatenation with a non-const fails with an error containing "ID()"; the same for Requires() with "Requires()"
+ - a plugin requirement at v0.0.0 fails with "not a released version"; a pseudo-version minimum passes and returns a warning
+ - --name override persists across a regeneration without flags; an explicit empty name clears it and then an H1 equal to the ID fails with a message containing "--name"
+ - CheckT on a stale fixture calls Fatalf once with the path; on a fresh one it does not
+ - Validate rejects a bad ID, a module with a leading "-", a non-vX.Y.Z framework, a schema greater than SchemaVersion
+ - plugin:add on a copy of examples/hello registers a fixture plugin whose ID() returns a const (regression for research F1); a literal-ID plugin registers exactly as before
+
+
+ Per D-02 as extended by user decision 1 (2026-10-29): add `identity.go` with `Identity{ID string; Requires []string}` and `ReadIdentity(ctx, dir string) (Identity, error)`. It parses the non-test .go files of dir with go/parser (never builds or runs them), finds the methods ID() and Requires() on any receiver, and resolves each returned expression statically: a string literal; an identifier naming a package-level const in any non-test file of the same directory (recursively, guarded against cycles); `pkg.Name` where the file's import (named imports honoured) is in the same module (directory = module root plus the import path suffix) or in another module (directory from `go list -m -json ` run in the module root, `.Dir`; with applyGoWork-style GOWORK handling left to Go's defaults). Requires() accepts nil, an empty slice, or a []string composite literal whose elements resolve the same way. Anything else is an error of the form `: ID() must return a string literal or constant` (or `Requires()`).
+ Switch Derive to ReadIdentity. Map each required ID to its providing module: the plugin's own module (any plugin.go directory under the module root) gives the empty minimum; otherwise each non-framework go.mod require is resolved with `go list -m -json` and its root identity read; a found ID takes that require's version; an ID found nowhere gets the empty minimum (soft dependency). A plugin requirement at v0.0.0 is an error ("not a released version", research F8); a pseudo-version is accepted with a warning. Add `Validate(Manifest) error` (ID via the same rule as internal/build ValidatePluginID, re-implemented in kiosk since internal/build cannot be imported; module and package paths reject a leading "-", spaces and backslashes; framework and non-empty minimums must be strict vX.Y.Z or a pseudo-version; schema between 1 and SchemaVersion) and call it from Parse and Generate.
+ Overrides per D-03: `--name`/`--description` set `Overrides`; the top-level name/description are the effective values; regenerating without flags keeps Overrides; an explicit empty value removes that override.
+ Add `TB interface{ Helper(); Fatalf(format string, args ...any) }` and `CheckT(t TB, dir string)`, which calls Check and fails with the StaleError text plus the command to rerun (`summer plugin:manifest `); kiosk must not import the testing package.
+ Replace the body of `internal/build/scaffold.go` pluginIDFromGo with a call to kiosk.ReadIdentity on the plugin.go's directory (keep the function name and its error prefix "build:"); every caller keeps working.
+ Fixtures: `testdata/consts` (module `example.test/sm-consts-plugin`, ID via `const PluginID`, Requires `ids.Translate` and `other.PluginID`, go.mod require and replace of `example.test/sm-other-plugin => ../other`, framework require v0.5.3), `testdata/consts/nested`, `testdata/other` (module `example.test/sm-other-plugin`, require v0.2.0 in consts' go.mod), and `testdata/framework` (a stub go.mod declaring the framework module path) that every fixture go.mod replaces the framework with, so `go list -m -json` runs offline; tests copy the testdata tree into t.TempDir() keeping the relative layout. Write the behavior subtests in TestManifestSmoke first (subtest names `consts`, `nested`, `errors`, `overrides`, `checkt`, `validate`) plus `TestAddPluginConstID` in internal/build/build_test.go, then implement.
+ Document ReadIdentity, Validate, Overrides and CheckT in modules/kiosk/README.md and add a "Testing the manifest in your plugin" section with the one-line `kiosk.CheckT(t, ".")` test to docs/plugins/marketplace.md. Commit as listed in Execution units.
+
+
+ go test ./modules/kiosk ./internal/build -run 'TestManifestSmoke|TestAddPluginConstID|TestMakeAndAddPluginOnHelloCopy' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30
+ a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/consts" or "--- PASS: TestAddPluginConstID" line
+ go run ./cmd/summer plugin:manifest --pretend --name=MFA ../sm-mfa-plugin 2>&1 | grep -E '^(id|module|framework):'
+ non-zero exit, or fewer than three lines, or no "id: golem15.mfa" line
+ go run ./cmd/summer plugin:manifest --pretend --name="Stripe subscriptions" ../sm-pgstripe-plugin/subscriptions 2>&1 | grep -E '^(id|package):|golem15\.'
+ non-zero exit, or no "id: golem15.pgstripesubscriptions" line, or no "package: git.golem15.com/golem15/sm-pgstripe-plugin/subscriptions" line
+ go vet ./... && go test -short ./...
+ non-zero exit or a "FAIL" line
+
+
+ - `grep -n "func ReadIdentity(ctx context.Context, dir string) (Identity, error)" modules/kiosk/identity.go` prints one line
+ - `grep -n "kiosk.ReadIdentity" internal/build/scaffold.go` prints at least one line
+ - `grep -n "func CheckT(t TB, dir string)" modules/kiosk/check.go` prints one line; `go list -deps ./modules/kiosk | grep -x testing` prints nothing
+ - `go run ./cmd/summer plugin:manifest --pretend ../sm-paymentgateway-plugin --name="Payment gateway"` prints `id: golem15.paymentgateway` (cross-package const)
+ - `go run ./cmd/summer plugin:manifest --pretend ../sm-chatbots-plugin` exits non-zero with "not a released version" (research F8), unless the sibling has already been fixed
+ - No file under ../sm-*-plugin is created or changed by this task (`git -C ../sm-mfa-plugin status --short` shows no plugin.yaml)
+
+ plugin:manifest derives a valid manifest for literal, constant, cross-package and nested plugins without running their code, plugin:add works for constant-ID plugins, and plugin repos have a one-line test helper.
+
+
+
+ Task 4: The registry v1 contract in Go: wire types, envelopes and strict version rules
+ modules/kiosk/registry.go, modules/kiosk/version.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/README.md, docs/plugins/marketplace.md
+
+ - the Task 1 answer (P34-02 as confirmed or adjusted)
+ - .planning/phases/34-plugin-marketplace/34-RESEARCH.md ("Proposed registry API shape", Anti-Patterns on git describe and semver)
+ - modules/kiosk/manifest.go (Requires type to reuse)
+
+
+ - RegistryPlugin, RegistryPluginDetail and RegistryVersion marshal to exactly the P34-02 field names (golden JSON in the test) and unmarshal JSON carrying unknown extra fields without error
+ - ValidVersion accepts v0.6.0 and v10.2.33, rejects 0.6.0, v0.6, v0.6.0-rc1, v0.6.0+meta and a leading "-"
+ - CompareVersions orders v0.10.0 after v0.9.9 and returns 0 for equal versions
+ - BaseVersion returns v0.3.1 for "v0.3.1-4-gabc1234" and for "v0.3.1", and "" for "abc1234" or ""
+ - Satisfies(have, min) is true when min is "", true when have >= min, false when have < min, and treats a pseudo-version minimum vX.Y.(Z+1)-0.- as satisfied by vX.Y.(Z+1) and later
+ - NewestCompatible(versions, framework) returns the newest RegistryVersion whose framework minimum Satisfies the given framework, and false when none does
+
+
+ Per D-16 and the Task 1 answer: add `registry.go` with `RegistryAPIPath = "/api/registry/v1"`, `RegistryPlugin`, `RegistryPluginDetail` (embeds RegistryPlugin plus ReadmeMarkdown, ReadmeHTML, Screenshots []RegistryScreenshot, Requires), `RegistryScreenshot{URL, Caption}`, `RegistryVersion`, `ListResponse{Data []RegistryPlugin; Meta ListMeta{Total int}}`, `DetailResponse`, `VersionsResponse`, `ErrorResponse{Error RegistryError{Code, Message}}`, `IngestRequest{Repository}`, `IngestResponse`, with JSON tags exactly as confirmed; time fields are time.Time encoded RFC 3339. These types are the contract: 34-03 encodes responses with them and 34-04/34-05 decode with them, so no other copy of the shape may exist.
+ Add `version.go` with `ValidVersion(v string) bool` (strict `^v\d+\.\d+\.\d+$`), `CompareVersions(a, b string) int` (numeric per component; only defined for valid versions), `BaseVersion(describe string) string` (the vX.Y.Z prefix of a git describe string or ""), `Satisfies(have, min string) bool`, `NewestCompatible(versions []RegistryVersion, framework string) (RegistryVersion, bool)`. Never feed a describe string to the comparer (research anti-pattern). Write the behavior subtests (`registry`, `versions`) in TestManifestSmoke first, then implement. Document the types and functions in modules/kiosk/README.md and add a "Registry API v1" reference section (paths, envelopes, fields, compatibility rules) to docs/plugins/marketplace.md. Commit as listed in Execution units.
+
+
+ go test ./modules/kiosk -run 'TestManifestSmoke/(registry|versions)' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20
+ a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/registry" line
+ go test ./cmd/summer -run TestDocsTree -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5
+ non-zero exit, or a "FAIL" or "unknown identifier" line
+
+
+ - `grep -n 'RegistryAPIPath = "/api/registry/v1"' modules/kiosk/registry.go` prints one line
+ - `grep -n 'json:"latest_version"\|json:"readme_html"\|json:"tagged_at"' modules/kiosk/registry.go` prints three lines (or the names confirmed at Task 1)
+ - `grep -n "func NewestCompatible(" modules/kiosk/version.go` prints one line
+ - docs/plugins/marketplace.md contains a "Registry API v1" heading and names `/api/registry/v1/plugins/{id}/versions`
+
+ The registry contract exists once, in Go, with version rules every consumer shares.
+
+
+
+ Task 5: summer build bakes plugin metadata and the effective framework version, and the app publishes them
+ internal/build/plugindirs.go, internal/build/build.go, internal/build/new.go, internal/build/plugininfo_smoke_test.go, internal/build/testdata/new/main.go.golden, internal/build/testdata/new/plugins.gen.go.golden, internal/build/testdata/new-no-plugin/plugins.gen.go.golden, modules/party/info.go, modules/party/README.md, examples/hello/main.go, examples/hello/plugins.gen.go, docs/architecture/go-modules-and-workspaces.md, docs/plugins/marketplace.md
+
+ - internal/build/build.go (whole file)
+ - internal/build/new.go (~430-460, the generate call) and internal/build/new_golden_test.go (the -update flag)
+ - internal/build/leaf.go (localModuleDir/defaultPluginDir: module-cache plugins are skipped there, no change needed)
+ - internal/dev/watch.go (~74, build.App reuse)
+ - modules/party/registry.go and modules/party/README.md
+ - modules/bonfire/version.go (BuildVersion: what runtime already reports)
+ - .planning/phases/34-plugin-marketplace/34-RESEARCH.md (F3, F5, Pattern 3, Pitfall 6)
+
+
+ - generatePluginsGen with fixed metadata emits `var PluginInfo = []party.Info{...}` in summer.yaml order with every string strconv-quoted and `const FrameworkVersion = ""`, byte-stable across two calls
+ - generateMain emits `party.Describe(app, PluginInfo, FrameworkVersion)` with the usual failure branch, before `party.Activate(app, PluginIDs)`
+ - resolvePluginDirs on a temp app with one in-app plugin (no own repo) and one plugin directory that is its own git repo with tag v0.1.0 plus one commit returns Source local with empty version and Source submodule with a version starting "v0.1.0-1-g"
+ - a plugin directory with uncommitted changes still reports the describe string without a dirty suffix
+ - EffectiveFramework returns the newest reachable tag of a locally replaced framework checkout, and the go.mod require version when the framework is not replaced; when git fails it returns the require version and a warning
+ - build.App on an app whose metadata resolution fails (go list error) still generates ID-only PluginInfo, prints one warning and builds
+ - party.Describe publishes party.Installed so app.Lookup[party.Installed]() returns the plugins and framework; calling it with a nil app is an error
+
+
+ Per D-06 and D-07. In `modules/party/info.go` (additive, no change to Plugin or Activate): `Info{ID, Module, Package, Name, Description, Version, Source string}` with Source constants `SourceModule`, `SourceSubmodule`, `SourceLocal`; `Installed{Framework string; Plugins []Info}`; `Describe(app *backpack.App, plugins []Info, framework string) error` publishing Installed. Document them in modules/party/README.md.
+ Add `internal/build/plugindirs.go`: `pluginMeta` resolution through one `go list -e -json=ImportPath,Dir,Module,Error ` run in the app dir with applyGoWork (research F5); per plugin read `/plugin.yaml` with kiosk.Parse when present (name, description, module, package; a parse error warns and is ignored); version: module-cache plugin → Module.Version; a local dir whose `git -C rev-parse --show-toplevel` equals the dir → `git describe --tags --match v[0-9]*` (no dirty flag, research Pitfall 6) and Source submodule; any other local dir → Source local and empty version. Never bake a filesystem path. Add exported `EffectiveFramework(ctx, appDir string) (string, error)` per research F3 (`go mod edit -json` of go.mod, `go work edit -json` of a go.work found by findGoWork; a local replace of kiosk.FrameworkModule → `git -C describe --tags --abbrev=0 --match v[0-9]*`; otherwise the require version; on git failure return the require version plus a non-nil warning error the caller prints).
+ Change `generate` to take the resolved metadata (`buildMeta{Plugins []party.Info; Framework string}`): build.App resolves it once per call, prints warnings to out and falls back to ID-only entries on any failure (never fails the build); `generatePluginsGen` imports party and emits PluginInfo and FrameworkVersion after PluginIDs; `generateMain` adds the Describe call before Activate. new.go passes ID-only metadata with an empty FrameworkVersion (the first summer build fills it), which keeps the `summer new` goldens deterministic; regenerate the goldens with -update and review the diff. Regenerate examples/hello's main.go and plugins.gen.go with `go run ./cmd/summer build` run from examples/hello (or the package function) and commit them.
+ Write `internal/build/plugininfo_smoke_test.go` (`TestPluginInfoSmoke`, subtests per behavior line, git fixtures created in t.TempDir with `git init` and a test-local identity; skip a subtest when git is not on PATH) first, then implement. Document the generated PluginInfo/FrameworkVersion in docs/architecture/go-modules-and-workspaces.md and an "Installed plugin metadata" section in docs/plugins/marketplace.md. Commit as listed in Execution units.
+
+
+ go test ./internal/build -run 'TestPluginInfoSmoke|TestNew|TestGenerate' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30
+ a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginInfoSmoke" line
+ cd examples/hello && go test ./... 2>&1 | tail -5
+ non-zero exit or a "FAIL" line
+ go vet ./... && go test -short ./...
+ non-zero exit or a "FAIL" line
+
+
+ - `grep -n "func Describe(app \*backpack.App, plugins \[\]Info, framework string) error" modules/party/info.go` prints one line
+ - `grep -n "party.Describe(app, PluginInfo, FrameworkVersion)" examples/hello/main.go` prints one line; `grep -n "var PluginInfo = \[\]party.Info" examples/hello/plugins.gen.go` prints one line
+ - `grep -n "func EffectiveFramework(ctx context.Context, appDir string) (string, error)" internal/build/plugindirs.go` prints one line
+ - `grep -n "describe" internal/build/plugindirs.go` shows `--tags` and `--match` arguments
+ - `git show --stat -- internal/build/testdata` lists only the three goldens named in files
+ - docs/architecture/go-modules-and-workspaces.md names PluginInfo and FrameworkVersion
+
+ Every built app carries its plugins' names, descriptions, versions and the framework version it was built against, ready for the admin's Installed tab.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| plugin source → kiosk reader | plugin.go, go.mod and README.md of any directory a developer points the CLI at |
+| CLI args → go/git subprocesses | dir paths and module paths reach `go mod edit`, `go list`, `git describe` |
+| plugin.yaml in a module-cache or submodule plugin → summer build | third-party text baked into generated Go source |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-34-01 | Elevation of privilege | kiosk.ReadIdentity | high | mitigate | AST-only: go/parser on source text, no `go run`, no build, no init(); TestManifestSmoke/errors proves non-const expressions are refused rather than evaluated |
+| T-34-02 | Tampering | exec of go/git with paths and module strings | high | mitigate | exec.CommandContext without a shell, fixed argument positions, module paths validated (leading "-" rejected) before use, `--` before positional git arguments where git accepts it |
+| T-34-03 | Tampering | plugin.yaml text in plugins.gen.go | high | mitigate | every baked string goes through strconv.Quote and formatSource; a malformed plugin.yaml is ignored with a warning (TestPluginInfoSmoke) |
+| T-34-04 | Information disclosure | plugins.gen.go | low | mitigate | only IDs, module paths, texts and versions are baked; no Dir or absolute path (acceptance grep in review) |
+| T-34-05 | Denial of service | summer build | medium | mitigate | metadata resolution failure degrades to ID-only and warns; build never fails on it (behavior test) |
+| T-34-06 | Repudiation | stale committed plugin.yaml | medium | mitigate | `--check` and kiosk.CheckT fail on any byte difference |
+| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | this plan installs no package (research: no new dependency) |
+
+
+
+- `go vet ./... && go test -short ./...` green after every commit; `cd examples/hello && go test ./...` green after Task 5.
+- `go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree'` and `go run ./cmd/summer docs:build --check` pass.
+- `go run ./cmd/summer plugin:manifest --pretend …` derives manifests for ../sm-mfa-plugin, ../sm-paymentgateway-plugin and ../sm-pgstripe-plugin/subscriptions without writing into them.
+
+
+
+- kiosk exists with manifest, identity, check, registry and version APIs, documented and in the root README table.
+- `summer plugin:manifest` generates, previews and checks plugin.yaml for every public plugin layout.
+- plugins.gen.go carries PluginInfo and FrameworkVersion; the generated main publishes them.
+- No push, no tag; no file in a sibling repository changed.
+
+
+
+
+## Artifacts this phase produces
+
+- Module: `modules/kiosk` (README, root README row).
+- Types: `kiosk.Manifest`, `kiosk.Overrides`, `kiosk.DeriveOptions`, `kiosk.Identity`, `kiosk.StaleError`, `kiosk.TB`, `kiosk.RegistryPlugin`, `kiosk.RegistryPluginDetail`, `kiosk.RegistryScreenshot`, `kiosk.RegistryVersion`, `kiosk.ListResponse`, `kiosk.ListMeta`, `kiosk.DetailResponse`, `kiosk.VersionsResponse`, `kiosk.ErrorResponse`, `kiosk.RegistryError`, `kiosk.IngestRequest`, `kiosk.IngestResponse`, `party.Info`, `party.Installed`.
+- Functions: `kiosk.Marshal`, `kiosk.Parse`, `kiosk.Validate`, `kiosk.Derive`, `kiosk.Generate`, `kiosk.Write`, `kiosk.Check`, `kiosk.CheckT`, `kiosk.ReadIdentity`, `kiosk.ValidVersion`, `kiosk.CompareVersions`, `kiosk.BaseVersion`, `kiosk.Satisfies`, `kiosk.NewestCompatible`, `party.Describe`, `build.EffectiveFramework`.
+- Constants: `kiosk.FileName`, `kiosk.SchemaVersion`, `kiosk.FrameworkModule`, `kiosk.Header`, `kiosk.RegistryAPIPath`, `party.SourceModule`, `party.SourceSubmodule`, `party.SourceLocal`.
+- Generated code: `PluginInfo` and `FrameworkVersion` in every app's plugins.gen.go; `party.Describe(app, PluginInfo, FrameworkVersion)` in generated main.
+- CLI: `summer plugin:manifest [dir]` with `--check`, `--pretend`, `--name`, `--description`.
+- Files: `plugin.yaml` format (schema 1); `docs/plugins/marketplace.md`; tests `TestManifestSmoke`, `TestPluginInfoSmoke`, `TestAddPluginConstID`.
+- Config keys: none in this plan (`plugins.registry_url` arrives in 34-04).
+
+## Phase-wide source coverage audit
+
+| Source | ID | Item | Plan | Status |
+|--------|----|------|------|--------|
+| GOAL | — | Browse at summercms.io/marketplace, install with one command, private git URLs, read-only admin Plugins screen | 01-06 | COVERED |
+| REQ | — | none (Requirements: TBD); SC-1..SC-5 used instead | — | n/a |
+| ROADMAP | SC-1 | plugin.yaml in every public sm-*-plugin repo, validated, anonymous fetch | 01, 02 | COVERED |
+| ROADMAP | SC-2 | ingest on tag, registry API, /marketplace list/search/category/detail, presentation fields in admin | 03 | COVERED |
+| ROADMAP | SC-3 | plugin:install ID (module, --source) and git URL, requirement checks, plugin:add registration | 04 | COVERED |
+| ROADMAP | SC-4 | admin Plugins screen Installed + Marketplace with copy | 01, 05 | COVERED |
+| ROADMAP | SC-5 | unit tests in the last plan | 06 | COVERED |
+| CONTEXT | D-01..D-05 | manifest generation, derivation, overrides, --check, minimums | 01, 02 | COVERED |
+| CONTEXT | D-06, D-07 | build metadata and versions | 01 | COVERED |
+| CONTEXT | D-08..D-12 | install resolution, deps + --yes, plugin:update, git URL submodule-only, --offline | 04 | COVERED |
+| CONTEXT | D-13..D-16 | pull ingester + ingest-now, admin-curated listing, version rows, public API v1 | 03 (contract types in 01) | COVERED |
+| CONTEXT | D-17..D-19 | cabana endpoints, plugins.registry_url, permission | 04 (config), 05 | COVERED |
+| RESEARCH | F1 | const IDs (user decision 1) | 01 | COVERED |
+| RESEARCH | F2 | go.work replace overrides for submodule installs | 04 | COVERED |
+| RESEARCH | F3 | effective framework version | 01, 04 | COVERED |
+| RESEARCH | F4 | ConfirmProceed auto-confirms; use Interactive + Confirm | 04 | COVERED |
+| RESEARCH | F5 | go list dir resolution | 01 | COVERED |
+| RESEARCH | F6 | plugin.yaml per plugin package dir | 01, 02 | COVERED |
+| RESEARCH | F7 | --name where README H1 == ID | 01, 02 | COVERED |
+| RESEARCH | F8 | v0.0.0 plugin requirements | 01, 02 | COVERED |
+| RESEARCH | F9 | untagged repos not listed; anonymous fetch | 02, 03 | COVERED |
+| RESEARCH | F10 | nginx, work_in_serve, nodePaths, app regeneration | 03, 06 (ledger) | COVERED |
+| RESEARCH | Pitfall 4 | summer.yaml comments preserved (user decision 5) | 04 | COVERED |
+| RESEARCH | Pitfall 13 | ingest identity checks (ID unique, module == repo path) | 03 | COVERED |
+| RESEARCH | OQ 3/6 | launch list = tagged sibling plugins; --source in a non-git app refuses (user decision 5) | 03, 04, 06 | COVERED |
+| CONTEXT | Deferred | --module for git URLs; paid plugins, themes, admin installs | — | excluded (deferred) |
diff --git a/.planning/phases/34-plugin-marketplace/34-02-PLAN.md b/.planning/phases/34-plugin-marketplace/34-02-PLAN.md
new file mode 100644
index 00000000..b27f7339
--- /dev/null
+++ b/.planning/phases/34-plugin-marketplace/34-02-PLAN.md
@@ -0,0 +1,331 @@
+---
+phase: 34-plugin-marketplace
+plan: 02
+type: execute
+wave: 4
+depends_on: ["34-01", "34-03", "34-05"]
+files_modified:
+ - ../sm-journal-plugin/plugin.yaml
+ - ../sm-journal-plugin/manifest_test.go
+ - ../sm-journal-plugin/go.mod
+ - ../sm-journal-plugin/go.sum
+ - ../sm-chat-plugin/plugin.yaml
+ - ../sm-chat-plugin/manifest_test.go
+ - ../sm-chat-plugin/go.mod
+ - ../sm-faq-plugin/plugin.yaml
+ - ../sm-faq-plugin/manifest_test.go
+ - ../sm-faq-plugin/go.mod
+ - ../sm-menu-plugin/plugin.yaml
+ - ../sm-menu-plugin/manifest_test.go
+ - ../sm-menu-plugin/go.mod
+ - ../sm-mfa-plugin/plugin.yaml
+ - ../sm-mfa-plugin/manifest_test.go
+ - ../sm-mfa-plugin/go.mod
+ - ../sm-oauthserver-plugin/plugin.yaml
+ - ../sm-oauthserver-plugin/manifest_test.go
+ - ../sm-oauthserver-plugin/go.mod
+ - ../sm-paymentgateway-plugin/plugin.yaml
+ - ../sm-paymentgateway-plugin/manifest_test.go
+ - ../sm-paymentgateway-plugin/go.mod
+ - ../sm-paymentgatewaysubscriptions-plugin/plugin.yaml
+ - ../sm-paymentgatewaysubscriptions-plugin/manifest_test.go
+ - ../sm-paymentgatewaysubscriptions-plugin/go.mod
+ - ../sm-pgstripe-plugin/plugin.yaml
+ - ../sm-pgstripe-plugin/manifest_test.go
+ - ../sm-pgstripe-plugin/subscriptions/plugin.yaml
+ - ../sm-pgstripe-plugin/subscriptions/manifest_test.go
+ - ../sm-pgstripe-plugin/go.mod
+ - ../sm-seo-plugin/plugin.yaml
+ - ../sm-seo-plugin/manifest_test.go
+ - ../sm-seo-plugin/go.mod
+ - ../sm-sitemap-plugin/plugin.yaml
+ - ../sm-sitemap-plugin/manifest_test.go
+ - ../sm-sitemap-plugin/go.mod
+ - ../sm-sociallogin-plugin/plugin.yaml
+ - ../sm-sociallogin-plugin/manifest_test.go
+ - ../sm-sociallogin-plugin/go.mod
+ - ../sm-translate-plugin/plugin.yaml
+ - ../sm-translate-plugin/manifest_test.go
+ - ../sm-translate-plugin/go.mod
+ - ../sm-user-plugin/plugin.yaml
+ - ../sm-user-plugin/manifest_test.go
+ - ../sm-user-plugin/go.mod
+ - ../sm-chatbots-plugin/plugin.yaml
+ - ../sm-chatbots-plugin/manifest_test.go
+ - ../sm-chatbots-plugin/go.mod
+ - ../sm-chatchannels-plugin/plugin.yaml
+ - ../sm-chatchannels-plugin/manifest_test.go
+ - ../sm-chatchannels-plugin/go.mod
+ - ../sm-chatvideo-plugin/plugin.yaml
+ - ../sm-chatvideo-plugin/manifest_test.go
+ - ../sm-chatvideo-plugin/go.mod
+ - ../sm-forum-plugin/plugin.yaml
+ - ../sm-forum-plugin/manifest_test.go
+ - ../sm-forum-plugin/go.mod
+ - ../sm-herald-plugin/plugin.yaml
+ - ../sm-herald-plugin/manifest_test.go
+ - ../sm-herald-plugin/go.mod
+ - ../sm-userfriends-plugin/plugin.yaml
+ - ../sm-userfriends-plugin/manifest_test.go
+ - ../sm-userfriends-plugin/go.mod
+autonomous: false
+requirements: [SC-1, D-01, D-02, D-03, D-04, D-05]
+
+estimate:
+ tokens: 90000
+ raw_tokens: 90000
+ tasks: 5
+ confidence: low
+
+must_haves:
+ truths:
+ - "Every public sm-*-plugin repository checked out next to the framework (20 repositories, 21 plugin packages including sm-pgstripe-plugin/subscriptions) commits a plugin.yaml generated by `summer plugin:manifest`, never hand-edited (SC-1, D-01, D-02)"
+ - "Each plugin.yaml carries id, module, package, name, description, framework and requires derived from the repo; READMEs whose H1 is the plugin ID get a persisted `--name` override, the others keep their README H1 (D-02, D-03, research F7)"
+ - "Each repository's go.mod requires the framework release that contains kiosk (v0.6.0 or the number confirmed at 34-01 Task 1), which therefore is its `framework:` minimum (user decision 2 of 2026-10-29, D-05)"
+ - "Each repository's `go test` runs TestPluginManifest, which calls kiosk.CheckT and fails when plugin.yaml is stale (D-04)"
+ - "A plugin requirement at v0.0.0 (sm-chatbots-plugin on sm-chat-plugin) is replaced by a released version, or the repository is left without plugin.yaml and the reason is recorded (research F8)"
+ - "Each listed repository answers an anonymous `git ls-remote` over HTTPS (SC-1)"
+ - "No plugin repository is pushed or tagged by this plan; plugin commits come first and app submodule pointer bumps are separate commits made only after Jakub reports the push (CLAUDE.md, user decision 2)"
+ artifacts:
+ - path: "../sm-journal-plugin/plugin.yaml"
+ provides: "tracer manifest"
+ contains: "id: golem15.journal"
+ - path: "../sm-journal-plugin/manifest_test.go"
+ provides: "TestPluginManifest"
+ contains: "kiosk.CheckT"
+ - path: "../sm-pgstripe-plugin/subscriptions/plugin.yaml"
+ provides: "nested plugin manifest"
+ contains: "package: git.golem15.com/golem15/sm-pgstripe-plugin/subscriptions"
+ key_links:
+ - from: "../sm-*-plugin/manifest_test.go"
+ to: "modules/kiosk/check.go"
+ via: "TestPluginManifest calls kiosk.CheckT(t, \".\") against the local framework through the repo's replace"
+ pattern: "kiosk\\.CheckT\\(t, \"\\.\"\\)"
+---
+
+## Phase Goal
+
+People browse the public Golem15 plugins at summercms.io/marketplace and install one with `summer plugin:install `. For the registry to list a plugin, its repository must carry a valid plugin.yaml at a tag. This plan puts a generated, test-guarded plugin.yaml into every public plugin repository; the tags that publish them are user-gated and follow the framework release.
+
+
+Roll out plugin.yaml and its check test to every public `sm-*-plugin` repository checked out next to the framework (user decision 5: these are the launch set; quizzes, presentations, golem and feedback have no sibling checkout and are out of this phase), using the 34-01 command. Untagged repositories (chatbots, chatchannels, chatvideo, forum, herald, userfriends) get plugin.yaml too but are not listed until Jakub tags them (D-14).
+
+Purpose: SC-1 and the input the registry ingester (34-03) and git-URL installs (34-04) read.
+
+Output: one commit per plugin repository (plugin.yaml, manifest_test.go, go.mod/go.sum), and, only after Jakub pushes, one pointer-bump commit per app. No push, no tag.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
+@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
+@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
+@.planning/phases/34-plugin-marketplace/34-03-SUMMARY.md
+@.planning/phases/34-plugin-marketplace/34-05-SUMMARY.md
+@.planning/notes/core-plugins-own-repos.md
+@CLAUDE.md
+
+
+From 34-01: `summer plugin:manifest [dir] [--check] [--pretend] [--name=…] [--description=…]`; `kiosk.CheckT(t TB, dir string)` (TB = Helper + Fatalf); plugin.yaml schema 1 per plugin package directory. Use the names in 34-01-SUMMARY.md where Task 1 adjusted them.
+Every sibling plugin go.mod replaces git.golem15.com/golem15/summercms with ../summercms.go, so a require of the unreleased framework version builds locally against this checkout.
+README H1 equals the plugin ID in faq, journal, menu, mfa, oauthserver, paymentgateway, paymentgatewaysubscriptions, pgstripe, pgstripe/subscriptions, seo, sitemap, sociallogin, translate, user (research F7). Human H1 in chat ("Chat"), chatbots ("Chat bots"), chatchannels ("ChatChannels"), chatvideo ("Chat video"), forum ("Forum"), herald ("Herald"), userfriends ("Friends").
+sm-chatbots-plugin/go.mod requires git.golem15.com/golem15/sm-chat-plugin v0.0.0 with a local replace (research F8); sm-chat-plugin's newest tag is v0.1.3.
+Apps mounting these plugins as submodules: sm-demo-app, sm-grzybyfunkcjonalne-app, sm-bm-app, sm-summercmsio-app, fonoteka.go.
+Wave order: this plan runs after 34-03 and 34-05 (declared in depends_on) because every plugin test compiles kiosk and most compile cabana from ../summercms.go, which 34-04 and 34-05 edit, and because Task 5 commits gitlinks in sm-summercmsio-app, where 34-03 commits. 34-03-SUMMARY.md lists the unpushed site plugin (sm-summercmsio-plugin) and vue-summercmsio-app commits whose gitlinks sm-summercmsio-app still has to record.
+
+
+
+## Execution units and resume points
+
+- Build the CLI once from this checkout into the executor's scratch directory (`go build -o /summer ./cmd/summer`) and call that binary; never `go install` over Jakub's installed summer.
+- Per repository: `git -C status --short` first. If another session has uncommitted changes in go.mod, go.sum, plugin.go or README.md, skip the repository, record it in the SUMMARY and retry it at the end of the task. Stage only the files named here, by path. No co-author tag.
+- Commit subject in each plugin repository: `feat: plugin.yaml manifest for the SummerCMS marketplace`. Resume check per repository: `git -C log --format=%s | grep -Fx ''`.
+
+| Task | Commits | Resume point |
+|------|---------|--------------|
+| 1 | sm-journal-plugin | TestPluginManifest passes in sm-journal-plugin |
+| 2 | 13 tagged repositories (pgstripe once, both plugin dirs) | every listed repo's TestPluginManifest passes |
+| 3 | 6 untagged repositories | same, or the recorded F8 skip |
+| 4 | none (answer recorded in the SUMMARY) | answer recorded |
+| 5 | one `chore: bump plugin pointers for plugin.yaml` per app, or none when deferred | pointers recorded |
+
+
+
+
+ Task 1: End-to-end "sm-journal-plugin commits a generated plugin.yaml that its own go test guards" — one repository only
+ ../sm-journal-plugin/plugin.yaml, ../sm-journal-plugin/manifest_test.go, ../sm-journal-plugin/go.mod, ../sm-journal-plugin/go.sum
+
+ - .planning/phases/34-plugin-marketplace/34-01-SUMMARY.md (confirmed names, release number, anything adjusted)
+ - modules/kiosk/README.md (CheckT usage)
+ - ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/go.mod, ../sm-journal-plugin/README.md (H1 and summary)
+ - ../sm-faq-plugin/commands_documented_test.go (the plugin-test style that imports a framework helper)
+
+
+ Per D-01..D-05 and user decision 2. In ../sm-journal-plugin: set the framework requirement to the release number confirmed at 34-01 Task 1 with `go -C ../sm-journal-plugin mod edit -require=git.golem15.com/golem15/summercms@`; add `manifest_test.go` in the package of plugin.go with `func TestPluginManifest(t *testing.T) { kiosk.CheckT(t, ".") }` and the kiosk import; run `go -C ../sm-journal-plugin mod tidy` and review the go.mod diff (new indirect requirements for kiosk's dependencies are expected; if tidy removes or downgrades an existing requirement, stop and record it); run `/summer plugin:manifest --name=Journal ../sm-journal-plugin` (README H1 is the ID). Confirm the test guards the file: change one word of plugin.yaml's description, run TestPluginManifest and see it fail, then restore with `git -C ../sm-journal-plugin checkout -- plugin.yaml` or by regenerating. Commit plugin.yaml, manifest_test.go, go.mod and go.sum in ../sm-journal-plugin.
+
+
+ go -C ../sm-journal-plugin test -run TestPluginManifest -count=1 -v . 2>&1 | grep -E '^(--- |FAIL|ok|PASS)'
+ a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginManifest" line
+ go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -short ./... 2>&1 | tail -5
+ non-zero exit or a "FAIL" line
+ go run ./cmd/summer plugin:manifest --check ../sm-journal-plugin && grep -E '^(id|name|framework):' ../sm-journal-plugin/plugin.yaml
+ non-zero exit, or no "id: golem15.journal" and "name: Journal" lines
+ GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-journal-plugin.git | head -1
+ non-zero exit, "Authentication failed" in the output, or empty output
+
+
+ - `grep -n "git.golem15.com/golem15/summercms v0.6.0" ../sm-journal-plugin/go.mod` prints one line (or the confirmed release number)
+ - `grep -n "framework: v0.6.0" ../sm-journal-plugin/plugin.yaml` prints one line and `grep -n "golem15.translate" ../sm-journal-plugin/plugin.yaml` prints the requirement with a vX.Y.Z minimum
+ - `grep -n "overrides:" ../sm-journal-plugin/plugin.yaml` prints one line
+ - `git -C ../sm-journal-plugin log -1 --format=%s` is the subject from Execution units and `git -C ../sm-journal-plugin show --stat HEAD` lists only the four files
+ - `git -C ../sm-journal-plugin tag --points-at HEAD` prints nothing (no tag)
+
+ One real plugin repository carries a generated, test-guarded plugin.yaml and builds against the local framework.
+
+
+
+ Task 2: The 13 other tagged plugin repositories carry plugin.yaml and TestPluginManifest
+ ../sm-chat-plugin/plugin.yaml, ../sm-chat-plugin/manifest_test.go, ../sm-chat-plugin/go.mod, ../sm-faq-plugin/plugin.yaml, ../sm-faq-plugin/manifest_test.go, ../sm-faq-plugin/go.mod, ../sm-menu-plugin/plugin.yaml, ../sm-menu-plugin/manifest_test.go, ../sm-menu-plugin/go.mod, ../sm-mfa-plugin/plugin.yaml, ../sm-mfa-plugin/manifest_test.go, ../sm-mfa-plugin/go.mod, ../sm-oauthserver-plugin/plugin.yaml, ../sm-oauthserver-plugin/manifest_test.go, ../sm-oauthserver-plugin/go.mod, ../sm-paymentgateway-plugin/plugin.yaml, ../sm-paymentgateway-plugin/manifest_test.go, ../sm-paymentgateway-plugin/go.mod, ../sm-paymentgatewaysubscriptions-plugin/plugin.yaml, ../sm-paymentgatewaysubscriptions-plugin/manifest_test.go, ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-pgstripe-plugin/plugin.yaml, ../sm-pgstripe-plugin/manifest_test.go, ../sm-pgstripe-plugin/subscriptions/plugin.yaml, ../sm-pgstripe-plugin/subscriptions/manifest_test.go, ../sm-pgstripe-plugin/go.mod, ../sm-seo-plugin/plugin.yaml, ../sm-seo-plugin/manifest_test.go, ../sm-seo-plugin/go.mod, ../sm-sitemap-plugin/plugin.yaml, ../sm-sitemap-plugin/manifest_test.go, ../sm-sitemap-plugin/go.mod, ../sm-sociallogin-plugin/plugin.yaml, ../sm-sociallogin-plugin/manifest_test.go, ../sm-sociallogin-plugin/go.mod, ../sm-translate-plugin/plugin.yaml, ../sm-translate-plugin/manifest_test.go, ../sm-translate-plugin/go.mod, ../sm-user-plugin/plugin.yaml, ../sm-user-plugin/manifest_test.go, ../sm-user-plugin/go.mod
+
+ - ../sm-journal-plugin/manifest_test.go (Task 1, the template)
+ - each repository's plugin.go (package name, ID/Requires form) and README.md (H1) before generating
+ - ../sm-pgstripe-plugin/subscriptions/plugin.go (nested plugin, package name)
+
+
+ Repeat Task 1 for each repository, in dependency order so a required plugin's repository is done before its dependants: translate, user, chat, faq, menu, seo, sitemap, mfa, oauthserver, sociallogin, paymentgateway, paymentgatewaysubscriptions, pgstripe. Names per D-03 for READMEs whose H1 is the ID (Claude's discretion, reversible with a later `--name`): faq "FAQ", menu "Menu", mfa "Multi-factor authentication", oauthserver "OAuth server", paymentgateway "Payment gateway", paymentgatewaysubscriptions "Payment gateway subscriptions", pgstripe "Stripe payments", pgstripe/subscriptions "Stripe subscriptions", seo "SEO", sitemap "Sitemap", sociallogin "Social login", translate "Translate", user "User"; chat keeps its README H1 (no override). In sm-pgstripe-plugin generate both `plugin.yaml` (root) and `subscriptions/plugin.yaml` (research F6), add a manifest test in each of the two packages (the subscriptions test calls `kiosk.CheckT(t, ".")` from its own directory), and commit both in one commit. Stage go.sum too where tidy changed it (it is not listed per repository above when unchanged). Record in the SUMMARY a table of repository, plugin ID, name, framework minimum, requires and commit SHA.
+
+
+ for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do go -C ../sm-$r-plugin test -run TestPluginManifest -count=1 ./... >/dev/null 2>&1 || echo "FAIL $r"; done; echo done
+ any "FAIL " line before "done"
+ for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do (go -C ../sm-$r-plugin vet ./... && go -C ../sm-$r-plugin test -short ./...) >/dev/null 2>&1 || echo "FAIL $r"; done; echo done
+ any "FAIL " line before "done"
+ for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-$r-plugin.git >/dev/null 2>&1 || echo "FAIL $r"; done; echo done
+ any "FAIL " line before "done"
+
+
+ - `ls ../sm-*-plugin/plugin.yaml ../sm-pgstripe-plugin/subscriptions/plugin.yaml | wc -l` is at least 15 after this task (journal plus these 13 plus subscriptions)
+ - `grep -h '^id:' ../sm-{chat,faq,menu,mfa,oauthserver,paymentgateway,paymentgatewaysubscriptions,pgstripe,seo,sitemap,sociallogin,translate,user}-plugin/plugin.yaml | sort -u | wc -l` is 13
+ - `grep -L '^name: golem15\.' ../sm-*-plugin/plugin.yaml` lists every plugin.yaml (no display name equals an ID)
+ - every repository's latest commit has the Execution units subject and no tag points at it
+
+ Every tagged public plugin is ready to publish its manifest at its next tag.
+
+
+
+ Task 3: The 6 untagged plugin repositories carry plugin.yaml, with the v0.0.0 chat requirement resolved
+ ../sm-chatbots-plugin/plugin.yaml, ../sm-chatbots-plugin/manifest_test.go, ../sm-chatbots-plugin/go.mod, ../sm-chatchannels-plugin/plugin.yaml, ../sm-chatchannels-plugin/manifest_test.go, ../sm-chatchannels-plugin/go.mod, ../sm-chatvideo-plugin/plugin.yaml, ../sm-chatvideo-plugin/manifest_test.go, ../sm-chatvideo-plugin/go.mod, ../sm-forum-plugin/plugin.yaml, ../sm-forum-plugin/manifest_test.go, ../sm-forum-plugin/go.mod, ../sm-herald-plugin/plugin.yaml, ../sm-herald-plugin/manifest_test.go, ../sm-herald-plugin/go.mod, ../sm-userfriends-plugin/plugin.yaml, ../sm-userfriends-plugin/manifest_test.go, ../sm-userfriends-plugin/go.mod
+
+ - ../sm-chatbots-plugin/go.mod (the v0.0.0 chat requirement and its replace)
+ - ../sm-userfriends-plugin/go.mod (pseudo-version requirement, accepted with a warning)
+ - each repository's plugin.go and README.md
+
+
+ Repeat Task 1 for chatchannels, chatvideo, forum, herald, userfriends and chatbots (no name overrides: their H1s are human names). For sm-chatbots-plugin (research F8): set the sm-chat-plugin requirement to v0.1.3 (its newest tag) with `go mod edit -require`, keep the local replace, and confirm chatbots still builds against v0.1.3's API by building in a scratch copy with the chat replace dropped (`GOFLAGS=-mod=mod`, network allowed for the proxy). If it does not build against v0.1.3, restore go.mod, leave chatbots without plugin.yaml, and record in the SUMMARY that chatbots needs a sm-chat-plugin tag before it can publish (it is untagged and unlisted, so SC-1 lists it as pending, not failed). Pseudo-version warnings (userfriends) are expected; record them.
+
+
+ for r in chatbots chatchannels chatvideo forum herald userfriends; do test -f ../sm-$r-plugin/plugin.yaml || { echo "SKIP $r"; continue; }; (go -C ../sm-$r-plugin vet ./... && go -C ../sm-$r-plugin test -short ./... && go -C ../sm-$r-plugin test -run TestPluginManifest -count=1 ./...) >/dev/null 2>&1 || echo "FAIL $r"; done; echo done
+ any "FAIL " line, or a "SKIP" line for a repository other than chatbots
+ for r in chatbots chatchannels chatvideo forum herald userfriends; do GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-$r-plugin.git >/dev/null 2>&1 || echo "FAIL $r"; done; echo done
+ any "FAIL " line before "done"
+
+
+ - `ls ../sm-*-plugin/plugin.yaml | wc -l` is 20, or 19 with the chatbots skip recorded in the SUMMARY with its build error
+ - `grep -n "sm-chat-plugin v0.0.0" ../sm-chatbots-plugin/go.mod` prints nothing when chatbots has a plugin.yaml
+ - no repository is tagged by this task (`git -C ../sm-forum-plugin tag` prints nothing)
+
+ All public plugin repositories carry plugin.yaml (or a recorded, explained exception) and are ready for their first tag.
+
+
+
+ Task 4: Push the framework and plugin commits now, or defer the app pointer bumps to the release
+ Jakub pushes (no tags) the framework master (34-01, 34-04 and 34-05 commits), the plugin repositories committed in Tasks 1-3, and the sm-summercmsio-plugin and vue-summercmsio-app commits from 34-03, so the apps' submodule pointers can be bumped in this plan; or the pointer bumps move to the release ledger 34-06 writes.
+
+ CLAUDE.md: plugin commits are pushed first, then the app pointer bump is its own commit; plans never push or tag. Every plugin now requires framework v0.6.0, which is not tagged yet: apps build in workspace mode (all five replace the framework locally), but app release builds and module consumers need the v0.6.0 tag first. The SUMMARY lists every repository with its new commit SHA in push order (framework, then translate, user, chat, the other plugins, then sm-summercmsio-plugin and vue-summercmsio-app).
+
+
+
+
+
+ Reply "pushed: " or "defer"
+
+
+
+ Task 5: App submodule pointers follow the pushed plugin commits, or are recorded for the release
+ none in this repository; app gitlinks under ../sm-demo-app, ../sm-grzybyfunkcjonalne-app, ../sm-bm-app, ../sm-summercmsio-app (plugin submodules plus plugins/golem15/summercms and vue-summercmsio-app), ../fonoteka.go when "pushed"
+
+ - the Task 4 answer
+ - each app's .gitmodules (which plugins it mounts)
+ - .planning/phases/34-plugin-marketplace/34-03-SUMMARY.md (pending site plugin and vue app SHAs)
+ - ../sm-summercmsio-app/CLAUDE.md (submodule rules; use ssu for status)
+
+
+ If Task 4 answered "defer": write the pending bumps (app, submodule path, target SHA) into the SUMMARY under "Pending pointer bumps" and stop. If "pushed": for each app and each mounted repository that Jakub pushed (in sm-summercmsio-app this includes plugins/golem15/summercms and vue-summercmsio-app at the 34-03 SHAs), run `git -C / fetch origin` and check out the pushed SHA, confirm `git -C status --short` shows only those gitlinks (plus any other session's own edits, which stay unstaged), run `go -C build ./...` in workspace mode, and commit only the gitlinks with `chore: bump plugin pointers for plugin.yaml`. Do not run summer build in the apps and do not commit regenerated main.go or plugins.gen.go: that regeneration belongs to each app's framework upgrade at release (research F10). Never push the apps.
+
+
+ for a in sm-demo-app sm-grzybyfunkcjonalne-app sm-bm-app sm-summercmsio-app fonoteka.go; do test -d ../$a || continue; go -C ../$a build ./... >/dev/null 2>&1 || echo "FAIL $a"; done; echo done
+ any "FAIL " line before "done"
+
+
+ - With "pushed": every bumped submodule's HEAD equals a SHA Jakub listed, and `git -C show --stat HEAD` lists only gitlinks
+ - With "defer": the SUMMARY contains a "Pending pointer bumps" table with app, path and SHA for every mounted plugin repository changed in Tasks 1-3
+ - No app is pushed (`git -C ../sm-demo-app status -sb | head -1` shows no change in the ahead count beyond the new commit)
+
+ Apps either track the new plugin commits or the release has an exact list of pointer bumps to make.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| plugin repository → registry and installers | plugin.yaml in a tag is read by summercms.io and by `summer plugin:install` |
+| developer workstation → shared remotes | pushes and tags (user-gated) |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-34-07 | Tampering | hand-edited or stale plugin.yaml | medium | mitigate | the file is generated only; TestPluginManifest in each repo fails on drift (Task 1 proves the failure) |
+| T-34-08 | Spoofing | a manifest claiming another module or ID | medium | mitigate | module and package come from go.mod and the directory; IDs from plugin.go; the ingester re-checks module == repository path (34-03) |
+| T-34-09 | Tampering | app pointers referencing unpushed commits | high | mitigate | pointer bumps only after Jakub reports the push (Task 4), and only gitlinks are committed |
+| T-34-10 | Denial of service | consumers resolving the unreleased framework minimum | medium | accept | plugins are not tagged until v0.6.0 exists (release order recorded in 34-06's ledger); existing tags are never moved |
+| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | no package is installed; go.sum changes come from `go mod tidy` against modules the framework already requires |
+
+
+
+- Every listed repository: `go vet ./...`, `go test -short ./...` and `TestPluginManifest` pass; `summer plugin:manifest --check` passes.
+- Anonymous `git ls-remote` succeeds for every listed repository.
+- No tags or pushes made by the agent.
+
+
+
+- 20 public plugin repositories (21 plugin packages) carry a generated plugin.yaml and its test, or a recorded F8 exception for chatbots.
+- Each requires the framework release containing kiosk.
+- App pointers are bumped after a reported push, or listed for the release.
+
+
+
+
+## Artifacts this phase produces
+
+- Files: `plugin.yaml` and `manifest_test.go` in each of ../sm-{chat,chatbots,chatchannels,chatvideo,faq,forum,herald,journal,menu,mfa,oauthserver,paymentgateway,paymentgatewaysubscriptions,pgstripe,seo,sitemap,sociallogin,translate,user,userfriends}-plugin, plus `subscriptions/plugin.yaml` and `subscriptions/manifest_test.go` in sm-pgstripe-plugin.
+- Tests: `TestPluginManifest` in every listed repository.
+- go.mod: framework requirement raised to the confirmed release (v0.6.0); sm-chatbots-plugin's sm-chat-plugin requirement raised from v0.0.0 to v0.1.3 when it builds.
+- Display names (plugin.yaml `overrides.name`): FAQ, Journal, Menu, Multi-factor authentication, OAuth server, Payment gateway, Payment gateway subscriptions, Stripe payments, Stripe subscriptions, SEO, Sitemap, Social login, Translate, User.
+- Config keys, CLI commands, types: none.
diff --git a/.planning/phases/34-plugin-marketplace/34-03-PLAN.md b/.planning/phases/34-plugin-marketplace/34-03-PLAN.md
new file mode 100644
index 00000000..b7c964d8
--- /dev/null
+++ b/.planning/phases/34-plugin-marketplace/34-03-PLAN.md
@@ -0,0 +1,409 @@
+---
+phase: 34-plugin-marketplace
+plan: 03
+type: execute
+wave: 2
+depends_on: ["34-01"]
+coupling_justified: ["34-04: same wave; 34-04 adds modules/kiosk/client.go and edits internal/build and cmd/summer in the framework checkout this plan compiles through go.work; only the kiosk package is shared, the change is one new file and each 34-04 commit compiles"]
+files_modified:
+ - ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/go.mod
+ - ../sm-summercmsio-app/plugins/golem15/summercms/go.sum
+ - ../sm-summercmsio-app/plugins/golem15/summercms/README.md
+ - ../sm-summercmsio-app/plugins/golem15/summercms/updates/registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/repository.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/version.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/columns.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/fields.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/columns.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/fields.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/registry/git.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/registry/query.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_list.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_form.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_list.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_form.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/console/registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/jobs.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/schedule.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/token_scopes.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/admin.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/admin_navigation.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/admin_permissions.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/lang.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/lang/en/lang.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/lang/pl/lang.yaml
+ - ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/testmain_test.go
+ - ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go
+ - ../sm-summercmsio-app/config/queue.yaml
+ - ../sm-summercmsio-app/config/http.yaml
+ - ../sm-summercmsio-app/deploy/nginx/summercms.io.conf
+ - ../sm-summercmsio-app/deploy/supervisor/summercms-io-changelog.conf
+ - ../sm-summercmsio-app/deploy/changelog.env.example
+ - ../sm-summercmsio-app/scripts/changelog-post.py
+ - ../sm-summercmsio-app/scripts/test_changelog_post.py
+ - ../sm-summercmsio-app/scripts/smoke.sh
+ - ../sm-summercmsio-app/DEPLOY.md
+ - ../sm-summercmsio-app/README.md
+ - ../sm-summercmsio-app/CLAUDE.md
+ - ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue
+ - ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/nuxt.config.ts
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/i18n/locales/en.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/index.vue
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/[vendor]/[name].vue
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useRegistry.ts
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/marketplace.ts
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/marketplace-smoke.mjs
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/expect-marketplace.txt
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugins.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugin-golem15.journal.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/versions-golem15.journal.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/package.json
+ - ../sm-summercmsio-app/vue-summercmsio-app/README.md
+autonomous: true
+requirements: [SC-2, D-04, D-13, D-14, D-15, D-16]
+user_setup:
+ - service: summercms-io-registry-token
+ why: "changelog-post.py calls the ingest-now endpoint after a plugin release; without a token scoped `registry` the call answers 403, is tolerated, and the hourly ingest picks the tag up"
+ dashboard_config:
+ - task: "Mint a personal API token with the scope `registry` (Account → API tokens on the summercms.io admin, through the SSH tunnel) and store it in ~/.config/summercms/registry-token with mode 600; the agent never mints, prints or copies tokens"
+ location: "summercms.io admin over the SSH tunnel (DEPLOY.md)"
+ - service: rome-git
+ why: "the ingester shells out to git; production must have it"
+ dashboard_config:
+ - task: "Before the deploy that ships the registry, confirm `command -v git` on rome (DEPLOY.md step added in Task 4)"
+ location: "rome (debian@10.8.3.118 -p 2222)"
+
+estimate:
+ tokens: 190000
+ raw_tokens: 190000
+ tasks: 4
+ confidence: low
+
+must_haves:
+ truths:
+ - "A summercms.io administrator curates the listing by adding a repository URL; the ingester fills every fact from the repository and a plugin is published only once one of its vX.Y.Z tags carries a valid plugin.yaml; tags without one are skipped (D-13, D-14)"
+ - "The ingester clones anonymously over HTTPS into a temporary bare partial clone, reads tags, each tag's plugin.yaml files and the latest tag's README, never checks out a tree, never recurses into submodules, caps every blob it reads, and refuses a plugin.yaml whose module differs from the repository path or whose ID another listed repository already owns (D-13, research Pitfall 13)"
+ - "Every vX.Y.Z tag becomes a version row with its framework minimum and plugin minimums; the README is stored for the latest tag only, as markdown and as breakwater-sanitized HTML (D-15)"
+ - "Ingestion runs hourly as a scheduled console command and on demand through POST /api/registry/v1/ingest, which needs a backend principal holding golem15.summercms.registry_ingest through a token scoped `registry`; no forge webhooks (D-13, user decision 3)"
+ - "GET /api/registry/v1/plugins?q=&category=, /plugins/{id} and /plugins/{id}/versions answer without auth with open CORS, Cache-Control and an ETag, encoded with kiosk's wire types so their shape is exactly the one frozen at 34-01 Task 1 (D-16)"
+ - "Category, featured flag, screenshots, icon and sort order are edited in the summercms.io admin and appear in the API; the facts (versions, requirements, module, README) are read-only there (SC-2, decision note)"
+ - "/marketplace lists, searches and filters plugins by category and /marketplace// shows the detail with the install command, README and versions, rendered by the changelog Node SSR app from the registry API (SC-2)"
+ - "`changelog-post.py publish … --repo` refuses a plugin release whose committed plugin.yaml fails `summer plugin:manifest --check`, and after a successful post asks the registry to ingest now, treating a 401/403/404 or network failure as a warning (D-04, D-13, user decision 3)"
+ - "Production config serves jobs (queue.work_in_serve true), nginx routes /marketplace to the Node app and allows POST only on the ingest path, and DEPLOY.md requires git on the server (research F10, Pitfalls 9-10)"
+ artifacts:
+ - path: "../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go"
+ provides: "pull ingester"
+ contains: "breakwater.Render"
+ - path: "../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go"
+ provides: "public registry API and ingest-now endpoint"
+ contains: "kiosk.ListResponse"
+ - path: "../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go"
+ provides: "registry tables"
+ contains: "golem15_summercms_registry_versions"
+ - path: "../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go"
+ provides: "TestRegistrySmoke (tracer)"
+ contains: "func TestRegistrySmoke("
+ - path: "../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/index.vue"
+ provides: "marketplace list page"
+ contains: "plugin:install"
+ - path: "../sm-summercmsio-app/scripts/changelog-post.py"
+ provides: "manifest check and ingest-now hooks"
+ contains: "api/registry/v1/ingest"
+ key_links:
+ - from: "../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go"
+ to: "modules/kiosk/manifest.go"
+ via: "each tag's plugin.yaml goes through kiosk.Parse and kiosk.Validate before a version row is written"
+ pattern: "kiosk\\.Parse\\("
+ - from: "../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go"
+ to: "modules/kiosk/registry.go"
+ via: "responses are kiosk.ListResponse, kiosk.DetailResponse, kiosk.VersionsResponse and kiosk.ErrorResponse values"
+ pattern: "kiosk\\.(ListResponse|DetailResponse|VersionsResponse|ErrorResponse)"
+ - from: "../sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useRegistry.ts"
+ to: "/api/registry/v1"
+ via: "SSR fetches through runtimeConfig.registry.apiBase (NUXT_REGISTRY_API_BASE), browser fetches through the same-origin path"
+ pattern: "registry"
+---
+
+## Phase Goal
+
+People browse the public Golem15 plugins at summercms.io/marketplace. This plan builds the marketplace itself: the registry that ingests plugin repositories into Postgres, its public read-only API, the admin that curates and decorates the listing, the pages, the deploy configuration and the release-script hooks.
+
+
+In the summercms.io application (`../sm-summercmsio-app`) and its two submodules, the site plugin `golem15.summercms` (`plugins/golem15/summercms`, repository sm-summercmsio-plugin) and `vue-summercmsio-app`:
+- registry tables, models and a pull ingester (D-13, D-14, D-15);
+- the public API at /api/registry/v1 using kiosk's wire types (D-16, shape frozen at 34-01 Task 1);
+- admin curation and presentation fields, the `registry` token scope and the ingest-now endpoint (user decision 3);
+- hourly scheduled ingestion with jobs enabled in serve;
+- /marketplace pages in the changelog Node SSR app (research A4), a landing header link;
+- deploy configs, DEPLOY.md and the changelog-post.py hooks (D-04, D-13).
+
+Launch listing (user decision 5): the tagged public plugins checked out next to the framework; the curated URLs are recorded in DEPLOY.md and added after the plugins' first plugin.yaml tags exist (user-gated release, ledger in 34-06).
+
+Output: commits in the site plugin repository, the vue app repository and the app repository (each its own commits). Submodule pointer bumps in sm-summercmsio-app are not committed in this plan: they follow Jakub's push (34-02 Task 4) or move to the release ledger (CLAUDE.md). Nothing is pushed, tagged or deployed.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
+@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
+@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
+@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
+@../sm-summercmsio-app/CLAUDE.md
+
+
+From 34-01 (names may be adjusted in its SUMMARY): kiosk.Parse, kiosk.Validate, kiosk.FileName, kiosk.ValidVersion, kiosk.CompareVersions; wire types kiosk.RegistryPlugin, RegistryPluginDetail, RegistryScreenshot, RegistryVersion, ListResponse{Data, Meta{Total}}, DetailResponse, VersionsResponse, ErrorResponse{Error{Code, Message}}, IngestRequest{Repository}, IngestResponse; kiosk.RegistryAPIPath = "/api/registry/v1".
+Framework: breakwater.Render(markdown string) string; cabana.ResolveBackend(app, r, cover...) and cabana.Allows(principal, []string); pact.HasMigrations/HasModels/HasAdminControllers/HasPermissions/HasNavigation/HasTokenScopes/HasJobs/HasSchedule/HasCommands/HasRoutes; pact.ScheduledCommand{Command, Cadence: pact.Every(time.Hour)}; conga.Job[T](fn, opts...) and conga.From(app) for enqueueing; surf middleware "throttle:" and "cors:" with http.cors_profiles. in config; lagoon/attach relations (sm-journal-plugin/models/post.go AttachRelations).
+Templates: ../sm-faq-plugin (updates/registry.go, migrations, models/registry.go, admin.go, admin_navigation.go, admin_permissions.go, controllers/*/config_*.yaml, routes.go, lang/, testmain_test.go with testcontainers), ../sm-journal-plugin (token_scopes.go, controllers/api/auth.go ~83-106), ../sm-chatvideo-plugin/schedule.go.
+Site plugin today: plugin.go declares only pact.HasRoutes; Routes fails closed without the public tree (tests use the fsys field); links_test.go nodePaths = ["/changelog"]; go.mod requires the framework v0.4.0 and the app's go.work replaces it with ../summercms.go.
+App: config/queue.yaml work_in_serve false; nginx denies non-GET outside /_journal/api/v1/ and 404s /backend; changelog Node app on 127.0.0.1:3095 with NUXT_JOURNAL_API_BASE; scripts/changelog-post.py has call(method, path, token, body=None, base=None), cmd_publish, SITE, FRAMEWORK, TOKEN_FILE.
+Changelog app: changelog/nuxt.config.ts (extends '..' and vue-journal-layer, buildAssetsDir /changelog/_nuxt/, runtimeConfig.journal.apiBase, routeRules swr 60, pages:extend removes the landing index); testing via vue-journal-layer's testing/nuxt-server.mjs startNuxtServer({outputDir, apiBase, port, env}) and smoke-ssr.mjs parseExpectLine.
+
+
+
+## Execution units and resume points
+
+- Three repositories, committed separately and in this order within a task: site plugin (`git -C ../sm-summercmsio-app/plugins/golem15/summercms`), vue app (`git -C ../sm-summercmsio-app/vue-summercmsio-app`), app (`git -C ../sm-summercmsio-app`). Stage explicit paths; never stage a submodule gitlink in the app (the gitlinks stay modified until Jakub pushes; record the pending SHAs in the SUMMARY). Check `git status --short` in each repository first and leave other sessions' edits alone. No co-author tag.
+- Docker-backed tests: if the socket is denied, run them through `sudo -u golem` (CLAUDE.md).
+- Resume check: `git -C log --format=%s | grep -Fx ''`.
+
+| Task | Commit subjects (repository) | Resume point |
+|------|------------------------------|--------------|
+| 1 | `feat: plugin registry tables, ingester and plugin detail API` (site) | TestRegistrySmoke/detail passes |
+| 2 | `feat: registry list, search and versions API, hourly ingest` (site); `feat: registry CORS profile and jobs in serve` (app) | TestRegistrySmoke passes |
+| 3 | `feat: registry admin, token scope and ingest-now endpoint` (site); `feat: manifest check and ingest-now in changelog-post` (app) | test_changelog_post.py passes |
+| 4 | `feat: marketplace pages` (vue); `feat: marketplace link` (site, links_test); `feat: marketplace deploy config and docs` (app) | marketplace smoke passes |
+
+
+
+
+ Task 1: End-to-end "a curated repository with a tagged plugin.yaml is served at GET /api/registry/v1/plugins/{id}" — one repository, one tag, one plugin
+ The JSON this endpoint returns is the D-16 contract; its shape was confirmed at 34-01 Task 1 and is encoded only through kiosk's wire types, so this task adds no field of its own.
+ .planning/phases/34-plugin-marketplace/34-01-SUMMARY.md records the 34-01 Task 1 answer that froze the /api/registry/v1 shape (the checkpoint:decision for this one-way door); halt if it is absent
+ ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/go.mod, ../sm-summercmsio-app/plugins/golem15/summercms/go.sum, ../sm-summercmsio-app/plugins/golem15/summercms/updates/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/repository.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/version.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/git.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/query.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/testmain_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/README.md
+
+ - ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, static.go, smoke_test.go (how tests build the plugin without the public tree)
+ - ../sm-faq-plugin/updates/registry.go, ../sm-faq-plugin/updates/202610110001_create_golem15_faq_faqs.go, ../sm-faq-plugin/models/registry.go, ../sm-faq-plugin/models/faq.go, ../sm-faq-plugin/routes.go, ../sm-faq-plugin/controllers/api/faqs.go, ../sm-faq-plugin/testmain_test.go
+ - modules/kiosk/registry.go, modules/kiosk/manifest.go, modules/kiosk/README.md (34-01 output)
+ - modules/breakwater/README.md
+ - internal/build/new.go (~286 runGit: exec pattern)
+ - .planning/phases/34-plugin-marketplace/34-RESEARCH.md ("Hardened git for the ingester", Pattern 7, Pitfall 13)
+
+
+ Per D-13, D-14, D-15, D-16 (shape from 34-01 Task 1). In the site plugin: raise the framework requirement to the release confirmed at 34-01 Task 1 (`go mod edit -require`, then `go mod tidy`; the app's go.work keeps replacing it). Migration `202610300001_create_golem15_summercms_registry` (raw SQL like faq, with Rollback dropping in reverse order) creates `golem15_summercms_registry_repositories` (id, url text unique, module text, enabled bool default true, tags_digest text, last_ingested_at, last_error text, sort timestamps), `golem15_summercms_registry_plugins` (id, repository_id fk, plugin_id text unique, path text, name, description, module, package, latest_version, latest_framework, readme_markdown, readme_html, category text, featured bool default false, sort_order int default 0, published bool default false, timestamps) and `golem15_summercms_registry_versions` (id, plugin_row_id fk on delete cascade, version, framework, requires jsonb, module, package, commit text, tagged_at, unique (plugin_row_id, version)). Models `Repository`, `Plugin`, `Version` with a models registry like faq. `Repository.module` is derived from an https URL (host plus path without `.git` and trailing slash) when saved; it is what plugin.yaml's module must equal.
+ `registry/git.go`: a runner that executes git with exec.CommandContext, `--` before every URL and path, a minimal environment (PATH plus a temporary HOME) and GIT_TERMINAL_PROMPT=0, GIT_ALLOW_PROTOCOL from a package variable `allowedProtocols` (default "https"; the test sets "https:file"), GIT_CONFIG_NOSYSTEM=1, GIT_CONFIG_GLOBAL=/dev/null; `exec.LookPath("git")` failure is a clear error recorded on the repository.
+ `registry/ingest.go`: `Ingester{DB *gorm.DB}` and `Ingester.Repository(ctx, repositoryID uint) error`: clone `--bare --filter=blob:none` into a temp dir (retry without the filter when the server refuses it; remove the dir always), list tags with for-each-ref (peeled commit, creator date), keep strict vX.Y.Z tags (kiosk.ValidVersion), find `(^|/)plugin.yaml$` with ls-tree per tag, read each with `git show :` through io.LimitReader (64 KiB), kiosk.Parse plus kiosk.Validate, require module == repository.module, upsert the plugin row (by plugin_id; reject an ID owned by another repository and record the error), insert the version row; for the newest valid tag, read `/README.md` (cap 512 KiB) into readme_markdown and `breakwater.Render` into readme_html, and set latest_version, latest_framework, name, description, module, package, published = true. A tag without a valid plugin.yaml is skipped and noted in last_error, never fatal (D-14). In this task handle the single-tag path fully; Task 2 adds the change check and the multi-tag/edge rules.
+ `registry/query.go`: `Detail(ctx, db, pluginID) (kiosk.RegistryPluginDetail, bool, error)`. `controllers/api/registry.go`: `Show` handler writing `kiosk.DetailResponse` or 404 `kiosk.ErrorResponse{not_found}`. `routes_registry.go`: `registerRegistryRoutes(r pact.Router, app)` mounting `GET /api/registry/v1/plugins/{id}` (callable from tests without the public tree); plugin.go calls it from Routes and declares HasMigrations and HasModels (update the package doc: the plugin now has registry tables). testmain_test.go starts testcontainers Postgres like faq (flag.Parse, testing.Short skip). `registry_smoke_test.go` `TestRegistrySmoke/detail`: create a source repo with git in t.TempDir (commit a plugin.yaml produced by kiosk.Marshal for module `example.test/acme/sm-acme-plugin`, ID `acme.blog`, plus a README), tag v0.1.0, `git clone --bare` it, insert a repository row with url `file://` and module `example.test/acme/sm-acme-plugin`, run the Ingester, then GET /api/registry/v1/plugins/acme.blog through an httptest server and assert latest_version v0.1.0, framework, readme_html non-empty and free of `
+
+
+
+```
+Same Node SSR app; new `pages/marketplace/index.vue` and `[id].vue` with a component like `ChangelogIndex`; runtimeConfig registry base mirrors `NUXT_JOURNAL_API_BASE` (supervisor conf); nginx `/marketplace` -> :3095 as for `/changelog`.
+
+### Plan 04 — `plugin:install` / `plugin:update`
+
+**Analog:** `addPluginCommand` (above) + `build.AddPlugin` (`internal/build/scaffold.go:144`). Prompt: do NOT use `bonfire.ConfirmProceed` (`modules/bonfire/helpers.go:144-149` returns true off a terminal). Use:
+```go
+if !yes {
+ if !bonfire.Interactive(out) { return fmt.Errorf("plugin:install: non-interactive run needs --yes") }
+ ok, err := out.Confirm(question, false)
+ ...
+}
+```
+Validate registry-provided module paths with `validateModulePath` (`internal/build/manifest.go:270-292`) and versions with `^v\d+\.\d+\.\d+$` before `go get`. go.work overrides via `go work edit -replace=` (F2), invoked like `runGoWorkUse`.
+
+### Plan 05 — cabana Plugins endpoints
+
+**Handler analog:** `modules/cabana/http.go:560-568`:
+```go
+func (s *service) settingsList(w http.ResponseWriter, r *http.Request) {
+ principal, ok := bouncer.User(r.Context())
+ if !ok || principal == nil || !principal.Backend {
+ WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated)
+ return
+ }
+ ...
+ WriteData(w, http.StatusOK, settings, map[string]any{"locale": ...})
+}
+```
+Add permission check for the new key (403 via `WriteError`). Routes go into the `r.GroupRaw(api, []string{"backend", "locale.from-principal"}, ...)` block at `http.go:316-339`: `g.Get("/plugins", s.pluginsInstalled)`, `g.Get("/plugins/marketplace", s.pluginsMarketplace)`.
+
+**Permission analog:** `modules/cabana/backend_screens.go:16-25, 80-87`:
+```go
+PermissionManageUsers = "backend.manage_users"
+...
+func (*backendScreens) Permissions() []pact.Permission {
+ return []pact.Permission{
+ {Code: PermissionManageUsers, Tab: "backend::lang.permissions.tab_system", Label: "backend::lang.permissions.manage_users"},
+ ...
+```
+Add `PermissionViewPlugins = "backend.view_plugins"` (no Roles; superusers pass all checks). Strings in `modules/phrasebook/backend/lang/{en,pl}/lang.yaml`.
+
+**swag analog:** `modules/cabana/admin_openapi.go:469-479`:
+```go
+// AdminSettingsList documents GET /settings.
+//
+// @Summary List admin settings
+// @Tags admin
+// @Produce json
+// @Security BackendBearer
+// @Success 200 {object} Envelope[[]SettingsEntry]
+// @Failure 401 {object} ErrorEnvelope
+// @Failure 403 {object} ErrorEnvelope
+// @Router /settings [get]
+```
+Then `scripts/check-admin-openapi.sh`; update `phase09_contract_test.go` / `security_coverage_test.go` inventories. Check for in-flight cabana edits from other sessions first.
+
+### Plan 05 — boardwalk view
+
+- Router (`admin/src/app/router.ts:70-74`): `{ path: '/settings', name: 'settings', component: SettingsIndexView, meta: { shell: true } }` — add `{ path: '/system/plugins', name: 'plugins', component: PluginsView, meta: { shell: true } }` before the `/:vendor/:plugin/:controller` routes.
+- Settings link (`admin/src/state/useSettings.ts:30-39`): `settingsPath(entry)` returns `controllerPath(entry.controller)` or `/settings/`; add a leading `if (entry.path) return entry.path` with an additive `Path` field on `cabana.SettingsEntry` (`modules/cabana/navigation.go:23-35`).
+- Copy button (`admin/src/components/account/ApiTokenCreateDialog.vue:255-268`):
+```ts
+try {
+ if (!navigator.clipboard?.writeText) { throw new Error('clipboard unavailable') }
+ await navigator.clipboard.writeText(value)
+ markCopied()
+} catch {
+ copyFailed.value = true
+ ...
+}
+```
+- Tabs via `reka-ui`; data via typed `api` client only. Rebuild and commit `modules/boardwalk/dist`, run `scripts/check-admin-dist.sh`.
+
+## Shared Patterns
+
+### CLI command shape
+**Source:** `cmd/summer/main.go:381-415`. **Apply to:** plugin:manifest, plugin:install, plugin:update. Full docs fields; Run delegates to a package function; `out.Success(...)` at the end.
+
+### Shelling out to go/git
+**Source:** `internal/build/new.go:286` `runGit`, `scaffold.go` `runGo` + `applyGoWork` (l.632). **Apply to:** plugindirs, derive, install, ingester (ingester adds hardened env: `GIT_TERMINAL_PROMPT=0`, `GIT_ALLOW_PROTOCOL=https`, args after `--`).
+
+### YAML
+`goccy/go-yaml` only (as `internal/build/manifest.go`); struct field order = output order.
+
+### Plugin capability assertions
+`var _ pact.HasX = (*Plugin)(nil)` next to each capability method (faq, journal files above).
+
+### Error envelopes
+cabana: `WriteError(w, status, code, msg)` / `WriteData(w, status, data, meta)`. Site plugin public API: `{"data":..., "meta":...}` / `{"error":{...}}` per RESEARCH registry shape.
+
+## No Analog Found
+
+| File | Role | Data Flow | Reason |
+|---|---|---|---|
+| `modules//version.go` | utility | transform | No semver comparer in tree; write a strict `vX.Y.Z` comparer (~20 lines), strip `-N-gHASH` from git describe |
+| ingester core (`classes/ingest/*.go` on site plugin) | service | batch (git clone/ls-tree) | No git-cloning service exists; follow RESEARCH "Hardened git for the ingester" and use `breakwater.Render` for README HTML |
+
+## Metadata
+
+**Analog search scope:** `summercms.go/{cmd,internal/build,modules/{cabana,bonfire,fetchguard,party},admin/src}`, `../sm-faq-plugin`, `../sm-journal-plugin`, `../sm-chatvideo-plugin`, `../sm-summercmsio-app/{plugins/golem15/summercms,scripts,vue-summercmsio-app/changelog}`
+**Files scanned:** ~35
+**Pattern extraction date:** 2026-10-29
diff --git a/.planning/phases/34-plugin-marketplace/34-RESEARCH.md b/.planning/phases/34-plugin-marketplace/34-RESEARCH.md
index af99e085..37b920cc 100644
--- a/.planning/phases/34-plugin-marketplace/34-RESEARCH.md
+++ b/.planning/phases/34-plugin-marketplace/34-RESEARCH.md
@@ -567,18 +567,24 @@ Plans 4 and 5 both depend only on plan 1 and the API shape (fixtures), not on pl
| A9 | Which repos are "public plugins" to list (quizzes, presentations, golem, feedback exist publicly but have no sibling checkout; six chat-family repos are untagged) | F9 | Wrong listing scope |
| A10 | Proposed new module name (e.g. `kiosk`) | Structure | Naming only |
-## Open Questions
+## Open Questions (RESOLVED)
+
+All six were answered by the user during /msd-plan-phase 34 on 2026-10-29.
1. **Constant IDs vs D-02's "literal returns" (A1).**
- What we know: 13/22 plugins return `PluginID`; two use cross-package constants; `plugin:add` already fails on them.
- - Recommendation: approve the static const resolver; it keeps D-02's "no code execution" intent.
+ - **RESOLVED:** follow constants (same-package and cross-package); `plugin:add` uses the same resolver. No `plugin.go` edits.
2. **How the plugin repos' `--check` runs in `go test` without inflating framework minimums.**
- Options: (a) import a framework helper. Each plugin's go.mod then requires the release containing it, which becomes its `framework:` minimum (likely the phase's release tag), and plugin tags wait for that framework tag. (b) The test shells out to `summer` from PATH or `go run git.golem15.com/golem15/summercms/cmd/summer@` (the framework go.mod has no replace, so `go run pkg@version` works) and skips under `-short` or when unavailable. No go.mod change, but weaker and network-bound.
- - Recommendation: (a) for simplicity, provided the user accepts that every listed plugin then requires the new framework release. Otherwise (b) plus the mandatory check in `changelog-post.py`.
+ - **RESOLVED:** (a) framework test helper; plugins require the phase's framework release and their tags wait for it.
3. **Which repos are listed at launch (A9)**, and whether untagged repos get a first tag in this phase (tags are user-gated).
+ - **RESOLVED:** only tagged public plugins checked out next to the framework; no new tags in this phase.
4. **Ingest token scope (A7)**: new `registry` scope (user mints a token), or accept the existing `journal`-scoped token for the ingest endpoint (lower friction, scope semantics blur).
+ - **RESOLVED:** new `registry` scope; the user mints the token, ingest-now is best-effort until then.
5. **`summer.yaml` comment preservation**: implement now (recommended) or document the loss.
+ - **RESOLVED:** preserve comments.
6. **plugin:install in a non-git app directory** (`summer new --no-git`) with `--source`: clone without a submodule, or refuse.
+ - **RESOLVED:** refuse with a clear message.
## Environment Availability
diff --git a/.planning/phases/34-plugin-marketplace/COVERAGE.md b/.planning/phases/34-plugin-marketplace/COVERAGE.md
new file mode 100644
index 00000000..60ce61dd
--- /dev/null
+++ b/.planning/phases/34-plugin-marketplace/COVERAGE.md
@@ -0,0 +1,18 @@
+# API Coverage — SummerCMS plugin registry API v1
+
+> Full coverage by default. Opt-outs are explicit, reasoned decisions. Phase 34 builds the registry API itself (server in 34-03, contract types in 34-01) and consumes it from the CLI (34-04), cabana (34-05), the /marketplace pages (34-03) and the release script (34-03). It also reaches two outside services through their own tools: the Go module proxy through the go command and git hosting through git.
+
+| capability | decision | reason |
+|---|---|---|
+| GET /api/registry/v1/plugins with q and category (list and search) | INTEGRATE | |
+| GET /api/registry/v1/plugins/{id} (detail: README, screenshots, requires) | INTEGRATE | |
+| GET /api/registry/v1/plugins/{id}/versions (tags with their minimums) | INTEGRATE | |
+| POST /api/registry/v1/ingest (authenticated ingest-now, token scope registry) | INTEGRATE | |
+| error envelope (404 not_found, 400 invalid_query) decoded by clients | INTEGRATE | |
+| Cache-Control and ETag with 304 on If-None-Match (server side) | INTEGRATE | |
+| conditional requests (If-None-Match) sent by kiosk.Client and cabana | OPT-OUT | not needed yet: cabana keeps an in-process TTL cache and the CLI makes one-shot calls; adding revalidation later changes no contract |
+| pagination of the plugin list (page, per_page) | OPT-OUT | not needed yet: the launch listing is fourteen plugins; adding pagination later is an additive change within v1 |
+| Go module proxy (proxy.golang.org) through go get and go list -m | INTEGRATE | |
+| git hosting: anonymous HTTPS (ingester), user's https or ssh (CLI) | INTEGRATE | |
+| forge webhooks (push or tag events) | OPT-OUT | explicitly out of scope: D-13 chose a pull ingester plus ingest-now, no forge webhooks |
+| forge HTTP APIs (raw files, tag listings) | OPT-OUT | explicitly out of scope: D-13 locks anonymous clones, which keeps ingestion independent of the forge |