From a299913d7842c8df708c1d44281a3047330202d1 Mon Sep 17 00:00:00 2001 From: mingmxren Date: Sun, 1 Mar 2026 14:47:17 +0800 Subject: [PATCH] docs(plan): add command centralization implementation plan --- ...1-command-centralization-implementation.md | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 docs/plans/2026-03-01-command-centralization-implementation.md diff --git a/docs/plans/2026-03-01-command-centralization-implementation.md b/docs/plans/2026-03-01-command-centralization-implementation.md new file mode 100644 index 000000000..82e7c790c --- /dev/null +++ b/docs/plans/2026-03-01-command-centralization-implementation.md @@ -0,0 +1,342 @@ +# Cross-Channel Command Centralization Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Centralize generic command parsing/policy/execution in `pkg/commands`, remove duplicated command business logic from `AgentLoop` and channel adapters, and enforce consistent cross-channel behavior. + +**Architecture:** Add a tri-state command executor (`handled` / `rejected` / `passthrough`) in `pkg/commands` and make `AgentLoop.processMessage` call it before LLM flow. Command handlers receive minimal runtime capabilities from agent (session operations, scope key, config, channel). Channels stop local business command execution and only forward inbound messages + platform command registration metadata. + +**Tech Stack:** Go, existing `pkg/commands`, `pkg/agent`, `pkg/channels/*`, table-driven tests with `go test`. + +--- + +**Required skills during execution:** @test-driven-development, @verification-before-completion, @requesting-code-review + +### Task 1: Add Executor Tri-State Contract in `pkg/commands` + +**Files:** +- Create: `pkg/commands/executor.go` +- Test: `pkg/commands/executor_test.go` +- Modify: `pkg/commands/definition.go` + +**Step 1: Write the failing test** + +```go +func TestExecutor_RegisteredButUnsupported_ReturnsRejected(t *testing.T) { + defs := []Definition{{Name: "show", Channels: []string{"telegram"}}} + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "whatsapp", Text: "/show"}, nil) + if res.Outcome != OutcomeRejected { + t.Fatalf("outcome=%v", res.Outcome) + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/commands -run TestExecutor_RegisteredButUnsupported_ReturnsRejected -v` +Expected: FAIL with undefined `NewExecutor/OutcomeRejected`. + +**Step 3: Write minimal implementation** + +```go +type Outcome int + +const ( + OutcomePassthrough Outcome = iota + OutcomeHandled + OutcomeRejected +) + +type ExecuteResult struct { + Outcome Outcome + Command string + Reply string + Err error +} + +type Executor struct { reg *Registry } +``` + +Implement `Execute(...)` to: +- parse slash command token, +- detect `registered+unsupported => rejected`, +- detect `unknown => passthrough`, +- keep `handled` path for supported commands with handlers. + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/commands -run TestExecutor_RegisteredButUnsupported_ReturnsRejected -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/commands/executor.go pkg/commands/executor_test.go pkg/commands/definition.go +git commit -m "feat(commands): add tri-state command executor contract" +``` + +### Task 2: Define Command Runtime Interfaces for Agent-Backed Handlers + +**Files:** +- Create: `pkg/commands/runtime.go` +- Test: `pkg/commands/runtime_test.go` + +**Step 1: Write the failing test** + +```go +func TestRuntimeContracts_MinimalSessionOps(t *testing.T) { + var _ SessionOps = (*fakeSessionOps)(nil) + var _ Runtime = (*fakeRuntime)(nil) +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/commands -run TestRuntimeContracts_MinimalSessionOps -v` +Expected: FAIL with undefined interfaces. + +**Step 3: Write minimal implementation** + +```go +type SessionOps interface { + ResolveActive(scopeKey string) (string, error) + StartNew(scopeKey string) (string, error) + List(scopeKey string) ([]session.SessionMeta, error) + Resume(scopeKey string, index int) (string, error) + Prune(scopeKey string, limit int) ([]string, error) +} + +type Runtime interface { + Channel() string + ScopeKey() string + SessionOps() SessionOps + Config() *config.Config +} +``` + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/commands -run TestRuntimeContracts_MinimalSessionOps -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/commands/runtime.go pkg/commands/runtime_test.go +git commit -m "refactor(commands): add runtime interfaces for agent-backed handlers" +``` + +### Task 3: Move `/new` and `/session` Business Logic into `pkg/commands` + +**Files:** +- Modify: `pkg/commands/builtin.go` +- Test: `pkg/commands/builtin_test.go` +- Create: `pkg/commands/session_handlers_test.go` + +**Step 1: Write the failing test** + +```go +func TestSessionHandlers_NewAndResume_UseRuntimeSessionOps(t *testing.T) { + // build fake runtime + fake request channel=whatsapp + // execute /new then /session resume 1 through Executor + // assert handler path is OutcomeHandled and fake session ops were called +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/commands -run TestSessionHandlers_NewAndResume_UseRuntimeSessionOps -v` +Expected: FAIL because `/new` and `/session` definitions are metadata-only. + +**Step 3: Write minimal implementation** + +Add handlers for: +- `/new` and alias `/reset` using `runtime.SessionOps().StartNew(...)` + `Prune(...)` +- `/session list` +- `/session resume ` + +Keep existing output format stable where possible. + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/commands -run TestSessionHandlers_NewAndResume_UseRuntimeSessionOps -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/commands/builtin.go pkg/commands/builtin_test.go pkg/commands/session_handlers_test.go +git commit -m "feat(commands): implement session command handlers via runtime" +``` + +### Task 4: Move `/show` and `/list` Agent Logic into Command Handlers + +**Files:** +- Modify: `pkg/commands/builtin.go` +- Test: `pkg/commands/builtin_test.go` +- Create: `pkg/commands/show_list_handlers_test.go` + +**Step 1: Write the failing test** + +```go +func TestShowListHandlers_ChannelPolicy(t *testing.T) { + // /show on telegram => handled + // /show on whatsapp => rejected (registered but unsupported) + // /foo on whatsapp => passthrough +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/commands -run TestShowListHandlers_ChannelPolicy -v` +Expected: FAIL with missing rejected/passthrough policy for these paths. + +**Step 3: Write minimal implementation** + +- Keep `/show` and `/list` handlers in `pkg/commands` only. +- Ensure unsupported registered command returns `OutcomeRejected` with explicit user text. +- Ensure unknown slash command returns `OutcomePassthrough`. + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/commands -run TestShowListHandlers_ChannelPolicy -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/commands/builtin.go pkg/commands/builtin_test.go pkg/commands/show_list_handlers_test.go +git commit -m "fix(commands): enforce reject-vs-passthrough command policy" +``` + +### Task 5: Wire `AgentLoop` to Executor and Remove Hardcoded Command Switch + +**Files:** +- Modify: `pkg/agent/loop.go` +- Test: `pkg/agent/loop_test.go` + +**Step 1: Write the failing test** + +```go +func TestProcessMessage_CommandOutcomes(t *testing.T) { + // Case A: /show on whatsapp => explicit unsupported message, no LLM call + // Case B: /foo on whatsapp => passes through and triggers LLM call + // Case C: /new on whatsapp => handled via commands runtime +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/agent -run TestProcessMessage_CommandOutcomes -v` +Expected: FAIL while `AgentLoop.handleCommand` switch remains authoritative. + +**Step 3: Write minimal implementation** + +- Add command executor field initialization in `AgentLoop` construction. +- Build runtime adapter from `route+agent+config`. +- In `processMessage`, call executor and branch by tri-state. +- Remove command business switch for `/new`, `/session`, `/show`, `/list`. + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/agent -run TestProcessMessage_CommandOutcomes -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/agent/loop.go pkg/agent/loop_test.go +git commit -m "refactor(agent): delegate generic command execution to commands executor" +``` + +### Task 6: Stop Channel-Side Generic Command Business Execution + +**Files:** +- Modify: `pkg/channels/telegram/telegram.go` +- Modify: `pkg/channels/telegram/telegram_dispatch.go` +- Modify: `pkg/channels/whatsapp/whatsapp.go` +- Modify: `pkg/channels/whatsapp_native/whatsapp_native.go` +- Test: `pkg/channels/telegram/telegram_dispatch_test.go` +- Test: `pkg/channels/whatsapp/whatsapp_command_test.go` +- Test: `pkg/channels/whatsapp_native/whatsapp_command_test.go` + +**Step 1: Write the failing test** + +```go +func TestChannelInbound_DoesNotConsumeGenericCommandsLocally(t *testing.T) { + // verify inbound /help,/new are still forwarded to bus/agent path, + // while Telegram registration path remains intact +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./pkg/channels/telegram ./pkg/channels/whatsapp -run DoesNotConsumeGenericCommandsLocally -v` +Expected: FAIL with existing local interception path. + +**Step 3: Write minimal implementation** + +- Remove/disable local generic command dispatch short-circuit in inbound handlers. +- Keep Telegram `RegisterCommands(...)` startup behavior unchanged. + +**Step 4: Run test to verify it passes** + +Run: `go test ./pkg/channels/telegram ./pkg/channels/whatsapp -run DoesNotConsumeGenericCommandsLocally -v` +Expected: PASS. + +**Step 5: Commit** + +```bash +git add pkg/channels/telegram/telegram.go pkg/channels/telegram/telegram_dispatch.go \ + pkg/channels/whatsapp/whatsapp.go pkg/channels/whatsapp_native/whatsapp_native.go \ + pkg/channels/telegram/telegram_dispatch_test.go pkg/channels/whatsapp/whatsapp_command_test.go \ + pkg/channels/whatsapp_native/whatsapp_command_test.go +git commit -m "refactor(channels): forward generic commands to agent-centric executor path" +``` + +### Task 7: Documentation + Full Verification + Review Gate + +**Files:** +- Modify: `README.md` +- Modify: `README.zh.md` +- Modify: `docs/plans/2026-03-01-command-centralization-design.md` (if needed for implementation notes) + +**Step 1: Add/extend failing behavior test (optional smoke)** + +```go +// Optional smoke in pkg/agent/loop_test.go: +// /show on whatsapp returns unsupported, /foo passes to LLM +``` + +**Step 2: Run targeted verification** + +Run: `go test ./pkg/commands ./pkg/agent ./pkg/channels/... -count=1` +Expected: PASS. + +**Step 3: Update docs** + +- Document unified command execution path. +- Document policy: + - unknown slash => LLM passthrough, + - registered but unsupported => explicit error. + +**Step 4: Run full verification** + +Run: `go test ./... -count=1` +Expected: PASS. + +Run: `git status --short` +Expected: clean working tree. + +**Step 5: Request code review before merge** + +Use `@requesting-code-review` skill with `BASE_SHA` and `HEAD_SHA`, fix Critical/Important findings before merge. + +**Step 6: Commit docs updates** + +```bash +git add README.md README.zh.md docs/plans/2026-03-01-command-centralization-design.md +git commit -m "docs(commands): document centralized command execution policy" +```