diff --git a/.ade/skills/adr-nygard/SKILL.md b/.ade/skills/adr-nygard/SKILL.md new file mode 100644 index 0000000..5575630 --- /dev/null +++ b/.ade/skills/adr-nygard/SKILL.md @@ -0,0 +1,45 @@ +--- +name: adr-nygard +description: Architecture Decision Records following Nygard's lightweight template +--- + +# Architecture Decision Records (Nygard) + +## When to Write an ADR + +- When making a significant architectural decision +- When choosing between multiple viable options +- When the decision will be hard to reverse +- When future developers will ask "why did we do this?" + +## Template + +Store ADRs in `docs/adr/` as numbered markdown files: `NNNN-title-with-dashes.md` + +```markdown +# N. Title + +## Status + +Proposed | Accepted | Deprecated | Superseded by [ADR-NNNN] + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Consequences + +What becomes easier or more difficult to do because of this change? +``` + +## Rules + +- ADRs are immutable once accepted — supersede, don't edit +- Keep context focused on forces at play at the time of the decision +- Write consequences as both positive and negative impacts +- Number sequentially, never reuse numbers +- Title should be a short noun phrase (e.g. "Use PostgreSQL for persistence") diff --git a/.ade/skills/conventional-commits/SKILL.md b/.ade/skills/conventional-commits/SKILL.md new file mode 100644 index 0000000..7a9a63f --- /dev/null +++ b/.ade/skills/conventional-commits/SKILL.md @@ -0,0 +1,36 @@ +--- +name: conventional-commits +description: Conventional Commits specification for structured commit messages +--- + +# Conventional Commits + +## Format + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +## Types + +- `feat`: A new feature (correlates with MINOR in SemVer) +- `fix`: A bug fix (correlates with PATCH in SemVer) +- `docs`: Documentation only changes +- `style`: Changes that do not affect the meaning of the code +- `refactor`: A code change that neither fixes a bug nor adds a feature +- `perf`: A code change that improves performance +- `test`: Adding missing tests or correcting existing tests +- `chore`: Changes to the build process or auxiliary tools + +## Rules + +- Subject line must not exceed 72 characters +- Use imperative mood in the subject line ("add" not "added") +- Do not end the subject line with a period +- Separate subject from body with a blank line +- Use the body to explain what and why, not how +- `BREAKING CHANGE:` footer or `!` after type/scope for breaking changes diff --git a/.ade/skills/tanstack-architecture/SKILL.md b/.ade/skills/tanstack-architecture/SKILL.md new file mode 100644 index 0000000..c2cac86 --- /dev/null +++ b/.ade/skills/tanstack-architecture/SKILL.md @@ -0,0 +1,25 @@ +--- +name: tanstack-architecture +description: Architecture conventions for TanStack applications +--- + +# TanStack Architecture Conventions + +## Project Structure + +- Use file-based routing with TanStack Router (`routes/` directory) +- Colocate route components with their loaders and actions +- Organize by feature, not by type (e.g. `features/auth/`, not `components/auth/`) + +## Data Flow + +- Use TanStack Query for all server state management +- Use TanStack Router loaders for route-level data requirements +- Keep client state minimal — prefer server state via Query +- Use `queryOptions()` factory pattern for reusable query definitions + +## Module Boundaries + +- Each feature exports a public API via `index.ts` +- Features must not import from other features' internals +- Shared code goes in `lib/` or `shared/` diff --git a/.ade/skills/tanstack-code/SKILL.md b/.ade/skills/tanstack-code/SKILL.md new file mode 100644 index 0000000..77f3075 --- /dev/null +++ b/.ade/skills/tanstack-code/SKILL.md @@ -0,0 +1,25 @@ +--- +name: tanstack-code +description: Code style conventions for TanStack applications +--- + +# TanStack Code Conventions + +## TypeScript + +- Enable strict mode in tsconfig +- Infer types from TanStack APIs rather than writing manual type annotations +- Use `satisfies` operator for type-safe object literals + +## Naming + +- Query keys: `['entity', ...params]` (e.g. `['user', userId]`) +- Query option factories: `entityQueryOptions` (e.g. `userQueryOptions`) +- Route files: `$param` for dynamic segments (e.g. `users/$userId.tsx`) +- Loaders: export as named `loader` from route file + +## Imports + +- Import from `@tanstack/react-query`, `@tanstack/react-router`, etc. +- Never import internal modules from TanStack packages +- Use path aliases for project imports (`@/features/...`) diff --git a/.ade/skills/tanstack-design/SKILL.md b/.ade/skills/tanstack-design/SKILL.md new file mode 100644 index 0000000..2cdd9cf --- /dev/null +++ b/.ade/skills/tanstack-design/SKILL.md @@ -0,0 +1,24 @@ +--- +name: tanstack-design +description: Design patterns for TanStack applications +--- + +# TanStack Design Patterns + +## Query Patterns + +- Define query options as standalone functions: `export const userQueryOptions = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetchUser(id) })` +- Use `useSuspenseQuery` in route components paired with `loader` for prefetching +- Use `useMutation` with `onSettled` for cache invalidation + +## Router Patterns + +- Define routes using `createFileRoute` for type-safe file-based routing +- Use `beforeLoad` for auth guards and redirects +- Use search params validation with `zodSearchValidator` for type-safe URL state + +## Form Patterns + +- Use TanStack Form with Zod validators for form state and validation +- Prefer field-level validation over form-level where possible +- Connect form submission to `useMutation` for server sync diff --git a/.ade/skills/tanstack-testing/SKILL.md b/.ade/skills/tanstack-testing/SKILL.md new file mode 100644 index 0000000..774ef49 --- /dev/null +++ b/.ade/skills/tanstack-testing/SKILL.md @@ -0,0 +1,24 @@ +--- +name: tanstack-testing +description: Testing conventions for TanStack applications +--- + +# TanStack Testing Conventions + +## Query Testing + +- Wrap components in `QueryClientProvider` with a fresh `QueryClient` per test +- Use `@testing-library/react` with `renderHook` for testing custom query hooks +- Mock at the network level with MSW, not at the query level + +## Router Testing + +- Use `createMemoryHistory` and `createRouter` for route testing +- Test route loaders independently as plain async functions +- Test search param validation with unit tests on the validator schema + +## Integration Tests + +- Test full user flows through route transitions +- Assert on visible UI state, not internal query cache state +- Use `waitFor` for async query resolution in component tests diff --git a/.agentskills/skills/adr-nygard/SKILL.md b/.agentskills/skills/adr-nygard/SKILL.md new file mode 100644 index 0000000..5575630 --- /dev/null +++ b/.agentskills/skills/adr-nygard/SKILL.md @@ -0,0 +1,45 @@ +--- +name: adr-nygard +description: Architecture Decision Records following Nygard's lightweight template +--- + +# Architecture Decision Records (Nygard) + +## When to Write an ADR + +- When making a significant architectural decision +- When choosing between multiple viable options +- When the decision will be hard to reverse +- When future developers will ask "why did we do this?" + +## Template + +Store ADRs in `docs/adr/` as numbered markdown files: `NNNN-title-with-dashes.md` + +```markdown +# N. Title + +## Status + +Proposed | Accepted | Deprecated | Superseded by [ADR-NNNN] + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Consequences + +What becomes easier or more difficult to do because of this change? +``` + +## Rules + +- ADRs are immutable once accepted — supersede, don't edit +- Keep context focused on forces at play at the time of the decision +- Write consequences as both positive and negative impacts +- Number sequentially, never reuse numbers +- Title should be a short noun phrase (e.g. "Use PostgreSQL for persistence") diff --git a/.agentskills/skills/commit/SKILL.md b/.agentskills/skills/commit/SKILL.md new file mode 100644 index 0000000..ec0974f --- /dev/null +++ b/.agentskills/skills/commit/SKILL.md @@ -0,0 +1,20 @@ +--- +name: commit +description: "Always apply this skill when committing to git" +--- + +Create a conventional commit message with the following body: + +```markdown +## Intent + + + +## Key changes + + + +## Dependencies and side effects + + +``` diff --git a/.agentskills/skills/tdd/SKILL.md b/.agentskills/skills/tdd/SKILL.md new file mode 100644 index 0000000..6b5e17d --- /dev/null +++ b/.agentskills/skills/tdd/SKILL.md @@ -0,0 +1,10 @@ +--- +name: tdd +description: "Apply this when developing new features that add to or change the business logic of the system" +--- + +Apply TDD with agents: + +1. Ask an agent to write a failing test (RED phase). The agent shall commit this as WIP. +2. Ask an agent to write the actual code to make the test pass (GREEN phase). The agent shall commit this as WIP. +3. Ask an agent to judge the previous two commits: The GREEN phase commit must not have changed semantics of tests implemented in the RED phase. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000..fd38f2a --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,85 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --status in_progress +bd update --status done + +# Sync with git remote +bd sync +``` + +### Working with Issues + +Issues in Beads are: + +- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +✨ **AI-Native Design** + +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** + +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** + +- Automatic sync with git commits +- Branch-aware issue tracking +- Intelligent JSONL merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +_Beads: Issue tracking that moves at the speed of thought_ ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..a8be24e --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,63 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +issue-prefix: "ade" + +# Use no-db mode: load from JSONL, no SQLite, write back after each command +# When true, bd will use .beads/issues.jsonl as the source of truth +# instead of SQLite database +no-db: true # JSONL-only mode, no SQLite database + + +# Disable daemon for RPC communication (forces direct database access) +# no-daemon: false + +# Disable auto-flush of database to JSONL after mutations +# no-auto-flush: false + +# Disable auto-import from JSONL when it's newer than database +# no-auto-import: false + +# Enable JSON output by default +# json: false + +# Default actor for audit trails (overridden by BD_ACTOR or --actor) +# actor: "" + +# Path to database (overridden by BEADS_DB or --db) +# db: "" + +# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON) +# auto-start-daemon: true + +# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE) +# flush-debounce: "5s" + +# Git branch for beads commits (bd sync will commit to this branch) +# IMPORTANT: Set this for team projects so all clones use the same sync branch. +# This setting persists across clones (unlike database config which is gitignored). +# Can also use BEADS_SYNC_BRANCH env var for local override. +# If not set, bd sync will require you to run 'bd config set sync.branch '. +# sync-branch: "beads-sync" + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct JSONL +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..43303ed --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,46 @@ +{"id":"ade-1","title":"ade: bugfix (development-plan-fix-no-arch-selected.md)","description":"Responsible vibe engineering session using bugfix workflow for ade","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:27.144211+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:27.144211+01:00"} +{"id":"ade-1.1","title":"Reproduce","description":"Gather specific information to reliably reproduce the reported bug: - What are the exact OS, browser/runtime versions, and hardware specs? - What is the precise sequence of actions that trigger the bug? - What error messages, logs, or stack traces are available? - Does this happen every time or intermittently? - How many users are affected and what is the business impact? Create test cases that demonstrate the problem. Document your findings and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:27.332884+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:27.332884+01:00","dependencies":[{"issue_id":"ade-1.1","depends_on_id":"ade-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.1.1","title":"Reproduce bug: Run CLI setup and select skip for architecture, verify if takstack skills/docsets are added","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:29:57.811413+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:32:07.755014+01:00","closed_at":"2026-03-18T08:32:07.755014+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-1.1.1","depends_on_id":"ade-1.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.1.2","title":"Investigate: Check if there are other scenarios where TanStack skills/docsets might be incorrectly added","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:32:12.308229+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:33:51.388698+01:00","closed_at":"2026-03-18T08:33:51.388698+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-1.1.2","depends_on_id":"ade-1.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.2","title":"Analyze","description":"Examine the code paths involved in the bug, identify the root cause, and understand why the issue occurs. Use debugging tools, add logging, and trace through the problematic code. Document your analysis and create tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:27.524127+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:27.524127+01:00","dependencies":[{"issue_id":"ade-1.2","depends_on_id":"ade-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-1.2","depends_on_id":"ade-1.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.3","title":"Fix","description":"Implement the solution based on your analysis: - If exists: Follow the design from it - Otherwise: Elaborate design options and present them to the user Before implementing, assess the approach: - How critical is this system? What is the blast radius if the fix causes issues? - Should this be a minimal fix or a more comprehensive solution? Make targeted changes that address the root cause without introducing new issues. Be careful to maintain existing functionality while fixing the bug.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:27.697356+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:27.697356+01:00","dependencies":[{"issue_id":"ade-1.3","depends_on_id":"ade-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-1.3","depends_on_id":"ade-1.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.4","title":"Verify","description":"Test the fix thoroughly to ensure the original bug is resolved and no new issues were introduced. Run existing tests, create new ones if needed, and verify the solution is robust.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:27.865128+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:27.865128+01:00","dependencies":[{"issue_id":"ade-1.4","depends_on_id":"ade-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-1.4","depends_on_id":"ade-1.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-1.5","title":"Finalize","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: - Remove all temporary debug output statements used during bug investigation (console logging, print statements, debug output functions) - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs and convert remaining ones to proper issue tracking if needed - Remove temporary debugging code, test code blocks, and commented-out code - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect the bug fix: - If exists, update it if design details were refined or changed during the fix - Compare documentation against the actual bug fix implementation - Update only the documentation sections that have functional changes - Remove references to investigation iterations, progress notes, and temporary decisions - Ensure documentation describes the final fixed state, not the debugging process - Ask the 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 bug fix is ready for production - Update task progress and mark completed work as you finalize the bug fix","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:27:28.031667+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:27:28.031667+01:00","dependencies":[{"issue_id":"ade-1.5","depends_on_id":"ade-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-1.5","depends_on_id":"ade-1.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2","title":"ade: epcc (development-plan-autonomy-facet.md)","description":"Responsible vibe engineering session using epcc workflow for ade","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:42:24.649306+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:24.649306+01:00"} +{"id":"ade-2.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-03-18T08:42:24.818903+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:24.818903+01:00","dependencies":[{"issue_id":"ade-2.1","depends_on_id":"ade-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.1","title":"Review existing behavior-oriented facets and catalog tests","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:46:07.923572+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:46:37.327782+01:00","closed_at":"2026-03-18T08:46:37.327782+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.1","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.2","title":"Define autonomy facet semantics and option structure","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:46:08.078116+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:51:49.76706+01:00","closed_at":"2026-03-18T08:51:49.76706+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.2","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.3","title":"Capture requirements and open questions for autonomy facet","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:46:08.214156+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:51:49.519989+01:00","closed_at":"2026-03-18T08:51:49.519989+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.3","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.4","title":"Trace harness permission model and MCP allowedTools integration","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:50:45.070267+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:51:49.634463+01:00","closed_at":"2026-03-18T08:51:49.634463+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.4","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.5","title":"Audit built-in and MCP permission controls for every harness writer","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:53:02.341058+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:54:27.952202+01:00","closed_at":"2026-03-18T08:54:27.952202+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.5","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.1.6","title":"Research official online permission and MCP docs for supported harnesses","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T08:57:45.709818+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:01:38.833489+01:00","closed_at":"2026-03-18T09:01:38.833489+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.1.6","depends_on_id":"ade-2.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.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-03-18T08:42:24.985109+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:24.985109+01:00","dependencies":[{"issue_id":"ade-2.2","depends_on_id":"ade-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2","depends_on_id":"ade-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.2.1","title":"Define shared autonomy policy model in core","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:14.478022+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:30.361315+01:00","closed_at":"2026-03-18T09:47:30.361315+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.2.1","depends_on_id":"ade-2.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.2.2","title":"Define per-harness permission mapping and conservative fallback behavior","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:14.654561+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:30.574129+01:00","closed_at":"2026-03-18T09:47:30.574129+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.2.2","depends_on_id":"ade-2.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.2","depends_on_id":"ade-2.2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.2.3","title":"Define autonomy facet options and writer contract","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:14.901262+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:30.719054+01:00","closed_at":"2026-03-18T09:47:30.719054+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.2.3","depends_on_id":"ade-2.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.3","depends_on_id":"ade-2.2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.3","depends_on_id":"ade-2.2.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.2.4","title":"Define test strategy for autonomy policy and harness mappings","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:15.080583+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:30.843779+01:00","closed_at":"2026-03-18T09:47:30.843779+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.2.4","depends_on_id":"ade-2.2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.4","depends_on_id":"ade-2.2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.4","depends_on_id":"ade-2.2.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.2.4","depends_on_id":"ade-2.2.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.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-03-18T08:42:25.149388+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:25.149388+01:00","dependencies":[{"issue_id":"ade-2.3","depends_on_id":"ade-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3","depends_on_id":"ade-2.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.1","title":"Implement shared autonomy policy types and resolver plumbing","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:15.273279+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:30.972666+01:00","closed_at":"2026-03-18T09:47:30.972666+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.1","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.1","depends_on_id":"ade-2.2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.1","depends_on_id":"ade-2.2.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.10","title":"Implement Claude Code harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:30.093088+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:29:21.191758+01:00","closed_at":"2026-03-18T11:29:21.191758+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.10","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.11","title":"Implement Copilot harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:30.289139+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:29:21.332749+01:00","closed_at":"2026-03-18T11:29:21.332749+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.11","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.12","title":"Implement OpenCode harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:30.49347+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:29:21.453832+01:00","closed_at":"2026-03-18T11:29:21.453832+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.12","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.13","title":"Implement Kiro harness mapping rewrite","status":"in_progress","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:30.663057+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:19:36.904021+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.13","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.14","title":"Implement Cline harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:30.881533+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:54:20.043232+01:00","closed_at":"2026-03-18T11:52:25.541203+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.14","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.15","title":"Implement Roo Code harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:31.073134+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:55:16.477017+01:00","closed_at":"2026-03-18T11:55:16.477017+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.15","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.16","title":"Implement Windsurf harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:31.2691+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:54:20.170992+01:00","closed_at":"2026-03-18T11:52:24.228288+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.16","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.17","title":"Implement Cursor harness mapping rewrite","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:31.474655+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:54:20.285805+01:00","closed_at":"2026-03-18T11:54:20.285805+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.17","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.18","title":"Document Universal harness limitations and autonomy behavior","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:31.649402+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:54:20.399842+01:00","closed_at":"2026-03-18T11:54:20.399842+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.18","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.19","title":"Document and enforce Universal autonomy limitation as instructions-only/no-op, with explicit tests","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:49:09.069988+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:52:20.8215+01:00","closed_at":"2026-03-18T11:52:20.8215+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.19","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.2","title":"Add autonomy facet and register it in the default catalog","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:15.458256+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:31.093414+01:00","closed_at":"2026-03-18T09:47:31.093414+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.2","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.2","depends_on_id":"ade-2.2.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.20","title":"Rewrite Roo Code autonomy mapping to capability-based mode config and separate MCP forwarding","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:49:27.176427+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:54:06.695191+01:00","closed_at":"2026-03-18T11:54:06.695191+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.20","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.21","title":"Fix Kiro agent discovery format","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T12:04:25.285287+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T12:05:22.626413+01:00","closed_at":"2026-03-18T12:05:22.626413+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.21","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.22","title":"Fix Kiro MCP exposure and Copilot MCP tool path format","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T12:26:00.150821+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T12:27:49.728151+01:00","closed_at":"2026-03-18T12:27:49.728151+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.22","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.23","title":"Fix Copilot MCP approval propagation","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T12:31:09.287703+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T12:34:04.967355+01:00","closed_at":"2026-03-18T12:34:04.967355+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.23","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.3","title":"Translate autonomy policy into harness-specific permission configs","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:15.660399+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:31.227711+01:00","closed_at":"2026-03-18T09:47:31.227711+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.3","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.3","depends_on_id":"ade-2.2.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.3","depends_on_id":"ade-2.3.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.4","title":"Extend catalog and harness tests for autonomy behavior","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:03:15.851771+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:47:31.349326+01:00","closed_at":"2026-03-18T09:47:31.349326+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.4","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.4","depends_on_id":"ade-2.3.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.4","depends_on_id":"ade-2.3.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.3.4","depends_on_id":"ade-2.3.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.5","title":"Add RED tests for autonomy facet and shared permission policy","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T09:11:13.472511+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T09:34:41.499038+01:00","closed_at":"2026-03-18T09:34:41.499038+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.5","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.6","title":"Fix OpenCode autonomy mapping and align sensible-defaults with sample permissions","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T10:00:41.881961+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T10:03:27.960435+01:00","closed_at":"2026-03-18T10:03:27.960435+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.6","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.7","title":"Research per-harness tool names and agent-level permission configuration surfaces","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T10:26:21.267422+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:55:16.592564+01:00","closed_at":"2026-03-18T11:53:13.403129+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.7","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.8","title":"Replan autonomy model per harness around built-in permissions only","status":"in_progress","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:01:01.074975+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:02:11.748853+01:00","dependencies":[{"issue_id":"ade-2.3.8","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.3.9","title":"Implement core autonomy abstraction rewrite for built-in permissions only","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-18T11:15:29.905083+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T11:49:04.856825+01:00","closed_at":"2026-03-18T11:49:04.856825+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-2.3.9","depends_on_id":"ade-2.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-2.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-03-18T08:42:25.322426+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T08:42:25.322426+01:00","dependencies":[{"issue_id":"ade-2.4","depends_on_id":"ade-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-2.4","depends_on_id":"ade-2.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched new file mode 100644 index 0000000..55f4700 --- /dev/null +++ b/.beads/last-touched @@ -0,0 +1 @@ +ade-2.3.23 diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..f581edc --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..85896b3 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "workflows": { + "command": "npx", + "args": ["@codemcp/workflows-server@latest"] + }, + "@codemcp/knowledge-server": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + }, + "agentskills": { + "command": "npx", + "args": ["-y", "@codemcp/skills-server"] + } + } +} diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..85896b3 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "workflows": { + "command": "npx", + "args": ["@codemcp/workflows-server@latest"] + }, + "@codemcp/knowledge-server": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + }, + "agentskills": { + "command": "npx", + "args": ["-y", "@codemcp/skills-server"] + } + } +} diff --git a/.cursor/rules/ade.mdc b/.cursor/rules/ade.mdc new file mode 100644 index 0000000..5ec51dc --- /dev/null +++ b/.cursor/rules/ade.mdc @@ -0,0 +1,10 @@ +--- +description: ADE project conventions +globs: * +--- + +You are an AI assistant that helps users develop software features using the workflows server. +IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow. +Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them. +Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan. +Do not use your own task management tools. diff --git a/.github/agents/ade.agent.md b/.github/agents/ade.agent.md new file mode 100644 index 0000000..144fddf --- /dev/null +++ b/.github/agents/ade.agent.md @@ -0,0 +1,28 @@ +--- +name: ade +description: ADE — Agentic Development Environment agent with project conventions and tools +tools: + - read + - edit + - search + - agent + - workflows/* + - agentskills/* +mcp-servers: + workflows: + type: stdio + command: "npx" + args: ["@codemcp/workflows-server@latest"] + tools: ["*"] + agentskills: + type: stdio + command: "npx" + args: ["-y", "@codemcp/skills-server"] + tools: ["*"] +--- + +You are an AI assistant that helps users develop software features using the workflows server. +IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow. +Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them. +Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan. +Do not use your own task management tools. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f93bc51 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,11 @@ +You are an AI assistant that helps users develop software features using the workflows server. +IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow. +Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them. +Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan. +Do not use your own task management tools. + +This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes. + +Use the conventional-commits skill (via use_skill()) when writing commit messages. + +This project uses Architecture Decision Records. Use the adr-nygard skill (via use_skill()) when making or documenting architectural decisions. Store ADRs in docs/adr/. diff --git a/.kiro/agents/ade.json b/.kiro/agents/ade.json new file mode 100644 index 0000000..f3f870d --- /dev/null +++ b/.kiro/agents/ade.json @@ -0,0 +1,20 @@ +{ + "name": "ade", + "description": "ADE — Agentic Development Environment agent with project conventions and tools.", + "prompt": "You are an AI assistant that helps users develop software features using the workflows server.\nIMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow.\nEach tool call returns a JSON response with an \"instructions\" field. Follow these instructions immediately after you receive them.\nUse the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan.\nDo not use your own task management tools.", + "mcpServers": { + "workflows": { + "command": "npx", + "args": ["@codemcp/workflows-server@latest"], + "autoApprove": ["*"] + }, + "agentskills": { + "command": "npx", + "args": ["-y", "@codemcp/skills-server"], + "autoApprove": ["*"] + } + }, + "tools": ["read", "write", "spec", "@workflows/*", "@agentskills/*"], + "allowedTools": ["read", "write", "spec", "@workflows/*", "@agentskills/*"], + "useLegacyMcpJson": true +} diff --git a/.kiro/settings/mcp.json b/.kiro/settings/mcp.json new file mode 100644 index 0000000..eaf31ce --- /dev/null +++ b/.kiro/settings/mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "workflows": { + "command": "npx", + "args": ["@codemcp/workflows-server@latest"], + "autoApprove": ["*"] + }, + "agentskills": { + "command": "npx", + "args": ["-y", "@codemcp/skills-server"], + "autoApprove": ["*"] + } + } +} diff --git a/.knowledge/.gitignore b/.knowledge/.gitignore new file mode 100644 index 0000000..62723e5 --- /dev/null +++ b/.knowledge/.gitignore @@ -0,0 +1,3 @@ +# Agentic Knowledge - Downloaded docsets +docsets/ +docsets diff --git a/.knowledge/.prettierignore b/.knowledge/.prettierignore new file mode 100644 index 0000000..40f4914 --- /dev/null +++ b/.knowledge/.prettierignore @@ -0,0 +1 @@ +docsets diff --git a/.knowledge/config.yaml b/.knowledge/config.yaml new file mode 100644 index 0000000..1e7aa0e --- /dev/null +++ b/.knowledge/config.yaml @@ -0,0 +1,9 @@ +version: "1.0" +docsets: + - id: conventional-commits-spec + name: The Conventional Commits specification + description: "Git repository: https://github.com/conventional-commits/conventionalcommits.org.git" + sources: + - url: https://github.com/conventional-commits/conventionalcommits.org.git + type: git_repo + branch: main diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..abfd20f --- /dev/null +++ b/.mcp.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "workflows": { + "command": "npx", + "args": ["@codemcp/workflows-server@latest"] + }, + "@codemcp/knowledge-server": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + }, + "agentskills": { + "command": "npx", + "args": ["-y", "@codemcp/skills-server"] + }, + "knowledge": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + } + } +} diff --git a/.opencode/agents/ade.md b/.opencode/agents/ade.md new file mode 100644 index 0000000..8a9d0a3 --- /dev/null +++ b/.opencode/agents/ade.md @@ -0,0 +1,118 @@ +--- +name: ade +description: ADE — Agentic Development Environment agent with project conventions and tools +permission: + read: + "*": "allow" + "*.env": "deny" + "*.env.*": "deny" + "*.env.example": "allow" + edit: "allow" + glob: "allow" + grep: "allow" + list: "allow" + lsp: "allow" + task: "allow" + todoread: "deny" + todowrite: "deny" + skill: "deny" + webfetch: "ask" + websearch: "ask" + codesearch: "ask" + bash: + "*": "deny" + "grep *": "allow" + "rg *": "allow" + "find *": "allow" + "fd *": "allow" + ls: "allow" + "ls *": "allow" + "cat *": "allow" + "head *": "allow" + "tail *": "allow" + "wc *": "allow" + "sort *": "allow" + "uniq *": "allow" + "diff *": "allow" + "echo *": "allow" + "printf *": "allow" + pwd: "allow" + "which *": "allow" + "type *": "allow" + whoami: "allow" + date: "allow" + "date *": "allow" + env: "allow" + "tree *": "allow" + "file *": "allow" + "stat *": "allow" + "readlink *": "allow" + "realpath *": "allow" + "dirname *": "allow" + "basename *": "allow" + "sed *": "allow" + "awk *": "allow" + "cut *": "allow" + "tr *": "allow" + "tee *": "allow" + "xargs *": "allow" + "jq *": "allow" + "yq *": "allow" + "mkdir *": "allow" + "touch *": "allow" + "cp *": "ask" + "mv *": "ask" + "ln *": "ask" + "npm *": "ask" + "node *": "ask" + "pip *": "ask" + "python *": "ask" + "python3 *": "ask" + "rm *": "deny" + "rmdir *": "deny" + "curl *": "deny" + "wget *": "deny" + "chmod *": "deny" + "chown *": "deny" + "sudo *": "deny" + "su *": "deny" + "sh *": "deny" + "bash *": "deny" + "zsh *": "deny" + "eval *": "deny" + "exec *": "deny" + "source *": "deny" + ". *": "deny" + "nohup *": "deny" + "dd *": "deny" + "mkfs *": "deny" + "mount *": "deny" + "umount *": "deny" + "kill *": "deny" + "killall *": "deny" + "pkill *": "deny" + "nc *": "deny" + "ncat *": "deny" + "ssh *": "deny" + "scp *": "deny" + "rsync *": "deny" + "docker *": "deny" + "kubectl *": "deny" + "systemctl *": "deny" + "service *": "deny" + "crontab *": "deny" + reboot: "deny" + "shutdown *": "deny" + "passwd *": "deny" + "useradd *": "deny" + "userdel *": "deny" + "iptables *": "deny" + external_directory: "deny" + doom_loop: "deny" +--- + +You are an AI assistant that helps users develop software features using the workflows server. +IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow. +Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them. +Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan. +Do not use your own task management tools. diff --git a/.prettierignore b/.prettierignore index 1521c8b..b121864 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ dist +.knowledge/docsets diff --git a/.vibe/.gitignore b/.vibe/.gitignore new file mode 100644 index 0000000..2e22035 --- /dev/null +++ b/.vibe/.gitignore @@ -0,0 +1,5 @@ +# Exclude conversation state files +conversations/ +# Legacy SQLite files (for migration compatibility) +*.sqlite +*.sqlite-* diff --git a/.vibe/beads-state-ade-autonomy-facet-46zodk.json b/.vibe/beads-state-ade-autonomy-facet-46zodk.json new file mode 100644 index 0000000..1bfa66d --- /dev/null +++ b/.vibe/beads-state-ade-autonomy-facet-46zodk.json @@ -0,0 +1,29 @@ +{ + "conversationId": "ade-autonomy-facet-46zodk", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/ade", + "epicId": "ade-2", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "ade-2.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "ade-2.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "ade-2.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "ade-2.4" + } + ], + "createdAt": "2026-03-18T07:42:25.681Z", + "updatedAt": "2026-03-18T07:42:25.681Z" +} diff --git a/.vibe/beads-state-ade-fix-no-arch-selected-hvfiio.json b/.vibe/beads-state-ade-fix-no-arch-selected-hvfiio.json new file mode 100644 index 0000000..b0123fe --- /dev/null +++ b/.vibe/beads-state-ade-fix-no-arch-selected-hvfiio.json @@ -0,0 +1,34 @@ +{ + "conversationId": "ade-fix-no-arch-selected-hvfiio", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/ade", + "epicId": "ade-1", + "phaseTasks": [ + { + "phaseId": "reproduce", + "phaseName": "Reproduce", + "taskId": "ade-1.1" + }, + { + "phaseId": "analyze", + "phaseName": "Analyze", + "taskId": "ade-1.2" + }, + { + "phaseId": "fix", + "phaseName": "Fix", + "taskId": "ade-1.3" + }, + { + "phaseId": "verify", + "phaseName": "Verify", + "taskId": "ade-1.4" + }, + { + "phaseId": "finalize", + "phaseName": "Finalize", + "taskId": "ade-1.5" + } + ], + "createdAt": "2026-03-18T07:27:28.580Z", + "updatedAt": "2026-03-18T07:27:28.580Z" +} diff --git a/.vibe/development-plan-autonomy-facet.md b/.vibe/development-plan-autonomy-facet.md new file mode 100644 index 0000000..a1d02b0 --- /dev/null +++ b/.vibe/development-plan-autonomy-facet.md @@ -0,0 +1,214 @@ +# Development Plan: ade (autonomy-facet branch) + +_Generated on 2026-03-18 by Vibe Feature MCP_ +_Workflow: [epcc](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/epcc)_ + +## Goal + +Add a new `autonomy` catalog facet that models how much initiative and execution freedom the agent should have, and register it consistently with the existing ADE facet system. + +## Explore + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +### Findings + +- Existing facets live in `packages/core/src/catalog/facets/` and are registered in `packages/core/src/catalog/index.ts`. +- Facets can be single-select or multi-select and may depend on `architecture` when options vary by stack. +- Catalog behavior is covered by `packages/core/src/catalog/catalog.spec.ts`. + +## Plan + + + +### Phase Entrance Criteria + +- [ ] The current catalog structure and facet conventions have been reviewed. +- [ ] The target role of the new `autonomy` facet is clear enough to choose `required`, `multiSelect`, and any `dependsOn` settings. +- [ ] The files and tests affected by the change have been identified. + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Code + + + +### Phase Entrance Criteria + +- [ ] The `autonomy` facet design is defined, including its option ids, labels, descriptions, and recipes. +- [ ] The registration changes in `catalog/index.ts` are identified. +- [ ] The necessary validation updates in `catalog.spec.ts` are identified. + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Commit + + + +### Phase Entrance Criteria + +- [ ] The new facet implementation is complete and wired into the default catalog. +- [ ] Relevant tests have been run and pass, or failures are understood and documented. +- [ ] The final user-facing impact of the new facet is summarized. + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Key Decisions + +_Important decisions will be documented here as they are made_ + +- Treat `autonomy` as a first-class facet rather than overloading `process` or `practices`, so agent initiative is configurable independently. +- Reuse existing provision writers for the first cut, with `instruction` as the most likely fit unless the user wants autonomy tied to skills or workflows. +- The clarified scope is to influence harness-writer permission output, not just instructions: built-in tools and MCP tools need a shared autonomy policy that each harness writer can translate. +- The clarified scope is to influence harness-writer permission output for built-in/basic operations only; MCP tool permissions will continue to flow from MCP provisioning. +- The autonomy modes should support three levels: rigid (ask), sensible defaults, and max autonomy (full allow, with sandbox-oriented guidance). +- Web access should remain on `ask` for all harnesses, even when broader autonomy settings are enabled. +- Recommended design: introduce a shared autonomy permission model in core and have each harness writer translate that model into its own config shape. +- Recommended design: introduce a shared autonomy permission model in core for built-in/basic operations and have each harness writer translate that model into its own config shape, while forwarding MCP tool permissions unchanged from MCP provisioning. +- When a harness cannot express the full shared policy, ADE should degrade conservatively rather than silently broadening permissions. +- The revised target policy shape should focus on built-in/basic operations rather than trying to re-model harness-specific MCP permissions. +- MCP permissions are explicitly out of scope for autonomy and must be forwarded from MCP provisioning rather than re-modeled in `permission_policy`. +- The next implementation pass should use an abstract capability model for built-in/basic operations and default unknown/unsupported capabilities to `ask`. +- Implement `autonomy` via a dedicated `permission-policy` provision writer so the catalog remains declarative while the resolver still has a built-in fallback for that writer in narrow test registries. +- Remove `PermissionPolicy.mcp` from the shared type so core autonomy models built-in/basic capabilities only; harnesses must forward MCP approvals from provisioning data instead. +- Implemented the core-facing autonomy contract as `permission_policy.capabilities`, using the abstract capability keys `read`, `edit_write`, `search_list`, `bash_safe`, `bash_unsafe`, `web`, and `task_agent`. +- The selected core profile mappings are: `rigid` = ask for every capability, `sensible-defaults` = allow `read`/`edit_write`/`search_list`/`bash_safe`/`task_agent` while keeping `bash_unsafe` and `web` on `ask`, and `max-autonomy` = allow everything except `web`, which remains `ask`. +- Removed MCP ownership from the shared autonomy model in core; harnesses must now derive MCP approvals exclusively from provisioned MCP server entries and their `allowedTools`. +- Claude Code should translate autonomy locally in its writer: only official built-in rule names go into `.claude/settings.json`, and only explicitly provisioned MCP tools are forwarded as `mcp__server__tool`. +- Kiro should be rewritten to use `.kiro/agents/ade.json` with documented built-in selectors (`read`, `write`, `shell`, `spec`) while forwarding MCP trust from provisioning into `.kiro/settings/mcp.json` via `autoApprove`; web stays omitted so approval remains required. +- Kiro custom agents are discovered from JSON files in `.kiro/agents/`, not Markdown files; the generated ADE agent must therefore be `.kiro/agents/ade.json` with `name`, `description`, `prompt`, and `tools` so `kiro-cli chat --agent ade` can resolve it. +- Copilot should translate autonomy only through documented `tools:` aliases, omit any capability that would still require approval (especially `web`), and forward MCP approvals exactly as `server//*` or `server//` based on provisioning. +- Cline should use the verified project-local MCP settings file name `cline_mcp_settings.json` instead of `.cline/mcp.json`, and autonomy must not invent agent-local permission semantics when only settings-level controls are documented. +- Windsurf should keep forwarding project-local MCP registration and `allowedTools` approvals through `.windsurf/mcp.json`, but built-in autonomy must degrade to clearly labeled advisory text in `.windsurfrules` until a verified committed Windsurf permission schema exists. +- Cursor should keep `.cursor/mcp.json` limited to documented MCP server registration and render autonomy only as an explicit non-enforcing note in `.cursor/rules/ade.mdc`, because no verified committed project-local built-in permission schema was found. + +## Notes + +_Additional context and observations_ + +- `backpressure` is the closest existing pattern for a behavior-oriented facet. +- The product docs currently frame “runtime agent behavior” as a non-goal, so the autonomy facet should likely influence generated instructions rather than introduce runtime control mechanisms. +- No existing `autonomy` concept appears anywhere in the repository yet, so option names and exact behaviors still need to be defined. +- Current permission handling is fragmented: + - `McpServerEntry.allowedTools` exists for per-server MCP permissions. + - `claude-code` maps MCP permissions into `.claude/settings.json`. + - `cline`, `roo-code`, and `windsurf` map MCP permissions into `alwaysAllow`. + - `kiro` writes both `tools` and `allowedTools`, but only MCP tools are derived from `allowedTools`. + - `copilot` writes a `tools:` allowlist in agent frontmatter for built-in tools plus `server/*`. + - `opencode` writes built-in tool approval defaults in agent frontmatter, but currently does not derive MCP permissions from `allowedTools`. + - `cursor` and `universal` currently emit no explicit permission policy. +- This implies a new shared policy object is likely needed in `LogicalConfig`, with harness-specific translation and partial support where a harness cannot express the full policy. +- The user explicitly wants network/web access to stay approval-gated across the board; this constraint should shape the sensible-defaults and max-autonomy mappings for each harness. +- Design options considered: + - Option A: encode autonomy directly inside each harness writer with no shared core model. Rejected because it duplicates policy logic and makes the catalog option semantics drift by harness. + - Option B: add a shared permission policy to `LogicalConfig`, populate it from the new `autonomy` facet, and let harness writers render the nearest supported representation. Recommended because it keeps the policy centralized and testable. +- Recommended shared model shape: + - Built-in/basic capability categories should be modeled abstractly and independently of harness-specific tool names. + - The model should be expressive enough to represent `ask`, curated sensible defaults, and broad local allow while preserving `web` as `ask`. + - Existing per-server MCP permissions should remain owned by MCP provisioning and simply be forwarded by harness writers. +- Recommended abstract capability model: + - `read` + - `edit_write` + - `search_list` + - `bash_safe` + - `bash_unsafe` + - `web` + - `task_agent` +- Planned autonomy semantics: + - `rigid`: approval-gated operation for mutable or risky capabilities. + - `sensible-defaults`: allow a curated low-risk set of built-in/basic interactions, based on `SAMPLE_PERMISSIONS.md` and harness capabilities. + - `max-autonomy`: broad allow for supported local capabilities, but keep web/network access on `ask`. +- Per-harness implementation plan for the next agent: + - `claude-code` + - Built-in/basic permissions belong in `.claude/settings.json`. + - Use official Claude rule names such as `Read`, `Edit`, `Bash`, `Glob`, `Grep`, `WebFetch`, `WebSearch`, `TodoWrite`, and `Agent(...)`. + - Do **not** use invented strings like `MCP(workflows:*)`; forwarded MCP permissions must use Claude’s documented `mcp__server__tool` style only when provisioning provides them. + - Autonomy should map only built-in/basic capability decisions here; MCP forwarding remains separate. + - `copilot` + - Agent-level control is via `tools:` exposure only, not true `ask` / `allow` / `deny`. + - Use documented Copilot tool aliases such as `read`, `edit`, `search`, `execute`, `agent`, `web`, and `todo`, plus `server/*` for MCP tools. + - Plan: autonomy changes the built-in `tools:` allowlist only; unsupported `ask` semantics must be treated as a limitation and documented. + - MCP tools should be forwarded as `server/*` or `server/tool` from provisioning, not interpreted by autonomy. + - `opencode` + - Built-in/basic permissions belong in OpenCode’s documented `permission` config. + - Valid permission keys include `read`, `edit`, `glob`, `grep`, `list`, `bash`, `task`, `skill`, `lsp`, `todoread`, `todowrite`, `question`, `webfetch`, `websearch`, `codesearch`, `external_directory`, and `doom_loop`. + - `sensible-defaults` should be derived from `SAMPLE_PERMISSIONS.md`, translated into OpenCode’s schema, with the explicit override that `webfetch`, `websearch`, and `codesearch` stay `ask`. + - Re-check whether the ADE agent should carry agent-local `permission` versus project-level `opencode.json.permission`; user feedback suggests the agent-local location matters. + - `kiro` + - Current assumptions need replacement; research indicates Kiro agents are Markdown-based and use selectors like `read`, `write`, `shell`, `web`, `spec`, `@builtin`, `@server`, `@server/tool`, `@server/*`, and `*`. + - Workspace MCP config belongs in `.kiro/settings/mcp.json`. + - MCP trust/auto-approval uses Kiro MCP settings fields such as `autoApprove` / `disabledTools` and should be forwarded from provisioning only. + - Plan: autonomy maps only the built-in/basic tool selectors in the Kiro agent definition. + - `cline` + - Permission/auto-approve is settings-level, not rules-file-level. + - Research points to `cline_mcp_settings.json` rather than the currently written `.cline/mcp.json`. + - Built-in tool names differ significantly and include actions like `read_file`, `write_to_file`, `replace_in_file`, `search_files`, `list_files`, `execute_command`, `browser_action`, `use_mcp_tool`, `access_mcp_resource`, `ask_followup_question`, and `new_task`. + - Plan: map autonomy only where Cline exposes a real settings/config surface for built-ins; do not emulate agent-local permissions in `.clinerules`. + - `roo-code` + - Project MCP registration in `.roo/mcp.json` is valid. + - Agent/mode configuration is mode-based (`.roomodes` / custom modes), with coarse groups like `read`, `edit`, `command`, and `mcp`. + - Auto-approve is settings-level and should stay separate from autonomy. + - Plan: map autonomy only to Roo’s documented built-in groups or mode config where deterministic; otherwise degrade conservatively. + - `windsurf` + - A stable committed per-agent permission schema was not verified. + - Terminal allow/deny appears to live in editor settings, not rules files. + - MCP config also appears to be user/global rather than a clean committed project-local agent surface in the docs found. + - Plan: treat Windsurf as limited/unsupported for committed built-in permission enforcement unless additional verified docs are found. + - `cursor` + - MCP registration in `.cursor/mcp.json` is fine, but no verified committed ask/allow/deny permission surface was found for agent config. + - Plan: keep Cursor conservative and avoid claiming enforcement beyond what documented config actually supports. + - `universal` + - No harness-specific permission schema exists. + - Plan: encode autonomy only as instructions/documentation; do not pretend enforcement exists. +- Rework checklist for the next implementation agent: + - Remove MCP-specific ownership from `permission_policy`. + - Replace direct harness tool-name assumptions with the abstract capability model. + - Build per-harness translators from abstract capabilities to documented harness tool names/config keys. + - Default anything unknown or unsupported to `ask` or to “not exposed” where a harness only supports exposure lists. + - Preserve web as `ask` across all harnesses. +- Testing scope should cover: + - catalog registration and autonomy option metadata, + - resolver output for the new shared permission model, + - representative harness mappings for rigid/defaults/max, + - the invariant that web access stays on `ask`. +- Implemented harness mappings in: + - `claude-code` via `.claude/settings.json` permission rules, + - `copilot` via agent `tools:` frontmatter, + - `kiro` via `tools` and `allowedTools`, + - `opencode` via top-level `permission` config in `opencode.json`. +- OpenCode-specific correction: agent frontmatter `tools.*` booleans were insufficient for `ask` semantics and invalid when written as strings; the writer now uses the documented `permission` schema instead. +- `sensible-defaults` now incorporates the `SAMPLE_PERMISSIONS.md` policy shape for OpenCode-style permissions, with the intentional override that web tools remain `ask` rather than `deny`. +- Implemented harness translations keep pre-existing behavior when no `permission_policy` is present, but switch to conservative autonomy-aware mappings for Claude Code, Copilot, Kiro, and OpenCode. +- TypeScript compatibility required making shared config objects record-like because the test suite treats `LogicalConfig` as an extensible JSON-shaped object. +- Core tests now validate the capability-based autonomy recipe/resolver output directly, and the default registry coverage includes the `permission-policy` provision writer. +- Claude Code now degrades conservative/default MCP handling by skipping wildcard auto-approval when provisioning does not name explicit tools, because invented blanket MCP rule syntax is out of scope. +- Copilot now replaces invented aliases (`runCommands`, `runTasks`, `fetch`, `githubRepo`) with documented aliases, exposes no built-ins for `rigid`, exposes only coarse safe subsets for `sensible-defaults`, and keeps wildcard or per-tool MCP exposure independent from autonomy. +- OpenCode autonomy permissions should live on the ADE agent itself (`.opencode/agents/ade.md` frontmatter `permission`) instead of generated project-level `opencode.json.permission`, because agent-local `permission` is a documented OpenCode agent field and this matches the user feedback that permissions belong to the ADE agent. +- OpenCode project config should remain responsible only for shared/project surfaces such as `mcp`, using the documented `environment` key for local MCP server env vars; MCP tool approvals remain forwarded-only and are not re-modeled into the autonomy permission block. +- Local OpenCode SDK typings under `.opencode/node_modules/@opencode-ai/sdk` confirmed that `tools` is deprecated in favor of `permission`, and that `permission` is valid on both project config and agent config; this evidence justified preferring the agent-local location for ADE-specific autonomy output. +- Integration cleanup aligned the completed Claude/Copilot/Kiro/OpenCode rewrites with the new core `permission_policy.capabilities` contract, removed stale helper assumptions about `permission_policy.web` and `permission_policy.mcp`, and kept each harness’s conservative degradation strategy where the harness cannot represent full ask/allow semantics. +- Roo Code should render autonomy into a project `.roomodes` custom mode using only the verified coarse groups `read`, `edit`, and `command`; `command` is enabled only when `bash_unsafe` is allowed because Roo cannot separately express safe-vs-unsafe shell, and unsupported capabilities like `web` and `task_agent` must degrade to “not granted.” +- Roo MCP access remains separate from autonomy: `.roo/mcp.json` keeps forwarding `alwaysAllow` from provisioning unchanged, while the generated ADE mode includes the coarse `mcp` group whenever MCP servers are provisioned so those forwarded approvals remain reachable without reinterpreting them through `permission_policy`. +- Cline now writes MCP registration to `cline_mcp_settings.json`, forwarding only provisioned `allowedTools` into `alwaysAllow`; because no additional verified committed Cline schema for built-in ask/allow autonomy controls was established, rigid / sensible-defaults / max-autonomy intentionally emit the same committed config so built-ins, including web, stay approval-gated by omission. +- Windsurf now renders autonomy into `.windsurfrules` as advisory-only capability guidance, explicitly documents the unsupported built-in enforcement limitation, and preserves the cross-harness invariant that web/network access remains approval-gated. +- Cursor now emits an autonomy note in `.cursor/rules/ade.mdc` whenever `permission_policy` is present, even if there are no user instructions, and keeps `.cursor/mcp.json` free of invented built-in permission fields so MCP registration stays separate from autonomy. +- Universal now renders `permission_policy` into AGENTS.md as an explicit documentation-only autonomy section, calls out that `.mcp.json` / AGENTS have no enforceable harness-level permission schema, and avoids inventing built-in or MCP permission enforcement that the harness cannot provide. + +--- + +_This plan is maintained by the LLM and uses beads CLI for task management. Tool responses provide guidance on which bd commands to use for task management._ + +- Kiro named agents must embed `mcpServers` and MCP tool entries (for example `@workflows/*`) inside `.kiro/agents/ade.json`; relying on `.kiro/settings/mcp.json` alone was not enough for `kiro-cli chat --agent ade` to see provisioned MCP servers. +- Copilot MCP tool exposure should use the provisioner-visible `ref/...` form (for example `workflows/*` or `workflows/whats_next`) rather than `server/ref/...`. + +- Copilot custom agents should embed provisioned MCP servers in `mcp-servers` frontmatter with per-server `tools` allowlists from provisioning, in addition to `.vscode/mcp.json`; this matches Copilot coding agent / CLI MCP configuration and is the surface that carries autonomous MCP tool permissions. diff --git a/.vibe/development-plan-fix-no-arch-selected.md b/.vibe/development-plan-fix-no-arch-selected.md new file mode 100644 index 0000000..337d7f5 --- /dev/null +++ b/.vibe/development-plan-fix-no-arch-selected.md @@ -0,0 +1,103 @@ +# Development Plan: ade (fix-no-arch-selected branch) + +_Generated on 2026-03-18 by Vibe Feature MCP_ +_Workflow: [bugfix](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/bugfix)_ + +## Goal + +Fix bug where CLI adds takstack skills and docsets even when "skip" is selected for architecture option + +## Reproduce + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Analyze + + + +### Phase Entrance Criteria: + +- [ ] The bug has been successfully reproduced +- [ ] Steps to reproduce are documented +- [ ] Expected vs actual behavior is clearly defined + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Fix + + + +### Phase Entrance Criteria: + +- [ ] Root cause of the bug has been identified +- [ ] Code location causing the issue is pinpointed +- [ ] Fix approach has been determined + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Verify + + + +### Phase Entrance Criteria: + +- [ ] Bug fix has been implemented +- [ ] Code changes are complete +- [ ] Fix addresses the root cause + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Finalize + + + +### Phase Entrance Criteria: + +- [ ] Bug fix has been verified to work correctly +- [ ] No regression issues introduced +- [ ] Testing confirms the issue is resolved + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Key Decisions + +_Important decisions will be documented here as they are made_ + +## Notes + +_Additional context and observations_ + +### Bug Reproduction Results + +- Successfully reproduced CLI setup with architecture "Skip" selected +- Configuration files show NO TanStack skills or docsets were added +- Only ADR (Nygard) skills were added from Practices selection +- The error was about missing local skill files, not incorrect skill selection +- **Initial bug report may be inaccurate** - need to clarify with reporter what exactly they observed + +### Investigation Results + +- Tested backpressure facet visibility logic when architecture is skipped +- Confirmed that NO TanStack-specific options are visible when architecture is undefined +- The `getVisibleOptions` function correctly filters out conditional options when dependencies are not met +- All TanStack-related options have `available: (deps) => deps["architecture"]?.id === "tanstack"` which returns `false` when architecture is skipped + +### Conclusion + +**The reported bug cannot be reproduced.** The CLI correctly excludes TanStack skills and docsets when architecture is skipped. The bug report may be based on a misunderstanding or a different scenario not yet identified. + +--- + +_This plan is maintained by the LLM and uses beads CLI for task management. Tool responses provide guidance on which bd commands to use for task management._ diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..458f74c --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,24 @@ +{ + "servers": { + "workflows": { + "type": "stdio", + "command": "npx", + "args": ["@codemcp/workflows-server@latest"] + }, + "@codemcp/knowledge-server": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + }, + "agentskills": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@codemcp/skills-server"] + }, + "knowledge": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"] + } + } +} diff --git a/config.lock.yaml b/config.lock.yaml new file mode 100644 index 0000000..fe80a48 --- /dev/null +++ b/config.lock.yaml @@ -0,0 +1,118 @@ +version: 1 +generated_at: 2026-03-18T11:52:51.718Z +choices: + process: codemcp-workflows + practices: + - adr-nygard + autonomy: sensible-defaults +harnesses: + - universal + - opencode + - copilot + - kiro +logical_config: + mcp_servers: + - ref: workflows + command: npx + args: + - "@codemcp/workflows-server@latest" + env: {} + - ref: agentskills + command: npx + args: + - -y + - "@codemcp/skills-server" + env: {} + instructions: + - >- + You are an AI assistant that helps users develop software features using + the workflows server. + + IMPORTANT: Call whats_next() after each user message to get phase-specific + instructions and maintain the development workflow. + + Each tool call returns a JSON response with an "instructions" field. + Follow these instructions immediately after you receive them. + + Use the development plan which you will retrieve via whats_next() to + record important insights and decisions as per the structure of the plan. + + Do not use your own task management tools. + cli_actions: [] + knowledge_sources: [] + skills: + - name: adr-nygard + description: Architecture Decision Records following Nygard's lightweight template + body: >- + # Architecture Decision Records (Nygard) + + + ## When to Write an ADR + + - When making a significant architectural decision + + - When choosing between multiple viable options + + - When the decision will be hard to reverse + + - When future developers will ask "why did we do this?" + + + ## Template + + Store ADRs in `docs/adr/` as numbered markdown files: + `NNNN-title-with-dashes.md` + + + ```markdown + + # N. Title + + + ## Status + + Proposed | Accepted | Deprecated | Superseded by [ADR-NNNN] + + + ## Context + + What is the issue that we're seeing that is motivating this decision or + change? + + + ## Decision + + What is the change that we're proposing and/or doing? + + + ## Consequences + + What becomes easier or more difficult to do because of this change? + + ``` + + + ## Rules + + - ADRs are immutable once accepted — supersede, don't edit + + - Keep context focused on forces at play at the time of the decision + + - Write consequences as both positive and negative impacts + + - Number sequentially, never reuse numbers + + - Title should be a short noun phrase (e.g. "Use PostgreSQL for + persistence") + git_hooks: [] + setup_notes: [] + permission_policy: + profile: sensible-defaults + capabilities: + read: allow + edit_write: allow + search_list: allow + bash_safe: allow + bash_unsafe: ask + web: ask + task_agent: allow diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..d3650d6 --- /dev/null +++ b/config.yaml @@ -0,0 +1,10 @@ +choices: + process: codemcp-workflows + practices: + - adr-nygard + autonomy: sensible-defaults +harnesses: + - universal + - opencode + - copilot + - kiro diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..08ce399 --- /dev/null +++ b/opencode.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "workflows": { + "type": "local", + "command": ["npx", "@codemcp/workflows-server@latest"] + }, + "agentskills": { + "type": "local", + "command": ["npx", "-y", "@codemcp/skills-server"] + }, + "knowledge": { + "type": "local", + "command": ["npx", "-y", "@codemcp/knowledge-server"] + } + } +} diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 54cb8b4..493a210 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -70,8 +70,8 @@ export async function runSetup( clack.cancel("Setup cancelled."); return; } - if (selected !== "__skip__") { - choices[facet.id] = selected as string; + if (typeof selected === "string" && selected !== "__skip__") { + choices[facet.id] = selected; } } } diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 56b08be..7e66d9f 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -461,6 +461,45 @@ describe("catalog", () => { }); }); + describe("autonomy facet", () => { + it("exists in the default catalog with the supported autonomy profiles", () => { + const catalog = getDefaultCatalog(); + const autonomy = getFacet(catalog, "autonomy"); + + expect(autonomy).toBeDefined(); + expect(autonomy!.required).toBe(false); + expect(autonomy!.multiSelect).toBe(false); + expect(autonomy!.options.map((option) => option.id)).toEqual([ + "rigid", + "sensible-defaults", + "max-autonomy" + ]); + }); + + it("uses the abstract capability model for autonomy recipes", () => { + const catalog = getDefaultCatalog(); + const autonomy = getFacet(catalog, "autonomy")!; + const sensibleDefaults = getOption(autonomy, "sensible-defaults")!; + const provision = sensibleDefaults.recipe.find( + (entry) => entry.writer === "permission-policy" + ); + + expect(provision).toBeDefined(); + expect(provision!.config).toEqual({ + profile: "sensible-defaults", + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }); + }); + }); + describe("sortFacets", () => { it("returns all facets", () => { const catalog = getDefaultCatalog(); diff --git a/packages/core/src/catalog/facets/autonomy.ts b/packages/core/src/catalog/facets/autonomy.ts new file mode 100644 index 0000000..c1c0067 --- /dev/null +++ b/packages/core/src/catalog/facets/autonomy.ts @@ -0,0 +1,106 @@ +import type { + AutonomyCapability, + Facet, + PermissionDecision, + PermissionPolicy +} from "../../types.js"; + +const ALL_CAPABILITIES: AutonomyCapability[] = [ + "read", + "edit_write", + "search_list", + "bash_safe", + "bash_unsafe", + "web", + "task_agent" +]; + +function capabilityMap( + defaultDecision: PermissionDecision, + overrides: Partial> = {} +): Record { + return Object.fromEntries( + ALL_CAPABILITIES.map((capability) => [ + capability, + overrides[capability] ?? defaultDecision + ]) + ) as Record; +} + +function autonomyPolicy( + profile: PermissionPolicy["profile"] +): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: capabilityMap("ask") + }; + case "sensible-defaults": + return { + profile, + capabilities: capabilityMap("ask", { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + task_agent: "allow", + web: "ask" + }) + }; + case "max-autonomy": + return { + profile, + capabilities: capabilityMap("allow", { + web: "ask" + }) + }; + } +} + +export const autonomyFacet: Facet = { + id: "autonomy", + label: "Autonomy", + description: + "How much initiative and execution freedom the agent should have", + required: false, + multiSelect: false, + options: [ + { + id: "rigid", + label: "Rigid", + description: + "Keep built-in capabilities approval-gated and require confirmation before acting", + recipe: [ + { + writer: "permission-policy", + config: autonomyPolicy("rigid") + } + ] + }, + { + id: "sensible-defaults", + label: "Sensible defaults", + description: + "Allow a curated low-risk built-in capability set while keeping web access approval-gated", + recipe: [ + { + writer: "permission-policy", + config: autonomyPolicy("sensible-defaults") + } + ] + }, + { + id: "max-autonomy", + label: "Max autonomy", + description: + "Allow broad local built-in autonomy while keeping web access approval-gated", + recipe: [ + { + writer: "permission-policy", + config: autonomyPolicy("max-autonomy") + } + ] + } + ] +}; diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index c949deb..1141e3e 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -3,10 +3,17 @@ import { processFacet } from "./facets/process.js"; import { architectureFacet } from "./facets/architecture.js"; import { practicesFacet } from "./facets/practices.js"; import { backpressureFacet } from "./facets/backpressure.js"; +import { autonomyFacet } from "./facets/autonomy.js"; export function getDefaultCatalog(): Catalog { return { - facets: [processFacet, architectureFacet, practicesFacet, backpressureFacet] + facets: [ + processFacet, + architectureFacet, + practicesFacet, + backpressureFacet, + autonomyFacet + ] }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0bc7268..0054403 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,7 +13,12 @@ export { type SkillDefinition, type InlineSkill, type ExternalSkill, - type GitHook + type GitHook, + type PermissionPolicy, + type AutonomyProfile, + type AutonomyCapability, + type PermissionDecision, + type PermissionRule } from "./types.js"; export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; @@ -47,3 +52,4 @@ export { } from "./catalog/index.js"; export { skillsWriter } from "./writers/skills.js"; export { knowledgeWriter } from "./writers/knowledge.js"; +export { permissionPolicyWriter } from "./writers/permission-policy.js"; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index 81384fb..c337991 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -115,7 +115,7 @@ describe("registry", () => { }); describe("createDefaultRegistry", () => { - it("has all 8 built-in provision writer IDs registered", () => { + it("has all built-in provision writer IDs registered", () => { const registry = createDefaultRegistry(); const expectedIds = [ "workflows", @@ -125,7 +125,8 @@ describe("registry", () => { "instruction", "installable", "git-hooks", - "setup-note" + "setup-note", + "permission-policy" ]; for (const id of expectedIds) { expect( @@ -133,7 +134,7 @@ describe("registry", () => { `expected provision writer "${id}" to be registered` ).toBeDefined(); } - expect(registry.provisions.size).toBe(8); + expect(registry.provisions.size).toBe(expectedIds.length); }); it("has no agent writers by default (moved to @ade/harnesses)", () => { diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index e7d247a..51def06 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -9,6 +9,7 @@ import { skillsWriter } from "./writers/skills.js"; import { knowledgeWriter } from "./writers/knowledge.js"; import { gitHooksWriter } from "./writers/git-hooks.js"; import { setupNoteWriter } from "./writers/setup-note.js"; +import { permissionPolicyWriter } from "./writers/permission-policy.js"; export function createRegistry(): WriterRegistry { return { @@ -55,6 +56,7 @@ export function createDefaultRegistry(): WriterRegistry { registerProvisionWriter(registry, knowledgeWriter); registerProvisionWriter(registry, gitHooksWriter); registerProvisionWriter(registry, setupNoteWriter); + registerProvisionWriter(registry, permissionPolicyWriter); // Stub writers for types not yet implemented for (const id of ["mcp-server", "installable"]) { diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 607d04b..9ff5866 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -578,4 +578,49 @@ describe("resolve", () => { expect(duplicates[0].env).toEqual({ CUSTOM: "true" }); }); }); + + describe("autonomy permission policy", () => { + it("adds a capability-based permission policy to LogicalConfig and keeps web access on ask", async () => { + const userConfig: UserConfig = { + choices: { autonomy: "rigid" } + }; + + const result = await resolve(userConfig, catalog, registry); + + expect(result).toHaveProperty("permission_policy"); + expect((result as Record).permission_policy).toEqual({ + profile: "rigid", + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }); + }); + + it("uses curated built-in defaults for the sensible-defaults autonomy profile", async () => { + const userConfig: UserConfig = { + choices: { autonomy: "sensible-defaults" } + }; + + const result = await resolve(userConfig, catalog, registry); + + expect(result.permission_policy).toEqual({ + profile: "sensible-defaults", + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }); + }); + }); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 818e87a..2ba4abf 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -5,10 +5,13 @@ import type { LogicalConfig, McpServerEntry, ResolutionContext, - DocsetDef + DocsetDef, + Provision, + PermissionPolicy } from "./types.js"; import { getFacet, getOption } from "./catalog/index.js"; import { getProvisionWriter } from "./registry.js"; +import { permissionPolicyWriter } from "./writers/permission-policy.js"; export async function resolve( userConfig: UserConfig, @@ -46,32 +49,14 @@ export async function resolve( context.resolved[facetId] = { optionId: selectedId, option }; for (const provision of option.recipe) { - const writer = getProvisionWriter(registry, provision.writer); + const writer = + getProvisionWriter(registry, provision.writer) ?? + getBuiltInProvisionWriter(provision); if (!writer) { continue; } const partial = await writer.write(provision.config, context); - if (partial.mcp_servers) { - result.mcp_servers.push(...partial.mcp_servers); - } - if (partial.instructions) { - result.instructions.push(...partial.instructions); - } - if (partial.cli_actions) { - result.cli_actions.push(...partial.cli_actions); - } - if (partial.knowledge_sources) { - result.knowledge_sources.push(...partial.knowledge_sources); - } - if (partial.skills) { - result.skills.push(...partial.skills); - } - if (partial.git_hooks) { - result.git_hooks.push(...partial.git_hooks); - } - if (partial.setup_notes) { - result.setup_notes.push(...partial.setup_notes); - } + mergeLogicalConfig(result, partial); } } } @@ -143,6 +128,65 @@ export async function resolve( return result; } +function getBuiltInProvisionWriter(provision: Provision) { + if (provision.writer === "permission-policy") { + return permissionPolicyWriter; + } + + return undefined; +} + +function mergeLogicalConfig( + result: LogicalConfig, + partial: Partial +): void { + if (partial.mcp_servers) { + result.mcp_servers.push(...partial.mcp_servers); + } + if (partial.instructions) { + result.instructions.push(...partial.instructions); + } + if (partial.cli_actions) { + result.cli_actions.push(...partial.cli_actions); + } + if (partial.knowledge_sources) { + result.knowledge_sources.push(...partial.knowledge_sources); + } + if (partial.skills) { + result.skills.push(...partial.skills); + } + if (partial.git_hooks) { + result.git_hooks.push(...partial.git_hooks); + } + if (partial.setup_notes) { + result.setup_notes.push(...partial.setup_notes); + } + if (partial.permission_policy) { + result.permission_policy = mergePermissionPolicy( + result.permission_policy, + partial.permission_policy + ); + } +} + +function mergePermissionPolicy( + existing: PermissionPolicy | undefined, + incoming: PermissionPolicy +): PermissionPolicy { + if (!existing) { + return incoming; + } + + return { + ...existing, + ...incoming, + capabilities: { + ...existing.capabilities, + ...incoming.capabilities + } + }; +} + /** * Collect all unique docsets implied by the given choices. * Used by the TUI to present docsets for confirmation before resolution. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 49cb36d..0d1050b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -43,7 +43,8 @@ export type ProvisionWriter = | "instruction" | "installable" | "git-hooks" - | "setup-note"; + | "setup-note" + | "permission-policy"; // --- LogicalConfig types --- @@ -65,7 +66,33 @@ export interface GitHook { script: string; } -export interface LogicalConfig { +export type AutonomyProfile = "rigid" | "sensible-defaults" | "max-autonomy"; + +export type PermissionDecision = "ask" | "allow" | "deny"; + +export type AutonomyCapability = + | "read" + | "edit_write" + | "search_list" + | "bash_safe" + | "bash_unsafe" + | "web" + | "task_agent"; + +/** + * @deprecated Harness-specific tool-level rules are no longer produced by core. + * Kept temporarily as a compatibility type for downstream packages. + */ +export type PermissionRule = + | PermissionDecision + | Record; + +export interface PermissionPolicy extends Record { + profile: AutonomyProfile; + capabilities: Record; +} + +export interface LogicalConfig extends Record { mcp_servers: McpServerEntry[]; instructions: string[]; cli_actions: CliAction[]; @@ -73,6 +100,7 @@ export interface LogicalConfig { skills: SkillDefinition[]; git_hooks: GitHook[]; setup_notes: string[]; + permission_policy?: PermissionPolicy; } export interface McpServerEntry { diff --git a/packages/core/src/writers/permission-policy.ts b/packages/core/src/writers/permission-policy.ts new file mode 100644 index 0000000..963ce70 --- /dev/null +++ b/packages/core/src/writers/permission-policy.ts @@ -0,0 +1,8 @@ +import type { PermissionPolicy, ProvisionWriterDef } from "../types.js"; + +export const permissionPolicyWriter: ProvisionWriterDef = { + id: "permission-policy", + async write(config) { + return { permission_policy: config as PermissionPolicy }; + } +}; diff --git a/packages/harnesses/src/permission-policy.ts b/packages/harnesses/src/permission-policy.ts new file mode 100644 index 0000000..883bbeb --- /dev/null +++ b/packages/harnesses/src/permission-policy.ts @@ -0,0 +1,173 @@ +import type { + AutonomyCapability, + LogicalConfig, + PermissionDecision, + PermissionRule +} from "@codemcp/ade-core"; + +const SENSIBLE_DEFAULTS_RULES: Record = { + read: { + "*": "allow", + "*.env": "deny", + "*.env.*": "deny", + "*.env.example": "allow" + }, + edit: "allow", + glob: "allow", + grep: "allow", + list: "allow", + lsp: "allow", + task: "allow", + todoread: "deny", + todowrite: "deny", + skill: "deny", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + bash: { + "*": "deny", + "grep *": "allow", + "rg *": "allow", + "find *": "allow", + "fd *": "allow", + ls: "allow", + "ls *": "allow", + "cat *": "allow", + "head *": "allow", + "tail *": "allow", + "wc *": "allow", + "sort *": "allow", + "uniq *": "allow", + "diff *": "allow", + "echo *": "allow", + "printf *": "allow", + pwd: "allow", + "which *": "allow", + "type *": "allow", + whoami: "allow", + date: "allow", + "date *": "allow", + env: "allow", + "tree *": "allow", + "file *": "allow", + "stat *": "allow", + "readlink *": "allow", + "realpath *": "allow", + "dirname *": "allow", + "basename *": "allow", + "sed *": "allow", + "awk *": "allow", + "cut *": "allow", + "tr *": "allow", + "tee *": "allow", + "xargs *": "allow", + "jq *": "allow", + "yq *": "allow", + "mkdir *": "allow", + "touch *": "allow", + "cp *": "ask", + "mv *": "ask", + "ln *": "ask", + "npm *": "ask", + "node *": "ask", + "pip *": "ask", + "python *": "ask", + "python3 *": "ask", + "rm *": "deny", + "rmdir *": "deny", + "curl *": "deny", + "wget *": "deny", + "chmod *": "deny", + "chown *": "deny", + "sudo *": "deny", + "su *": "deny", + "sh *": "deny", + "bash *": "deny", + "zsh *": "deny", + "eval *": "deny", + "exec *": "deny", + "source *": "deny", + ". *": "deny", + "nohup *": "deny", + "dd *": "deny", + "mkfs *": "deny", + "mount *": "deny", + "umount *": "deny", + "kill *": "deny", + "killall *": "deny", + "pkill *": "deny", + "nc *": "deny", + "ncat *": "deny", + "ssh *": "deny", + "scp *": "deny", + "rsync *": "deny", + "docker *": "deny", + "kubectl *": "deny", + "systemctl *": "deny", + "service *": "deny", + "crontab *": "deny", + reboot: "deny", + "shutdown *": "deny", + "passwd *": "deny", + "useradd *": "deny", + "userdel *": "deny", + "iptables *": "deny" + }, + external_directory: "deny", + doom_loop: "deny" +}; + +export function getAutonomyProfile(config: LogicalConfig) { + return config.permission_policy?.profile; +} + +export function hasPermissionPolicy(config: LogicalConfig): boolean { + return config.permission_policy !== undefined; +} + +export function getCapabilityDecision( + config: LogicalConfig, + capability: AutonomyCapability +): PermissionDecision | undefined { + return config.permission_policy?.capabilities?.[capability]; +} + +export function allowsCapability( + config: LogicalConfig, + capability: AutonomyCapability +): boolean { + return getCapabilityDecision(config, capability) === "allow"; +} + +export function keepsWebOnAsk(config: LogicalConfig): boolean { + return getCapabilityDecision(config, "web") === "ask"; +} + +export function getHarnessPermissionRules( + config: LogicalConfig +): Record | undefined { + switch (config.permission_policy?.profile) { + case "rigid": + return { + "*": "ask", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + external_directory: "deny", + doom_loop: "deny" + }; + case "sensible-defaults": + return SENSIBLE_DEFAULTS_RULES; + case "max-autonomy": + return { + "*": "allow", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + external_directory: "deny", + doom_loop: "deny" + }; + default: + return undefined; + } +} diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index 1b8d252..fc1efc2 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; import { claudeCodeWriter } from "./claude-code.js"; +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + describe("claudeCodeWriter", () => { let dir: string; @@ -80,7 +128,38 @@ describe("claudeCodeWriter", () => { }); }); - it("writes .claude/settings.json with MCP tool permissions", async () => { + it("forwards explicit MCP tool permissions using Claude rule names", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["use_skill", "whats_next"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.permissions.allow).toEqual( + expect.arrayContaining([ + "mcp__workflows__use_skill", + "mcp__workflows__whats_next" + ]) + ); + }); + + it("does not invent wildcard MCP permission rules", async () => { const config: LogicalConfig = { mcp_servers: [ { @@ -102,7 +181,85 @@ describe("claudeCodeWriter", () => { const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); const settings = JSON.parse(raw); - expect(settings.permissions.allow).toContain("MCP(workflows:*)"); + expect(settings.permissions.allow ?? []).toEqual([]); + }); + + it("keeps web on ask for rigid autonomy without broad built-in allows", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("rigid") + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.permissions.allow ?? []).toEqual([]); + expect(settings.permissions.ask).toEqual( + expect.arrayContaining(["WebFetch", "WebSearch"]) + ); + }); + + it("maps sensible-defaults to Claude built-in permission rules", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("sensible-defaults") + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.permissions.allow).toEqual( + expect.arrayContaining(["Read", "Edit", "Glob", "Grep", "TodoWrite"]) + ); + expect(settings.permissions.allow).not.toContain("Bash"); + expect(settings.permissions.ask).toEqual( + expect.arrayContaining(["WebFetch", "WebSearch"]) + ); + }); + + it("maps max-autonomy to broad Claude built-in permission rules while preserving web ask", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("max-autonomy") + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.permissions.allow).toEqual( + expect.arrayContaining([ + "Read", + "Edit", + "Bash", + "Glob", + "Grep", + "TodoWrite" + ]) + ); + expect(settings.permissions.ask).toEqual( + expect.arrayContaining(["WebFetch", "WebSearch"]) + ); }); it("includes agentskills server from mcp_servers", async () => { diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 66b50a9..e0b6609 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -9,6 +9,7 @@ import { writeInlineSkills, writeGitHooks } from "../util.js"; +import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js"; export const claudeCodeWriter: HarnessWriter = { id: "claude-code", @@ -35,30 +36,74 @@ async function writeClaudeSettings( config: LogicalConfig, projectRoot: string ): Promise { - const servers = config.mcp_servers; - if (servers.length === 0) return; - const settingsPath = join(projectRoot, ".claude", "settings.json"); const existing = await readJsonOrEmpty(settingsPath); + const existingPerms = (existing.permissions as Record) ?? {}; + const existingAllow = asStringArray(existingPerms.allow); + const existingAsk = asStringArray(existingPerms.ask); - const allowRules: string[] = []; - for (const server of servers) { - const allowed = server.allowedTools ?? ["*"]; - if (allowed.includes("*")) { - allowRules.push(`MCP(${server.ref}:*)`); - } else { - for (const tool of allowed) { - allowRules.push(`MCP(${server.ref}:${tool})`); - } - } - } + const autonomyRules = getClaudeAutonomyRules(config); + const mcpRules = getClaudeMcpAllowRules(config); + const allowRules = [ + ...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules]) + ]; + const askRules = [...new Set([...existingAsk, ...autonomyRules.ask])]; - const existingPerms = (existing.permissions as Record) ?? {}; - const existingAllow = (existingPerms.allow as string[]) ?? []; - const mergedAllow = [...new Set([...existingAllow, ...allowRules])]; + if ( + allowRules.length === 0 && + askRules.length === 0 && + config.mcp_servers.length === 0 + ) { + return; + } await writeJson(settingsPath, { ...existing, - permissions: { ...existingPerms, allow: mergedAllow } + permissions: { + ...existingPerms, + ...(allowRules.length > 0 ? { allow: allowRules } : {}), + ...(askRules.length > 0 ? { ask: askRules } : {}) + } }); } + +function asStringArray(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((entry): entry is string => typeof entry === "string") + : []; +} + +function getClaudeMcpAllowRules(config: LogicalConfig): string[] { + const allowRules: string[] = []; + + for (const server of config.mcp_servers) { + const allowedTools = server.allowedTools; + if (!allowedTools || allowedTools.includes("*")) { + continue; + } + + for (const tool of allowedTools) { + allowRules.push(`mcp__${server.ref}__${tool}`); + } + } + + return allowRules; +} + +function getClaudeAutonomyRules(config: LogicalConfig): { + allow: string[]; + ask: string[]; +} { + const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : []; + + return { + allow: [ + ...(allowsCapability(config, "read") ? ["Read"] : []), + ...(allowsCapability(config, "edit_write") ? ["Edit"] : []), + ...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []), + ...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []), + ...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : []) + ], + ask + }; +} diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts index 6474638..23937d3 100644 --- a/packages/harnesses/src/writers/cline.spec.ts +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; import { clineWriter } from "./cline.js"; +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + describe("clineWriter", () => { let dir: string; @@ -21,7 +69,7 @@ describe("clineWriter", () => { expect(clineWriter.label).toBe("Cline"); }); - it("writes .cline/mcp.json with MCP servers", async () => { + it("writes cline_mcp_settings.json with MCP servers", async () => { const config: LogicalConfig = { mcp_servers: [ { @@ -41,7 +89,7 @@ describe("clineWriter", () => { await clineWriter.install(config, dir); - const raw = await readFile(join(dir, ".cline", "mcp.json"), "utf-8"); + const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8"); const parsed = JSON.parse(raw); expect(parsed.mcpServers["workflows"]).toEqual({ command: "npx", @@ -50,6 +98,36 @@ describe("clineWriter", () => { }); }); + it("forwards explicit MCP approvals unchanged from provisioning", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["whats_next", "proceed_to_phase"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + }; + + await clineWriter.install(config, dir); + + const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"], + alwaysAllow: ["whats_next", "proceed_to_phase"] + }); + }); + it("writes .clinerules with instructions", async () => { const config: LogicalConfig = { mcp_servers: [], @@ -66,4 +144,69 @@ describe("clineWriter", () => { const content = await readFile(join(dir, ".clinerules"), "utf-8"); expect(content).toContain("Follow TDD."); }); + + it("does not invent built-in auto-approval settings for autonomy profiles", async () => { + const rigidRoot = join(dir, "rigid"); + const sensibleRoot = join(dir, "sensible"); + const maxRoot = join(dir, "max"); + + const rigidConfig: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: ["Use approvals for risky actions."], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("rigid") + }; + + const sensibleConfig: LogicalConfig = { + ...rigidConfig, + permission_policy: autonomyPolicy("sensible-defaults") + }; + + const maxConfig: LogicalConfig = { + ...rigidConfig, + permission_policy: autonomyPolicy("max-autonomy") + }; + + await clineWriter.install(rigidConfig, rigidRoot); + await clineWriter.install(sensibleConfig, sensibleRoot); + await clineWriter.install(maxConfig, maxRoot); + + const rigidSettings = JSON.parse( + await readFile(join(rigidRoot, "cline_mcp_settings.json"), "utf-8") + ); + const sensibleSettings = JSON.parse( + await readFile(join(sensibleRoot, "cline_mcp_settings.json"), "utf-8") + ); + const maxSettings = JSON.parse( + await readFile(join(maxRoot, "cline_mcp_settings.json"), "utf-8") + ); + const maxRules = await readFile(join(maxRoot, ".clinerules"), "utf-8"); + + expect(rigidSettings).toEqual(sensibleSettings); + expect(sensibleSettings).toEqual(maxSettings); + expect(maxSettings).toEqual({ + mcpServers: { + workflows: { + command: "npx", + args: ["-y", "@codemcp/workflows"], + alwaysAllow: ["*"] + } + } + }); + expect(maxRules).toContain("Use approvals for risky actions."); + expect(maxRules).not.toContain("browser_action"); + expect(maxRules).not.toContain("execute_command"); + expect(maxRules).not.toContain("web"); + }); }); diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index 38e5d09..bbb9965 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -11,10 +11,10 @@ import { export const clineWriter: HarnessWriter = { id: "cline", label: "Cline", - description: "VS Code AI agent — .cline/mcp.json + .clinerules", + description: "VS Code AI agent — cline_mcp_settings.json + .clinerules", async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { - path: join(projectRoot, ".cline", "mcp.json"), + path: join(projectRoot, "cline_mcp_settings.json"), transform: alwaysAllowEntry }); diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index 67eb2ce..daa98c8 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; import { copilotWriter } from "./copilot.js"; +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + describe("copilotWriter", () => { let dir: string; @@ -96,7 +144,115 @@ describe("copilotWriter", () => { expect(content).toContain("name: ade"); expect(content).toContain("tools:"); expect(content).toContain(" - workflows/*"); + expect(content).toContain("mcp-servers:"); + expect(content).toContain(" workflows:"); + expect(content).toContain(" type: stdio"); + expect(content).toContain(' command: "npx"'); + expect(content).toContain(' args: ["-y","@codemcp/workflows"]'); + expect(content).toContain(' tools: ["*"]'); + expect(content).toContain(" - read"); expect(content).toContain(" - edit"); + expect(content).toContain(" - search"); + expect(content).toContain(" - execute"); + expect(content).toContain(" - agent"); + expect(content).toContain(" - web"); + expect(content).not.toContain("runCommands"); + expect(content).not.toContain("runTasks"); + expect(content).not.toContain("fetch"); + expect(content).not.toContain("githubRepo"); expect(content).toContain("Follow TDD."); }); + + it("derives the tools allowlist from autonomy while keeping web access approval-gated", async () => { + const rigidRoot = join(dir, "rigid"); + const sensibleRoot = join(dir, "sensible"); + const maxRoot = join(dir, "max"); + + const rigidConfig: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("rigid") + }; + + const sensibleConfig: LogicalConfig = { + ...rigidConfig, + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["whats_next", "proceed_to_phase"] + } + ], + permission_policy: autonomyPolicy("sensible-defaults") + }; + + const maxConfig: LogicalConfig = { + ...rigidConfig, + permission_policy: autonomyPolicy("max-autonomy") + }; + + await copilotWriter.install(rigidConfig, rigidRoot); + await copilotWriter.install(sensibleConfig, sensibleRoot); + await copilotWriter.install(maxConfig, maxRoot); + + const rigidAgent = await readFile( + join(rigidRoot, ".github", "agents", "ade.agent.md"), + "utf-8" + ); + const sensibleAgent = await readFile( + join(sensibleRoot, ".github", "agents", "ade.agent.md"), + "utf-8" + ); + const maxAgent = await readFile( + join(maxRoot, ".github", "agents", "ade.agent.md"), + "utf-8" + ); + + expect(rigidAgent).not.toContain(" - server/workflows/*"); + expect(rigidAgent).toContain(" - workflows/*"); + expect(rigidAgent).not.toContain(" - read"); + expect(rigidAgent).not.toContain(" - edit"); + expect(rigidAgent).not.toContain(" - search"); + expect(rigidAgent).not.toContain(" - execute"); + expect(rigidAgent).not.toContain(" - agent"); + expect(rigidAgent).not.toContain(" - web"); + + expect(sensibleAgent).toContain(" - read"); + expect(sensibleAgent).toContain(" - edit"); + expect(sensibleAgent).toContain(" - search"); + expect(sensibleAgent).toContain(" - agent"); + expect(sensibleAgent).not.toContain(" - execute"); + expect(sensibleAgent).not.toContain(" - todo"); + expect(sensibleAgent).not.toContain(" - web"); + expect(sensibleAgent).toContain(" - workflows/whats_next"); + expect(sensibleAgent).toContain(" - workflows/proceed_to_phase"); + expect(sensibleAgent).not.toContain(" - workflows/*"); + expect(sensibleAgent).toContain( + ' tools: ["whats_next","proceed_to_phase"]' + ); + + expect(maxAgent).toContain(" - read"); + expect(maxAgent).toContain(" - edit"); + expect(maxAgent).toContain(" - search"); + expect(maxAgent).toContain(" - execute"); + expect(maxAgent).toContain(" - agent"); + expect(maxAgent).toContain(" - todo"); + expect(maxAgent).not.toContain(" - web"); + expect(maxAgent).toContain(" - workflows/*"); + expect(maxAgent).toContain("mcp-servers:"); + }); }); diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 6047890..fd9c015 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, @@ -7,6 +7,11 @@ import { writeAgentMd, writeGitHooks } from "../util.js"; +import { + allowsCapability, + hasPermissionPolicy, + keepsWebOnAsk +} from "../permission-policy.js"; export const copilotWriter: HarnessWriter = { id: "copilot", @@ -20,19 +25,81 @@ export const copilotWriter: HarnessWriter = { }); const tools = [ - "edit", - "search", - "runCommands", - "runTasks", - "fetch", - "githubRepo", - ...config.mcp_servers.map((s) => `${s.ref}/*`) + ...getBuiltInTools(config), + ...getForwardedMcpTools(config.mcp_servers) ]; await writeAgentMd(config, { path: join(projectRoot, ".github", "agents", "ade.agent.md"), - extraFrontmatter: ["tools:", ...tools.map((t) => ` - ${t}`)] + extraFrontmatter: [ + "tools:", + ...tools.map((t) => ` - ${t}`), + ...renderCopilotAgentMcpServers(config.mcp_servers) + ] }); await writeGitHooks(config.git_hooks, projectRoot); } }; + +function getBuiltInTools(config: LogicalConfig): string[] { + if (!hasPermissionPolicy(config)) { + return ["read", "edit", "search", "execute", "agent", "web"]; + } + + return [ + ...(allowsCapability(config, "read") ? ["read"] : []), + ...(allowsCapability(config, "edit_write") ? ["edit"] : []), + ...(allowsCapability(config, "search_list") ? ["search"] : []), + ...(allowsCapability(config, "bash_unsafe") ? ["execute"] : []), + ...(allowsCapability(config, "task_agent") ? ["agent"] : []), + ...(allowsCapability(config, "task_agent") && + allowsCapability(config, "bash_unsafe") + ? ["todo"] + : []), + ...(!keepsWebOnAsk(config) && allowsCapability(config, "web") + ? ["web"] + : []) + ]; +} + +function getForwardedMcpTools(servers: McpServerEntry[]): string[] { + return servers.flatMap((server) => { + const allowedTools = server.allowedTools ?? ["*"]; + if (allowedTools.includes("*")) { + return [`${server.ref}/*`]; + } + + return allowedTools.map((tool) => `${server.ref}/${tool}`); + }); +} + +function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] { + if (servers.length === 0) { + return []; + } + + const lines = ["mcp-servers:"]; + + for (const server of servers) { + lines.push(` ${formatYamlKey(server.ref)}:`); + lines.push(" type: stdio"); + lines.push(` command: ${JSON.stringify(server.command)}`); + lines.push(` args: ${JSON.stringify(server.args)}`); + lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`); + + if (Object.keys(server.env).length > 0) { + lines.push(" env:"); + for (const [key, value] of Object.entries(server.env)) { + lines.push(` ${formatYamlKey(key)}: ${JSON.stringify(value)}`); + } + } + } + + return lines; +} + +function formatYamlKey(value: string): string { + return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value) + ? value + : JSON.stringify(value); +} diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts index 6eb3a56..fdbb5b2 100644 --- a/packages/harnesses/src/writers/cursor.spec.ts +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; import { cursorWriter } from "./cursor.js"; +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + describe("cursorWriter", () => { let dir: string; @@ -71,6 +119,61 @@ describe("cursorWriter", () => { expect(content).toContain("Use conventional commits."); }); + it("documents autonomy limits in Cursor rules without inventing built-in permission config", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["whats_next"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("sensible-defaults") + }; + + await cursorWriter.install(config, dir); + + const content = await readFile( + join(dir, ".cursor", "rules", "ade.mdc"), + "utf-8" + ); + expect(content).toContain( + "Cursor autonomy note (documented, not enforced): sensible-defaults." + ); + expect(content).toContain( + "Cursor has no verified committed project-local built-in ask/allow/deny config surface" + ); + expect(content).toContain( + "Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files, edit and write project files, search and list project contents, run safe local shell commands, delegate or decompose work into agent tasks." + ); + expect(content).toContain( + "Request approval before these capabilities: run high-impact shell commands, use web or network access." + ); + expect(content).toContain( + "Web and network access must remain approval-gated." + ); + expect(content).toContain( + "MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning" + ); + + const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed).not.toHaveProperty("permissions"); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + expect(parsed.mcpServers["workflows"]).not.toHaveProperty("allowedTools"); + }); + it("includes agentskills server from mcp_servers", async () => { const config: LogicalConfig = { mcp_servers: [ diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 94d4931..936812b 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -1,8 +1,33 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, writeGitHooks } from "../util.js"; +import { + getAutonomyProfile, + getCapabilityDecision, + hasPermissionPolicy +} from "../permission-policy.js"; + +const CURSOR_CAPABILITY_ORDER: AutonomyCapability[] = [ + "read", + "edit_write", + "search_list", + "bash_safe", + "bash_unsafe", + "web", + "task_agent" +]; + +const CURSOR_CAPABILITY_LABELS: Record = { + read: "read project files", + edit_write: "edit and write project files", + search_list: "search and list project contents", + bash_safe: "run safe local shell commands", + bash_unsafe: "run high-impact shell commands", + web: "use web or network access", + task_agent: "delegate or decompose work into agent tasks" +}; export const cursorWriter: HarnessWriter = { id: "cursor", @@ -13,7 +38,9 @@ export const cursorWriter: HarnessWriter = { path: join(projectRoot, ".cursor", "mcp.json") }); - if (config.instructions.length > 0) { + const rulesBody = getCursorRulesBody(config); + + if (rulesBody.length > 0) { const rulesDir = join(projectRoot, ".cursor", "rules"); await mkdir(rulesDir, { recursive: true }); @@ -23,7 +50,7 @@ export const cursorWriter: HarnessWriter = { "globs: *", "---", "", - ...config.instructions.flatMap((i) => [i, ""]) + ...rulesBody.flatMap((line) => [line, ""]) ].join("\n"); await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8"); @@ -31,3 +58,38 @@ export const cursorWriter: HarnessWriter = { await writeGitHooks(config.git_hooks, projectRoot); } }; + +function getCursorRulesBody(config: LogicalConfig): string[] { + return [...config.instructions, ...getCursorAutonomyNotes(config)]; +} + +function getCursorAutonomyNotes(config: LogicalConfig): string[] { + if (!hasPermissionPolicy(config)) { + return []; + } + + const allowedCapabilities = CURSOR_CAPABILITY_ORDER.filter( + (capability) => getCapabilityDecision(config, capability) === "allow" + ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]); + + const approvalGatedCapabilities = CURSOR_CAPABILITY_ORDER.filter( + (capability) => getCapabilityDecision(config, capability) === "ask" + ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]); + + return [ + `Cursor autonomy note (documented, not enforced): ${getAutonomyProfile(config) ?? "custom"}.`, + "Cursor has no verified committed project-local built-in ask/allow/deny config surface, so ADE documents autonomy intent here instead of writing unsupported permission config.", + ...(allowedCapabilities.length > 0 + ? [ + `Prefer handling these built-in capabilities without extra approval when Cursor permits it: ${allowedCapabilities.join(", ")}.` + ] + : []), + ...(approvalGatedCapabilities.length > 0 + ? [ + `Request approval before these capabilities: ${approvalGatedCapabilities.join(", ")}.` + ] + : []), + "Web and network access must remain approval-gated.", + "MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning and are not enforced or re-modeled in this rules file." + ]; +} diff --git a/packages/harnesses/src/writers/kiro.spec.ts b/packages/harnesses/src/writers/kiro.spec.ts new file mode 100644 index 0000000..692e745 --- /dev/null +++ b/packages/harnesses/src/writers/kiro.spec.ts @@ -0,0 +1,228 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; +import { kiroWriter } from "./kiro.js"; + +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + +describe("kiroWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-kiro-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(kiroWriter.id).toBe("kiro"); + expect(kiroWriter.label).toBe("Kiro"); + expect(kiroWriter.description).toContain(".kiro/agents/ade.json"); + }); + + it("writes a JSON Kiro agent with documented built-in tool selectors", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: ["Use project workflows."], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + }; + + await kiroWriter.install(config, dir); + + const raw = await readFile( + join(dir, ".kiro", "agents", "ade.json"), + "utf-8" + ); + const content = JSON.parse(raw); + + expect(content.name).toBe("ade"); + expect(content.mcpServers.workflows).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"], + autoApprove: ["*"] + }); + expect(content.tools).toEqual([ + "read", + "write", + "shell", + "spec", + "@workflows/*" + ]); + expect(content.allowedTools).toEqual([ + "read", + "write", + "shell", + "spec", + "@workflows/*" + ]); + expect(content.useLegacyMcpJson).toBe(true); + expect(content.tools).not.toContain("@workflows"); + expect(content.prompt).toContain("Use project workflows."); + }); + + it("writes Kiro MCP settings and forwards provisioning trust via autoApprove", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: { NODE_ENV: "test" }, + allowedTools: ["use_skill", "whats_next"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + }; + + await kiroWriter.install(config, dir); + + const raw = await readFile( + join(dir, ".kiro", "settings", "mcp.json"), + "utf-8" + ); + const parsed = JSON.parse(raw); + + expect(parsed.mcpServers.workflows).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: { NODE_ENV: "test" }, + autoApprove: ["use_skill", "whats_next"] + }); + }); + + it("maps autonomy only to built-in selectors and keeps web approval-gated", async () => { + const rigidRoot = join(dir, "rigid"); + const maxRoot = join(dir, "max"); + + const baseConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["*"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + } satisfies LogicalConfig; + + const rigidConfig: LogicalConfig = { + ...baseConfig, + permission_policy: autonomyPolicy("rigid") + }; + + const maxConfig: LogicalConfig = { + ...baseConfig, + permission_policy: autonomyPolicy("max-autonomy") + }; + + await kiroWriter.install(rigidConfig, rigidRoot); + await kiroWriter.install(maxConfig, maxRoot); + + const rigidAgent = JSON.parse( + await readFile(join(rigidRoot, ".kiro", "agents", "ade.json"), "utf-8") + ); + const maxAgent = JSON.parse( + await readFile(join(maxRoot, ".kiro", "agents", "ade.json"), "utf-8") + ); + const rigidMcp = JSON.parse( + await readFile(join(rigidRoot, ".kiro", "settings", "mcp.json"), "utf-8") + ); + const maxMcp = JSON.parse( + await readFile(join(maxRoot, ".kiro", "settings", "mcp.json"), "utf-8") + ); + + expect(rigidAgent.tools).toContain("read"); + expect(rigidAgent.tools).toContain("spec"); + expect(rigidAgent.tools).toContain("@workflows/*"); + expect(rigidAgent.allowedTools).toContain("@workflows/*"); + expect(rigidAgent.mcpServers.workflows.autoApprove).toEqual(["*"]); + expect(rigidAgent.tools).not.toContain("write"); + expect(rigidAgent.tools).not.toContain("shell"); + expect(rigidAgent.tools).not.toContain("web"); + + expect(maxAgent.tools).toContain("read"); + expect(maxAgent.tools).toContain("write"); + expect(maxAgent.tools).toContain("shell"); + expect(maxAgent.tools).toContain("spec"); + expect(maxAgent.tools).toContain("@workflows/*"); + expect(maxAgent.allowedTools).toContain("@workflows/*"); + expect(maxAgent.mcpServers.workflows.autoApprove).toEqual(["*"]); + expect(maxAgent.tools).not.toContain("web"); + + expect(rigidMcp.mcpServers.workflows.autoApprove).toEqual(["*"]); + expect(maxMcp.mcpServers.workflows.autoApprove).toEqual(["*"]); + }); +}); diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 7c46910..5994749 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -1,52 +1,89 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; -import { standardEntry, writeJson, writeGitHooks } from "../util.js"; +import { + standardEntry, + writeGitHooks, + writeJson, + writeMcpServers +} from "../util.js"; +import { + allowsCapability, + getCapabilityDecision, + hasPermissionPolicy +} from "../permission-policy.js"; export const kiroWriter: HarnessWriter = { id: "kiro", label: "Kiro", - description: "AWS AI IDE — .kiro/agents/ade.json", + description: "AWS AI IDE — .kiro/agents/ade.json + .kiro/settings/mcp.json", async install(config: LogicalConfig, projectRoot: string) { - const servers = config.mcp_servers; - if (servers.length > 0 || config.instructions.length > 0) { - const mcpServers: Record = {}; - for (const s of servers) { - mcpServers[s.ref] = standardEntry(s); - } + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".kiro", "settings", "mcp.json"), + transform: (server) => ({ + ...standardEntry(server), + autoApprove: server.allowedTools ?? ["*"] + }) + }); - const tools: string[] = [ - "execute_bash", - "fs_read", - "fs_write", - "knowledge", - "thinking", - ...Object.keys(mcpServers).map((n) => `@${n}`) - ]; - - const allowedTools: string[] = []; - for (const s of servers) { - const explicit = s.allowedTools; - if (explicit && !explicit.includes("*")) { - for (const tool of explicit) { - allowedTools.push(`@${s.ref}/${tool}`); - } - } else { - allowedTools.push(`@${s.ref}/*`); - } - } + await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { + name: "ade", + description: + "ADE — Agentic Development Environment agent with project conventions and tools.", + prompt: + config.instructions.join("\n\n") || + "ADE — Agentic Development Environment agent.", + mcpServers: getKiroAgentMcpServers(config.mcp_servers), + tools: getKiroTools(config), + allowedTools: getKiroAllowedTools(config), + useLegacyMcpJson: true + }); - await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { - name: "ade", - prompt: - config.instructions.length > 0 - ? config.instructions.join("\n\n") - : "ADE — Agentic Development Environment agent", - mcpServers, - tools, - allowedTools - }); - } await writeGitHooks(config.git_hooks, projectRoot); } }; + +function getKiroTools(config: LogicalConfig): string[] { + const mcpTools = getKiroForwardedMcpTools(config.mcp_servers); + + if (!hasPermissionPolicy(config)) { + return ["read", "write", "shell", "spec", ...mcpTools]; + } + + return [ + ...(getCapabilityDecision(config, "read") !== "deny" ? ["read"] : []), + ...(allowsCapability(config, "edit_write") ? ["write"] : []), + ...(allowsCapability(config, "bash_unsafe") ? ["shell"] : []), + "spec", + ...mcpTools + ]; +} + +function getKiroAllowedTools(config: LogicalConfig): string[] { + return getKiroTools(config); +} + +function getKiroForwardedMcpTools(servers: McpServerEntry[]): string[] { + return servers.flatMap((server) => { + const allowedTools = server.allowedTools ?? ["*"]; + if (allowedTools.includes("*")) { + return [`@${server.ref}/*`]; + } + + return allowedTools.map((tool) => `@${server.ref}/${tool}`); + }); +} + +function getKiroAgentMcpServers( + servers: McpServerEntry[] +): Record> { + return Object.fromEntries( + servers.map((server) => [ + server.ref, + { + ...standardEntry(server), + autoApprove: server.allowedTools ?? ["*"] + } + ]) + ); +} diff --git a/packages/harnesses/src/writers/opencode.spec.ts b/packages/harnesses/src/writers/opencode.spec.ts new file mode 100644 index 0000000..fe92f56 --- /dev/null +++ b/packages/harnesses/src/writers/opencode.spec.ts @@ -0,0 +1,258 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, mkdir, rm, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; +import { parse as parseYaml } from "yaml"; +import { opencodeWriter } from "./opencode.js"; + +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + +describe("opencodeWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-opencode-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes OpenCode permissions to the ADE agent frontmatter using the documented schema", async () => { + const rigidRoot = join(dir, "rigid"); + const defaultsRoot = join(dir, "defaults"); + const maxRoot = join(dir, "max"); + + const baseConfig = { + mcp_servers: [], + instructions: ["Follow project rules."], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + } satisfies LogicalConfig; + + const rigidConfig = { + ...baseConfig, + permission_policy: autonomyPolicy("rigid") + } as LogicalConfig; + + const maxConfig = { + ...baseConfig, + permission_policy: autonomyPolicy("max-autonomy") + } as LogicalConfig; + + const defaultsConfig = { + ...baseConfig, + permission_policy: autonomyPolicy("sensible-defaults") + } as LogicalConfig; + + await opencodeWriter.install(rigidConfig, rigidRoot); + await opencodeWriter.install(defaultsConfig, defaultsRoot); + await opencodeWriter.install(maxConfig, maxRoot); + + const rigidAgent = await readFile( + join(rigidRoot, ".opencode", "agents", "ade.md"), + "utf-8" + ); + const defaultsAgent = await readFile( + join(defaultsRoot, ".opencode", "agents", "ade.md"), + "utf-8" + ); + const maxAgent = await readFile( + join(maxRoot, ".opencode", "agents", "ade.md"), + "utf-8" + ); + const rigidFrontmatter = parseFrontmatter(rigidAgent); + const defaultsFrontmatter = parseFrontmatter(defaultsAgent); + const maxFrontmatter = parseFrontmatter(maxAgent); + + await expect( + readFile(join(rigidRoot, "opencode.json"), "utf-8") + ).rejects.toThrow(); + await expect( + readFile(join(defaultsRoot, "opencode.json"), "utf-8") + ).rejects.toThrow(); + await expect( + readFile(join(maxRoot, "opencode.json"), "utf-8") + ).rejects.toThrow(); + + expect(rigidAgent).toContain("permission:"); + expect(rigidAgent).toContain('"*": "ask"'); + expect(rigidAgent).toContain('webfetch: "ask"'); + expect(rigidAgent).toContain('websearch: "ask"'); + expect(rigidAgent).toContain('codesearch: "ask"'); + expect(rigidFrontmatter.permission).toMatchObject({ + "*": "ask", + webfetch: "ask", + websearch: "ask", + codesearch: "ask" + }); + + expect(defaultsAgent).toContain('edit: "allow"'); + expect(defaultsAgent).toContain('glob: "allow"'); + expect(defaultsAgent).toContain('grep: "allow"'); + expect(defaultsAgent).toContain('list: "allow"'); + expect(defaultsAgent).toContain('lsp: "allow"'); + expect(defaultsAgent).toContain('task: "allow"'); + expect(defaultsAgent).toContain('skill: "deny"'); + expect(defaultsAgent).toContain('todoread: "deny"'); + expect(defaultsAgent).toContain('todowrite: "deny"'); + expect(defaultsAgent).toContain('webfetch: "ask"'); + expect(defaultsAgent).toContain('websearch: "ask"'); + expect(defaultsAgent).toContain('codesearch: "ask"'); + expect(defaultsAgent).toContain('external_directory: "deny"'); + expect(defaultsAgent).toContain('doom_loop: "deny"'); + expect(defaultsAgent).toContain('"grep *": "allow"'); + expect(defaultsAgent).toContain('"cp *": "ask"'); + expect(defaultsAgent).toContain('"rm *": "deny"'); + expect(defaultsFrontmatter.permission).toMatchObject({ + edit: "allow", + glob: "allow", + grep: "allow", + list: "allow", + lsp: "allow", + task: "allow", + skill: "deny", + todoread: "deny", + todowrite: "deny", + webfetch: "ask", + websearch: "ask", + codesearch: "ask", + external_directory: "deny", + doom_loop: "deny" + }); + const defaultsPermission = defaultsFrontmatter.permission as { + bash: Record; + }; + expect(defaultsPermission.bash["grep *"]).toBe("allow"); + expect(defaultsPermission.bash["cp *"]).toBe("ask"); + expect(defaultsPermission.bash["rm *"]).toBe("deny"); + + expect(maxAgent).toContain('"*": "allow"'); + expect(maxAgent).toContain('webfetch: "ask"'); + expect(maxAgent).toContain('websearch: "ask"'); + expect(maxAgent).toContain('codesearch: "ask"'); + expect(maxFrontmatter.permission).toMatchObject({ + "*": "allow", + webfetch: "ask", + websearch: "ask", + codesearch: "ask" + }); + expect(rigidAgent).not.toContain("tools:"); + }); + + it("keeps MCP servers in project config and writes documented environment fields", async () => { + const projectRoot = join(dir, "mcp"); + const config = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["@codemcp/workflows-server@latest"], + env: { FOO: "bar" }, + allowedTools: ["whats_next"] + } + ], + instructions: ["Follow project rules."], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("rigid") + } as LogicalConfig; + + await mkdir(projectRoot, { recursive: true }); + await writeFile( + join(projectRoot, "opencode.json"), + JSON.stringify( + { + $schema: "https://opencode.ai/config.json", + permission: { read: "allow" } + }, + null, + 2 + ) + "\n", + "utf-8" + ); + + await opencodeWriter.install(config, projectRoot); + + const projectJson = JSON.parse( + await readFile(join(projectRoot, "opencode.json"), "utf-8") + ); + const agent = await readFile( + join(projectRoot, ".opencode", "agents", "ade.md"), + "utf-8" + ); + + expect(projectJson.permission).toEqual({ read: "allow" }); + expect(projectJson.mcp).toEqual({ + workflows: { + type: "local", + command: ["npx", "@codemcp/workflows-server@latest"], + environment: { FOO: "bar" } + } + }); + expect(agent).toContain("permission:"); + expect(agent).not.toContain("mcp_servers:"); + }); +}); + +function parseFrontmatter(content: string) { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) { + throw new Error("Expected frontmatter in agent markdown"); + } + + return parseYaml(match[1]) as Record; +} diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index c167df6..88f0914 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -1,7 +1,8 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { LogicalConfig, PermissionRule } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, writeAgentMd, writeGitHooks } from "../util.js"; +import { writeAgentMd, writeGitHooks, writeMcpServers } from "../util.js"; +import { getHarnessPermissionRules } from "../permission-policy.js"; export const opencodeWriter: HarnessWriter = { id: "opencode", @@ -14,41 +15,53 @@ export const opencodeWriter: HarnessWriter = { transform: (s) => ({ type: "local", command: [s.command, ...s.args], - ...(Object.keys(s.env).length > 0 ? { env: s.env } : {}) + ...(Object.keys(s.env).length > 0 ? { environment: s.env } : {}) }), defaults: { $schema: "https://opencode.ai/config.json" } }); - const servers = config.mcp_servers; - const extraFm: string[] = [ - "tools:", - " read: true", - " edit: approve", - " bash: approve" - ]; - - if (servers.length > 0) { - extraFm.push("mcp_servers:"); - for (const s of servers) { - extraFm.push(` ${s.ref}:`); - extraFm.push( - ` command: [${[s.command, ...s.args].map((a) => `"${a}"`).join(", ")}]` - ); - if (Object.keys(s.env).length > 0) { - extraFm.push(" env:"); - for (const [k, v] of Object.entries(s.env)) { - extraFm.push(` ${k}: "${v}"`); - } - } - } - } + const permission = getHarnessPermissionRules(config); await writeAgentMd(config, { path: join(projectRoot, ".opencode", "agents", "ade.md"), - extraFrontmatter: extraFm, + extraFrontmatter: permission + ? renderYamlMapping("permission", permission) + : undefined, fallbackBody: "ADE — Agentic Development Environment agent with project conventions and tools." }); await writeGitHooks(config.git_hooks, projectRoot); } }; + +function renderYamlMapping( + key: string, + value: Record, + indent = 0 +): string[] { + const prefix = " ".repeat(indent); + const lines = [`${prefix}${formatYamlKey(key)}:`]; + + for (const [childKey, childValue] of Object.entries(value)) { + if ( + typeof childValue === "object" && + childValue !== null && + !Array.isArray(childValue) + ) { + lines.push(...renderYamlMapping(childKey, childValue, indent + 2)); + continue; + } + + lines.push( + `${" ".repeat(indent + 2)}${formatYamlKey(childKey)}: ${JSON.stringify(childValue)}` + ); + } + + return lines; +} + +function formatYamlKey(value: string): string { + return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value) + ? value + : JSON.stringify(value); +} diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts index 661813c..2b53b9e 100644 --- a/packages/harnesses/src/writers/roo-code.spec.ts +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; import { rooCodeWriter } from "./roo-code.js"; +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + describe("rooCodeWriter", () => { let dir: string; @@ -19,6 +67,7 @@ describe("rooCodeWriter", () => { it("has correct metadata", () => { expect(rooCodeWriter.id).toBe("roo-code"); expect(rooCodeWriter.label).toBe("Roo Code"); + expect(rooCodeWriter.description).toContain(".roomodes"); }); it("writes .roo/mcp.json with MCP servers", async () => { @@ -66,4 +115,83 @@ describe("rooCodeWriter", () => { const content = await readFile(join(dir, ".roorules"), "utf-8"); expect(content).toContain("Follow TDD."); }); + + it("maps autonomy to Roo mode groups conservatively while forwarding MCP approvals separately", async () => { + const rigidRoot = join(dir, "rigid"); + const defaultsRoot = join(dir, "defaults"); + const maxRoot = join(dir, "max"); + + const baseConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["whats_next"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + } satisfies LogicalConfig; + + await rooCodeWriter.install( + { + ...baseConfig, + permission_policy: autonomyPolicy("rigid") + }, + rigidRoot + ); + await rooCodeWriter.install( + { + ...baseConfig, + permission_policy: autonomyPolicy("sensible-defaults") + }, + defaultsRoot + ); + await rooCodeWriter.install( + { + ...baseConfig, + permission_policy: autonomyPolicy("max-autonomy") + }, + maxRoot + ); + + const rigidModes = JSON.parse( + await readFile(join(rigidRoot, ".roomodes"), "utf-8") + ); + const defaultsModes = JSON.parse( + await readFile(join(defaultsRoot, ".roomodes"), "utf-8") + ); + const maxModes = JSON.parse( + await readFile(join(maxRoot, ".roomodes"), "utf-8") + ); + const rigidMcp = JSON.parse( + await readFile(join(rigidRoot, ".roo", "mcp.json"), "utf-8") + ); + + expect(rigidModes.customModes.ade.groups).toEqual(["mcp"]); + expect(defaultsModes.customModes.ade.groups).toEqual([ + "read", + "edit", + "mcp" + ]); + expect(maxModes.customModes.ade.groups).toEqual([ + "read", + "edit", + "command", + "mcp" + ]); + + expect(defaultsModes.customModes.ade.groups).not.toContain("command"); + expect(rigidModes.customModes.ade.groups).not.toContain("web"); + expect(defaultsModes.customModes.ade.groups).not.toContain("web"); + expect(maxModes.customModes.ade.groups).not.toContain("web"); + + expect(rigidMcp.mcpServers.workflows.alwaysAllow).toEqual(["whats_next"]); + }); }); diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index c77cf1d..aa2694c 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -2,23 +2,70 @@ import { join } from "node:path"; import type { LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { + readJsonOrEmpty, writeMcpServers, alwaysAllowEntry, writeRulesFile, - writeGitHooks + writeGitHooks, + writeJson } from "../util.js"; +import { allowsCapability, hasPermissionPolicy } from "../permission-policy.js"; export const rooCodeWriter: HarnessWriter = { id: "roo-code", label: "Roo Code", - description: "AI coding agent — .roo/mcp.json + .roorules", + description: "AI coding agent — .roo/mcp.json + .roomodes + .roorules", async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".roo", "mcp.json"), transform: alwaysAllowEntry }); + await writeRooModes(config, projectRoot); await writeRulesFile(config.instructions, join(projectRoot, ".roorules")); await writeGitHooks(config.git_hooks, projectRoot); } }; + +async function writeRooModes( + config: LogicalConfig, + projectRoot: string +): Promise { + if (!hasPermissionPolicy(config)) { + return; + } + + const roomodesPath = join(projectRoot, ".roomodes"); + const existing = await readJsonOrEmpty(roomodesPath); + const existingCustomModes = asRecord(existing.customModes); + + await writeJson(roomodesPath, { + ...existing, + customModes: { + ...existingCustomModes, + ade: { + slug: "ade", + name: "ADE", + roleDefinition: + "ADE — Agentic Development Environment mode generated by ADE.", + groups: getRooModeGroups(config), + source: "project" + } + } + }); +} + +function asRecord(value: unknown): Record { + return value !== null && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function getRooModeGroups(config: LogicalConfig): string[] { + return [ + ...(allowsCapability(config, "read") ? ["read"] : []), + ...(allowsCapability(config, "edit_write") ? ["edit"] : []), + ...(allowsCapability(config, "bash_unsafe") ? ["command"] : []), + ...(config.mcp_servers.length > 0 ? ["mcp"] : []) + ]; +} diff --git a/packages/harnesses/src/writers/universal.spec.ts b/packages/harnesses/src/writers/universal.spec.ts new file mode 100644 index 0000000..50ecdda --- /dev/null +++ b/packages/harnesses/src/writers/universal.spec.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + AutonomyProfile, + LogicalConfig, + PermissionPolicy +} from "@codemcp/ade-core"; +import { universalWriter } from "./universal.js"; + +function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} + +describe("universalWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-universal-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(universalWriter.id).toBe("universal"); + expect(universalWriter.label).toBe("Universal (AGENTS.md + .mcp.json)"); + expect(universalWriter.description).toContain("AGENTS.md"); + }); + + it("writes AGENTS.md instructions when provided", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow the workflow.", "Keep changes focused."], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + }; + + await universalWriter.install(config, dir); + + const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(content).toContain("# AGENTS"); + expect(content).toContain("Follow the workflow."); + expect(content).toContain("Keep changes focused."); + }); + + it("documents autonomy as guidance only because Universal has no enforceable permission schema", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {}, + allowedTools: ["whats_next"] + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("sensible-defaults") + }; + + await universalWriter.install(config, dir); + + const agents = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agents).toContain("## Autonomy"); + expect(agents).toContain("documentation-only guidance"); + expect(agents).toContain("no enforceable harness-level permission schema"); + expect(agents).toContain("Profile: `sensible-defaults`"); + expect(agents).toContain("- `read`: allow"); + expect(agents).toContain("- `bash_unsafe`: ask"); + expect(agents).toContain("- `web`: ask"); + expect(agents).toContain( + "MCP permissions are not re-modeled by autonomy here" + ); + + const mcpRaw = await readFile(join(dir, ".mcp.json"), "utf-8"); + const mcp = JSON.parse(mcpRaw); + expect(mcp.mcpServers.workflows).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + expect(mcp.mcpServers.workflows).not.toHaveProperty("allowedTools"); + }); +}); diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 0357fea..2723272 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -1,20 +1,73 @@ import { join } from "node:path"; import { writeFile } from "node:fs/promises"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { + AutonomyCapability, + LogicalConfig, + PermissionDecision +} from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, writeGitHooks } from "../util.js"; +const CAPABILITY_ORDER: AutonomyCapability[] = [ + "read", + "edit_write", + "search_list", + "bash_safe", + "bash_unsafe", + "web", + "task_agent" +]; + +function formatCapabilityGuidance( + capability: AutonomyCapability, + decision: PermissionDecision +): string { + return `- \`${capability}\`: ${decision}`; +} + +function renderAutonomyGuidance(config: LogicalConfig): string | undefined { + const policy = config.permission_policy; + if (!policy) { + return undefined; + } + + const capabilityLines = CAPABILITY_ORDER.map((capability) => + formatCapabilityGuidance(capability, policy.capabilities[capability]) + ); + + return [ + "## Autonomy", + "", + "Universal harness limitation: `AGENTS.md` + `.mcp.json` provide documentation and server registration only; there is no enforceable harness-level permission schema here.", + "", + "Treat this autonomy profile as documentation-only guidance for built-in/basic operations.", + "", + `Profile: \`${policy.profile}\``, + "", + "Built-in/basic capability guidance:", + ...capabilityLines, + "", + "MCP permissions are not re-modeled by autonomy here; any MCP approvals must come from provisioning-aware consuming harnesses rather than the Universal writer." + ].join("\n"); +} + export const universalWriter: HarnessWriter = { id: "universal", label: "Universal (AGENTS.md + .mcp.json)", description: - "Cross-tool standard — AGENTS.md + .mcp.json (works with any agent)", + "Cross-tool standard — AGENTS.md + .mcp.json (portable instructions and MCP registration, not enforceable permissions)", async install(config: LogicalConfig, projectRoot: string) { - if (config.instructions.length > 0) { + const autonomyGuidance = renderAutonomyGuidance(config); + const instructionSections = [...config.instructions]; + if (autonomyGuidance) { + instructionSections.push(autonomyGuidance); + } + + if (instructionSections.length > 0) { const lines = [ "# AGENTS", "", - ...config.instructions.flatMap((i) => [i, ""]) + ...instructionSections.flatMap((instruction) => [instruction, ""]) ]; await writeFile( join(projectRoot, "AGENTS.md"), diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts index 3e38806..ec3b7a0 100644 --- a/packages/harnesses/src/writers/windsurf.spec.ts +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -21,14 +21,15 @@ describe("windsurfWriter", () => { expect(windsurfWriter.label).toBe("Windsurf"); }); - it("writes .windsurf/mcp.json with MCP servers", async () => { + it("writes .windsurf/mcp.json with forwarded MCP approvals", async () => { const config: LogicalConfig = { mcp_servers: [ { ref: "workflows", command: "npx", args: ["-y", "@codemcp/workflows"], - env: { API_KEY: "test" } + env: { API_KEY: "test" }, + allowedTools: ["whats_next", "proceed_to_phase"] } ], instructions: [], @@ -47,10 +48,71 @@ describe("windsurfWriter", () => { command: "npx", args: ["-y", "@codemcp/workflows"], env: { API_KEY: "test" }, - alwaysAllow: ["*"] + alwaysAllow: ["whats_next", "proceed_to_phase"] }); }); + it("records autonomy as advisory guidance because Windsurf has no verified committed built-in permission schema", async () => { + const rigidRoot = join(dir, "rigid"); + const sensibleRoot = join(dir, "sensible"); + const maxRoot = join(dir, "max"); + + const rigidConfig: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [], + permission_policy: autonomyPolicy("rigid") + }; + + const sensibleConfig: LogicalConfig = { + ...rigidConfig, + permission_policy: autonomyPolicy("sensible-defaults") + }; + + const maxConfig: LogicalConfig = { + ...rigidConfig, + permission_policy: autonomyPolicy("max-autonomy") + }; + + await windsurfWriter.install(rigidConfig, rigidRoot); + await windsurfWriter.install(sensibleConfig, sensibleRoot); + await windsurfWriter.install(maxConfig, maxRoot); + + const rigidRules = await readFile( + join(rigidRoot, ".windsurfrules"), + "utf-8" + ); + const sensibleRules = await readFile( + join(sensibleRoot, ".windsurfrules"), + "utf-8" + ); + const maxRules = await readFile(join(maxRoot, ".windsurfrules"), "utf-8"); + + expect(rigidRules).toContain("Windsurf limitation:"); + expect(rigidRules).toContain("advisory only"); + expect(rigidRules).toContain( + "Ask before: read files, edit and write files, search and list files, safe local shell commands, unsafe local shell commands, web and network access, task or agent delegation." + ); + + expect(sensibleRules).toContain("Windsurf limitation:"); + expect(sensibleRules).toContain( + "May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, task or agent delegation." + ); + expect(sensibleRules).toContain( + "Ask before: unsafe local shell commands, web and network access." + ); + + expect(maxRules).toContain("Windsurf limitation:"); + expect(maxRules).toContain( + "May proceed without extra approval: read files, edit and write files, search and list files, safe local shell commands, unsafe local shell commands, task or agent delegation." + ); + expect(maxRules).toContain("Ask before: web and network access."); + }); + it("writes .windsurfrules with instructions", async () => { const config: LogicalConfig = { mcp_servers: [], @@ -68,3 +130,49 @@ describe("windsurfWriter", () => { expect(content).toContain("Follow TDD."); }); }); + +function autonomyPolicy( + profile: "rigid" | "sensible-defaults" | "max-autonomy" +): LogicalConfig["permission_policy"] { + switch (profile) { + case "rigid": + return { + profile, + capabilities: { + read: "ask", + edit_write: "ask", + search_list: "ask", + bash_safe: "ask", + bash_unsafe: "ask", + web: "ask", + task_agent: "ask" + } + }; + case "sensible-defaults": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "ask", + web: "ask", + task_agent: "allow" + } + }; + case "max-autonomy": + return { + profile, + capabilities: { + read: "allow", + edit_write: "allow", + search_list: "allow", + bash_safe: "allow", + bash_unsafe: "allow", + web: "ask", + task_agent: "allow" + } + }; + } +} diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index 6353bc1..ce731a9 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { LogicalConfig } from "@codemcp/ade-core"; +import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; import { writeMcpServers, @@ -7,6 +7,7 @@ import { writeRulesFile, writeGitHooks } from "../util.js"; +import { hasPermissionPolicy } from "../permission-policy.js"; export const windsurfWriter: HarnessWriter = { id: "windsurf", @@ -19,9 +20,70 @@ export const windsurfWriter: HarnessWriter = { }); await writeRulesFile( - config.instructions, + getWindsurfRules(config), join(projectRoot, ".windsurfrules") ); await writeGitHooks(config.git_hooks, projectRoot); } }; + +function getWindsurfRules(config: LogicalConfig): string[] { + if (!hasPermissionPolicy(config)) { + return config.instructions; + } + + const { capabilities } = config.permission_policy!; + const allow = listCapabilities(capabilities, "allow"); + const ask = listCapabilities(capabilities, "ask"); + const deny = listCapabilities(capabilities, "deny"); + + const autonomyGuidance = [ + "Windsurf limitation: ADE could not verify a stable committed project-local permission schema for Windsurf built-in tools, so this autonomy policy is advisory only and should be applied conservatively.", + formatGuidance(allow, ask, deny) + ]; + + return [...autonomyGuidance, ...config.instructions]; +} + +function listCapabilities( + capabilities: NonNullable["capabilities"], + decision: "ask" | "allow" | "deny" +): string[] { + return (Object.entries(capabilities) as Array<[AutonomyCapability, string]>) + .filter(([, value]) => value === decision) + .map(([capability]) => CAPABILITY_LABELS[capability]); +} + +function formatGuidance( + allow: string[], + ask: string[], + deny: string[] +): string { + const lines = ["Autonomy guidance for Windsurf built-in capabilities:"]; + + if (allow.length > 0) { + lines.push(`- May proceed without extra approval: ${allow.join(", ")}.`); + } + + if (ask.length > 0) { + lines.push(`- Ask before: ${ask.join(", ")}.`); + } + + if (deny.length > 0) { + lines.push( + `- Do not use unless the user explicitly overrides: ${deny.join(", ")}.` + ); + } + + return lines.join("\n"); +} + +const CAPABILITY_LABELS: Record = { + read: "read files", + edit_write: "edit and write files", + search_list: "search and list files", + bash_safe: "safe local shell commands", + bash_unsafe: "unsafe local shell commands", + web: "web and network access", + task_agent: "task or agent delegation" +}; diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..7e5dfb3 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "skills": { + "adr-nygard": { + "source": "/Users/oliverjaegle/projects/privat/codemcp/ade/.ade/skills/adr-nygard", + "sourceType": "local", + "computedHash": "13cd33eb604e9e090057cce458469b2a1b609a6db3313a03df88d91776095b19" + }, + "commit": { + "source": "mrsimpson/skills-coding", + "sourceType": "github", + "computedHash": "fc628c7d577d2d9cf3cb0a917d3c5e2e35b460fdc62c353595b7472b6f1c6548" + }, + "tdd": { + "source": "mrsimpson/skills-coding", + "sourceType": "github", + "computedHash": "f8b4618b54124be85c06cf13099f797594322df3f94e339e7d2f38dd4974e1f1" + } + } +}