Setup (3) + Foundational (15) + US4 backup (5) + US1 toggle (5) + US3 wiki extraction (12) + US5 failure isolation (4) + US2 author docs (6) + Verification (9) + Polish (5). MVP = Setup + Foundational + US3 + US1. Parallel opportunities marked [P] within each story. All tasks follow checklist format with concrete file paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 KiB
Tasks — Plugin System for SynapBus Core
Feature: 019-plugin-system
Branch: 019-plugin-system
Worktree: /Users/user/repos/synapbus-plugin-system
Tasks are organized by the five user stories in spec.md. Story priorities:
- US1 (P1): Operator toggles an optional feature without rebuilding
- US2 (P1): Plugin author builds a new feature without touching core
- US3 (P1): Wiki survives the refactor as a fully-functional pilot plugin
- US4 (P1): Backup-and-squash preserves operator data
- US5 (P2): Failed plugin does not crash the bus
Scope reminder (FR-031): only the wiki plugin is extracted in this feature. The framework must be general enough for the nine follow-ups but we don't build them here.
Phase 1 — Setup
- T001 Confirm worktree
/Users/user/repos/synapbus-plugin-systemis on branch019-plugin-systemand clean; abort if not. - T002 Add dev dependencies to
go.mod:cloudflare/tableflip,gopkg.in/yaml.v3,xeipuuv/gojsonschema; rungo mod tidyand commit. - T003 Create directory skeletons
internal/plugin/,internal/plugin/plugintest/,internal/plugins/standard/,internal/plugins/wiki/schema/,internal/plugins/wiki/ui/,scripts/,docs/plugins/,test/integration/.
Phase 2 — Foundational (must complete before any user story)
- T010 Write core interface file
internal/plugin/plugin.goexposingPlugin,HasMigrations,HasMCPTools,HasActions,HasHTTPRoutes,HasWebPanels,HasCLICommands,HasChannelType,HasEventHook,HasLifecycle,HasConfigSchema,HasStability,Migration,ActionRegistration,MCPTool,Event,PanelManifest,ChannelTypeDef,Scopepercontracts/plugin.md. - T011 Write
internal/plugin/host.goexposing theHoststruct percontracts/host.mdincludingBaseURL()andExecTx(). - T012 [P] Write
internal/plugin/status.gowithStatustype (registered|migrated|initialized|started|failed|disabled),StatusEntrystruct, and thread-safeStatusStorethat collects transitions and exposes JSON for/api/plugins/status. - T013 [P] Write
internal/plugin/config.gothat loadssynapbus.yaml, resolves$SYNAPBUS_CONFIG_PATHenv var, extracts per-pluginjson.RawMessage, and rejects unknown plugin names with a pointing error. - T014 Write
internal/plugin/registry.gothat constructs aRegistryfrom a[]Plugin+ config, enforces name uniqueness (panic on dup), assigns enabled state, and exposesEnabled(),Get(name),All(). - T015 Write
internal/plugin/migrator.gothat: ensuresplugin_migrations (plugin, version, name, applied_at, checksum)core table exists; for each enabled plugin implementingHasMigrations, applies its unapplied migrations inside one transaction per plugin; records rows with SHA-256 checksum; refuses to apply a previously-applied migration whose checksum differs. - T016 Write
internal/plugin/lifecycle.gothat drives the three-phase boot: (1) migrate all enabled plugins, (2) build a per-pluginHostwith scoped logger/tracer/metrics/secrets/datadir, callInit, register all capabilities, on error mark failed and notify owner, (3) for plugins withHasLifecyclecallStartin registration order and recordstartedAt; onShutdownreverse order. - T017 Write
internal/plugin/restart.gousingcloudflare/tableflip: install SIGHUP handler that callsupg.Upgrade(), waits for new process readiness, drains HTTP+SSE, exits. ExposeTriggerRestart()helper invoked by enable/disable endpoints. - T018 [P] Write
internal/plugin/plugintest/nop_host.go: constructs an in-memory*sqlx.DBviamodernc.org/sqlite, tempDataDir, no-op messaging/channels stubs, discard logger,NopMetrics, returns*plugin.Host. - T019 [P] Write
internal/plugin/plugintest/run.go:Run(t, plugin)applies migrations, callsInit, ifHasLifecyclecallsStart+Shutdown, asserts that every declared capability is observable on the host-side registries. - T020 [P] Write
internal/plugin/plugintest/assertions.go:HasTool(t, reg, "name"),HasAction(t, reg, "name"),HasRoute(t, reg, "/api/plugins/<plugin>/…"),HasMigration(t, host, "plugin", version),HasPanel(t, reg, id). - T021 Write unit tests
internal/plugin/plugin_test.go,registry_test.go,migrator_test.go,lifecycle_test.go,config_test.go,status_test.gocovering happy paths, duplicate names, unknown plugin names, migration checksum mismatch, Init failure isolation, plugin without capabilities, config decode error. - T022 Add go-analysis boundary-lint tool
tools/arch-lint/main.go: rejects (a) imports frominternal/plugins/*in files outside that subtree that are not ininternal/pluginorcmd/synapbus, (b) imports of core packages (other thaninternal/plugin) from withininternal/plugins/*. Addmake arch-linttarget. - T023 Wire
internal/plugin.Registryinto existing core bootstrap incmd/synapbus/main.go: replace hard-coded feature wiring withregistry.InitAll(ctx, host...)call. Createcmd/synapbus/plugins.gowithdefaultPlugins()returning an empty slice for now (wiki added later). - T024 Add
/api/plugins/statushandler mounted on the existing internal API router percontracts/rest.md. Returns JSON fromStatusStore. AddPOST /api/plugins/{name}/enableand.../disable(admin-only) that editsynapbus.yamlatomically and callTriggerRestart().
Phase 3 — User Story 4 (P1): Backup and squash before any destruction
Story goal: ensure data safety before touching anything else.
Independent test: run scripts/backup-kubic.sh against a reference SQLite database; load the resulting archive into a scratch dir; diff schemas.
- T030 [P] [US4] Write
scripts/backup-kubic.sh: takes--host,--remote-dir,--outflags; performssqlite3 .backup, tars attachments + secrets.key + hnsw.idx, writesmanifest.jsonwith SHA-256 per entry. Idempotent; refuses to overwrite unless--force. - T031 [P] [US4] Write
scripts/verify-backup.sh: takes--archive+--scratch-dir; extracts archive, opens the DB read-only, prints schema, hashes each manifest entry, diffs against manifest. - T032 [P] [US4] Write
scripts/generate-squash.sh: takes--source-db+--output; runssqlite3 .schemaminussqlite_*tables minus the 26 legacyschema_migrationsrows, normalizes whitespace, writes toschema/000_initial.sql(creating the file). Includes seed INSERTs for any "default admin" rows discovered in core. - T033 [US4] Stage the squash: move existing
schema/*.sqltoschema/legacy/(keep as reference). Generateschema/000_initial.sqllocally from~/repos/synapbus/data/synapbus.db(the developer's own data). Add an entry('core', 0, '000_initial', NOW(), <sha256>)intoplugin_migrationsat first boot so the squash is recorded as applied. - T034 [US4] Document in
docs/plugins/migration-notes.md: how to back up, how to verify, how the squash was generated, how to re-generate if core schema changes before the first real release.
Phase 4 — User Story 1 (P1): Operator enables/disables plugins
Story goal: operator toggles plugins.wiki.enabled and sees the change after a graceful restart.
Independent test: boot with wiki enabled → wiki present; set enabled=false → SIGHUP → wiki absent; set enabled=true → wiki back with data.
Depends on Phase 2 foundational work. This story runs in parallel with Phase 5 (wiki extraction) because wiki doesn't exist until Phase 5 — we validate the enable/disable mechanic with a synthetic test plugin first.
- T040 [US1] Add
internal/plugin/plugintest/fake_plugin.go: a fixtureFakePluginimplementingHasMigrations,HasActions,HasHTTPRoutes,HasWebPanelswith trivial implementations, used by lifecycle tests. - T041 [US1] Write
internal/plugin/lifecycle_enable_test.go: builds a registry from[FakePlugin]withenabled=true, asserts capabilities registered, rebuilds withenabled=false, asserts none registered, flips back, asserts capabilities return. - T042 [US1] Wire
/api/plugins/{name}/enableand.../disableend-to-end: update the YAML file on disk, re-read to verify the edit, triggerTriggerRestart(). Unit tests use a tempdir-backed YAML + a stubbed restarter. - T043 [US1] Write
synapbus plugin enable <name>andsynapbus plugin disable <name>cobra subcommands undercmd/synapbus/plugin_cli.gohitting the same code path. - T044 [US1] Integration test
test/integration/plugin_toggle_test.go: builds the binary, spawns it with a fixture YAML containingfakeenabled, curls/api/plugins/statusand asserts status=started, curls/api/plugins/fake/pingexpecting 200, writes YAML with fake disabled, sends SIGHUP, polls/api/plugins/statusuntil status=disabled (≤ 2 s), curls/api/plugins/fake/pingexpecting 404.
Phase 5 — User Story 3 (P1): Extract wiki as canonical pilot plugin
Story goal: the existing wiki feature lives as internal/plugins/wiki/ and behaves identically from the agent's and operator's point of view.
Independent test: restore the developer's existing wiki data (17 articles), bring up the new binary, call list_articles / get_article via MCP, assert identical contents.
Depends on Phase 2 foundational work and (optionally) Phase 4 toggle mechanics.
- T050 [US3] Read the current
internal/wiki/*.goimplementation; list every exported symbol and every SQL statement. Note the existing table names and FK/index dependencies. - T051 [US3] Create
internal/plugins/wiki/schema/001_initial.sqlcreatingplugin_wiki_articlesandplugin_wiki_backlinksmatching the current wiki schema, withplugin_wiki_prefix and identical column definitions. Add the CTEs and indexes used by the existing queries. - T052 [US3] Create
internal/plugins/wiki/store.go: a thin layer overplugin.Host.DBwith the same query set asinternal/wiki/store.go, but againstplugin_wiki_*tables. Port unit tests frominternal/wiki/store_test.go. - T053 [US3] Create
internal/plugins/wiki/plugin.go:WikiPluginstruct,Name/Version/Init,Migrations()embedding the SQL,Actions()returningcreate_article,get_article,list_articles,update_article,get_backlinkswith identical argument schemas and output shapes as the current bridged actions. Each action delegates tostore.go. - T054 [P] [US3] Create
internal/plugins/wiki/ui/index.html: a minimal self-contained page that fetches/api/plugins/wiki/articlesand renders a list + markdown preview. Uses vanilla HTML + fetch + [Markdown-it CDN or embedded]. No Svelte build step required. - T055 [P] [US3] Create
internal/plugins/wiki/routes.go: REST routes under/api/plugins/wiki/—GET /articles(list),GET /articles/{slug}(detail), to back the UI panel. Auth via existing session middleware. - T056 [P] [US3] Create
internal/plugins/wiki/panel.go:WebPanels()returns onePanelManifest(id=wiki, Route=/ui/plugins/wiki);PanelHandler()serves the embeddedindex.htmland static assets viahttp.FS. - T057 [US3] Write
internal/plugins/wiki/plugin_test.go:plugintest.Run(t, wiki.New())smoke test; action-level tests for each of the five MCP actions against aNopHost; fixture-backed test that inserts 3 articles and verifies backlink computation. - T058 [US3] Add
wiki.New()tocmd/synapbus/plugins.go'sdefaultPlugins()slice. Addplugins: { wiki: { enabled: true } }to the default generatedsynapbus.yaml. - T059 [US3] Decommission
internal/wiki/: delete the old package, the bridged action registrations in the legacy MCP bridge, and thewiki_articlestable creation in the old migrations (already removed by squash). Update any core imports pointing to it (should be none once the arch-lint passes). - T060 [US3] Import the developer's own existing wiki articles from
/Users/user/repos/synapbus/data/synapbus.dbinto the refactored schema as part of the squash seed; on next boot the articles are visible via the new plugin. Scripted viascripts/import-legacy-wiki.sh(idempotent). - T061 [US3] Integration test
test/integration/wiki_plugin_test.go: boots binary with wiki enabled, curls/api/plugins/wiki/articlesand expects the imported articles, calls MCPlist_articles,get_article,create_article,update_article,get_backlinks, asserts correct responses.
Phase 6 — User Story 5 (P2): Failure isolation
Story goal: a broken plugin does not break core.
Independent test: start with [wiki, broken], observe wiki works and broken is listed as failed with the error, owner receives a DM.
- T070 [US5] Add
internal/plugin/plugintest/broken_plugin.go: a fixtureBrokenPluginwhoseInitreturns an explanatory error OR panics (two variants), used only in tests. - T071 [US5] Extend
internal/plugin/lifecycle.goto:recover()around eachInitcall,recover()around eachStartgoroutine, mark plugin failed with error message, post a DM toHost.DefaultOwnerciting plugin name + error, emitplugin.status.changedevent. - T072 [US5] Unit test
internal/plugin/lifecycle_failure_test.go: registers[FakePlugin, BrokenPlugin], callsInitAll, asserts fake is started, broken is failed, status store carries both, a fake-messaging.Bus records exactly one DM containing "broken". - T073 [US5] Integration test
test/integration/plugin_failure_test.go: boots binary with fixturebrokenplugin enabled (reusing the BrokenPlugin fixture; conditional//go:build integration); curls/api/plugins/status, asserts broken is failed with explanatory error, curls wiki endpoints and asserts they still work.
Phase 7 — User Story 2 (P1): Plugin author onboarding
Story goal: a contributor can write a new plugin in under 20 minutes following quickstart.md.
Independent test: create the hello plugin per quickstart.md, run go test, boot, curl.
- T080 [P] [US2] Verify
quickstart.mdagainst the real package by implementing thehelloplugin exactly as written into a scratch directory (_examples/hello/), runningplugintest.Run, confirming it passes. Check every code fence compiles without edits. - T081 [P] [US2] Write
docs/plugins/authoring.md— step-by-step narrative version ofquickstart.mdwith rationale at each step. Link tocontracts/plugin.mdandcontracts/host.md. - T082 [P] [US2] Write
docs/plugins/lifecycle.md— sequence diagram of migrate → init → start → shutdown with call-order guarantees. - T083 [P] [US2] Write
docs/plugins/capabilities.md— one section perHasXinterface with a minimal example snippet. - T084 [US2] Add template generator
scripts/new-plugin.sh <name>: scaffoldsinternal/plugins/<name>/{plugin.go, schema/001_initial.sql, ui/index.html, plugin_test.go}from a heredoc template. - T085 [US2] Verify once more: run the generator for "greeter",
go test ./internal/plugins/greeter/..., passes without edits. Delete the scaffold afterward.
Phase 8 — End-to-End Verification
- T090 Run
make build— binary compiles cleanly. - T091 Run
go test ./...— all tests pass including newinternal/plugin/...,internal/plugins/wiki/...,test/integration/...(withgo test -tags=integration ./test/integration/...). - T092 Run
make arch-lint— boundary invariants hold. - T093 Run
make lint— no new lint issues. - T094 Boot the binary with the default
synapbus.yaml(wiki enabled); verify via browser:/ui/loads the shell and shows the wiki panel in nav./ui/plugins/wikirenders the article list./api/plugins/statusshows wiki=started.
- T095 Boot the binary, then
curl -X POST -H 'admin-token' /api/plugins/wiki/disable; observe graceful restart in logs (≤ 2 s);/ui/plugins/wikireturns 404;/api/plugins/statusshows wiki=disabled;curl -X POST .../enable; restart; wiki back; articles preserved. - T096 Exercise MCP actions directly from Claude Code: call
mcp__synapbus__execute call("list_articles", {}); callget_article({slug: "synapbus-architecture"}); assert expected shapes. - T097 Chrome-in-Claude smoke test: navigate to
http://localhost:8080/ui/plugins/wiki, assert the article list renders (at least one article visible), click on an article, assert body is shown. - T098 SC measurement: time a graceful restart end-to-end (SIGHUP dispatch → new-process readiness); record in
autonomous_summary.md; assert < 2 s.
Phase 9 — Polish
- T099 Update
README.mdandCLAUDE.md: note the plugin directory (internal/plugins/<name>), the quickstart link, the arch-lint rule. - T100 Update
docs/plugins/README.mdlinking authoring / lifecycle / capabilities / migration-notes. - T101 Archive
internal/wiki/and the legacy schema directory underdocs/legacy/as reference. - T102 Write
autonomous_summary.mdin repo root with: shipped features, test results, SC measurements, open follow-ups (9 remaining plugin extractions). - T103 Open a draft PR description summarizing the feature from operator, author, and maintainer perspectives.
Dependencies
Setup (T001-T003)
└── Foundational (T010-T024)
├── US4 Backup (T030-T034) [parallel with others, but T033 blocks US3 import]
├── US1 Toggle (T040-T044) [uses FakePlugin, independent of wiki code]
├── US3 Wiki (T050-T061) [needs squashed schema from US4 T033/T060]
├── US5 Failure (T070-T073) [uses BrokenPlugin, independent]
└── US2 Onboarding docs (T080-T085) [independent]
└── Verification (T090-T098)
└── Polish (T099-T103)
Parallel execution opportunities
Within each user story the [P] tasks can be worked on concurrently by subagents:
- US3: T054, T055, T056 (UI asset, routes, panel manifest) can go in parallel once T052 store layer lands.
- US4: T030, T031, T032 scripts are all independent.
- US2: T080, T081, T082, T083 docs are all independent.
- Foundational: T012, T013 (status, config) can land in parallel with T010/T011 (interfaces/host).
MVP scope
Minimum shippable increment: Setup + Foundational + US3 (wiki extraction) + US1 enable/disable.
- US4 backup is P1 but runs against the operator's local data only; it MUST precede the schema squash but doesn't change the code that ships.
- US5 failure isolation is P2 — absence doesn't block merging; presence is what makes the framework production-safe.
- US2 onboarding is docs; can slip to a follow-up PR if time pressure demands.
Format validation
All tasks above follow: - [ ] T### [P]? [US#]? Description with path. Setup and foundational have no story label. Story tasks carry [US1]..[US5]. Polish tasks have no story label. Every implementation task names a concrete file path.