From b469509a045bc5d4cd665f5a2a143dac22bfb0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Fri, 3 Apr 2026 00:23:26 +0200 Subject: [PATCH] feat: add WORKFLOW_AGENTS environment variable for per-agent workflow filtering ## Intent Allow users to restrict workflow activation to specific agents via the WORKFLOW_AGENTS environment variable. This prevents subagents (Tasks) from being interrupted by workflow instructions when they are not expected to follow the workflow. ## Major changes ### opencode-plugin - New WORKFLOW_AGENTS env var: comma-separated list of agent names (e.g., 'general,architect') - If unset (default), workflows are active for all agents (backward compatible) - If set, only agents in the list can use workflows - Agent filter evaluated on every chat.message hook - Tools throw clear error if agent not in whitelist: 'Workflows are not enabled for this agent' - Removed WORKFLOW=off global toggle (no longer needed with per-agent filtering) - Removed /workflow on|off command (YAGNI) ### opencode-tui-plugin - Parse and respect WORKFLOW_AGENTS environment variable - Widget visibility derived from agent whitelist + current agent - Automatically hide/show widget on agent switches (reactive, no session state) - If agent not in filter, widget returns null (no DOM footprint) ### Tests - Updated test suite to validate agent filtering instead of global on/off toggle - All 53 tests passing - New tests: agent not in filter throws error, agent in filter works, handles whitespace ### Documentation - Updated opencode-plugin README with Configuration section - Updated TUI plugin README with agent-based visibility explanation - Removed .opencode/commands/workflow.md (command no longer exists) ## Side effects - Backward compatible: workflows default to all agents when WORKFLOW_AGENTS is unset - Simpler codebase: removed all session-state caching for per-session toggles - Cleaner semantics: agent filter is now the single source of truth - Better UX: users get clear error messages instead of silent failures Co-authored-by: udondan --- .beads/issues.jsonl | 12 ++ .beads/last-touched | 2 +- .opencode/commands/workflow.md | 18 -- ...ble-vibe-no-tools-by-subagents-trs5uu.json | 29 +++ ...ible-vibe-per-agent-activation-mh4d6e.json | 29 +++ .../development-plan-per-agent-activation.md | 57 ++++++ .vibe/development-plan.md | 52 +++++ opencode.json | 4 +- packages/opencode-plugin/README.md | 22 ++ packages/opencode-plugin/src/plugin.ts | 105 ++++------ .../opencode-plugin/test/e2e/plugin.test.ts | 166 ++++++++------- packages/opencode-tui-plugin/README.md | 9 + .../opencode-tui-plugin/workflows-phase.tsx | 191 +++++++++++------- tui.json | 4 +- 14 files changed, 473 insertions(+), 227 deletions(-) delete mode 100644 .opencode/commands/workflow.md create mode 100644 .vibe/beads-state-responsible-vibe-no-tools-by-subagents-trs5uu.json create mode 100644 .vibe/beads-state-responsible-vibe-per-agent-activation-mh4d6e.json create mode 100644 .vibe/development-plan-per-agent-activation.md create mode 100644 .vibe/development-plan.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 10356cb6..1c1c9037 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -302,6 +302,18 @@ {"id":"responsible-vibe-3.2.3","title":"Test version information functionality","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-19T17:40:49.472891+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-19T17:42:51.635776+01:00","closed_at":"2026-01-19T17:42:51.635776+01:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-3.2.3","depends_on_id":"responsible-vibe-3.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-3.3","title":"Finalize","description":"minor workflow finalize phase tasks","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-19T17:36:03.061534+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-19T17:36:03.061534+01:00","dependencies":[{"issue_id":"responsible-vibe-3.3","depends_on_id":"responsible-vibe-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-3.3","depends_on_id":"responsible-vibe-3.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-3.3.1","title":"Code cleanup - remove debug output and review comments","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-19T17:43:27.029293+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-19T17:44:11.212552+01:00","closed_at":"2026-01-19T17:44:11.212552+01:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-3.3.1","depends_on_id":"responsible-vibe-3.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30","title":"responsible-vibe: epcc (development-plan-per-agent-activation.md)","description":"Responsible vibe engineering session using epcc workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:30:34.747524+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:30:34.747524+02:00"} +{"id":"responsible-vibe-30.1","title":"Explore","description":"Research the codebase to understand existing patterns and gather context about the problem space. - If uncertain about conventions or rules, ask the user about them - Read relevant files and documentation - If exists: Understand and document requirements there - Otherwise: Document requirements in your task management system Focus on understanding without writing code yet. Document your findings and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:30:34.910722+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:30:34.910722+02:00","dependencies":[{"issue_id":"responsible-vibe-30.1","depends_on_id":"responsible-vibe-30","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2","title":"Plan","description":"Create a detailed implementation strategy based on your exploration: - If exists: Base your strategy on requirements from it - Otherwise: Use existing task context Break down the work into specific, actionable tasks. Consider edge cases, dependencies, and potential challenges. - If architectural changes needed and exists: Document in - Otherwise: Create tasks to track architectural decisions - If exists: Adhere to the design in it - Otherwise: Elaborate design options and present them to the user Document the planning work thoroughly and create implementation tasks as part of the code phase as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:30:35.084948+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:30:35.084948+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2","depends_on_id":"responsible-vibe-30","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-30.2","depends_on_id":"responsible-vibe-30.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.1","title":"Simplify opencode-plugin: remove sessionEnabled + sessionAgents maps","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:09.500848+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:40:07.553498+02:00","closed_at":"2026-04-02T23:40:07.553498+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-30.2.1","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.2","title":"System prompt injection: evaluate agent filter on every chat.message, inject suppression if needed","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:12.778193+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:33:12.778193+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2.2","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.3","title":"Update tool wrappers: throw error if agent not in WORKFLOW_AGENTS filter","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:15.984134+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:40:07.554316+02:00","closed_at":"2026-04-02T23:40:07.554316+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-30.2.3","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.4","title":"TUI plugin: simplify to agent filter only, remove sessionOverrideMap","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:19.510073+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:33:19.510073+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2.4","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.5","title":"Rename WORKFLOW_ACTIVE_AGENTS to WORKFLOW_AGENTS throughout codebase","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:36.52056+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:33:36.52056+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2.5","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.6","title":"Update tests: remove sessionEnabled tests, add agent filter validation tests","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:41.669587+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:33:41.669587+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2.6","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.2.7","title":"Update docs: clarify WORKFLOW_AGENTS behavior, agent switching, system prompt injection","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:33:44.789611+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:33:44.789611+02:00","dependencies":[{"issue_id":"responsible-vibe-30.2.7","depends_on_id":"responsible-vibe-30.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.3","title":"Code","description":"Follow your plan to build the solution: - If exists: Follow the design from it - Otherwise: Elaborate design options and present them to the user - If exists: Build according to the architecture from it - Otherwise: Elaborate architectural options and present them to the user - If exists: Ensure requirements from it are met - Otherwise: Ensure existing requirements are met based on your task context Write clean, well-structured code with proper error handling. Prevent regression by building, linting, and executing existing tests. Stay flexible and adapt the plan as you learn more during implementation. Update task progress and create new tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:30:35.261366+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:30:35.261366+02:00","dependencies":[{"issue_id":"responsible-vibe-30.3","depends_on_id":"responsible-vibe-30","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-30.3","depends_on_id":"responsible-vibe-30.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-30.4","title":"Commit","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: 1. **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. 2. **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed 3. **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: 1. **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update it if requirements changed during development - If exists: Update it if architectural impacts were identified - If exists: Update it if design details were refined or changed - Otherwise: Document any changes in the plan file 2. **Compare Against Implementation**: Review documentation against actual implemented functionality 3. **Update Changed Sections**: Only modify documentation sections that have functional changes 4. **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions 5. **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process 6. **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure code is ready for production/delivery Update task progress and mark completed work as you finalize the feature.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-02T23:30:35.431638+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-02T23:30:35.431638+02:00","dependencies":[{"issue_id":"responsible-vibe-30.4","depends_on_id":"responsible-vibe-30","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-30.4","depends_on_id":"responsible-vibe-30.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4","title":"Responsible-Vibe Development: responsible-vibe","description":"Development session using minor workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.033247+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.033247+01:00"} {"id":"responsible-vibe-4.1","title":"Explore","description":"minor workflow explore phase tasks","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.241377+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.241377+01:00","dependencies":[{"issue_id":"responsible-vibe-4.1","depends_on_id":"responsible-vibe-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4.1.1","title":"Analyze current verbose task management sections in plan file templates","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:46:16.960883+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:46:58.154323+01:00","closed_at":"2026-01-20T08:46:58.154323+01:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-4.1.1","depends_on_id":"responsible-vibe-4.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index e6028190..b4fc02b0 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -responsible-vibe-29.5 +responsible-vibe-30.2.7 diff --git a/.opencode/commands/workflow.md b/.opencode/commands/workflow.md deleted file mode 100644 index 20ee2228..00000000 --- a/.opencode/commands/workflow.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -description: Enable or disable workflows for this session ---- - -Toggle workflows for the current session: - -- `/workflow on` - Enable workflows -- `/workflow off` - Disable workflows -- `/wf on` - Enable workflows (shorthand) -- `/wf off` - Disable workflows (shorthand) - -When workflows are disabled, the plugin will not inject development instructions or enforce file edit restrictions. - -You can also set the initial state via environment variable: - -```bash -WORKFLOW=off opencode -``` diff --git a/.vibe/beads-state-responsible-vibe-no-tools-by-subagents-trs5uu.json b/.vibe/beads-state-responsible-vibe-no-tools-by-subagents-trs5uu.json new file mode 100644 index 00000000..333024ab --- /dev/null +++ b/.vibe/beads-state-responsible-vibe-no-tools-by-subagents-trs5uu.json @@ -0,0 +1,29 @@ +{ + "conversationId": "responsible-vibe-no-tools-by-subagents-trs5uu", + "projectPath": "/Users/oliverjaegle/projects/privat/mcp-server/responsible-vibe", + "epicId": "responsible-vibe-30", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "responsible-vibe-30.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "responsible-vibe-30.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "responsible-vibe-30.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "responsible-vibe-30.4" + } + ], + "createdAt": "2026-04-02T20:43:55.969Z", + "updatedAt": "2026-04-02T20:43:55.969Z" +} \ No newline at end of file diff --git a/.vibe/beads-state-responsible-vibe-per-agent-activation-mh4d6e.json b/.vibe/beads-state-responsible-vibe-per-agent-activation-mh4d6e.json new file mode 100644 index 00000000..c4dd7544 --- /dev/null +++ b/.vibe/beads-state-responsible-vibe-per-agent-activation-mh4d6e.json @@ -0,0 +1,29 @@ +{ + "conversationId": "responsible-vibe-per-agent-activation-mh4d6e", + "projectPath": "/Users/oliverjaegle/projects/privat/mcp-server/responsible-vibe", + "epicId": "responsible-vibe-30", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "responsible-vibe-30.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "responsible-vibe-30.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "responsible-vibe-30.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "responsible-vibe-30.4" + } + ], + "createdAt": "2026-04-02T21:30:35.849Z", + "updatedAt": "2026-04-02T21:30:35.849Z" +} \ No newline at end of file diff --git a/.vibe/development-plan-per-agent-activation.md b/.vibe/development-plan-per-agent-activation.md new file mode 100644 index 00000000..81c11723 --- /dev/null +++ b/.vibe/development-plan-per-agent-activation.md @@ -0,0 +1,57 @@ +# Development Plan: responsible-vibe (per-agent-activation branch) + +*Generated on 2026-04-02 by Vibe Feature MCP* +*Workflow: [epcc](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/epcc)* + +## Goal +Simplify PR #257's per-agent workflow activation by eliminating redundant session-level state caches. Replace session-based `/workflow on|off` toggling with pure per-agent filtering using tool-execution validation. + +## Key Decisions +1. **Remove session state tracking entirely** — No `sessionEnabled` map, no `sessionOverrideMap`, no `/workflow on|off` command ✓ +2. **Agent filtering happens per-message** — On every `chat.message` hook, check if current agent is in `WORKFLOW_AGENTS` filter ✓ +3. **NO system prompt injection** — Only use agent filter in hooks and tool wrappers (simpler, cleaner) +4. **NO `/workflow on|off` command** — YAGNI. Workflows available = agent in filter. That's it. +5. **Agent not in filter** → Hooks skip, tools throw error (clear, simple) +6. **Rename env var** — `WORKFLOW_ACTIVE_AGENTS` → `WORKFLOW_AGENTS` (clearer intent) ✓ +7. **Agent switching works automatically** — As agent changes, hooks re-evaluate without session state + +## Notes +- Simplified dramatically: only 3 components need changes (plugin init, chat.message hook, tool wrappers) +- TUI updates: parse `WORKFLOW_AGENTS` env var, derive agent from messages, compute `isActive` memo +- No command hook needed - eliminated YAGNI feature entirely +- Build succeeds without errors ✓ + +## Explore + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Plan + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-30.2.1` Simplify opencode-plugin: remove sessionEnabled + sessionAgents maps +- [ ] `responsible-vibe-30.2.2` System prompt injection: evaluate agent filter on every chat.message, inject suppression if needed +- [x] `responsible-vibe-30.2.3` Update tool wrappers: throw error if agent not in WORKFLOW_AGENTS filter +- [ ] `responsible-vibe-30.2.4` TUI plugin: simplify to agent filter only, remove sessionOverrideMap +- [ ] `responsible-vibe-30.2.5` Rename WORKFLOW_ACTIVE_AGENTS to WORKFLOW_AGENTS throughout codebase +- [ ] `responsible-vibe-30.2.6` Update tests: remove sessionEnabled tests, add agent filter validation tests +- [ ] `responsible-vibe-30.2.7` Update docs: clarify WORKFLOW_AGENTS behavior, agent switching, system prompt injection + +## Code + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Commit + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + diff --git a/.vibe/development-plan.md b/.vibe/development-plan.md new file mode 100644 index 00000000..f9df362c --- /dev/null +++ b/.vibe/development-plan.md @@ -0,0 +1,52 @@ +# Development Plan: responsible-vibe (main branch) + +*Generated on 2026-04-02 by Vibe Feature MCP* +*Workflow: [bugfix](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/bugfix)* + +## Goal +*Define what you're building or fixing - this will be updated as requirements are gathered* +## Key Decisions +*Important decisions will be documented here as they are made* + +## Notes +*Additional context and observations* + +## Reproduce +### Tasks +- [ ] *Tasks will be added as they are identified* + +### Completed +- [x] Created development plan file + +## Analyze +### Tasks +- [ ] *To be added when this phase becomes active* + +### Completed +*None yet* + +## Fix +### Tasks +- [ ] *To be added when this phase becomes active* + +### Completed +*None yet* + +## Verify +### Tasks +- [ ] *To be added when this phase becomes active* + +### Completed +*None yet* + +## Finalize +### Tasks +- [ ] *To be added when this phase becomes active* + +### Completed +*None yet* + + + +--- +*This plan is maintained by the LLM. Tool responses provide guidance on which section to focus on and what tasks to work on.* diff --git a/opencode.json b/opencode.json index 366e4640..22a77434 100644 --- a/opencode.json +++ b/opencode.json @@ -1,7 +1,9 @@ { "$schema": "https://opencode.ai/config.json", "logLevel": "DEBUG", - "plugin": ["@codemcp/workflows-opencode"], + "plugin": [ + "/Users/oliverjaegle/projects/privat/mcp-server/responsible-vibe/packages/opencode-plugin/dist/index.js" + ], "mcp": { "workflows": { "type": "local", diff --git a/packages/opencode-plugin/README.md b/packages/opencode-plugin/README.md index 32b23920..e369687d 100644 --- a/packages/opencode-plugin/README.md +++ b/packages/opencode-plugin/README.md @@ -75,6 +75,28 @@ Or for local development: } ``` +## Configuration + +### Agent Filtering + +By default, the plugin is active for all agents. Set `WORKFLOW_AGENTS` to a comma-separated list of agent names to restrict it to specific agents only: + +```bash +# Only activate for the "coder" and "architect" agents +WORKFLOW_AGENTS=coder,architect npx opencode +``` + +When the env var is set, workflow hooks are skipped and tools throw a clear error for any agent not in the list. This prevents subagents (Tasks) from being interrupted by workflow instructions when they are not expected to follow the workflow. + +**When unset**, workflows are active for all agents (default behavior). + +### Per-Agent Behavior + +- **Agent in filter**: Workflow instructions are injected on every message, tools work normally +- **Agent not in filter**: Workflow instructions are skipped, tools throw "not enabled for this agent" error + +This design makes agent switching automatic—no session state needed. When the user switches agents, the TUI widget visibility and hook behavior adapt immediately based on the new agent. + ## Status Integrated with `@codemcp/workflows-core` for real state management and phase-based file restrictions. diff --git a/packages/opencode-plugin/src/plugin.ts b/packages/opencode-plugin/src/plugin.ts index 5e367ca9..40f5a610 100644 --- a/packages/opencode-plugin/src/plugin.ts +++ b/packages/opencode-plugin/src/plugin.ts @@ -114,10 +114,33 @@ export const WorkflowsPlugin: Plugin = async ( worktree: input.worktree, }); - // Initialize workflows enabled state from environment variable - const envWorkflows = process.env.WORKFLOW?.toLowerCase(); - let workflowsEnabled = envWorkflows === 'off' ? false : true; // default: enabled - logger.info('Workflows state initialized', { workflowsEnabled }); + // Parse WORKFLOW_AGENTS env var: comma-separated list of agent names. + // When set, workflows only activate for agents in that list. + // When not set (or empty), workflows activate for all agents (default). + const envAgentFilter = process.env.WORKFLOW_AGENTS; + const agentFilter: Set | null = + envAgentFilter && envAgentFilter.trim() + ? new Set( + envAgentFilter + .split(',') + .map(a => a.trim().toLowerCase()) + .filter(Boolean) + ) + : null; // null = no filter, all agents active + + /** + * Check if workflows should run for the given agent. + * If WORKFLOW_AGENTS is set, only agents in that list are active. + * If WORKFLOW_AGENTS is not set, all agents are active. + */ + function isAgentEnabled(agent: string | undefined): boolean { + if (agentFilter === null) return true; // no filter → all agents active + return agentFilter.has((agent ?? '').toLowerCase()); + } + + logger.info('Workflows state initialized', { + agentFilter: agentFilter ? [...agentFilter] : 'all (no filter)', + }); // Initialize instruction generator const planManager = new PlanManager(); @@ -262,9 +285,11 @@ export const WorkflowsPlugin: Plugin = async ( } } - // Skip if workflows are disabled - if (!workflowsEnabled) { - logger.debug('chat.message: Workflows disabled, skipping hook'); + // Skip if agent is not in the active agent filter + if (!isAgentEnabled(hookInput.agent)) { + logger.debug('chat.message: Agent does not support workflows', { + agent: hookInput.agent, + }); return; } @@ -367,12 +392,6 @@ export const WorkflowsPlugin: Plugin = async ( * Fires before each tool execution. We block disallowed file edits based on phase. */ 'tool.execute.before': async (hookInput, output) => { - // Skip if workflows are disabled - if (!workflowsEnabled) { - logger.debug('tool.execute.before: Workflows disabled, skipping hook'); - return; - } - const editTools = ['edit', 'write', 'patch', 'apply_patch', 'multiedit']; if (!editTools.includes(hookInput.tool)) { return; @@ -428,14 +447,6 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin * to preserve and instruct the summary to end with phase continuation. */ 'experimental.session.compacting': async (hookInput, output) => { - // Skip if workflows are disabled - if (!workflowsEnabled) { - logger.debug( - 'experimental.session.compacting: Workflows disabled, skipping hook' - ); - return; - } - logger.debug('experimental.session.compacting hook fired', { sessionID: hookInput.sessionID, }); @@ -457,54 +468,22 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin }, /** - * Hook 4: command.execute.before - * Intercept /workflow and /wf commands to toggle workflows enabled state - */ - 'command.execute.before': async (hookInput, output) => { - const cmd = hookInput.command.toLowerCase(); - const args = (hookInput.arguments || '').toLowerCase().trim(); - - if (cmd === 'workflow' || cmd === 'wf') { - if (args === 'on') { - workflowsEnabled = true; - output.parts.push({ - id: `prt_workflows_toggle_${Date.now()}`, - type: 'text' as const, - text: 'Workflows enabled for this session.', - }); - logger.info('Workflows toggled via command', { workflowsEnabled }); - } else if (args === 'off') { - workflowsEnabled = false; - output.parts.push({ - id: `prt_workflows_toggle_${Date.now()}`, - type: 'text' as const, - text: 'Workflows disabled for this session. Plugin will not inject instructions or enforce file restrictions.', - }); - logger.info('Workflows toggled via command', { workflowsEnabled }); - } else { - output.parts.push({ - id: `prt_workflows_toggle_${Date.now()}`, - type: 'text' as const, - text: `Usage: /workflow on|off or /wf on|off\nCurrent state: ${workflowsEnabled ? 'enabled' : 'disabled'}`, - }); - } - } - }, - - /** - * Custom tools - always registered so /workflow on can re-enable them mid-session. - * Each tool's execute method checks workflowsEnabled at call time and throws a - * clear message when disabled, rather than silently failing. + * Custom tools - always registered to allow clear error messages. + * Each tool's execute method checks the agent filter at call time and throws + * an error if the agent is not allowed to use workflows. */ tool: await (async (): Promise<{ [key: string]: ToolDefinition }> => { - const DISABLED_MSG = - 'Workflows are disabled (WORKFLOW=off). Enable with /workflow on or /wf on'; const wrap = (def: ToolDefinition): ToolDefinition => ({ ...def, execute: async (args, ctx) => { - if (!workflowsEnabled) { - throw new Error(DISABLED_MSG); + const agent = ctx.agent; + + if (!isAgentEnabled(agent)) { + throw new Error( + `Workflows are not enabled for this agent (${agent}). Set WORKFLOW_AGENTS environment variable to include this agent, or use a different agent.` + ); } + return def.execute(args, ctx); }, }); diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index f723e2b6..d3e7e9d1 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -744,148 +744,174 @@ describe('File Pattern Restrictions', () => { } }); -describe('WORKFLOW=off environment variable', () => { - it('registers tools when WORKFLOW=off, but execute throws a clear disabled error', async () => { +describe('WORKFLOW_AGENTS environment variable', () => { + it('skips chat.message hook for agents not in WORKFLOW_AGENTS filter', async () => { const dir = createTempDir(); - const originalEnv = process.env.WORKFLOW; + const originalEnv = process.env.WORKFLOW_AGENTS; try { - process.env.WORKFLOW = 'off'; + process.env.WORKFLOW_AGENTS = 'general,architect'; const hooks = await WorkflowsPlugin(createMockPluginInput(dir)); - // Tools are still registered (so /workflow on can re-enable them) - expect(hooks.tool).toBeDefined(); - expect(hooks.tool).toHaveProperty('start_development'); - expect(hooks.tool).toHaveProperty('proceed_to_phase'); - expect(hooks.tool).toHaveProperty('conduct_review'); - expect(hooks.tool).toHaveProperty('reset_development'); - expect(hooks.tool).toHaveProperty('setup_project_docs'); + // chat.message should not inject when agent is not in filter + const output: { message: UserMessage; parts: Part[] } = { + message: { id: 'msg1', sessionID: 'sess1', role: 'user' }, + parts: [], + }; - // But executing a tool throws with a clear message - await expect( - hooks.tool!['start_development'].execute({ workflow: 'minor' }, { - sessionID: 'test-session', - } as unknown) - ).rejects.toThrow(/disabled/i); + await hooks['chat.message']!( + { + sessionID: 'sess1', + agent: 'explore', // Not in filter + messageID: 'msg1', + }, + output + ); - // Command hook is available for toggling - expect(hooks['command.execute.before']).toBeDefined(); + // No workflow prompt should be injected for non-whitelisted agent + expect(output.parts.length).toBe(0); } finally { if (originalEnv === undefined) { - delete process.env.WORKFLOW; + delete process.env.WORKFLOW_AGENTS; } else { - process.env.WORKFLOW = originalEnv; + process.env.WORKFLOW_AGENTS = originalEnv; } cleanupDir(dir); } }); - it('allows tool execution after /wf on when started with WORKFLOW=off', async () => { + it('throws error when tool is called by agent not in WORKFLOW_AGENTS', async () => { const dir = createTempDir(); - const originalEnv = process.env.WORKFLOW; + const originalEnv = process.env.WORKFLOW_AGENTS; try { - process.env.WORKFLOW = 'off'; + process.env.WORKFLOW_AGENTS = 'general,architect'; const hooks = await WorkflowsPlugin(createMockPluginInput(dir)); - // Confirm disabled initially + // Tool should throw when agent is not in filter await expect( hooks.tool!['start_development'].execute({ workflow: 'minor' }, { sessionID: 'test-session', + agent: 'explore', // Not in filter } as unknown) - ).rejects.toThrow(/disabled/i); + ).rejects.toThrow(/not enabled for this agent/i); + } finally { + if (originalEnv === undefined) { + delete process.env.WORKFLOW_AGENTS; + } else { + process.env.WORKFLOW_AGENTS = originalEnv; + } + cleanupDir(dir); + } + }); - // Toggle on via command - const output: { parts: Part[] } = { parts: [] }; - await hooks['command.execute.before']!( - { command: 'workflow', arguments: 'on', sessionID: 'test-session' }, - output - ); - expect( - output.parts[0]?.type === 'text' && output.parts[0].text - ).toContain('enabled'); + it('allows tool execution when agent is in WORKFLOW_AGENTS filter', async () => { + const dir = createTempDir(); + const originalEnv = process.env.WORKFLOW_AGENTS; + try { + process.env.WORKFLOW_AGENTS = 'general,architect'; + + const hooks = await WorkflowsPlugin(createMockPluginInput(dir)); - // Now the tool should no longer throw the disabled error - // (it may fail for other reasons like no plan file, but not the disabled guard) + // Tool should NOT throw the agent filter error when agent is whitelisted + // (it may fail for other reasons like no plan file, but not the filter guard) let thrownMessage: string | undefined; try { await hooks.tool!['start_development'].execute({ workflow: 'minor' }, { sessionID: 'test-session', + agent: 'general', // In filter } as unknown); } catch (err) { thrownMessage = (err as Error).message; } - // If it did throw, it must NOT be the disabled message + // If it did throw, it must NOT be the agent filter message if (thrownMessage !== undefined) { - expect(thrownMessage).not.toMatch(/disabled/i); + expect(thrownMessage).not.toMatch(/not enabled for this agent/i); } } finally { if (originalEnv === undefined) { - delete process.env.WORKFLOW; + delete process.env.WORKFLOW_AGENTS; } else { - process.env.WORKFLOW = originalEnv; + process.env.WORKFLOW_AGENTS = originalEnv; } cleanupDir(dir); } }); - it('loads all tools and hooks when WORKFLOW is not set (default)', async () => { + it('allows all agents when WORKFLOW_AGENTS is not set', async () => { const dir = createTempDir(); - const originalEnv = process.env.WORKFLOW; + const originalEnv = process.env.WORKFLOW_AGENTS; try { - delete process.env.WORKFLOW; + delete process.env.WORKFLOW_AGENTS; const hooks = await WorkflowsPlugin(createMockPluginInput(dir)); - // When WORKFLOW is not set, all hooks and tools should be registered + // When WORKFLOW_AGENTS is not set, all hooks and tools should work for any agent expect(hooks['chat.message']).toBeDefined(); expect(hooks['tool.execute.before']).toBeDefined(); expect(hooks['experimental.session.compacting']).toBeDefined(); - expect(hooks['command.execute.before']).toBeDefined(); expect(hooks.tool).toBeDefined(); - // Tools should be populated + // Tools should be populated and allow any agent expect(hooks.tool).toHaveProperty('start_development'); - expect(hooks.tool).toHaveProperty('proceed_to_phase'); - expect(hooks.tool).toHaveProperty('conduct_review'); - expect(hooks.tool).toHaveProperty('reset_development'); - expect(hooks.tool).toHaveProperty('setup_project_docs'); + + // Tool should not throw agent filter error for any agent + let thrownMessage: string | undefined; + try { + await hooks.tool!['start_development'].execute({ workflow: 'minor' }, { + sessionID: 'test-session', + agent: 'any-agent', // Should work when no filter is set + } as unknown); + } catch (err) { + thrownMessage = (err as Error).message; + } + if (thrownMessage !== undefined) { + expect(thrownMessage).not.toMatch(/not enabled for this agent/i); + } } finally { if (originalEnv === undefined) { - delete process.env.WORKFLOW; + delete process.env.WORKFLOW_AGENTS; } else { - process.env.WORKFLOW = originalEnv; + process.env.WORKFLOW_AGENTS = originalEnv; } cleanupDir(dir); } }); - it('loads all tools and hooks when WORKFLOW=on', async () => { + it('handles comma-separated agent list with whitespace', async () => { const dir = createTempDir(); - const originalEnv = process.env.WORKFLOW; + const originalEnv = process.env.WORKFLOW_AGENTS; try { - process.env.WORKFLOW = 'on'; + process.env.WORKFLOW_AGENTS = ' general , architect , explore '; const hooks = await WorkflowsPlugin(createMockPluginInput(dir)); - // When WORKFLOW=on, all hooks and tools should be registered - expect(hooks['chat.message']).toBeDefined(); - expect(hooks['tool.execute.before']).toBeDefined(); - expect(hooks['experimental.session.compacting']).toBeDefined(); - expect(hooks['command.execute.before']).toBeDefined(); - expect(hooks.tool).toBeDefined(); + // Should parse correctly and allow the whitelisted agents + let thrownMessage: string | undefined; + try { + await hooks.tool!['start_development'].execute({ workflow: 'minor' }, { + sessionID: 'test-session', + agent: 'architect', // Should work after trimming + } as unknown); + } catch (err) { + thrownMessage = (err as Error).message; + } + if (thrownMessage !== undefined) { + expect(thrownMessage).not.toMatch(/not enabled for this agent/i); + } - // Tools should be populated - expect(hooks.tool).toHaveProperty('start_development'); - expect(hooks.tool).toHaveProperty('proceed_to_phase'); - expect(hooks.tool).toHaveProperty('conduct_review'); - expect(hooks.tool).toHaveProperty('reset_development'); - expect(hooks.tool).toHaveProperty('setup_project_docs'); + // And reject agents not in the list + await expect( + hooks.tool!['start_development'].execute({ workflow: 'minor' }, { + sessionID: 'test-session', + agent: 'other-agent', // Not in list + } as unknown) + ).rejects.toThrow(/not enabled for this agent/i); } finally { if (originalEnv === undefined) { - delete process.env.WORKFLOW; + delete process.env.WORKFLOW_AGENTS; } else { - process.env.WORKFLOW = originalEnv; + process.env.WORKFLOW_AGENTS = originalEnv; } cleanupDir(dir); } diff --git a/packages/opencode-tui-plugin/README.md b/packages/opencode-tui-plugin/README.md index 8065f059..033c3928 100644 --- a/packages/opencode-tui-plugin/README.md +++ b/packages/opencode-tui-plugin/README.md @@ -26,6 +26,15 @@ epcc: code The plugin reads state from `.vibe/conversations/*/state.json` in your project directory and updates whenever any responsible-vibe tool is invoked. +### Agent-based visibility + +The plugin respects the `WORKFLOW_AGENTS` environment variable: + +- **When `WORKFLOW_AGENTS` is set**: Widget only appears for agents in the whitelist +- **When `WORKFLOW_AGENTS` is unset**: Widget appears for all agents (default behavior) + +Widget visibility updates automatically when you switch agents—no manual refresh needed. + ## Supported tool modes The plugin works with both integration modes: diff --git a/packages/opencode-tui-plugin/workflows-phase.tsx b/packages/opencode-tui-plugin/workflows-phase.tsx index a4c85231..883cb234 100644 --- a/packages/opencode-tui-plugin/workflows-phase.tsx +++ b/packages/opencode-tui-plugin/workflows-phase.tsx @@ -235,6 +235,28 @@ const tui: TuiPlugin = async api => { // Set WORKFLOW=off to disable the TUI sidebar widget. if (process.env.WORKFLOW?.toLowerCase() === 'off') return; + // Parse WORKFLOW_AGENTS env var: comma-separated list of agent names. + // When set, workflows only activate for agents in that list. + // When not set (or empty), workflows activate for all agents (default). + const envAgentFilter = process.env.WORKFLOW_AGENTS; + const agentFilter: Set | null = + envAgentFilter && envAgentFilter.trim() + ? new Set( + envAgentFilter + .split(',') + .map(a => a.trim().toLowerCase()) + .filter(Boolean) + ) + : null; // null = no filter, all agents active + + /** + * Check if workflows should run for the given agent. + */ + function isAgentEnabled(agent: string | undefined): boolean { + if (agentFilter === null) return true; // no filter → all agents active + return agentFilter.has((agent ?? '').toLowerCase()); + } + api.slots.register({ order: 5, slots: { @@ -247,6 +269,25 @@ const tui: TuiPlugin = async api => { } | null>(null); const [collapsed, setCollapsed] = createSignal(false); + // Derive the current agent for this session from the last message. + // api.state.session.messages() is a reactive SolidJS accessor. + const currentAgent = createMemo(() => { + const messages = api.state.session.messages(props.session_id); + if (!messages || messages.length === 0) return undefined; + // Walk backwards to find the most recent message with an agent field + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] as { agent?: string }; + if (msg.agent) return msg.agent.toLowerCase(); + } + return undefined; + }); + + // Derive whether the widget should be visible based on agent filter + const isActive = createMemo(() => { + const agent = currentAgent(); + return isAgentEnabled(agent); + }); + // Spinner frames for the current-phase icon const SPINNER = ['◐', '◓', '◑', '◒']; const [spinnerFrame, setSpinnerFrame] = createSignal(0); @@ -295,17 +336,37 @@ const tui: TuiPlugin = async api => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- JSX element typed as `error` by @opentui/solid's JSX types; safe at runtime return ( - - {/* Header row — clickable to collapse/expand when an active workflow is present */} - state() && setCollapsed(c => !c)} - > - {state() ? ( - (state()?.phases ?? []).length === 0 ? ( - // Phases unknown - collapsed() ? ( - // Collapsed: ▶ workflowName phaseName + // Return null (no DOM node at all) when the agent filter excludes this session's agent. + // Returning an empty would still occupy a line in the sidebar. + !isActive() ? null : ( + + {/* Header row — clickable to collapse/expand when an active workflow is present */} + state() && setCollapsed(c => !c)} + > + {state() ? ( + (state()?.phases ?? []).length === 0 ? ( + // Phases unknown + collapsed() ? ( + // Collapsed: ▶ workflowName phaseName + + {'▶ '} + {state()?.workflow} + + {' '} + {state()?.phase} + + + ) : ( + // Expanded: ▼ Workflow (body shows workflowName phaseName) + + {'▼ '} + Workflow + + ) + ) : collapsed() ? ( + // Collapsed + active: ▶ workflowName phaseName {'▶ '} {state()?.workflow} @@ -315,78 +376,62 @@ const tui: TuiPlugin = async api => { ) : ( - // Expanded: ▼ Workflow (body shows workflowName phaseName) + // Expanded + active: ▼ Workflow workflowName {'▼ '} - Workflow + Workflow {state()?.workflow} ) - ) : collapsed() ? ( - // Collapsed + active: ▶ workflowName phaseName - - {'▶ '} + ) : ( + // No active workflow + // eslint-disable-next-line solid/style-prop -- `fg` is an OpenTUI-specific style prop, not a standard CSS property + Workflow + )} + + {/* Expanded phase list */} + {!collapsed() && state() ? ( + (state()?.phases ?? []).length > 0 ? ( + + + {(phase, index) => ( + = 0 && + index < currentPhaseIndex() + ? theme().success + : theme().textMuted + } + > + {phase() === state()?.phase + ? `${SPINNER[spinnerFrame()]} ` + : currentPhaseIndex() >= 0 && + index < currentPhaseIndex() + ? '● ' + : '○ '} + {phase()} + + )} + + + ) : ( + // Phases unknown — show workflowName phaseName + {state()?.workflow} {' '} {state()?.phase} - - ) : ( - // Expanded + active: ▼ Workflow workflowName - - {'▼ '} - Workflow {state()?.workflow} - + ) - ) : ( - // No active workflow - // eslint-disable-next-line solid/style-prop -- `fg` is an OpenTUI-specific style prop, not a standard CSS property - Workflow - )} - - {/* Expanded phase list */} - {!collapsed() && state() ? ( - (state()?.phases ?? []).length > 0 ? ( - - - {(phase, index) => ( - = 0 && - index < currentPhaseIndex() - ? theme().success - : theme().textMuted - } - > - {phase() === state()?.phase - ? `${SPINNER[spinnerFrame()]} ` - : currentPhaseIndex() >= 0 && - index < currentPhaseIndex() - ? '● ' - : '○ '} - {phase()} - - )} - - - ) : ( - // Phases unknown — show workflowName phaseName - - {state()?.workflow} - - {' '} - {state()?.phase} - - - ) - ) : null} - {/* No active workflow message */} - {!state() ? ( - No Active Workflow - ) : null} - + ) : null} + {/* No active workflow message */} + {!state() ? ( + No Active Workflow + ) : null} + + ) ); }, }, diff --git a/tui.json b/tui.json index 91c1ee02..6efa7835 100644 --- a/tui.json +++ b/tui.json @@ -1,4 +1,6 @@ { "$schema": "https://opencode.ai/tui.json", - "plugin": ["@codemcp/workflows-opencode-tui"] + "plugin": [ + "/Users/oliverjaegle/projects/privat/mcp-server/responsible-vibe/packages/opencode-tui-plugin" + ] }