Refactor phase 5: the Execution context, Claude Code as a plugin, questions across restarts #10
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-5-execution"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 5: the Execution context, and Claude Code as a plugin.
The runner becomes a context plus one plugin.
server/agent/runner.ts(1,087 lines) andplan.tsare shims of ~100 lines over the Execution context:packages/core/src/executionholds the domain (sessions, questions, the pure tool-gate rules), its events, errors, ports, commands, Kanbot's agent tools and the prompts, one per file;server/kanbot/executionruns sessions as fibers over theAgentRuntimesport, turns a runtime's events into the timeline and the metrics, and tells the workflow when a stage ends, exactly as before. The characterisation suite passes with its expectations untouched.A runtime is a plugin. The SDK gains the
AgentRuntimecontract:@RuntimeType({ type, label, icon }) class X extends AgentRuntime(orPromiseAgentRuntimefor async/await) takes aSessionSpec— the prompt and rules, Kanbot's tools already bound to the task, a gate it asks before each of the model's own tool calls, a way to ask the human — and streamsRuntimeEvents; it never touches the board or the database.plugins/claude-codeis that over@anthropic-ai/claude-agent-sdk, in thirteen small files;RuntimeKindregisters runtimes into the core's port likeTrackerKinddoes trackers; aruntimeContractkit passes against the plugin (over the fake SDK) and against a scripted fake. Plugins gainctx.process.exec, gated by a newprocess:spawnpermission.Sessions and questions survive a restart.
agent_sessions(migration 0010) records every run with its metrics; questions stay intask_questions; the live wait is aDeferredkept only as a cache.AnswerQuestiondelivers to a live session, or resumes the session by its runtime id with the answer, or wakes the workflow's gate (the three outcomes the characterisation test pins).RecoverSessionsat boot replacesrecoverOnBoot. Capacity is read from the running sessions in the database.Over RPC: the
Executiongroup (answer, dismiss, comment, plan, rework, plan-answer); three routes are gone and two kept thin for the Electron shell and uploads.LEGACY_API_ROUTESis at 8 (from 12).Checked: 583 tests (the restart scenario among them, on a scripted runtime); lint, typecheck, Storybook, the Nuxt build. On the production build with a database seeded as if killed mid-question: after the restart the task stayed waiting (nothing resumed at boot, the open row was closed as stopped), the answer came back
resumed, and a newclaude-codesession row was opened to resumesess-prod-1. It then failed because the executable was a stub, which is what the check expects.Deferred, on purpose:
task_workspacesandtask_planstables (the first belongs with the Workspace port in Phase 7, the second gains nothing until Phase 8); live projections for the new events (screens still hear these moments through the existing signals; adding projections would change the live-message snapshots — a follow-up when the signals go); the packaged desktop app was not rebuilt. Not run on Windows.Part of the DDD/CQRS refactor; the PRs are stacked, each based on the one before, so merge them in order: Phase 0 → U0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 → structure → plugin classes → UI pages → Phase 5.
CI: the Forgejo job has not run on these branches (every run was cancelled before a runner picked it up; no runner is registered for the repo). The same steps (
npm ci, lint, typecheck, tests, Storybook build) pass innode:24-bookwormlocally.🤖 Generated with Claude Code
packages/core/src/execution holds what a session is told and allowed: the AgentSession and Question Schemas, the Session*/Question*/Plan/Review/SentBack events, the pure rules (readOnlyViolation, planFirstViolation, mutatingGitCommand, classifyCommand, decideToolUse), Kanbot's tools as AgentTools over a ToolHost port, and the prompts as pure functions with {{tool:name}} tokens. Its ports — AgentRuntimes, SessionStore, QuestionStore, Workspace, TaskBoard, LiveSessions, WorkflowGate — are served by the adapters in server/kanbot/execution and by the new agent_sessions table (migration 0010, dual-written with the task's session id and metrics). server/kanbot/execution runs the sessions: ExecutionService forks each task, review and plan session into a FiberMap keyed by task, builds the SessionSpec (tools bound with JSON Schema, the gate over the rules and approvals, ask over the question row), turns the runtime's events into the same timeline lines the runner wrote, and tells the workflow as before. Claude Code runs behind the AgentRuntimes port as a legacy adapter over the SDK until the claude-code plugin serves it. server/agent/runner.ts, plan.ts and prompt.ts are shims over the service, so every caller and mock keeps working; the characterisation suite's expectations are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>Both tracks defined the AgentRuntimes port the same way; the Execution context now sits above the plugin host in the layer stack and takes the runtimes from RuntimeKind, so the interim adapter and its private Claude runtime go. Tests put a scripted runtime in front through startKanbot({ runtimes }), a layer that wraps the plugins' AgentRuntimes (test/support/scriptedRuntime.ts); without an override the sessions run on the claude-code plugin over the fake SDK, which the deliverAnswer characterisation test relies on. The plugin list also names a plugin's agent runtimes, and labels the two new permissions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>A setting had to be added in four places (the defaults and the per-field merge in getSettings, the zod schema and setSetting in PUT /api/settings, the app's defaultSettings) or it silently dropped. Now each section is a Schema in the Settings context (packages/core/src/settings), its defaults in SETTINGS_DEFAULTS beside it (plain data, so the app shows them without loading Effect), and reading is decoding the stored rows with it (readSettings): a field or section never stored reads back as its default, a row that does not decode as its section's default. The legacy fixes are decode steps: groomingModel becomes planningModel, this machine's launchers and the KANBOT_* numbers apply only where nothing was stored, and the columns workflows provide are added by the server's shim. getSettings, getSettingsRow, publicSettings and setSetting stay as that synchronous shim, so the legacy readers and the characterisation tests are unchanged; the old merge is kept in test/settings as the reference every stored shape must still read back as. Changing settings is the ChangeSettings command over the new Settings RPC group (get, update a patch of whole sections): checked by the Schemas, text trimmed and the board's own columns kept as PUT did, and one SettingsChanged (LocaleChanged for the language) per section that really changed. They are projected to the messages screens already handle (board.changed, launchers.changed, runner.changed, sync.changed, locale.changed) plus settings.changed, which the app's single settings query listens for; a consumer restarts the standup timer and lets the scheduler start what a higher limit allows. PUT /api/settings goes; GET stays for Electron. The app reads one shared copy (useAppSettings on useRpcQuery) and writes through useSettingsCommands, whose answer replaces that copy, so the refreshNuxtData('app-settings') workaround and defaultSettings() go. The tour finishes through the same command. Discord is a plugin now: its section, types and form leave the settings (the Integrations section goes; the tour and the menu point at Plugins). At boot, before the plugins start, a stored `discord` row becomes the Discord plugin's settings and secret (and an off bot an off plugin), then the row is dropped, so it happens once. The legacy bot reads the plugin's config until its plugin replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>