The remote git@git.golem15.com:golem15/sm-user-plugin.git has a master branch holding the plugin's full history split out of fonoteka.go (one commit per fonoteka.go commit that touched plugins/golem15/user, 21 at planning time), followed by the module-path rename, a go.mod tidy and a README commit; the remote master SHA equals the submodule HEAD that fonoteka.go records
fonoteka.go/plugins/golem15/user is a git submodule with url git@git.golem15.com:golem15/sm-user-plugin.git at the same path, so go.work still lists ./plugins/golem15/user unchanged; `git submodule status` shows it clean (no -, + or U prefix)
The plugin's module path is git.golem15.com/golem15/sm-user-plugin everywhere: its go.mod, every import in the plugin, the app, parity and the fonoteka plugin, both go.mod require/replace pairs, summer.yaml and the regenerated plugins.gen.go; no text file under fonoteka.go (outside .git) contains the old app-nested module path
Pure move per locked decision 1: plugin ID golem15.user, table names, migration IDs, config keys golem15.user.*, routes and payloads are unchanged; every changed line in the plugin's .go files and in fonoteka.go's code files is an import or module-path line
fonoteka.go builds (`go build -o bin/fonoteka .`) and the full workspace suite (root module, plugins/golem15/fonoteka, plugins/golem15/user, Docker/testcontainers included) passes; the plugin also vets in standalone module mode (GOWORK=off)
The plugin README follows the module README structure and names no consuming application
A decision note in summercms.go/.planning/notes supersedes the Phase 1 deferral of shared-plugin extraction, the deferral line points to it, and PROJECT.md, ROADMAP.md and the unexecuted Phase 12 plans name sm-user-plugin and the submodule commit workflow
summer build reads plugins[].module and emits the blank import
sm-user-plugin
from
to
via
pattern
fonoteka.go gitlink plugins/golem15/user
sm-user-plugin remote master
recorded submodule SHA equals the pushed master SHA
git ls-remote
from
to
via
pattern
.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md deferred line
.planning/notes/core-plugins-own-repos.md
Superseded pointer
core-plugins-own-repos
Extract the shared user plugin `golem15.user` out of the application repo fonoteka.go into its own repository `sm-user-plugin` (remote `git@git.golem15.com:golem15/sm-user-plugin.git`, created empty by the user), with history preserved, renamed to module `git.golem15.com/golem15/sm-user-plugin`, and mount it back into fonoteka.go as a git submodule at the same path `plugins/golem15/user`. Then record the decision in summercms.go planning docs and point the unexecuted Phase 12 plans at the new repo.
Purpose: the user plugin is a core plugin shared across Golem15 projects. A second application (sm-summercmsio-app) now exists and the Journal port is next, so the plugin must live once, in its own sm-<name>-plugin repo, the same way sm-summercmsio-app already mounts sm-summercmsio-plugin. This supersedes the Phase 1 deferral ("only when a second app (keios.eu) needs one").
Output: the pushed sm-user-plugin repo; fonoteka.go with the submodule, rewritten imports, updated go.mod/summer.yaml/plugins.gen.go, README and CLAUDE.md layout notes; a decision note and planning-doc updates in summercms.go.
Locked decisions (from the orchestrator; implement exactly):
LD-1 Module path git.golem15.com/golem15/sm-user-plugin. Plugin ID stays golem15.user; table names, migration IDs, config keys golem15.user.* and the API are unchanged. Pure move, no behaviour change.
LD-2 Preserve history with git subtree split --prefix=plugins/golem15/user, commit the module-path rename in the new repo, push master to the remote. Pushing to that empty remote is authorized. Never force-push.
LD-3 Mount back as a git submodule at fonoteka.go/plugins/golem15/user (same path, go.work unchanged); update the fonoteka.go go.mod replace to the new module path; rewrite all imports; update summer.yaml; regenerate generated files.
LD-4 Decision note in summercms.go/.planning/notes superseding the Phase 1 deferral; update planning docs (Phase 12 plans, ROADMAP, PROJECT) to name sm-user-plugin. Planning docs and code in separate commits; the planning commit is in summercms.go.
LD-5 summercms.go code and docs must not reference the old module path (verified at planning time: only .planning/ and the scripts/check-phase*.sh directory paths ./plugins/golem15/user/... mention the plugin, and those paths stay valid because the mount path is unchanged; no summercms.go code change). Update the fonoteka.go README and CLAUDE.md repo-structure text. The plugin README must not name the consuming application.
Repos and commits: sm-user-plugin (submodule checkout), fonoteka.go, summercms.go. Commits in every repo carry no co-author or attribution trailers (user's global rule overrides any attribution reminder). One logical change per commit. workflow.use_worktrees=false: work sequentially on the main trees. A dev server (bin/fonoteka serve on 127.0.0.1:8080) may be running; rebuilding bin/fonoteka is fine, do not kill it.
Facts gathered at planning time (re-check with the commands named; do not trust counts blindly):
fonoteka.go has no git remote, a clean tree, and 184 commits; git log --oneline -- plugins/golem15/user | wc -l = 21, no merge commits touch the path. git status --ignored plugins/golem15/user shows no untracked or ignored files inside the plugin.
Old module path appears in: the plugin's go.mod and 20 of its .go files; fonoteka.go go.mod (require line ~10, replace line ~121); plugins/golem15/fonoteka/go.mod (require line ~6, replace ... => ../user line ~123); summer.yaml; plugins.gen.go; app/app.go; six parity/*.go files; 19 .go files under plugins/golem15/fonoteka. main.go does not import the plugin. Re-derive the list with grep -rlI --exclude-dir=.git at execution time.
The plugin's own go.mod keeps replace git.golem15.com/golem15/summercms => ../../../../summercms.go, which resolves only when mounted at <app>/plugins/golem15/user next to summercms.go. Precedent: sm-summercmsio-plugin's go.mod has the same replace, and sm-summercmsio-app mounts it as submodule plugins/golem15/summercms with module path git.golem15.com/golem15/sm-summercmsio-plugin, replace ... => ./plugins/golem15/summercms in the app go.mod and an SSH url in .gitmodules.
GOWORK=off go -C plugins/golem15/user vet ./... currently fails with "updates to go.mod needed". go mod tidy -diff shows only added // indirect requirements (github.com/tidwall/gjson, match, pretty, sjson) and go.sum lines (robfig/cron/v3, tidwall/*): transitive framework deps the plugin go.mod never recorded.
summer build (summercms.go internal/build) regenerates main.go and plugins.gen.go from summer.yaml with writeIfChanged, runs the models-leaf check (resolves plugin dirs through go.mod replace), then go build -o bin/<binary> . with the workspace. No summer binary is on PATH; build it from summercms.go.
In workspace mode, go test ./... from the fonoteka.go root covers only the root module; plugin modules need explicit patterns ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... (as the summercms.go scripts/check-phase*.sh do).
Known test coupling, kept as is (pure move): TestRegisterCORSPath in register_test.go walks up to the host app's config/http.yaml, so that test passes only when the plugin is mounted inside an application checkout. Record it in the SUMMARY as a follow-up, do not change it.
Plugin code comments mention the consuming application in two places (models/organisation.go, updates/10_organisations.go) and register_test.go guards the import direction against the app plugin's path. These are code, not the README; leave them (pure move) and list them in the SUMMARY as follow-ups.
summercms.go working tree has an unrelated modified .planning/state.json and an untracked zip; stage only this plan's files by explicit path.
Task 1: fonoteka.go builds golem15.user from the sm-user-plugin submodule (history pushed, same path, same bytes)
`git -C ../fonoteka.go status --porcelain` prints nothing, and `git ls-remote git@git.golem15.com:golem15/sm-user-plugin.git` exits 0 and prints nothing (remote still empty). If the remote already has refs, stop and report; never force-push.
../fonoteka.go/.gitmodules, ../fonoteka.go/plugins/golem15/user (directory replaced by gitlink)
../fonoteka.go/go.work, ../fonoteka.go/go.mod (replace block), ../sm-summercmsio-app/.gitmodules (submodule precedent), ../fonoteka.go/plugins/golem15/user/go.mod
Work in /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go. Per LD-2 and LD-3:
(1) Record the baseline: the pre-extraction tree hash `git rev-parse HEAD:plugins/golem15/user` and the count `git log --oneline -- plugins/golem15/user | wc -l`. Write both into the SUMMARY later.
(2) Split the history: `git subtree split --prefix=plugins/golem15/user -b split/sm-user-plugin`. Confirm the split tip's tree equals the baseline tree hash (`git rev-parse 'split/sm-user-plugin^{tree}'`) and `git rev-list --count split/sm-user-plugin` equals the baseline count. If the counts differ but the tree hash matches, record both numbers in the SUMMARY and continue; if the tree hash differs, stop.
(3) Secret scan before publishing (T-261002-01): `git log -p split/sm-user-plugin` must contain no private key block and no non-empty secret, password or DSN value (config keeps `secret: ""`). Stop and report if anything matches.
(4) Publish: `git push git@git.golem15.com:golem15/sm-user-plugin.git split/sm-user-plugin:refs/heads/master` (a plain push to the empty remote, authorized by the user). Then delete the local branch with `git branch -D split/sm-user-plugin`.
(5) Replace the in-tree directory with the submodule at the same path: `git rm -r -q plugins/golem15/user`, remove any leftover empty directory, then `git submodule add git@git.golem15.com:golem15/sm-user-plugin.git plugins/golem15/user` (SSH url, no `-b`, matching the sm-summercmsio-app precedent). Make sure the checkout is on branch master tracking origin/master (`git -C plugins/golem15/user checkout master` if the clone left a detached HEAD). go.work, go.mod and every import stay untouched in this task: the submodule HEAD is the split tip, whose module path is still the old one, so the workspace builds exactly as before.
(6) Verify (see verify block), then commit .gitmodules plus the gitlink and the staged deletions in fonoteka.go with exactly the subject `refactor(user): mount golem15.user from the sm-user-plugin submodule` (the verify gates locate the commit by this subject) and a body noting the history was split with git subtree and the tree is byte-identical. No co-author or attribution trailers. Do not edit README.md or CLAUDE.md here; Task 2 documents the final layout.
cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && M=$(git log -1 --format=%H --grep='mount golem15.user from the sm-user-plugin submodule') && test -n "$M" && SS=$(git submodule status plugins/golem15/user) && printf '%s\n' "$SS" | grep -Eq '^ [0-9a-f]{40} plugins/golem15/user' && grep -q 'url = git@git.golem15.com:golem15/sm-user-plugin.git' .gitmodules && grep -q './plugins/golem15/user' go.work && SUB=$(git -C plugins/golem15/user rev-parse HEAD) && RM=$(git ls-remote git@git.golem15.com:golem15/sm-user-plugin.git refs/heads/master) && test "${RM%%:space:*}" = "$SUB" && LINK=$(git rev-parse "$M:plugins/golem15/user") && test "$LINK" = "$SUB" && SUBTREE=$(git -C plugins/golem15/user rev-parse 'HEAD^{tree}') && PRETREE=$(git rev-parse "$M^:plugins/golem15/user") && test "$SUBTREE" = "$PRETREE" && go vet ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... && go test -short -count=1 ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... && ST=$(git status --porcelain) && test -z "$ST"
Any command exits non-zero: the submodule is missing, uninitialized or modified; the .gitmodules url differs; the remote master is not the recorded gitlink; the submodule tree differs from the pre-extraction directory tree; vet or the short test run fails; or fonoteka.go has uncommitted changes after the commit.
- `git -C ../fonoteka.go show --stat ` (the commit with subject `refactor(user): mount golem15.user from the sm-user-plugin submodule`) shows .gitmodules added, plugins/golem15/user as a gitlink, and the old plugin files deleted, and nothing else.
- The submodule HEAD tree hash equals the pre-extraction tree hash recorded in step (1).
- `git -C ../fonoteka.go branch --list 'split/*'` prints nothing.
fonoteka.go builds and passes its short suite with golem15.user coming from the sm-user-plugin repo, whose master on git.golem15.com holds the plugin's full history; the commit is in fonoteka.go.
Task 2: The plugin is module git.golem15.com/golem15/sm-user-plugin in its repo and in every fonoteka.go importer, with the full suite green
Task 1 is committed: `git -C ../fonoteka.go submodule status plugins/golem15/user` starts with a space, and Docker is reachable for testcontainers (`docker info` exits 0).
../fonoteka.go/plugins/golem15/user/{go.mod, go.sum, README.md, and the 20 .go files listed in files_modified}, ../fonoteka.go/go.mod, ../fonoteka.go/plugins/golem15/fonoteka/go.mod, ../fonoteka.go/summer.yaml, ../fonoteka.go/plugins.gen.go, ../fonoteka.go/app/app.go, ../fonoteka.go/parity/*.go (six files), ../fonoteka.go/plugins/golem15/fonoteka/**/*.go (19 files), ../fonoteka.go/README.md, ../fonoteka.go/CLAUDE.md
../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/routes.go, ../fonoteka.go/plugins/golem15/user/config/config.yaml (facts for the README), ../sm-summercmsio-app/plugins/golem15/summercms/README.md (plugin README precedent), ../fonoteka.go/README.md, ../fonoteka.go/CLAUDE.md, CLAUDE.md "Documentation" section (module README structure), internal/build/build.go (what summer build writes)
Per LD-1, LD-3 and LD-5. The rewrite is a prefix replacement of the old app-nested module path with `git.golem15.com/golem15/sm-user-plugin`; subpackages keep their suffixes (`.../classes`, `.../models`, `.../updates`, `.../controllers`, `.../console`).
The old path is `git.golem15.com/golem15/fonoteka/plugins/golem15/user`.
A. In the submodule (git -C ../fonoteka.go/plugins/golem15/user), three commits, not pushed yet:
(1) Rename: change the go.mod module line and rewrite every occurrence in the plugin's .go files (grep -rlI inside the submodule, then sed with an escaped-dot pattern). Run gofmt -l . (must print nothing; the new path sorts between the same neighbours as the old one, so no import reordering is expected). Touch nothing else: no identifier, string, table name, migration ID, config key or comment changes. Commit refactor: rename module to git.golem15.com/golem15/sm-user-plugin.
(2) Tidy for standalone module mode: GOWORK=off go mod tidy in the submodule. The go.mod diff may only add // indirect requirement lines (at planning time: tidwall gjson/match/pretty/sjson) and go.sum lines; if tidy wants to change or drop any direct requirement or bump a version, revert it and report instead. Commit build: tidy go.mod for standalone module mode.
(3) README.md (new, Claude's discretion on wording, LD-5 binding): follow the module README structure from CLAUDE.md "Documentation": H1 golem15.user, a one-sentence summary, the module line git.golem15.com/golem15/sm-user-plugin, then Overview, Features (registration, login, JWT issue/refresh and blacklist, organisations, personal API tokens, throttling, must-change-password lock, avatar upload, mail templates; derive the exact list from plugin.go and routes.go, name only identifiers that exist), Usage (mount in an application: git submodule add git@git.golem15.com:golem15/sm-user-plugin.git plugins/golem15/user, add ./plugins/golem15/user to go.work, require plus replace git.golem15.com/golem15/sm-user-plugin => ./plugins/golem15/user in the app go.mod, and a summer.yaml entry id: golem15.user with that module; the plugin expects the framework checkout at ../../../../summercms.go relative to itself), Configuration (keys under golem15.user.*, including golem15.user.jwt.secret overridable with SUMMER_GOLEM15__USER__JWT__SECRET, no production default), Dependencies (the framework git.golem15.com/golem15/summercms) and Testing (go test -short ./... for the fast run, full run needs Docker; one test reads the host application's config/http.yaml, so run tests inside an application checkout). Use "the application" or "host application" and neutral example names (acme, blog) only; never name the consuming application. Commit docs: describe the user plugin.
Then check the submodule alone: GOWORK=off go -C ../fonoteka.go/plugins/golem15/user vet ./... must pass.
B. In fonoteka.go:
(4) Rewrite every importer and manifest: root go.mod (require and replace lines; the replace target stays ./plugins/golem15/user), plugins/golem15/fonoteka/go.mod (require and replace; the target stays ../user), summer.yaml (module: of golem15.user), app/app.go, the six parity files and the 19 fonoteka-plugin .go files. Re-derive the file list with grep -rlI --exclude-dir=.git over the old path and exclude plugins.gen.go, main.go and the submodule directory (already done in A). Do not run go mod tidy in fonoteka.go modules.
(5) Regenerate, never hand-edit plugins.gen.go/main.go: build the CLI into a temporary directory outside both repos (the executor scratchpad or mktemp -d) with go -C /media/nvme/dev/golem15/summercms.io/summercms/summercms.go build -o <tmp>/summer ./cmd/summer, then run <tmp>/summer build with cwd fonoteka.go. It rewrites plugins.gen.go (the blank import becomes _ "git.golem15.com/golem15/sm-user-plugin", PluginIDs order unchanged), leaves main.go unchanged, and builds bin/fonoteka (overwriting the binary of a running dev server is fine; do not kill it).
(6) Docs (fonoteka.go is the application, so naming it is fine here): README.md gets a short "Layout" section after the intro, modeled on the sm-summercmsio-app README table: plugins/golem15/fonoteka/ is the application plugin in this repo; plugins/golem15/user/ is submodule golem15/sm-user-plugin, the shared user plugin golem15.user, module git.golem15.com/golem15/sm-user-plugin; after cloning run git submodule update --init. CLAUDE.md gets a "Submodules" section: plugins/golem15/user is the sm-user-plugin submodule; change it inside the submodule, commit and push its master first, then commit the bumped pointer in fonoteka.go as a separate commit; it is a core plugin shared across projects, so no breaking changes to its contract unless the user asks.
(7) Gates, in this order: gofmt on the touched trees; workspace vet go vet ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/...; go build -o bin/fonoteka .; the negative grep for the old path (text files only, -I, excluding .git); the pure-move diff gates in the verify block; then the full Docker suite go test -count=1 -timeout 30m ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/.... The full run can exceed the 10-minute foreground limit: run it in the background with output captured to a log in the scratchpad, then require exit status 0 and no FAIL line in the log. If any gate needs a plugin change, add it as another submodule commit before pushing.
(8) Publish and record: git -C plugins/golem15/user push origin master (fast-forward only, never force). Then in fonoteka.go stage the gitlink bump, both go.mod files, summer.yaml, plugins.gen.go, the rewritten .go files, README.md and CLAUDE.md, confirm git status --porcelain lists nothing unexpected, and commit with exactly the subject refactor(user): import golem15.user from git.golem15.com/golem15/sm-user-plugin (the verify gates locate the commit by this subject). No co-author or attribution trailers in any commit.
cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && test -z "$(gofmt -l app parity plugins/golem15/fonoteka plugins/golem15/user .go)" && ! grep -rIn --exclude-dir=.git 'fonoteka/plugins/golem15/user' . && grep -qx 'module git.golem15.com/golem15/sm-user-plugin' plugins/golem15/user/go.mod && grep -q 'module: git.golem15.com/golem15/sm-user-plugin' summer.yaml && grep -q '_ "git.golem15.com/golem15/sm-user-plugin"' plugins.gen.go && grep -q 'git.golem15.com/golem15/sm-user-plugin => ./plugins/golem15/user' go.mod && grep -q 'git.golem15.com/golem15/sm-user-plugin => ../user' plugins/golem15/fonoteka/go.mod && grep -q '"golem15.user"' plugins.gen.go && go vet ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... && GOWORK=off go -C plugins/golem15/user vet ./... && go -C plugins/golem15/user vet ./... && go build -o bin/fonoteka . && test -f plugins/golem15/user/README.md && ! grep -qiE 'fonoteka|plytarium|płytarium' plugins/golem15/user/README.md && M=$(git log -1 --format=%H --grep='mount golem15.user from the sm-user-plugin submodule') && R=$(git log -1 --format=%H --grep='import golem15.user from git.golem15.com/golem15/sm-user-plugin') && test -n "$M" && test -n "$R" && BASE=$(git rev-parse "$M:plugins/golem15/user") && PD=$(git -C plugins/golem15/user diff "$BASE"..HEAD -- '.go') && test -z "$(printf '%s\n' "$PD" | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v 'git.golem15.com/golem15/')" && AD=$(git diff "$M".."$R" -- '.go' summer.yaml go.mod plugins/golem15/fonoteka/go.mod ':!plugins/golem15/user') && test -z "$(printf '%s\n' "$AD" | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v 'git.golem15.com/golem15/')" && SUB=$(git -C plugins/golem15/user rev-parse HEAD) && LINK=$(git rev-parse "$R:plugins/golem15/user") && test "$LINK" = "$SUB" && SS=$(git submodule status plugins/golem15/user) && printf '%s\n' "$SS" | grep -Eq '^ [0-9a-f]{40} plugins/golem15/user' && RM=$(git ls-remote git@git.golem15.com:golem15/sm-user-plugin.git refs/heads/master) && test "${RM%%:space:}" = "$SUB" && SUBST=$(git -C plugins/golem15/user status --porcelain) && test -z "$SUBST" && ST=$(git status --porcelain) && test -z "$ST" && go test -count=1 -timeout 30m ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/...
<fails_when>Any command exits non-zero: gofmt reports a file; any text file still carries the old module path; a manifest, go.mod or plugins.gen.go lacks the new path; vet fails in workspace or standalone mode; the binary does not build; the README is missing or names the consuming application; a changed .go/manifest line is not a module-path line (behaviour or identifier change slipped in); the submodule is dirty, unpushed or not at the recorded gitlink; fonoteka.go has uncommitted changes; or the full suite (Docker) has any failure.</fails_when>
<acceptance_criteria>
- git -C ../fonoteka.go/plugins/golem15/user log --oneline -3 shows the README, tidy and rename commits on top of the split history, and origin/master points at the same SHA.
- git -C ../fonoteka.go/plugins/golem15/user diff <split-tip-sha>..HEAD -- go.mod shows only the module line and added // indirect lines.
- git -C ../fonoteka.go diff --stat <mount-sha>..<rename-sha> -- main.go prints nothing (main.go unchanged by summer build).
- grep -q 'id: golem15.user' ../fonoteka.go/summer.yaml succeeds and the plugin ID string in ../fonoteka.go/plugins/golem15/user/plugin.go is unchanged.
- grep -q 'sm-user-plugin' ../fonoteka.go/README.md && grep -q 'git submodule update --init' ../fonoteka.go/README.md && grep -q 'sm-user-plugin' ../fonoteka.go/CLAUDE.md succeeds.
- Full-suite log shows ok for the root module packages, plugins/golem15/fonoteka and plugins/golem15/user packages and no FAIL.
</acceptance_criteria>
The plugin's own repo and every fonoteka.go importer use git.golem15.com/golem15/sm-user-plugin, the plugin vets on its own, its README names no consuming application, the binary builds, the full suite passes, the submodule commits are pushed and fonoteka.go records them.
Task 3: summercms.go planning docs record that core plugins live in sm-*-plugin repos and point Phase 12 at sm-user-plugin (planning docs only)
Task 2 is committed in fonoteka.go and the sm-user-plugin remote master equals the recorded gitlink.
.planning/notes/core-plugins-own-repos.md, .planning/phases/01-framework-kernel-foundation/01-CONTEXT.md, .planning/PROJECT.md, .planning/ROADMAP.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-01-PLAN.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-02-PLAN.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-05-PLAN.md
.planning/notes/lagoon-per-query-collation.md (note frontmatter and Supersedes precedent), .planning/phases/01-framework-kernel-foundation/01-CONTEXT.md (deferred section, line ~111), .planning/PROJECT.md (Constraints "Two repositories" bullet line ~137, Key Decisions table line ~142), .planning/ROADMAP.md (Phase 12 section, Repos line and the 12-01 plan list entry; Phase 11.2 naming-convention line), .planning/phases/12-p-ytarium-api-collections-and-albums/12-01-PLAN.md (objective, Task 3 action and acceptance criteria), 12-02-PLAN.md (interface context line ~145), 12-05-PLAN.md (Task 3 action)
Per LD-4 and LD-5. Use Edit (scoped replacements) on existing files; Write only for the new note.
(1) New note `.planning/notes/core-plugins-own-repos.md` with frontmatter title, date 2026-10-02 and context (quick task 261002-esz). Sections: Decision (shared core plugins, starting with the user plugin and later blog, pages, payment and the other cross-project Golem15 plugins as they are ported, live in their own repos `git.golem15.com/golem15/sm--plugin` with module path equal to the repo path; the Go package and plugin ID keep their plain names, so `golem15.user` is unchanged; each application mounts them as git submodules at `plugins//`, lists them in go.work and adds a go.mod require plus local replace; the plugin's own go.mod replaces the framework at `../../../../summercms.go`; application-specific plugins such as golem15.fonoteka stay in the application repo). Supersedes (the Phase 1 01-CONTEXT deferred item about extracting shared stack plugins only when keios.eu needs one). Trigger (a second application, sm-summercmsio-app, exists and the Journal port is next; the sm-summercmsio-app/sm-summercmsio-plugin submodule layout is the precedent). How it was done for golem15.user (history split with git subtree, pushed as master, module renamed to `git.golem15.com/golem15/sm-user-plugin`, mounted back at fonoteka.go/plugins/golem15/user, no behaviour change: tables, migration IDs, config keys and API unchanged). Workflow (change a submodule plugin inside its checkout, commit and push its master first, then commit the bumped pointer in the application repo as a separate commit; core plugin contracts stay non-breaking unless the user asks). Consequences and follow-ups (the plugin README never names a consuming application; TestRegisterCORSPath reads the host application's config/http.yaml so the plugin's tests run only inside an application checkout; two code comments still name the application; fonoteka.go itself becoming sm-fonoteka-app is a separate pending rename).
(2) 01-CONTEXT.md: append to the deferred line about extracting shared stack plugins a pointer that it was superseded on 2026-10-02 by `.planning/notes/core-plugins-own-repos.md` (golem15.user moved to sm-user-plugin). Change nothing else in that file.
(3) PROJECT.md: in the Constraints "Two repositories" bullet, say the fonoteka.go workspace holds the application plugins (fonoteka, translate, feedback, sitemap) and mounts shared core plugins from their own `sm--plugin` repos as git submodules, the user plugin being `sm-user-plugin` (module `git.golem15.com/golem15/sm-user-plugin`) at `plugins/golem15/user`; keep the rest of the bullet (websockets/lighthouse wording, the framework replace, roadmap repos, planning docs location). Add a Key Decisions row: shared core plugins live in their own sm-*-plugin repos mounted as submodules (first sm-user-plugin); rationale: a second application exists and Journal is next, one copy per core plugin, supersedes the Phase 1 keios.eu trigger; outcome `✓ Good (2026-10-02, quick 261002-esz)`.
(4) ROADMAP.md, Phase 12 section only: the `**Repos:**` line becomes `fonoteka.go, sm-user-plugin (submodule at fonoteka.go/plugins/golem15/user)`; in the 12-01 plan list entry, the user-groups clause names the Go user plugin as sm-user-plugin. Do not touch Phase 12 goal or criteria wording (12-01 Task 4 rewrites them and greps them), and do not edit completed phases.
(5) 12-01-PLAN.md: in the objective, the parenthetical that places the user plugin in fonoteka.go becomes: repo sm-user-plugin, module `git.golem15.com/golem15/sm-user-plugin`, mounted as a git submodule at `../fonoteka.go/plugins/golem15/user`; the objective's `Repos:` sentence lists summercms.go (framework and planning docs), sm-user-plugin (user groups, committed inside the submodule and pushed to its origin master first) and fonoteka.go (parity schema allow-list plus the bumped submodule pointer). In Task 3's action, item (3)'s parenthetical naming the fonoteka.go user plugin becomes sm-user-plugin, and append item (6) Commits: commit the user-plugin files inside the submodule (`git -C ../fonoteka.go/plugins/golem15/user`), push its master, then commit parity/schema_diff_test.go together with the bumped submodule pointer in fonoteka.go, no co-author tags. In Task 3's acceptance criteria, rewrite the second clause of the `"groups":` bullet so it says this task's sm-user-plugin commit touches no file under `controllers/` (payload untouched). File paths under `../fonoteka.go/plugins/golem15/user/` stay as they are (the mount path is unchanged).
(6) 12-02-PLAN.md: where the interface context names the user plugin's `classes.HasGroupCode(ctx, db, userID, "admin")`, add its import path `git.golem15.com/golem15/sm-user-plugin/classes`.
(7) 12-05-PLAN.md: append to Task 3's action one sentence: the user plugin test file lives in the sm-user-plugin submodule; commit it there and push its master, then commit the bumped pointer in fonoteka.go with the other fonoteka.go test changes.
(8) Validate the three Phase 12 plans still parse (verify block), then commit only these seven files by explicit path (never `git add -A`: `.planning/state.json` and an untracked zip must stay out) with exactly the subject `docs: move golem15.user to sm-user-plugin and supersede the Phase 1 extraction deferral` (the verify gate locates the commit by this subject). No co-author or attribution trailers. No code in this commit.
cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && N=.planning/notes/core-plugins-own-repos.md && P=.planning/phases/12-p-ytarium-api-collections-and-albums && grep -q 'Supersedes' $N && grep -q 'git.golem15.com/golem15/sm-user-plugin' $N && grep -q 'submodule' $N && grep -q 'core-plugins-own-repos' .planning/phases/01-framework-kernel-foundation/01-CONTEXT.md && grep -q 'sm-user-plugin' .planning/PROJECT.md && grep -A6 '### Phase 12:' .planning/ROADMAP.md | grep -q 'sm-user-plugin' && grep -q 'git.golem15.com/golem15/sm-user-plugin' $P/12-01-PLAN.md && grep -q 'sm-user-plugin commit touches no file' $P/12-01-PLAN.md && grep -q 'git.golem15.com/golem15/sm-user-plugin/classes' $P/12-02-PLAN.md && grep -q 'sm-user-plugin' $P/12-05-PLAN.md && for f in $P/12-01-PLAN.md $P/12-02-PLAN.md $P/12-05-PLAN.md; do node ~/.claude/gsd-core/bin/gsd-tools.cjs query verify.plan-structure "$f" | grep -q '"valid": *true' || exit 1; done && D=$(git log -1 --format=%H --grep='supersede the Phase 1 extraction deferral') && test -n "$D" && FL=$(git diff-tree --no-commit-id --name-only -r "$D") && test "$(printf '%s\n' "$FL" | wc -l)" -eq 7 && test -z "$(printf '%s\n' "$FL" | grep -v '^\.planning/')" && ! printf '%s\n' "$FL" | grep -q 'state.json' && ! grep -rIn --exclude-dir=.git --exclude-dir=.planning --exclude-dir=node_modules 'golem15/fonoteka/plugins' . && go vet ./...
Any command exits non-zero: the note lacks Supersedes, the new module path or the submodule workflow; the 01-CONTEXT pointer, the PROJECT or ROADMAP Phase 12 mention, or any Phase 12 plan edit is missing; a Phase 12 plan no longer passes verify.plan-structure; the commit does not contain exactly seven files, contains a non-.planning path or includes state.json; summercms.go code or docs reference an app-nested plugin module path; or go vet fails.
- `git show --stat ` (subject `docs: move golem15.user to sm-user-plugin and supersede the Phase 1 extraction deferral`) lists exactly the seven files of this task.
- The ROADMAP Phase 12 goal and success-criteria lines are byte-identical to before (`git diff ^.. -- .planning/ROADMAP.md` touches only the Repos line and the 12-01 plan entry).
- `grep -q 'keios.eu' .planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` still succeeds (the deferral is annotated, not deleted).
Anyone reading the planning docs learns that core plugins live in sm-*-plugin repos mounted as submodules, that golem15.user is sm-user-plugin, and how Phase 12 commits user-plugin changes; the Phase 1 deferral points to the superseding note.
<threat_model>
Trust Boundaries
Boundary
Description
local fonoteka.go history -> git.golem15.com remote
Plugin source and its full history leave the machine for the first time
remote sm-user-plugin -> fonoteka.go build
The application now compiles code fetched from a submodule url
STRIDE Threat Register
Threat ID
Category
Component
Severity
Disposition
Mitigation Plan
T-261002-01
Information Disclosure
split history pushed to sm-user-plugin
medium
mitigate
Task 1 step (3) scans git log -p of the split branch for private keys and non-empty secret/password/DSN values before the push; config ships secret: ""; the remote is the user's private golem15 repo.
T-261002-02
Tampering
.gitmodules url and recorded gitlink
medium
mitigate
Verify gates require the exact SSH url, remote master SHA equal to the gitlink, and (Task 1) a submodule tree hash identical to the pre-extraction directory tree.
T-261002-03
Tampering
rename slipping behaviour changes into a core plugin
high
mitigate
Task 2 diff gates fail if any changed .go, go.mod or summer.yaml line is not a module-path line; the full suite including parity and user-plugin tests must pass; plugin ID, tables, migration IDs and config keys are untouched by construction.
T-261002-04
Repudiation
overwriting remote history
low
mitigate
Precondition requires an empty remote; pushes are plain fast-forward pushes, never force.
T-261002-SC
Tampering
go mod tidy in the plugin
low
mitigate
No new packages are installed; tidy may only add // indirect lines for modules already in the framework's dependency graph (tidwall/*, robfig/cron/v3); any direct requirement change aborts the tidy commit.
</threat_model>
- sm-user-plugin: remote master = submodule HEAD = fonoteka.go gitlink; history = split commits + rename + tidy + README; `GOWORK=off go -C ../fonoteka.go/plugins/golem15/user vet ./...` green; README names no consuming application.
- fonoteka.go: `git submodule status` clean for plugins/golem15/user; go.work unchanged; `grep -rIn --exclude-dir=.git` for the old module path prints nothing; `go vet` and the full `go test -count=1 ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/user/...` (Docker) green; `go build -o bin/fonoteka .` works; two commits (mount, rename).
- summercms.go: one planning-docs commit with seven files; Phase 12 plans still pass verify.plan-structure; no code or docs reference an app-nested plugin module path; `go vet ./...` green (no code touched).
<success_criteria>
The user plugin lives in git.golem15.com/golem15/sm-user-plugin with its history, as module git.golem15.com/golem15/sm-user-plugin, and fonoteka.go consumes it as a submodule at the unchanged path with no behaviour change.
Every gate in the three verify blocks passes; no commit in any repo carries a co-author or attribution trailer.
The decision note supersedes the Phase 1 deferral and the planning docs, including the unexecuted Phase 12 plans, describe the submodule workflow.
</success_criteria>
Create `.planning/quick/261002-esz-extract-golem15-user-plugin-from-fonotek/261002-esz-SUMMARY.md` when done. Record: baseline tree hash and commit count vs the split result, the pushed sm-user-plugin SHAs, the fonoteka.go and summercms.go commit SHAs, the full-suite duration and package results, the tidy diff, and follow-ups (TestRegisterCORSPath host coupling, the two application-naming code comments in the plugin, the pending fonoteka.go to sm-fonoteka-app rename).