Plugin Command Architecture

This document captures the architecture that emerged while migrating todo2 from a legacy plugin to a structured command system.

The important point is not todo2 itself.

The point is that another plugin with a large legacy surface - for example bm, or any future plugin with mixed one-shot and interactive commands - should be able to use this as a migration guide.

Final target shape

The target is:

The migration is complete when:

plugins/<alias>/
  adapter.ts
  definition.ts
  help.ts
  init.ts
  ARCHITECTURE.md

  ai/
    parse.ts
    prompt.ts
    schema.ts
    tooling.ts

  db/           # only for logic shared by 2+ subcommands
    drafts.ts
    open.ts
    todo-row.ts
    todos.ts

  output/       # only for output logic shared by 2+ subcommands
    draft/
      format.ts
    message/
      builder.ts
      renderers/
        cli.ts
      schema.ts
    todo-detail/
      format.ts
    todo-tree/
      format.ts

  renderers/
    cli.ts

  types/        # only for types shared by 2+ subcommands
    drafts.ts
    todos.ts

  commands/
    <subcommand>/
      adapter.ts
      definition.ts
      handler.ts
      db.ts        # optional, but only if this subcommand owns the implementation
      format.ts    # optional, but only if this subcommand owns the implementation
      types.ts     # optional, but only if this subcommand owns the implementation
      representation/  # optional
      renderers/       # optional

Layer responsibilities

1. definition.ts

This is the command contract.

Definitions should be authored as source, not treated as untrusted input.

2. adapter.ts

This is the top-level router.

It should:

It should not:

Important lesson from todo2:

3. commands/<name>/handler.ts

Handlers do domain work only.

They should:

They should not:

4. commands/<name>/adapter.ts

Subcommand adapters bridge parsed input to handler results.

They should:

They should not:

5. output/

Rendering-related output concepts belong outside commands/.

Examples:

This is important because shared output is not itself a command.

6. renderers/

Renderers format representation objects into CLI strings.

Renderers should not be mixed into handlers.

7. Local first, shared second

Subcommands should own their implementation by default.

If logic is only needed by one subcommand:

Only move code to shared plugin folders when it is actually needed by 2+ subcommands.

This is the default rule for:

Local vs shared rule

Use this rule aggressively during migration.

If a thing is used by only one command, keep it local:

If a thing is used by multiple commands, move it to a shared plugin folder:

Examples from todo2:

Important anti-pattern:

Migration strategy for a legacy plugin

For another legacy plugin, do not try to rewrite everything at once.

Use this order.

Step 1: introduce the new command shell

Create:

Then migrate a few easy one-shot commands first.

This proves the boundaries before touching the hardest flows.

Step 2: migrate commands, but keep ownership local first

When a migrated command still needs legacy logic:

The goal is for the subcommand to own its behavior immediately, and only promote code outward if reuse becomes real.

Step 3: invert dependencies

This is the most important migration phase.

The new architecture is not complete until:

In todo2, this meant moving these into shared root modules:

Useful audit question:

But that is only the first pass.

The real question is:

Step 4: move interactive flows after the one-shot commands stabilize

Interactive flows are usually the hardest part.

For todo2, these were:

They still fit the architecture, but often need shared orchestration helpers and prompt-loop handling.

Important lesson:

Step 5: collapse legacy files into wrappers

Once new modules own the implementation:

For todo2, the final cleanup looked like this:

That is the point where the migration is effectively done.

Note:

Interactive-flow learnings

Two patterns emerged.

1. Some interactive commands are recomputable

duel does not really need a persisted session table.

That kind of interactive command can stay centered around shared db/domain logic.

2. Some interactive commands need grouping

ai creates multiple draft rows that belong to one review flow.

The minimal working model was:

Important rule:

This same pattern is likely useful for other plugins that create grouped drafts or grouped staged actions.

Practical wiring rules

These came directly from bugs encountered during the migration.

1. Default command behavior matters

If /plugin should show help, implement that intentionally in the top-level adapter.

Do not rely on parse failures to imply help.

2. File moves can silently break DB paths

When moving openDb() into a deeper folder:

This caused a real regression in todo2: commands started reading an empty DB because the moved opener pointed at the wrong file.

3. Help text becomes stale quickly during migration

If you migrate command ownership:

4. Parent wrappers should become boring

Once migration is done, old root files should look like:

If a root file still contains real logic, it is probably still a hidden source of truth.

Inside the new architecture, avoid recreating the same problem with command-local forwarding wrappers.

Core system expectations

This plugin architecture assumes some reusable core contracts exist under src/system/.

Examples used by todo2:

A migrated plugin should use those boundaries instead of rebuilding its own parser/help stack.

What to avoid

Handoff guidance for the next plugin migration

If another agent is migrating a legacy plugin, use this checklist:

  1. create the root command shell (definition.ts, adapter.ts, help.ts, renderer entry)
  2. migrate a few one-shot commands first
  3. audit local-vs-shared command dependencies
  4. move reused pieces into shared root folders
  5. eliminate all new-to-old imports
  6. migrate interactive flows
  7. collapse old files into wrappers
  8. delete wrappers once nothing depends on them

If you follow that order, the migration stays understandable and reversible at every step.